"use client" import Link from "next/link" import { UserIcon } from "lucide-react" import { useRouter } from "next/navigation" import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { InvoiceForm } from "@/modules/invoices/invoice-form" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { formatCurrency, formatDate, formatEnum } from "@/shared/utils/formatters" import { INVOICE_ROUTES } from "@garage/api" import type { InvoicesClient } from "@garage/api" type InvoiceItem = { id: number subject?: string invoice_title?: string invoice_number?: string customer_name?: string status?: string invoice_date?: string due_date?: string created_at?: string total?: number | string balance_due?: number | string customer?: { id?: number | string first_name?: string last_name?: string company_name?: string phone?: string } | null vehicle?: { make?: string model?: string sub_model?: string year?: number | string license_plate?: string } | null } function getCustomerLabel(item: InvoiceItem): string { const firstName = item.customer?.first_name?.trim() const lastName = item.customer?.last_name?.trim() const fullName = [firstName, lastName].filter(Boolean).join(" ").trim() return fullName || item.customer_name || item.customer?.company_name || "—" } function getVehicleLabel(item: InvoiceItem): string { const vehicle = item.vehicle if (!vehicle) return "—" const core = [vehicle.make, vehicle.model, vehicle.sub_model].filter(Boolean).join(" ") const suffix = [vehicle.year, vehicle.license_plate].filter(Boolean).join(" • ") if (core && suffix) return `${core} (${suffix})` return core || suffix || "—" } function getDueMeta(dueDate?: string) { if (!dueDate) return { text: "No due date", tone: "muted" as const } const due = new Date(dueDate) if (isNaN(due.getTime())) return { text: "Invalid due date", tone: "muted" as const } const today = new Date() today.setHours(0, 0, 0, 0) due.setHours(0, 0, 0, 0) const days = Math.ceil((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)) if (days < 0) return { text: `${Math.abs(days)} day(s) overdue`, tone: "danger" as const } if (days === 0) return { text: "Due today", tone: "warning" as const } if (days <= 3) return { text: `Due in ${days} day(s)`, tone: "warning" as const } return { text: `Due in ${days} day(s)`, tone: "ok" as const } } export default function InvoicesPage() { const router = useRouter() return ( pageTitle="Invoices" routeKey={INVOICE_ROUTES.INDEX} getClient={(api) => api.invoices} onRowClick={(row) => router.push(`/sales/invoice/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( {(resourceId) => ( )} ), })} columns={({ actionsColumn }) => [ { accessorKey: "subject", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem const title = item.subject || item.invoice_title || "Untitled invoice" return (

{title}

{item.invoice_number || "No invoice number"}

) }, }, { accessorKey: "invoice_number", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem return ( {item.invoice_number || "—"} ) }, }, { accessorKey: "customer_name", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem const customerLabel = getCustomerLabel(item) const phone = item.customer?.phone return (
{item.customer?.id ? ( ) : (

{customerLabel} {phone && ( · {phone} )}

)}
) }, }, { id: "vehicle", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem return (

{getVehicleLabel(item)}

{item.vehicle?.license_plate || "—"}

) }, }, { accessorKey: "status", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem const status = item.status const classMap: Record = { draft: "bg-slate-100 text-slate-700", open: "bg-blue-100 text-blue-700", paid: "bg-emerald-100 text-emerald-700", over_due: "bg-red-100 text-red-700", un_paid: "bg-orange-100 text-orange-700", partially_paid: "bg-amber-100 text-amber-700", void: "bg-zinc-100 text-zinc-700", } return ( {formatEnum(status) || "—"} ) }, }, { accessorKey: "invoice_date", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem return formatDate(item.invoice_date) }, }, { accessorKey: "due_date", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem const dueMeta = getDueMeta(item.due_date) const toneClassMap = { danger: "text-red-600", warning: "text-amber-600", ok: "text-emerald-600", muted: "text-muted-foreground", } return (

{formatDate(item.due_date)}

{dueMeta.text}

) }, }, { id: "amounts", header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as InvoiceItem return (

Total: {formatCurrency(item.total ?? 0)}

Due: {formatCurrency(item.balance_due ?? 0)}

) }, }, actionsColumn(), ]} /> ) }