Scrum4Me/components/backlog/pbi-list.tsx
janpeter visser ffda65490f feat: ST-101-ST-110 M1 producten, PBI backlog, iconen en PWA manifest
- Product aanmaken/bewerken/archiveren/herstellen (ST-101, ST-103)
- SplitPane component met versleepbare splitter en localStorage (ST-104)
- PanelNavBar herbruikbaar paneelheader component (ST-105)
- PbiList met prioriteitsgroepen, inline aanmaken, filter en verwijderen (ST-106-ST-110)
- StoryPanel placeholder rechter paneel met selectie via Zustand (ST-109)
- App iconen geinstalleerd: favicon, apple-icon, PWA manifest (192/512px)
- AppIcon SVG component in navigatiebar
- Root layout metadata bijgewerkt naar Nederlands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:33:47 +02:00

237 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useTransition } from 'react'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { PanelNavBar } from '@/components/shared/panel-nav-bar'
import { useSelectionStore } from '@/stores/selection-store'
import { createPbiAction, deletePbiAction } from '@/actions/pbis'
import { cn } from '@/lib/utils'
const PRIORITY_LABELS: Record<number, string> = {
1: 'Kritiek',
2: 'Hoog',
3: 'Gemiddeld',
4: 'Laag',
}
const PRIORITY_COLORS: Record<number, string> = {
1: 'bg-priority-critical/15 text-priority-critical border-priority-critical/30',
2: 'bg-priority-high/15 text-priority-high border-priority-high/30',
3: 'bg-priority-medium/15 text-priority-medium border-priority-medium/30',
4: 'bg-priority-low/15 text-priority-low border-priority-low/30',
}
interface Pbi {
id: string
title: string
priority: number
}
interface PbiListProps {
productId: string
pbis: Pbi[]
isDemo: boolean
}
function CreatePbiForm({ productId, priority, onDone }: { productId: string; priority: number; onDone: () => void }) {
const [state, formAction] = useActionState(
async (_prev: unknown, fd: FormData) => {
const result = await createPbiAction(_prev, fd)
if (result?.success) onDone()
return result
},
undefined
)
const error = state?.error
return (
<form action={formAction} className="flex gap-2 p-2">
<input type="hidden" name="productId" value={productId} />
<input type="hidden" name="priority" value={priority} />
<Input
name="title"
autoFocus
placeholder="PBI-titel…"
className="h-7 text-sm"
required
/>
<CreateSubmitButton />
<Button type="button" variant="ghost" size="sm" className="h-7" onClick={onDone}>
Annuleren
</Button>
{typeof error === 'string' && (
<p className="text-xs text-error self-center">{error}</p>
)}
</form>
)
}
function CreateSubmitButton() {
const { pending } = useFormStatus()
return (
<Button type="submit" size="sm" className="h-7" disabled={pending}>
{pending ? '…' : 'Toevoegen'}
</Button>
)
}
export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
const { selectedPbiId, selectPbi } = useSelectionStore()
const [filterPriority, setFilterPriority] = useState<number | null>(null)
const [creatingForPriority, setCreatingForPriority] = useState<number | null>(null)
const [, startTransition] = useTransition()
const filtered = filterPriority ? pbis.filter(p => p.priority === filterPriority) : pbis
const grouped = [1, 2, 3, 4].reduce<Record<number, Pbi[]>>((acc, p) => {
acc[p] = filtered.filter(pbi => pbi.priority === p)
return acc
}, {} as Record<number, Pbi[]>)
const visiblePriorities = [1, 2, 3, 4].filter(
p => grouped[p].length > 0 || creatingForPriority === p
)
function handleDelete(id: string) {
startTransition(async () => {
await deletePbiAction(id)
if (selectedPbiId === id) selectPbi(null)
})
}
return (
<div className="flex flex-col h-full">
<PanelNavBar
title="Product Backlog"
actions={
<>
{filterPriority !== null && (
<button
onClick={() => setFilterPriority(null)}
className="flex items-center gap-1 text-xs text-primary hover:underline"
>
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
{PRIORITY_LABELS[filterPriority]}
</Badge>
<span>×</span>
</button>
)}
<Select
value={filterPriority?.toString() ?? 'all'}
onValueChange={(v) => setFilterPriority(!v || v === 'all' ? null : parseInt(v))}
>
<SelectTrigger className="h-7 w-28 text-xs">
<SelectValue placeholder="Filter" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Alle</SelectItem>
<SelectItem value="1">Kritiek</SelectItem>
<SelectItem value="2">Hoog</SelectItem>
<SelectItem value="3">Gemiddeld</SelectItem>
<SelectItem value="4">Laag</SelectItem>
</SelectContent>
</Select>
{!isDemo && (
<Button
size="sm"
className="h-7 text-xs"
onClick={() => setCreatingForPriority(creatingForPriority ? null : 2)}
>
+ PBI
</Button>
)}
</>
}
/>
<div className="flex-1 overflow-y-auto">
{pbis.length === 0 && creatingForPriority === null ? (
<div className="p-8 text-center text-muted-foreground text-sm space-y-3">
<p>Nog geen PBI&apos;s aangemaakt.</p>
{!isDemo && (
<Button size="sm" variant="outline" onClick={() => setCreatingForPriority(2)}>
Maak je eerste PBI aan
</Button>
)}
</div>
) : (
<div className="py-2">
{visiblePriorities.map(priority => (
<div key={priority}>
{/* Priority group header */}
<div className="flex items-center gap-2 px-4 py-1.5">
<span className={cn('text-xs font-semibold px-2 py-0.5 rounded-full border', PRIORITY_COLORS[priority])}>
{PRIORITY_LABELS[priority]}
</span>
<div className="flex-1 h-px bg-border" />
{!isDemo && (
<button
onClick={() => setCreatingForPriority(priority)}
className="text-xs text-muted-foreground hover:text-foreground"
>
+
</button>
)}
</div>
{/* PBI items */}
{grouped[priority].map(pbi => (
<div
key={pbi.id}
onClick={() => selectPbi(pbi.id)}
className={cn(
'group flex items-center justify-between px-4 py-2 cursor-pointer transition-colors hover:bg-surface-container',
selectedPbiId === pbi.id && 'bg-primary-container text-primary-container-foreground'
)}
>
<span className="text-sm truncate flex-1">{pbi.title}</span>
{!isDemo && (
<button
onClick={(e) => { e.stopPropagation(); handleDelete(pbi.id) }}
className="opacity-0 group-hover:opacity-100 ml-2 text-muted-foreground hover:text-error text-xs shrink-0"
aria-label="Verwijder PBI"
>
×
</button>
)}
</div>
))}
{/* Inline create form for this priority */}
{creatingForPriority === priority && (
<CreatePbiForm
productId={productId}
priority={priority}
onDone={() => setCreatingForPriority(null)}
/>
)}
</div>
))}
{/* If creating for a priority that has no items yet and isn't in visiblePriorities */}
{creatingForPriority !== null && !visiblePriorities.includes(creatingForPriority) && (
<div>
<div className="flex items-center gap-2 px-4 py-1.5">
<span className={cn('text-xs font-semibold px-2 py-0.5 rounded-full border', PRIORITY_COLORS[creatingForPriority])}>
{PRIORITY_LABELS[creatingForPriority]}
</span>
<div className="flex-1 h-px bg-border" />
</div>
<CreatePbiForm
productId={productId}
priority={creatingForPriority}
onDone={() => setCreatingForPriority(null)}
/>
</div>
)}
</div>
)}
</div>
</div>
)
}