diff --git a/app/(app)/products/[id]/sprint/page.tsx b/app/(app)/products/[id]/sprint/page.tsx index e72bf56..3e3a36c 100644 --- a/app/(app)/products/[id]/sprint/page.tsx +++ b/app/(app)/products/[id]/sprint/page.tsx @@ -3,6 +3,7 @@ import { notFound, redirect } from 'next/navigation' import { getSession } from '@/lib/auth' import { getAccessibleProduct } from '@/lib/product-access' import { prisma } from '@/lib/prisma' +import { pbiStatusToApi } from '@/lib/task-status' import { SprintBoardClient } from '@/components/sprint/sprint-board-client' import { SprintHeader } from '@/components/sprint/sprint-header' import type { SprintStory, PbiWithStories, ProductMember } from '@/components/sprint/sprint-backlog' @@ -69,6 +70,10 @@ export default async function SprintBoardPage({ params, searchParams }: Props) { id: s.id, code: s.code, title: s.title, + description: s.description, + acceptance_criteria: s.acceptance_criteria, + pbi_id: s.pbi_id, + created_at: s.created_at, priority: s.priority, status: s.status, taskCount: s.tasks.length, @@ -108,10 +113,17 @@ export default async function SprintBoardPage({ params, searchParams }: Props) { id: pbi.id, code: pbi.code, title: pbi.title, + priority: pbi.priority, + status: pbiStatusToApi(pbi.status), + description: pbi.description, stories: pbi.stories.map(s => ({ id: s.id, code: s.code, title: s.title, + description: s.description, + acceptance_criteria: s.acceptance_criteria, + pbi_id: s.pbi_id, + created_at: s.created_at, priority: s.priority, status: s.status, taskCount: 0, diff --git a/components/sprint/sprint-backlog.tsx b/components/sprint/sprint-backlog.tsx index 3555912..e65f363 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 { Trash2, MoreHorizontal, ChevronsUp, ChevronsDown, ListFilter } from 'lucide-react' +import { useState, useTransition, useEffect } from 'react' +import { Trash2, MoreHorizontal, ChevronsUp, ChevronsDown, ListFilter, Pencil } 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,8 +19,12 @@ 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 { PbiDialog, type PbiDialogState } from '@/components/backlog/pbi-dialog' +import { StoryDialog, type StoryDialogState } from '@/components/backlog/story-dialog' +import type { PbiStatusApi } from '@/lib/task-status' import { cn } from '@/lib/utils' const STATUS_COLORS: Record = { @@ -32,6 +38,10 @@ export interface SprintStory { id: string code: string | null title: string + description: string | null + acceptance_criteria: string | null + pbi_id: string + created_at: Date priority: number status: string taskCount: number @@ -49,19 +59,23 @@ export interface PbiWithStories { id: string code: string | null title: string + priority: number + status: PbiStatusApi + description: string | null stories: SprintStory[] } // --- Left panel: Sprint Backlog --- function SortableSprintRow({ - story, isDemo, onRemove, onSelect, isSelected, + story, isDemo, onRemove, onSelect, onEdit, isSelected, currentUserId, productId, members, onAssigneeChange, }: { story: SprintStory isDemo: boolean onRemove: () => void onSelect: () => void + onEdit: () => void isSelected: boolean currentUserId: string productId: string @@ -189,6 +203,16 @@ function SortableSprintRow({ + + + + ))} + + + ) +} + function DraggablePbiStoryRow({ story, isDemo, @@ -371,10 +461,11 @@ interface SprintBacklogRightProps { pbisWithStories: PbiWithStories[] sprintStoryIds: Set isDemo: boolean + productId: string onAdd: (storyId: string) => void } -export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, onAdd }: SprintBacklogRightProps) { +export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, productId, onAdd }: SprintBacklogRightProps) { const [collapsed, setCollapsed] = useState>(() => { const auto = new Set() for (const pbi of pbisWithStories) { @@ -384,8 +475,47 @@ 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 [pbiDialogState, setPbiDialogState] = useState(null) 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 +525,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 +534,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 +542,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 => (
- + + + +
{!collapsed.has(pbi.id) && pbi.stories.map(story => { const inSprint = sprintStoryIds.has(story.id) @@ -490,6 +700,11 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on
))}
+ setPbiDialogState(null)} + isDemo={isDemo} + /> ) } diff --git a/components/sprint/sprint-board-client.tsx b/components/sprint/sprint-board-client.tsx index 4a6501a..4437f54 100644 --- a/components/sprint/sprint-board-client.tsx +++ b/components/sprint/sprint-board-client.tsx @@ -210,6 +210,7 @@ export function SprintBoardClient({ pbisWithStories={pbisWithStories} sprintStoryIds={sprintStoryIds} isDemo={isDemo} + productId={productId} onAdd={handleAdd} />, + + + )