/* ════════════════════════════════════════════════════════ NASAMA PROPERTIES — PRINT DOCUMENT TEMPLATES Purpose-built for print/PDF — completely separate from the screen UI. These components produce a classic A4 financial statement document. They are NEVER shown on screen (hidden via CSS; revealed only in @media print). ════════════════════════════════════════════════════════ */ // ── Compact number formatter (no "AED" prefix — column header carries the unit) const pFmt = c => ((c || 0) / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, }); // ── Design tokens for the print document // Values are px — the browser handles pt conversion for printing. // Target: ~10–11pt body, ~18–20pt title when printed at A4. const PD = { // Typefaces serif: "'Georgia', 'Times New Roman', Times, serif", sans: "'Inter', Arial, Helvetica, sans-serif", mono: "'ui-monospace', 'Courier New', Courier, monospace", // Ink palette — restrained, almost monochrome navy: "#0C0F1E", gold: "#A8853A", // subdued gold (print-safe) ink: "#000000", inkDk: "#111111", inkMd: "#2C2C2C", inkLt: "#4A4A4A", inkSub: "#6B6B6B", rule: "#BBBBBB", ruleLt: "#DEDEDE", // Semantic (used only on totals/balance) green: "#065F46", amber: "#78350F", red: "#991B1B", // Sizes fXs: 9.5, fSm: 10.5, fBase: 11.5, fMd: 12.5, fLg: 14, fXl: 18, fXxl: 24, // Print colour preservation pca: { WebkitPrintColorAdjust: "exact", printColorAdjust: "exact", }, }; // ── Shared 3-column layout for financial tables // Columns: [Code 60px] [Description flex] [Amount 148px] function DocCols() { return ( ); } /* ════════════════════════════════════════════════════════ DOCUMENT HEADER ════════════════════════════════════════════════════════ */ function PrintDocHeader({ company, title, periodLine, currency, trn }) { const generated = new Date().toLocaleString("en-GB", { day: "2-digit", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }); return (
{/* ── Identity row: logo + company name */}
{company}
{trn && (
Tax Registration No. {trn}
)}
{/* ── Double rule — structural signature of the document */}
{/* ── Report title block — centred */}
{title}
{periodLine}
{/* ── Meta bar */}
Currency: {currency || "AED"} {trn && TRN: {trn}} Generated: {generated}
); } /* ════════════════════════════════════════════════════════ SECTION LABEL (e.g. "REVENUE", "EXPENSES") ════════════════════════════════════════════════════════ */ function PrintSectionHead({ label }) { return (
{label} AED
); } /* ════════════════════════════════════════════════════════ ACCOUNT ROW TABLE ════════════════════════════════════════════════════════ */ function PrintAccountRows({ accounts, ledger, emptyMsg }) { const rows = (accounts || []).filter(a => accountBalance(a, ledger) !== 0); if (rows.length === 0) { return (
{emptyMsg || "No activity in this period"}
); } return ( {rows.map(a => ( {/* Account code */} {/* Account name */} {/* Amount */} ))}
{a.code} {a.name} {pFmt(accountBalance(a, ledger))}
); } /* ════════════════════════════════════════════════════════ TOTAL / SUBTOTAL ROW Uses accounting-convention rule lines. isGrand = double underline below (grand total) ════════════════════════════════════════════════════════ */ function PrintTotalRow({ label, amount, color, isGrand }) { const topRule = "0.75px solid " + PD.rule; const grandTopRule = "1.5px solid " + PD.inkDk; const grandBotRule = "3px double " + PD.inkDk; const cellBase = { borderTop: isGrand ? grandTopRule : topRule, borderBottom: isGrand ? grandBotRule : "none", padding: (isGrand ? "8px" : "5px") + " 0", fontFamily: PD.sans, }; return (
{label} {pFmt(amount)}
); } /* ════════════════════════════════════════════════════════ NET INCOME CONCLUSION BLOCK ════════════════════════════════════════════════════════ */ function PrintNetIncome({ amount }) { const positive = amount >= 0; const amtColor = positive ? PD.green : PD.red; return (
{/* Double opening rule */}
Net Income {pFmt(amount)}
{/* Double closing rule */}
); } /* ════════════════════════════════════════════════════════ DOCUMENT FOOTER NOTE (certification line) ════════════════════════════════════════════════════════ */ function PrintDocNote({ company }) { return (
Prepared by {company} · Accounting System v2
This statement is computer-generated and has not been audited.
Company Stamp
); } /* ════════════════════════════════════════════════════════ P&L PRINT DOCUMENT ════════════════════════════════════════════════════════ */ function PLPrintDoc({ accounts, filteredLedger, totalRev, totalExp, dateFilter, settings }) { const netIncome = totalRev - totalExp; const company = settings?.company || "Nasama Properties"; const currency = settings?.currency || "AED"; const trn = settings?.trn; const periodLine = dateFilter.from && dateFilter.to ? `For the period ${fmtDate(dateFilter.from)} to ${fmtDate(dateFilter.to)}` : dateFilter.to ? `As of ${fmtDate(dateFilter.to)}` : "All Periods"; const revenueRows = (accounts || []) .filter(a => a.type === "Revenue" && accountBalance(a, filteredLedger) !== 0); const expenseRows = (accounts || []) .filter(a => a.type === "Expense" && accountBalance(a, filteredLedger) !== 0) .sort((a, b) => accountBalance(b, filteredLedger) - accountBalance(a, filteredLedger)); return (
{/* ── REVENUE ── */} {/* ── EXPENSES ── */} {/* ── NET INCOME ── */}
); } /* ════════════════════════════════════════════════════════ BALANCE SHEET PRINT DOCUMENT ════════════════════════════════════════════════════════ */ function BSPrintDoc({ accounts, toDateLedger, totalAssets, totalLiabilities, totalEquity, netIncome, dateFilter, settings, }) { const company = settings?.company || "Nasama Properties"; const currency = settings?.currency || "AED"; const trn = settings?.trn; const totalLE = totalLiabilities + totalEquity + netIncome; const balanced = Math.abs(totalAssets - totalLE) <= 1; const assetRows = (accounts || []).filter(a => a.type === "Asset" && accountBalance(a, toDateLedger) !== 0); const liabRows = (accounts || []).filter(a => a.type === "Liability" && accountBalance(a, toDateLedger) !== 0); const equityRows = (accounts || []).filter(a => a.type === "Equity" && accountBalance(a, toDateLedger) !== 0); const periodLine = `As of ${dateFilter.to ? fmtDate(dateFilter.to) : "today"}`; return (
{/* Retained earnings inline row */}
Retained Earnings (Net Income to date) {pFmt(netIncome)}
{/* Summary: Total L + E */}
{/* Balance check */}
{balanced ? "✓ Balance sheet is balanced — Assets = Liabilities + Equity" : "✗ Balance sheet is NOT balanced — Difference: " + fmtAED(Math.abs(totalAssets - totalLE))}
); } /* ════════════════════════════════════════════════════════ TRIAL BALANCE PRINT DOCUMENT ════════════════════════════════════════════════════════ */ function TBPrintDoc({ accounts, filteredLedger, dateFilter, settings }) { const company = settings?.company || "Nasama Properties"; const currency = settings?.currency || "AED"; const trn = settings?.trn; const periodLine = dateFilter.from && dateFilter.to ? `For the period ${fmtDate(dateFilter.from)} to ${fmtDate(dateFilter.to)}` : "All Periods"; const rows = (accounts || []).slice() .sort((a, b) => a.code.localeCompare(b.code)) .filter(a => { const e = filteredLedger[a.id] || { debit: 0, credit: 0 }; return e.debit > 0 || e.credit > 0; }) .map(a => { const e = filteredLedger[a.id] || { debit: 0, credit: 0 }; const nb = NORMAL_BAL[a.type]; const bal = nb === "debit" ? e.debit - e.credit : e.credit - e.debit; return { id: a.id, code: a.code, name: a.name, type: a.type, debit: nb === "debit" && bal !== 0 ? bal : 0, credit: nb === "credit" && bal !== 0 ? bal : 0, }; }); const totalDebit = rows.reduce((s, r) => s + r.debit, 0); const totalCredit = rows.reduce((s, r) => s + r.credit, 0); const isBalanced = Math.abs(totalDebit - totalCredit) <= 1; const thTB = (extra = {}) => ({ padding: "5px 8px 5px 0", fontSize: PD.fXs, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.09em", color: PD.navy, borderBottom: "1.5px solid " + PD.navy, whiteSpace: "nowrap", fontFamily: PD.sans, ...extra, }); const tdTB = (extra = {}) => ({ padding: "4px 8px 4px 0", fontSize: PD.fBase, borderBottom: "0.5px solid " + PD.ruleLt, verticalAlign: "top", fontFamily: PD.sans, ...extra, }); return (
{rows.length === 0 && ( )} {rows.map(row => ( ))}
Code Account Name Type Debit (AED) Credit (AED)
No transactions in this period
{row.code} {row.name} {row.type} {row.debit ? pFmt(row.debit) : "—"} {row.credit ? pFmt(row.credit) : "—"}
Totals {pFmt(totalDebit)} {pFmt(totalCredit)}
{isBalanced ? "✓ Trial balance is balanced" : "✗ Out of balance by " + fmtAED(Math.abs(totalDebit - totalCredit))}
); } /* ════════════════════════════════════════════════════════ GENERAL LEDGER PRINT DOCUMENT ════════════════════════════════════════════════════════ */ function GLPrintDoc({ accounts, txns, filteredTxns, dateFilter, settings, selectedId, typeFilter, searchText }) { const company = settings?.company || "Nasama Properties"; const currency = settings?.currency || "AED"; const trn = settings?.trn; const periodLine = dateFilter.from && dateFilter.to ? `For the period ${fmtDate(dateFilter.from)} to ${fmtDate(dateFilter.to)}` : "All Periods"; const groups = React.useMemo(() => { const acctIds = new Set( (filteredTxns || []) .filter(t => !t.isVoid) .flatMap(t => (t.lines || []).map(l => l.accountId)) ); return (accounts || []) .filter(a => acctIds.has(a.id)) .sort((a, b) => a.code.localeCompare(b.code)) .map(acct => { const nb = NORMAL_BAL[acct.type]; let priorBalance = 0; if (dateFilter.from) { (txns || []) .filter(t => !t.isVoid && (t.date || "") < dateFilter.from) .forEach(t => { (t.lines || []).forEach(l => { if (l.accountId !== acct.id) return; const dr = l.debit || 0, cr = l.credit || 0; priorBalance += nb === "debit" ? dr - cr : cr - dr; }); }); } let runningBal = priorBalance; const rows = (filteredTxns || []) .filter(t => !t.isVoid) .sort((a, b) => (a.date || "").localeCompare(b.date || "")) .flatMap(t => (t.lines || []) .filter(l => l.accountId === acct.id) .map(l => { const dr = l.debit || 0, cr = l.credit || 0; runningBal += nb === "debit" ? dr - cr : cr - dr; return { date: t.date, ref: t.ref, desc: l.memo || t.description || "—", debit: dr, credit: cr, balance: runningBal, key: t.id + "-" + l.id, }; }) ); return { acct, nb, openingBal: priorBalance, rows, closing: runningBal }; }); }, [accounts, txns, filteredTxns, dateFilter.from]); // Apply the same account filters that the screen is showing const filteredGroups = React.useMemo(() => { return groups.filter(g => { if (selectedId && g.acct.id !== selectedId) return false; if (typeFilter && typeFilter !== "All" && g.acct.type !== typeFilter) return false; if (searchText && searchText.trim()) { const q = searchText.trim().toLowerCase(); if (!g.acct.code.toLowerCase().includes(q) && !g.acct.name.toLowerCase().includes(q)) return false; } return true; }); }, [groups, selectedId, typeFilter, searchText]); const thGL = (extra = {}) => ({ padding: "5px 8px 5px 0", fontSize: PD.fXs, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", color: PD.navy, borderBottom: "1.5px solid " + PD.navy, whiteSpace: "nowrap", fontFamily: PD.sans, ...extra, }); const tdGL = (extra = {}) => ({ padding: "3.5px 8px 3.5px 0", fontSize: PD.fBase, borderBottom: "0.5px solid " + PD.ruleLt, verticalAlign: "top", fontFamily: PD.sans, ...extra, }); return (
{filteredGroups.length === 0 && (
No transactions found for the selected period.
)} {filteredGroups.map((g, gi) => { const totalDr = g.rows.reduce((s, r) => s + r.debit, 0); const totalCr = g.rows.reduce((s, r) => s + r.credit, 0); return (
0 ? 30 : 0, pageBreakInside: "avoid" }}> {/* Account heading */}
{g.acct.code} — {g.acct.name} {g.acct.type}
{/* 6-column table — tightly sized to fit A4 portrait (180mm ≈ 680px available) Fixed cols total ≈ 362px; description takes the remaining ~318px. Each number cell has explicit overflow:hidden so nothing bleeds right. */} {/* Date */} {/* Ref */} {/* Description */} {/* Debit */} {/* Credit */} {/* Balance */} {/* Opening balance row */} {/* Transaction rows */} {g.rows.map(row => { const refShort = row.ref ? (row.ref.length > 10 ? row.ref.slice(0, 9) + "…" : row.ref) : "—"; return ( {/* Debit — always dark when non-zero; show dash only in muted */} {/* Credit */} {/* Running balance */} ); })}
Date Ref Description Debit Credit Balance
{dateFilter.from ? fmtDate(dateFilter.from) : "—"} Opening Balance {pFmt(g.openingBal)}
{fmtDate(row.date)} {refShort} {row.desc} {row.debit ? pFmt(row.debit) : "—"} {row.credit ? pFmt(row.credit) : "—"} = 0 ? PD.inkDk : PD.red, ...PD.pca })}> {pFmt(row.balance)}
Closing Balance {pFmt(totalDr)} {pFmt(totalCr)} = 0 ? PD.navy : PD.red, borderTop: "1.5px solid " + PD.navy, whiteSpace: "nowrap", overflow: "hidden", ...PD.pca, }}> {pFmt(g.closing)}
); })}
); }