From cc6baeebc188d1ff4e09e93ed1b2bdb611273699 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 4 May 2026 09:10:05 +0200 Subject: [PATCH] feat(sprint): filter popover op Product-Backlog-kolom Spiegelt het filter-patroon van de Product Backlog-pagina (PbiList) naar de Sprint-Product-Backlog-kolom: prioriteit + status pills, actieve-filter chips, localStorage-persistentie. Bestaande collapse-/expand-/alleen-niet-klaar-knoppen blijven naast de popover staan. Co-Authored-By: Claude Opus 4.7 (1M context) --- components/sprint/sprint-backlog.tsx | 216 +++++++++++++++++++++++---- 1 file changed, 190 insertions(+), 26 deletions(-) diff --git a/components/sprint/sprint-backlog.tsx b/components/sprint/sprint-backlog.tsx index 3555912..5453896 100644 --- a/components/sprint/sprint-backlog.tsx +++ b/components/sprint/sprint-backlog.tsx @@ -1,12 +1,14 @@ 'use client' -import { useState, useTransition } from 'react' +import { useState, useTransition, useEffect } from 'react' import { Trash2, MoreHorizontal, ChevronsUp, ChevronsDown, ListFilter } from 'lucide-react' import { useDroppable, useDraggable } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { CodeBadge } from '@/components/shared/code-badge' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -17,6 +19,7 @@ import { UserAvatar } from '@/components/shared/user-avatar' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { PRIORITY_BORDER } from '@/components/backlog/backlog-card' +import { PRIORITY_COLORS } from '@/components/shared/priority-select' import { useSprintStore } from '@/stores/sprint-store' import { claimStoryAction, unclaimStoryAction, reassignStoryAction, claimAllUnassignedInActiveSprintAction } from '@/actions/stories' import { cn } from '@/lib/utils' @@ -304,6 +307,65 @@ export function SprintBacklogLeft({ // --- Right panel: Product Backlog grouped by PBI --- +const PRIORITY_LABELS_SPRINT: Record = { + 1: 'Kritiek', + 2: 'Hoog', + 3: 'Gemiddeld', + 4: 'Laag', +} + +const PRIORITY_OPTIONS_SPRINT: Array<{ value: number | 'all'; label: string }> = [ + { value: 'all', label: 'Alle' }, + { value: 1, label: 'Kritiek' }, + { value: 2, label: 'Hoog' }, + { value: 3, label: 'Gemiddeld' }, + { value: 4, label: 'Laag' }, +] + +type StoryStatusFilter = 'OPEN' | 'IN_SPRINT' | 'DONE' | 'all' + +const STATUS_OPTIONS_SPRINT: Array<{ value: StoryStatusFilter; label: string }> = [ + { value: 'all', label: 'Alle' }, + { value: 'OPEN', label: 'Open' }, + { value: 'IN_SPRINT', label: 'In Sprint' }, + { value: 'DONE', label: 'Klaar' }, +] + +function FilterPills({ + label, + options, + value, + onChange, +}: { + label: string + options: Array<{ value: T; label: string }> + value: T + onChange: (v: T) => void +}) { + return ( +
+

{label}

+
+ {options.map((opt) => ( + + ))} +
+
+ ) +} + function DraggablePbiStoryRow({ story, isDemo, @@ -384,8 +446,46 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on } return auto }) + const [filterPriority, setFilterPriority] = useState('all') + const [filterStatus, setFilterStatus] = useState('all') + const [prefsLoaded, setPrefsLoaded] = useState(false) const { setNodeRef, isOver } = useDroppable({ id: 'backlog-zone' }) + // Hydrate filter prefs from localStorage post-mount (avoids SSR mismatch). + // setState calls here are intentional: hydrating from localStorage on first paint. + useEffect(() => { + const savedPriority = localStorage.getItem('scrum4me:sprint_pb_filter_priority') + if (savedPriority && savedPriority !== 'all') { + const n = parseInt(savedPriority, 10) + // eslint-disable-next-line react-hooks/set-state-in-effect + if (Number.isInteger(n) && n >= 1 && n <= 4) setFilterPriority(n) + } + const savedStatus = localStorage.getItem('scrum4me:sprint_pb_filter_status') + if (savedStatus === 'OPEN' || savedStatus === 'IN_SPRINT' || savedStatus === 'DONE') { + + setFilterStatus(savedStatus) + } + + setPrefsLoaded(true) + }, []) + + useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_filter_priority', String(filterPriority)) }, [filterPriority, prefsLoaded]) + useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_filter_status', filterStatus) }, [filterStatus, prefsLoaded]) + + const filteredPbis = pbisWithStories + .map(pbi => ({ + ...pbi, + stories: pbi.stories.filter(s => + (filterPriority === 'all' || s.priority === filterPriority) && + (filterStatus === 'all' || s.status === filterStatus) + ), + })) + .filter(pbi => pbi.stories.length > 0) + + const activeFilterCount = + (filterPriority !== 'all' ? 1 : 0) + + (filterStatus !== 'all' ? 1 : 0) + function toggle(pbiId: string) { setCollapsed(prev => { const next = new Set(prev) @@ -395,7 +495,7 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on } function collapseAll() { - setCollapsed(new Set(pbisWithStories.map(p => p.id))) + setCollapsed(new Set(filteredPbis.map(p => p.id))) } function expandAll() { @@ -404,7 +504,7 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on function onlyNotDone() { const auto = new Set() - for (const pbi of pbisWithStories) { + for (const pbi of filteredPbis) { if (pbi.stories.length > 0 && pbi.stories.every(s => s.status === 'DONE')) { auto.add(pbi.id) } @@ -412,32 +512,96 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on setCollapsed(auto) } - const collapseActions = ( - - - - - - Alles inklappen - - - - - - Alles uitklappen - - - - - - Alleen niet klaar - - + const headerActions = ( + <> + {filterPriority !== 'all' && ( + + )} + {filterStatus !== 'all' && ( + + )} + + + {`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`} + + } + /> + + + +
+ +
+
+
+ + + + + + Alles inklappen + + + + + + Alles uitklappen + + + + + + Alleen niet klaar + + + ) return (
- +
- {pbisWithStories.map(pbi => ( + {filteredPbis.map(pbi => (