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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user