From c8659f3e910618a7a2033836fa554555ee44acbd Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:13:53 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- .../cases/merge-collections-dialog.tsx | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/components/cases/merge-collections-dialog.tsx diff --git a/src/components/cases/merge-collections-dialog.tsx b/src/components/cases/merge-collections-dialog.tsx new file mode 100644 index 0000000..d8a7651 --- /dev/null +++ b/src/components/cases/merge-collections-dialog.tsx @@ -0,0 +1,251 @@ +import { useEffect, useMemo, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { supabase } from "@/integrations/supabase/client"; +import { Loader2, Merge, AlertTriangle } from "lucide-react"; +import { toast } from "sonner"; + +interface CollectionRow { + id: string; + status: string; + name: string | null; + homeowner_id: string | null; + homeowner?: { first_name?: string | null; last_name?: string | null; unit_number?: string | null } | null; +} + +export function MergeCollectionsDialog({ + open, + onOpenChange, + caseId, + collections, + onMerged, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; + caseId: string; + collections: CollectionRow[]; + onMerged?: () => void; +}) { + const [keepId, setKeepId] = useState(""); + const [mergeIds, setMergeIds] = useState>(new Set()); + const [submitting, setSubmitting] = useState(false); + const [counts, setCounts] = useState>({}); + const [loadingCounts, setLoadingCounts] = useState(false); + + useEffect(() => { + if (!open) return; + setKeepId(collections[0]?.id ?? ""); + setMergeIds(new Set()); + setCounts({}); + if (collections.length === 0) return; + (async () => { + setLoadingCounts(true); + const ids = collections.map((c) => c.id); + const [{ data: entries }, { data: tasks }] = await Promise.all([ + supabase.from("collection_ledger_entries").select("collection_id").in("collection_id", ids), + supabase.from("collection_tasks").select("collection_id").in("collection_id", ids), + ]); + const next: Record = {}; + for (const id of ids) next[id] = { entries: 0, tasks: 0 }; + for (const e of (entries ?? []) as any[]) next[e.collection_id].entries++; + for (const t of (tasks ?? []) as any[]) next[t.collection_id].tasks++; + setCounts(next); + setLoadingCounts(false); + })(); + }, [open, collections]); + + const labelOf = (c: CollectionRow) => + c.name?.trim() || + (c.homeowner + ? `${c.homeowner.last_name ?? ""}, ${c.homeowner.first_name ?? ""}`.replace(/^,\s*/, "") + : "Unassigned"); + + const sources = useMemo( + () => collections.filter((c) => c.id !== keepId), + [collections, keepId], + ); + + const toggleSource = (id: string) => + setMergeIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + + const submit = async () => { + if (!keepId) { + toast.error("Pick a collection to keep"); + return; + } + if (mergeIds.size === 0) { + toast.error("Select at least one collection to merge in"); + return; + } + setSubmitting(true); + try { + const sourceIds = Array.from(mergeIds); + + const { error: e1 } = await supabase + .from("collection_ledger_entries") + .update({ collection_id: keepId }) + .in("collection_id", sourceIds); + if (e1) throw e1; + + const { error: e2 } = await supabase + .from("collection_payment_allocations") + .update({ collection_id: keepId }) + .in("collection_id", sourceIds); + if (e2) throw e2; + + const { error: e3 } = await supabase + .from("collection_tasks") + .update({ collection_id: keepId }) + .in("collection_id", sourceIds); + if (e3) throw e3; + + const { error: e4 } = await supabase + .from("collections") + .delete() + .in("id", sourceIds); + if (e4) throw e4; + + toast.success( + `Merged ${sourceIds.length} collection${sourceIds.length === 1 ? "" : "s"} into the kept ledger`, + ); + onOpenChange(false); + onMerged?.(); + } catch (err: any) { + toast.error("Could not merge collections", { description: err?.message }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + Merge collections on this case + + + Combine multiple per-homeowner collections into a single ledger. + Ledger entries, payments, and workflow tasks are moved into the + kept collection. The merged-in collections are then deleted. + + + +
+ {collections.length < 2 ? ( + + + + This case needs at least 2 collections to merge. + + + ) : ( + <> +
+ + +
+ +
+ +
+ {sources.length === 0 ? ( +

+ No other collections on this case. +

+ ) : ( + sources.map((c) => ( + + )) + )} +
+
+ + {mergeIds.size > 0 && ( + + + + This permanently deletes {mergeIds.size} collection + {mergeIds.size === 1 ? "" : "s"} after moving their data. + It cannot be undone. + + + )} + + )} +
+ + + + + +
+
+ ); +}