From a33cab379e68833d928c15a776f1de174f5d0c56 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:36:37 +0000 Subject: [PATCH 1/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index a0298cb..9cdcb84 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -140,6 +140,7 @@ export function CaseCollectionsTab({ const [collections, setCollections] = useState([]); const [homeowners, setHomeowners] = useState([]); + const [contactHomeowners, setContactHomeowners] = useState([]); const [balances, setBalances] = useState>({}); const [loading, setLoading] = useState(true); const [activeId, setActiveId] = useState(null); @@ -151,7 +152,7 @@ export function CaseCollectionsTab({ const load = async () => { setLoading(true); - const [{ data: cols }, { data: hos }] = await Promise.all([ + const [{ data: cols }, { data: hos }, { data: ccs }] = await Promise.all([ supabase .from("collections") .select("*, homeowner:homeowners(*)") @@ -162,10 +163,23 @@ export function CaseCollectionsTab({ .select("*") .eq("client_id", clientId) .order("last_name"), + supabase + .from("case_contacts") + .select("role, contact:contacts(*)") + .eq("case_id", caseId), ]); const list = cols ?? []; setCollections(list); setHomeowners(hos ?? []); + // Filter: case_contacts.role = 'homeowner' OR contact.contact_type = 'homeowner' + const hoContacts = (ccs ?? []) + .filter((cc: any) => { + const roleMatch = (cc.role || "").toLowerCase() === "homeowner"; + const typeMatch = (cc.contact?.contact_type || "").toLowerCase() === "homeowner"; + return cc.contact && (roleMatch || typeMatch); + }) + .map((cc: any) => ({ ...cc.contact, _link_role: cc.role })); + setContactHomeowners(hoContacts); // pull balances per collection if (list.length) { From 42a6fda27828de0a96f40e5e6d770c0b2c1ebc4f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:36:46 +0000 Subject: [PATCH 2/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index 9cdcb84..edd6789 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -357,6 +357,7 @@ export function CaseCollectionsTab({ clientId={clientId} caseTitle={caseRecord.title || ""} homeowners={homeowners} + contactHomeowners={contactHomeowners} existingHomeownerIds={collections.map((c) => c.homeowner_id)} onCreated={load} /> From 360867d97a758dc286f9ce007216c88cb33b138c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:37:25 +0000 Subject: [PATCH 3/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 122 ++++++++++++++++++++--- 1 file changed, 106 insertions(+), 16 deletions(-) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index edd6789..8288ea6 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -1087,6 +1087,7 @@ function AddCollectionDialog({ clientId, caseTitle, homeowners, + contactHomeowners, existingHomeownerIds, onCreated, }: { @@ -1096,11 +1097,13 @@ function AddCollectionDialog({ clientId: string; caseTitle: string; homeowners: any[]; + contactHomeowners: any[]; existingHomeownerIds: string[]; onCreated: () => void; }) { const { user } = useAuth(); - const [homeownerId, setHomeownerId] = useState(""); + // Picker value uses prefix: "ho:" for homeowner row, "ct:" for contact + const [pickerValue, setPickerValue] = useState(""); const [status, setStatus] = useState("none"); const [notes, setNotes] = useState(""); const [submitting, setSubmitting] = useState(false); @@ -1109,11 +1112,41 @@ function AddCollectionDialog({ (h) => !existingHomeownerIds.includes(h.id), ); + // Build combined options. Skip contacts that already have a matching homeowner row + // attached as a collection (best-effort dedupe by name+unit). + const homeownerKeySet = new Set( + homeowners + .filter((h) => existingHomeownerIds.includes(h.id)) + .map((h) => + `${(h.first_name || "").toLowerCase().trim()}|${(h.last_name || "").toLowerCase().trim()}|${(h.unit_number || "").toLowerCase().trim()}`, + ), + ); + + const splitName = (full: string) => { + const s = (full || "").trim(); + if (!s) return { first: "", last: "" }; + if (s.includes(",")) { + const [last, first] = s.split(",", 2).map((x) => x.trim()); + return { first: first || "", last: last || "" }; + } + const parts = s.split(/\s+/); + return { first: parts.slice(0, -1).join(" ") || parts[0], last: parts.length > 1 ? parts.at(-1)! : "" }; + }; + + const contactOptions = (contactHomeowners ?? []) + .map((c: any) => { + const { first, last } = splitName(c.name); + return { contact: c, first, last }; + }) + .filter(({ first, last }) => { + const key = `${first.toLowerCase()}|${last.toLowerCase()}|`; + return !homeownerKeySet.has(key); + }); + useEffect(() => { if (open) { - // Try to pre-select a homeowner whose surname matches the case title const match = findUniqueHomeownerByTitle(caseTitle, available); - setHomeownerId(match ? match.id : ""); + setPickerValue(match ? `ho:${match.id}` : ""); setStatus("none"); setNotes(""); } @@ -1121,11 +1154,56 @@ function AddCollectionDialog({ }, [open, caseTitle]); const submit = async () => { - if (!homeownerId) { + if (!pickerValue) { toast.error("Pick a homeowner"); return; } setSubmitting(true); + let homeownerId = ""; + + if (pickerValue.startsWith("ho:")) { + homeownerId = pickerValue.slice(3); + } else if (pickerValue.startsWith("ct:")) { + // Find or create a homeowner row for this contact + const contactId = pickerValue.slice(3); + const contact = contactHomeowners.find((c: any) => c.id === contactId); + if (!contact) { + setSubmitting(false); + toast.error("Contact not found"); + return; + } + const { first, last } = splitName(contact.name); + // Try match an existing homeowner on this client by name + const existing = homeowners.find( + (h) => + (h.first_name || "").toLowerCase().trim() === first.toLowerCase() && + (h.last_name || "").toLowerCase().trim() === last.toLowerCase(), + ); + if (existing) { + homeownerId = existing.id; + } else { + const { data: newHo, error: hoErr } = await supabase + .from("homeowners") + .insert({ + client_id: clientId, + first_name: first || contact.name || "Unknown", + last_name: last || "", + email: contact.email || null, + phone: contact.phone || null, + address: contact.address_line1 || null, + created_by: user?.id, + }) + .select() + .single(); + if (hoErr || !newHo) { + setSubmitting(false); + toast.error("Could not create homeowner", { description: hoErr?.message }); + return; + } + homeownerId = newHo.id; + } + } + const { error } = await supabase.from("collections").insert({ case_id: caseId, homeowner_id: homeownerId, @@ -1143,36 +1221,48 @@ function AddCollectionDialog({ onCreated(); }; + const hoOptions = available.map((h) => ({ + value: `ho:${h.id}`, + label: `${h.last_name}, ${h.first_name}${h.unit_number ? ` — Unit ${h.unit_number}` : ""}`, + keywords: `${h.first_name} ${h.last_name} ${h.unit_number ?? ""}`, + group: "HOA homeowners", + })); + const ctOptions = contactOptions.map(({ contact, first, last }) => ({ + value: `ct:${contact.id}`, + label: `${last || contact.name}${first ? `, ${first}` : ""} — Case contact`, + keywords: `${first} ${last} ${contact.name} ${contact.email ?? ""}`, + group: "Case contacts (homeowner)", + })); + const allOptions = [...hoOptions, ...ctOptions]; + return ( Add homeowner collection - Attach a homeowner from this HOA to this matter. Each homeowner can - only have one collection per case. + Attach a homeowner to this matter. You can pick from this HOA's + homeowners or any case contact tagged as a homeowner. Each + homeowner can only have one collection per case.
- {available.length === 0 ? ( + {allOptions.length === 0 ? (

- {homeowners.length === 0 - ? "No homeowners yet on this HOA. Use 'New homeowner' first." - : "All homeowners on this HOA already have a collection on this matter."} + {homeowners.length === 0 && contactHomeowners.length === 0 + ? "No homeowners yet. Use 'New homeowner' or link a homeowner contact to this case." + : "All available homeowners already have a collection on this matter."}

) : ( { - const label = `${h.last_name}, ${h.first_name}${h.unit_number ? ` — Unit ${h.unit_number}` : ""}`; - return { value: h.id, label, keywords: `${h.first_name} ${h.last_name} ${h.unit_number ?? ""}` }; - })} + options={allOptions} /> )}
From f23f80a0948b569d55746d1591e68c9ee77a6153 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 23:37:36 +0000 Subject: [PATCH 4/4] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index 8288ea6..6a700a8 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -1296,7 +1296,7 @@ function AddCollectionDialog({