diff --git a/src/components/cases/convert-to-collections-dialog.tsx b/src/components/cases/convert-to-collections-dialog.tsx new file mode 100644 index 0000000..d349a70 --- /dev/null +++ b/src/components/cases/convert-to-collections-dialog.tsx @@ -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([]); + const [existingIds, setExistingIds] = useState>(new Set()); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [selected, setSelected] = useState>(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 ( + + + + + Convert case to collections + + + Select homeowners from this client to create one collection per homeowner under this case. + + + +
+
+
+ + +
+
+ +
+ {selected.size} of {available.length} available +
+
+
+ +
+ + setSearch(e.target.value)} + className="pl-8" + /> +
+ +
+ {loading ? ( +

Loading…

+ ) : available.length === 0 ? ( +

+ {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."} +

+ ) : ( + <> +
+ + + {allChecked ? "Unselect all" : "Select all visible"} + +
+
    + {filtered.map((h) => ( +
  • toggle(h.id)} + > + toggle(h.id)} + onClick={(e) => e.stopPropagation()} + /> +
    +
    + {h.last_name}, {h.first_name} +
    + {(h.unit_number || h.address) && ( +
    + {h.unit_number ? `Unit ${h.unit_number}` : ""} + {h.unit_number && h.address ? " · " : ""} + {h.address ?? ""} +
    + )} +
    +
  • + ))} + {filtered.length === 0 && ( +
  • + No matches. +
  • + )} +
+ + )} +
+
+ + + + + +
+
+ ); +}