From 86ca026f63bd92dfe48bf4da3463900499326d99 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:05:08 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- .../collections/payment-plans-panel.tsx | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/src/components/collections/payment-plans-panel.tsx b/src/components/collections/payment-plans-panel.tsx index 33bff81..de59105 100644 --- a/src/components/collections/payment-plans-panel.tsx +++ b/src/components/collections/payment-plans-panel.tsx @@ -23,9 +23,19 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { CalendarClock, Loader2, Plus, Trash2 } from "lucide-react"; +import { + CalendarClock, + CreditCard, + ExternalLink, + Loader2, + Plus, + RefreshCw, + Trash2, +} from "lucide-react"; import { toast } from "sonner"; import { formatCurrency, formatDate, parseDateOnly } from "@/lib/format"; +import { useServerFn } from "@tanstack/react-start"; +import { createPaymentRequest, refreshPaymentStatus } from "@/lib/payments.functions"; type Frequency = "weekly" | "biweekly" | "monthly"; type PlanStatus = "active" | "completed" | "defaulted" | "cancelled"; @@ -55,6 +65,13 @@ interface Installment { paid_on: string | null; paid_amount: number | null; notes: string | null; + payment_request_id?: string | null; +} + +interface PaymentRequestRow { + id: string; + status: string; + stripe_checkout_url: string | null; } function addInterval(dateStr: string, frequency: Frequency, n: number) { @@ -77,6 +94,10 @@ export function PaymentPlansPanel({ const [installments, setInstallments] = useState>({}); const [loading, setLoading] = useState(true); const [createOpen, setCreateOpen] = useState(false); + const [requests, setRequests] = useState>({}); + const [busyId, setBusyId] = useState(null); + const createReq = useServerFn(createPaymentRequest); + const refreshReq = useServerFn(refreshPaymentStatus); const load = useCallback(async () => { setLoading(true); @@ -102,8 +123,24 @@ export function PaymentPlansPanel({ (grouped[i.plan_id] ||= []).push(i as Installment); }); setInstallments(grouped); + // Load any associated payment requests + const reqIds = (ins ?? []) + .map((i: any) => i.payment_request_id) + .filter(Boolean) as string[]; + if (reqIds.length) { + const { data: prs } = await supabase + .from("payment_requests") + .select("id, status, stripe_checkout_url") + .in("id", reqIds); + const map: Record = {}; + (prs ?? []).forEach((r: any) => (map[r.id] = r)); + setRequests(map); + } else { + setRequests({}); + } } else { setInstallments({}); + setRequests({}); } setLoading(false); }, [collectionId]); @@ -160,6 +197,76 @@ export function PaymentPlansPanel({ else { toast.success("Plan deleted"); load(); } }; + const generatePayment = async (inst: Installment, plan: Plan) => { + setBusyId(inst.id); + try { + // Fetch homeowner + client info for nice recipient label + const { data: coll } = await supabase + .from("collections") + .select( + "case_id, homeowner:homeowners(first_name, last_name, email), case:cases(client_id, client:clients(name))", + ) + .eq("id", collectionId) + .maybeSingle(); + const homeowner: any = (coll as any)?.homeowner; + const clientName: string | undefined = (coll as any)?.case?.client?.name; + const clientId: string | undefined = (coll as any)?.case?.client_id ?? undefined; + const recipientName = + [homeowner?.first_name, homeowner?.last_name].filter(Boolean).join(" ").trim() || + clientName || + "Payment"; + const res = await createReq({ + data: { + recipient_name: recipientName, + recipient_email: homeowner?.email || undefined, + description: `${plan.name || "Payment Plan"} – Installment ${inst.sort_order + 1}`, + base_amount_cents: Math.round(Number(inst.amount) * 100), + collection_id: collectionId, + case_id: caseId ?? undefined, + client_id: clientId, + }, + }); + await supabase + .from("payment_plan_installments") + .update({ payment_request_id: res.id }) + .eq("id", inst.id); + toast.success("Payment link generated"); + if (res.checkout_url) window.open(res.checkout_url, "_blank"); + load(); + } catch (e: any) { + toast.error(e?.message ?? "Failed to generate payment"); + } finally { + setBusyId(null); + } + }; + + const refreshStatus = async (inst: Installment) => { + if (!inst.payment_request_id) return; + setBusyId(inst.id); + try { + const res = await refreshReq({ data: { id: inst.payment_request_id } }); + if (res.status === "paid" && !inst.paid) { + await supabase + .from("payment_plan_installments") + .update({ + paid: true, + paid_on: new Date().toISOString().slice(0, 10), + paid_amount: inst.amount, + paid_method: "online", + }) + .eq("id", inst.id); + toast.success("Marked as paid"); + } else { + toast.message(`Status: ${res.status}`); + } + load(); + } catch (e: any) { + toast.error(e?.message ?? "Failed to refresh"); + } finally { + setBusyId(null); + } + }; + return (