'use client' import { useState } from 'react' import { CheckSquare, Square } from 'lucide-react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { useShallow } from 'zustand/react/shallow' import { useUserSettingsStore } from '@/stores/user-settings/store' import { useProductWorkspaceStore } from '@/stores/product-workspace/store' import { selectStoriesForActivePbi, selectStoryIsBlocked, } from '@/stores/product-workspace/selectors' import type { BacklogStory as WorkspaceStory } from '@/stores/product-workspace/types' import { StoryDialog, type StoryDialogState } from './story-dialog' import { debugProps } from '@/lib/debug' import { BacklogCard } from './backlog-card' import { EmptyPanel } from './empty-panel' import { DemoTooltip } from '@/components/shared/demo-tooltip' import { cn } from '@/lib/utils' type SortMode = 'priority' | 'code' | 'date' const STATUS_COLORS: Record = { 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', DONE: 'bg-status-done/15 text-status-done border-status-done/30', } const STATUS_LABELS: Record = { OPEN: 'Open', IN_SPRINT: 'In Sprint', DONE: 'Klaar', } export interface Story { id: string code: string | null title: string description: string | null acceptance_criteria: string | null priority: number sort_order: number status: string pbi_id: string sprint_id: string | null created_at: Date } interface StoryPanelProps { productId: string isDemo: boolean activeSprintId?: string | null } function StoryBlock({ story, isSelected, cherrypick, onSelect, onEdit, }: { story: Story isSelected: boolean cherrypick: { checked: boolean blocked: { sprintName: string } | null onToggle: () => void } | null onSelect: () => void onEdit: () => void }) { return ( {STATUS_LABELS[story.status] ?? story.status} } actions={
{cherrypick && }
} /> ) } function StoryCherrypickButton({ checked, blocked, onToggle, }: { checked: boolean blocked: { sprintName: string } | null onToggle: () => void }) { const icon = checked ? ( ) : ( ) if (blocked) { return ( e.stopPropagation()} className="inline-flex items-center justify-center min-h-7 min-w-7 rounded opacity-40 cursor-not-allowed text-muted-foreground" > {icon} Zit in sprint {blocked.sprintName} ) } return ( ) } // --- Main component --- export function StoryPanel({ productId, isDemo, activeSprintId = null }: StoryPanelProps) { const selectedPbiId = useProductWorkspaceStore((s) => s.context.activePbiId) const selectedStoryId = useProductWorkspaceStore((s) => s.context.activeStoryId) const rawStories = useProductWorkspaceStore(useShallow(selectStoriesForActivePbi)) as WorkspaceStory[] const [filterStatus, setFilterStatus] = useState(null) const [filterPriority, setFilterPriority] = useState(null) const sortMode: SortMode = useUserSettingsStore( (s) => s.entities.settings.views?.storyPanel?.sort ?? 'priority', ) const setPref = useUserSettingsStore((s) => s.setPref) const setSortMode = (v: SortMode) => void setPref(['views', 'storyPanel', 'sort'], v) const [storyDialogState, setStoryDialogState] = useState(null) const base = rawStories .filter(s => !filterStatus || s.status === filterStatus) .filter(s => !filterPriority || s.priority === filterPriority) 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 hasActiveFilters = filterStatus !== null || filterPriority !== null return (
{hasActiveFilters && ( )} {selectedPbiId && ( )} } />
{selectedPbiId === null ? ( ) : rawStories.length === 0 ? ( setStoryDialogState({ mode: 'create', pbiId: selectedPbiId, productId, defaultPriority: 2 }), disabled: isDemo }} /> ) : (
{filtered.map(story => ( useProductWorkspaceStore.getState().setActiveStory(story.id)} onEdit={() => setStoryDialogState({ mode: 'edit', story, productId })} /> ))}
)}
setStoryDialogState(null)} isDemo={isDemo} />
) } // PBI-79 / ST-1337: wrapper rond StoryBlock met cherrypick-handling. function StoryBlockWithCherrypick({ story, productId, activeSprintId, isSelected, onSelect, onEdit, }: { story: Story productId: string activeSprintId: string | null isSelected: boolean onSelect: () => void onEdit: () => void }) { const draft = useUserSettingsStore( (s) => s.entities.settings.workflow?.pendingSprintDraft?.[productId], ) const upsertStoryOverride = useUserSettingsStore((s) => s.upsertStoryOverride) const toggleStorySprintMembership = useProductWorkspaceStore( (s) => s.toggleStorySprintMembership, ) const pending = useProductWorkspaceStore((s) => s.sprintMembership.pending) const blocked = useProductWorkspaceStore((s) => selectStoryIsBlocked(s, story.id), ) let cherrypick: { checked: boolean blocked: { sprintName: string } | null onToggle: () => void } | null = null if (draft) { const intent = draft.pbiIntent[story.pbi_id] ?? 'none' const override = draft.storyOverrides[story.pbi_id] ?? { add: [], remove: [], } const checked = (intent === 'all' && !override.remove.includes(story.id)) || override.add.includes(story.id) cherrypick = { checked, blocked: blocked ? { sprintName: blocked.sprintName } : null, onToggle: () => { if (intent === 'all') { void upsertStoryOverride( productId, story.pbi_id, story.id, checked ? 'remove' : 'clear', ) } else { void upsertStoryOverride( productId, story.pbi_id, story.id, checked ? 'clear' : 'add', ) } }, } } else if (activeSprintId) { const inSprintDb = story.sprint_id === activeSprintId const inAdds = pending.adds.includes(story.id) const inRemoves = pending.removes.includes(story.id) const checked = inAdds || (inSprintDb && !inRemoves) cherrypick = { checked, blocked: blocked ? { sprintName: blocked.sprintName } : null, onToggle: () => { toggleStorySprintMembership(story.id, inSprintDb) }, } } return ( ) }