"use client"; import { useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/app-shell"; import { apiFetch } from "@/lib/api"; type Payee = { id: string; name: string; category?: string | null; accountNumberLast4?: string | null; }; type Bill = { id: string; payeeId?: string | null; name: string; amount: string | number; currency: string; dueDate: string; status: "pending" | "scheduled" | "paid" | "skipped" | "cancelled"; computedStatus: string; recurrence: string; autopay: boolean; reminderDays: number; paidAt?: string | null; payee?: Payee | null; }; type BillSummary = { activeCount: number; upcomingCount: number; overdueCount: number; totalDueNext30: string; }; const today = new Date().toISOString().slice(0, 10); export default function BillsPage() { const [payees, setPayees] = useState([]); const [bills, setBills] = useState([]); const [summary, setSummary] = useState(null); const [status, setStatus] = useState(""); const [filter, setFilter] = useState("all"); const [payeeForm, setPayeeForm] = useState({ name: "", category: "", accountNumberLast4: "" }); const [billForm, setBillForm] = useState({ payeeId: "", name: "", amount: "", dueDate: today, status: "pending", recurrence: "none", autopay: false, reminderDays: "3", notes: "", }); const filteredBills = useMemo(() => { if (filter === "all") return bills; if (filter === "overdue") return bills.filter((bill) => bill.computedStatus === "overdue"); return bills.filter((bill) => bill.status === filter); }, [bills, filter]); const money = (value: string | number, currency = "USD") => new Intl.NumberFormat("en-US", { style: "currency", currency }).format(Number(value ?? 0)); const load = async () => { const [payeesRes, billsRes, summaryRes] = await Promise.all([ apiFetch("/api/bill-pay/payees"), apiFetch("/api/bill-pay/bills"), apiFetch("/api/bill-pay/summary"), ]); if (!payeesRes.error) setPayees(payeesRes.data ?? []); if (!billsRes.error) setBills(billsRes.data ?? []); if (!summaryRes.error) setSummary(summaryRes.data ?? null); }; useEffect(() => { load().catch(() => setStatus("Unable to load bills.")); }, []); const createPayee = async () => { if (!payeeForm.name.trim()) { setStatus("Payee name is required."); return; } const res = await apiFetch("/api/bill-pay/payees", { method: "POST", body: JSON.stringify(payeeForm), }); if (res.error) { setStatus(res.error.message ?? "Unable to create payee."); return; } setPayeeForm({ name: "", category: "", accountNumberLast4: "" }); setStatus("Payee created."); await load(); }; const createBill = async () => { if (!billForm.name.trim() || !billForm.amount || !billForm.dueDate) { setStatus("Bill name, amount, and due date are required."); return; } const payload = { ...billForm, payeeId: billForm.payeeId || undefined, amount: Number(billForm.amount), reminderDays: Number(billForm.reminderDays || 3), }; const res = await apiFetch("/api/bill-pay/bills", { method: "POST", body: JSON.stringify(payload), }); if (res.error) { setStatus(res.error.message ?? "Unable to create bill."); return; } setBillForm({ payeeId: "", name: "", amount: "", dueDate: today, status: "pending", recurrence: "none", autopay: false, reminderDays: "3", notes: "" }); setStatus("Bill created."); await load(); }; const markPaid = async (bill: Bill) => { const res = await apiFetch(`/api/bill-pay/bills/${bill.id}/pay`, { method: "POST", body: JSON.stringify({ amount: Number(bill.amount), paidAt: new Date().toISOString(), method: bill.autopay ? "autopay" : "manual", }), }); if (res.error) { setStatus(res.error.message ?? "Unable to mark bill paid."); return; } setStatus(`${bill.name} marked paid.`); await load(); }; const updateStatus = async (bill: Bill, nextStatus: Bill["status"]) => { const res = await apiFetch(`/api/bill-pay/bills/${bill.id}`, { method: "PATCH", body: JSON.stringify({ status: nextStatus }), }); if (res.error) { setStatus(res.error.message ?? "Unable to update bill."); return; } setStatus("Bill updated."); await load(); }; const inputCls = "mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary focus:outline-none"; const labelCls = "text-xs text-muted-foreground font-semibold uppercase tracking-wider"; return (

Active bills

{summary?.activeCount ?? 0}

Due next 30 days

{summary?.upcomingCount ?? 0}

Overdue

{summary?.overdueCount ?? 0}

Total due next 30 days

{money(summary?.totalDueNext30 ?? 0)}

Payees

setPayeeForm((prev) => ({ ...prev, name: event.target.value }))} className={inputCls} />
setPayeeForm((prev) => ({ ...prev, category: event.target.value }))} className={inputCls} />
setPayeeForm((prev) => ({ ...prev, accountNumberLast4: event.target.value }))} className={inputCls} />
{payees.length === 0 &&

No payees yet.

} {payees.map((payee) => (

{payee.name}

{payee.category || "Uncategorized"}{payee.accountNumberLast4 ? ` · ${payee.accountNumberLast4}` : ""}

))}

New Bill

setBillForm((prev) => ({ ...prev, name: event.target.value }))} className={inputCls} />
setBillForm((prev) => ({ ...prev, amount: event.target.value }))} className={inputCls} />
setBillForm((prev) => ({ ...prev, dueDate: event.target.value }))} className={inputCls} />
setBillForm((prev) => ({ ...prev, reminderDays: event.target.value }))} className={inputCls} />
{status &&

{status}

}

Bill Schedule

{filteredBills.length === 0 &&

No bills for this view.

} {filteredBills.map((bill) => (

{bill.name}

{bill.computedStatus} {bill.autopay && autopay}

{money(bill.amount, bill.currency)} due {new Date(bill.dueDate).toLocaleDateString()} · {bill.recurrence}

{bill.payee &&

Payee: {bill.payee.name}

}
{bill.status !== "paid" && ( )} {bill.status !== "skipped" && bill.status !== "paid" && ( )} {bill.status !== "cancelled" && bill.status !== "paid" && ( )}
))}
); }