Voegt todayLocalDate()-helper toe (toLocaleDateString('en-CA') voor YYYY-MM-DD
zonder UTC-drift) en gebruikt hem als defaultValue op start_date en end_date.
Dialog unmount bij sluiten zorgt automatisch voor reset naar vandaag bij heropenen.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
5.6 KiB
TypeScript
154 lines
5.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useActionState, useRef } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
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 { createSprintAction } from '@/actions/sprints'
|
|
|
|
interface StartSprintButtonProps {
|
|
productId: string
|
|
isDemo?: boolean
|
|
}
|
|
|
|
interface ActionResult {
|
|
success?: boolean
|
|
error?: string
|
|
code?: number
|
|
fieldErrors?: Record<string, string[]>
|
|
sprintId?: string
|
|
}
|
|
|
|
function todayLocalDate() {
|
|
return new Date().toLocaleDateString('en-CA')
|
|
}
|
|
|
|
export function StartSprintButton({ productId, isDemo = false }: StartSprintButtonProps) {
|
|
const [open, setOpen] = useState(false)
|
|
const [dirty, setDirty] = useState(false)
|
|
const formRef = useRef<HTMLFormElement>(null)
|
|
const router = useRouter()
|
|
|
|
const [state, formAction, pending] = useActionState<ActionResult | undefined, FormData>(
|
|
async (_prev, fd) => {
|
|
const result = await createSprintAction(_prev, fd) as ActionResult
|
|
if (result?.success) {
|
|
setOpen(false)
|
|
setDirty(false)
|
|
router.push(`/products/${productId}/sprint`)
|
|
} else if (result?.code !== 422 && result?.error) {
|
|
// Toast handled by caller; here we just keep the form open
|
|
}
|
|
return result
|
|
},
|
|
undefined,
|
|
)
|
|
|
|
const fieldError = (field: string) => state?.fieldErrors?.[field]?.[0]
|
|
const globalError = state?.code !== 422 ? state?.error : undefined
|
|
|
|
const closeGuard = useDirtyCloseGuard(dirty, () => setOpen(false))
|
|
const handleKeyDown = useDialogSubmitShortcut(() => formRef.current?.requestSubmit())
|
|
|
|
return (
|
|
<>
|
|
<DemoTooltip show={isDemo}>
|
|
<Button size="sm" onClick={() => setOpen(true)} disabled={isDemo}>
|
|
Sprint starten
|
|
</Button>
|
|
</DemoTooltip>
|
|
|
|
<Dialog open={open} onOpenChange={(o) => { if (!o) closeGuard.attemptClose(); else setOpen(o) }}>
|
|
<DialogContent
|
|
showCloseButton={false}
|
|
onKeyDown={handleKeyDown}
|
|
className={entityDialogContentClasses}
|
|
>
|
|
<div className={entityDialogHeaderClasses}>
|
|
<DialogTitle className="text-xl font-semibold">Nieuwe Sprint starten</DialogTitle>
|
|
</div>
|
|
|
|
<form
|
|
ref={formRef}
|
|
id="start-sprint-form"
|
|
action={formAction}
|
|
onChange={() => setDirty(true)}
|
|
className="flex-1 overflow-y-auto px-6 py-6 space-y-6"
|
|
>
|
|
<input type="hidden" name="productId" value={productId} />
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-foreground">
|
|
Sprint Goal <span className="text-error">*</span>
|
|
</label>
|
|
<Textarea
|
|
name="sprint_goal"
|
|
required
|
|
rows={3}
|
|
placeholder="Wat wil je aan het einde van deze Sprint bereikt hebben?"
|
|
autoFocus
|
|
aria-invalid={!!fieldError('sprint_goal')}
|
|
className={fieldError('sprint_goal') ? 'border-error' : ''}
|
|
/>
|
|
{fieldError('sprint_goal') && (
|
|
<p className="text-xs text-error">{fieldError('sprint_goal')}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-foreground">Startdatum</label>
|
|
<input type="date" name="start_date" defaultValue={todayLocalDate()} className="w-full rounded-md border border-border bg-surface-container px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary" />
|
|
{fieldError('start_date') && (
|
|
<p className="text-xs text-error">{fieldError('start_date')}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-medium text-foreground">Einddatum</label>
|
|
<input type="date" name="end_date" defaultValue={todayLocalDate()} className="w-full rounded-md border border-border bg-surface-container px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary" />
|
|
{fieldError('end_date') && (
|
|
<p className="text-xs text-error">{fieldError('end_date')}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{globalError && (
|
|
<div className="bg-error-container text-error-container-foreground rounded-lg px-3 py-2 text-sm border-l-4 border-error">
|
|
{globalError}
|
|
</div>
|
|
)}
|
|
</form>
|
|
|
|
<div className={entityDialogFooterClasses}>
|
|
<div className="flex justify-end gap-2">
|
|
<Button type="button" variant="ghost" onClick={closeGuard.attemptClose} disabled={pending}>
|
|
Annuleren
|
|
</Button>
|
|
<Button type="submit" form="start-sprint-form" disabled={pending}>
|
|
{pending ? 'Aanmaken…' : 'Sprint starten'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<DirtyCloseGuardDialog guard={closeGuard} />
|
|
</>
|
|
)
|
|
}
|