"use client"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { ChevronLeft, ChevronRight } from "lucide-react"; import type { AttendanceStatus } from "@prisma/client"; import { cn } from "@/lib/utils"; import { saveAttendance } from "./actions"; type Assignment = { id: string; crewName: string; rank: string; location: string; marks: Record }; const INPUT = "rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"; // Tap cycle (§8.10): Unmarked → Present → Absent → Leave → Half day → Unmarked. const CYCLE: (AttendanceStatus | null)[] = [null, "PRESENT", "ABSENT", "ON_LEAVE", "HALF_DAY"]; const next = (s: AttendanceStatus | null) => CYCLE[(CYCLE.indexOf(s ?? null) + 1) % CYCLE.length]; const CELL: Record = { PRESENT: "bg-success-100 text-success-700 border-success-200", ABSENT: "bg-danger-100 text-danger-700 border-danger-200", ON_LEAVE: "bg-warning-100 text-warning-700 border-warning-200", HALF_DAY: "bg-primary-100 text-primary-700 border-primary-200", SIGN_OFF: "bg-neutral-200 text-neutral-600 border-neutral-300", }; const ABBR: Record = { PRESENT: "P", ABSENT: "A", ON_LEAVE: "L", HALF_DAY: "½", SIGN_OFF: "S" }; const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"]; const iso = (y: number, m: number, d: number) => `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; export function AttendanceCalendar({ assignments, canEdit }: { assignments: Assignment[]; canEdit: boolean }) { const router = useRouter(); const today = new Date(); const [selectedId, setSelectedId] = useState(assignments[0]?.id ?? ""); const [y, setY] = useState(today.getFullYear()); const [m, setM] = useState(today.getMonth()); const [edits, setEdits] = useState>>({}); const [pending, setPending] = useState(false); const selected = assignments.find((a) => a.id === selectedId) ?? null; const myEdits = edits[selectedId] ?? {}; const statusOf = (date: string): AttendanceStatus | null => { if (date in myEdits) return myEdits[date]; return selected?.marks[date] ?? null; }; const daysInMonth = new Date(y, m + 1, 0).getDate(); const firstWeekday = new Date(y, m, 1).getDay(); const days = useMemo(() => Array.from({ length: daysInMonth }, (_, i) => i + 1), [daysInMonth]); const summary = useMemo(() => { let present = 0, absent = 0, leave = 0; for (const d of days) { const s = (date => (date in myEdits ? myEdits[date] : selected?.marks[date] ?? null))(iso(y, m, d)); if (s === "PRESENT") present++; else if (s === "ABSENT") absent++; else if (s === "ON_LEAVE") leave++; } return { present, absent, leave }; }, [days, myEdits, selected, y, m]); const unmarkedToDate = useMemo(() => { const isCurrentOrPast = y < today.getFullYear() || (y === today.getFullYear() && m <= today.getMonth()); if (!isCurrentOrPast) return 0; const lastDay = (y === today.getFullYear() && m === today.getMonth()) ? today.getDate() : daysInMonth; let n = 0; for (let d = 1; d <= lastDay; d++) if (statusOf(iso(y, m, d)) === null) n++; return n; }, [y, m, daysInMonth, myEdits, selected]); // eslint-disable-line react-hooks/exhaustive-deps const dirty = Object.keys(myEdits).length > 0; function cycleDay(date: string) { if (!canEdit) return; setEdits((e) => ({ ...e, [selectedId]: { ...(e[selectedId] ?? {}), [date]: next(statusOf(date)) } })); } function shiftMonth(delta: number) { const nm = m + delta; if (nm < 0) { setM(11); setY(y - 1); } else if (nm > 11) { setM(0); setY(y + 1); } else setM(nm); } async function save() { setPending(true); // Null edits (cleared cells) are skipped — clearing a saved mark isn't supported here. const marks = Object.entries(myEdits).filter(([, s]) => s !== null).map(([date, status]) => ({ date, status: status as AttendanceStatus })); const res = await saveAttendance(selectedId, marks); setPending(false); if ("ok" in res) { setEdits((e) => ({ ...e, [selectedId]: {} })); router.refresh(); } } if (assignments.length === 0) { return (

Attendance

No active crew to mark attendance for.

); } return (

Attendance

{canEdit && ( )}
{MONTHS[m]} {y}
{unmarkedToDate > 0 && (
{unmarkedToDate} day{unmarkedToDate === 1 ? "" : "s"} still need marking.
)}
{([["Present", summary.present], ["Absent", summary.absent], ["On leave", summary.leave]] as const).map(([k, v]) => (

{v}

{k}

))}
{["Sun","Mon","Tue","Wed","Thu","Fri","Sat"].map((d) =>
{d}
)}
{Array.from({ length: firstWeekday }).map((_, i) =>
)} {days.map((d) => { const date = iso(y, m, d); const s = statusOf(date); return ( ); })}
Present Absent Leave Half day
{!canEdit &&

View only — attendance is marked by site staff.

}
); }