import { Image, View } from "@react-pdf/renderer";
import type { ReactNode } from "react";
import { colsDetailed, pinned } from "../../engine/columns";
import { CLOSING_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, TOTALS_ID } from "../../engine/ids";
import { AvailableWidthProvider } from "../../blocks/AvailableWidth";
import { BottomSpacer } from "../../blocks/BottomSpacer";
import { FirstPageHeader } from "../../blocks/FirstPageHeader";
import { FitText } from "../../blocks/FitText";
import { IdText } from "../../blocks/IdText";
import { Cell, Row } from "../../blocks/Row";
import { SafeTxt } from "../../blocks/SafeTxt";
import { pdfFlags } from "../../blocks/pdfProps";
import { Slot } from "../../blocks/slots";
import { TableGuard } from "../../blocks/TableGuard";
import { Txt } from "../../blocks/Txt";
import { ContactIcon } from "../../decor/CircledIcon";
import { META_ID, SUPPLIER_ID, TITLE_ID } from "../classic/ids";
import type { PlacedLogo, TemplateProps } from "../contract";
import { includesName } from "../logoName";
import { CANCELLED_INK, CANCELLED_SENTENCE } from "../neutral/plan";
import type { CellPart, TableCellValue } from "../../blocks/ItemsTable";
import { LABEL_GAP, NOTES_FLOW_GAP, paymentLines, planHighlighter, REVERSE_GAP, signatureOptions, taxLabel, type HighlighterPlan } from "./plan";
import { BLACK, INDIGO, YELLOW, type HighlighterVariant } from "./tokens";
/** Layout-tree ids of the blocks that are Highlighter's own (the harness ids are in classic/ids.ts). */
export const PAY_ID = "highlighter-pay";
export const PAD_PREFIX = "highlighter-pad-";
/** A block that starts with less than a line of room left is cut into zero-height lines: it starts on the next page instead. */
const NOTES_HOLD = 70;
interface Ctx {
model: TemplateProps["model"];
plan: HighlighterPlan;
v: HighlighterVariant;
logo: PlacedLogo | null;
}
function Spacer({ h }: { h: number }) {
return h > 0 ? : null;
}
// --- header -----------------------------------------------------------------------------------------------------
function Title({ ctx }: { ctx: Ctx }) {
const { model, plan, v } = ctx;
const h = plan.head;
return (
{model.docTitle}
);
}
/** The vendor at the top right: the logo (right-aligned at the slot's top), the name, GSTIN and PAN. */
function Vendor({ ctx }: { ctx: Ctx }) {
const { model, plan, v, logo } = ctx;
const h = plan.head;
const r = v.roles;
const s = model.supplier;
return (
{logo ? : null}
{h.showName ? (
{s.name}
) : null}
{s.pan ? (
{`PAN: ${s.pan}`}
) : null}
{model.cancelled ? (
CANCELLED
) : null}
);
}
function HeaderRow({ ctx }: { ctx: Ctx }) {
const { plan, v } = ctx;
return (
);
}
// --- fields and payment -----------------------------------------------------------------------------------------
function Field({ ctx, index }: { ctx: Ctx; index: number }) {
const { plan, v } = ctx;
const f = v.fields;
const spec = plan.fields.specs[index];
const labelW = plan.fields.labelW[index];
const valueW = f.w - labelW - f.labelGap;
return (
0 ? f.rowGap : 0 }}>
{spec.label}
{spec.value}
);
}
function Fields({ ctx }: { ctx: Ctx }) {
const { model, plan, v } = ctx;
const r = v.roles;
const c = model.client;
const f = v.fields;
return (
{plan.fields.specs.map((_, i) => (
))}
{plan.fields.subH > 0 ? (
{c.addressLines.map((line, i) => (
{line}
))}
{c.poNumber ? {`PO: ${c.poNumber}`} : null}
) : null}
);
}
function Payment({ ctx }: { ctx: Ctx }) {
const { model, v } = ctx;
const r = v.roles;
const lines = paymentLines(model);
if (lines.length === 0) return ;
const w = v.pay.w;
return (
Payment Method:
{lines.map((line, i) => (
{line}
))}
);
}
// --- table ------------------------------------------------------------------------------------------------------
const partsOf = (value: TableCellValue): CellPart[] => (Array.isArray(value) ? (value.length ? value : [""]) : [value]);
/** One table line inside the 3 pt frame: the frame's sides are the line's own borders, the dividers are its cells'. */
function Line({
ctx,
id,
last,
flags,
minH,
children,
}: {
ctx: Ctx;
id: string;
last: boolean;
flags: ReturnType;
minH?: number;
children: ReactNode;
}) {
const { plan, v } = ctx;
return (
{children}
);
}
function colInner(ctx: Ctx, i: number): number {
const { plan, v } = ctx;
const w = plan.table.widths[i];
const last = i === plan.columns.length - 1;
return Math.max(0, w - plan.padLeft[i] - (plan.padRight[i] - (last ? 0 : v.hair)) - (last ? 0 : v.hair));
}
function BodyCell({ ctx, i, children }: { ctx: Ctx; i: number; children?: ReactNode }) {
const { plan, v } = ctx;
const last = i === plan.columns.length - 1;
return (
{children}
|
);
}
function DataRow({ ctx, index, lastRow }: { ctx: Ctx; index: number; lastRow: boolean }) {
const { plan, v } = ctx;
const r = v.roles;
const cells = plan.rows[index];
return (
{cells.map((value, i) => (
{partsOf(value).map((part, li) => {
const text = typeof part === "string" ? part : part.text;
const role = typeof part === "string" ? r.tableBody : (part.role as typeof r.tableBody);
return i === 0 && typeof part === "string" ? (
{text}
) : (
{text}
);
})}
))}
);
}
function PadRow({ ctx, index, lastRow }: { ctx: Ctx; index: number; lastRow: boolean }) {
const { plan, v } = ctx;
return (
{plan.columns.map((_, i) => (
))}
);
}
/** The header group, repeated on every page: the yellow bar, the gap, and the body's 3 pt top rule. */
function HeaderGroup({ ctx }: { ctx: Ctx }) {
const { plan, v } = ctx;
const t = v.table;
const r = v.roles;
const fr = v.frame;
return (
{plan.headerCells.map((text, i) => {
const last = i === plan.columns.length - 1;
return (
|
{text}
|
);
})}
);
}
function Table({ ctx }: { ctx: Ctx }) {
const { plan } = ctx;
const n = plan.rows.length;
return (
{plan.rows.map((_, i) => (
))}
{Array.from({ length: plan.padRows }, (_, i) => (
))}
);
}
// --- totals -----------------------------------------------------------------------------------------------------
function TotalRow({ ctx, label, amount, total }: { ctx: Ctx; label: string; amount: string; total?: boolean }) {
const { plan, v } = ctx;
const t = plan.tail;
const r = v.roles;
const o = v.totals;
const pad = { paddingTop: o.padY, paddingBottom: o.padY };
return (
{label}
{amount}
);
}
function TotalsBox({ ctx }: { ctx: Ctx }) {
const { model, plan, v } = ctx;
const t = plan.tail;
return (
{t.stack.map((row, i) => (
))}
} />
);
}
function Tail({ ctx }: { ctx: Ctx }) {
const { model, plan, v } = ctx;
const t = plan.tail;
const r = v.roles;
const o = v.totals;
return (
{t.reverseH > 0 ? 0 ? REVERSE_GAP : 0 }} /> : null}
);
}
// --- closing ----------------------------------------------------------------------------------------------------
/** Notes in flow mode (too long for the ruled lines): a label held with the first line, every line unbreakable. */
function NotesFlow({ ctx, width }: { ctx: Ctx; width: number }) {
const { plan, v } = ctx;
const r = v.roles;
const lines = plan.closing.notes;
if (lines.length === 0) return null;
return (
Notes:
{lines[0]}
{lines.slice(1).map((line, i) => (
{line}
))}
);
}
function SignOff({ ctx }: { ctx: Ctx }) {
const { plan, v } = ctx;
const r = v.roles;
const cl = v.closing;
const c = plan.closing;
const w = c.leftW - v.inset;
return (
THANK YOU
{c.contacts.map((x, i) => (
0 ? cl.contactGap : 0 }}>
{x.icon === "location" ? (
{x.text}
) : (
// A phone number or e-mail is an identifier: one fitted line (shrunk, then split at "@ . / -"), never a wrapped one.
{x.text}
)}
))}
);
}
/** The design's ruled lines: the notes written on them (a line per wrapped line), then the signature on the last one. */
function Ruled({ ctx }: { ctx: Ctx }) {
const { model, plan, v } = ctx;
const r = v.roles;
const cl = v.closing;
const c = plan.closing;
const opts = signatureOptions(v);
return (
{Array.from({ length: c.ruledRows }, (_, i) => (
{!c.notesFlow && c.ruled[i] !== undefined ? (
{c.ruled[i]}
) : null}
))}
);
}
function Closing({ ctx }: { ctx: Ctx }) {
const { model, plan, v } = ctx;
const cl = v.closing;
const c = plan.closing;
return (
<>
{model.cancelled ? (
{CANCELLED_SENTENCE}
) : null}
>
);
}
// --- the layout -------------------------------------------------------------------------------------------------
export function HighlighterBody({ model, frame, logo, prefs, variant: v }: TemplateProps & { variant: HighlighterVariant }) {
const plan = planHighlighter(model, frame, v, logo, includesName({ model, prefs }));
const ctx: Ctx = { model, plan, v, logo };
const notesFlow: ReactNode = !plan.closing.notesFlow ? null : (
<>
>
);
return (
<>
{notesFlow}
>
);
}