import { Suspense } from 'react' 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 { getSprintSwitcherData } from '@/lib/sprint-switcher-data' import { BacklogSplitPane } from '@/components/backlog/backlog-split-pane' import { PbiList } from '@/components/backlog/pbi-list' import { StoryPanel } from '@/components/backlog/story-panel' import type { Story } from '@/components/backlog/story-panel' import { TaskPanel } from '@/components/backlog/task-panel' import { BacklogHydrationWrapper } from '@/components/backlog/backlog-hydration-wrapper' import { UrlTaskSync } from '@/components/backlog/url-task-sync' import { TaskDialog } from '@/app/_components/tasks/task-dialog' import { EditTaskLoader } from '@/app/_components/tasks/edit-task-loader' import { TaskDialogSkeleton } from '@/app/_components/tasks/task-dialog-skeleton' import { NewSprintTrigger } from '@/components/backlog/new-sprint-trigger' import { SprintDraftBanner } from '@/components/backlog/sprint-draft-banner' import { SprintDraftLeaveGuard } from '@/components/backlog/sprint-draft-leave-guard' import { SaveSprintButton } from '@/components/backlog/save-sprint-button' import { ActiveSelectionHydrator } from '@/components/backlog/active-selection-hydrator' import { ActivateProductButton } from '@/components/shared/activate-product-button' import { EditProductButton } from '@/components/products/edit-product-button' import { SprintSwitcher } from '@/components/shared/sprint-switcher' import Link from 'next/link' interface Props { params: Promise<{ id: string }> searchParams: Promise<{ newTask?: string; storyId?: string; editTask?: string }> } export default async function ProductBacklogPage({ params, searchParams }: Props) { const { id } = await params const { newTask, storyId: storyIdParam, editTask } = await searchParams const closePath = `/products/${id}` const session = await getSession() if (!session.userId) redirect('/login') const product = await getAccessibleProduct(id, session.userId) if (!product) notFound() const [user, switcherData] = await Promise.all([ prisma.user.findUnique({ where: { id: session.userId! }, select: { active_product_id: true } }), getSprintSwitcherData(id, { userId: session.userId }), ]) const { sprintItems, buildingSprintIds, activeSprintItem } = switcherData const hasOpenSprint = sprintItems.some(s => s.status === 'open') const isActiveProduct = user?.active_product_id === id const pbis = await prisma.pbi.findMany({ where: { product_id: id }, orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], }) const [stories, tasks] = await Promise.all([ prisma.story.findMany({ where: { product_id: id }, orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }], select: { id: true, code: true, title: true, description: true, acceptance_criteria: true, priority: true, sort_order: true, status: true, pbi_id: true, sprint_id: true, created_at: true, }, }), prisma.task.findMany({ where: { story: { pbi: { product_id: id } } }, select: { id: true, code: true, title: true, description: true, priority: true, status: true, sort_order: true, story_id: true, created_at: true, }, orderBy: [{ sort_order: 'asc' }, { created_at: 'asc' }], }), ]) // Group stories by PBI id (status uit DB blijft UPPER_SNAKE in dit hydratie-pad) const storiesByPbi: Record = {} for (const story of stories) { if (!storiesByPbi[story.pbi_id]) storiesByPbi[story.pbi_id] = [] storiesByPbi[story.pbi_id].push(story) } // Group tasks by story id const tasksByStory: Record = {} for (const task of tasks) { if (!tasksByStory[task.story_id]) tasksByStory[task.story_id] = [] tasksByStory[task.story_id].push(task) } const isDemo = session.isDemo ?? false return (
{/* Product header — sprint-switcher gecentreerd, actions rechts */}
{isActiveProduct && ( )}
{!isActiveProduct && ( )} {hasOpenSprint && ( Sprint actief → )} {activeSprintItem && !isDemo && ( )} {!isDemo && ( )} {!isDemo && product.user_id === session.userId && ( )} Instellingen
{/* Sprint definition banner (state A′) + beforeunload-guard */} {/* Split pane */}
({ id: p.id, code: p.code, title: p.title, priority: p.priority, sort_order: p.sort_order, description: p.description, created_at: p.created_at, status: pbiStatusToApi(p.status) })), storiesByPbi, tasksByStory, }} > , , , ]} />
{newTask && ( )} {editTask && !newTask && ( }> )}
) }