"use client"; import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { RefreshCw, FileText } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Modal } from "@/components/ui/Modal"; import { formatDate, formatCurrency } from "@/lib/utils"; import api from "@/lib/api"; import { toast } from "sonner"; interface Invoice { id: string; invoiceNumber: string; clientId: string; client?: { firstName: string; lastName: string; accountNumber: string }; subtotal: string; lateFee: string; total: string; amountPaid: string; balance: string; status: string; dueDate: string; periodStart?: string; periodEnd?: string; notes?: string; createdAt: string; } interface InvoicesResponse { data: Invoice[]; total: number; page: number; limit: number; } const statusVariant: Record = { PAID: "success", PARTIAL: "warning", OVERDUE: "danger", SENT: "muted", DRAFT: "muted", VOID: "muted", }; const statusFilters = ["", "SENT", "PARTIAL", "OVERDUE", "PAID", "VOID"]; export default function InvoicesPage() { const qc = useQueryClient(); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [page, setPage] = useState(1); const [selected, setSelected] = useState(null); const [payForm, setPayForm] = useState({ amount: "", channel: "CASH", referenceNumber: "", notes: "" }); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ["invoices", search, statusFilter, page], queryFn: async () => { const params = new URLSearchParams({ page: String(page), limit: "20" }); if (search) params.set("search", search); if (statusFilter) params.set("status", statusFilter); const res = await api.get(`/api/v1/invoices?${params}`); return res.data; }, }); const recordPayment = useMutation({ mutationFn: async () => { await api.post("/api/v1/payments", { clientId: selected!.clientId, invoiceId: selected!.id, amount: Number(payForm.amount), channel: payForm.channel, referenceNumber: payForm.referenceNumber || undefined, notes: payForm.notes || undefined, paymentDate: new Date().toISOString(), }); }, onSuccess: () => { toast.success("Payment recorded!"); setSelected(null); setPayForm({ amount: "", channel: "CASH", referenceNumber: "", notes: "" }); qc.invalidateQueries({ queryKey: ["invoices"] }); refetch(); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Payment failed"), }); const voidInvoice = useMutation({ mutationFn: async (id: string) => { await api.patch(`/api/v1/invoices/${id}/void`); }, onSuccess: () => { toast.success("Invoice voided"); setSelected(null); qc.invalidateQueries({ queryKey: ["invoices"] }); }, onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to void"), }); const invoices = data?.data ?? []; const total = data?.total ?? 0; return (

Invoices

{total} total invoices

{ setSearch(e.target.value); setPage(1); }} />
{statusFilters.map(s => ( ))}
{isLoading ? ( Array.from({ length: 8 }).map((_, i) => ( )) ) : isError ? ( ) : invoices.length === 0 ? ( } /> ) : invoices.map(inv => ( setSelected(inv)} className="cursor-pointer hover:bg-blue-50 transition-colors"> ))}
Invoice #ClientTotalPaidBalanceDue DateStatus

Failed to load invoices.

{inv.invoiceNumber} {inv.client ? `${inv.client.firstName} ${inv.client.lastName}` : "—"}
{inv.client?.accountNumber}
{formatCurrency(Number(inv.total))} {formatCurrency(Number(inv.amountPaid))} 0 ? "text-red-600 font-medium" : "text-gray-400"}> {formatCurrency(Number(inv.balance))} {formatDate(inv.dueDate)} {inv.status} View →
{total > 20 && (
Showing {(page - 1) * 20 + 1}–{Math.min(page * 20, total)} of {total}
)}
{/* Invoice Detail Modal */} setSelected(null)} title={`Invoice ${selected?.invoiceNumber ?? ""}`} className="max-w-lg" footer={selected ? ( <> {selected.status !== "VOID" && selected.status !== "PAID" && ( )} ) : undefined} > {selected && (
Client {selected.client ? `${selected.client.firstName} ${selected.client.lastName}` : "—"}
Period {selected.periodStart ? `${formatDate(selected.periodStart)} – ${formatDate(selected.periodEnd!)}` : "—"}
Due Date {formatDate(selected.dueDate)}

Subtotal{formatCurrency(Number(selected.subtotal))}
Late Fee{formatCurrency(Number(selected.lateFee))}
Total{formatCurrency(Number(selected.total))}
Amount Paid{formatCurrency(Number(selected.amountPaid))}
0 ? "text-red-600" : "text-green-600"}`}> Balance{formatCurrency(Number(selected.balance))}
Status {selected.status}
{/* Pay form (only for unpaid) */} {["SENT", "PARTIAL", "OVERDUE"].includes(selected.status) && (

Record Payment

setPayForm(f => ({ ...f, amount: e.target.value }))} hint={`Balance due: ${formatCurrency(Number(selected.balance))}`} />
setPayForm(f => ({ ...f, referenceNumber: e.target.value }))} />
)}
)}
); }