From 54617504b48bf7b3210b4c60488fd9e8f4aef871 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 03:16:39 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/app-shell.tsx | 2 +- src/routes/calendar.index.tsx | 333 ++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 src/routes/calendar.index.tsx diff --git a/src/components/app-shell.tsx b/src/components/app-shell.tsx index 6f63b91..e532ac3 100644 --- a/src/components/app-shell.tsx +++ b/src/components/app-shell.tsx @@ -1,7 +1,7 @@ import { Link, useLocation, useNavigate } from "@tanstack/react-router"; import { useAuth } from "@/lib/auth"; import { Button } from "@/components/ui/button"; -import { Briefcase, Users, FileText, Receipt, ShieldCheck, LogOut, Scale, LayoutDashboard } from "lucide-react"; +import { Briefcase, Users, FileText, Receipt, ShieldCheck, LogOut, Scale, LayoutDashboard, Calendar as CalendarIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import type { ReactNode } from "react"; diff --git a/src/routes/calendar.index.tsx b/src/routes/calendar.index.tsx new file mode 100644 index 0000000..6dd5fe7 --- /dev/null +++ b/src/routes/calendar.index.tsx @@ -0,0 +1,333 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect, useMemo, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { ProtectedLayout } from "@/components/protected-layout"; +import { PageContainer, PageHeader } from "@/components/app-shell"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +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 { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + 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 { toast } from "sonner"; +import { cn } from "@/lib/utils"; +import { Link } from "@tanstack/react-router"; + +export const Route = createFileRoute("/calendar/")({ + component: CalendarPage, +}); + +interface TaskEvt { + id: string; + title: string; + due_date: string; + case_id: string | null; + kind: "task"; +} +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; + title: string; + case_number: string; +} + +function CalendarPage() { + const { user } = useAuth(); + const [selected, setSelected] = useState(new Date()); + const [events, setEvents] = useState([]); + const [cases, setCases] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [saving, setSaving] = useState(false); + + // form + const [fTitle, setFTitle] = useState(""); + const [fDate, setFDate] = useState(undefined); + const [fCase, setFCase] = useState("none"); + const [fNotes, setFNotes] = useState(""); + + const reload = async () => { + setLoading(true); + const [tasksRes, casesRes] = await Promise.all([ + supabase + .from("tasks") + .select("id,title,due_date,case_id,status") + .not("due_date", "is", null) + .eq("status", "incomplete"), + supabase + .from("cases") + .select("id,title,case_number,next_hearing_date,next_hearing_notes") + .order("title"), + ]); + + 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, + })); + + setEvents([...tEvts, ...hEvts]); + setCases((casesRes.data ?? []).map((c) => ({ id: c.id, title: c.title, case_number: c.case_number }))); + setLoading(false); + }; + + useEffect(() => { + reload(); + }, []); + + const eventsByDay = useMemo(() => { + const m = new Map(); + for (const e of events) { + const key = e.due_date.slice(0, 10); + const arr = m.get(key) ?? []; + arr.push(e); + m.set(key, arr); + } + return m; + }, [events]); + + 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 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]); + + const handleCreate = async () => { + if (!user || !fTitle.trim() || !fDate) { + toast.error("Title and date are required"); + return; + } + setSaving(true); + const { error } = await supabase.from("tasks").insert({ + title: fTitle.trim(), + due_date: format(fDate, "yyyy-MM-dd"), + case_id: fCase === "none" ? null : fCase, + description: fNotes.trim() || null, + created_by: user.id, + status: "incomplete", + priority: "normal", + }); + setSaving(false); + if (error) { + toast.error(error.message); + return; + } + toast.success("Event scheduled"); + setDialogOpen(false); + setFTitle(""); + setFDate(undefined); + setFCase("none"); + setFNotes(""); + reload(); + }; + + return ( + + + + + + + + + Schedule event + +
+
+ + setFTitle(e.target.value)} + placeholder="Hearing, deadline, meeting…" + /> +
+
+ +
+ +
+ {fDate && ( +

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

+ )} +
+
+ + +
+
+ +