Scrum4Me/components/sprint/sprint-backlog.tsx
Scrum4Me Agent f68d985c2c refactor(dnd): remove drag-and-drop reorder for stories and tasks
- Remove reorderStoriesAction, reorderTasksAction, reorderSprintStoriesAction
- Delete REST route app/api/stories/[id]/tasks/reorder/route.ts
- Remove DnD from backlog story-panel and task-panel (flat list)
- Remove reorder-within-sprint branch from sprint-board-client handleDragEnd
- Switch SortableSprintRow to plain SprintRow using useDraggable (membership drag kept)
- Remove all DnD from task-list (status toggle + edit kept)
- Remove story-order/task-order/sprint-story-order/sprint-task-order mutation types and store handlers
- Remove related tests for deleted reorder route; fix sprint store tests
2026-05-14 16:29:56 +02:00

665 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useMemo, useState, useTransition } from 'react'
import { Trash2, MoreHorizontal, ChevronsUp, ChevronsDown, Pencil } from 'lucide-react'
import { useDroppable, useDraggable } from '@dnd-kit/core'
import { toast } from 'sonner'
import { useShallow } from 'zustand/react/shallow'
import { Badge } from '@/components/ui/badge'
import { CodeBadge } from '@/components/shared/code-badge'
import { useUserSettingsStore } from '@/stores/user-settings/store'
import {
BacklogFilterPopover,
PRIORITY_LABELS as SHARED_PRIORITY_LABELS,
type SortDir,
} from '@/components/shared/backlog-filter-popover'
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent,
} from '@/components/ui/dropdown-menu'
import { PanelNavBar } from '@/components/shared/panel-nav-bar'
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 { useSprintWorkspaceStore } from '@/stores/sprint-workspace/store'
import { selectStoriesForActiveSprint } from '@/stores/sprint-workspace/selectors'
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'
import { debugProps } from '@/lib/debug'
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',
DONE: 'bg-status-done/15 text-status-done border-status-done/30',
}
const STATUS_LABELS: Record<string, string> = { OPEN: 'Open', IN_SPRINT: 'In Sprint', DONE: 'Klaar' }
export interface SprintStory {
id: string
code: string | null
title: string
description: string | null
acceptance_criteria: string | null
pbi_id: string
sprint_id: string | null
created_at: Date
priority: number
sort_order: number
status: string
taskCount: number
doneCount: number
assignee_id: string | null
assignee_username: string | null
}
export interface ProductMember {
userId: string
username: string
}
export interface PbiWithStories {
id: string
code: string | null
title: string
priority: number
status: PbiStatusApi
description: string | null
stories: SprintStory[]
}
// --- Left panel: Sprint Backlog ---
function SprintRow({
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
members: ProductMember[]
onAssigneeChange: (storyId: string, id: string | null, username: string | null) => void
}) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id: story.id })
const style = { opacity: isDragging ? 0.4 : 1 }
const [, startTransition] = useTransition()
function handleClaim(e: React.MouseEvent) {
e.stopPropagation()
const me = members.find(m => m.userId === currentUserId)
onAssigneeChange(story.id, currentUserId, me?.username ?? null)
startTransition(async () => {
const result = await claimStoryAction(story.id, productId)
if (!result.success) {
onAssigneeChange(story.id, story.assignee_id, story.assignee_username)
toast.error(result.error ?? 'Claimen mislukt')
} else {
toast.success('Story geclaimd')
}
})
}
function handleUnclaim(e: React.MouseEvent) {
e.stopPropagation()
onAssigneeChange(story.id, null, null)
startTransition(async () => {
const result = await unclaimStoryAction(story.id, productId)
if (!result.success) {
onAssigneeChange(story.id, story.assignee_id, story.assignee_username)
toast.error(result.error ?? 'Teruggeven mislukt')
}
})
}
function handleReassign(e: React.MouseEvent, targetUserId: string, targetUsername: string) {
e.stopPropagation()
onAssigneeChange(story.id, targetUserId, targetUsername)
startTransition(async () => {
const result = await reassignStoryAction(story.id, productId, targetUserId)
if (!result.success) {
onAssigneeChange(story.id, story.assignee_id, story.assignee_username)
toast.error(result.error ?? 'Toewijzen mislukt')
} else {
toast.success(`Toegewezen aan ${targetUsername}`)
}
})
}
return (
<div
ref={setNodeRef}
style={style}
onClick={onSelect}
className="group px-2 py-1 cursor-pointer"
>
<div className={cn(
'flex items-start gap-2 rounded border border-border px-3 py-2 transition-colors',
PRIORITY_BORDER[story.priority],
isSelected
? 'bg-primary-container border-primary text-primary-container-foreground'
: 'bg-surface-container hover:bg-surface-container-high'
)}>
{!isDemo && (
<span
{...attributes}
{...listeners}
aria-label="Versleep om te sorteren of naar Product Backlog"
className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 select-none text-sm mt-0.5"
onClick={e => e.stopPropagation()}
>
</span>
)}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<p className="text-sm leading-snug line-clamp-2 flex-1">{story.title}</p>
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
</div>
<div className="flex items-center justify-between gap-2 mt-1.5">
<div className="flex items-center gap-1.5 min-w-0">
<Badge className={cn('text-[10px] px-1.5 py-0 border shrink-0', STATUS_COLORS[story.status])}>
{STATUS_LABELS[story.status]}
</Badge>
<span className="text-xs text-muted-foreground shrink-0">{story.doneCount}/{story.taskCount} klaar</span>
{story.assignee_id ? (
<div className="flex items-center gap-1 min-w-0">
<UserAvatar userId={story.assignee_id} username={story.assignee_username ?? '?'} size="xs" />
<span className="text-xs text-muted-foreground truncate">{story.assignee_username}</span>
</div>
) : (
<span className="text-xs text-muted-foreground italic">Niet geclaimd</span>
)}
</div>
<div className="flex items-center gap-1 shrink-0" onClick={e => e.stopPropagation()}>
<DemoTooltip show={isDemo}>
<DropdownMenu>
<DropdownMenuTrigger
disabled={isDemo}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground p-0.5 rounded bg-transparent border-0 cursor-pointer"
aria-label="Story opties"
>
<MoreHorizontal size={14} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={e => e.stopPropagation()}>
{story.assignee_id !== currentUserId && (
<DropdownMenuItem onClick={handleClaim}>Pak op</DropdownMenuItem>
)}
{story.assignee_id && (
<DropdownMenuItem onClick={handleUnclaim}>Geef terug aan team</DropdownMenuItem>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>Wijs toe aan</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{members.map(m => (
<DropdownMenuItem key={m.userId} onClick={e => handleReassign(e, m.userId, m.username)}>
<UserAvatar userId={m.userId} username={m.username} size="xs" />
<span>{m.username}</span>
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
</DemoTooltip>
<DemoTooltip show={isDemo}>
<button
onClick={e => { e.stopPropagation(); if (!isDemo) onEdit() }}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Bewerk story"
disabled={isDemo}
>
<Pencil size={14} />
</button>
</DemoTooltip>
<DemoTooltip show={isDemo}>
<button
onClick={e => { e.stopPropagation(); if (!isDemo) onRemove() }}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-error disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Verwijder uit sprint"
disabled={isDemo}
>
<Trash2 size={14} />
</button>
</DemoTooltip>
</div>
</div>
</div>
</div>
</div>
)
}
interface SprintBacklogLeftProps {
sprintId: string
isDemo: boolean
onRemove: (storyId: string) => void
onSelect: (storyId: string) => void
selectedStoryId: string | null
currentUserId: string
productId: string
members: ProductMember[]
onAssigneeChange: (storyId: string, id: string | null, username: string | null) => void
}
export function SprintBacklogLeft({
sprintId: _sprintId, isDemo, onRemove, onSelect, selectedStoryId,
currentUserId, productId, members, onAssigneeChange,
}: SprintBacklogLeftProps) {
const orderedStories = useSprintWorkspaceStore(
useShallow((s) => selectStoriesForActiveSprint(s) as SprintStory[]),
)
const { setNodeRef, isOver } = useDroppable({ id: 'sprint-zone' })
const [isPending, startTransition] = useTransition()
const [storyDialogState, setStoryDialogState] = useState<StoryDialogState | null>(null)
const unassignedCount = orderedStories.filter(s => (s.assignee_id ?? null) === null).length
const currentUserUsername = members.find(m => m.userId === currentUserId)?.username ?? null
function handleClaimAll() {
const unassigned = orderedStories.filter(s => (s.assignee_id ?? null) === null)
unassigned.forEach(s => onAssigneeChange(s.id, currentUserId, currentUserUsername))
startTransition(async () => {
const result = await claimAllUnassignedInActiveSprintAction(productId)
if (!result.success) {
unassigned.forEach(s => onAssigneeChange(s.id, null, null))
toast.error(result.error ?? 'Claimen mislukt')
} else {
toast.success(`${result.count} ${result.count === 1 ? 'story' : 'stories'} geclaimd`)
}
})
}
return (
<div className="flex flex-col h-full" {...debugProps('sprint-backlog-left', 'SprintBacklogLeft', 'components/sprint/sprint-backlog.tsx')}>
<PanelNavBar
title="Sprint Backlog"
actions={
<DemoTooltip show={isDemo}>
<button
onClick={handleClaimAll}
disabled={isDemo || unassignedCount === 0 || isPending}
className="text-xs text-primary hover:text-primary/80 disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
>
{isPending ? 'Claimen…' : `Claim ongeclaimd (${unassignedCount})`}
</button>
</DemoTooltip>
}
/>
<div
ref={setNodeRef}
data-debug-id="sprint-backlog-left__list"
className={cn(
'flex-1 overflow-y-auto transition-colors',
isOver && 'bg-primary/5 ring-2 ring-inset ring-primary/20 rounded'
)}
>
{orderedStories.length === 0 ? (
<p className={cn(
'text-sm text-muted-foreground text-center mt-8 px-4',
isOver && 'text-primary'
)}>
{isOver ? 'Loslaten om toe te voegen aan Sprint' : 'Geen stories in de Sprint. Sleep stories vanuit het linkerpaneel.'}
</p>
) : (
<>
{orderedStories.map(story => (
<SprintRow
key={story.id}
story={story}
isDemo={isDemo}
onRemove={() => onRemove(story.id)}
onSelect={() => onSelect(story.id)}
onEdit={() => setStoryDialogState({ mode: 'edit', story, productId })}
isSelected={selectedStoryId === story.id}
currentUserId={currentUserId}
productId={productId}
members={members}
onAssigneeChange={onAssigneeChange}
/>
))}
</>
)}
</div>
<StoryDialog
state={storyDialogState}
onClose={() => setStoryDialogState(null)}
isDemo={isDemo}
/>
</div>
)
}
// --- Right panel: Product Backlog grouped by PBI ---
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' },
]
type PbiSort = 'code' | 'priority' | 'status'
const SORT_OPTIONS_SPRINT: Array<{ value: PbiSort; label: string }> = [
{ value: 'code', label: 'Code' },
{ value: 'priority', label: 'Prioriteit' },
{ value: 'status', label: 'Status' },
]
const PBI_STATUS_ORDER: Record<PbiStatusApi, number> = {
ready: 0,
blocked: 1,
failed: 2,
done: 3,
}
function comparePbis(a: PbiWithStories, b: PbiWithStories, sort: PbiSort): number {
const codeCmp = (a.code ?? '').localeCompare(b.code ?? '', undefined, { numeric: true })
if (sort === 'priority') {
if (a.priority !== b.priority) return a.priority - b.priority
return codeCmp
}
if (sort === 'status') {
const sa = PBI_STATUS_ORDER[a.status] ?? 99
const sb = PBI_STATUS_ORDER[b.status] ?? 99
if (sa !== sb) return sa - sb
return codeCmp
}
return codeCmp
}
function DraggablePbiStoryRow({
story,
isDemo,
onAdd,
}: {
story: SprintStory
isDemo: boolean
onAdd: () => void
}) {
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: `pb:${story.id}` })
const style = transform
? { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, zIndex: 50, position: 'relative' as const }
: undefined
return (
<div
ref={setNodeRef}
style={style}
className={cn(
'px-2 py-1',
isDemo && 'opacity-60',
isDragging && 'opacity-40'
)}
>
<div className={cn(
'flex items-center gap-2 rounded border border-border px-3 py-2 transition-colors bg-surface-container',
PRIORITY_BORDER[story.priority],
)}>
{!isDemo && (
<span
{...attributes}
{...listeners}
aria-label="Sleep naar Sprint Backlog"
className="text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 select-none text-sm"
>
</span>
)}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<p className="text-sm leading-snug line-clamp-2 flex-1">{story.title}</p>
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
</div>
<div className="mt-1.5">
<Badge className={cn('text-[10px] px-1.5 py-0 border', STATUS_COLORS[story.status])}>
{STATUS_LABELS[story.status]}
</Badge>
</div>
</div>
<DemoTooltip show={isDemo}>
<button
onClick={() => !isDemo && onAdd()}
className="text-xs text-primary hover:underline shrink-0 disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline"
disabled={isDemo}
>
+ Toevoegen
</button>
</DemoTooltip>
</div>
</div>
)
}
interface SprintBacklogRightProps {
pbisWithStories: PbiWithStories[]
sprintStoryIds: Set<string>
isDemo: boolean
productId: string
onAdd: (storyId: string) => void
}
export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, productId, onAdd }: SprintBacklogRightProps) {
const prefs = useUserSettingsStore(
useShallow((s) => s.entities.settings.views?.sprintBacklog ?? {}),
)
const setPref = useUserSettingsStore((s) => s.setPref)
const filterPriority = prefs.filterPriority ?? 'all'
const filterStatus: StoryStatusFilter = prefs.filterStatus ?? 'OPEN'
const sort: PbiSort = prefs.sort ?? 'code'
const sortDir: SortDir = prefs.sortDir ?? 'asc'
const filterPopoverOpen = prefs.filterPopoverOpen ?? false
const collapsed = useMemo<Set<string>>(() => {
if (prefs.collapsedPbis !== undefined) return new Set(prefs.collapsedPbis)
// Default: auto-collapse PBIs whose stories are all DONE.
const auto = new Set<string>()
for (const pbi of pbisWithStories) {
if (pbi.stories.length > 0 && pbi.stories.every((s) => s.status === 'DONE')) {
auto.add(pbi.id)
}
}
return auto
}, [prefs.collapsedPbis, pbisWithStories])
const setFilterPriority = (v: number | 'all') =>
void setPref(['views', 'sprintBacklog', 'filterPriority'], v)
const setFilterStatus = (v: StoryStatusFilter) =>
void setPref(['views', 'sprintBacklog', 'filterStatus'], v)
const setSort = (v: PbiSort) => void setPref(['views', 'sprintBacklog', 'sort'], v)
const setSortDir = (v: SortDir) => void setPref(['views', 'sprintBacklog', 'sortDir'], v)
const setFilterPopoverOpen = (v: boolean) =>
void setPref(['views', 'sprintBacklog', 'filterPopoverOpen'], v)
const setCollapsedArray = (next: Set<string>) =>
void setPref(['views', 'sprintBacklog', 'collapsedPbis'], Array.from(next))
const [pbiDialogState, setPbiDialogState] = useState<PbiDialogState | null>(null)
const { setNodeRef, isOver } = useDroppable({ id: 'backlog-zone' })
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)
.sort((a, b) => (sortDir === 'desc' ? -1 : 1) * comparePbis(a, b, sort))
const activeFilterCount =
(filterPriority !== 'all' ? 1 : 0) +
(filterStatus !== 'OPEN' ? 1 : 0)
function toggle(pbiId: string) {
const next = new Set(collapsed)
if (next.has(pbiId)) next.delete(pbiId)
else next.add(pbiId)
setCollapsedArray(next)
}
function collapseAll() {
setCollapsedArray(new Set(filteredPbis.map((p) => p.id)))
}
function expandAll() {
setCollapsedArray(new Set())
}
const headerActions = (
<>
{filterPriority !== 'all' && (
<button
onClick={() => setFilterPriority('all')}
className="flex items-center gap-1 text-xs text-primary hover:underline"
aria-label="Wis prioriteitsfilter"
>
<Badge className={cn('text-xs', PRIORITY_COLORS[filterPriority])}>
{SHARED_PRIORITY_LABELS[filterPriority]}
</Badge>
<span>×</span>
</button>
)}
{filterStatus !== 'OPEN' && (
<button
onClick={() => setFilterStatus('OPEN')}
className="flex items-center gap-1 text-xs text-primary hover:underline"
aria-label="Wis statusfilter"
>
<Badge className={cn('text-[10px] px-1.5 py-0 border', filterStatus === 'all' ? '' : STATUS_COLORS[filterStatus])}>
{filterStatus === 'all' ? 'Alle' : STATUS_LABELS[filterStatus]}
</Badge>
<span>×</span>
</button>
)}
<BacklogFilterPopover
open={filterPopoverOpen}
onOpenChange={setFilterPopoverOpen}
filterPriority={filterPriority}
onFilterPriorityChange={setFilterPriority}
filterStatus={filterStatus}
onFilterStatusChange={setFilterStatus}
statusOptions={STATUS_OPTIONS_SPRINT}
sort={sort}
onSortChange={setSort}
sortDir={sortDir}
onSortDirChange={setSortDir}
sortOptions={SORT_OPTIONS_SPRINT}
activeFilterCount={activeFilterCount}
resetDisabled={filterPriority === 'all' && filterStatus === 'OPEN' && sort === 'code' && sortDir === 'asc'}
onReset={() => {
setFilterPriority('all')
setFilterStatus('OPEN')
setSort('code')
setSortDir('asc')
}}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger onClick={collapseAll} className="text-muted-foreground hover:text-foreground p-0.5 rounded" aria-label="Alles inklappen">
<ChevronsUp size={14} />
</TooltipTrigger>
<TooltipContent>Alles inklappen</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger onClick={expandAll} className="text-muted-foreground hover:text-foreground p-0.5 rounded" aria-label="Alles uitklappen">
<ChevronsDown size={14} />
</TooltipTrigger>
<TooltipContent>Alles uitklappen</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)
return (
<div className="flex flex-col h-full" {...debugProps('sprint-backlog-right', 'SprintBacklogRight', 'components/sprint/sprint-backlog.tsx')}>
<PanelNavBar title="Product Backlog" actions={headerActions} />
<div
ref={setNodeRef}
data-debug-id="sprint-backlog-right__list"
className={cn(
'flex-1 overflow-y-auto py-2 transition-colors',
isOver && 'bg-error/5 ring-2 ring-inset ring-error/20 rounded'
)}
>
{filteredPbis.map(pbi => (
<div key={pbi.id}>
<div
role="button"
tabIndex={0}
onClick={() => toggle(pbi.id)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(pbi.id) } }}
className="group w-full flex items-center gap-2 px-4 py-1.5 hover:bg-surface-container transition-colors text-left select-none cursor-pointer"
>
<span className="text-xs">{collapsed.has(pbi.id) ? '▶' : '▼'}</span>
<span className="text-sm font-medium truncate flex-1">{pbi.title}</span>
{pbi.code && <CodeBadge code={pbi.code} />}
<span className="text-xs text-muted-foreground">
{pbi.stories.filter(s => s.status === 'DONE').length}/{pbi.stories.length} klaar
</span>
<DemoTooltip show={isDemo}>
<button
onClick={(e) => {
e.stopPropagation()
if (!isDemo) setPbiDialogState({ mode: 'edit', productId, pbi: { id: pbi.id, title: pbi.title, code: pbi.code, priority: pbi.priority, status: pbi.status, description: pbi.description } })
}}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground p-0.5 rounded disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Bewerk PBI"
disabled={isDemo}
>
<Pencil size={14} />
</button>
</DemoTooltip>
</div>
{!collapsed.has(pbi.id) && pbi.stories.map(story => {
const inSprint = sprintStoryIds.has(story.id)
if (inSprint) {
return (
<div key={story.id} className="px-2 py-1 opacity-40">
<div className={cn(
'flex items-center gap-2 rounded border border-border px-3 py-2 bg-surface-container',
PRIORITY_BORDER[story.priority]
)}>
<div className="w-[14px] shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<p className="text-sm leading-snug line-clamp-2 flex-1">{story.title}</p>
{story.code && <CodeBadge code={story.code} className="shrink-0 mt-0.5" />}
</div>
<div className="mt-1.5">
<Badge className={cn('text-[10px] px-1.5 py-0 border', STATUS_COLORS[story.status])}>
{STATUS_LABELS[story.status]}
</Badge>
</div>
</div>
<span className="text-xs text-muted-foreground shrink-0">In Sprint</span>
</div>
</div>
)
}
return <DraggablePbiStoryRow key={story.id} story={story} isDemo={isDemo} onAdd={() => onAdd(story.id)} />
})}
</div>
))}
</div>
<PbiDialog
state={pbiDialogState}
onClose={() => setPbiDialogState(null)}
isDemo={isDemo}
/>
</div>
)
}