feat(backlog): sorteer PBI's en stories op prio/code/datum, onthoud keuze in localStorage; vergroot sprint-afronden dialoog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-27 18:10:05 +02:00
parent 413e8ad861
commit c74d1337ce
4 changed files with 94 additions and 106 deletions

View file

@ -29,16 +29,10 @@ import { useSelectionStore } from '@/stores/selection-store'
import { usePlannerStore } from '@/stores/planner-store'
import { reorderStoriesAction } from '@/actions/stories'
import { StoryDialog, type StoryDialogState } from './story-dialog'
import { BacklogCard, PRIORITY_BORDER } from './backlog-card'
import { BacklogCard } from './backlog-card'
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',
}
type SortMode = 'priority' | 'code' | 'date'
const STATUS_COLORS: Record<string, string> = {
OPEN: 'bg-status-todo/15 text-status-todo border-status-todo/30',
IN_SPRINT: 'bg-status-in-progress/15 text-status-in-progress border-status-in-progress/30',
@ -59,6 +53,7 @@ export interface Story {
priority: number
status: string
pbi_id: string
created_at: Date
}
interface StoryPanelProps {
@ -111,10 +106,16 @@ export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps)
const { storyOrder, initStories, reorderStories, rollbackStories } = usePlannerStore()
const [filterStatus, setFilterStatus] = useState<string | null>(null)
const [filterPriority, setFilterPriority] = useState<number | null>(null)
const [sortMode, setSortMode] = useState<SortMode>(() => {
const saved = typeof window !== 'undefined' ? localStorage.getItem('scrum4me:story_sort') : null
return (saved === 'priority' || saved === 'code' || saved === 'date') ? saved : 'priority'
})
const [storyDialogState, setStoryDialogState] = useState<StoryDialogState | null>(null)
const [activeDragId, setActiveDragId] = useState<string | null>(null)
const [, startTransition] = useTransition()
useEffect(() => { localStorage.setItem('scrum4me:story_sort', sortMode) }, [sortMode])
const rawStories = selectedPbiId ? (storiesByPbi[selectedPbiId] ?? []) : []
// Sync into store — use stable string dep to avoid infinite loop
@ -130,16 +131,19 @@ export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps)
const order = (selectedPbiId ? storyOrder[selectedPbiId] : null) ?? rawStories.map(s => s.id)
const orderedStories = order.map(id => storyMap[id]).filter(Boolean)
const filtered = orderedStories
const base = orderedStories
.filter(s => !filterStatus || s.status === filterStatus)
.filter(s => !filterPriority || s.priority === filterPriority)
const grouped = [1, 2, 3, 4].reduce<Record<number, Story[]>>((acc, p) => {
acc[p] = filtered.filter(s => s.priority === p)
return acc
}, {} as Record<number, Story[]>)
const visiblePriorities = [1, 2, 3, 4].filter(p => grouped[p].length > 0)
const filtered = [...base].sort((a, b) => {
if (sortMode === 'code') {
return (a.code ?? '').localeCompare(b.code ?? '', 'nl', { numeric: true })
}
if (sortMode === 'date') {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
}
return a.priority !== b.priority ? a.priority - b.priority : 0
})
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
@ -195,6 +199,16 @@ export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps)
Filter wissen ×
</button>
)}
<Select value={sortMode} onValueChange={(v) => setSortMode(v as SortMode)}>
<SelectTrigger className="h-7 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="priority">Prioriteit</SelectItem>
<SelectItem value="code">Code</SelectItem>
<SelectItem value="date">Datum</SelectItem>
</SelectContent>
</Select>
<Select
value={filterStatus ?? 'all'}
onValueChange={(v) => setFilterStatus(!v || v === 'all' ? null : v)}
@ -244,42 +258,17 @@ export function StoryPanel({ productId, storiesByPbi, isDemo }: StoryPanelProps)
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="space-y-4">
{visiblePriorities.map(priority => (
<div key={priority}>
<div className="flex items-center gap-2 mb-2">
<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 && selectedPbiId && (
<button
onClick={() => setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: priority })}
className="text-xs text-muted-foreground hover:text-foreground"
>
+
</button>
)}
</div>
<SortableContext
items={grouped[priority].map(s => s.id)}
strategy={rectSortingStrategy}
>
<div className="grid grid-cols-3 gap-2">
{grouped[priority].map(story => (
<SortableStoryBlock
key={story.id}
story={story}
onClick={() => setStoryDialogState({ mode: 'edit', story, productId })}
/>
))}
</div>
</SortableContext>
</div>
))}
</div>
<SortableContext items={filtered.map(s => s.id)} strategy={rectSortingStrategy}>
<div className="grid grid-cols-3 gap-2">
{filtered.map(story => (
<SortableStoryBlock
key={story.id}
story={story}
onClick={() => setStoryDialogState({ mode: 'edit', story, productId })}
/>
))}
</div>
</SortableContext>
<DragOverlay>
{activeDragId && storyMap[activeDragId] && (