Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user