Scrum4Me/components/solo/solo-task-card.tsx
Janpeter Visser 78543ee796
feat(ST-dgognlsz): SoloTaskCard 4-regels layout met Tooltips (#117)
4-regels layout: taaknaam+task_code badge (tooltip: naam+beschrijving),
beschrijving+pbi_code badge (tooltip: pbi_title+pbi_description), story+job-badge.
SoloTaskCardOverlay identieke 4-regels structuur zonder tooltips.
PBI-velden toegevoegd aan SoloTask-interface + Prisma-queries + test-fixtures.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 03:35:32 +02:00

170 lines
6.1 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 type React from 'react'
import { useDraggable } from '@dnd-kit/core'
import { CSS } from '@dnd-kit/utilities'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { CodeBadge } from '@/components/shared/code-badge'
import { JOB_STATUS_LABELS, JOB_STATUS_COLORS, JOB_STATUS_ACTIVE } from '@/components/shared/job-status'
import { useSoloStore } from '@/stores/solo-store'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
import type { SoloTask } from './solo-board'
const PRIORITY_BORDER: Record<number, string> = {
1: 'border-l-4 border-l-priority-critical',
2: 'border-l-4 border-l-priority-high',
3: 'border-l-4 border-l-priority-medium',
4: 'border-l-4 border-l-priority-low',
}
interface SoloTaskCardProps {
task: SoloTask
isDemo: boolean
onClick: () => void
}
export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
const job = useSoloStore(s => s.claudeJobsByTaskId[task.id])
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
id: task.id,
disabled: isDemo,
})
const style: React.CSSProperties | undefined = transform
? { transform: CSS.Translate.toString(transform) }
: { viewTransitionName: `solo-task-${task.id}` }
return (
<div
ref={setNodeRef}
style={style}
onClick={onClick}
className={cn(
'bg-surface-container rounded border border-border px-3 py-2 select-none transition-colors',
PRIORITY_BORDER[task.priority],
isDragging ? 'opacity-40 z-50 shadow-lg' : 'hover:bg-surface-container-high',
isDemo ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing',
)}
{...(!isDemo ? { ...attributes, ...listeners } : {})}
>
{/* Regel 1: taaknaam + task_code */}
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
{task.task_code && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="shrink-0 mt-0.5" />}>
<CodeBadge code={task.task_code} />
</TooltipTrigger>
<TooltipContent side="left">
<p className="font-semibold">{task.title}</p>
{task.description && (
<p className="text-muted-foreground italic">{task.description.slice(0, 100)}</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{/* Regels 23: beschrijving + pbi_code */}
<div className="flex items-start justify-between gap-2 mt-0.5">
{task.description ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={
<p className="text-xs text-muted-foreground line-clamp-2 flex-1" />
}>
{task.description}
</TooltipTrigger>
{task.description.length > 80 && (
<TooltipContent side="bottom">
{task.description}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : (
<div className="flex-1" />
)}
{task.pbi_code && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="shrink-0" />}>
<CodeBadge code={task.pbi_code} />
</TooltipTrigger>
<TooltipContent side="left">
<p className="font-semibold">{task.pbi_title}</p>
{task.pbi_description && (
<p className="text-muted-foreground italic">{task.pbi_description.slice(0, 100)}</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{/* Regel 4: story-info + job-badge */}
<div className="flex items-center justify-between gap-2 mt-0.5">
<p className="text-xs text-muted-foreground truncate flex-1">
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
{task.story_title}
</p>
{job && (
<span
className={cn(
'text-[10px] px-1.5 py-0 rounded border flex items-center gap-1 shrink-0 cursor-pointer',
JOB_STATUS_COLORS[job.status],
)}
onClick={(e) => { e.stopPropagation(); onClick() }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.stopPropagation()
onClick()
}
}}
role="button"
tabIndex={0}
aria-label={`Agent-status: ${JOB_STATUS_LABELS[job.status]}`}
>
{JOB_STATUS_ACTIVE.has(job.status) && <Loader2 className="animate-spin" size={8} />}
{JOB_STATUS_LABELS[job.status]}
</span>
)}
</div>
</div>
)
}
export function SoloTaskCardOverlay({ task }: { task: SoloTask }) {
return (
<div
className={cn(
'bg-surface-container rounded border border-primary px-3 py-2 shadow-xl opacity-90',
PRIORITY_BORDER[task.priority],
)}
>
{/* Regel 1 */}
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-snug flex-1">{task.title}</p>
{task.task_code && <CodeBadge code={task.task_code} className="shrink-0 mt-0.5" />}
</div>
{/* Regels 23 */}
<div className="flex items-start justify-between gap-2 mt-0.5">
{task.description ? (
<p className="text-xs text-muted-foreground line-clamp-2 flex-1">{task.description}</p>
) : (
<div className="flex-1" />
)}
{task.pbi_code && <CodeBadge code={task.pbi_code} className="shrink-0" />}
</div>
{/* Regel 4 */}
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{task.story_code && <span className="font-mono mr-1">{task.story_code}</span>}
{task.story_title}
</p>
</div>
)
}