"use client"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Trash2, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog"; import { createScheduledTodo, deleteScheduledTodo, getScheduledTodoForEdit, updateScheduledTodo, } from "@/lib/actions/scheduled-todos"; import type { RecurrenceInput } from "@/lib/validation/scheduled-todo"; import { REMIND_OPTIONS } from "@/lib/time-of-day"; const TITLE_MAX = 100; const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; const MONTH_LABELS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; const FREQUENCY_UNIT: Record = { DAILY: "day(s)", WEEKLY: "week(s)", MONTHLY: "month(s)", YEARLY: "year(s)", }; /** "YYYY-MM-DD" for today in the browser's own local calendar -- just a * sensible default for the date input, which the user can change anyway. */ function todayDateKey(): string { const d = new Date(); const localMidnight = new Date(d.getTime() - d.getTimezoneOffset() * 60_000); return localMidnight.toISOString().slice(0, 10); } export function ScheduledTodoDialog({ open, onOpenChange, projectId, scheduledTodoId, onSaved, onDeleted, }: { open: boolean; onOpenChange: (open: boolean) => void; projectId: string | null; // null = creating a new one; set = editing an existing one. scheduledTodoId: string | null; onSaved: () => void; onDeleted: () => void; }) { const [title, setTitle] = useState(""); const [details, setDetails] = useState(""); const [startDate, setStartDate] = useState(todayDateKey()); // "HH:MM" (24-hour) or "" for no time due -- exactly what reads and writes, so it needs no reformatting either way. const [timeDue, setTimeDue] = useState(""); // Only meaningful (and only shown, enabled) while timeDue is set -- // defaults to "When due" the moment a time gets set, since that's what a // to-do with a time due already did before "Remind me" existed. Null = // "Don't remind me", picked explicitly. const [remindMinutesBefore, setRemindMinutesBefore] = useState(0); const [isRecurring, setIsRecurring] = useState(false); const [frequency, setFrequency] = useState("WEEKLY"); const [interval, setInterval] = useState(1); const [daysOfWeek, setDaysOfWeek] = useState([]); const [dayOfMonth, setDayOfMonth] = useState(1); const [month, setMonth] = useState(1); const [endType, setEndType] = useState<"never" | "until" | "count">("never"); const [endDate, setEndDate] = useState(""); const [endCount, setEndCount] = useState(10); const [loading, setLoading] = useState(false); const [pending, setPending] = useState(false); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); useEffect(() => { if (!open) return; if (!scheduledTodoId) { setTitle(""); setDetails(""); setStartDate(todayDateKey()); setTimeDue(""); setRemindMinutesBefore(0); setIsRecurring(false); setFrequency("WEEKLY"); setInterval(1); setDaysOfWeek([]); setDayOfMonth(1); setMonth(1); setEndType("never"); setEndDate(""); setEndCount(10); return; } setLoading(true); getScheduledTodoForEdit(scheduledTodoId) .then((todo) => { setTitle(todo.title); setDetails(todo.details ?? ""); setStartDate(todo.startDate); setTimeDue(todo.timeDue ?? ""); setRemindMinutesBefore(todo.timeDue ? todo.remindMinutesBefore : 0); const recurrence = todo.recurrence; setIsRecurring(!!recurrence); setFrequency(recurrence?.frequency ?? "WEEKLY"); setInterval(recurrence?.interval ?? 1); setDaysOfWeek(recurrence?.daysOfWeek ?? []); setDayOfMonth(recurrence?.dayOfMonth ?? 1); setMonth(recurrence?.month ?? 1); setEndType(recurrence?.end.type ?? "never"); setEndDate(recurrence?.end.type === "until" ? recurrence.end.date : ""); setEndCount(recurrence?.end.type === "count" ? recurrence.end.count : 10); }) .catch(() => toast.error("Couldn't load scheduled to-do.")) .finally(() => setLoading(false)); }, [open, scheduledTodoId]); function toggleDayOfWeek(day: number) { setDaysOfWeek((prev) => (prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day].sort())); } function buildRecurrence(): RecurrenceInput | undefined { if (!isRecurring) return undefined; return { frequency, interval, daysOfWeek: frequency === "WEEKLY" ? daysOfWeek : undefined, dayOfMonth: frequency === "MONTHLY" || frequency === "YEARLY" ? dayOfMonth : undefined, month: frequency === "YEARLY" ? month : undefined, end: endType === "until" ? { type: "until", date: endDate } : endType === "count" ? { type: "count", count: endCount } : { type: "never" }, }; } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const trimmedTitle = title.trim(); if (!trimmedTitle) return; if (isRecurring && frequency === "WEEKLY" && daysOfWeek.length === 0) { toast.error("Pick at least one day of the week."); return; } if (isRecurring && endType === "until" && !endDate) { toast.error("Pick an end date."); return; } setPending(true); try { if (scheduledTodoId) { await updateScheduledTodo(scheduledTodoId, { title: trimmedTitle, details: details.trim() || null, startDate, recurrence: buildRecurrence() ?? null, timeDue: timeDue || null, remindMinutesBefore: timeDue ? remindMinutesBefore : null, }); } else { await createScheduledTodo( { projectId }, { title: trimmedTitle, details: details.trim() || undefined, startDate, recurrence: buildRecurrence(), timeDue: timeDue || undefined, remindMinutesBefore: timeDue ? remindMinutesBefore : null, } ); } onSaved(); onOpenChange(false); } catch { toast.error(`Couldn't ${scheduledTodoId ? "update" : "add"} scheduled to-do. Try again.`); } finally { setPending(false); } } async function handleDelete() { if (!scheduledTodoId) return; setPending(true); try { await deleteScheduledTodo(scheduledTodoId); onDeleted(); onOpenChange(false); } catch { toast.error("Couldn't delete scheduled to-do. Try again."); } finally { setPending(false); } } return ( <> {scheduledTodoId ? "Edit scheduled to-do" : "Add scheduled to-do"} {loading ? (

Loading…

) : (
setTitle(e.target.value)} placeholder="e.g. Take out the trash" />