199 lines
5.8 KiB
TypeScript
199 lines
5.8 KiB
TypeScript
import { useState, useMemo } from "react";
|
|
import { format } from "date-fns";
|
|
import { Plus, CalendarIcon } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Calendar } from "@/components/ui/calendar";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { cn } from "@/lib/utils";
|
|
import { isNonWorkDay } from "@/lib/workdays";
|
|
import type { GeneratedMilestone } from "@/lib/templates";
|
|
|
|
const STAGE_PRESETS = [
|
|
{ label: "Client Review", description: "Client reviews deliverable" },
|
|
{ label: "Revisions", description: "Incorporate feedback and polish" },
|
|
{ label: "Final QA", description: "Quality assurance and sign-off" },
|
|
] as const;
|
|
|
|
interface AddStageDialogProps {
|
|
dueDate: Date;
|
|
holidays: Set<string>;
|
|
onAdd: (milestone: GeneratedMilestone) => void;
|
|
}
|
|
|
|
const AddStageDialog = ({ dueDate, holidays, onAdd }: AddStageDialogProps) => {
|
|
const [open, setOpen] = useState(false);
|
|
const [step, setStep] = useState<"pick" | "custom" | "date">("pick");
|
|
const [label, setLabel] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [date, setDate] = useState<Date | undefined>();
|
|
|
|
const today = useMemo(() => {
|
|
const d = new Date();
|
|
d.setHours(0, 0, 0, 0);
|
|
return d;
|
|
}, []);
|
|
|
|
const reset = () => {
|
|
setStep("pick");
|
|
setLabel("");
|
|
setDescription("");
|
|
setDate(undefined);
|
|
};
|
|
|
|
const handlePreset = (preset: (typeof STAGE_PRESETS)[number]) => {
|
|
setLabel(preset.label);
|
|
setDescription(preset.description);
|
|
setStep("date");
|
|
};
|
|
|
|
const handleCustomNext = () => {
|
|
if (label.trim()) setStep("date");
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (!date || !label.trim()) return;
|
|
const d = new Date(date);
|
|
d.setHours(0, 0, 0, 0);
|
|
onAdd({
|
|
label: label.trim(),
|
|
description: description.trim() || label.trim(),
|
|
date: d,
|
|
daysBeforeDue: Math.round(
|
|
(dueDate.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)
|
|
),
|
|
isPast: d < today,
|
|
});
|
|
reset();
|
|
setOpen(false);
|
|
};
|
|
|
|
return (
|
|
<Popover
|
|
open={open}
|
|
onOpenChange={(v) => {
|
|
setOpen(v);
|
|
if (!v) reset();
|
|
}}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="gap-1.5 text-xs uppercase tracking-wide"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
Add Stage
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-72 p-0" align="start">
|
|
{step === "pick" && (
|
|
<div className="p-3 space-y-1">
|
|
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
|
|
Choose stage type
|
|
</p>
|
|
{STAGE_PRESETS.map((preset) => (
|
|
<button
|
|
key={preset.label}
|
|
onClick={() => handlePreset(preset)}
|
|
className="w-full text-left px-3 py-2 rounded-md text-sm hover:bg-accent transition-colors"
|
|
>
|
|
{preset.label}
|
|
</button>
|
|
))}
|
|
<button
|
|
onClick={() => setStep("custom")}
|
|
className="w-full text-left px-3 py-2 rounded-md text-sm hover:bg-accent transition-colors text-muted-foreground"
|
|
>
|
|
Custom…
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{step === "custom" && (
|
|
<div className="p-3 space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Label</Label>
|
|
<Input
|
|
placeholder="e.g. Internal Review"
|
|
value={label}
|
|
onChange={(e) => setLabel(e.target.value)}
|
|
className="h-8 text-sm"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Description</Label>
|
|
<Input
|
|
placeholder="Optional description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
className="h-8 text-sm"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
reset();
|
|
setStep("pick");
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
Back
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleCustomNext}
|
|
disabled={!label.trim()}
|
|
className="text-xs"
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === "date" && (
|
|
<div className="p-3 space-y-3">
|
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
{label} — Pick a date
|
|
</p>
|
|
<Calendar
|
|
mode="single"
|
|
selected={date}
|
|
onSelect={setDate}
|
|
disabled={(d) => d < today || isNonWorkDay(d, holidays)}
|
|
className={cn("p-0 pointer-events-auto")}
|
|
/>
|
|
<div className="flex gap-2 justify-end">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setStep("pick")}
|
|
className="text-xs"
|
|
>
|
|
Back
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSubmit}
|
|
disabled={!date}
|
|
className="text-xs"
|
|
>
|
|
Add
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
export default AddStageDialog;
|