Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 07:27:38 +00:00
co-authored by renee-png
parent 0584792924
commit dec6619db5
@@ -0,0 +1,266 @@
import { useEffect, useMemo, useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { toast } from "sonner";
import { Loader2, Search, Users } from "lucide-react";
const STATUS_OPTIONS = [
{ value: "late_notice", label: "Late Notice" },
{ value: "second_notice", label: "Second Notice" },
{ value: "intent_to_lien", label: "Intent to Lien" },
{ value: "attorney", label: "Attorney" },
{ value: "payment_plan", label: "Payment Plan" },
{ value: "resolved", label: "Resolved" },
];
export function ConvertToCollectionsDialog({
open,
onOpenChange,
caseId,
clientId,
onCreated,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
caseId: string;
clientId: string;
onCreated?: () => void;
}) {
const { user } = useAuth();
const [homeowners, setHomeowners] = useState<any[]>([]);
const [existingIds, setExistingIds] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [search, setSearch] = useState("");
const [status, setStatus] = useState("late_notice");
useEffect(() => {
if (!open) return;
setSelected(new Set());
setSearch("");
setStatus("late_notice");
(async () => {
setLoading(true);
const [{ data: hos }, { data: cols }] = await Promise.all([
supabase
.from("homeowners")
.select("id, first_name, last_name, unit_number, address, opening_balance")
.eq("client_id", clientId)
.is("archived_at", null)
.order("last_name"),
supabase
.from("collections")
.select("homeowner_id")
.eq("case_id", caseId),
]);
setHomeowners(hos ?? []);
setExistingIds(new Set((cols ?? []).map((c) => c.homeowner_id)));
setLoading(false);
})();
}, [open, caseId, clientId]);
const available = useMemo(
() => homeowners.filter((h) => !existingIds.has(h.id)),
[homeowners, existingIds],
);
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return available;
return available.filter((h) =>
`${h.last_name} ${h.first_name} ${h.unit_number ?? ""} ${h.address ?? ""}`
.toLowerCase()
.includes(q),
);
}, [available, search]);
const toggle = (id: string) => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const toggleAll = () => {
if (filtered.every((h) => selected.has(h.id))) {
setSelected((prev) => {
const next = new Set(prev);
filtered.forEach((h) => next.delete(h.id));
return next;
});
} else {
setSelected((prev) => {
const next = new Set(prev);
filtered.forEach((h) => next.add(h.id));
return next;
});
}
};
const submit = async () => {
if (selected.size === 0) {
toast.error("Pick at least one homeowner");
return;
}
setSubmitting(true);
const rows = Array.from(selected).map((homeowner_id) => ({
case_id: caseId,
homeowner_id,
status,
created_by: user?.id,
}));
const { error } = await supabase.from("collections").insert(rows);
setSubmitting(false);
if (error) {
toast.error("Could not create collections", { description: error.message });
return;
}
toast.success(
`Created ${rows.length} collection${rows.length === 1 ? "" : "s"}`,
);
onOpenChange(false);
onCreated?.();
};
const allChecked = filtered.length > 0 && filtered.every((h) => selected.has(h.id));
const someChecked = filtered.some((h) => selected.has(h.id));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle className="font-serif flex items-center gap-2">
<Users className="h-4 w-4" /> Convert case to collections
</DialogTitle>
<DialogDescription>
Select homeowners from this client to create one collection per homeowner under this case.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Initial status</Label>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Selected</Label>
<div className="h-9 px-3 flex items-center text-sm border rounded-md bg-muted/40">
{selected.size} of {available.length} available
</div>
</div>
</div>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search homeowners…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-8"
/>
</div>
<div className="border rounded-md max-h-80 overflow-y-auto">
{loading ? (
<p className="p-4 text-sm text-muted-foreground">Loading…</p>
) : available.length === 0 ? (
<p className="p-4 text-sm text-muted-foreground italic">
{homeowners.length === 0
? "No homeowners on this client yet — add some on the client page first."
: "All homeowners on this client already have a collection on this case."}
</p>
) : (
<>
<div className="flex items-center gap-2 px-3 py-2 border-b bg-muted/30 text-xs">
<Checkbox
checked={allChecked ? true : someChecked ? "indeterminate" : false}
onCheckedChange={toggleAll}
/>
<span className="text-muted-foreground">
{allChecked ? "Unselect all" : "Select all visible"}
</span>
</div>
<ul className="divide-y">
{filtered.map((h) => (
<li
key={h.id}
className="flex items-center gap-3 px-3 py-2 text-sm hover:bg-muted/50 cursor-pointer"
onClick={() => toggle(h.id)}
>
<Checkbox
checked={selected.has(h.id)}
onCheckedChange={() => toggle(h.id)}
onClick={(e) => e.stopPropagation()}
/>
<div className="flex-1">
<div className="font-medium">
{h.last_name}, {h.first_name}
</div>
{(h.unit_number || h.address) && (
<div className="text-xs text-muted-foreground">
{h.unit_number ? `Unit ${h.unit_number}` : ""}
{h.unit_number && h.address ? " · " : ""}
{h.address ?? ""}
</div>
)}
</div>
</li>
))}
{filtered.length === 0 && (
<li className="p-3 text-sm text-muted-foreground italic">
No matches.
</li>
)}
</ul>
</>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={submit} disabled={submitting || selected.size === 0}>
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Create {selected.size > 0 ? selected.size : ""} collection{selected.size === 1 ? "" : "s"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}