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,52 @@
|
||||
import { CompanyProfile } from "@/types/company-profile"
|
||||
import { InvoiceTemplateConfig, InvoiceTemplateStyle } from "@/types/invoice-template"
|
||||
|
||||
interface InvoicePrintFooterProps {
|
||||
company: CompanyProfile | null
|
||||
template: InvoiceTemplateConfig | null
|
||||
}
|
||||
|
||||
// A real (not purely cosmetic) effect of the template style: how much breathing room and
|
||||
// accent the footer block gets, from denser to more formal.
|
||||
const styleClass: Record<InvoiceTemplateStyle, string> = {
|
||||
Compact: "border-t pt-3 gap-2",
|
||||
Classic: "border-t pt-5 gap-3",
|
||||
Modern: "border-t-2 border-t-primary pt-5 gap-3",
|
||||
}
|
||||
|
||||
/** Bank details, terms & conditions, and footer note for an invoice print view — sourced from
|
||||
* the shared Invoice Template Config and Company Profile. Used by both the Sales Invoice and
|
||||
* Purchase Invoice print pages. */
|
||||
export function InvoicePrintFooter({ company, template }: InvoicePrintFooterProps) {
|
||||
if (!template) return null
|
||||
|
||||
const hasBankDetails =
|
||||
template.showBankDetails && company && (company.bankName || company.accountNumber)
|
||||
|
||||
if (!hasBankDetails && !template.termsAndConditions.trim() && !template.footerNote.trim()) return null
|
||||
|
||||
return (
|
||||
<div className={`invoice-footer flex flex-col ${styleClass[template.templateStyle]}`}>
|
||||
{hasBankDetails && (
|
||||
<div className="grid gap-1 text-sm">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Bank details</div>
|
||||
{company!.bankName && <div className="text-muted-foreground">Bank: {company!.bankName}{company!.bankBranch ? ` — ${company!.bankBranch}` : ""}</div>}
|
||||
{company!.accountName && <div className="text-muted-foreground">Account name: {company!.accountName}</div>}
|
||||
{company!.accountNumber && <div className="text-muted-foreground">Account number: {company!.accountNumber}</div>}
|
||||
{company!.swiftCode && <div className="text-muted-foreground">SWIFT: {company!.swiftCode}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template.termsAndConditions.trim() && (
|
||||
<div className="grid gap-1 text-sm">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Terms & conditions</div>
|
||||
<p className="whitespace-pre-wrap text-muted-foreground">{template.termsAndConditions}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template.footerNote.trim() && (
|
||||
<p className="whitespace-pre-wrap text-xs text-muted-foreground">{template.footerNote}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CompanyProfile } from "@/types/company-profile"
|
||||
import { InvoiceTemplateConfig, LogoPlacement } from "@/types/invoice-template"
|
||||
|
||||
interface InvoicePrintHeaderProps {
|
||||
company: CompanyProfile | null
|
||||
template: InvoiceTemplateConfig | null
|
||||
}
|
||||
|
||||
/** Company identity block for an invoice print view — logo (per template.logoPlacement),
|
||||
* name/address/contact, and tax reg no (only when template.showTaxRegNo). Used by both the
|
||||
* Sales Invoice and Purchase Invoice print pages so they render from one shared source. */
|
||||
export function InvoicePrintHeader({ company, template }: InvoicePrintHeaderProps) {
|
||||
if (!company) {
|
||||
return (
|
||||
<div className="grid gap-2 text-sm md:justify-items-end">
|
||||
<div className="font-semibold text-foreground">Company details are not configured yet.</div>
|
||||
<div className="text-muted-foreground">Set them up under Settings → Company Profile.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const placement: LogoPlacement = template?.logoPlacement ?? "None"
|
||||
const showTaxRegNo = template?.showTaxRegNo ?? false
|
||||
const hasLogo = placement !== "None" && !!company.logoUrl
|
||||
|
||||
// Logo always sits on its own line above the address block (never beside it) — only
|
||||
// its horizontal position changes with placement, and the address text below matches
|
||||
// that same alignment so the whole block reads as one column, not two misaligned ones.
|
||||
const alignmentClasses: Record<LogoPlacement, string> = {
|
||||
Left: "justify-items-start text-left",
|
||||
Center: "justify-items-center text-center",
|
||||
Right: "justify-items-end text-right",
|
||||
None: "md:justify-items-end md:text-right",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`grid gap-2 text-sm ${alignmentClasses[placement]}`}>
|
||||
{hasLogo && (
|
||||
// eslint-disable-next-line @next/next/no-img-element -- print view, no next/image optimization needed
|
||||
<img src={company.logoUrl!} alt={company.legalName} className="h-12 w-auto object-contain" />
|
||||
)}
|
||||
<div className="font-semibold text-foreground">{company.tradeName || company.legalName}</div>
|
||||
<div className="text-muted-foreground">{company.addressLine1}</div>
|
||||
{company.addressLine2 && <div className="text-muted-foreground">{company.addressLine2}</div>}
|
||||
<div className="text-muted-foreground">{[company.city, company.country].filter(Boolean).join(", ")}</div>
|
||||
{company.phone && <div className="text-muted-foreground">{company.phone}</div>}
|
||||
{company.email && <div className="text-muted-foreground">{company.email}</div>}
|
||||
{showTaxRegNo && company.taxRegistrationNo && (
|
||||
<div className="text-muted-foreground">Tax Reg No: {company.taxRegistrationNo}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { InvoiceTemplateStyle } from "@/types/invoice-template"
|
||||
|
||||
/** Which document structure a style renders as — not just color/spacing, the actual
|
||||
* placement of blocks on the page:
|
||||
* - "standard": title+doc no. on the left, company block on the right, then a 3-up
|
||||
* info row (party, second party detail, totals card), then the line table.
|
||||
* - "compact": title and company stacked into one dense strip, info collapsed into a
|
||||
* single inline summary line instead of three cards, to fit more on one page.
|
||||
* - "highlight": company block leads (left), title/doc no. trails (right) — reversed
|
||||
* from standard — then a full-width totals banner ahead of the line table instead of
|
||||
* a totals card buried in the 3-up row, with party/second-party as a 2-up row below it.
|
||||
*/
|
||||
export type InvoiceArrangement = "standard" | "compact" | "highlight"
|
||||
|
||||
/** Layout classes + arrangement that make each `InvoiceTemplateStyle` a genuinely
|
||||
* different print template (structure, density, accenting) rather than a cosmetic
|
||||
* tweak. Shared by the Sales Invoice and Purchase Invoice print pages so a style change
|
||||
* rearranges both document types identically. */
|
||||
export interface InvoiceLayoutClasses {
|
||||
arrangement: InvoiceArrangement
|
||||
sheet: string
|
||||
header: string
|
||||
eyebrow: string
|
||||
title: string
|
||||
meta: string
|
||||
infoGrid: string
|
||||
card: string
|
||||
cardLabel: string
|
||||
cardValue: string
|
||||
tableWrap: string
|
||||
tableHeaderRow: string
|
||||
totalsHighlight: string
|
||||
totalsBanner: string
|
||||
totalsBannerLabel: string
|
||||
totalsBannerValue: string
|
||||
}
|
||||
|
||||
const presets: Record<InvoiceTemplateStyle, InvoiceLayoutClasses> = {
|
||||
Classic: {
|
||||
arrangement: "standard",
|
||||
sheet: "gap-6 p-6",
|
||||
header: "gap-4 border-b pb-5",
|
||||
eyebrow: "text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground",
|
||||
title: "mt-2 text-3xl font-semibold text-foreground",
|
||||
meta: "mt-2 text-sm text-muted-foreground",
|
||||
infoGrid: "gap-4",
|
||||
card: "rounded-2xl border bg-muted/20 p-4",
|
||||
cardLabel: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
|
||||
cardValue: "mt-2 text-lg font-semibold text-foreground",
|
||||
tableWrap: "overflow-hidden rounded-2xl border",
|
||||
tableHeaderRow: "",
|
||||
totalsHighlight: "font-semibold text-foreground",
|
||||
totalsBanner: "hidden",
|
||||
totalsBannerLabel: "",
|
||||
totalsBannerValue: "",
|
||||
},
|
||||
Compact: {
|
||||
arrangement: "compact",
|
||||
sheet: "gap-3 p-4",
|
||||
header: "gap-1 border-b pb-2",
|
||||
eyebrow: "text-[10px] font-semibold uppercase tracking-widest text-muted-foreground",
|
||||
title: "text-xl font-semibold text-foreground",
|
||||
meta: "text-xs text-muted-foreground",
|
||||
infoGrid: "gap-x-6 gap-y-1",
|
||||
card: "rounded-md border bg-muted/10 p-2.5",
|
||||
cardLabel: "text-[10px] font-semibold uppercase tracking-wide text-muted-foreground",
|
||||
cardValue: "mt-1 text-sm font-semibold text-foreground",
|
||||
tableWrap: "overflow-hidden rounded-md border",
|
||||
tableHeaderRow: "",
|
||||
totalsHighlight: "font-semibold text-foreground",
|
||||
totalsBanner: "hidden",
|
||||
totalsBannerLabel: "",
|
||||
totalsBannerValue: "",
|
||||
},
|
||||
Modern: {
|
||||
arrangement: "highlight",
|
||||
sheet: "gap-6 p-6",
|
||||
header: "gap-4 border-b-2 border-b-primary pb-5",
|
||||
eyebrow: "text-xs font-semibold uppercase tracking-[0.3em] text-primary",
|
||||
title: "mt-2 text-3xl font-bold text-foreground",
|
||||
meta: "mt-2 text-sm text-muted-foreground",
|
||||
infoGrid: "gap-4",
|
||||
card: "rounded-2xl border border-primary/20 bg-primary/5 p-4",
|
||||
cardLabel: "text-xs font-semibold uppercase tracking-wide text-primary",
|
||||
cardValue: "mt-2 text-lg font-semibold text-foreground",
|
||||
tableWrap: "overflow-hidden rounded-2xl border border-primary/20",
|
||||
tableHeaderRow: "bg-primary/10 [&_th]:text-primary",
|
||||
totalsHighlight: "font-semibold text-primary",
|
||||
totalsBanner: "flex items-center justify-between rounded-2xl bg-primary px-5 py-4 text-primary-foreground",
|
||||
totalsBannerLabel: "text-xs font-semibold uppercase tracking-[0.2em] text-primary-foreground/80",
|
||||
totalsBannerValue: "text-2xl font-bold text-primary-foreground",
|
||||
},
|
||||
}
|
||||
|
||||
export function getInvoiceLayoutClasses(style: InvoiceTemplateStyle | undefined): InvoiceLayoutClasses {
|
||||
return presets[style ?? "Classic"]
|
||||
}
|
||||
Reference in New Issue
Block a user