'use client' import { useEffect, useRef, useState, useTransition } from 'react' import { useActionState } from 'react' import { Markdown } from '@/components/markdown' import { toast } from 'sonner' import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' import { PrioritySelect, PRIORITY_LABELS, PRIORITY_COLORS } from '@/components/shared/priority-select' import { StoryLog } from '@/components/shared/story-log' 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 { entityDialogContentClasses, entityDialogFooterClasses, entityDialogHeaderClasses, } from '@/components/shared/entity-dialog-layout' import { createStoryAction, updateStoryAction, deleteStoryAction, getStoryLogsAction } from '@/actions/stories' import { cn } from '@/lib/utils' import type { Story } from './story-panel' export type StoryDialogState = | { mode: 'create'; pbiId: string; productId: string; defaultPriority?: number } | { mode: 'edit'; story: Story; productId: string } interface StoryDialogProps { state: StoryDialogState | null onClose: () => void isDemo?: boolean } interface ActionResult { success?: boolean error?: string code?: number fieldErrors?: Record story?: unknown } 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 function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps) { const isEdit = state?.mode === 'edit' const story = isEdit ? (state as Extract).story : null const createState_ = isEdit ? null : (state as Extract | null) const [priority, setPriority] = useState(story?.priority ?? createState_?.defaultPriority ?? 2) const [confirmDelete, setConfirmDelete] = useState(false) const [isDeleting, startDeleteTransition] = useTransition() const [logs, setLogs] = useState> | null>(null) const [dirty, setDirty] = useState(false) const formRef = useRef(null) useEffect(() => { if (!state) return // eslint-disable-next-line react-hooks/set-state-in-effect setConfirmDelete(false) setDirty(false) if (state.mode === 'edit') { setPriority(state.story.priority) setLogs(null) getStoryLogsAction(state.story.id).then(setLogs) } else { setPriority(state.defaultPriority ?? 2) } }, [state]) const [createResult, createAction, createPending] = useActionState( async (_prev, fd) => { const result = await createStoryAction(_prev, fd) as ActionResult if (result?.success) { toast.success('Story aangemaakt'); onClose() } else if (result?.code !== 422 && result?.error) toast.error(result.error) return result }, undefined, ) const [updateResult, updateAction, updatePending] = useActionState( async (_prev, fd) => { const result = await updateStoryAction(_prev, fd) as ActionResult if (result?.success) { toast.success('Story opgeslagen'); onClose() } else if (result?.code !== 422 && result?.error) toast.error(result.error) return result }, undefined, ) const pending = isEdit ? updatePending : createPending const activeResult = isEdit ? updateResult : createResult const fieldError = (field: string) => activeResult?.fieldErrors?.[field]?.[0] function handleDelete() { if (!story) return setConfirmDelete(false) startDeleteTransition(async () => { const result = await deleteStoryAction(story.id) if (result && 'error' in result) toast.error(result.error ?? 'Verwijderen mislukt') else toast.success('Story verwijderd') onClose() }) } const titleRef = useRef(null) useEffect(() => { if (state) setTimeout(() => titleRef.current?.focus(), 50) }, [state]) const closeGuard = useDirtyCloseGuard(dirty, onClose) const handleKeyDown = useDialogSubmitShortcut(() => formRef.current?.requestSubmit()) const showForm = !isDemo || !isEdit return ( <> { if (!open) closeGuard.attemptClose() }}>
{isEdit ? story!.title : 'Nieuwe story'} {isEdit && story!.code && ( {story!.code} )}
{isEdit && (
{PRIORITY_LABELS[priority]} {STATUS_LABELS[story!.status]}
)}
setDirty(true)} className="flex-1 overflow-y-auto" > {isEdit && } {!isEdit && ( <> )} {showForm ? (
{fieldError('code') &&

{fieldError('code')}

}
{fieldError('title') &&

{fieldError('title')}

}
Prioriteit { setPriority(v); setDirty(true) }} />