Fixed calendar show-all events

X-Lovable-Edit-ID: edt-8019b6c0-9f3a-4f4f-9f9e-0bdbec6413a7
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 21:01:50 +00:00
co-authored by renee-png
+176 -41
View File
@@ -35,6 +35,7 @@ import {
ChevronRight,
CalendarDays,
MapPin,
Trash2,
} from "lucide-react";
import {
format,
@@ -93,6 +94,8 @@ interface Evt {
event_type: string;
case_id: string | null;
case_label: string | null;
client_id: string | null;
client_label: string | null;
owner_id: string | null;
owner_name: string | null;
location: string | null;
@@ -105,6 +108,11 @@ interface CaseRow {
case_number: string;
}
interface ClientRow {
id: string;
name: string;
}
interface StaffRow {
id: string;
full_name: string;
@@ -119,6 +127,7 @@ function CalendarPage() {
const [events, setEvents] = useState<Evt[]>([]);
const [cases, setCases] = useState<CaseRow[]>([]);
const [clients, setClients] = useState<ClientRow[]>([]);
const [staff, setStaff] = useState<StaffRow[]>([]);
const [loading, setLoading] = useState(true);
@@ -130,6 +139,7 @@ function CalendarPage() {
// dialog state
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [fTitle, setFTitle] = useState("");
const [fType, setFType] = useState<EventTypeKey>("client_meeting");
@@ -138,25 +148,40 @@ function CalendarPage() {
const [fEndTime, setFEndTime] = useState("10:00");
const [fAllDay, setFAllDay] = useState(false);
const [fCase, setFCase] = useState<string>("none");
const [fClient, setFClient] = useState<string>("none");
const [fLocation, setFLocation] = useState("");
const [fNotes, setFNotes] = useState("");
const [fAssignee, setFAssignee] = useState<string>("self");
const reload = async () => {
setLoading(true);
const [eventsRes, tasksRes, casesRes, staffRes] = await Promise.all([
// Pull a wide window around the cursor so month/week/list views are populated.
// (Avoids hitting Supabase's default 1000-row cap on large event tables.)
const winStart = format(addMonths(startOfMonth(cursor), -6), "yyyy-MM-dd");
const winEnd = format(addMonths(endOfMonth(cursor), 12), "yyyy-MM-dd");
const [eventsRes, tasksRes, casesRes, clientsRes, staffRes] = await Promise.all([
supabase
.from("events")
.select("id,title,start_at,end_at,all_day,event_type,case_id,location,description,created_by"),
.select(
"id,title,start_at,end_at,all_day,event_type,case_id,client_id,location,description,created_by",
)
.gte("start_at", `${winStart}T00:00:00`)
.lte("start_at", `${winEnd}T23:59:59`)
.order("start_at", { ascending: true })
.limit(5000),
supabase
.from("tasks")
.select("id,title,due_date,case_id,status,created_by")
.not("due_date", "is", null)
.eq("status", "incomplete"),
.eq("status", "incomplete")
.gte("due_date", winStart)
.lte("due_date", winEnd),
supabase
.from("cases")
.select("id,title,case_number,next_hearing_date,next_hearing_notes")
.select("id,title,case_number,next_hearing_date,next_hearing_notes,client_id")
.order("title"),
supabase.from("clients").select("id,name").order("name"),
supabase.from("profiles").select("id,full_name,email").order("full_name"),
]);
@@ -164,16 +189,25 @@ function CalendarPage() {
(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<string, CaseRow>();
const caseMap = new Map<string, CaseRow & { client_id: string | null }>();
(casesRes.data ?? []).forEach((c) =>
caseMap.set(c.id, { id: c.id, title: c.title, case_number: c.case_number }),
caseMap.set(c.id, {
id: c.id,
title: c.title,
case_number: c.case_number,
client_id: c.client_id ?? null,
}),
);
const clientMap = new Map<string, ClientRow>();
(clientsRes.data ?? []).forEach((c) => clientMap.set(c.id, { id: c.id, name: c.name }));
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;
const clientId = e.client_id ?? c?.client_id ?? null;
const cl = clientId ? clientMap.get(clientId) : null;
evts.push({
id: e.id,
source: "event",
@@ -184,6 +218,8 @@ function CalendarPage() {
event_type: e.event_type ?? "other",
case_id: e.case_id,
case_label: c ? `${c.case_number} — ${c.title}` : null,
client_id: clientId,
client_label: cl?.name ?? null,
owner_id: e.created_by,
owner_name: owner?.full_name ?? null,
location: e.location ?? null,
@@ -194,6 +230,7 @@ function CalendarPage() {
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;
const cl = c?.client_id ? clientMap.get(c.client_id) : null;
evts.push({
id: t.id,
source: "task",
@@ -204,6 +241,8 @@ function CalendarPage() {
event_type: "task",
case_id: t.case_id,
case_label: c ? `${c.case_number} — ${c.title}` : null,
client_id: c?.client_id ?? null,
client_label: cl?.name ?? null,
owner_id: t.created_by,
owner_name: owner?.full_name ?? null,
location: null,
@@ -213,6 +252,7 @@ function CalendarPage() {
for (const c of casesRes.data ?? []) {
if (!c.next_hearing_date) continue;
const cl = c.client_id ? clientMap.get(c.client_id) : null;
evts.push({
id: c.id,
source: "case_hearing",
@@ -223,6 +263,8 @@ function CalendarPage() {
event_type: "hearing",
case_id: c.id,
case_label: `${c.case_number} — ${c.title}`,
client_id: c.client_id ?? null,
client_label: cl?.name ?? null,
owner_id: null,
owner_name: null,
location: null,
@@ -231,14 +273,16 @@ function CalendarPage() {
}
setEvents(evts);
setCases(Array.from(caseMap.values()));
setCases(Array.from(caseMap.values()).map(({ id, title, case_number }) => ({ id, title, case_number })));
setClients(Array.from(clientMap.values()));
setStaff(Array.from(profileMap.values()));
setLoading(false);
};
useEffect(() => {
reload();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cursor.getFullYear(), cursor.getMonth()]);
const visibleEvents = useMemo(() => {
return events.filter((e) => {
@@ -297,6 +341,7 @@ function CalendarPage() {
};
const openCreate = (presetDate?: Date) => {
setEditingId(null);
setFTitle("");
setFType("client_meeting");
setFStart(presetDate ?? selected);
@@ -304,12 +349,33 @@ function CalendarPage() {
setFEndTime("10:00");
setFAllDay(false);
setFCase("none");
setFClient("none");
setFLocation("");
setFNotes("");
setFAssignee("self");
setDialogOpen(true);
};
const openEdit = (evt: Evt) => {
if (evt.source !== "event") return; // tasks/hearings edit elsewhere
setEditingId(evt.id);
setFTitle(evt.title);
setFType((EVENT_TYPE_MAP[evt.event_type] ? evt.event_type : "other") as EventTypeKey);
const dt = parseISO(evt.start);
setFStart(dt);
setFAllDay(evt.all_day);
if (!evt.all_day) {
setFStartTime(format(dt, "HH:mm"));
setFEndTime(evt.end ? format(parseISO(evt.end), "HH:mm") : format(dt, "HH:mm"));
}
setFCase(evt.case_id ?? "none");
setFClient(evt.client_id ?? "none");
setFLocation(evt.location ?? "");
setFNotes(evt.description ?? "");
setFAssignee(evt.owner_id ?? "self");
setDialogOpen(true);
};
const handleCreate = async () => {
if (!user || !fTitle.trim() || !fStart) {
toast.error("Title and date are required");
@@ -321,23 +387,42 @@ function CalendarPage() {
const endISO = fAllDay ? null : `${dateStr}T${fEndTime}:00`;
const ownerId = fAssignee === "self" ? user.id : fAssignee;
const { error } = await supabase.from("events").insert({
const payload = {
title: fTitle.trim(),
event_type: fType,
start_at: startISO,
end_at: endISO,
all_day: fAllDay,
case_id: fCase === "none" ? null : fCase,
client_id: fClient === "none" ? null : fClient,
location: fLocation.trim() || null,
description: fNotes.trim() || null,
created_by: ownerId,
});
};
const { error } = editingId
? await supabase.from("events").update(payload).eq("id", editingId)
: await supabase.from("events").insert({ ...payload, created_by: ownerId });
setSaving(false);
if (error) {
toast.error(error.message);
return;
}
toast.success("Event scheduled");
toast.success(editingId ? "Event updated" : "Event scheduled");
setDialogOpen(false);
reload();
};
const handleDelete = async () => {
if (!editingId) return;
if (!confirm("Delete this event?")) return;
setSaving(true);
const { error } = await supabase.from("events").delete().eq("id", editingId);
setSaving(false);
if (error) {
toast.error(error.message);
return;
}
toast.success("Event deleted");
setDialogOpen(false);
reload();
};
@@ -510,7 +595,7 @@ function CalendarPage() {
onSelectDay={(d) => setSelected(d)}
/>
) : view === "list" ? (
<ListView events={visibleEvents} />
<ListView events={visibleEvents} onEdit={openEdit} />
) : null}
</Card>
@@ -524,7 +609,7 @@ function CalendarPage() {
) : (
<ul className="space-y-2">
{dayEvents.map((e) => (
<EventRow key={`${e.source}-${e.id}`} evt={e} />
<EventRow key={`${e.source}-${e.id}`} evt={e} onEdit={openEdit} />
))}
</ul>
)}
@@ -537,7 +622,7 @@ function CalendarPage() {
) : (
<ul className="space-y-2">
{upcoming.map((e) => (
<EventRow key={`up-${e.source}-${e.id}`} evt={e} showDate />
<EventRow key={`up-${e.source}-${e.id}`} evt={e} showDate onEdit={openEdit} />
))}
</ul>
)}
@@ -549,7 +634,7 @@ function CalendarPage() {
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Add event</DialogTitle>
<DialogTitle>{editingId ? "Edit event" : "Add event"}</DialogTitle>
</DialogHeader>
<div className="space-y-4 max-h-[70vh] overflow-auto pr-1">
<div>
@@ -637,21 +722,39 @@ function CalendarPage() {
</div>
</div>
)}
<div>
<Label>Related case</Label>
<Select value={fCase} onValueChange={setFCase}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{cases.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.case_number} — {c.title}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Related case</Label>
<Select value={fCase} onValueChange={setFCase}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{cases.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.case_number} — {c.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Related client</Label>
<Select value={fClient} onValueChange={setFClient}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{clients.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label>Location</Label>
@@ -666,13 +769,20 @@ function CalendarPage() {
<Textarea value={fNotes} onChange={(e) => setFNotes(e.target.value)} rows={3} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
Cancel
</Button>
<Button onClick={handleCreate} disabled={saving}>
{saving ? "Saving…" : "Save event"}
</Button>
<DialogFooter className="gap-2 sm:justify-between">
{editingId ? (
<Button variant="ghost" onClick={handleDelete} disabled={saving} className="text-destructive">
<Trash2 className="h-4 w-4 mr-1" /> Delete
</Button>
) : <span />}
<div className="flex gap-2">
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
Cancel
</Button>
<Button onClick={handleCreate} disabled={saving}>
{saving ? "Saving…" : editingId ? "Save changes" : "Save event"}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -836,7 +946,7 @@ function WeekStrip({
);
}
function ListView({ events }: { events: Evt[] }) {
function ListView({ events, onEdit }: { events: Evt[]; onEdit?: (e: Evt) => void }) {
const today = startOfDay(new Date());
const upcoming = [...events]
.filter((e) => parseISO(e.start) >= today)
@@ -859,7 +969,7 @@ function ListView({ events }: { events: Evt[] }) {
</h4>
<ul className="space-y-2">
{evts.map((e) => (
<EventRow key={`${e.source}-${e.id}`} evt={e} />
<EventRow key={`${e.source}-${e.id}`} evt={e} onEdit={onEdit} />
))}
</ul>
</div>
@@ -868,7 +978,15 @@ function ListView({ events }: { events: Evt[] }) {
);
}
function EventRow({ evt, showDate }: { evt: Evt; showDate?: boolean }) {
function EventRow({
evt,
showDate,
onEdit,
}: {
evt: Evt;
showDate?: boolean;
onEdit?: (e: Evt) => void;
}) {
const meta = typeMeta(evt.event_type);
const Icon = evt.source === "task" ? CheckSquare : Briefcase;
const content = (
@@ -887,6 +1005,9 @@ function EventRow({ evt, showDate }: { evt: Evt; showDate?: boolean }) {
{!evt.all_day && <span>{format(parseISO(evt.start), "h:mm a")}</span>}
{evt.all_day && <span>All day</span>}
{evt.case_label && <span className="truncate">· {evt.case_label}</span>}
{evt.client_label && !evt.case_label && (
<span className="truncate">· {evt.client_label}</span>
)}
{evt.owner_name && <span>· {evt.owner_name}</span>}
{evt.location && (
<span className="flex items-center gap-1">
@@ -897,6 +1018,20 @@ function EventRow({ evt, showDate }: { evt: Evt; showDate?: boolean }) {
</div>
</div>
);
// For real "event" rows, clicking opens the edit dialog (preferred over case nav).
if (evt.source === "event" && onEdit) {
return (
<li>
<button
type="button"
onClick={() => onEdit(evt)}
className="w-full text-left"
>
{content}
</button>
</li>
);
}
if (evt.case_id) {
return (
<li>