'use client' import { useState, useTransition } from 'react' import { useRouter } from 'next/navigation' import { useForm, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import TextareaAutosize from 'react-textarea-autosize' import { toast } from 'sonner' import { Loader2 } from 'lucide-react' import type { TaskStatus } from '@prisma/client' import { taskSchema, type TaskInput } from '@/lib/schemas/task' import { saveTask, deleteTask } from '@/actions/tasks' import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogAction, AlertDialogCancel, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' 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 { useDirtyCloseGuard, DirtyCloseGuardDialog, } from '@/components/shared/use-dirty-close-guard' import { useDialogSubmitShortcut } from '@/components/shared/use-dialog-submit-shortcut' import { entityDialogBodyClasses, entityDialogContentClasses, entityDialogFooterClasses, entityDialogHeaderClasses, } from '@/components/shared/entity-dialog-layout' import { PrioritySegmented } from './priority-segmented' import { StatusSelect } from './status-select' import { cn } from '@/lib/utils' export interface TaskDialogTask { id: string code: string | null title: string description: string | null implementation_plan: string | null priority: number status: TaskStatus created_at: Date } interface TaskDialogProps { task?: TaskDialogTask storyId?: string productId: string closePath?: string onClose?: () => void onSaved?: (taskId: string) => void isDemo?: boolean } function CharCount({ value, max }: { value: string; max: number }) { const len = (value ?? '').length if (len < Math.floor(max * 0.75)) return null return ( {len} / {max} ) } const textareaClass = cn( 'flex w-full rounded-lg border border-border bg-input-background px-2.5 py-2 text-sm', 'transition-colors outline-none placeholder:text-muted-foreground resize-none', 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50', 'overflow-y-auto', ) export function TaskDialog({ task, storyId, productId, closePath, onClose, onSaved, isDemo = false }: TaskDialogProps) { const router = useRouter() const [isPending, startTransition] = useTransition() const [confirmDelete, setConfirmDelete] = useState(false) const isEdit = !!task const form = useForm({ resolver: zodResolver(taskSchema), mode: 'onTouched', defaultValues: { code: task?.code ?? '', title: task?.title ?? '', description: task?.description ?? '', implementation_plan: task?.implementation_plan ?? '', priority: task?.priority ?? 3, status: task?.status, }, }) function close() { if (onClose) { onClose(); return } if (closePath) router.push(closePath) } const closeGuard = useDirtyCloseGuard(form.formState.isDirty, close) const handleKeyDown = useDialogSubmitShortcut(() => form.handleSubmit(onSubmit)()) function onSubmit(data: TaskInput) { startTransition(async () => { const result = await saveTask(data, { taskId: task?.id, storyId, productId, }) if (result.ok) { toast.success(isEdit ? 'Taak opgeslagen' : 'Taak aangemaakt') onSaved?.(result.task.id) close() return } if (result.code === 422 && result.error === 'validation') { for (const [field, errors] of Object.entries(result.fieldErrors)) { form.setError(field as keyof TaskInput, { message: errors[0] }) } const firstError = Object.keys(result.fieldErrors)[0] as keyof TaskInput form.setFocus(firstError) return } if (result.code === 403) { toast.error( result.error === 'demo_readonly' ? 'Demo-modus: opslaan uitgeschakeld' : 'Geen toegang', ) return } toast.error('Er ging iets mis. Probeer het opnieuw.', { action: { label: 'Opnieuw', onClick: () => form.handleSubmit(onSubmit)() }, }) }) } function handleDelete() { if (!task) return setConfirmDelete(false) startTransition(async () => { const result = await deleteTask(task.id, { productId }) if (result.ok) { toast.success('Taak verwijderd') close() return } if (result.code === 403) { toast.error( result.error === 'demo_readonly' ? 'Demo-modus: verwijderen uitgeschakeld' : 'Geen toegang', ) return } toast.error('Verwijderen mislukt') }) } return ( <> { if (!open) closeGuard.attemptClose() }}> {/* Sticky header */}
{isEdit ? 'Taak bewerken' : 'Nieuwe taak'} {isEdit && task?.code && }
{isEdit && ( Aangemaakt:{' '} {new Intl.DateTimeFormat('nl-NL', { day: 'numeric', month: 'short', year: 'numeric', }).format(new Date(task.created_at))} )}
{/* Scrollable form body */}
{/* Code */}
{ if (e.key === 'Enter') e.preventDefault() }} /> {form.formState.errors.code && (

{form.formState.errors.code.message}

)}
{/* Title */}
{ if (e.key === 'Enter') e.preventDefault() }} /> {form.formState.errors.title && (

{form.formState.errors.title.message}

)}
{/* Description */}
( <>

Markdown ondersteund (lijstjes, **vet**, `code`)

)} /> {form.formState.errors.description && (

{form.formState.errors.description.message}

)}
{/* Implementation plan */}
( <>

Markdown ondersteund (lijstjes, **vet**, `code`)

)} /> {form.formState.errors.implementation_plan && (

{form.formState.errors.implementation_plan.message}

)}
{/* Priority */}
( )} />
{/* Status — edit only */} {isEdit && (
( )} />
)}
{/* Sticky footer */}
{isEdit ? ( ) : (
)}
{/* Dirty-check confirm */} {/* Delete confirm */} Taak verwijderen Weet je zeker dat je deze taak wilt verwijderen? Dit kan niet ongedaan worden gemaakt. setConfirmDelete(false)}> Annuleren {isPending ? : 'Verwijderen'} ) }