# Scrum4Me — Implementatie Backlog **Versie:** 0.1 — april 2026 **Volgt op:** Functionele Specificatie v0.2, Architectuur v0.1 --- ## MVP-definitie De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan doorlopen: een product aanmaken, een Product Backlog opbouwen met PBI's en stories, een Sprint plannen, taken aanmaken, en Claude Code de volgende story laten ophalen, implementeren en vastleggen — allemaal zonder hulp of handleiding. De app draait stabiel op Vercel en is volledig lokaal opzetbaar via één README. --- ## Milestone-overzicht | Milestone | Doel | Tasks | |---|---|---| | M0: Foundation | Project, database, auth, navigatieshell | ST-001 – ST-008 | | M1: Producten & Product Backlog | Producten, PBI's, gesplitst scherm | ST-101 – ST-110 | | M2: Stories & Drag-and-drop | Stories als blokken, dnd-kit, Zustand | ST-201 – ST-210 | | M3: Sprint Backlog & Sprint Planning | Sprint aanmaken, stories slepen, taken | ST-301 – ST-313 | | M3.5: Solo Paneel & Story Assignment | Story-claim, persoonlijk Kanban-bord per product | ST-350 – ST-360 | | M4: Claude Code REST API | Alle endpoints, tokenbeheer | ST-401 – ST-410 | | M5: Todo-lijst | Todo CRUD, promotie naar PBI/story; Data Table + detail-kaart | ST-501 – ST-506, ST-509 – ST-510 | | M6: Polish & Launch-ready | Foutafhandeling, toegankelijkheid, CI/CD, beveiliging | ST-601 – ST-612 | --- ## Backlog ### M0: Foundation - [x] **ST-001** Project scaffolding - `create-next-app` met TypeScript strict, Tailwind CSS, App Router; installeer shadcn/ui, Zustand, dnd-kit, iron-session, bcrypt, Zod; configureer path aliases (`@/`) - Done when: `npm run dev` start zonder fouten; `npm run lint` geeft geen errors; shadcn `Button` rendert op een testpagina - [x] **ST-002** Prisma v7 setup + `prisma.config.ts` - Installeer Prisma v7 + `@prisma/adapter-pg`; schrijf `prisma.config.ts` met `DATABASE_URL` via Zod-gevalideerde env; schrijf `lib/prisma.ts` singleton - Done when: `npx prisma db push` slaagt; Prisma Client importeerbaar in een testbestand zonder fouten - [x] **ST-003** Database schema migratie (volledige initiële migratie) - Schrijf het volledige `schema.prisma` op basis van het architectuurdocument: `User`, `UserRole`, `ApiToken`, `Product`, `Pbi`, `Story`, `StoryLog`, `Sprint`, `Task`, `Todo`; alle enums, indexes, cascade deletes - Done when: `npx prisma migrate dev --name init` slaagt; alle tabellen zichtbaar in DB-client; `npx prisma validate` geeft geen fouten - [x] **ST-004** Seed met testdata - Schrijf `prisma/seed.ts` op basis van het Product Backlog document (devplanner-product-backlog.md); seed één gebruiker, één product (Scrum4Me zelf), alle PBI's en stories als testdata; voeg demo-gebruiker toe - Done when: `npx prisma db seed` slaagt; DB bevat alle PBI's en stories uit het backlog-document; demo-gebruiker aanwezig - [x] **ST-005** Environment variabelen + `lib/env.ts` - Schrijf Zod-schema voor alle env vars (`DATABASE_URL`, `DIRECT_URL`, `SESSION_SECRET`, `NODE_ENV`); exporteer gevalideerd `env` object; schrijf `.env.example` met instructies - Done when: app gooit een begrijpelijke fout bij ontbrekende env var; `.env.example` volledig gedocumenteerd - [x] **ST-006** Authenticatie — registratie en inloggen - Schrijf `lib/auth.ts` (registreer met bcrypt hash, verifieer bij inloggen); schrijf `lib/session.ts` (iron-session config); implementeer `/register` en `/login` pagina's met Server Actions; sla `{ userId, isDemo }` op in sessiecookie - Done when: registreren → ingelogde sessie → redirect `/dashboard`; inloggen met verkeerde credentials geeft generieke foutmelding; sessie blijft actief na paginaverversing - [x] **ST-007** Route-beveiliging via `proxy.ts` - Schrijf `proxy.ts` die sessiecookie-aanwezigheid controleert; redirect naar `/login` bij alle `/dashboard`, `/products/*`, `/todos`, `/settings/*` routes zonder sessiecookie; authenticated users worden van `/login` en `/register` doorgestuurd naar `/dashboard`; volledige sessievalidatie gebeurt server-side in de app layout - Done when: directe navigatie naar `/dashboard` zonder sessie redirect naar `/login`; ingelogde gebruiker op `/login` redirect naar `/dashboard` - [x] **ST-008** Navigatieshell + dashboard-layout - Schrijf `app/(app)/layout.tsx` met navigatiebalk (logo, productenlink, todolink, instellingen, uitlogknop); implementeer uitlog Server Action; implementeer `/dashboard` als lege productenlijstpagina met "Maak je eerste product aan" lege staat; zet demo-badge zichtbaar als `isDemo === true` - Done when: volledige auth-flow (register → login → dashboard → logout → login) werkt end-to-end; demo-gebruiker ziet badge in navigatie --- ### M1: Producten & Product Backlog - [x] **ST-101** Product aanmaken - `/products/new` pagina met formulier (naam, beschrijving, repo URL, definition of done); `createProduct` Server Action met Zod-validatie; uniekheidscontrole op naam per gebruiker; redirect naar `/products/[id]` na aanmaken - Done when: product aangemaakt en zichtbaar op dashboard; dubbele naam geeft inline validatiefout; lege naam blokkeert submit - [x] **ST-102** Productenlijst op dashboard - Haal actieve producten op via Prisma Server Component; toon naam, beschrijving (ingekort 80 tekens), repo-link; lege staat met CTA; klikken opent Product Backlog - Done when: twee producten zichtbaar na aanmaken; gearchiveerd product niet zichtbaar in standaardlijst - [x] **ST-103** Product bewerken en archiveren - Bewerkformulier (naam, beschrijving, repo URL, DoD) via Server Action; archiveerknop met bevestigingsdialoog; hersteloptie voor gearchiveerde producten; "toon gearchiveerd"-filter op dashboard - Done when: naam bijwerken persisteert; archiveren verbergt product; herstel maakt het weer zichtbaar - [x] **ST-104** Gesplitst scherm layout component (`SplitPane`) - Bouw herbruikbaar `` Client Component met versleepbare horizontale splitter; sla splitter-positie op in `localStorage` per sleutel; standaard 40/60 verhouding; minimale panelbreedte 200px; responsive fallback naar tabs op < 1024px - Done when: splitter versleepbaar en positie behouden na paginaverversing; tabs getoond op smal scherm - [x] **ST-105** Navigatiebar-component per paneel - Bouw herbruikbaar `` component met slots voor knoppen (aanmaken, filter, verwijderen); consistent design voor linker- en rechterpaneel - Done when: navigatiebar herbruikt in minimaal twee gesplitste schermen zonder duplicatie - [x] **ST-106** PBI aanmaken en weergeven - Linkerpaneel van `/products/[id]`: haal PBI's op gegroepeerd op prioriteit en sort_order; "PBI aanmaken" knop opent inline formulier (titel, prioriteit); `createPbi` Server Action; nieuw PBI verschijnt onderaan de juiste prioriteitsgroep - Done when: PBI aangemaakt en zichtbaar in juiste prioriteitsgroep; lege staat toont prompt - [x] **ST-107** PBI prioriteitsgroepen met visuele scheiding - Render PBI's gegroepeerd per prioriteit (1–4) met gelabelde scheidingslijn per groep (bijv. "Kritiek", "Hoog"); lege groepen zijn niet zichtbaar; prioriteitsbadge per PBI - Done when: vier prioriteitsgroepen correct gerenderd met labels; PBI met prioriteit 1 staat boven prioriteit 4 - [x] **ST-108** PBI bewerken en verwijderen - Inline bewerkingsmodus via dubbelklik of contextmenu (titel, omschrijving, prioriteit); `updatePbi` Server Action; verwijderen met bevestigingsdialoog inclusief waarschuwing cascade; `deletePbi` Server Action - Done when: titelbewering opgeslagen zonder paginaverversing; verwijderen cascade-verwijdert stories (verifieerbaar in DB) - [x] **ST-109** PBI selecteren → stories laden - Klikken op PBI in linkerpaneel toont bijbehorende stories rechts via `useSelectionStore`; geselecteerd PBI visueel gemarkeerd; lege staat rechts als geen stories - Done when: klikken op PBI A toont stories van A rechts; klikken op PBI B schakelt direct over - [x] **ST-110** PBI filter - Filterknop in linkerpaneel navigatiebar; dropdown voor prioriteit (1–4, alle); filter werkt realtime op gerenderde lijst; actief filter zichtbaar als badge; wissen via ×-knop - Done when: filter op prioriteit 1 verbergt alle andere PBI's; wissen herstelt volledige lijst --- ### M2: Stories & Drag-and-drop - [x] **ST-201** `usePlannerStore` Zustand-store - Schrijf `stores/planner-store.ts` met `pbiOrder`, `storyOrder`, `taskOrder`; `init*`, `reorder*`, `rollback*` actions; TypeScript strict types - Done when: store importeerbaar in een Client Component; `initPbis` vult order; `reorderPbis` muteert order; `rollbackPbis` herstelt vorige staat - [x] **ST-202** `useSelectionStore` Zustand-store - Schrijf `stores/selection-store.ts` met `selectedPbiId`, `selectedStoryId`, setters en `clearSelection` - Done when: selectie in linkerpaneel via store zichtbaar in rechterpaneel zonder prop drilling - [x] **ST-203** dnd-kit setup + PBI drag-and-drop - Installeer dnd-kit; wrap linkerpaneel in `DndContext` + `SortableContext`; implementeer `useSortable` per PBI-rij; `onDragEnd`: bereken nieuwe `sort_order` via float-gemiddelde; optimistisch updaten via `usePlannerStore`; `reorderPbisAction` Server Action; rollback bij fout - Done when: PBI versleepbaar binnen prioriteitsgroep; volgorde opgeslagen na loslaten; UI rollback bij gesimuleerde server-fout - [x] **ST-204** PBI drag-and-drop over prioriteitsgrens - Uitbreiding ST-203: slepen over een prioriteitsgrens wijzigt `priority` van het PBI; `sort_order` wordt onderaan de doelgroep geplaatst; `updatePbiPriority` Server Action - Done when: PBI naar prioriteit 2 slepen vanuit prioriteit 3 wijzigt zowel prioriteit als volgorde - [x] **ST-205** Story aanmaken en weergeven als blokken - Rechterpaneel van Product Backlog: haal stories op voor geselecteerd PBI; render als blokken (~10% schermbreedte, horizontaal); elk blok toont titel (ingekort), prioriteitsbadge, statusbadge; "Story aanmaken" knop; `createStory` Server Action - Done when: drie stories zichtbaar als blokken; nieuw blok verschijnt in juiste prioriteitsgroep - [x] **ST-206** Story prioriteitsgroepen met visuele scheiding - Groepeer story-blokken per prioriteit; gekleurde band of scheidingslijn per groep; blokken horizontaal gerangschikt per rij; nieuwe rij bij overloop - Done when: stories van vier prioriteiten correct gescheiden weergegeven - [x] **ST-207** Story drag-and-drop (horizontaal, binnen en tussen groepen) - dnd-kit horizontale `SortableContext` per prioriteitsgroep; `onDragEnd`: herrangschikking via float-gemiddelde in `storyOrder`; slepen naar andere groep wijzigt prioriteit; optimistisch via `usePlannerStore`; `reorderStoriesAction` Server Action; rollback bij fout - Done when: story versleepbaar binnen groep en naar andere groep; volgorde en prioriteit persistent na loslaten - [x] **ST-208** Story detail-modal / slide-over - Klikken op storyblok opent slide-over of modal met titel, omschrijving, acceptatiecriteria, statusbadge, activiteitenlog (leeg bij nieuwe story); bewerkformulier voor titel/omschrijving/acceptatiecriteria; `updateStory` Server Action - Done when: klikken op blok opent detail; bewerken persisteert; sluiten keert terug naar backlog - [x] **ST-209** Story verwijderen - Verwijderknop in story-detail of contextmenu; bevestigingsdialoog met waarschuwing cascade (taken); `deleteStory` Server Action; blok verdwijnt optimistisch uit het rechterpaneel - Done when: story verwijderd incl. cascade-taken (verifieerbaar in DB); blok direct verdwenen uit UI - [x] **ST-210** Story filter in rechterpaneel - Filterknop in rechterpaneel navigatiebar; filter op status (OPEN, IN_SPRINT, DONE) en prioriteit; realtime; actief filter als badge; wissbaar - Done when: filter op OPEN verbergt IN_SPRINT stories --- ### M3: Sprint Backlog & Sprint Planning - [x] **ST-301** `useSprintStore` Zustand-store - Schrijf `stores/sprint-store.ts`; `initSprint`, `addStoryToSprint`, `removeStoryFromSprint`, `reorderSprintStories`, `rollbackSprint` - Done when: store beheert sprint-story-volgorde onafhankelijk van planner-store - [x] **ST-302** Sprint aanmaken - "Sprint starten" knop op productpagina (zichtbaar als geen actieve Sprint); modal met Sprint Goal invoerveld; `createSprint` Server Action; max. 1 actieve Sprint per product afgedwongen in service-laag - Done when: Sprint aangemaakt met Goal; tweede sprint aanmaken terwijl eerste actief is geeft foutmelding - [x] **ST-303** Sprint Backlog scherm — layout - `/products/[id]/sprint` pagina; `SplitPane` met Sprint Backlog links (stories in Sprint op volgorde) en rechts de Product Backlog stories gegroepeerd per PBI (inklapbaar); Sprint Goal zichtbaar bovenaan; lege staat links met instructie - Done when: pagina rendert correct; Sprint Goal zichtbaar; beide panelen tonen juiste data - [x] **ST-304** Story vanuit Product Backlog naar Sprint slepen - dnd-kit drag vanuit rechterpaneel naar linkerpaneel; `onDragEnd`: `addStoryToSprint` in store; story krijgt badge "In Sprint" in Product Backlog; `addStoryToSprintAction` Server Action (zet `sprint_id` + status `IN_SPRINT`); rollback bij fout - Done when: story gesleept naar Sprint verschijnt links en toont "In Sprint" badge rechts; persistent na herlaad - [x] **ST-305** Sprint Backlog story volgorde aanpassen - dnd-kit verticale `SortableContext` in linkerpaneel; herrangschikking via float-gemiddelde in `useSprintStore`; `reorderSprintStoriesAction` Server Action - Done when: volgorde in Sprint Backlog persistent na loslaten en na paginaverversing - [x] **ST-306** Story uit Sprint verwijderen - Verwijderknop per story in Sprint Backlog; `removeStoryFromSprintAction` Server Action (wist `sprint_id`, zet status terug op `OPEN`); story verdwijnt links en badge verdwijnt rechts - Done when: verwijderen persistent; story beschikbaar in Product Backlog rechterpaneel - [x] **ST-307** Sprint Planning scherm — layout - `/products/[id]/sprint/planning` pagina; `SplitPane` met Sprint Backlog stories links (op volgorde) en taken van geselecteerde story rechts; Sprint Goal zichtbaar; lege staat rechts als geen story geselecteerd - Done when: pagina rendert; story selecteren links toont taken rechts - [x] **ST-308** Taak aanmaken - "Taak aanmaken" knop in rechterpaneel navigatiebar; inline formulier (titel, omschrijving, prioriteit); `createTask` Server Action; voortgangsindicator per story (bijv. "0/0 Done") - Done when: taak aangemaakt en zichtbaar in takenlijst; voortgangsindicator toont "0/1 Done" - [x] **ST-309** Taak drag-and-drop (verticaal) - dnd-kit verticale `SortableContext` in rechterpaneel; herrangschikking via float-gemiddelde in `usePlannerStore.taskOrder`; `reorderTasksAction` Server Action - Done when: taken versleepbaar; volgorde persistent na loslaten - [x] **ST-310** Taakstatus bijhouden - Status-toggle per taak (TO_DO → IN_PROGRESS → DONE) via klikbare badge of dropdown; `updateTaskStatus` Server Action; voortgangsindicator op story updatet optimistisch - Done when: taak op DONE zetten verhoogt teller in voortgangsindicator; persistent na herlaad - [x] **ST-311** Taak bewerken en verwijderen - Inline bewerken van titel, omschrijving en prioriteit; `updateTask` Server Action; verwijderen met bevestiging; `deleteTask` Server Action - Done when: titelwijziging persisteert; verwijderde taak verdwijnt uit lijst - [x] **ST-312** Sprint afronden - "Sprint afronden" knop op Sprint-pagina; dialoog toont per story de status en vraagt: "Markeer als Done of terug naar Backlog?"; `completeSprint` Server Action zet Sprint op COMPLETED, verwerkt keuzes per story - Done when: Sprint afgerond; stories correct verplaatst naar DONE of OPEN; nieuwe Sprint aanmaakbaar - [x] **ST-313** Sprint Board — drie-panelen layout (vervangt ST-303 + ST-307) - **Doel:** `/products/[id]/sprint` wordt één scherm met drie panelen van links naar rechts: Product Backlog · Sprint Backlog · Taken. De losse `/sprint/planning` route wordt verwijderd (redirect → `/sprint`). - **Panelen:** - *Links — Product Backlog:* PBIs met stories gegroepeerd en inklapbaar; stories die al in sprint zijn grijs/disabled; klikken of slepen voegt story toe aan Sprint Backlog (midden) - *Midden — Sprint Backlog:* stories in sprint op volgorde; klikken selecteert story → taken laden rechts; versleepbaar om te sorteren; trash-knop verwijdert uit sprint - *Rechts — Taken:* `TaskList` voor de geselecteerde story; lege staat "Selecteer een story" als niets geselecteerd; "+ Taak" knop zoals huidig - **Layout:** `TriplePane` component — drie verticale panelen met twee versleepbare scheidingslijnen; opslaan in `localStorage` per product (key: `sprint-triple-${productId}`) - **DnD:** één `DndContext` omhult alle drie panelen; drag van links naar midden werkt via `DragOverlay`; reorder binnen midden via `SortableContext`; taken-reorder in eigen geneste `DndContext` - **State:** `SprintBoardClient` beheert sprint stories, product backlog data, `selectedStoryId`, en taken per story (vanuit server props); `useSelectionStore.selectedStoryId` voor story-selectie - **Navigatie:** "Sprint Planning →" link onderaan Sprint Backlog pagina verwijderd; `SprintHeader` blijft bovenaan met "Sprint afronden" - **Route cleanup:** `/sprint/planning/page.tsx` vervangt door redirect naar `/products/[id]/sprint`; `PlanningLeft`, `PlanningRightClient` components verwijderen - Done when: één `/sprint` pagina toont alle drie panelen; story slepen van links naar midden werkt; story selecteren toont taken rechts; taak aanmaken en sorteren werkt; pagina hervat na herlaad met juiste data; `/sprint/planning` redirect werkt --- ### M3.5: Solo Paneel & Story Assignment > **Doel:** een persoonlijk Kanban-bord per product dat de taken toont van stories die geclaimd zijn door de ingelogde developer. Story-level assignment volgt het Scrum self-organizing principe: developers claimen vrijwillig stories (pull, niet push). Volledige technische specificatie in `solo-paneel-spec.md`. - [x] **ST-350** Story.assignee_id schema-migratie + auth-helpers - **Schema:** voeg `assignee_id String?` + `assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)` toe aan `Story`; voeg `assigned_stories Story[] @relation("StoryAssignee")` toe aan `User`; voeg index `@@index([sprint_id, assignee_id])` toe; migratie via `prisma migrate dev --name add_story_assignee` - **Auth-helpers:** schrijf `lib/auth.ts` met `getSession`, `requireUser`, `requireWriter`, `requireProductAccess`, `requireProductWriter` — laatste twee doen membership-check via owner (`Product.user_id`) OF lid (`ProductMember`); demo-check op basis van `session.isDemo` (uit ST-006); throwt *"Niet beschikbaar in demo-modus"* bij demo-write-poging - Done when: migratie slaagt; `requireProductWriter` blokkeert demo-user; `requireProductAccess` accepteert zowel owner als member - [x] **ST-351** `` herbruikbare component - Wrapper rond shadcn `Avatar`; props: `userId`, `username`, `size` ('xs' | 'sm' | 'md' | 'lg'), `className`; `` met fallback naar initialen (eerste 2 tekens username) op `bg-primary-container`; vier groottes via Tailwind classes - Done when: avatar rendert in 4 sizes; bij ontbrekende avatar-data (404) fallback naar initialen zichtbaar; component bruikbaar in story-kaart, sprint board, instellingen - [x] **ST-352** Story-claim Server Actions - Vier acties in `actions/stories.ts`: `claimStoryAction` (zet `assignee_id = currentUserId`), `unclaimStoryAction` (null), `reassignStoryAction` (valideert dat target user lid van product is), `claimAllUnassignedInActiveSprintAction` (bulk via `updateMany` voor ongeclaimde stories in actieve sprint); allemaal Zod-gevalideerd, achter `requireProductWriter`, met `revalidatePath` voor `/sprint` én `/solo`; tenant-guard via `where: { id, product_id }` - Done when: alle vier acties testbaar via testbestand; demo-user krijgt foutmelding; reassignment naar niet-lid faalt met foutmelding; bulk claimt alleen ongeclaimde - [x] **ST-353** Sprint Board: assignee-chip + dropdown menu op story-kaart - Op story-kaart in middenpaneel van ST-313 Sprint Board: assignee-chip onderaan met `` + username (of muted "Niet geclaimd" badge als `assignee_id === null`); shadcn `DropdownMenu` (3-dots rechtsboven) met items "Pak op" / "Geef terug aan team" / "Wijs toe aan ▶" (submenu met members); items conditioneel zichtbaar op basis van huidige assignee; demo-modus: dropdown disabled met tooltip "Niet beschikbaar in demo-modus" - Done when: chip toont juiste state; dropdown roept juiste acties aan; revalidatie ververst kaart; toast "Story geclaimd" / "Toegewezen aan X" bij succes; demo-user ziet disabled-tooltip - [x] **ST-354** Sprint Board: bulk-claim knop "Claim alle ongeclaimde" - Knop bovenaan Sprint Backlog paneel met telling: "Claim alle ongeclaimde stories (N)"; disabled als N=0 of `isDemo`; klik roept `claimAllUnassignedInActiveSprintAction` aan; Sonner success-toast "{count} stories geclaimd"; pending state via `useTransition` - Done when: telling correct; claimen werkt; knop disabled bij 0 ongeclaimd of demo; toast verschijnt na succes - [x] **ST-355** Solo route — `/solo` redirect + `/products/[id]/solo` pagina + cookie - **Cookie-helper:** schrijf `lib/cookies.ts` met `setLastProductCookie(productId)` (HTTP-only, sameSite lax, 30 dagen) - **`/solo` page.tsx:** Server Component; leest cookie `lastProductId`; valideert toegang en redirect naar `/products/[id]/solo`, of toont `` als geen cookie of cookie ongeldig - **`/products/[id]/solo` page.tsx:** Server Component; haalt active sprint op (404 → empty state ``); haalt taken op via `Task.findMany` met `where: { sprint_id, story: { assignee_id: session.userId } }` + count ongeclaimde stories parallel; geeft data door aan ``; zet `lastProductId` cookie bij elk bezoek - **Empty state:** `` met titel, uitleg, link naar productpagina - **``:** lijst van toegankelijke producten, klikken redirect naar `/products/[id]/solo` - Done when: `/solo` zonder cookie toont picker; met geldige cookie redirect; pagina toont juiste taken; geen actieve sprint toont empty state; cookie persisteert tussen sessies - [x] **ST-356** Solo Kanban-bord met DnD en Zustand - **Store `stores/solo-store.ts`:** `tasks`, `initTasks`, `optimisticMove(taskId, toStatus)` (returnt vorige status), `rollback(taskId, prevStatus)`, `updatePlan(taskId, plan)`; volgt patroon van `usePlannerStore` (ST-201) - **`` Client Component:** root met `DndContext` (overslaan als `isDemo`), `PointerSensor` met `activationConstraint: { distance: 5 }`, `closestCorners` collision detection; header met productnaam, sprint goal, knop "Toon openstaande stories (N)"; grid met drie kolommen - **``:** drop target per status (`TO_DO` / `IN_PROGRESS` / `DONE`); header met statuskleur via MD3 tokens (`bg-status-todo/15` etc.); count en lege staat - **``:** hergebruik bestaande task-card (ST-310); draggable; toont prioriteit-indicator, taaktitel, story-titel; klik opent detail-dialoog (ST-357); demo: niet draggable - **`onDragEnd` flow:** optimistische update via `optimisticMove`, dan `updateTaskStatusAction` aanroepen, op error rollback + Sonner error-toast "Status bijwerken mislukt — taak teruggeplaatst"; geen success-toast (te frequent) - Done when: kaart sleepbaar tussen kolommen; status persisteert; gesimuleerde server-fout rollbackt UI; demo-user kan niet slepen - [x] **ST-357** Task detail-dialoog + `updateTaskPlanAction` - **`updateTaskPlanAction`** in `actions/tasks.ts`: Zod-schema `{ taskId, productId, implementationPlan }`; `requireProductWriter`; tenant-guard via `where: { id: taskId, story: { product_id: productId } }`; `revalidatePath` - **``** shadcn `Dialog`: header met taaktitel + statusbadge (MD3 tokens); sectie *Beschrijving* (read-only, volg bestaand patroon); sectie *Implementatieplan* met `