import { Image, View } from "@react-pdf/renderer"; import { pinned } from "../engine/columns"; import { textLines } from "../engine/textplan"; import { roleToken, type RoleName, type TypeToken } from "../fonts/roles"; import type { RenderModel } from "../model/build"; import { useAvailableWidth } from "./AvailableWidth"; import { IdText } from "./IdText"; import { Row } from "./Row"; import { SafeTxt } from "./SafeTxt"; import { Txt } from "./Txt"; /* * Regulated data (GSTIN, place of supply, HSN/SAC, tax rows, amount in words, signatory, reverse * charge) is printed ONLY through these components, so one contract test can prove that every * template carries what a document type requires (model/slots.ts) and nothing it forbids. Each slot * stamps an id (`slot-*`) on its output so the test finds it in the layout tree. Templates must not * read these model fields themselves (slots.lint.test.ts). */ export const SLOT_ID = { gstin: "slot-gstin", pos: "slot-pos", taxRows: "slot-tax-rows", amountInWords: "slot-words", signatory: "slot-signatory", reverseCharge: "slot-reverse-charge", } as const; type Role = RoleName | TypeToken; const INK_SUB = "#393939"; const FINE = "#6f6f6f"; const SIGNATURE_RULE = "#8d8d8d"; function Gstin({ model, party = "supplier", role = "body", color }: { model: RenderModel; party?: "supplier" | "client"; role?: Role; color?: string }) { const value = party === "supplier" ? (model.slots.gstin ? model.supplier.gstin : null) : model.client.gstin; if (!value) return null; return ( {`GSTIN: ${value}`} ); } function PlaceOfSupply({ model, role = "body", color }: { model: RenderModel; role?: Role; color?: string }) { if (!model.client.posLabel) return null; return ( {`Place of Supply: ${model.client.posLabel}`} ); } /** The HSN/SAC column: header and one value per item row, or null when no item carries a code. */ function hsnSacColumn(model: RenderModel): { header: string; values: string[] } | null { if (!model.slots.hsn) return null; return { header: "HSN/SAC", values: model.items.map((i) => i.hsnSac ?? "—") }; } export interface TaxRowsProps { model: RenderModel; role?: Role; amountWidth: number; rowStyle?: Record; } /** CGST/SGST (UTGST) or IGST rows, as label (rate) on the left and the amount on the right. */ function TaxRows({ model, role = "body", amountWidth, rowStyle }: TaxRowsProps) { const w = useAvailableWidth(); if (model.totals.taxRows.length === 0) return null; return ( {model.totals.taxRows.map((r, i) => ( {r.rateText ? `${r.label} (${r.rateText})` : r.label} {r.amountText} ))} ); } function AmountInWords({ model, role = "body", style }: { model: RenderModel; role?: Role; style?: Record }) { if (!model.amountInWords) return null; return ( {`Amount in words: ${model.amountInWords}`} ); } function ReverseCharge({ model, role = "body", color, style }: { model: RenderModel; role?: Role; color?: string; style?: Record }) { if (!model.reverseCharge) return null; return ( {model.reverseCharge} ); } /** Predicted heights of the slots that sit in the totals block, for its presence value. */ export const slotHeights = { /** Number of tax rows (each is one body line plus the template's row padding). */ taxRowCount: (model: RenderModel): number => model.totals.taxRows.length, amountInWords: (model: RenderModel, width: number): number => model.amountInWords ? textLines("body", `Amount in words: ${model.amountInWords}`, width, { italic: true }) * (roleToken("body").leading as number) : 0, reverseCharge: (model: RenderModel, width: number): number => model.reverseCharge ? textLines("small", model.reverseCharge, width) * (roleToken("small").leading as number) : 0, }; export const SIGNATURE_IMAGE_H = 58; const FINE_LINE = "Computer-generated invoice; no signature is required."; /** Height of the signatory block at `width`, for the closing block's presence value. */ export function signatoryHeight(model: RenderModel, width: number): number { const leading = (r: Role) => roleToken(r).leading as number; const { name, designation, dataUri } = model.signature; let h = (dataUri ? SIGNATURE_IMAGE_H : 0) + 4 + 0.8 + 3; h += textLines("bodyStrong", name, width) * leading("bodyStrong"); if (designation) h += textLines("body", designation, width) * leading("body"); if (!dataUri) h += 6 + textLines("fine", FINE_LINE, width) * leading("fine"); return h; } /** * Signature image (if any), a rule, the signatory's name and designation. Without an image the * "computer-generated" line stands in for the signature. */ function Signatory({ model, width }: { model: RenderModel; width?: number }) { const avail = useAvailableWidth(); const w = width ?? avail; const { name, designation, dataUri } = model.signature; return ( {dataUri ? ( // Pinned box (an Image may be pinned); the picture sits at its right and bottom edge, just above the rule. ) : null} {name} {designation ? ( {designation} ) : null} {!dataUri ? ( {FINE_LINE} ) : null} ); } export const Slot = { Gstin, PlaceOfSupply, HsnSac: hsnSacColumn, TaxRows, AmountInWords, Signatory, ReverseCharge };