import { useState, useEffect } from "react"; import { CreditCard, DollarSign, Package, Zap } from "lucide-react"; interface Invoice { id: string; status: "pending" | "paid" | "failed" | "refunded"; totalCents: number; date: string; description?: string; } interface PaymentMethod { brand: string; last4: string; expiryMonth: number; expiryYear: number; } interface Package { name: string; remaining: number; } interface BillingPaymentsProps { sessionId: string | null; readOnly: boolean; } export function BillingPayments({ sessionId, readOnly }: BillingPaymentsProps) { const [invoices, setInvoices] = useState([]); const [paymentMethods, setPaymentMethods] = useState([]); const [packages, setPackages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tab, setTab] = useState<"invoices" | "payment" | "packages">("invoices"); const [autopay, setAutopay] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false); useEffect(() => { async function fetchData() { if (!sessionId) { setLoading(false); return; } try { const response = await fetch("/api/portal/invoices", { headers: { "X-Impersonation-Session-Id": sessionId, }, }); if (!response.ok) { throw new Error("Failed to fetch invoices"); } const data = await response.json(); setInvoices(Array.isArray(data) ? data : data.invoices || []); setPaymentMethods(data.paymentMethods || []); setPackages(data.packages || []); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { setLoading(false); } } fetchData(); }, [sessionId]); const formatCents = (cents: number) => { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(cents / 100); }; const pending = invoices.filter((i) => i.status === "pending"); const totalPending = pending.reduce((sum, i) => sum + i.totalCents, 0); if (loading) { return (
); } if (error) { return (
Error: {error}
); } return (
{/* Outstanding Balance Banner */} {totalPending > 0 && (

Outstanding Balance

{formatCents(totalPending)}

{pending.length} unpaid invoice{pending.length > 1 ? "s" : ""}

)} {/* Tabs */}
{([ { id: "invoices" as const, label: "Invoices", icon: DollarSign }, { id: "payment" as const, label: "Payment Methods", icon: CreditCard }, { id: "packages" as const, label: "Packages", icon: Package }, ]).map(({ id, label, icon: Icon }) => ( ))}
{/* Invoices */} {tab === "invoices" && (
{invoices.map((inv) => ( ))}
Date Description Amount Status
{new Date(inv.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", })} {inv.description || `Invoice ${inv.id.slice(0, 8)}`} {formatCents(inv.totalCents)} {inv.status.charAt(0).toUpperCase() + inv.status.slice(1)}
)} {/* Payment Methods */} {tab === "payment" && (
{paymentMethods.length === 0 ? (

No payment methods on file

) : (
{paymentMethods.map((method) => (
{method.brand.toUpperCase()}
**** {method.last4} {method.expiryMonth}/{method.expiryYear}
{!readOnly && ( )}
))}
)} {/* Autopay */}

Autopay

Automatically charge after each appointment

{!readOnly ? ( ) : ( {autopay ? "Enabled" : "Disabled"} )}
)} {/* Packages */} {tab === "packages" && (
{packages.length === 0 ? (

No packages purchased

) : ( packages.map((pkg, index) => (
{pkg.name} {pkg.remaining} remaining
)) )}
)} {/* Payment Modal */} {showPaymentModal && ( setShowPaymentModal(false)} /> )}
); } function PaymentModal({ pending, totalPending: _totalPending, onClose, }: { pending: Invoice[]; totalPending: number; onClose: () => void; }) { const [selectedInvoices, setSelectedInvoices] = useState>( new Set(pending.map((i) => i.id)) ); const [isProcessing, setIsProcessing] = useState(false); const [isComplete, setIsComplete] = useState(false); const formatCents = (cents: number) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(cents / 100); const toggleInvoice = (id: string) => { const next = new Set(selectedInvoices); if (next.has(id)) { next.delete(id); } else { next.add(id); } setSelectedInvoices(next); }; const handlePay = async () => { setIsProcessing(true); await new Promise((resolve) => setTimeout(resolve, 1500)); setIsProcessing(false); setIsComplete(true); }; const selectedTotal = pending .filter((i) => selectedInvoices.has(i.id)) .reduce((sum, i) => sum + i.totalCents, 0); if (isComplete) { return (

Payment Successful

Your payment of {formatCents(selectedTotal)} has been processed. A receipt has been sent to your email.

); } return (

Pay Outstanding Balance

Select invoices to pay:

{pending.map((inv) => ( ))}
Total {formatCents(selectedTotal)}
); } export default BillingPayments;