/* ════════════════════════════════════════════════════════ NASAMA PROPERTIES — PRINTABLE FINANCIAL REPORTS Premium Corporate Design · Audit-Ready Layout ════════════════════════════════════════════════════════ */ // ── PRINT TRIGGER ──────────────────────────────────────── // Uses a hidden iframe instead of window.print() on the main SPA page. // This completely bypasses SPA layout isolation issues, @page injection timing, // and landscape override unreliability in Microsoft Print to PDF / Chrome. function rptPrint(landscape) { var el = document.getElementById('rpt-print-area'); if (!el) { console.warn('rptPrint: #rpt-print-area not found'); return; } // Snapshot the already-rendered React output as HTML var content = el.innerHTML; // Remove any stale iframe var old = document.getElementById('__rpt_iframe__'); if (old) old.remove(); // Create a hidden iframe — must be in the DOM for contentWindow to work var iframe = document.createElement('iframe'); iframe.id = '__rpt_iframe__'; iframe.style.cssText = [ 'position:fixed', 'left:-9999px', 'top:-9999px', 'width:' + (landscape ? '297mm' : '210mm'), 'height:' + (landscape ? '210mm' : '297mm'), 'border:0', 'visibility:hidden', ].join(';'); document.body.appendChild(iframe); var pageSize = landscape ? 'A4 landscape' : 'A4 portrait'; // Build a complete standalone HTML document for the iframe. // @page is set directly here — no injection timing issue. var html = '' + '' + '' + '' + '' + '' + '' + '' + '' + content + '' + ''; var doc = iframe.contentDocument || iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); // Wait for fonts to load in the iframe, then print function doprint() { iframe.contentWindow.focus(); iframe.contentWindow.print(); // Clean up after the print dialog is dismissed setTimeout(function () { var f = document.getElementById('__rpt_iframe__'); if (f) f.remove(); }, 8000); } // Try the fonts-ready promise (Chrome/Edge); fall back to a fixed delay try { iframe.contentDocument.fonts.ready.then(function () { doprint(); }); } catch (e) { setTimeout(doprint, 700); } } // ── DESIGN TOKENS ──────────────────────────────────────── var RPT = { // Core brand navy: "#0C0F1E", navyMid: "#141829", navyAccent: "#1E2540", gold: "#C9A044", goldBright: "#DFB455", goldD: "#A8853A", // Text ink: "#0F1623", textHeavy: "#1A1F36", text: "#374151", muted: "#6B7280", subtle: "#9CA3AF", pale: "#C4C9D4", // Structure rule: "#E5E7EB", ruleMd: "#D1D5DB", ruleDk: "#6B7280", // Backgrounds bg: "#FFFFFF", bgPage: "#F8F9FB", bgAlt: "#FAFAFA", bgSection: "#F3F5F8", // Semantic green: "#059669", greenBg: "#ECFDF5", red: "#DC2626", redBg: "#FEF2F2", blue: "#1D4ED8", blueBg: "#EFF6FF", purple: "#6D28D9", purpleBg: "#F5F3FF", amber: "#B45309", amberBg: "#FFFBEB", // Layout aliases (back-compat) fSm: 11, fBase: 13, fMd: 13, fLg: 14, fXl: 18, fXxl: 24, // Cell padding cPad: "11px 16px", hPad: "10px 16px", }; // ── PRINT COLOUR PRESERVATION ──────────────────────────── var PC = { WebkitPrintColorAdjust: "exact", printColorAdjust: "exact", }; // ── TABLE COLUMN WIDTHS ────────────────────────────────── var COL_CODE = { width: 80, minWidth: 70 }; var COL_AMOUNT = { width: 180, minWidth: 160 }; /* ════════════════════════════════════════════════════════ REPORT HEADER ════════════════════════════════════════════════════════ */ function RptHeader(props) { var company = props.company || "Nasama Properties"; var title = props.title || "Report"; var subtitle = props.subtitle || ""; var from = props.from; var to = props.to; var currency = props.currency || "AED"; var trn = props.trn; var periodLabel = from && to ? (fmtDate(from) + "\u2009\u2013\u2009" + fmtDate(to)) : to ? ("As of " + fmtDate(to)) : "All Periods"; var generated = new Date().toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }); // Build meta row items var meta = [ { k: "Period", v: periodLabel }, { k: "Currency", v: currency }, ]; if (trn) meta.push({ k: "TRN", v: trn }); meta.push({ k: "Generated", v: generated }); return React.createElement("div", { style: Object.assign({}, PC, { background: RPT.navy, borderRadius: "10px 10px 0 0", overflow: "hidden", }) }, /* ── Gold accent stripe at top */ React.createElement("div", { style: Object.assign({}, PC, { height: 4, background: "linear-gradient(90deg, " + RPT.gold + " 0%, " + RPT.goldBright + " 40%, " + RPT.goldD + " 100%)", }) }), /* ── Main header body */ React.createElement("div", { style: { padding: "24px 32px 22px", display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 20, } }, /* Left: identity + title */ React.createElement("div", { style: { flex: 1 } }, /* Company name */ React.createElement("div", { style: { fontSize: 10, fontWeight: 700, letterSpacing: "0.18em", textTransform: "uppercase", color: RPT.gold, marginBottom: 10, display: "flex", alignItems: "center", gap: 8, } }, React.createElement("span", { style: Object.assign({}, PC, { display: "inline-block", width: 18, height: 2, background: RPT.gold, verticalAlign: "middle", }) }), company ), /* Report title */ React.createElement("div", { style: { fontSize: 30, fontWeight: 800, letterSpacing: "-0.03em", lineHeight: 1.05, color: "#FFFFFF", marginBottom: subtitle ? 8 : 18, } }, title), /* Subtitle */ subtitle && React.createElement("div", { style: { fontSize: 12, color: "#7B85AE", marginBottom: 18, letterSpacing: "0.01em", } }, subtitle), /* Meta row */ React.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: 0, borderTop: "1px solid rgba(255,255,255,0.08)", paddingTop: 12, } }, meta.map(function (m, i) { return React.createElement("div", { key: i, style: { display: "flex", alignItems: "baseline", gap: 5, paddingRight: 20, marginRight: 20, borderRight: i < meta.length - 1 ? "1px solid rgba(255,255,255,0.1)" : "none", marginBottom: 2, } }, React.createElement("span", { style: { fontSize: 9.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: RPT.gold, flexShrink: 0, } }, m.k + ":"), React.createElement("span", { style: { fontSize: 11.5, color: "#9AA0C8", lineHeight: 1.4 } }, m.v) ); }) ) ), /* Right: Logo badge */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8, flexShrink: 0, } }, React.createElement("div", { style: Object.assign({}, PC, { width: 56, height: 56, borderRadius: 10, background: "rgba(201,160,68,0.15)", border: "1px solid rgba(201,160,68,0.3)", display: "flex", alignItems: "center", justifyContent: "center", }) }, React.createElement(Logo, { size: 34, style: { filter: "brightness(1.1)" } }) ), React.createElement("span", { style: { fontSize: 8.5, color: "#3B4270", letterSpacing: "0.18em", textTransform: "uppercase", } }, "Accounting v2") ) ) ); } /* ════════════════════════════════════════════════════════ KPI SUMMARY CARDS ════════════════════════════════════════════════════════ */ function RptKPIBar(props) { var cards = props.cards || []; return React.createElement("div", { style: Object.assign({}, PC, { display: "flex", background: RPT.bgPage, borderLeft: "1px solid " + RPT.rule, borderRight: "1px solid " + RPT.rule, borderBottom: "1px solid " + RPT.rule, }) }, cards.map(function (card, i) { var accentColor = card.color || RPT.text; return React.createElement("div", { key: i, style: Object.assign({}, PC, { flex: "1 1 0", padding: "20px 22px 18px", borderRight: i < cards.length - 1 ? "1px solid " + RPT.rule : "none", display: "flex", flexDirection: "column", justifyContent: "space-between", minWidth: 0, position: "relative", overflow: "hidden", }) }, /* Colored top indicator bar */ React.createElement("div", { style: Object.assign({}, PC, { position: "absolute", top: 0, left: 0, right: 0, height: 3, background: accentColor, }) }), /* Label */ React.createElement("div", { style: { fontSize: 9.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.13em", color: RPT.muted, marginBottom: 10, marginTop: 2, } }, card.label), /* Value */ React.createElement("div", { style: { fontVariantNumeric: "tabular-nums", lineHeight: 1.1, } }, /* "AED" prefix on its own line if value starts with "AED" */ card.value && card.value.startsWith && card.value.startsWith("AED ") ? React.createElement("div", null, React.createElement("div", { style: { fontSize: 10.5, fontWeight: 600, color: RPT.muted, letterSpacing: "0.04em", marginBottom: 2 } }, "AED"), React.createElement("div", { style: { fontSize: 22, fontWeight: 800, color: accentColor, lineHeight: 1, letterSpacing: "-0.02em", } }, card.value.replace("AED ", "")) ) : React.createElement("div", { style: { fontSize: card.big ? 22 : 18, fontWeight: 800, color: accentColor, letterSpacing: "-0.02em", } }, card.value) ), /* Sub-text */ card.sub && React.createElement("div", { style: { fontSize: 10, color: RPT.subtle, marginTop: 6, borderTop: "1px solid " + RPT.rule, paddingTop: 5, } }, card.sub) ); }) ); } /* ════════════════════════════════════════════════════════ REPORT ACTIONS (hidden in print) ════════════════════════════════════════════════════════ */ function RptActions(props) { return React.createElement("div", { className: "no-print", style: { display: "flex", gap: 8, alignItems: "center" } }, React.createElement("button", { onClick: props.onPrint, style: { display: "inline-flex", alignItems: "center", gap: 6, padding: "9px 16px", borderRadius: 8, fontSize: 13, fontWeight: 600, background: RPT.navy, color: "#fff", border: "none", cursor: "pointer", } }, "\uD83D\uDDA8\uFE0F Print"), React.createElement("button", { onClick: props.onPDF, style: { display: "inline-flex", alignItems: "center", gap: 6, padding: "9px 16px", borderRadius: 8, fontSize: 13, fontWeight: 600, background: RPT.gold, color: "#fff", border: "none", cursor: "pointer", } }, "\uD83D\uDCC4 Export PDF"), React.createElement("span", { style: { fontSize: 11, color: RPT.muted } }, "Select \u201CSave as PDF\u201D in the print dialog") ); } /* ════════════════════════════════════════════════════════ SECTION LABEL ════════════════════════════════════════════════════════ */ function RptSectionLabel(props) { var label = props.label; var color = props.color || RPT.text; return React.createElement("div", { style: { padding: "28px 24px 0", pageBreakAfter: "avoid", } }, React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, paddingBottom: 10, borderBottom: "1.5px solid " + color, } }, /* Small dot accent */ React.createElement("div", { style: Object.assign({}, PC, { width: 8, height: 8, borderRadius: "50%", background: color, flexShrink: 0, }) }), /* Label text */ React.createElement("span", { style: { fontSize: 10.5, fontWeight: 800, textTransform: "uppercase", letterSpacing: "0.15em", color: RPT.ink, } }, label) ) ); } /* ════════════════════════════════════════════════════════ SHARED TABLE PRIMITIVES ════════════════════════════════════════════════════════ */ function thS(extra) { return Object.assign({}, PC, { padding: "11px 16px", background: RPT.bgSection, fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: RPT.text, borderBottom: "1px solid " + RPT.ruleMd, borderTop: "1px solid " + RPT.rule, whiteSpace: "nowrap", }, extra || {}); } var tdCode = { padding: "11px 16px", fontSize: 11, fontFamily: "ui-monospace, 'SF Mono', Consolas, monospace", color: RPT.subtle, borderBottom: "1px solid " + RPT.rule, whiteSpace: "nowrap", verticalAlign: "middle", }; var tdName = { padding: "11px 16px", fontSize: 13, color: RPT.textHeavy, borderBottom: "1px solid " + RPT.rule, verticalAlign: "middle", }; /* ── Amount cell */ function AmtCell(props) { var amount = props.amount != null ? props.amount : 0; var color = props.color; var bold = props.bold; var size = props.size; var noBorder = props.noBorder; // Split "AED" prefix from the number for cleaner presentation var full = fmtAED(amount); var parts = full.match(/^(AED)\s(.+)$/); return React.createElement("td", { style: { textAlign: "right", padding: "11px 16px", fontVariantNumeric: "tabular-nums", fontWeight: bold ? 700 : 500, fontSize: size || 13, color: color || RPT.textHeavy, whiteSpace: "nowrap", borderBottom: noBorder ? "none" : "1px solid " + RPT.rule, verticalAlign: "middle", } }, parts ? React.createElement("span", null, React.createElement("span", { style: { fontSize: 10, color: bold ? color : RPT.subtle, marginRight: 3 } }, parts[1]), parts[2] ) : full ); } /* ── Total footer row (accounting-convention double-line emphasis) */ function TotalRow(props) { var label = props.label; var amount = props.amount; var color = props.color || RPT.text; var cols = props.cols || 2; var isGrand = props.isGrand; // double-rule below var cellBase = Object.assign({}, PC, { padding: "12px 16px", background: RPT.bgSection, borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: isGrand ? "3px double " + RPT.ink : "1.5px solid " + RPT.ruleDk, fontWeight: 700, }); return React.createElement("tr", null, React.createElement("td", { colSpan: cols, style: Object.assign({}, cellBase, { fontSize: 13, color: RPT.ink, letterSpacing: "0.01em", }) }, label), React.createElement("td", { style: Object.assign({}, cellBase, { textAlign: "right", fontSize: 14, color: color, fontVariantNumeric: "tabular-nums", }) }, /* Split AED from amount for cleaner total rendering */ (function () { var full = fmtAED(amount); var parts = full.match(/^(AED)\s(.+)$/); if (!parts) return full; return React.createElement("span", null, React.createElement("span", { style: { fontSize: 10.5, fontWeight: 600, marginRight: 3, color: RPT.muted } }, parts[1]), parts[2] ); })() ) ); } /* ════════════════════════════════════════════════════════ PRINT PAGE FOOTER (fixed position — appears on every page) ════════════════════════════════════════════════════════ */ function RptPageFooter(props) { var company = props.company || "Nasama Properties"; var title = props.title || "Financial Report"; return React.createElement("div", { className: "rpt-page-footer", style: { display: "none" }, // CSS overrides this in @media print }, React.createElement("span", null, company + " \u00B7 " + title), React.createElement("span", null, "Nasama Properties Accounting System v2") ); } /* ════════════════════════════════════════════════════════ PROFIT & LOSS REPORT ════════════════════════════════════════════════════════ */ function PLReport(props) { var accounts = props.accounts || []; var filteredLedger = props.filteredLedger || {}; var totalRev = props.totalRev || 0; var totalExp = props.totalExp || 0; var dateFilter = props.dateFilter || {}; var settings = props.settings || {}; var netIncome = totalRev - totalExp; var netColor = netIncome >= 0 ? RPT.green : RPT.red; var netBg = netIncome >= 0 ? RPT.greenBg : RPT.redBg; var revenueRows = accounts.filter(function (a) { return a.type === "Revenue" && accountBalance(a, filteredLedger) !== 0; }); var expenseRows = accounts.filter(function (a) { return a.type === "Expense" && accountBalance(a, filteredLedger) !== 0; }).sort(function (a, b) { return accountBalance(b, filteredLedger) - accountBalance(a, filteredLedger); }); var kpis = [ { label: "Total Revenue", value: fmtAED(totalRev), color: RPT.green, big: true }, { label: "Total Expenses", value: fmtAED(totalExp), color: RPT.amber, big: true }, { label: "Net Income", value: fmtAED(netIncome), color: netColor, big: true }, { label: "Net Margin", value: totalRev > 0 ? ((netIncome / totalRev) * 100).toFixed(1) + "%" : "\u2014", color: netIncome >= 0 ? RPT.green : RPT.red, sub: "Net income \u00F7 Revenue", }, ]; var company = settings.company || "Nasama Properties"; var title = "Profit & Loss Statement"; /* Shared account row renderer */ function AccountRows(rowAccts, ledger, emptyMsg) { if (rowAccts.length === 0) { return React.createElement("tr", null, React.createElement("td", { colSpan: 3, style: { padding: "18px 16px", textAlign: "center", color: RPT.subtle, fontStyle: "italic", fontSize: 12 } }, emptyMsg) ); } return rowAccts.map(function (a, i) { return React.createElement("tr", { key: a.id }, React.createElement("td", { style: tdCode }, a.code), React.createElement("td", { style: tdName }, a.name), React.createElement(AmtCell, { amount: accountBalance(a, ledger) }) ); }); } return React.createElement("div", { style: { background: "#fff", fontFamily: "Inter, Arial, sans-serif" } }, /* Header */ React.createElement(RptHeader, { company: company, title: title, subtitle: "Income statement for the selected period", from: dateFilter.from, to: dateFilter.to, currency: settings.currency || "AED", trn: settings.trn, }), /* KPI bar */ React.createElement(RptKPIBar, { cards: kpis }), /* Report body */ React.createElement("div", { style: { border: "1px solid " + RPT.rule, borderTop: "none", borderRadius: "0 0 10px 10px", overflow: "hidden", marginBottom: 0, } }, /* ── REVENUE SECTION */ React.createElement(RptSectionLabel, { label: "Revenue", color: RPT.green }), React.createElement("table", { style: { width: "100%", borderCollapse: "collapse", tableLayout: "fixed" } }, React.createElement("colgroup", null, React.createElement("col", { style: { width: 80 } }), React.createElement("col", null), React.createElement("col", { style: { width: 180 } }) ), React.createElement("thead", null, React.createElement("tr", null, React.createElement("th", { style: thS() }, "Code"), React.createElement("th", { style: thS() }, "Account Name"), React.createElement("th", { style: thS({ textAlign: "right" }) }, "Amount") ) ), React.createElement("tbody", null, AccountRows(revenueRows, filteredLedger, "No revenue recorded in this period") ), React.createElement("tfoot", null, React.createElement(TotalRow, { label: "Total Revenue", amount: totalRev, color: RPT.green }) ) ), /* ── EXPENSES SECTION */ React.createElement(RptSectionLabel, { label: "Expenses", color: RPT.amber }), React.createElement("table", { style: { width: "100%", borderCollapse: "collapse", tableLayout: "fixed" } }, React.createElement("colgroup", null, React.createElement("col", { style: { width: 80 } }), React.createElement("col", null), React.createElement("col", { style: { width: 180 } }) ), React.createElement("thead", null, React.createElement("tr", null, React.createElement("th", { style: thS() }, "Code"), React.createElement("th", { style: thS() }, "Account Name"), React.createElement("th", { style: thS({ textAlign: "right" }) }, "Amount") ) ), React.createElement("tbody", null, AccountRows(expenseRows, filteredLedger, "No expenses recorded in this period") ), React.createElement("tfoot", null, React.createElement(TotalRow, { label: "Total Expenses", amount: totalExp, color: RPT.amber, isGrand: true }) ) ), /* ── NET INCOME CONCLUSION */ React.createElement("div", { style: Object.assign({}, PC, { background: RPT.navy, display: "flex", justifyContent: "space-between", alignItems: "center", padding: "22px 24px", pageBreakInside: "avoid", }) }, React.createElement("div", null, React.createElement("div", { style: { fontSize: 9.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.18em", color: "rgba(255,255,255,0.45)", marginBottom: 5, } }, "Statement Result"), React.createElement("div", { style: { fontSize: 18, fontWeight: 800, color: "#FFFFFF", letterSpacing: "-0.01em", } }, "NET INCOME") ), React.createElement("div", { style: { textAlign: "right" } }, React.createElement("div", { style: { fontSize: 10.5, fontWeight: 600, color: "rgba(255,255,255,0.45)", letterSpacing: "0.08em", marginBottom: 4, } }, "AED"), React.createElement("div", { style: { fontSize: 32, fontWeight: 800, color: netIncome >= 0 ? RPT.goldBright : "#F87171", fontVariantNumeric: "tabular-nums", letterSpacing: "-0.02em", lineHeight: 1, } }, fmtAED(netIncome).replace("AED ", "")) ) ) ), /* Print-only page footer */ React.createElement(RptPageFooter, { company: company, title: title }) ); } /* ════════════════════════════════════════════════════════ BALANCE SHEET REPORT ════════════════════════════════════════════════════════ */ function BSReport(props) { var accounts = props.accounts || []; var toDateLedger = props.toDateLedger || {}; var totalAssets = props.totalAssets || 0; var totalLiabilities = props.totalLiabilities || 0; var totalEquity = props.totalEquity || 0; var netIncome = props.netIncome || 0; var dateFilter = props.dateFilter || {}; var settings = props.settings || {}; var totalLE = totalLiabilities + totalEquity + netIncome; var isBalanced = Math.abs(totalAssets - totalLE) <= 1; var assetRows = accounts.filter(function (a) { return a.type === "Asset" && accountBalance(a, toDateLedger) !== 0; }); var liabRows = accounts.filter(function (a) { return a.type === "Liability" && accountBalance(a, toDateLedger) !== 0; }); var equityRows = accounts.filter(function (a) { return a.type === "Equity" && accountBalance(a, toDateLedger) !== 0; }); var kpis = [ { label: "Total Assets", value: fmtAED(totalAssets), color: RPT.blue }, { label: "Total Liabilities", value: fmtAED(totalLiabilities), color: RPT.red }, { label: "Equity + Retained", value: fmtAED(totalEquity + netIncome), color: RPT.purple }, { label: "Balance Check", value: isBalanced ? "\u2713 Balanced" : "\u2717 Imbalance", color: isBalanced ? RPT.green : RPT.red, sub: isBalanced ? "Assets = Liabilities + Equity" : "Investigate discrepancy", }, ]; var company = settings.company || "Nasama Properties"; var title = "Balance Sheet"; function BSSection(sectionAccts, ledger, label, color, total, extras) { return React.createElement("div", { className: "rpt-section" }, React.createElement(RptSectionLabel, { label: label, color: color }), React.createElement("table", { style: { width: "100%", borderCollapse: "collapse", tableLayout: "fixed" } }, React.createElement("colgroup", null, React.createElement("col", { style: { width: 80 } }), React.createElement("col", null), React.createElement("col", { style: { width: 180 } }) ), React.createElement("thead", null, React.createElement("tr", null, React.createElement("th", { style: thS() }, "Code"), React.createElement("th", { style: thS() }, "Account Name"), React.createElement("th", { style: thS({ textAlign: "right" }) }, "Balance") ) ), React.createElement("tbody", null, sectionAccts.length === 0 && (!extras || extras.length === 0) && React.createElement("tr", null, React.createElement("td", { colSpan: 3, style: { padding: "18px 16px", textAlign: "center", color: RPT.subtle, fontStyle: "italic", fontSize: 12 } }, "No balances") ), sectionAccts.map(function (a) { return React.createElement("tr", { key: a.id }, React.createElement("td", { style: tdCode }, a.code), React.createElement("td", { style: tdName }, a.name), React.createElement(AmtCell, { amount: accountBalance(a, ledger) }) ); }), (extras || []).map(function (row, i) { return React.createElement("tr", { key: "x" + i }, React.createElement("td", { style: Object.assign({}, tdCode, { fontStyle: "italic" }) }, "\u2014"), React.createElement("td", { style: Object.assign({}, tdName, { fontStyle: "italic", color: RPT.muted }) }, row.name), React.createElement(AmtCell, { amount: row.amount }) ); }) ), React.createElement("tfoot", null, React.createElement(TotalRow, { label: "Total " + label, amount: total, color: color }) ) ) ); } return React.createElement("div", { style: { background: "#fff", fontFamily: "Inter, Arial, sans-serif" } }, React.createElement(RptHeader, { company: company, title: title, subtitle: "Cumulative financial position as of the selected date", from: null, to: dateFilter.to, currency: settings.currency || "AED", trn: settings.trn, }), React.createElement(RptKPIBar, { cards: kpis }), React.createElement("div", { style: { border: "1px solid " + RPT.rule, borderTop: "none", borderRadius: "0 0 10px 10px", overflow: "hidden" } }, BSSection(assetRows, toDateLedger, "Assets", RPT.blue, totalAssets), BSSection(liabRows, toDateLedger, "Liabilities", RPT.red, totalLiabilities), BSSection(equityRows, toDateLedger, "Equity", RPT.purple, totalEquity + netIncome, [ { name: "Retained Earnings (Net Income to date)", amount: netIncome } ]), /* Summary totals */ React.createElement("div", { style: { padding: "0 24px" } }, React.createElement("table", { style: { width: "100%", borderCollapse: "collapse", tableLayout: "fixed", marginTop: 8 } }, React.createElement("colgroup", null, React.createElement("col", null), React.createElement("col", { style: { width: 180 } }) ), React.createElement("tbody", null, React.createElement("tr", null, React.createElement("td", { style: Object.assign({}, tdName, { fontWeight: 600 }) }, "Total Assets"), React.createElement(AmtCell, { amount: totalAssets, color: RPT.blue, bold: true }) ), React.createElement("tr", null, React.createElement("td", { style: Object.assign({}, tdName, { fontWeight: 600 }) }, "Total Liabilities + Equity"), React.createElement(AmtCell, { amount: totalLE, bold: true }) ) ) ) ), /* Balance indicator */ React.createElement("div", { style: Object.assign({}, PC, { margin: "12px 24px 16px", padding: "10px 16px", borderRadius: 6, background: isBalanced ? RPT.greenBg : RPT.redBg, border: "1px solid " + (isBalanced ? "#A7F3D0" : "#FECACA"), fontSize: 12, fontWeight: 600, color: isBalanced ? RPT.green : RPT.red, display: "flex", alignItems: "center", gap: 8, }) }, isBalanced ? "\u2705 Balance sheet is balanced \u2014 Assets = Liabilities + Equity" : "\u274C Not balanced \u2014 Difference: " + fmtAED(Math.abs(totalAssets - totalLE)) ) ), React.createElement(RptPageFooter, { company: company, title: title }) ); } /* ════════════════════════════════════════════════════════ TRIAL BALANCE REPORT ════════════════════════════════════════════════════════ */ function TBReport(props) { var accounts = props.accounts || []; var filteredLedger = props.filteredLedger || {}; var dateFilter = props.dateFilter || {}; var settings = props.settings || {}; var typeColors = { Asset: RPT.blue, Liability: RPT.red, Equity: RPT.purple, Revenue: RPT.green, Expense: RPT.amber, }; var rows = accounts.slice() .sort(function (a, b) { return a.code.localeCompare(b.code); }) .filter(function (a) { var e = filteredLedger[a.id] || { debit: 0, credit: 0 }; return e.debit > 0 || e.credit > 0; }) .map(function (a) { var e = filteredLedger[a.id] || { debit: 0, credit: 0 }; var nb = NORMAL_BAL[a.type]; var 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, }; }); var totalDebit = rows.reduce(function (s, r) { return s + r.debit; }, 0); var totalCredit = rows.reduce(function (s, r) { return s + r.credit; }, 0); var isBalanced = Math.abs(totalDebit - totalCredit) <= 1; var kpis = [ { label: "Total Debit", value: fmtAED(totalDebit), color: RPT.blue }, { label: "Total Credit", value: fmtAED(totalCredit), color: RPT.purple }, { label: "Balance", value: isBalanced ? "\u2713 Balanced" : "\u2717 Out of balance", color: isBalanced ? RPT.green : RPT.red }, { label: "Active Accounts", value: "" + rows.length, color: RPT.text }, ]; var company = settings.company || "Nasama Properties"; var title = "Trial Balance"; return React.createElement("div", { style: { background: "#fff", fontFamily: "Inter, Arial, sans-serif" } }, React.createElement(RptHeader, { company: company, title: title, subtitle: "Net account balances for the selected period", from: dateFilter.from, to: dateFilter.to, currency: settings.currency || "AED", trn: settings.trn, }), React.createElement(RptKPIBar, { cards: kpis }), React.createElement("div", { style: { border: "1px solid " + RPT.rule, borderTop: "none", borderRadius: "0 0 10px 10px", overflow: "hidden" } }, React.createElement("table", { style: { width: "100%", borderCollapse: "collapse" } }, React.createElement("thead", null, React.createElement("tr", null, React.createElement("th", { style: thS({ width: 72 }) }, "Code"), React.createElement("th", { style: thS() }, "Account Name"), React.createElement("th", { style: thS({ width: 100 }) }, "Type"), React.createElement("th", { style: thS({ textAlign: "right", width: 160 }) }, "Debit"), React.createElement("th", { style: thS({ textAlign: "right", width: 160 }) }, "Credit") ) ), React.createElement("tbody", null, rows.length === 0 && React.createElement("tr", null, React.createElement("td", { colSpan: 5, style: { textAlign: "center", padding: 28, color: RPT.subtle, fontStyle: "italic" } }, "No transactions in this period") ), rows.map(function (row) { var tColor = typeColors[row.type] || RPT.text; return React.createElement("tr", { key: row.id }, React.createElement("td", { style: tdCode }, row.code), React.createElement("td", { style: tdName }, row.name), React.createElement("td", { style: { padding: "11px 16px", borderBottom: "1px solid " + RPT.rule, verticalAlign: "middle" } }, React.createElement("span", { style: Object.assign({}, PC, { display: "inline-block", padding: "2px 8px", borderRadius: 20, fontSize: 9.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.07em", background: tColor + "18", color: tColor, }) }, row.type) ), React.createElement("td", { style: { padding: "11px 16px", textAlign: "right", fontVariantNumeric: "tabular-nums", color: row.debit ? RPT.blue : RPT.subtle, fontWeight: row.debit ? 600 : 400, fontSize: 13, borderBottom: "1px solid " + RPT.rule, } }, row.debit ? fmtAED(row.debit) : "\u2014"), React.createElement("td", { style: { padding: "11px 16px", textAlign: "right", fontVariantNumeric: "tabular-nums", color: row.credit ? RPT.purple : RPT.subtle, fontWeight: row.credit ? 600 : 400, fontSize: 13, borderBottom: "1px solid " + RPT.rule, } }, row.credit ? fmtAED(row.credit) : "\u2014") ); }) ), React.createElement("tfoot", null, React.createElement("tr", { style: Object.assign({}, PC, { background: RPT.bgSection }) }, React.createElement("td", { colSpan: 3, style: { padding: "12px 16px", fontWeight: 700, fontSize: 13, borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "3px double " + RPT.ink } }, "TOTALS"), React.createElement("td", { style: { padding: "12px 16px", textAlign: "right", fontWeight: 700, fontSize: 14, color: RPT.blue, fontVariantNumeric: "tabular-nums", borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "3px double " + RPT.ink } }, fmtAED(totalDebit)), React.createElement("td", { style: { padding: "12px 16px", textAlign: "right", fontWeight: 700, fontSize: 14, color: RPT.purple, fontVariantNumeric: "tabular-nums", borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "3px double " + RPT.ink } }, fmtAED(totalCredit)) ), !isBalanced && React.createElement("tr", { style: Object.assign({}, PC, { background: RPT.redBg }) }, React.createElement("td", { colSpan: 5, style: { padding: "9px 16px", color: RPT.red, fontWeight: 600, fontSize: 12 } }, "\u274C Trial balance is out of balance by " + fmtAED(Math.abs(totalDebit - totalCredit)) ) ) ) ) ), React.createElement(RptPageFooter, { company: company, title: title }) ); } /* ════════════════════════════════════════════════════════ GENERAL LEDGER REPORT ════════════════════════════════════════════════════════ */ function GLReport(props) { var accounts = props.accounts || []; var txns = props.txns || []; var filteredTxns = props.filteredTxns || []; var dateFilter = props.dateFilter || {}; var settings = props.settings || {}; // ── Account filter state — controlled by parent when props are provided var _sf = React.useState("All"); var typeFilter = props.typeFilter !== undefined ? props.typeFilter : _sf[0]; var setTypeFilter = props.setTypeFilter || _sf[1]; var _ss = React.useState(""); var searchText = props.searchText !== undefined ? props.searchText : _ss[0]; var setSearchText = props.setSearchText || _ss[1]; var _sa = React.useState(null); var selectedId = props.selectedId !== undefined ? props.selectedId : _sa[0]; var setSelectedId = props.setSelectedId || _sa[1]; var groups = React.useMemo(function () { var acctIds = new Set( filteredTxns .filter(function (t) { return !t.isVoid; }) .flatMap(function (t) { return (t.lines || []).map(function (l) { return l.accountId; }); }) ); return accounts .filter(function (a) { return acctIds.has(a.id); }) .sort(function (a, b) { return a.code.localeCompare(b.code); }) .map(function (acct) { var nb = NORMAL_BAL[acct.type]; var priorBalance = 0; if (dateFilter.from) { txns.filter(function (t) { return !t.isVoid && (t.date || "") < dateFilter.from; }) .forEach(function (t) { (t.lines || []).forEach(function (l) { if (l.accountId !== acct.id) return; var dr = l.debit || 0, cr = l.credit || 0; priorBalance += nb === "debit" ? dr - cr : cr - dr; }); }); } var runningBal = priorBalance; var rows = filteredTxns .filter(function (t) { return !t.isVoid; }) .sort(function (a, b) { return (a.date || "").localeCompare(b.date || ""); }) .flatMap(function (t) { return (t.lines || []) .filter(function (l) { return l.accountId === acct.id; }) .map(function (l) { var 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 || "\u2014", debit: dr, credit: cr, balance: runningBal, key: t.id + "-" + l.id, }; }); }); return { acct: acct, nb: nb, openingBal: priorBalance, rows: rows, closing: runningBal }; }); }, [accounts, txns, filteredTxns, dateFilter.from]); // ── Apply account filters (screen only) var filteredGroups = React.useMemo(function () { return groups.filter(function (g) { if (selectedId && g.acct.id !== selectedId) return false; if (typeFilter !== "All" && g.acct.type !== typeFilter) return false; if (searchText.trim()) { var q = searchText.trim().toLowerCase(); if (!g.acct.code.toLowerCase().includes(q) && !g.acct.name.toLowerCase().includes(q)) return false; } return true; }); }, [groups, typeFilter, searchText, selectedId]); var td = function (extra) { return Object.assign({ padding: "9px 14px", fontSize: 12, borderBottom: "1px solid " + RPT.rule, verticalAlign: "middle" }, extra || {}); }; var company = settings.company || "Nasama Properties"; var title = "General Ledger"; return React.createElement("div", { style: { background: "#fff", fontFamily: "Inter, Arial, sans-serif" } }, React.createElement(RptHeader, { company: company, title: title, subtitle: "All transactions grouped by account with running balance", from: dateFilter.from, to: dateFilter.to, currency: settings.currency || "AED", trn: settings.trn, }), /* ── Account filter bar (screen only, not printed) ── */ React.createElement("div", { className: "no-print", style: { background: "#F8F9FB", border: "1px solid " + RPT.rule, borderTop: "none", padding: "12px 16px", display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center", } }, /* Search input */ React.createElement("div", { style: { position: "relative", flex: "1 1 180px", minWidth: 150 } }, React.createElement("span", { style: { position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", color: RPT.subtle, fontSize: 14, pointerEvents: "none" } }, "\uD83D\uDD0D"), React.createElement("input", { type: "text", placeholder: "Search code or name\u2026", value: searchText, onChange: function (e) { setSearchText(e.target.value); setSelectedId(null); }, style: { width: "100%", boxSizing: "border-box", padding: "7px 10px 7px 30px", fontSize: 12.5, border: "1px solid " + RPT.rule, borderRadius: 6, outline: "none", background: "#fff", fontFamily: "Inter, Arial, sans-serif", } }) ), /* Type filter buttons */ React.createElement("div", { style: { display: "flex", gap: 4, flexWrap: "wrap" } }, ["All", "Asset", "Liability", "Equity", "Revenue", "Expense"].map(function (t) { var active = typeFilter === t; var colors = { All: ["#1E3A5F", "#fff"], Asset: ["#1D4ED8", "#EFF6FF"], Liability: ["#DC2626", "#FEF2F2"], Equity: ["#7C3AED", "#F5F3FF"], Revenue: ["#059669", "#ECFDF5"], Expense: ["#D97706", "#FFFBEB"], }; var col = colors[t] || ["#555", "#f0f0f0"]; return React.createElement("button", { key: t, onClick: function () { setTypeFilter(t); setSelectedId(null); }, style: { padding: "5px 11px", fontSize: 11.5, fontWeight: active ? 700 : 500, borderRadius: 5, cursor: "pointer", border: "1.5px solid " + (active ? col[0] : RPT.rule), background: active ? col[0] : "#fff", color: active ? (t === "All" ? "#fff" : col[1] === "#fff" ? "#fff" : col[0]) : RPT.subtle, transition: "all 0.15s", fontFamily: "Inter, Arial, sans-serif", } }, t); }) ), /* Account jump dropdown */ React.createElement("select", { value: selectedId || "", onChange: function (e) { var val = e.target.value; setSelectedId(val || null); setSearchText(""); setTypeFilter("All"); }, style: { padding: "6px 10px", fontSize: 12, border: "1px solid " + RPT.rule, borderRadius: 6, background: "#fff", color: RPT.textHeavy, flex: "0 1 200px", minWidth: 140, fontFamily: "Inter, Arial, sans-serif", } }, React.createElement("option", { value: "" }, "Jump to account\u2026"), groups.map(function (g) { return React.createElement("option", { key: g.acct.id, value: g.acct.id }, g.acct.code + " \u2014 " + g.acct.name ); }) ), /* Counter */ React.createElement("span", { style: { fontSize: 11.5, color: RPT.subtle, whiteSpace: "nowrap", marginLeft: "auto" } }, filteredGroups.length === groups.length ? groups.length + " accounts" : filteredGroups.length + " of " + groups.length + " accounts" ), /* Clear filters button (shown only when filters are active) */ (selectedId || typeFilter !== "All" || searchText.trim()) && React.createElement("button", { onClick: function () { setSelectedId(null); setTypeFilter("All"); setSearchText(""); }, style: { padding: "5px 10px", fontSize: 11.5, border: "1px solid " + RPT.rule, borderRadius: 5, background: "#fff", color: RPT.subtle, cursor: "pointer", fontFamily: "Inter, Arial, sans-serif", } }, "\u00D7 Clear") ), filteredGroups.length === 0 && React.createElement("div", { style: { padding: 40, textAlign: "center", color: RPT.subtle, fontStyle: "italic", border: "1px solid " + RPT.rule, borderTop: "none", borderRadius: "0 0 10px 10px" } }, groups.length === 0 ? "No transactions found for the selected period." : "No accounts match the current filter."), filteredGroups.map(function (g, gi) { var totalDr = g.rows.reduce(function (s, r) { return s + r.debit; }, 0); var totalCr = g.rows.reduce(function (s, r) { return s + r.credit; }, 0); return React.createElement("div", { key: g.acct.id, className: "rpt-acct-block", style: { border: "1px solid " + RPT.rule, borderTop: gi === 0 ? "none" : "1px solid " + RPT.rule, marginTop: gi > 0 ? 12 : 0, borderRadius: gi === 0 ? "0 0 0 0" : 8, overflow: "hidden", pageBreakInside: "avoid", } }, /* Account heading */ React.createElement("div", { style: Object.assign({}, PC, { background: RPT.navyAccent, color: "#fff", padding: "10px 16px", display: "flex", justifyContent: "space-between", alignItems: "center", }) }, React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, React.createElement("span", { style: Object.assign({}, PC, { display: "inline-block", width: 3, height: 18, borderRadius: 2, background: RPT.gold, flexShrink: 0, }) }), React.createElement("span", { style: { fontWeight: 700, fontSize: 13, letterSpacing: "0.01em" } }, g.acct.code + " \u2014 " + g.acct.name) ), React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14 } }, React.createElement("span", { style: Object.assign({}, PC, { fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: "rgba(255,255,255,0.4)", background: "rgba(255,255,255,0.08)", padding: "3px 8px", borderRadius: 4, }) }, g.acct.type), React.createElement("span", { style: { fontSize: 11, color: "rgba(255,255,255,0.5)" } }, "Closing: ", React.createElement("span", { style: { color: RPT.gold, fontWeight: 700 } }, fmtAED(g.closing)) ) ) ), /* Transactions table */ React.createElement("div", { style: { overflowX: "auto", WebkitOverflowScrolling: "touch" } }, React.createElement("table", { style: { width: "100%", minWidth: 680, borderCollapse: "collapse", fontSize: 12 } }, React.createElement("thead", null, React.createElement("tr", null, React.createElement("th", { style: thS({ width: 86, whiteSpace: "nowrap" }) }, "Date"), React.createElement("th", { style: thS({ width: 96, whiteSpace: "nowrap" }) }, "Reference"), React.createElement("th", { style: thS() }, "Description"), React.createElement("th", { style: thS({ textAlign: "right", width: 118, whiteSpace: "nowrap" }) }, "Debit"), React.createElement("th", { style: thS({ textAlign: "right", width: 118, whiteSpace: "nowrap" }) }, "Credit"), React.createElement("th", { style: thS({ textAlign: "right", width: 126, whiteSpace: "nowrap" }) }, "Balance") ) ), React.createElement("tbody", null, /* Opening balance row */ React.createElement("tr", { style: Object.assign({}, PC, { background: "#F0F4FF" }) }, React.createElement("td", { style: td({ color: RPT.subtle, whiteSpace: "nowrap" }) }, dateFilter.from ? fmtDate(dateFilter.from) : "\u2014"), React.createElement("td", { style: td({ color: RPT.subtle }) }, "\u2014"), React.createElement("td", { style: td({ fontStyle: "italic", color: RPT.subtle }) }, "Opening Balance"), React.createElement("td", { style: td({ textAlign: "right" }) }, "\u2014"), React.createElement("td", { style: td({ textAlign: "right" }) }, "\u2014"), React.createElement("td", { style: td({ textAlign: "right", fontWeight: 600, fontVariantNumeric: "tabular-nums" }) }, fmtAED(g.openingBal)) ), g.rows.map(function (row) { /* Truncate long refs (e.g. Firestore IDs) to first 10 chars */ var refDisplay = row.ref ? (row.ref.length > 12 ? row.ref.slice(0, 10) + "\u2026" : row.ref) : "\u2014"; return React.createElement("tr", { key: row.key }, React.createElement("td", { style: td({ color: RPT.subtle, whiteSpace: "nowrap" }) }, fmtDate(row.date)), React.createElement("td", { title: row.ref || "", style: td({ fontFamily: "ui-monospace,'SF Mono',Consolas,monospace", fontSize: 10.5, color: RPT.subtle, whiteSpace: "nowrap" }) }, refDisplay), React.createElement("td", { style: td({ color: RPT.textHeavy, maxWidth: 260, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }), title: row.desc }, row.desc), React.createElement("td", { style: td({ textAlign: "right", fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", color: row.debit ? RPT.blue : RPT.subtle, fontWeight: row.debit ? 600 : 400 }) }, row.debit ? fmtAED(row.debit) : "\u2014" ), React.createElement("td", { style: td({ textAlign: "right", fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", color: row.credit ? RPT.purple : RPT.subtle, fontWeight: row.credit ? 600 : 400 }) }, row.credit ? fmtAED(row.credit) : "\u2014" ), React.createElement("td", { style: td({ textAlign: "right", fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", color: row.balance >= 0 ? RPT.textHeavy : RPT.red }) }, fmtAED(row.balance) ) ); }) ), React.createElement("tfoot", null, React.createElement("tr", { style: Object.assign({}, PC, { background: RPT.bgSection }) }, React.createElement("td", { colSpan: 3, style: { padding: "9px 16px", fontWeight: 700, fontSize: 12, borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "1.5px solid " + RPT.ruleDk } }, "Closing Balance"), React.createElement("td", { style: { padding: "9px 16px", textAlign: "right", color: RPT.blue, fontWeight: 700, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "1.5px solid " + RPT.ruleDk } }, fmtAED(totalDr)), React.createElement("td", { style: { padding: "9px 16px", textAlign: "right", color: RPT.purple, fontWeight: 700, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "1.5px solid " + RPT.ruleDk } }, fmtAED(totalCr)), React.createElement("td", { style: { padding: "9px 16px", textAlign: "right", fontWeight: 800, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", color: g.closing >= 0 ? RPT.textHeavy : RPT.red, borderTop: "1.5px solid " + RPT.ruleDk, borderBottom: "1.5px solid " + RPT.ruleDk } }, fmtAED(g.closing)) ) ) )) ); }), React.createElement(RptPageFooter, { company: company, title: title }) ); }