From 89e5164a28142205f9fbf564982c3f16495956d9 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 16:55:55 +0200 Subject: [PATCH] feat(ST-358): add unassigned stories sheet with claim-on-click - UnassignedStoriesSheet: slide-in sheet listing unassigned sprint stories - ClaimStoryRow: form action + ClaimButton with useFormStatus pending state - Successful claim removes story from local list and shows success toast - Empty state: "Geen ongeclaimde stories. Lekker bezig!" - Demo: DemoTooltip wraps Pak op button, claim button disabled - Page now fetches stories with _count.tasks instead of just count - claimStoryAction also revalidates /products/[id]/solo path Co-Authored-By: Claude Sonnet 4.6 --- actions/stories.ts | 1 + app/(app)/products/[id]/solo/page.tsx | 19 +++- components/solo/unassigned-stories-sheet.tsx | 111 +++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 components/solo/unassigned-stories-sheet.tsx diff --git a/actions/stories.ts b/actions/stories.ts index 1495f4d..29119ae 100644 --- a/actions/stories.ts +++ b/actions/stories.ts @@ -223,6 +223,7 @@ export async function claimStoryAction(storyId: string, productId: string) { await prisma.story.update({ where: { id: storyId }, data: { assignee_id: session.userId } }) revalidatePath(`/products/${productId}/sprint`) + revalidatePath(`/products/${productId}/solo`) revalidatePath('/solo') return { success: true } } diff --git a/app/(app)/products/[id]/solo/page.tsx b/app/(app)/products/[id]/solo/page.tsx index e88773c..e402301 100644 --- a/app/(app)/products/[id]/solo/page.tsx +++ b/app/(app)/products/[id]/solo/page.tsx @@ -6,6 +6,7 @@ import { prisma } from '@/lib/prisma' import { SoloBoard } from '@/components/solo/solo-board' import { NoActiveSprint } from '@/components/solo/no-active-sprint' import type { SoloTask } from '@/components/solo/solo-board' +import type { UnassignedStory } from '@/components/solo/unassigned-stories-sheet' interface Props { params: Promise<{ id: string }> @@ -33,7 +34,7 @@ export default async function SoloProductPage({ params }: Props) { ) } - const [rawTasks, unassignedCount] = await Promise.all([ + const [rawTasks, rawUnassigned] = await Promise.all([ prisma.task.findMany({ where: { story: { @@ -50,8 +51,14 @@ export default async function SoloProductPage({ params }: Props) { { sort_order: 'asc' }, ], }), - prisma.story.count({ + prisma.story.findMany({ where: { sprint_id: sprint.id, assignee_id: null }, + select: { + id: true, + title: true, + _count: { select: { tasks: true } }, + }, + orderBy: { sort_order: 'asc' }, }), ]) @@ -67,13 +74,19 @@ export default async function SoloProductPage({ params }: Props) { story_title: t.story.title, })) + const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({ + id: s.id, + title: s.title, + task_count: s._count.tasks, + })) + return ( diff --git a/components/solo/unassigned-stories-sheet.tsx b/components/solo/unassigned-stories-sheet.tsx new file mode 100644 index 0000000..8d35690 --- /dev/null +++ b/components/solo/unassigned-stories-sheet.tsx @@ -0,0 +1,111 @@ +'use client' + +import { useState } from 'react' +import { useFormStatus } from 'react-dom' +import { toast } from 'sonner' +import { + Sheet, SheetContent, SheetHeader, SheetTitle, +} from '@/components/ui/sheet' +import { DemoTooltip } from '@/components/shared/demo-tooltip' +import { claimStoryAction } from '@/actions/stories' + +export interface UnassignedStory { + id: string + title: string + task_count: number +} + +interface UnassignedStoriesSheetProps { + stories: UnassignedStory[] + productId: string + isDemo: boolean + open: boolean + onOpenChange: (open: boolean) => void + onClaim: (storyId: string) => void +} + +function ClaimButton({ isDemo }: { isDemo: boolean }) { + const { pending } = useFormStatus() + return ( + + + + ) +} + +function ClaimStoryRow({ + story, productId, isDemo, onClaim, +}: { story: UnassignedStory; productId: string; isDemo: boolean; onClaim: (id: string) => void }) { + async function formAction() { + const result = await claimStoryAction(story.id, productId) + if (result && 'error' in result) { + toast.error(result.error) + } else { + onClaim(story.id) + toast.success(`"${story.title}" geclaimd`) + } + } + + return ( +
+
+

{story.title}

+

+ {story.task_count} {story.task_count === 1 ? 'taak' : 'taken'} +

+
+
+ + +
+ ) +} + +export function UnassignedStoriesSheet({ + stories: initialStories, productId, isDemo, open, onOpenChange, onClaim, +}: UnassignedStoriesSheetProps) { + const [stories, setStories] = useState(initialStories) + + function handleClaim(storyId: string) { + setStories(prev => prev.filter(s => s.id !== storyId)) + onClaim(storyId) + } + + return ( + + + + Openstaande stories + + +
+ {stories.length === 0 ? ( +
+

+ Geen ongeclaimde stories. Lekker bezig! +

+
+ ) : ( +
+ {stories.map(story => ( + + ))} +
+ )} +
+
+
+ ) +}