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:
+176
-41
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user