177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { createSprint } from "@/app/(frontend)/projects/actions";
|
|
|
|
interface CreateSprintDialogProps {
|
|
readonly projectId: number;
|
|
readonly open: boolean;
|
|
readonly onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
export function CreateSprintDialog({ projectId, open, onOpenChange }: CreateSprintDialogProps) {
|
|
const router = useRouter();
|
|
const [name, setName] = useState("");
|
|
const [goal, setGoal] = useState("");
|
|
const [startDate, setStartDate] = useState("");
|
|
const [endDate, setEndDate] = useState("");
|
|
const [capacity, setCapacity] = useState("");
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
function reset() {
|
|
setName("");
|
|
setGoal("");
|
|
setStartDate("");
|
|
setEndDate("");
|
|
setCapacity("");
|
|
setPending(false);
|
|
setError(null);
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const trimmedName = name.trim();
|
|
const parsedCapacity = capacity ? Number(capacity) : null;
|
|
if (!trimmedName) {
|
|
setError("Sprint name is required.");
|
|
return;
|
|
}
|
|
if (parsedCapacity !== null && (!Number.isInteger(parsedCapacity) || parsedCapacity < 0)) {
|
|
setError("Capacity must be a whole number of points.");
|
|
return;
|
|
}
|
|
|
|
setPending(true);
|
|
setError(null);
|
|
try {
|
|
const result = await createSprint({
|
|
projectId,
|
|
name: trimmedName,
|
|
goal: goal.trim() || undefined,
|
|
startDate: startDate || undefined,
|
|
endDate: endDate || undefined,
|
|
capacity: parsedCapacity ?? undefined,
|
|
});
|
|
if (!result.success) {
|
|
setError(result.error ?? "Could not create the sprint.");
|
|
setPending(false);
|
|
return;
|
|
}
|
|
onOpenChange(false);
|
|
reset();
|
|
toast.success("Sprint created");
|
|
router.refresh();
|
|
} catch (caught) {
|
|
setError(caught instanceof Error ? caught.message : "Could not create the sprint.");
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(nextOpen) => {
|
|
onOpenChange(nextOpen);
|
|
if (!nextOpen) reset();
|
|
}}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create sprint</DialogTitle>
|
|
<DialogDescription>Define a focused delivery window for this project.</DialogDescription>
|
|
</DialogHeader>
|
|
<form
|
|
className="flex flex-col gap-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void handleSubmit();
|
|
}}
|
|
>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="sprint-name">Name</Label>
|
|
<Input
|
|
id="sprint-name"
|
|
placeholder="Sprint 01"
|
|
value={name}
|
|
onChange={(event) => setName(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="sprint-goal">Goal</Label>
|
|
<Textarea
|
|
id="sprint-goal"
|
|
rows={3}
|
|
placeholder="What should this sprint deliver?"
|
|
value={goal}
|
|
onChange={(event) => setGoal(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="sprint-start">Start date</Label>
|
|
<Input
|
|
id="sprint-start"
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(event) => setStartDate(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="sprint-end">End date</Label>
|
|
<Input
|
|
id="sprint-end"
|
|
type="date"
|
|
value={endDate}
|
|
onChange={(event) => setEndDate(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="sprint-capacity">Capacity</Label>
|
|
<Input
|
|
id="sprint-capacity"
|
|
type="number"
|
|
min={0}
|
|
step={1}
|
|
placeholder="40"
|
|
value={capacity}
|
|
onChange={(event) => setCapacity(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{error && (
|
|
<p role="alert" className="text-sm text-destructive">
|
|
{error}
|
|
</p>
|
|
)}
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={pending}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={pending}>
|
|
{pending ? "Creating..." : "Create sprint"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|