- useSprintStore met sprintStoryOrder/taskOrder (ST-301) - Sprint aanmaken modal met Sprint Goal validatie (ST-302) - Sprint Backlog pagina SplitPane layout met Sprint Goal header (ST-303) - Stories toevoegen aan Sprint via knop in rechterpaneel (ST-304) - Sprint Backlog volgorde aanpassen via dnd-kit (ST-305) - Story uit Sprint verwijderen met status terug naar OPEN (ST-306) - Sprint Planning pagina SplitPane met story selectie (ST-307) - Taken aanmaken inline in rechterpaneel (ST-308) - Taak drag-and-drop verticaal met optimistische update (ST-309) - Taakstatus toggle TO_DO/IN_PROGRESS/DONE met voortgangsindicator (ST-310) - Taak inline bewerken en verwijderen (ST-311) - Sprint afronden dialoog met per-story Done/Terug keuze (ST-312) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useActionState } from 'react'
|
|
import { useFormStatus } from 'react-dom'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { createSprintAction } from '@/actions/sprints'
|
|
|
|
interface StartSprintButtonProps {
|
|
productId: string
|
|
}
|
|
|
|
function SubmitButton() {
|
|
const { pending } = useFormStatus()
|
|
return (
|
|
<Button type="submit" disabled={pending}>
|
|
{pending ? 'Aanmaken…' : 'Sprint starten'}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
export function StartSprintButton({ productId }: StartSprintButtonProps) {
|
|
const [open, setOpen] = useState(false)
|
|
const router = useRouter()
|
|
|
|
const [state, formAction] = useActionState(
|
|
async (_prev: unknown, fd: FormData) => {
|
|
const result = await createSprintAction(_prev, fd)
|
|
if (result.success) {
|
|
setOpen(false)
|
|
router.push(`/products/${productId}/sprint`)
|
|
}
|
|
return result
|
|
},
|
|
undefined
|
|
)
|
|
|
|
const globalError = typeof state?.error === 'string' ? state.error : undefined
|
|
|
|
return (
|
|
<>
|
|
<Button size="sm" onClick={() => setOpen(true)}>
|
|
Sprint starten
|
|
</Button>
|
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Nieuwe Sprint starten</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form action={formAction} className="space-y-4 p-1">
|
|
<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
|
|
/>
|
|
{typeof state?.error === 'object' && (state.error as Record<string, string[]>).sprint_goal && (
|
|
<p className="text-xs text-error">{(state.error as Record<string, string[]>).sprint_goal[0]}</p>
|
|
)}
|
|
</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>
|
|
)}
|
|
|
|
<div className="flex gap-2 justify-end">
|
|
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
|
Annuleren
|
|
</Button>
|
|
<SubmitButton />
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|