feat: ST-201-ST-210 M2 stories, drag-and-drop en Zustand stores

- usePlannerStore met pbiOrder/storyOrder init/reorder/rollback (ST-201)
- useSelectionStore uitgebreid met selectedStoryId en clearSelection (ST-202)
- PBI drag-and-drop binnen prioriteitsgroep via dnd-kit (ST-203)
- PBI slepen over prioriteitsgrens wijzigt priority (ST-204)
- Stories als blokken met prioriteit- en statusbadge (ST-205/ST-206)
- Story drag-and-drop horizontaal binnen en tussen groepen (ST-207)
- Story detail slide-over met bewerkformulier (ST-208)
- Story verwijderen met bevestigingsstap (ST-209)
- Filter op status en prioriteit in rechterpaneel (ST-210)
- Fix: infinite loop in useEffect door stabiele string dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-24 11:46:18 +02:00
parent ffda65490f
commit 4dd62c199c
25 changed files with 1794 additions and 100 deletions

View file

@ -6,6 +6,8 @@ import { prisma } from '@/lib/prisma'
import { SplitPane } from '@/components/split-pane/split-pane'
import { PbiList } from '@/components/backlog/pbi-list'
import { StoryPanel } from '@/components/backlog/story-panel'
import type { Story } from '@/components/backlog/story-panel'
import Link from 'next/link'
interface Props {
params: Promise<{ id: string }>
@ -28,16 +30,26 @@ export default async function ProductBacklogPage({ params }: Props) {
const stories = await prisma.story.findMany({
where: { product_id: id },
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
select: { id: true, title: true, status: true, pbi_id: true },
select: {
id: true,
title: true,
description: true,
acceptance_criteria: true,
priority: true,
status: true,
pbi_id: true,
},
})
// Group stories by PBI id
const storiesByPbi: Record<string, typeof stories> = {}
const storiesByPbi: Record<string, Story[]> = {}
for (const story of stories) {
if (!storiesByPbi[story.pbi_id]) storiesByPbi[story.pbi_id] = []
storiesByPbi[story.pbi_id].push(story)
}
const isDemo = session.isDemo ?? false
return (
<div className="flex flex-col h-full">
{/* Product header */}
@ -48,12 +60,12 @@ export default async function ProductBacklogPage({ params }: Props) {
<p className="text-xs text-muted-foreground mt-0.5">{product.description}</p>
)}
</div>
<a
<Link
href={`/products/${id}/settings`}
className="text-xs text-muted-foreground hover:text-foreground"
>
Instellingen
</a>
</Link>
</div>
{/* Split pane */}
@ -64,13 +76,14 @@ export default async function ProductBacklogPage({ params }: Props) {
<PbiList
productId={id}
pbis={pbis.map(p => ({ id: p.id, title: p.title, priority: p.priority }))}
isDemo={session.isDemo ?? false}
isDemo={isDemo}
/>
}
right={
<StoryPanel
productId={id}
storiesByPbi={storiesByPbi}
isDemo={session.isDemo ?? false}
isDemo={isDemo}
/>
}
/>