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 };