feat(codes): UI toont en accepteert code voor taken

- TaskDialog: code-input boven titel (font-mono, optional,
  placeholder "auto"), CodeBadge in dialog header bij edit
- EditTaskLoader: select code uit DB voor de dialog
- Solo page: vervang inline deriveTaskCode-logica door directe
  task.code uit DB
- Sprint page + TaskList + SprintBoardClient: Task-type krijgt
  verplicht code-veld; TaskList laat ongebruikte storyCode prop
  vallen omdat code-derivatie niet meer nodig is

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-04 08:36:52 +02:00
parent 7c82a736f5
commit 081a0a51c3
6 changed files with 52 additions and 29 deletions

View file

@ -72,26 +72,21 @@ export default async function SoloProductPage({ params }: Props) {
}), }),
]) ])
const tasks: SoloTask[] = rawTasks.map(t => { const tasks: SoloTask[] = rawTasks.map(t => ({
const positionInStory = t.story.tasks.findIndex(st => st.id === t.id) id: t.id,
const taskCode = title: t.title,
t.story.code && positionInStory >= 0 ? `${t.story.code}.${positionInStory + 1}` : null description: t.description,
return { implementation_plan: t.implementation_plan,
id: t.id, priority: t.priority,
title: t.title, sort_order: t.sort_order,
description: t.description, status: t.status as SoloTask['status'],
implementation_plan: t.implementation_plan, verify_only: t.verify_only,
priority: t.priority, verify_required: t.verify_required as SoloTask['verify_required'],
sort_order: t.sort_order, story_id: t.story.id,
status: t.status as SoloTask['status'], story_code: t.story.code,
verify_only: t.verify_only, story_title: t.story.title,
verify_required: t.verify_required as SoloTask['verify_required'], task_code: t.code,
story_id: t.story.id, }))
story_code: t.story.code,
story_title: t.story.title,
task_code: taskCode,
}
})
const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({ const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({
id: s.id, id: s.id,

View file

@ -81,6 +81,7 @@ export default async function SprintBoardPage({ params, searchParams }: Props) {
for (const story of sprintStories) { for (const story of sprintStories) {
tasksByStory[story.id] = story.tasks.map(t => ({ tasksByStory[story.id] = story.tasks.map(t => ({
id: t.id, id: t.id,
code: t.code,
title: t.title, title: t.title,
description: t.description, description: t.description,
priority: t.priority, priority: t.priority,

View file

@ -25,6 +25,7 @@ export async function EditTaskLoader({
}, },
select: { select: {
id: true, id: true,
code: true,
title: true, title: true,
description: true, description: true,
implementation_plan: true, implementation_plan: true,

View file

@ -27,6 +27,8 @@ import {
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { CodeBadge } from '@/components/shared/code-badge'
import { MAX_CODE_LENGTH } from '@/lib/code'
import { DemoTooltip } from '@/components/shared/demo-tooltip' import { DemoTooltip } from '@/components/shared/demo-tooltip'
import { import {
useDirtyCloseGuard, useDirtyCloseGuard,
@ -45,6 +47,7 @@ import { cn } from '@/lib/utils'
export interface TaskDialogTask { export interface TaskDialogTask {
id: string id: string
code: string | null
title: string title: string
description: string | null description: string | null
implementation_plan: string | null implementation_plan: string | null
@ -88,6 +91,7 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
resolver: zodResolver(taskSchema), resolver: zodResolver(taskSchema),
mode: 'onTouched', mode: 'onTouched',
defaultValues: { defaultValues: {
code: task?.code ?? '',
title: task?.title ?? '', title: task?.title ?? '',
description: task?.description ?? '', description: task?.description ?? '',
implementation_plan: task?.implementation_plan ?? '', implementation_plan: task?.implementation_plan ?? '',
@ -173,9 +177,12 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
> >
{/* Sticky header */} {/* Sticky header */}
<div className={entityDialogHeaderClasses}> <div className={entityDialogHeaderClasses}>
<DialogTitle className="text-xl font-semibold"> <div className="flex items-center gap-2">
{isEdit ? 'Taak bewerken' : 'Nieuwe taak'} <DialogTitle className="text-xl font-semibold">
</DialogTitle> {isEdit ? 'Taak bewerken' : 'Nieuwe taak'}
</DialogTitle>
{isEdit && task?.code && <CodeBadge code={task.code} />}
</div>
{isEdit && ( {isEdit && (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Aangemaakt:{' '} Aangemaakt:{' '}
@ -190,6 +197,27 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
{/* Scrollable form body */} {/* Scrollable form body */}
<div className={entityDialogBodyClasses}> <div className={entityDialogBodyClasses}>
{/* Code */}
<div>
<label htmlFor="task-code" className="text-sm font-medium mb-2 block">
Code
</label>
<Input
id="task-code"
{...form.register('code')}
aria-invalid={!!form.formState.errors.code}
placeholder="auto (T-1, T-2, ...)"
className="font-mono"
maxLength={MAX_CODE_LENGTH}
onKeyDown={(e) => { if (e.key === 'Enter') e.preventDefault() }}
/>
{form.formState.errors.code && (
<p className="text-xs text-destructive mt-1">
{form.formState.errors.code.message}
</p>
)}
</div>
{/* Title */} {/* Title */}
<div> <div>
<label className="text-sm font-medium mb-2 block"> <label className="text-sm font-medium mb-2 block">

View file

@ -229,7 +229,6 @@ export function SprintBoardClient({
<TaskList <TaskList
key="tasks" key="tasks"
storyId={selectedStoryId} storyId={selectedStoryId}
storyCode={stories.find(s => s.id === selectedStoryId)?.code ?? null}
sprintId={sprintId} sprintId={sprintId}
productId={productId} productId={productId}
tasks={selectedTasks} tasks={selectedTasks}

View file

@ -17,7 +17,6 @@ import { Badge } from '@/components/ui/badge'
import { CodeBadge } from '@/components/shared/code-badge' import { CodeBadge } from '@/components/shared/code-badge'
import { PanelNavBar } from '@/components/shared/panel-nav-bar' import { PanelNavBar } from '@/components/shared/panel-nav-bar'
import { PRIORITY_BORDER } from '@/components/backlog/backlog-card' import { PRIORITY_BORDER } from '@/components/backlog/backlog-card'
import { deriveTaskCode } from '@/lib/code'
import { useSprintStore } from '@/stores/sprint-store' import { useSprintStore } from '@/stores/sprint-store'
import { updateTaskStatusAction, reorderTasksAction } from '@/actions/tasks' import { updateTaskStatusAction, reorderTasksAction } from '@/actions/tasks'
import { DemoTooltip } from '@/components/shared/demo-tooltip' import { DemoTooltip } from '@/components/shared/demo-tooltip'
@ -38,6 +37,7 @@ const STATUS_LABELS: Record<string, string> = { TO_DO: 'To Do', IN_PROGRESS: 'Be
export interface Task { export interface Task {
id: string id: string
code: string
title: string title: string
description: string | null description: string | null
priority: number priority: number
@ -48,7 +48,6 @@ export interface Task {
interface TaskListProps { interface TaskListProps {
storyId: string storyId: string
storyCode: string | null
sprintId: string sprintId: string
productId: string productId: string
tasks: Task[] tasks: Task[]
@ -126,7 +125,7 @@ function SortableTaskRow({
) )
} }
export function TaskList({ storyId, storyCode, sprintId: _sprintId, productId: _productId, tasks, isDemo }: TaskListProps) { export function TaskList({ storyId, sprintId: _sprintId, productId: _productId, tasks, isDemo }: TaskListProps) {
const { taskOrder, initTasks, reorderTasks, rollbackTasks } = useSprintStore() const { taskOrder, initTasks, reorderTasks, rollbackTasks } = useSprintStore()
const [activeDragId, setActiveDragId] = useState<string | null>(null) const [activeDragId, setActiveDragId] = useState<string | null>(null)
const [, startTransition] = useTransition() const [, startTransition] = useTransition()
@ -222,11 +221,11 @@ export function TaskList({ storyId, storyCode, sprintId: _sprintId, productId: _
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<SortableContext items={orderedTasks.map(t => t.id)} strategy={verticalListSortingStrategy}> <SortableContext items={orderedTasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
{orderedTasks.map((task, idx) => ( {orderedTasks.map((task) => (
<SortableTaskRow <SortableTaskRow
key={task.id} key={task.id}
task={task} task={task}
code={deriveTaskCode(storyCode, idx + 1)} code={task.code}
isDemo={isDemo} isDemo={isDemo}
onStatusToggle={() => handleStatusToggle(task)} onStatusToggle={() => handleStatusToggle(task)}
onEdit={() => openEditDialog(task.id)} onEdit={() => openEditDialog(task.id)}