Added bulk event updates

X-Lovable-Edit-ID: edt-665f8020-d963-4fa8-a4ca-e7aed206d108
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 23:18:07 +00:00
co-authored by renee-png
+140 -3
View File
@@ -605,7 +605,12 @@ function CalendarPage() {
onSelectDay={(d) => setSelected(d)}
/>
) : view === "list" ? (
<ListView events={visibleEvents} onEdit={openEdit} />
<ListView
events={visibleEvents}
onEdit={openEdit}
staff={staff}
onBulkUpdated={reload}
/>
) : null}
</Card>
@@ -959,7 +964,17 @@ function WeekStrip({
);
}
function ListView({ events, onEdit }: { events: Evt[]; onEdit?: (e: Evt) => void }) {
function ListView({
events,
onEdit,
staff,
onBulkUpdated,
}: {
events: Evt[];
onEdit?: (e: Evt) => void;
staff: StaffRow[];
onBulkUpdated: () => void | Promise<void>;
}) {
const today = startOfDay(new Date());
const upcoming = [...events]
.filter((e) => parseISO(e.start) >= today)
@@ -971,10 +986,119 @@ function ListView({ events, onEdit }: { events: Evt[]; onEdit?: (e: Evt) => void
arr.push(e);
grouped.set(k, arr);
});
// Bulk-edit state. Only rows with source === "event" can be bulk updated
// because they're the only ones backed by the editable `events` table.
const [selected, setSelected] = useState<Set<string>>(new Set());
const [bulkType, setBulkType] = useState<string>("");
const [bulkAssignee, setBulkAssignee] = useState<string>("");
const [applying, setApplying] = useState(false);
const editableUpcoming = upcoming.filter((e) => e.source === "event");
const allEditableSelected =
editableUpcoming.length > 0 && editableUpcoming.every((e) => selected.has(e.id));
const toggleOne = (id: string) => {
setSelected((s) => {
const n = new Set(s);
if (n.has(id)) n.delete(id);
else n.add(id);
return n;
});
};
const toggleAll = () => {
if (allEditableSelected) setSelected(new Set());
else setSelected(new Set(editableUpcoming.map((e) => e.id)));
};
const applyBulk = async () => {
if (selected.size === 0) {
toast.error("Select at least one event");
return;
}
if (!bulkType && !bulkAssignee) {
toast.error("Choose a category or assignee to update");
return;
}
setApplying(true);
const patch: { event_type?: string; assigned_to?: string } = {};
if (bulkType) patch.event_type = bulkType;
if (bulkAssignee) patch.assigned_to = bulkAssignee;
const { error } = await supabase
.from("events")
.update(patch)
.in("id", Array.from(selected));
setApplying(false);
if (error) {
toast.error(error.message);
return;
}
toast.success(`Updated ${selected.size} event${selected.size === 1 ? "" : "s"}`);
setSelected(new Set());
setBulkType("");
setBulkAssignee("");
await onBulkUpdated();
};
if (upcoming.length === 0)
return <p className="text-sm text-muted-foreground py-8 text-center">No upcoming events.</p>;
return (
<div className="space-y-4">
{/* Bulk action toolbar */}
<div className="flex flex-wrap items-center gap-2 border rounded-md p-2 bg-muted/30">
<label className="flex items-center gap-2 text-xs cursor-pointer pl-1">
<Checkbox
checked={allEditableSelected}
onCheckedChange={toggleAll}
disabled={editableUpcoming.length === 0}
/>
<span className="text-muted-foreground">
{selected.size > 0 ? `${selected.size} selected` : "Select all"}
</span>
</label>
<div className="h-5 w-px bg-border mx-1" />
<Select value={bulkType} onValueChange={setBulkType}>
<SelectTrigger className="h-8 w-[170px] text-xs">
<SelectValue placeholder="Set category…" />
</SelectTrigger>
<SelectContent>
{EVENT_TYPES.map((t) => (
<SelectItem key={t.key} value={t.key}>
<span className="flex items-center gap-2">
<span className={cn("h-2.5 w-2.5 rounded-full", t.bg)} />
{t.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={bulkAssignee} onValueChange={setBulkAssignee}>
<SelectTrigger className="h-8 w-[180px] text-xs">
<SelectValue placeholder="Assign to…" />
</SelectTrigger>
<SelectContent>
{staff.map((s) => (
<SelectItem key={s.id} value={s.id}>
{s.full_name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
onClick={applyBulk}
disabled={applying || selected.size === 0 || (!bulkType && !bulkAssignee)}
>
{applying ? "Applying…" : "Apply"}
</Button>
{selected.size > 0 && (
<Button size="sm" variant="ghost" onClick={() => setSelected(new Set())}>
Clear
</Button>
)}
</div>
{Array.from(grouped.entries()).map(([day, evts]) => (
<div key={day}>
<h4 className="font-serif text-sm font-semibold mb-2">
@@ -982,7 +1106,20 @@ function ListView({ events, onEdit }: { events: Evt[]; onEdit?: (e: Evt) => void
</h4>
<ul className="space-y-2">
{evts.map((e) => (
<EventRow key={`${e.source}-${e.id}`} evt={e} onEdit={onEdit} />
<li key={`${e.source}-${e.id}`} className="flex items-start gap-2">
{e.source === "event" ? (
<Checkbox
className="mt-3 ml-1"
checked={selected.has(e.id)}
onCheckedChange={() => toggleOne(e.id)}
/>
) : (
<span className="w-4 ml-1" />
)}
<div className="flex-1 min-w-0">
<EventRow evt={e} onEdit={onEdit} />
</div>
</li>
))}
</ul>
</div>