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:
parent
413e8ad861
commit
c74d1337ce
4 changed files with 94 additions and 106 deletions
|
|
@ -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] && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue