feat: add purchase invoice batch print page and related components

- Implemented PurchaseInvoiceBatchPrintPage for printing purchase invoices.
- Created InvoicePrintHeader and InvoicePrintFooter components for invoice layout.
- Added invoice layout presets for different styles (Classic, Compact, Modern).
- Developed PurchaseInvoicePaymentDialog for handling payments against invoices.
- Introduced API mocks for company profile and invoice template configuration.
- Added validation functions for company profile and purchase invoices.
- Defined types for company profile, invoice template configuration, and purchase invoices.
This commit is contained in:
2026-09-01 15:25:32 +05:30
parent 160c687db3
commit bebec10eb3
70 changed files with 12446 additions and 589 deletions
@@ -0,0 +1,155 @@
"use client"
import { use, useEffect, useState } from "react"
import { Printer } from "lucide-react"
import { purchaseInvoicesApi } from "@/lib/api/purchase-invoices"
import { companyProfileApi } from "@/lib/api/company-profile"
import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config"
import { errorMessage } from "@/lib/error-map"
import { formatAmount } from "@/lib/format"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader"
import { InvoicePrintFooter } from "@/components/invoice-template/InvoicePrintFooter"
import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets"
import { CompanyProfile } from "@/types/company-profile"
import { InvoiceTemplateConfig } from "@/types/invoice-template"
import { PurchaseInvoice } from "@/types/purchase-invoices"
export default function PurchaseInvoicePrintPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params)
const purchaseInvoiceId = Number(resolvedParams.id)
const [invoice, setInvoice] = useState<PurchaseInvoice | null>(null)
const [company, setCompany] = useState<CompanyProfile | null>(null)
const [template, setTemplate] = useState<InvoiceTemplateConfig | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!Number.isFinite(purchaseInvoiceId)) {
setError(`Invalid purchase invoice id '${resolvedParams.id}'.`)
return
}
Promise.all([
purchaseInvoicesApi.get(purchaseInvoiceId),
companyProfileApi.get().catch(() => ({ data: null, etag: null })),
invoiceTemplateConfigApi.get().catch(() => ({ data: null, etag: null })),
])
.then(([doc, companyRes, templateRes]) => {
setInvoice(doc.data)
setCompany(companyRes.data)
setTemplate(templateRes.data)
})
.catch((err) => setError(errorMessage(err)))
}, [resolvedParams.id, purchaseInvoiceId])
if (error && !invoice) {
return <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>
}
if (!invoice) {
return <div className="rounded-2xl border border-dashed p-8 text-muted-foreground">{error ?? "Invoice print data is loading or unavailable."}</div>
}
const layout = getInvoiceLayoutClasses(template?.templateStyle)
const titleBlock = (
<div>
<div className={layout.eyebrow}>Purchase Invoice</div>
<h1 className={layout.title}>{invoice.docNo}</h1>
<div className={layout.meta}>
Vendor invoice: {invoice.vendorInvoiceNo} · Status: {invoice.status}
</div>
</div>
)
const companyBlock = <InvoicePrintHeader company={company} template={template} />
return (
<div className={`invoice-sheet mx-auto flex w-full max-w-5xl flex-col rounded-3xl border bg-white shadow-sm print:rounded-none print:border-0 print:p-0 print:shadow-none ${layout.sheet}`}>
<div className="flex items-center justify-end print:hidden">
<Button variant="outline" onClick={() => window.print()}>
<Printer className="size-4" />
Print
</Button>
</div>
{layout.arrangement === "compact" ? (
<div className={`invoice-header flex flex-wrap items-start justify-between ${layout.header}`}>
{titleBlock}
{companyBlock}
</div>
) : (
<div className={`invoice-header grid md:grid-cols-[1.4fr_1fr] ${layout.header}`}>
{layout.arrangement === "highlight" ? companyBlock : titleBlock}
{layout.arrangement === "highlight" ? titleBlock : companyBlock}
</div>
)}
{layout.arrangement === "highlight" && (
<div className={layout.totalsBanner}>
<span className={layout.totalsBannerLabel}>Grand total</span>
<span className={layout.totalsBannerValue}>{formatAmount(invoice.totals.grandTotal)}</span>
</div>
)}
{layout.arrangement === "compact" ? (
<div className={`flex flex-wrap items-center text-sm ${layout.infoGrid}`}>
<div><span className="text-muted-foreground">Vendor: </span><span className="font-medium text-foreground">{invoice.vendorSnapshotName}</span></div>
<div><span className="text-muted-foreground">Vendor invoice date: </span><span className="font-medium text-foreground">{new Date(invoice.vendorInvoiceDate).toLocaleDateString()}</span></div>
<div><span className="text-muted-foreground">Grand total: </span><span className={layout.totalsHighlight}>{formatAmount(invoice.totals.grandTotal)}</span></div>
</div>
) : (
<div className={`grid ${layout.arrangement === "highlight" ? "md:grid-cols-2" : "md:grid-cols-3"} ${layout.infoGrid}`}>
<div className={layout.card}>
<div className={layout.cardLabel}>Vendor</div>
<div className={layout.cardValue}>{invoice.vendorSnapshotName}</div>
{invoice.vendorSnapshotTaxNo ? <div className="mt-1 text-sm text-muted-foreground">Tax No: {invoice.vendorSnapshotTaxNo}</div> : null}
</div>
<div className={layout.card}>
<div className={layout.cardLabel}>Dates</div>
<div className="mt-2 text-sm text-muted-foreground">Vendor invoice date: {new Date(invoice.vendorInvoiceDate).toLocaleDateString()}</div>
<div className="mt-1 text-sm text-muted-foreground">Printed: {new Date().toLocaleString()}</div>
</div>
{layout.arrangement !== "highlight" && (
<div className={layout.card}>
<div className={layout.cardLabel}>Totals</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-sm">
<div className="text-muted-foreground">Subtotal</div><div className="text-right font-medium">{formatAmount(invoice.totals.subtotal)}</div>
<div className="text-muted-foreground">VAT</div><div className="text-right font-medium">{formatAmount(invoice.totals.vatTotal)}</div>
<div className="text-muted-foreground">Grand total</div><div className={`text-right ${layout.totalsHighlight}`}>{formatAmount(invoice.totals.grandTotal)}</div>
</div>
</div>
)}
</div>
)}
<div className={layout.tableWrap}>
<Table>
<TableHeader>
<TableRow className={layout.tableHeaderRow}>
<TableHead>GRN</TableHead>
<TableHead>Posted</TableHead>
<TableHead className="text-right">Subtotal</TableHead>
<TableHead className="text-right">VAT</TableHead>
<TableHead className="text-right">Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoice.lines.map((line) => (
<TableRow key={line.purchaseInvoiceLineId}>
<TableCell className="font-medium">{line.grnDocNo}</TableCell>
<TableCell>{new Date(line.grnPostedAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">{formatAmount(line.subtotal)}</TableCell>
<TableCell className="text-right">{formatAmount(line.vatTotal)}</TableCell>
<TableCell className="text-right font-medium">{formatAmount(line.lineTotal)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<InvoicePrintFooter company={company} template={template} />
</div>
)
}
@@ -0,0 +1,137 @@
"use client"
import { Suspense, useEffect, useState } from "react"
import { useSearchParams } from "next/navigation"
import { Printer } from "lucide-react"
import { purchaseInvoicesApi } from "@/lib/api/purchase-invoices"
import { companyProfileApi } from "@/lib/api/company-profile"
import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config"
import { errorMessage } from "@/lib/error-map"
import { formatAmount } from "@/lib/format"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader"
import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets"
import { CompanyProfile } from "@/types/company-profile"
import { InvoiceTemplateConfig } from "@/types/invoice-template"
import { PurchaseInvoiceSummary } from "@/types/purchase-invoices"
function statusClass(status: PurchaseInvoiceSummary["status"]) {
switch (status) {
case "Draft":
return "border-amber-200 bg-amber-50 text-amber-800"
case "Posted":
return "border-emerald-200 bg-emerald-50 text-emerald-800"
case "Cancelled":
return "border-rose-200 bg-rose-50 text-rose-800"
}
}
function PurchaseInvoiceBatchPrintContent() {
const searchParams = useSearchParams()
const status = searchParams.get("status") ?? "All"
const query = searchParams.get("q") ?? ""
const [rows, setRows] = useState<PurchaseInvoiceSummary[] | null>(null)
const [company, setCompany] = useState<CompanyProfile | null>(null)
const [template, setTemplate] = useState<InvoiceTemplateConfig | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
Promise.all([
purchaseInvoicesApi.list({ page: 1, pageSize: 200 }),
companyProfileApi.get().catch(() => ({ data: null, etag: null })),
invoiceTemplateConfigApi.get().catch(() => ({ data: null, etag: null })),
])
.then(([res, companyRes, templateRes]) => {
setRows(res.items)
setCompany(companyRes.data)
setTemplate(templateRes.data)
})
.catch((err) => setError(errorMessage(err)))
}, [])
const visibleRows = rows?.filter((row) => {
const matchesStatus = status === "All" || row.status === status
const matchesQuery = `${row.docNo} ${row.vendorInvoiceNo} ${row.vendorSnapshotName}`.toLowerCase().includes(query.toLowerCase())
return matchesStatus && matchesQuery
})
if (error && !rows) {
return <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>
}
if (!rows) {
return <div className="rounded-2xl border border-dashed p-8 text-muted-foreground">{error ?? "Purchase invoice batch print data is loading or unavailable."}</div>
}
const layout = getInvoiceLayoutClasses(template?.templateStyle)
return (
<div className={`mx-auto flex w-full max-w-6xl flex-col rounded-3xl border bg-white shadow-sm print:rounded-none print:border-0 print:p-0 print:shadow-none ${layout.sheet}`}>
<div className="flex items-center justify-end print:hidden">
<Button variant="outline" onClick={() => window.print()}>
<Printer className="size-4" />
Print
</Button>
</div>
<div className={`md:grid ${layout.arrangement === "compact" ? "" : "md:grid-cols-[1.4fr_1fr]"} ${layout.header}`}>
{layout.arrangement === "highlight" && <InvoicePrintHeader company={company} template={template} />}
<div>
<div className={layout.eyebrow}>Purchase Invoices</div>
<h1 className={layout.title}>Purchase Invoice Batch Print</h1>
<p className={layout.meta}>Printed register snapshot of current purchase invoices.</p>
</div>
{layout.arrangement !== "highlight" && <InvoicePrintHeader company={company} template={template} />}
</div>
<div className={layout.tableWrap}>
<Table>
<TableHeader>
<TableRow className={layout.tableHeaderRow}>
<TableHead>Invoice</TableHead>
<TableHead>Vendor invoice no.</TableHead>
<TableHead>Vendor</TableHead>
<TableHead>Date</TableHead>
<TableHead className="text-right">GRNs</TableHead>
<TableHead className="text-right">Subtotal</TableHead>
<TableHead className="text-right">VAT</TableHead>
<TableHead className="text-right">Total</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visibleRows?.map((row) => (
<TableRow key={row.purchaseInvoiceId}>
<TableCell className="font-medium">{row.docNo}</TableCell>
<TableCell>{row.vendorInvoiceNo}</TableCell>
<TableCell>{row.vendorSnapshotName}</TableCell>
<TableCell>{new Date(row.vendorInvoiceDate).toLocaleDateString()}</TableCell>
<TableCell className="text-right">{row.grnCount}</TableCell>
<TableCell className="text-right">{formatAmount(row.totals.subtotal)}</TableCell>
<TableCell className="text-right">{formatAmount(row.totals.vatTotal)}</TableCell>
<TableCell className="text-right font-medium">{formatAmount(row.totals.grandTotal)}</TableCell>
<TableCell>
<Badge variant="outline" className={statusClass(row.status)}>
{row.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}
export default function PurchaseInvoiceBatchPrintPage() {
return (
<Suspense fallback={<Skeleton className="h-48 w-full" />}>
<PurchaseInvoiceBatchPrintContent />
</Suspense>
)
}
@@ -9,11 +9,18 @@ import { itemsApi } from "@/lib/api/items"
import { uomsApi } from "@/lib/api/uoms"
import { baseUomLabel } from "@/lib/uom-label"
import { warehousesApi } from "@/lib/api/warehouses"
import { companyProfileApi } from "@/lib/api/company-profile"
import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config"
import { errorMessage } from "@/lib/error-map"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader"
import { InvoicePrintFooter } from "@/components/invoice-template/InvoicePrintFooter"
import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets"
import { Customer } from "@/types/customers"
import { ItemListItem, Uom, Warehouse } from "@/types/master-data"
import { CompanyProfile } from "@/types/company-profile"
import { InvoiceTemplateConfig } from "@/types/invoice-template"
import { SalesInvoice } from "@/types/sales"
export default function SalesInvoicePrintPage({ params }: { params: Promise<{ id: string }> }) {
@@ -24,6 +31,8 @@ export default function SalesInvoicePrintPage({ params }: { params: Promise<{ id
const [items, setItems] = useState<ItemListItem[]>([])
const [uoms, setUoms] = useState<Uom[]>([])
const [warehouses, setWarehouses] = useState<Warehouse[]>([])
const [company, setCompany] = useState<CompanyProfile | null>(null)
const [template, setTemplate] = useState<InvoiceTemplateConfig | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
@@ -38,13 +47,17 @@ export default function SalesInvoicePrintPage({ params }: { params: Promise<{ id
uomsApi.list({ pageSize: 200 }),
warehousesApi.list({ pageSize: 200 }),
salesApi.getInvoice(invoiceId),
companyProfileApi.get().catch(() => ({ data: null, etag: null })),
invoiceTemplateConfigApi.get().catch(() => ({ data: null, etag: null })),
])
.then(([cust, itemRes, uomRes, whRes, doc]) => {
.then(([cust, itemRes, uomRes, whRes, doc, companyRes, templateRes]) => {
setCustomers(cust.items)
setItems(itemRes.items)
setUoms(uomRes.items)
setWarehouses(whRes.items)
setInvoice(doc.data)
setCompany(companyRes.data)
setTemplate(templateRes.data)
})
.catch((err) => setError(errorMessage(err)))
}, [resolvedParams.id, invoiceId])
@@ -61,9 +74,19 @@ export default function SalesInvoicePrintPage({ params }: { params: Promise<{ id
const customer = customers.find((c) => c.customerId === invoice.customerId)
const warehouse = warehouses.find((w) => w.warehouseId === invoice.warehouseId)
const layout = getInvoiceLayoutClasses(template?.templateStyle)
const titleBlock = (
<div>
<div className={layout.eyebrow}>Sales Invoice</div>
<h1 className={layout.title}>{invoice.invoiceNo}</h1>
<div className={layout.meta}>Status: {invoice.status} · Type: {invoice.invoiceType}</div>
</div>
)
const companyBlock = <InvoicePrintHeader company={company} template={template} />
return (
<div className="invoice-sheet mx-auto flex w-full max-w-5xl flex-col gap-6 rounded-3xl border bg-white p-6 shadow-sm print:rounded-none print:border-0 print:p-0 print:shadow-none">
<div className={`invoice-sheet mx-auto flex w-full max-w-5xl flex-col rounded-3xl border bg-white shadow-sm print:rounded-none print:border-0 print:p-0 print:shadow-none ${layout.sheet}`}>
<div className="flex items-center justify-end print:hidden">
<Button variant="outline" onClick={() => window.print()}>
<Printer className="size-4" />
@@ -71,50 +94,65 @@ export default function SalesInvoicePrintPage({ params }: { params: Promise<{ id
</Button>
</div>
<div className="invoice-header grid gap-4 border-b pb-5 md:grid-cols-[1.4fr_1fr]">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground">Sales Invoice</div>
<h1 className="mt-2 text-3xl font-semibold text-foreground">{invoice.invoiceNo}</h1>
<div className="mt-2 text-sm text-muted-foreground">Status: {invoice.status} · Type: {invoice.invoiceType}</div>
{layout.arrangement === "compact" ? (
<div className={`invoice-header flex flex-wrap items-start justify-between ${layout.header}`}>
{titleBlock}
{companyBlock}
</div>
<div className="grid gap-2 text-sm md:justify-items-end">
<div className="font-semibold text-foreground">ERP Core Trading</div>
<div className="text-muted-foreground">Invoice print view</div>
<div className="text-muted-foreground">Invoice date: {new Date(invoice.invoiceDate).toLocaleDateString()}</div>
<div className="text-muted-foreground">Printed: {new Date().toLocaleString()}</div>
<div className="text-muted-foreground">Free qty total: {freeQtyTotal.toFixed(2)}</div>
) : (
<div className={`invoice-header grid md:grid-cols-[1.4fr_1fr] ${layout.header}`}>
{layout.arrangement === "highlight" ? companyBlock : titleBlock}
{layout.arrangement === "highlight" ? titleBlock : companyBlock}
</div>
</div>
)}
<div className="grid gap-4 md:grid-cols-3">
<div className="rounded-2xl border bg-muted/20 p-4">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Customer</div>
<div className="mt-2 text-lg font-semibold text-foreground">{invoice.customerSnapshotName}</div>
<div className="mt-1 text-sm text-muted-foreground">Customer ID: {invoice.customerId}</div>
{invoice.customerSnapshotTaxNo ? <div className="mt-1 text-sm text-muted-foreground">Tax No: {invoice.customerSnapshotTaxNo}</div> : null}
{customer?.displayName ? <div className="mt-1 text-sm text-muted-foreground">Customer: {customer.displayName}</div> : null}
{layout.arrangement === "highlight" && (
<div className={layout.totalsBanner}>
<span className={layout.totalsBannerLabel}>Net payable</span>
<span className={layout.totalsBannerValue}>{invoice.totals.netPayable.toFixed(2)}</span>
</div>
<div className="rounded-2xl border bg-muted/20 p-4">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Warehouse</div>
<div className="mt-2 text-lg font-semibold text-foreground">{warehouse?.name ?? `#${invoice.warehouseId}`}</div>
<div className="mt-1 text-sm text-muted-foreground">Code: {warehouse?.code ?? invoice.warehouseId}</div>
)}
{layout.arrangement === "compact" ? (
<div className={`flex flex-wrap items-center text-sm ${layout.infoGrid}`}>
<div><span className="text-muted-foreground">Customer: </span><span className="font-medium text-foreground">{invoice.customerSnapshotName}</span></div>
<div><span className="text-muted-foreground">Warehouse: </span><span className="font-medium text-foreground">{warehouse?.name ?? `#${invoice.warehouseId}`}</span></div>
<div><span className="text-muted-foreground">Free qty: </span><span className="font-medium text-foreground">{freeQtyTotal.toFixed(2)}</span></div>
<div><span className="text-muted-foreground">Net payable: </span><span className={layout.totalsHighlight}>{invoice.totals.netPayable.toFixed(2)}</span></div>
</div>
<div className="rounded-2xl border bg-muted/20 p-4">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Totals</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-sm">
<div className="text-muted-foreground">Subtotal</div><div className="text-right font-medium">{invoice.totals.subtotal.toFixed(2)}</div>
<div className="text-muted-foreground">Discount</div><div className="text-right font-medium">{invoice.totals.discountTotal.toFixed(2)}</div>
<div className="text-muted-foreground">Free qty</div><div className="text-right font-medium">{freeQtyTotal.toFixed(2)}</div>
<div className="text-muted-foreground">Tax</div><div className="text-right font-medium">{invoice.totals.taxTotal.toFixed(2)}</div>
<div className="text-muted-foreground">Net payable</div><div className="text-right font-semibold">{invoice.totals.netPayable.toFixed(2)}</div>
) : (
<div className={`grid ${layout.arrangement === "highlight" ? "md:grid-cols-2" : "md:grid-cols-3"} ${layout.infoGrid}`}>
<div className={layout.card}>
<div className={layout.cardLabel}>Customer</div>
<div className={layout.cardValue}>{invoice.customerSnapshotName}</div>
<div className="mt-1 text-sm text-muted-foreground">Customer ID: {invoice.customerId}</div>
{invoice.customerSnapshotTaxNo ? <div className="mt-1 text-sm text-muted-foreground">Tax No: {invoice.customerSnapshotTaxNo}</div> : null}
{customer?.displayName ? <div className="mt-1 text-sm text-muted-foreground">Customer: {customer.displayName}</div> : null}
</div>
<div className={layout.card}>
<div className={layout.cardLabel}>Warehouse</div>
<div className={layout.cardValue}>{warehouse?.name ?? `#${invoice.warehouseId}`}</div>
<div className="mt-1 text-sm text-muted-foreground">Code: {warehouse?.code ?? invoice.warehouseId}</div>
</div>
{layout.arrangement !== "highlight" && (
<div className={layout.card}>
<div className={layout.cardLabel}>Totals</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-sm">
<div className="text-muted-foreground">Subtotal</div><div className="text-right font-medium">{invoice.totals.subtotal.toFixed(2)}</div>
<div className="text-muted-foreground">Discount</div><div className="text-right font-medium">{invoice.totals.discountTotal.toFixed(2)}</div>
<div className="text-muted-foreground">Free qty</div><div className="text-right font-medium">{freeQtyTotal.toFixed(2)}</div>
<div className="text-muted-foreground">Tax</div><div className="text-right font-medium">{invoice.totals.taxTotal.toFixed(2)}</div>
<div className="text-muted-foreground">Net payable</div><div className={`text-right ${layout.totalsHighlight}`}>{invoice.totals.netPayable.toFixed(2)}</div>
</div>
</div>
)}
</div>
</div>
)}
<div className="overflow-hidden rounded-2xl border">
<div className={layout.tableWrap}>
<Table>
<TableHeader>
<TableRow>
<TableRow className={layout.tableHeaderRow}>
<TableHead className="w-[34%]">Item</TableHead>
<TableHead>UOM</TableHead>
<TableHead className="text-right">Qty</TableHead>
@@ -144,6 +182,8 @@ export default function SalesInvoicePrintPage({ params }: { params: Promise<{ id
</TableBody>
</Table>
</div>
<InvoicePrintFooter company={company} template={template} />
</div>
)
}
@@ -5,11 +5,17 @@ import { useSearchParams } from "next/navigation"
import { Printer } from "lucide-react"
import { salesApi } from "@/lib/api/sales"
import { companyProfileApi } from "@/lib/api/company-profile"
import { invoiceTemplateConfigApi } from "@/lib/api/invoice-template-config"
import { errorMessage } from "@/lib/error-map"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { InvoicePrintHeader } from "@/components/invoice-template/InvoicePrintHeader"
import { getInvoiceLayoutClasses } from "@/components/invoice-template/invoiceLayoutPresets"
import { CompanyProfile } from "@/types/company-profile"
import { InvoiceTemplateConfig } from "@/types/invoice-template"
import { SalesInvoiceSummary } from "@/types/sales"
function statusClass(status: SalesInvoiceSummary["status"]) {
@@ -28,13 +34,22 @@ function SalesInvoiceBatchPrintContent() {
const status = searchParams.get("status") ?? "All"
const query = searchParams.get("q") ?? ""
const [rows, setRows] = useState<SalesInvoiceSummary[] | null>(null)
const [company, setCompany] = useState<CompanyProfile | null>(null)
const [template, setTemplate] = useState<InvoiceTemplateConfig | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
salesApi
.listInvoices({ page: 1, pageSize: 200 })
.then((res) => setRows(res.items))
.catch((err) => setError(errorMessage(err)))
Promise.all([
salesApi.listInvoices({ page: 1, pageSize: 200 }),
companyProfileApi.get().catch(() => ({ data: null, etag: null })),
invoiceTemplateConfigApi.get().catch(() => ({ data: null, etag: null })),
])
.then(([res, companyRes, templateRes]) => {
setRows(res.items)
setCompany(companyRes.data)
setTemplate(templateRes.data)
})
.catch((err) => setError(errorMessage(err)))
}, [])
const visibleRows = rows?.filter((row) => {
@@ -52,8 +67,10 @@ function SalesInvoiceBatchPrintContent() {
return <div className="rounded-2xl border border-dashed p-8 text-muted-foreground">{error ?? "Invoice batch print data is loading or unavailable."}</div>
}
const layout = getInvoiceLayoutClasses(template?.templateStyle)
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 rounded-3xl border bg-white p-6 shadow-sm print:rounded-none print:border-0 print:p-0 print:shadow-none">
<div className={`mx-auto flex w-full max-w-6xl flex-col rounded-3xl border bg-white shadow-sm print:rounded-none print:border-0 print:p-0 print:shadow-none ${layout.sheet}`}>
<div className="flex items-center justify-end print:hidden">
<Button variant="outline" onClick={() => window.print()}>
<Printer className="size-4" />
@@ -61,16 +78,20 @@ function SalesInvoiceBatchPrintContent() {
</Button>
</div>
<div className="border-b pb-4">
<div className="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground">Sales Invoices</div>
<h1 className="mt-2 text-3xl font-semibold text-foreground">Invoice Batch Print</h1>
<p className="mt-2 text-sm text-muted-foreground">Printed register snapshot of current invoices.</p>
<div className={`md:grid ${layout.arrangement === "compact" ? "" : "md:grid-cols-[1.4fr_1fr]"} ${layout.header}`}>
{layout.arrangement === "highlight" && <InvoicePrintHeader company={company} template={template} />}
<div>
<div className={layout.eyebrow}>Sales Invoices</div>
<h1 className={layout.title}>Invoice Batch Print</h1>
<p className={layout.meta}>Printed register snapshot of current invoices.</p>
</div>
{layout.arrangement !== "highlight" && <InvoicePrintHeader company={company} template={template} />}
</div>
<div className="overflow-hidden rounded-2xl border">
<div className={layout.tableWrap}>
<Table>
<TableHeader>
<TableRow>
<TableRow className={layout.tableHeaderRow}>
<TableHead>Invoice</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Date</TableHead>