Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-20 00:13:53 +00:00
co-authored by renee-png
parent 05d14f2efa
commit c8659f3e91
@@ -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<string>("");
const [mergeIds, setMergeIds] = useState<Set<string>>(new Set());
const [submitting, setSubmitting] = useState(false);
const [counts, setCounts] = useState<Record<string, { entries: number; tasks: number }>>({});
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<string, { entries: number; tasks: number }> = {};
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle className="font-serif flex items-center gap-2">
<Merge className="h-4 w-4" /> Merge collections on this case
</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
{collections.length < 2 ? (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
This case needs at least 2 collections to merge.
</AlertDescription>
</Alert>
) : (
<>
<div>
<Label>Keep this collection (target)</Label>
<Select value={keepId} onValueChange={setKeepId}>
<SelectTrigger>
<SelectValue placeholder="Pick the kept ledger" />
</SelectTrigger>
<SelectContent>
{collections.map((c) => (
<SelectItem key={c.id} value={c.id}>
{labelOf(c)}
{counts[c.id]
? ` · ${counts[c.id].entries} entries · ${counts[c.id].tasks} tasks`
: ""}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Merge these into the kept ledger</Label>
<div className="border rounded-md max-h-72 overflow-y-auto mt-1 divide-y">
{sources.length === 0 ? (
<p className="p-3 text-sm text-muted-foreground italic">
No other collections on this case.
</p>
) : (
sources.map((c) => (
<label
key={c.id}
className="flex items-center gap-3 px-3 py-2 text-sm hover:bg-muted/50 cursor-pointer"
>
<Checkbox
checked={mergeIds.has(c.id)}
onCheckedChange={() => toggleSource(c.id)}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium">{labelOf(c)}</span>
<Badge variant="outline" className="text-[10px] capitalize">
{c.status}
</Badge>
</div>
<div className="text-xs text-muted-foreground">
{loadingCounts
? "Loading…"
: `${counts[c.id]?.entries ?? 0} ledger entries · ${counts[c.id]?.tasks ?? 0} tasks`}
</div>
</div>
</label>
))
)}
</div>
</div>
{mergeIds.size > 0 && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
This permanently deletes {mergeIds.size} collection
{mergeIds.size === 1 ? "" : "s"} after moving their data.
It cannot be undone.
</AlertDescription>
</Alert>
)}
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={submit}
disabled={submitting || collections.length < 2 || !keepId || mergeIds.size === 0}
>
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Merge {mergeIds.size} into 1
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}