Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-26 09:05:08 +00:00
co-authored by renee-png
parent 9695af17e5
commit 86ca026f63
@@ -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<Record<string, Installment[]>>({});
const [loading, setLoading] = useState(true);
const [createOpen, setCreateOpen] = useState(false);
const [requests, setRequests] = useState<Record<string, PaymentRequestRow>>({});
const [busyId, setBusyId] = useState<string | null>(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<string, PaymentRequestRow> = {};
(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 (
<Card className="border-border/60">
<CardContent className="p-4 space-y-4">