From 421c6779270877def7d21548ffa5a7574b640d6f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:22:29 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/calendar.index.tsx | 901 +++++++++++++++++++++++++++------- 1 file changed, 733 insertions(+), 168 deletions(-) diff --git a/src/routes/calendar.index.tsx b/src/routes/calendar.index.tsx index 19d195f..599363a 100644 --- a/src/routes/calendar.index.tsx +++ b/src/routes/calendar.index.tsx @@ -1,4 +1,4 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, Link } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; @@ -10,6 +10,8 @@ import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Card } from "@/components/ui/card"; import { Calendar } from "@/components/ui/calendar"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, @@ -25,32 +27,75 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Plus, Calendar as CalIcon, Briefcase, CheckSquare } from "lucide-react"; -import { format, isSameDay, parseISO, startOfDay } from "date-fns"; +import { + Plus, + Briefcase, + CheckSquare, + ChevronLeft, + ChevronRight, + CalendarDays, + MapPin, +} from "lucide-react"; +import { + format, + isSameDay, + isSameMonth, + parseISO, + startOfMonth, + endOfMonth, + startOfWeek, + endOfWeek, + addDays, + addMonths, + subMonths, + startOfDay, +} from "date-fns"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; -import { Link } from "@tanstack/react-router"; export const Route = createFileRoute("/calendar/")({ component: CalendarPage, }); -interface TaskEvt { +// ---- Event type config ---- +const EVENT_TYPES = [ + { key: "court", label: "Court", color: "var(--chart-1, hsl(0 72% 51%))", bg: "bg-red-500" }, + { key: "deposition", label: "Deposition", color: "var(--chart-2)", bg: "bg-purple-500" }, + { key: "mediation", label: "Mediation", color: "var(--chart-3)", bg: "bg-amber-500" }, + { key: "client_meeting", label: "Client Meeting", color: "var(--chart-4)", bg: "bg-blue-500" }, + { key: "phone_call", label: "Phone Call / Consult", color: "var(--chart-5)", bg: "bg-cyan-500" }, + { key: "hearing", label: "Hearing", color: "", bg: "bg-rose-500" }, + { key: "deadline", label: "Deadline", color: "", bg: "bg-orange-500" }, + { key: "task", label: "Task", color: "", bg: "bg-emerald-500" }, + { key: "other", label: "Other", color: "", bg: "bg-slate-500" }, +] as const; + +type EventTypeKey = (typeof EVENT_TYPES)[number]["key"]; + +const EVENT_TYPE_MAP: Record = Object.fromEntries( + EVENT_TYPES.map((t) => [t.key, t]), +); + +function typeMeta(key: string) { + return EVENT_TYPE_MAP[key] ?? EVENT_TYPE_MAP.other; +} + +// ---- Unified event ---- +interface Evt { id: string; + source: "event" | "task" | "case_hearing"; title: string; - due_date: string; + start: string; // ISO + end: string | null; + all_day: boolean; + event_type: string; case_id: string | null; - kind: "task"; + case_label: string | null; + owner_id: string | null; + owner_name: string | null; + location: string | null; + description: string | null; } -interface HearingEvt { - id: string; // case id - title: string; - due_date: string; - case_id: string; - kind: "hearing"; - notes: string | null; -} -type Evt = TaskEvt | HearingEvt; interface CaseRow { id: string; @@ -58,55 +103,134 @@ interface CaseRow { case_number: string; } +interface StaffRow { + id: string; + full_name: string; + email: string; +} + function CalendarPage() { const { user } = useAuth(); + const [view, setView] = useState<"month" | "week" | "day" | "list">("month"); + const [cursor, setCursor] = useState(new Date()); const [selected, setSelected] = useState(new Date()); + const [events, setEvents] = useState([]); const [cases, setCases] = useState([]); + const [staff, setStaff] = useState([]); const [loading, setLoading] = useState(true); + + // filters + const [activeTypes, setActiveTypes] = useState>( + new Set(EVENT_TYPES.map((t) => t.key)), + ); + const [activeStaff, setActiveStaff] = useState>(new Set()); // empty = all + + // dialog state const [dialogOpen, setDialogOpen] = useState(false); const [saving, setSaving] = useState(false); - - // form const [fTitle, setFTitle] = useState(""); - const [fDate, setFDate] = useState(undefined); + const [fType, setFType] = useState("client_meeting"); + const [fStart, setFStart] = useState(undefined); + const [fStartTime, setFStartTime] = useState("09:00"); + const [fEndTime, setFEndTime] = useState("10:00"); + const [fAllDay, setFAllDay] = useState(false); const [fCase, setFCase] = useState("none"); + const [fLocation, setFLocation] = useState(""); const [fNotes, setFNotes] = useState(""); + const [fAssignee, setFAssignee] = useState("self"); const reload = async () => { setLoading(true); - const [tasksRes, casesRes] = await Promise.all([ + const [eventsRes, tasksRes, casesRes, staffRes] = await Promise.all([ + supabase + .from("events") + .select("id,title,start_at,end_at,all_day,event_type,case_id,location,description,created_by"), supabase .from("tasks") - .select("id,title,due_date,case_id,status") + .select("id,title,due_date,case_id,status,created_by") .not("due_date", "is", null) .eq("status", "incomplete"), supabase .from("cases") .select("id,title,case_number,next_hearing_date,next_hearing_notes") .order("title"), + supabase.from("profiles").select("id,full_name,email").order("full_name"), ]); - const tEvts: Evt[] = (tasksRes.data ?? []).map((t) => ({ - id: t.id, - title: t.title, - due_date: t.due_date as string, - case_id: t.case_id, - kind: "task" as const, - })); - const hEvts: Evt[] = (casesRes.data ?? []) - .filter((c) => c.next_hearing_date) - .map((c) => ({ - id: c.id, - title: `Hearing: ${c.title}`, - due_date: c.next_hearing_date as string, - case_id: c.id, - kind: "hearing" as const, - notes: c.next_hearing_notes, - })); + const profileMap = new Map(); + (staffRes.data ?? []).forEach((p) => + profileMap.set(p.id, { id: p.id, full_name: p.full_name || p.email, email: p.email }), + ); + const caseMap = new Map(); + (casesRes.data ?? []).forEach((c) => + caseMap.set(c.id, { id: c.id, title: c.title, case_number: c.case_number }), + ); - setEvents([...tEvts, ...hEvts]); - setCases((casesRes.data ?? []).map((c) => ({ id: c.id, title: c.title, case_number: c.case_number }))); + const evts: Evt[] = []; + + for (const e of eventsRes.data ?? []) { + const owner = e.created_by ? profileMap.get(e.created_by) : null; + const c = e.case_id ? caseMap.get(e.case_id) : null; + evts.push({ + id: e.id, + source: "event", + title: e.title, + start: e.start_at as string, + end: (e.end_at as string | null) ?? null, + all_day: e.all_day ?? false, + event_type: e.event_type ?? "other", + case_id: e.case_id, + case_label: c ? `${c.case_number} — ${c.title}` : null, + owner_id: e.created_by, + owner_name: owner?.full_name ?? null, + location: e.location ?? null, + description: e.description ?? null, + }); + } + + for (const t of tasksRes.data ?? []) { + const owner = t.created_by ? profileMap.get(t.created_by) : null; + const c = t.case_id ? caseMap.get(t.case_id) : null; + evts.push({ + id: t.id, + source: "task", + title: t.title, + start: `${t.due_date}T00:00:00`, + end: null, + all_day: true, + event_type: "task", + case_id: t.case_id, + case_label: c ? `${c.case_number} — ${c.title}` : null, + owner_id: t.created_by, + owner_name: owner?.full_name ?? null, + location: null, + description: null, + }); + } + + for (const c of casesRes.data ?? []) { + if (!c.next_hearing_date) continue; + evts.push({ + id: c.id, + source: "case_hearing", + title: `Hearing: ${c.title}`, + start: `${c.next_hearing_date}T00:00:00`, + end: null, + all_day: true, + event_type: "hearing", + case_id: c.id, + case_label: `${c.case_number} — ${c.title}`, + owner_id: null, + owner_name: null, + location: null, + description: c.next_hearing_notes, + }); + } + + setEvents(evts); + setCases(Array.from(caseMap.values())); + setStaff(Array.from(profileMap.values())); setLoading(false); }; @@ -114,45 +238,95 @@ function CalendarPage() { reload(); }, []); + const visibleEvents = useMemo(() => { + return events.filter((e) => { + if (!activeTypes.has(e.event_type)) return false; + if (activeStaff.size > 0) { + if (!e.owner_id || !activeStaff.has(e.owner_id)) return false; + } + return true; + }); + }, [events, activeTypes, activeStaff]); + const eventsByDay = useMemo(() => { const m = new Map(); - for (const e of events) { - const key = e.due_date.slice(0, 10); + for (const e of visibleEvents) { + const key = e.start.slice(0, 10); const arr = m.get(key) ?? []; arr.push(e); m.set(key, arr); } + for (const arr of m.values()) { + arr.sort((a, b) => a.start.localeCompare(b.start)); + } return m; - }, [events]); + }, [visibleEvents]); - const dayHasEvent = (date: Date) => eventsByDay.has(format(date, "yyyy-MM-dd")); - - const dayEvents = useMemo(() => { - return events.filter((e) => isSameDay(parseISO(e.due_date), selected)); - }, [events, selected]); + const dayEvents = useMemo( + () => eventsByDay.get(format(selected, "yyyy-MM-dd")) ?? [], + [eventsByDay, selected], + ); const upcoming = useMemo(() => { const today = startOfDay(new Date()); - return [...events] - .filter((e) => parseISO(e.due_date) >= today) - .sort((a, b) => a.due_date.localeCompare(b.due_date)) - .slice(0, 8); - }, [events]); + return [...visibleEvents] + .filter((e) => parseISO(e.start) >= today) + .sort((a, b) => a.start.localeCompare(b.start)) + .slice(0, 10); + }, [visibleEvents]); + + const toggleType = (key: string) => { + setActiveTypes((s) => { + const n = new Set(s); + if (n.has(key)) n.delete(key); + else n.add(key); + return n; + }); + }; + const toggleStaff = (id: string) => { + setActiveStaff((s) => { + const n = new Set(s); + if (n.has(id)) n.delete(id); + else n.add(id); + return n; + }); + }; + + const openCreate = (presetDate?: Date) => { + setFTitle(""); + setFType("client_meeting"); + setFStart(presetDate ?? selected); + setFStartTime("09:00"); + setFEndTime("10:00"); + setFAllDay(false); + setFCase("none"); + setFLocation(""); + setFNotes(""); + setFAssignee("self"); + setDialogOpen(true); + }; const handleCreate = async () => { - if (!user || !fTitle.trim() || !fDate) { + if (!user || !fTitle.trim() || !fStart) { toast.error("Title and date are required"); return; } setSaving(true); - const { error } = await supabase.from("tasks").insert({ + const dateStr = format(fStart, "yyyy-MM-dd"); + const startISO = fAllDay ? `${dateStr}T00:00:00` : `${dateStr}T${fStartTime}:00`; + const endISO = fAllDay ? null : `${dateStr}T${fEndTime}:00`; + const ownerId = fAssignee === "self" ? user.id : fAssignee; + + const { error } = await supabase.from("events").insert({ title: fTitle.trim(), - due_date: format(fDate, "yyyy-MM-dd"), + event_type: fType, + start_at: startISO, + end_at: endISO, + all_day: fAllDay, case_id: fCase === "none" ? null : fCase, + location: fLocation.trim() || null, description: fNotes.trim() || null, - created_by: user.id, - status: "incomplete", - priority: "normal", + created_by: ownerId, }); setSaving(false); if (error) { @@ -161,121 +335,192 @@ function CalendarPage() { } toast.success("Event scheduled"); setDialogOpen(false); - setFTitle(""); - setFDate(undefined); - setFCase("none"); - setFNotes(""); reload(); }; + // ---- Month grid ---- + const monthDays = useMemo(() => { + const start = startOfWeek(startOfMonth(cursor), { weekStartsOn: 0 }); + const end = endOfWeek(endOfMonth(cursor), { weekStartsOn: 0 }); + const days: Date[] = []; + let d = start; + while (d <= end) { + days.push(d); + d = addDays(d, 1); + } + return days; + }, [cursor]); + + const weekDays = useMemo(() => { + const start = startOfWeek(cursor, { weekStartsOn: 0 }); + return Array.from({ length: 7 }, (_, i) => addDays(start, i)); + }, [cursor]); + return ( - - - - - - Schedule event - -
-
- - setFTitle(e.target.value)} - placeholder="Hearing, deadline, meeting…" - /> -
-
- -
- -
- {fDate && ( -

- {format(fDate, "PPP")} -

- )} -
-
- - -
-
- -