Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
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">
|
||||
|
||||
Reference in New Issue
Block a user