Files
sap-erp/public/costing-pl.html
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

2081 lines
109 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Monthy Accounts | MITRA ERP Portal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Space Grotesk',sans-serif;background:#f0f2f5;color:#1a1a2e;min-height:100vh}
.pg{padding:20px 24px}
.hdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:18px}
.hdr h1{font-size:20px;font-weight:700;color:#1a1a2e}
.hdr h1 span{color:#047e7e}
.btn{height:36px;padding:0 16px;border-radius:8px;font-size:12px;font-weight:700;font-family:inherit;border:none;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.btn-primary{background:#047e7e;color:#fff}.btn-primary:hover{background:#035b5b}
.btn-primary:disabled{background:#b0bec5;cursor:not-allowed;opacity:.7}
.btn-outline{background:#fff;color:#047e7e;border:1.5px solid #047e7e}.btn-outline:hover{background:#f0fafa}
.btn-sm{height:30px;padding:0 12px;font-size:11px}
/* ── Card ── */
.card{background:#fff;border-radius:14px;box-shadow:0 1px 8px rgba(0,0,0,.08);overflow:hidden}
/* ── Filter bar ── */
.filter-bar{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid #e8eaed;background:#fff}
.fg{display:flex;flex-direction:column;gap:5px}
.fg label{font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px}
.fg input,.fg select{height:36px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 12px;font-size:13px;font-family:inherit;color:#1a1a2e;background:#fff;outline:none;transition:border-color .15s}
.fg input:focus,.fg select:focus{border-color:#047e7e;box-shadow:0 0 0 3px rgba(4,126,126,.08)}
/* ── Summary chips ── */
.summary-chips{display:flex;gap:8px;flex-wrap:wrap;padding:12px 20px;border-bottom:1px solid #e8eaed;background:#f8fafb}
.chip{background:#fff;border:1.5px solid #b2dfdf;border-radius:10px;padding:6px 14px;font-size:11px;font-weight:600;color:#047e7e;min-width:80px;cursor:pointer;transition:all .15s}
.chip:hover{border-color:#047e7e;background:#f0fafa}
.chip span{font-size:15px;font-weight:700;display:block;color:#1a1a2e;margin-top:1px}
/* ── Tabs ── */
.tabs{display:flex;flex-wrap:nowrap;overflow-x:auto;border-bottom:2px solid #e8eaed;background:#fff;scrollbar-width:none}
.tabs::-webkit-scrollbar{display:none}
.tab{flex-shrink:0;text-align:center;padding:13px 16px;font-size:12px;font-weight:700;color:#6b7280;cursor:pointer;border-bottom:3px solid transparent;margin-bottom:-2px;white-space:nowrap;transition:all .15s;letter-spacing:.2px;background:none;border-top:none;border-left:none;border-right:none;font-family:inherit}
.tab:hover{color:#047e7e;background:#f0fafa}
.tab.active{color:#047e7e;border-bottom-color:#047e7e;background:#f5fefe}
.tab-badge{display:inline-block;background:#e0f4f4;color:#047e7e;border-radius:10px;padding:1px 7px;font-size:10px;font-weight:700;margin-left:4px}
.tab.active .tab-badge{background:#047e7e;color:#fff}
/* ── Search bar ── */
.search-bar{display:flex;align-items:center;gap:10px;padding:10px 20px;border-bottom:1px solid #e8eaed;background:#fafbfc}
.search-bar input{flex:1;height:34px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 12px 0 34px;font-size:12px;font-family:inherit;color:#1a1a2e;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") no-repeat 10px center;outline:none;transition:border-color .15s}
.search-bar input:focus{border-color:#047e7e;box-shadow:0 0 0 3px rgba(4,126,126,.08)}
.srch-count{font-size:11px;color:#9ca3af;white-space:nowrap;flex-shrink:0;font-weight:600}
/* ── Tab panes ── */
.tab-content{display:none}
.tab-content.active{display:block}
/* ── Table ── */
.tbl-wrap{overflow-x:auto;overflow-y:auto;max-height:54vh}
.tbl-wrap::-webkit-scrollbar{height:7px;width:7px}
.tbl-wrap::-webkit-scrollbar-track{background:#f1f3f5}
.tbl-wrap::-webkit-scrollbar-thumb{background:#c1c7ce;border-radius:4px}
.tbl-wrap::-webkit-scrollbar-thumb:hover{background:#9ca3af}
table.pl-tbl{border-collapse:collapse;min-width:100%;font-size:12px}
.pl-tbl thead th{background:#f0f4f8;color:#4b5563;font-weight:700;padding:10px 12px;text-align:left;white-space:nowrap;position:sticky;top:0;z-index:2;border-bottom:2px solid #d1dae3;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
.pl-tbl thead th.num{text-align:right}
.pl-tbl tbody tr{border-bottom:1px solid #f0f2f4;transition:background .1s}
.pl-tbl tbody tr:hover td{background:#edfafa}
.pl-tbl tbody tr.no-hover:hover td{background:inherit!important;color:inherit!important}
.pl-tbl tbody tr.grand-total td,.pl-tbl tbody tr.grand-total td span{color:#fff!important;font-weight:800!important}
.pl-tbl tbody td{padding:9px 12px;color:#374151;font-size:12px;vertical-align:middle}
.pl-tbl tbody td.num{text-align:right;font-variant-numeric:tabular-nums}
.pl-tbl tbody tr.total-row td{background:#e0f4f4;font-weight:700;border-top:2px solid #80cbc4;border-bottom:none}
.pl-tbl tbody tr.total-row td.num{color:#004d40}
.pl-tbl td .code{color:#047e7e;font-weight:700}
/* ── Editable inputs ── */
.edit-inp{width:120px;border:1.5px solid #dde1e7;border-radius:6px;padding:5px 8px;font-size:12px;text-align:right;outline:none;transition:border-color .15s;font-family:inherit;background:#fff}
.edit-inp:focus{border-color:#047e7e;box-shadow:0 0 0 3px rgba(4,126,126,.08)}
.edit-inp.dirty{border-color:#f59e0b;background:#fffbeb}
.edit-inp.saved{border-color:#10b981;background:#f0fdf4}
.note-inp{width:160px;border:1.5px solid #dde1e7;border-radius:6px;padding:5px 8px;font-size:11px;font-family:inherit;outline:none;transition:border-color .15s;background:#fff}
.note-inp:focus{border-color:#047e7e}
.note-inp.dirty{border-color:#f59e0b;background:#fffbeb}
/* ── Amounts ── */
.amt-pos{color:#047e7e;font-weight:600}
.amt-neg{color:#dc2626;font-weight:600}
.amt-zero{color:#9ca3af}
/* ── Empty ── */
.empty{text-align:center;padding:56px 20px;color:#9ca3af;font-size:13px}
/* ── P&L Summary ── */
.pl-summary{padding:20px 24px;max-height:54vh;overflow-y:auto}
.pl-summary h3{font-size:14px;font-weight:700;color:#1a1a2e;margin-bottom:16px;padding-bottom:10px;border-bottom:2px solid #e8eaed}
.pl-s-section{margin-bottom:18px}
.pl-s-title{font-size:10px;font-weight:700;color:#047e7e;text-transform:uppercase;letter-spacing:.6px;margin-bottom:8px;background:#e0f4f4;padding:4px 10px;border-radius:5px;display:inline-block}
.pl-s-row{display:flex;justify-content:space-between;align-items:center;padding:5px 8px;border-radius:6px;font-size:13px;transition:background .1s}
.pl-s-row:hover{background:#f8fafb}
.pl-s-row .lbl{color:#374151}
.pl-s-row .val{font-weight:700;font-variant-numeric:tabular-nums;font-size:13px}
.pl-s-sub{padding-left:18px}
.pl-s-divider{height:1px;background:#e8eaed;margin:12px 0}
.pl-s-total{background:#e0f4f4;border-radius:10px;padding:14px 18px;display:flex;justify-content:space-between;align-items:center;margin-top:10px;border:1.5px solid #4db6ac}
.pl-s-total .lbl{font-size:15px;font-weight:700;color:#004d40}
.pl-s-total .val{font-size:20px;font-weight:800;color:#047e7e}
.pl-s-loss{background:#fef2f2;border-color:#fca5a5}
.pl-s-loss .lbl{color:#991b1b}
.pl-s-loss .val{color:#dc2626}
/* ── Configure ── */
.cfg-sel{border:1.5px solid #dde1e7;border-radius:6px;padding:5px 8px;font-size:12px;font-family:inherit;outline:none;min-width:130px;transition:border-color .15s;background:#fff}
.cfg-sel:focus{border-color:#047e7e}
.cfg-ord{width:70px;border:1.5px solid #dde1e7;border-radius:6px;padding:5px 8px;font-size:12px;font-family:monospace;outline:none;text-align:right;transition:border-color .15s;background:#fff}
.cfg-ord:focus{border-color:#047e7e}
/* ── Status bar ── */
.status-bar{display:flex;align-items:center;gap:10px;padding:10px 20px;background:#f8fafb;border-top:1px solid #e8eaed;font-size:12px;color:#6b7280;border-radius:0 0 14px 14px}
.status-dot{width:8px;height:8px;border-radius:50%;background:#d1d5db;flex-shrink:0;transition:background .3s}
.status-dot.ok{background:#10b981}.status-dot.err{background:#ef4444}
.status-dot.loading{background:#f59e0b;animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
/* ── Toast ── */
.toast{position:fixed;bottom:24px;right:24px;z-index:9999;padding:12px 20px;border-radius:10px;font-size:13px;font-weight:600;color:#fff;box-shadow:0 4px 20px rgba(0,0,0,.15);transform:translateY(20px);opacity:0;transition:all .3s;pointer-events:none;max-width:360px}
.toast.show{transform:translateY(0);opacity:1}
.toast.success{background:#10b981}.toast.error{background:#ef4444}.toast.info{background:#047e7e}
/* ── Spinner ── */
.spinner{display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,.4);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle;margin-right:4px}
@keyframes spin{to{transform:rotate(360deg)}}
@media(max-width:768px){.pg{padding:12px}.tabs{flex-wrap:nowrap;overflow-x:auto}}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<script src="/sidebar.js?v=25"></script>
<div class="pg">
<div class="hdr">
<h1 id="pageTitle"><span>Monthly Accounts</span> P&amp;L</h1>
<div style="display:flex;gap:8px;align-items:center">
<!-- Mode toggle -->
<div style="display:flex;border:1.5px solid #047e7e;border-radius:9px;overflow:hidden">
<button id="btnModeMA" onclick="setMode('monthly')"
style="padding:0 16px;height:34px;font-size:12px;font-weight:700;font-family:inherit;border:none;cursor:pointer;background:#047e7e;color:#fff;transition:all .15s">
Monthly Accounts
</button>
<button id="btnModeTB" onclick="setMode('trial')"
style="padding:0 16px;height:34px;font-size:12px;font-weight:700;font-family:inherit;border:none;cursor:pointer;background:#fff;color:#047e7e;transition:all .15s">
Trial Balance
</button>
</div>
<button class="btn btn-outline" id="btnPrint" onclick="printPLSummary()">Print Summary</button>
<button class="btn btn-outline" id="btnExport">Download Excel</button>
</div>
</div>
<div class="card" id="maCard">
<!-- Filter bar row 1: date range + action buttons -->
<div class="filter-bar">
<div class="fg"><label>From Date</label><input type="date" id="dtFrom"></div>
<div class="fg"><label>To Date</label><input type="date" id="dtTo"></div>
<button class="btn btn-primary" id="btnLoad" onclick="loadData()" style="margin-top:18px">Load Data</button>
<button class="btn btn-primary" id="btnSaveAll" onclick="saveAll()" style="background:#1a7f3c;margin-top:18px" disabled>Save All</button>
<button class="btn btn-primary" id="btnSnapshot" onclick="saveSnapshot()" style="background:#7c3aed;margin-top:18px" disabled>Save Snapshot</button>
</div>
<!-- Filter bar row 2: snapshot loader -->
<div style="display:flex;align-items:center;gap:10px;padding:10px 20px;border-bottom:1px solid #e8eaed;background:#f8fafb;flex-wrap:wrap">
<span style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px;white-space:nowrap">Load Snapshot</span>
<select id="snapSelect" style="flex:1;min-width:200px;max-width:420px;height:34px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 10px;font-size:12px;font-family:inherit;outline:none;background:#fff">
<option value="">— Select a saved snapshot —</option>
</select>
<button class="btn btn-outline" id="btnLoadSnap" onclick="loadSnapshot()" disabled style="height:34px">Load</button>
<button class="btn btn-outline" id="btnDelSnap" onclick="deleteSnapshot()" disabled style="height:34px;color:#dc2626;border-color:#fca5a5">Delete</button>
</div>
<!-- Summary chips -->
<div class="summary-chips" id="summaryChips" style="display:none">
<div class="chip" onclick="switchTab('Revenue')">Revenue<span id="cnt-Revenue">0</span></div>
<div class="chip" onclick="switchTab('Purchase')">Purchase<span id="cnt-Purchase">0</span></div>
<div class="chip" onclick="switchTab('Employee')">Employee<span id="cnt-Employee">0</span></div>
<div class="chip" onclick="switchTab('Factory')">Factory<span id="cnt-Factory">0</span></div>
<div class="chip" onclick="switchTab('Admin')">Admin<span id="cnt-Admin">0</span></div>
<div class="chip" onclick="switchTab('SND')">S&amp;D<span id="cnt-SND">0</span></div>
<div class="chip" onclick="switchTab('Finance')">Finance<span id="cnt-Finance">0</span></div>
<div class="chip" onclick="switchTab('OtherIncome')">Other Income<span id="cnt-OtherIncome">0</span></div>
<div class="chip" onclick="switchTab('Other')">Other<span id="cnt-Other">0</span></div>
</div>
<!-- Tabs -->
<div class="tabs" id="tabBar">
<div class="tab active" data-tab="Revenue">Revenue <span class="tab-badge" id="badge-Revenue">0</span></div>
<div class="tab" data-tab="Purchase">Purchase <span class="tab-badge" id="badge-Purchase">0</span></div>
<div class="tab" data-tab="Employee">Employee <span class="tab-badge" id="badge-Employee">0</span></div>
<div class="tab" data-tab="Factory">Factory <span class="tab-badge" id="badge-Factory">0</span></div>
<div class="tab" data-tab="Admin">Admin <span class="tab-badge" id="badge-Admin">0</span></div>
<div class="tab" data-tab="SND">S&amp;D <span class="tab-badge" id="badge-SND">0</span></div>
<div class="tab" data-tab="Finance">Finance <span class="tab-badge" id="badge-Finance">0</span></div>
<div class="tab" data-tab="OtherIncome">Other Income <span class="tab-badge" id="badge-OtherIncome">0</span></div>
<div class="tab" data-tab="Other">Other <span class="tab-badge" id="badge-Other">0</span></div>
<div class="tab" data-tab="Provisions">Provision Summary</div>
<div class="tab" data-tab="Summary">P&amp;L Summary</div>
<div class="tab" data-tab="Configure" id="tabConfigure" style="display:none">Configure</div>
</div>
<!-- Search bar -->
<div class="search-bar">
<input type="text" id="searchInput" placeholder="Search account code or name…">
<span class="srch-count" id="srchCount"></span>
</div>
<!-- Tab panes -->
<div class="tab-content active" id="pane-Revenue"><div class="empty">Load data to view Revenue accounts</div></div>
<div class="tab-content" id="pane-Purchase"><div class="empty">Load data to view Purchase accounts</div></div>
<div class="tab-content" id="pane-Employee"><div class="empty">Load data to view Employee accounts</div></div>
<div class="tab-content" id="pane-Factory"><div class="empty">Load data to view Factory accounts</div></div>
<div class="tab-content" id="pane-Admin"><div class="empty">Load data to view Admin accounts</div></div>
<div class="tab-content" id="pane-SND"><div class="empty">Load data to view S&amp;D accounts</div></div>
<div class="tab-content" id="pane-Finance"><div class="empty">Load data to view Finance accounts</div></div>
<div class="tab-content" id="pane-OtherIncome"><div class="empty">Load data to view Other Income accounts</div></div>
<div class="tab-content" id="pane-Other"><div class="empty">Load data to view Other accounts</div></div>
<div class="tab-content" id="pane-Provisions"><div class="empty">Load data to view Provision Summary</div></div>
<div class="tab-content" id="pane-Summary"><div class="empty">Load data to view P&amp;L Summary</div></div>
<div class="tab-content" id="pane-Configure"><div class="empty">Load data first to configure account mappings</div></div>
<!-- Status bar -->
<div class="status-bar">
<div class="status-dot" id="statusDot"></div>
<span id="statusMsg">Select date range and click Load Data</span>
</div>
</div>
<!-- ── Trial Balance Card (hidden by default) ── -->
<div class="card" id="tbCard" style="display:none;margin-top:0">
<!-- Date + Load row -->
<div class="filter-bar" style="flex-wrap:wrap;gap:10px">
<div class="fg"><label>From Date</label><input type="date" id="tbFrom"></div>
<div class="fg"><label>To Date</label><input type="date" id="tbTo"></div>
<button class="btn btn-primary" onclick="loadTrialBalance()" style="margin-top:18px">Load Trial Balance</button>
<button class="btn btn-outline" id="btnTbExport" onclick="exportTbXlsx()" style="margin-top:18px" disabled>Download Excel</button>
<button class="btn" id="btnTbSnapshot" onclick="saveTbSnapshot()" style="margin-top:18px;background:#7c3aed;color:#fff" disabled>Save Snapshot</button>
</div>
<!-- Snapshot loader row -->
<div style="display:flex;align-items:center;gap:10px;padding:10px 20px;border-bottom:1px solid #e8eaed;background:#f8fafb;flex-wrap:wrap">
<span style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px;white-space:nowrap">Load Snapshot</span>
<select id="tbSnapSelect" style="flex:1;min-width:200px;max-width:420px;height:34px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 10px;font-size:12px;font-family:inherit;outline:none;background:#fff">
<option value="">— Select a saved snapshot —</option>
</select>
<button class="btn btn-outline" id="btnTbLoadSnap" onclick="loadTbSnapshotData()" disabled style="height:34px">Load</button>
</div>
<!-- Provisions + Stock panel -->
<div style="padding:12px 20px;border-bottom:1px solid #e8eaed;background:#f8fafb;display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end">
<!-- Provision loader -->
<div style="display:flex;flex-direction:column;gap:5px">
<label style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px">Provisions</label>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn btn-outline btn-sm" onclick="loadTbProvisions()" title="Load provisions from Monthly Accounts for the same period">
Load from Monthly Accounts
</button>
<span id="tbProvStatus" style="font-size:11px;color:#047e7e;font-weight:600"></span>
</div>
</div>
<!-- Opening Stock -->
<div style="display:flex;flex-direction:column;gap:5px">
<label style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px">Opening Stock (₹)</label>
<div style="display:flex;gap:6px;align-items:center">
<input type="number" id="tbStockOpen" placeholder="0.00" style="width:150px;height:34px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 10px;font-size:13px;font-family:inherit;outline:none" oninput="renderTb(_tbFiltered)">
<button class="btn btn-outline btn-sm" onclick="loadTbStock()" title="Load from Monthly Accounts">From MA</button>
</div>
</div>
<!-- Closing Stock -->
<div style="display:flex;flex-direction:column;gap:5px">
<label style="font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px">Closing Stock (₹)</label>
<div style="display:flex;gap:6px;align-items:center">
<input type="number" id="tbStockClose" placeholder="0.00" style="width:150px;height:34px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 10px;font-size:13px;font-family:inherit;outline:none" oninput="renderTb(_tbFiltered)">
<button class="btn btn-outline btn-sm" onclick="loadTbStock()" title="Load from Monthly Accounts">From MA</button>
</div>
</div>
</div>
<!-- Search -->
<div class="search-bar">
<input type="text" id="tbSearch" placeholder="Search account code or name…" oninput="filterTb()">
<span class="srch-count" id="tbCount"></span>
</div>
<!-- Table -->
<div class="tbl-wrap" id="tbWrap" style="max-height:58vh">
<div class="empty">Select date range and click Load Trial Balance</div>
</div>
<!-- Status -->
<div class="status-bar">
<div class="status-dot" id="tbStatusDot"></div>
<span id="tbStatusMsg">Select date range and click Load Trial Balance</span>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function(){
const tok = sessionStorage.getItem('portal_token');
if (!tok) { window.location.href = '/'; return; }
let user = null;
try { user = JSON.parse(sessionStorage.getItem('portal_user')); } catch(_) {}
const isAdmin = user?.role === 'admin' || user?.role === 'sap_adder';
const hdrs = { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' };
if (isAdmin) document.getElementById('tabConfigure').style.display = '';
// ── Wire tab clicks ───────────────────────────────────────────────────────────
document.querySelectorAll('.tab[data-tab]').forEach(t => {
t.addEventListener('click', () => switchTab(t.dataset.tab));
});
// ── Wire search ───────────────────────────────────────────────────────────────
document.getElementById('searchInput').addEventListener('input', filterSearch);
// ── Head options ──────────────────────────────────────────────────────────────
const HEAD_OPTIONS = [
'Direct Expenses',
'Staff Salary',
'Contractor Salary',
'Salary - R&D',
'Salaries and Wages S&M',
'Staff Welfare',
'Purchases',
'Opening Stock',
'Closing Stock',
'Sales Revenue',
'Other Income',
'Power & Fuel',
'Repair and Maintenance',
'Depreciation',
'R&D Expenses',
'Admin Direct Expenses',
'Legal & Professional Charges',
'Social Expenses',
'S&D Direct Expenses',
'Sales Promotion',
'Branding Expenses',
'Freight Paid',
'Travelling',
'Insurance',
];
// ── Sheet definitions ─────────────────────────────────────────────────────────
const SHEETS = [
{ key: 'Revenue', label: 'Revenue' },
{ key: 'Purchase', label: 'Purchase / COGS' },
{ key: 'Employee', label: 'Employee Costs' },
{ key: 'Factory', label: 'Factory Expense' },
{ key: 'Admin', label: 'Admin Expenses' },
{ key: 'SND', label: 'Selling & Distrib.'},
{ key: 'Finance', label: 'Finance Cost' },
{ key: 'OtherIncome', label: 'Other Income' },
{ key: 'Other', label: 'Other' },
];
// ── State ─────────────────────────────────────────────────────────────────────
let allData = [];
let activeTab = 'Revenue';
let fromDate = '';
let toDate = '';
let openingStock = 0;
let closingStock = 0;
// Stock detail fields (stored as pl_overrides with special keys)
const STOCK_KEYS = {
open: { rm:'__OPEN_RM__', wip:'__OPEN_WIP__', fg:'__OPEN_FG__', trade:'__OPEN_TRADE__', conv:'__OPEN_CONV__' },
close:{ rm:'__CLOSE_RM__',wip:'__CLOSE_WIP__',fg:'__CLOSE_FG__',trade:'__CLOSE_TRADE__',conv:'__CLOSE_CONV__'}
};
let stockDetail = {
open: { rm:0, wip:0, fg:0, trade:0, conv:0 },
close: { rm:0, wip:0, fg:0, trade:0, conv:0 },
};
// Default dates (Apr–Mar financial year)
(function(){
const d = new Date(), m = d.getMonth()+1, y = d.getFullYear();
const fy = m >= 4 ? y : y - 1;
document.getElementById('dtFrom').value = `${fy}-04-01`;
document.getElementById('dtTo').value = `${y}-${String(m).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
})();
// ── Load data ─────────────────────────────────────────────────────────────────
async function loadData() {
fromDate = document.getElementById('dtFrom').value;
toDate = document.getElementById('dtTo').value;
if (!fromDate || !toDate) { showToast('Select both dates', 'error'); return; }
const dirtyCount = document.querySelectorAll('.edit-inp.dirty,.note-inp.dirty').length;
if (dirtyCount > 0 && !confirm(`You have ${dirtyCount} unsaved change${dirtyCount!==1?'s':''}. Click "Save All" first or continue to discard them.`)) return;
const btn = document.getElementById('btnLoad');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Loading…';
setStatus('Loading data…', 'loading');
try {
const r = await fetch(`/api/costing/data?from=${fromDate}&to=${toDate}`, { headers: hdrs });
const j = await r.json();
if (!j.success) throw new Error(j.message);
allData = j.data.map(r => ({ ...r, provision: r.override_val != null ? Number(r.override_val) : null }));
await loadStockValues();
updateChips();
renderAllSheets();
renderSummary();
renderConfigure();
if (activeTab !== 'Configure' && activeTab !== 'Summary') renderSheet(activeTab);
document.getElementById('btnSaveAll').disabled = false;
document.getElementById('btnSnapshot').disabled = false;
setStatus(`${allData.length} accounts loaded — ${fmtDateDisplay(fromDate)} to ${fmtDateDisplay(toDate)}`, 'ok');
showToast('Data loaded successfully', 'success');
} catch (err) {
setStatus('Load failed: ' + err.message, 'err');
showToast('Load failed: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = 'Load Data';
}
}
window.loadData = loadData;
// ── Save All overrides ────────────────────────────────────────────────────────
async function saveAll() {
if (!fromDate || !toDate || !allData.length) { showToast('Load data first', 'error'); return; }
const btn = document.getElementById('btnSaveAll');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Saving…';
setStatus('Saving all overrides…', 'loading');
try {
// Sync any DOM values that may not have triggered blur yet
document.querySelectorAll('.edit-inp[data-code]').forEach(inp => {
const row = allData.find(r => r.acct_code === inp.dataset.code);
if (row) row.provision = inp.value === '' ? null : parseFloat(inp.value);
});
document.querySelectorAll('.note-inp[data-code]').forEach(inp => {
const row = allData.find(r => r.acct_code === inp.dataset.code);
if (row) row.note = inp.value;
});
// Save all accounts that have a provision or note (plus previously-saved rows even if now null)
const toSave = allData.filter(r => r.provision !== null || (r.note && r.note.trim()) || r.override_val !== null);
await Promise.all(toSave.map(row =>
fetch('/api/costing/override', { method:'POST', headers:hdrs,
body: JSON.stringify({ acct_code:row.acct_code, from:fromDate, to:toDate,
override_val:row.provision, note:row.note||'' }) })
));
// Also ensure stock net values are persisted
await Promise.all([
fetch('/api/costing/stock', { method:'POST', headers:hdrs,
body: JSON.stringify({ from:fromDate, to:toDate, opening_stock:openingStock, closing_stock:closingStock }) }),
]);
// Clear dirty markers
document.querySelectorAll('.edit-inp.dirty,.note-inp.dirty').forEach(inp => {
inp.classList.remove('dirty'); inp.classList.add('saved');
setTimeout(() => inp.classList.remove('saved'), 2500);
});
setStatus(`${toSave.length} override${toSave.length!==1?'s':''} saved — ${fmtDateDisplay(fromDate)} to ${fmtDateDisplay(toDate)}`, 'ok');
showToast(`Saved ${toSave.length} override${toSave.length!==1?'s':''}`, 'success');
} catch(err) {
setStatus('Save failed: ' + err.message, 'err');
showToast('Save failed: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = 'Save All';
}
}
window.saveAll = saveAll;
// ── Snapshot functions ────────────────────────────────────────────────────────
async function loadSnapshotList() {
try {
const r = await fetch('/api/costing/snapshots', { headers: hdrs });
const j = await r.json();
if (!j.success) return;
const buildOpt = s => {
const opt = document.createElement('option');
opt.value = s.id;
const dt = new Date(s.saved_at).toLocaleDateString('en-IN');
opt.textContent = `${s.snap_name} (${s.period_from} → ${s.period_to}) — ${dt} by ${s.saved_by||'?'}`;
return opt;
};
const isTb = s => /^TB:/i.test(s.snap_name || '');
// Populate MA dropdown — only Monthly Accounts snapshots
const maSnaps = j.data.filter(s => !isTb(s));
const sel = document.getElementById('snapSelect');
sel.innerHTML = '<option value="">— Select snapshot —</option>';
maSnaps.forEach(s => sel.appendChild(buildOpt(s)));
const hasItems = maSnaps.length > 0;
document.getElementById('btnLoadSnap').disabled = !hasItems;
document.getElementById('btnDelSnap').disabled = !hasItems;
// Populate TB dropdown — only Trial Balance snapshots
const tbSnaps = j.data.filter(isTb);
const tbSel = document.getElementById('tbSnapSelect');
tbSel.innerHTML = '<option value="">— Select a saved snapshot —</option>';
tbSnaps.forEach(s => tbSel.appendChild(buildOpt(s)));
document.getElementById('btnTbLoadSnap').disabled = tbSnaps.length === 0;
} catch(_) {}
}
async function loadTbSnapshotData() {
const id = document.getElementById('tbSnapSelect').value;
if (!id) { showToast('Select a snapshot first', 'error'); return; }
const btn = document.getElementById('btnTbLoadSnap');
btn.disabled = true; btn.textContent = 'Loading…';
try {
const r = await fetch(`/api/costing/snapshot/${id}`, { headers: hdrs });
const j = await r.json();
if (!j.success) throw new Error(j.message);
const d = j.data;
// Restore TB state from snapshot
_tbData = (d.accounts || []).map(a => ({
...a,
provision: a.override_val != null ? Number(a.override_val) : (a.provision != null ? Number(a.provision) : null),
}));
_tbFiltered = _tbData;
// Restore dates from snapshot period
const opt = document.getElementById('tbSnapSelect').selectedOptions[0];
const m = opt ? opt.textContent.match(/\((\d{4}-\d{2}-\d{2}) → (\d{4}-\d{2}-\d{2})\)/) : null;
if (m) {
document.getElementById('tbFrom').value = m[1];
document.getElementById('tbTo').value = m[2];
}
// Restore stock values
if (d.opening_stock != null && d.opening_stock !== 0)
document.getElementById('tbStockOpen').value = d.opening_stock;
if (d.closing_stock != null && d.closing_stock !== 0)
document.getElementById('tbStockClose').value = d.closing_stock;
renderTb(_tbFiltered);
document.getElementById('btnTbExport').disabled = false;
document.getElementById('btnTbSnapshot').disabled = false;
const label = opt ? opt.textContent.split(' (')[0] : id;
setTbStatus('Snapshot loaded — ' + label, 'ok');
showToast('Loaded: ' + label, 'success');
} catch(e) {
showToast('Error: ' + e.message, 'error');
} finally {
btn.disabled = false; btn.textContent = 'Load';
}
}
window.loadTbSnapshotData = loadTbSnapshotData;
let _snapSource = 'ma'; // 'ma' = Monthly Accounts, 'tb' = Trial Balance
function saveSnapshot() {
if (!allData.length) { showToast('Load data first', 'error'); return; }
_snapSource = 'ma';
document.getElementById('snapNameInput').value =
`${fmtDateDisplay(fromDate)} to ${fmtDateDisplay(toDate)}`;
document.getElementById('snapModal').style.display = 'flex';
setTimeout(() => document.getElementById('snapNameInput').select(), 50);
}
window.saveSnapshot = saveSnapshot;
async function saveTbSnapshot() {
const from = document.getElementById('tbFrom').value;
const to = document.getElementById('tbTo').value;
if (!from || !to || !_tbData.length) { showToast('Load Trial Balance first', 'error'); return; }
_snapSource = 'tb';
document.getElementById('snapNameInput').value = `TB: ${fmtDateDisplay(from)} to ${fmtDateDisplay(to)}`;
document.getElementById('snapModal').style.display = 'flex';
setTimeout(() => document.getElementById('snapNameInput').select(), 50);
}
window.saveTbSnapshot = saveTbSnapshot;
function closeSnapModal() { document.getElementById('snapModal').style.display = 'none'; }
window.closeSnapModal = closeSnapModal;
async function confirmSnapshot() {
const btn = document.getElementById('btnSnapConfirm');
btn.disabled = true; btn.textContent = 'Saving…';
try {
if (_snapSource === 'tb') {
await _confirmTbSnapshot();
} else {
await _confirmMaSnapshot();
}
} catch(err) { showToast('Error: ' + err.message, 'error'); }
finally { btn.disabled = false; btn.textContent = 'Save'; }
}
window.confirmSnapshot = confirmSnapshot;
async function _confirmMaSnapshot() {
const name = document.getElementById('snapNameInput').value.trim() || `${fromDate} to ${toDate}`;
const payload = {
from: fromDate, to: toDate, name,
data: { accounts: allData, opening_stock: openingStock, closing_stock: closingStock, stock_detail: stockDetail }
};
const r = await fetch('/api/costing/snapshot', { method:'POST', headers:hdrs, body:JSON.stringify(payload) });
const j = await r.json();
if (j.success) { closeSnapModal(); showToast('Snapshot saved: ' + name, 'success'); await loadSnapshotList(); }
else showToast('Save failed: ' + j.message, 'error');
}
async function _confirmTbSnapshot() {
const from = document.getElementById('tbFrom').value;
const to = document.getElementById('tbTo').value;
const name = document.getElementById('snapNameInput').value.trim() || `TB: ${from} to ${to}`;
// Fetch account mapping + stock detail in parallel
const [mapRes, stockDetailRes] = await Promise.all([
fetch('/api/costing/mapping', { headers:hdrs }).then(r => r.json()).catch(() => ({ success:false })),
fetch(`/api/costing/stock-detail?from=${from}&to=${to}`, { headers:hdrs }).then(r => r.json()).catch(() => ({ success:false })),
]);
// Build sheet map from account mapping
const sheetMap = {};
if (mapRes.success) mapRes.data.forEach(m => { sheetMap[m.acct_code] = m.sheet; });
// Only P&L accounts (GroupMask 4=Revenue, 5=Expenses) get a P&L sheet.
// Balance sheet accounts (1=Assets, 2=Liabilities, 3=Equity) MUST stay 'Other'
// so calcNetTaxFromAccounts skips them and they never distort net profit.
const accounts = _tbData.map(r => ({
...r,
sheet: (r.group_mask === 4 || r.group_mask === 5)
? (sheetMap[r.acct_code] || (r.group_mask === 4 ? 'Revenue' : 'Other'))
: 'Other',
}));
// Opening / closing stock from inputs or saved overrides
const opening_stock = parseFloat(document.getElementById('tbStockOpen').value) || 0;
const closing_stock = parseFloat(document.getElementById('tbStockClose').value) || 0;
// Stock detail (conv cost) for net stock calculation
let stock_detail = null;
if (stockDetailRes.success) {
const d = stockDetailRes.data;
stock_detail = {
open: { conv: d['__OPEN_CONV__'] || 0 },
close: { conv: d['__CLOSE_CONV__'] || 0 },
};
}
const payload = { from, to, name, data: { accounts, opening_stock, closing_stock, stock_detail } };
const r = await fetch('/api/costing/snapshot', { method:'POST', headers:hdrs, body:JSON.stringify(payload) });
const j = await r.json();
if (j.success) {
closeSnapModal();
showToast('TB Snapshot saved: ' + name, 'success');
await loadSnapshotList();
} else showToast('Save failed: ' + j.message, 'error');
}
async function loadSnapshot() {
const id = document.getElementById('snapSelect').value;
if (!id) { showToast('Select a snapshot', 'error'); return; }
const btn = document.getElementById('btnLoadSnap');
btn.disabled = true; btn.textContent = 'Loading…';
try {
const r = await fetch(`/api/costing/snapshot/${id}`, { headers: hdrs });
const j = await r.json();
if (!j.success) throw new Error(j.message);
const d = j.data;
// Restore state from snapshot
allData = d.accounts.map(r => ({ ...r, provision: r.override_val != null ? Number(r.override_val) : null }));
openingStock = d.opening_stock || 0;
closingStock = d.closing_stock || 0;
if (d.stock_detail) stockDetail = d.stock_detail;
// Restore dates from snapshot data
const opt = document.getElementById('snapSelect').selectedOptions[0];
const m = opt ? opt.textContent.match(/(\d{4}-\d{2}-\d{2}) → (\d{4}-\d{2}-\d{2})/) : null;
if (m) { fromDate = m[1]; toDate = m[2]; document.getElementById('dtFrom').value = fromDate; document.getElementById('dtTo').value = toDate; }
updateChips(); renderAllSheets(); renderSummary(); renderConfigure();
document.getElementById('btnSaveAll').disabled = false;
document.getElementById('btnSnapshot').disabled = false;
setStatus(`Snapshot loaded — ${opt ? opt.textContent : ''}`, 'ok');
showToast('Snapshot loaded', 'success');
} catch(err) { showToast('Error: ' + err.message, 'error'); }
finally { btn.disabled = false; btn.textContent = 'Load'; }
}
window.loadSnapshot = loadSnapshot;
async function deleteSnapshot() {
const id = document.getElementById('snapSelect').value;
if (!id) return;
if (!confirm('Delete this snapshot? This cannot be undone.')) return;
try {
const r = await fetch(`/api/costing/snapshot/${id}`, { method: 'DELETE', headers: hdrs });
const j = await r.json();
if (j.success) { showToast('Snapshot deleted', 'success'); await loadSnapshotList(); }
else showToast('Delete failed: ' + j.message, 'error');
} catch(err) { showToast('Error: ' + err.message, 'error'); }
}
window.deleteSnapshot = deleteSnapshot;
// Pre-render all sheet panes so switching is instant
function renderAllSheets() {
SHEETS.forEach(s => renderSheet(s.key));
}
// ── Tab switching ─────────────────────────────────────────────────────────────
function switchTab(key) {
activeTab = key;
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === key));
document.querySelectorAll('.tab-content').forEach(p => p.classList.toggle('active', p.id === 'pane-' + key));
if (key === 'Provisions') renderProvisions();
filterSearch();
}
window.switchTab = switchTab;
// ── Update chips & badges ─────────────────────────────────────────────────────
function updateChips() {
const counts = {};
SHEETS.forEach(s => { counts[s.key] = 0; });
allData.forEach(r => { const k = r.sheet||'Other'; if(counts[k]!==undefined) counts[k]++; });
SHEETS.forEach(s => {
const b = document.getElementById('badge-' + s.key);
const c = document.getElementById('cnt-' + s.key);
if (b) b.textContent = counts[s.key];
if (c) c.textContent = counts[s.key];
});
document.getElementById('summaryChips').style.display = 'flex';
}
// ── Status bar ────────────────────────────────────────────────────────────────
function setStatus(msg, type='') {
const dot = document.getElementById('statusDot');
const txt = document.getElementById('statusMsg');
if (dot) dot.className = 'status-dot' + (type ? ' '+type : '');
if (txt) txt.textContent = msg;
}
// ── Search / filter ───────────────────────────────────────────────────────────
function filterSearch() {
const q = document.getElementById('searchInput')?.value?.toLowerCase().trim() || '';
const pane = document.querySelector('.tab-content.active');
if (!pane) return;
const rows = pane.querySelectorAll('tbody tr:not(.total-row)');
let visible = 0;
rows.forEach(tr => {
const show = !q || tr.textContent.toLowerCase().includes(q);
tr.style.display = show ? '' : 'none';
if (show) visible++;
});
const cnt = document.getElementById('srchCount');
if (cnt) cnt.textContent = q && rows.length ? `${visible} of ${rows.length}` : '';
}
// ── Render a sheet into its pane ──────────────────────────────────────────────
function renderSheet(sheetKey) {
const pane = document.getElementById('pane-' + sheetKey);
if (!pane) return;
const rows = allData.filter(r => r.sheet === sheetKey)
.sort((a,b) => (a.sort_order - b.sort_order) || a.acct_code.localeCompare(b.acct_code));
const def = SHEETS.find(s => s.key === sheetKey) || { label: sheetKey };
if (!rows.length) {
pane.innerHTML = `<div class="empty">No accounts mapped to <b>${esc(def.label)}</b>.${isAdmin?' Use the <b>Configure</b> tab to assign accounts.':''}</div>`;
return;
}
const isRev = sheetKey === 'Revenue';
const thead = `<thead><tr>
<th>Code</th><th>Account Name</th>
<th class="num">Period Debit (&#8377;)</th>
<th class="num">Period Credit (&#8377;)</th>
<th class="num">Provision</th>
<th class="num">Net &#8377; <span style="font-size:9px;font-weight:400;color:#9ca3af">${isRev?'(Cr−Dr+Prov)':'(Dr−Cr+Prov)'}</span></th>
<th>Notes</th>
</tr></thead>`;
// Check if any row has a head assigned
const hasHeads = rows.some(r => r.head && r.head.trim());
let bodyHtml = '';
let tPD = 0, tPC = 0, tProv = 0, tFinal = 0;
if (hasHeads) {
// Group by head; rows without head go under '' group
const headOrder = [];
const headMap = {};
rows.forEach(r => {
const h = (r.head || '').trim();
if (!headMap[h]) { headMap[h] = []; headOrder.push(h); }
headMap[h].push(r);
});
headOrder.forEach(h => {
const hRows = headMap[h];
let hPD = 0, hPC = 0, hProv = 0, hFinal = 0;
hRows.forEach(r => {
hPD += r.period_debit;
hPC += r.period_credit;
hProv += r.provision || 0;
hFinal += finalVal(r, sheetKey);
tPD += r.period_debit; tPC += r.period_credit;
tProv += r.provision || 0; tFinal += finalVal(r, sheetKey);
});
if (h) {
bodyHtml += `<tr><td colspan="7" style="background:#e0f4f4;padding:7px 12px;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#004d40;border-top:2px solid #80cbc4">${esc(h)}</td></tr>`;
}
bodyHtml += hRows.map(r => rowHtml(r, sheetKey)).join('');
bodyHtml += `<tr style="background:#f5fefe">
<td colspan="2" style="padding:6px 12px;font-size:11px;font-weight:700;color:#047e7e;padding-left:24px">
${h ? 'Sub-total — '+esc(h) : 'Sub-total'}
</td>
<td class="num" style="font-weight:700">${raw(hPD)}</td>
<td class="num" style="font-weight:700">${raw(hPC)}</td>
<td class="num" style="font-weight:700">${hProv !== 0 ? raw(hProv) : '—'}</td>
<td class="num" style="font-weight:700">${raw(hFinal)}</td>
<td></td>
</tr>`;
});
} else {
rows.forEach(r => {
tPD += r.period_debit;
tPC += r.period_credit;
tProv += r.provision || 0;
tFinal += finalVal(r, sheetKey);
bodyHtml += rowHtml(r, sheetKey);
});
}
// Grand total row
bodyHtml += `<tr class="total-row">
<td colspan="2">Total — ${esc(def.label)}</td>
<td class="num">${raw(tPD)}</td>
<td class="num">${raw(tPC)}</td>
<td class="num">${tProv !== 0 ? raw(tProv) : '—'}</td>
<td class="num"><b>${raw(tFinal)}</b></td>
<td></td>
</tr>`;
const stockPanelHtml = sheetKey === 'Purchase' ? buildStockPanel() : '';
pane.innerHTML = stockPanelHtml + `<div class="tbl-wrap"><table class="pl-tbl">${thead}<tbody>${bodyHtml}</tbody></table></div>`;
}
function buildStockPanel() {
const o = stockDetail.open, c = stockDetail.close;
const oTotal = (o.rm+o.wip+o.fg+o.trade), cTotal = (c.rm+c.wip+c.fg+c.trade);
const oNet = oTotal - o.conv, cNet = cTotal - c.conv;
const inp = (side, field, val) =>
`<input type="number" step="0.01" value="${val||''}" placeholder="0.00"
style="width:160px;border:1.5px solid #dde1e7;border-radius:6px;padding:4px 8px;font-size:12px;
font-family:inherit;text-align:right;outline:none;background:#fff"
onfocus="this.style.borderColor='#047e7e'"
onblur="this.style.borderColor='';onStockInput('${side}','${field}',this)">`;
// Opening Stock is frozen/auto-carried from previous month's Closing Stock —
// shown read-only so it can't be edited directly.
const frozen = val =>
`<div style="width:160px;border:1.5px solid #eef0f3;border-radius:6px;padding:4px 8px;font-size:12px;
font-family:inherit;text-align:right;background:#f3f4f6;color:#6b7280">${fmtN(val)}</div>`;
const row = (lbl, openCell, closeCell, isSub, isBold) =>
`<tr style="${isBold?'font-weight:700;background:#e0f4f4':''}">
<td style="padding:6px 12px;font-size:12px;${isSub?'padding-left:24px;color:#6b7280':''}">${lbl}</td>
<td style="padding:6px 12px;text-align:right">${openCell}</td>
<td style="padding:6px 12px;text-align:right">${closeCell}</td>
</tr>`;
return `
<div style="padding:16px 20px;border-bottom:2px solid #e8eaed;background:#f8fafb">
<div style="font-size:12px;font-weight:700;color:#374151;margin-bottom:12px">Stock Entry</div>
<div style="font-size:10.5px;color:#9ca3af;margin-bottom:8px">Opening Stock is auto-carried from the previous month's Closing Stock and frozen.</div>
<table style="border-collapse:collapse;font-size:12px;width:100%;max-width:600px">
<thead><tr>
<th style="padding:6px 12px;text-align:left;font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;width:200px"></th>
<th style="padding:6px 12px;text-align:right;font-size:10px;font-weight:700;color:#047e7e;text-transform:uppercase;letter-spacing:.5px">Opening Stock</th>
<th style="padding:6px 12px;text-align:right;font-size:10px;font-weight:700;color:#7c3aed;text-transform:uppercase;letter-spacing:.5px">Closing Stock</th>
</tr></thead>
<tbody>
${row('Raw Material', frozen(o.rm), inp('close','rm',c.rm), true,false)}
${row('WIP', frozen(o.wip), inp('close','wip',c.wip), true,false)}
${row('FG', frozen(o.fg), inp('close','fg',c.fg), true,false)}
${row('Trading', frozen(o.trade), inp('close','trade',c.trade),true,false)}
${row('Total',`<span id="stk_open_total">${fmtN(oTotal)}</span>`,`<span id="stk_close_total">${fmtN(cTotal)}</span>`,false,true)}
${row('Less: Conversion Cost in FG/Quarantine', frozen(o.conv), inp('close','conv',c.conv), true,false)}
${row('Net Stock',`<span id="stk_open_net" style="color:#047e7e">${fmtN(oNet)}</span>`,`<span id="stk_close_net" style="color:#7c3aed">${fmtN(cNet)}</span>`,false,true)}
</tbody>
</table>
</div>`;
}
function rowHtml(r, sheetKey) {
const fv = finalVal(r, sheetKey);
const pStr = r.provision != null ? String(r.provision) : '';
return `<tr>
<td><span class="code">${esc(r.acct_code)}</span></td>
<td>${esc(r.acct_name)}</td>
<td class="num">${raw(r.period_debit)}</td>
<td class="num">${raw(r.period_credit)}</td>
<td class="num">
<input class="edit-inp" type="number" step="0.01" data-code="${esc(r.acct_code)}"
value="${esc(pStr)}" placeholder="0" oninput="markDirty(this)" onblur="saveProvision(this)">
</td>
<td class="num ${amtCls(fv)}"><b>${raw(fv)}</b></td>
<td><input class="note-inp" type="text" data-code="${esc(r.acct_code)}"
value="${esc(r.note||'')}" placeholder="Note…" oninput="markDirtyNote(this)" onblur="saveNote(this)"></td>
</tr>`;
}
// Revenue: Credit − Debit + Provision | Others: Debit − Credit + Provision
function finalVal(r, sheetKey) {
const prov = r.provision != null ? Number(r.provision) : 0;
if (sheetKey === 'Revenue') return r.period_credit - r.period_debit + prov;
return r.period_debit - r.period_credit + prov;
}
// ── Provision & Note save ─────────────────────────────────────────────────────
function markDirty(inp) { inp.classList.add('dirty'); inp.classList.remove('saved'); }
window.markDirty = markDirty;
function markDirtyNote(inp) { inp.classList.add('dirty'); }
window.markDirtyNote = markDirtyNote;
async function saveProvision(inp) {
if (!inp.classList.contains('dirty')) return;
const code = inp.dataset.code;
const val = inp.value === '' ? null : parseFloat(inp.value);
const note = inp.closest('tr')?.querySelector('.note-inp')?.value || '';
try {
const r = await fetch('/api/costing/override', { method:'POST', headers:hdrs,
body: JSON.stringify({ acct_code:code, from:fromDate, to:toDate, override_val:val, note }) });
const j = await r.json();
if (j.success) {
inp.classList.remove('dirty'); inp.classList.add('saved');
const row = allData.find(d => d.acct_code === code);
if (row) row.provision = val;
setTimeout(() => inp.classList.remove('saved'), 2000);
const netTd = inp.closest('tr')?.querySelectorAll('td')[5];
if (netTd) { const fv = finalVal(row, row?.sheet); netTd.innerHTML = `<b class="${amtCls(fv)}">${raw(fv)}</b>`; }
renderSummary();
} else showToast('Save failed: '+j.message,'error');
} catch(err) { showToast('Error: '+err.message,'error'); }
}
window.saveProvision = saveProvision;
async function saveNote(inp) {
if (!inp.classList.contains('dirty')) return;
const code = inp.dataset.code;
const note = inp.value;
const pEl = inp.closest('tr')?.querySelector('.edit-inp');
const pVal = pEl?.value === '' ? null : (pEl?.value != null ? parseFloat(pEl.value) : null);
try {
const r = await fetch('/api/costing/override', { method:'POST', headers:hdrs,
body: JSON.stringify({ acct_code:code, from:fromDate, to:toDate, override_val:pVal, note }) });
const j = await r.json();
if (j.success) { inp.classList.remove('dirty'); const row=allData.find(d=>d.acct_code===code); if(row) row.note=note; }
else showToast('Save failed: '+j.message,'error');
} catch(err) { showToast('Error: '+err.message,'error'); }
}
window.saveNote = saveNote;
// ── Opening / Closing Stock ───────────────────────────────────────────────────
async function loadStockValues() {
try {
const [r, detailRes] = await Promise.all([
fetch(`/api/costing/stock?from=${fromDate}&to=${toDate}`, { headers:hdrs }),
fetch(`/api/costing/stock-detail?from=${fromDate}&to=${toDate}`, { headers:hdrs }),
]);
const j = await r.json();
if (j.success) { openingStock = Number(j.opening_stock)||0; closingStock = Number(j.closing_stock)||0; }
if (detailRes.ok) {
const dj = await detailRes.json();
if (dj.success) {
const d = dj.data;
stockDetail.open.rm = d['__OPEN_RM__'] || 0;
stockDetail.open.wip = d['__OPEN_WIP__'] || 0;
stockDetail.open.fg = d['__OPEN_FG__'] || 0;
stockDetail.open.trade = d['__OPEN_TRADE__'] || 0;
stockDetail.open.conv = d['__OPEN_CONV__'] || 0;
stockDetail.close.rm = d['__CLOSE_RM__'] || 0;
stockDetail.close.wip = d['__CLOSE_WIP__'] || 0;
stockDetail.close.fg = d['__CLOSE_FG__'] || 0;
stockDetail.close.trade= d['__CLOSE_TRADE__']|| 0;
stockDetail.close.conv = d['__CLOSE_CONV__'] || 0;
// Opening stock is frozen: if not yet set for this month, auto-carry
// forward the previous month's Closing Stock as this month's Opening.
// Exception: the financial year's first month (April) keeps its own
// fixed opening balance and is never auto-carried from the prior FY.
const isFyStart = /-04-01$/.test(fromDate);
const openIsEmpty = !stockDetail.open.rm && !stockDetail.open.wip &&
!stockDetail.open.fg && !stockDetail.open.trade && !stockDetail.open.conv;
if (openIsEmpty && isFyStart) {
// Fixed FY-opening was saved against the first month's own period
// (e.g. 2026-04-01 → 2026-04-30). Look that up regardless of the
// current "to" date so multi-month ranges still show it.
const fyYear = fromDate.slice(0,4);
try {
const fixedRes = await fetch(`/api/costing/stock-detail?from=${fromDate}&to=${fyYear}-04-30`, { headers:hdrs });
if (fixedRes.ok) {
const fixedJ = await fixedRes.json();
if (fixedJ.success) {
const fd = fixedJ.data;
stockDetail.open = {
rm: fd['__OPEN_RM__'] || 0,
wip: fd['__OPEN_WIP__'] || 0,
fg: fd['__OPEN_FG__'] || 0,
trade: fd['__OPEN_TRADE__'] || 0,
conv: fd['__OPEN_CONV__'] || 0,
};
}
}
} catch(_) {}
} else if (openIsEmpty && !isFyStart) {
const f = new Date(fromDate);
const prevTo = new Date(f.getTime() - 86400000);
const prevFrom = new Date(prevTo.getFullYear(), prevTo.getMonth(), 1);
const pad = n => String(n).padStart(2,'0');
const toStr = d2 => `${d2.getFullYear()}-${pad(d2.getMonth()+1)}-${pad(d2.getDate())}`;
try {
const prevRes = await fetch(`/api/costing/stock-detail?from=${toStr(prevFrom)}&to=${toStr(prevTo)}`, { headers:hdrs });
if (prevRes.ok) {
const prevJ = await prevRes.json();
if (prevJ.success) {
const pd = prevJ.data;
const carry = {
rm: pd['__CLOSE_RM__'] || 0,
wip: pd['__CLOSE_WIP__'] || 0,
fg: pd['__CLOSE_FG__'] || 0,
trade: pd['__CLOSE_TRADE__'] || 0,
conv: pd['__CLOSE_CONV__'] || 0,
};
if (carry.rm||carry.wip||carry.fg||carry.trade||carry.conv) {
stockDetail.open = carry;
for (const field of Object.keys(carry)) {
await saveStockField(STOCK_KEYS.open[field], carry[field]);
}
}
}
}
} catch(_) {}
}
// Update net stock from saved details
openingStock = (stockDetail.open.rm+stockDetail.open.wip+stockDetail.open.fg+stockDetail.open.trade) - stockDetail.open.conv;
closingStock = (stockDetail.close.rm+stockDetail.close.wip+stockDetail.close.fg+stockDetail.close.trade) - stockDetail.close.conv;
}
}
} catch(_) {}
}
async function saveStockField(key, value) {
try {
await fetch('/api/costing/override', { method:'POST', headers:hdrs,
body: JSON.stringify({ acct_code:key, from:fromDate, to:toDate, override_val:value, note:'' }) });
} catch(err) { showToast('Save error: '+err.message,'error'); }
}
function recalcStock(side) {
const s = stockDetail[side];
const total = (s.rm||0)+(s.wip||0)+(s.fg||0)+(s.trade||0);
const net = total - (s.conv||0);
const totalEl = document.getElementById(`stk_${side}_total`);
const netEl = document.getElementById(`stk_${side}_net`);
if (totalEl) totalEl.textContent = fmtN(total);
if (netEl) netEl.textContent = fmtN(net);
if (side === 'open') {
openingStock = net;
const el = document.getElementById('inp_open_stock');
if (el) el.value = net;
} else {
closingStock = net;
const el = document.getElementById('inp_close_stock');
if (el) el.value = net;
}
saveStockField(side==='open'?'__STOCK_OPEN__':'__STOCK_CLOSE__', net);
recalcSummary();
}
window.recalcStock = recalcStock;
async function onStockInput(side, field, el) {
const val = parseFloat(el.value)||0;
stockDetail[side][field] = val;
const key = STOCK_KEYS[side][field];
await saveStockField(key, val);
recalcStock(side);
}
window.onStockInput = onStockInput;
function fmtN(v) {
return (v||0).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
}
async function saveStock() {
// legacy — kept for P&L summary inputs
const os = openingStock;
const cs = closingStock;
try {
await fetch('/api/costing/stock', { method:'POST', headers:hdrs,
body: JSON.stringify({ from:fromDate, to:toDate, opening_stock:os, closing_stock:cs }) });
} catch(err) {}
}
window.saveStock = saveStock;
function recalcSummary() { renderSummary(); }
window.recalcSummary = recalcSummary;
// ── P&L Summary ───────────────────────────────────────────────────────────────
function renderSummary() {
const pane = document.getElementById('pane-Summary');
if (!pane) return;
if (!allData.length) { pane.innerHTML='<div class="empty">Load data to view P&amp;L Summary</div>'; return; }
// Head-level totals per sheet (ordered by sort_order)
function getHeadTotals(sheetKey, negate) {
const rows = allData.filter(r => r.sheet === sheetKey);
const headMap = {}, headOrder = [];
rows.forEach(r => {
const h = (r.head || '').trim() || r.acct_name;
if (!headMap[h]) { headMap[h] = { total:0, minOrd: r.sort_order }; headOrder.push(h); }
const v = finalVal(r, sheetKey);
headMap[h].total += negate ? -v : v;
headMap[h].minOrd = Math.min(headMap[h].minOrd, r.sort_order);
});
headOrder.sort((a,b) => headMap[a].minOrd - headMap[b].minOrd);
return headOrder.map(h => ({ head: h, total: headMap[h].total }));
}
const tot = {}; SHEETS.forEach(s=>{tot[s.key]=0;});
allData.forEach(r=>{const s=r.sheet||'Other';if(tot[s]!==undefined)tot[s]+=finalVal(r,s);});
const rev = tot['Revenue'] || 0;
const purchT = tot['Purchase'] || 0;
const matCost = openingStock + purchT - closingStock;
const openConv = stockDetail.open.conv || 0;
const closeConv= stockDetail.close.conv || 0;
const convChange = openConv - closeConv;
const totalConsumption = matCost + convChange;
const grossP = rev - totalConsumption;
const empT = tot['Employee'] || 0;
const factT = tot['Factory'] || 0;
const adminT = tot['Admin'] || 0;
const sndT = tot['SND'] || 0;
const finT = tot['Finance'] || 0;
const oiT = -(tot['OtherIncome'] || 0);
const totalExp = empT + factT + adminT + sndT + finT;
const netOps = grossP - totalExp;
const netTax = netOps + oiT;
const pct = v => rev !== 0 ? (Math.abs(v) / Math.abs(rev) * 100).toFixed(1) + '%' : '—';
const n = v => Math.abs(v||0).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
// ── Row builders ──
const blankRow = () => `<tr><td colspan="3" style="height:10px"></td></tr>`;
const sectionHdr = (label) =>
`<tr style="background:#f8fafb;border-top:1px solid #e8eaed">
<td colspan="3" style="padding:8px 14px 4px;font-size:12px;font-weight:700;color:#1a1a2e">${esc(label)}</td>
</tr>`;
const subRow = (label, val, showPct=true) =>
`<tr>
<td style="padding:5px 14px 5px 30px;font-size:12px;color:#047e7e">${esc(label)}</td>
<td style="padding:5px 14px;text-align:right;font-size:11px;color:#9ca3af">${showPct?pct(val):''}</td>
<td style="padding:5px 14px;text-align:right;font-size:12px;font-weight:600;color:#047e7e">${n(val)}</td>
</tr>`;
const totalRow = (label, val, size=12) =>
`<tr style="background:#e0f4f4">
<td style="padding:7px 14px;font-size:${size}px;font-weight:700;color:#004d40">${esc(label)}</td>
<td style="padding:7px 14px;text-align:right;font-size:11px;font-weight:700;color:#004d40">${pct(val)}</td>
<td style="padding:7px 14px;text-align:right;font-size:${size}px;font-weight:800;color:${val>=0?'#047e7e':'#dc2626'}">${n(val)}</td>
</tr>`;
const buildSection = (sheetKey, sectionLabel, totalLabel, negate=false) => {
if (!allData.some(r => r.sheet === sheetKey)) return '';
const heads = getHeadTotals(sheetKey, negate);
let html = sectionHdr(sectionLabel);
heads.forEach(({head, total}) => { html += subRow(head, total); });
const sheetTotal = negate ? -(tot[sheetKey]||0) : (tot[sheetKey]||0);
html += totalRow(totalLabel, sheetTotal);
html += blankRow();
return html;
};
// Employee section: "Direct Expenses" head expands to individual accounts; all other heads show as one row
const buildEmployeeSection = () => {
const rows = allData.filter(r => r.sheet === 'Employee');
if (!rows.length) return '';
const headMap = {}, headOrder = [];
rows.forEach(r => {
const h = (r.head || '').trim() || r.acct_name;
if (!headMap[h]) { headMap[h] = { accts:[], total:0, minOrd:r.sort_order }; headOrder.push(h); }
headMap[h].accts.push(r);
headMap[h].total += finalVal(r, 'Employee');
headMap[h].minOrd = Math.min(headMap[h].minOrd, r.sort_order);
});
headOrder.sort((a,b) => headMap[a].minOrd - headMap[b].minOrd);
let html = sectionHdr('Employee Benefit Expenses');
headOrder.forEach(h => {
if (h === 'Direct Expenses') {
headMap[h].accts
.sort((a,b) => (a.sort_order - b.sort_order) || a.acct_name.localeCompare(b.acct_name))
.forEach(r => { html += subRow(r.acct_name, finalVal(r,'Employee')); });
} else {
html += subRow(h, headMap[h].total);
}
});
html += totalRow('Total Employee Benefit Expenses', tot['Employee']||0);
html += blankRow();
return html;
};
pane.innerHTML = `<div style="overflow-x:auto;max-height:65vh;overflow-y:auto">
<table style="border-collapse:collapse;width:100%;min-width:480px">
<thead>
<tr style="background:#047e7e">
<th style="padding:11px 14px;text-align:left;font-size:11px;font-weight:700;color:#fff;letter-spacing:.3px">Particulars</th>
<th style="padding:11px 14px;text-align:right;font-size:11px;font-weight:700;color:#fff;width:120px">% of Revenue</th>
<th style="padding:11px 14px;text-align:right;font-size:11px;font-weight:700;color:#fff;width:160px">${esc(fmtDateDisplay(fromDate))} &mdash; ${esc(fmtDateDisplay(toDate))}</th>
</tr>
</thead>
<tbody>
${totalRow('Revenue from Operations', rev, 13)}
${blankRow()}
${subRow('Opening Stock', openingStock)}
${subRow('Purchases', purchT)}
${subRow('Closing Stock', closingStock)}
${totalRow('Consumption of Material', matCost)}
<tr>
<td style="padding:5px 14px 5px 30px;font-size:12px;color:${convChange>=0?'#047e7e':'#dc2626'}">Change in Conversion Cost</td>
<td style="padding:5px 14px;text-align:right;font-size:11px;color:#9ca3af">${pct(convChange)}</td>
<td style="padding:5px 14px;text-align:right;font-size:12px;font-weight:600;color:${convChange>=0?'#047e7e':'#dc2626'}">${convChange<0?'-':''}${n(convChange)}</td>
</tr>
${totalRow('Total Consumption', totalConsumption)}
${blankRow()}
<tr style="background:#e0f4f4;border-top:2px solid #80cbc4">
<td style="padding:9px 14px;font-size:13px;font-weight:800;color:#004d40">Gross Profit</td>
<td style="padding:9px 14px;text-align:right;font-size:11px;font-weight:700;color:#004d40">${pct(grossP)}</td>
<td style="padding:9px 14px;text-align:right;font-size:14px;font-weight:800;color:${grossP>=0?'#047e7e':'#dc2626'}">${n(grossP)}</td>
</tr>
${blankRow()}
${buildEmployeeSection()}
${buildSection('Factory', 'Factory / Utility Expenses','Total Factory Expenses')}
${buildSection('Admin', 'Administration Expenses','Total Administration Expenses')}
${buildSection('SND', 'Selling &amp; Distribution Expenses','Total Selling &amp; Distribution Expenses')}
${buildSection('Finance', 'Finance Cost','Total Finance Cost')}
${totalRow('Total Expenses', totalExp)}
${blankRow()}
<tr style="background:${netOps>=0?'#d1fae5':'#fee2e2'};border-top:2px solid ${netOps>=0?'#6ee7b7':'#fca5a5'}">
<td style="padding:9px 14px;font-size:13px;font-weight:800;color:${netOps>=0?'#065f46':'#991b1b'}">Net Profit from Operations</td>
<td style="padding:9px 14px;text-align:right;font-size:11px;font-weight:700;color:${netOps>=0?'#065f46':'#991b1b'}">${pct(netOps)}</td>
<td style="padding:9px 14px;text-align:right;font-size:14px;font-weight:800;color:${netOps>=0?'#047e7e':'#dc2626'}">${n(netOps)}</td>
</tr>
${oiT !== 0 ? sectionHdr('Other Income') +
getHeadTotals('OtherIncome', true).map(({head,total}) => subRow(head, total)).join('') +
`<tr style="background:#f0fdf4">
<td style="padding:6px 14px;font-size:12px;font-weight:700;color:#166534">Total Other Income</td>
<td style="padding:6px 14px;text-align:right;font-size:11px;color:#166534">${pct(oiT)}</td>
<td style="padding:6px 14px;text-align:right;font-size:12px;font-weight:700;color:#166534">${n(oiT)}</td>
</tr>${blankRow()}` : ''}
<tr style="background:${netTax>=0?'#004d40':'#7f1d1d'}">
<td style="padding:13px 14px;font-size:14px;font-weight:800;color:#fff">${netTax>=0?'Net Profit before Tax':'Net Loss before Tax'}</td>
<td style="padding:13px 14px;text-align:right;font-size:12px;font-weight:700;color:rgba(255,255,255,.8)">${pct(netTax)}</td>
<td style="padding:13px 14px;text-align:right;font-size:16px;font-weight:800;color:#fff">${n(netTax)}</td>
</tr>
</tbody>
</table>
<div style="padding:8px 12px;font-size:10px;color:#9ca3af">
* Revenue &amp; Other Income: credit accounts (Credit − Debit + Provision). Override values included.
</div>
</div>`;
}
// ── Provision Summary ─────────────────────────────────────────────────────────
let _provData = null;
async function renderProvisions() {
const pane = document.getElementById('pane-Provisions');
if (!pane) return;
pane.innerHTML = '<div class="empty">Loading…</div>';
try {
if (!_provData) {
const r = await fetch('/api/costing/provisions', { headers: hdrs });
const j = await r.json();
if (!j.success) { pane.innerHTML = `<div class="empty">${j.message || 'Failed to load'}</div>`; return; }
_provData = j.data || [];
}
if (!_provData.length) { pane.innerHTML = '<div class="empty">No provisions entered yet</div>'; return; }
const n = v => {
const num = parseFloat(v) || 0;
const s = Math.abs(num).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
return num < 0 ? `<span class="amt-neg">(${s})</span>` : `<span class="amt-pos">${s}</span>`;
};
// Columns = distinct periods (months), ordered by period_from
const periodKey = r => r.period_from + '|' + r.period_to;
const periodMap = {};
_provData.forEach(r => { periodMap[periodKey(r)] = { from:r.period_from, to:r.period_to }; });
const periods = Object.keys(periodMap).sort().map(k => periodMap[k]);
// Rows = distinct accounts
const acctMap = {};
const acctOrder = [];
_provData.forEach(r => {
if (!acctMap[r.acct_code]) {
acctMap[r.acct_code] = { acct_code:r.acct_code, acct_name:r.acct_name, head:r.head, sheet:r.sheet, vals:{} };
acctOrder.push(r.acct_code);
}
acctMap[r.acct_code].vals[periodKey(r)] = r;
});
const colTotals = periods.map(() => 0);
let grandTotal = 0;
const rowsHtml = acctOrder.map(code => {
const a = acctMap[code];
let rowTotal = 0;
const cells = periods.map((p, i) => {
const rec = a.vals[p.from + '|' + p.to];
if (!rec) return `<td style="padding:6px 10px;text-align:right;font-size:12px">&nbsp;</td>`;
const val = parseFloat(rec.override_val) || 0;
rowTotal += val; colTotals[i] += val;
return `<td style="padding:6px 10px;text-align:right;font-size:12px">${n(rec.override_val)}</td>`;
}).join('');
grandTotal += rowTotal;
return `<tr>
<td style="padding:6px 14px;font-size:12px;white-space:nowrap">${esc(a.acct_code)}</td>
<td style="padding:6px 14px;font-size:12px;white-space:nowrap">${esc(a.acct_name || '')}</td>
<td style="padding:6px 14px;font-size:12px;white-space:nowrap">${esc(a.head || a.sheet || '')}</td>
${cells}
<td style="padding:6px 14px;text-align:right;font-size:12px;font-weight:700">${n(rowTotal)}</td>
</tr>`;
}).join('');
const periodHeaders = periods.map(p => `<th class="bdr" style="min-width:110px;text-align:right;padding:6px 10px;font-size:11px;color:#6b7280">${esc(p.from)}<br>&rarr; ${esc(p.to)}</th>`).join('');
const totalRow = `<tr style="background:#f3f4f6">
<td colspan="3" style="padding:6px 14px;font-size:12px;font-weight:700">Total</td>
${colTotals.map(t => `<td style="padding:6px 10px;text-align:right;font-size:12px;font-weight:700">${n(t)}</td>`).join('')}
<td style="padding:6px 14px;text-align:right;font-size:12px;font-weight:700;color:#7c3aed">${n(grandTotal)}</td>
</tr>`;
let html = `<div style="padding:12px;overflow-x:auto">
<table style="width:100%;border-collapse:collapse">
<thead>
<tr style="background:#fafafa">
<th style="padding:6px 14px;text-align:left;font-size:11px;color:#6b7280">Acct Code</th>
<th style="padding:6px 14px;text-align:left;font-size:11px;color:#6b7280">Account Name</th>
<th style="padding:6px 14px;text-align:left;font-size:11px;color:#6b7280">Head / Sheet</th>
${periodHeaders}
<th style="padding:6px 14px;text-align:right;font-size:11px;color:#6b7280">Total</th>
</tr>
</thead>
<tbody>${rowsHtml}${totalRow}</tbody>
</table>
</div>`;
pane.innerHTML = html;
} catch (err) {
pane.innerHTML = `<div class="empty">${err.message}</div>`;
}
}
async function deleteProvision(acctCode, from, to) {
if (!confirm('Delete this provision entry?')) return;
try {
const r = await fetch(`/api/costing/override?acct_code=${encodeURIComponent(acctCode)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
{ method: 'DELETE', headers: hdrs });
const j = await r.json();
if (!j.success) { alert(j.message || 'Delete failed'); return; }
_provData = null;
renderProvisions();
} catch (err) { alert(err.message); }
}
window.deleteProvision = deleteProvision;
// ── Configure ─────────────────────────────────────────────────────────────────
function renderConfigure() {
const pane = document.getElementById('pane-Configure');
if (!pane) return;
if (!isAdmin) { pane.innerHTML='<div class="empty">Admin access required</div>'; return; }
if (!allData.length) { pane.innerHTML='<div class="empty">Load data first to configure account mappings</div>'; return; }
const sorted = [...allData].sort((a,b)=>a.acct_code.localeCompare(b.acct_code));
const rows = sorted.map(r=>`<tr>
<td><span class="code">${esc(r.acct_code)}</span></td>
<td>${esc(r.acct_name)}</td>
<td><select class="cfg-sel" data-code="${esc(r.acct_code)}" onchange="autoSaveSingleMapping(this)">
${SHEETS.map(s=>`<option value="${s.key}"${r.sheet===s.key?' selected':''}>${s.label}</option>`).join('')}
</select></td>
<td><select class="cfg-head" data-code="${esc(r.acct_code)}"
style="border:1.5px solid #dde1e7;border-radius:6px;padding:5px 8px;font-size:12px;font-family:inherit;outline:none;min-width:160px;background:#fff"
onchange="autoSaveSingleMapping(this.closest('tr').querySelector('.cfg-sel'))">
<option value="">— None —</option>
${HEAD_OPTIONS.map(h=>`<option value="${esc(h)}"${(r.head||'')===h?' selected':''}>${esc(h)}</option>`).join('')}
</select></td>
<td><input class="cfg-ord" type="number" value="${r.sort_order}" data-code="${esc(r.acct_code)}" onblur="autoSaveOrderChange(this)"></td>
</tr>`).join('');
pane.innerHTML = `
<div style="padding:12px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border-bottom:1px solid #e8eaed;background:#f8fafb">
<span style="font-size:12px;font-weight:700;color:#374151">Account Sheet Configuration &mdash;
<span style="font-weight:400;color:#6b7280">${sorted.length} accounts &nbsp;&bull;&nbsp; changes save automatically</span>
</span>
</div>
<div class="tbl-wrap">
<table class="pl-tbl">
<thead><tr><th>Account Code</th><th>Account Name</th><th>Sheet</th><th>Head</th><th>Order</th></tr></thead>
<tbody>${rows}</tbody>
</table></div>`;
}
// ── Auto-save mapping ─────────────────────────────────────────────────────────
async function autoSaveSingleMapping(sel) {
const code = sel.dataset.code, sheet = sel.value;
const ord = parseInt(sel.closest('tr').querySelector('.cfg-ord')?.value)||999;
const head = sel.closest('tr').querySelector('.cfg-head')?.value || '';
const row = allData.find(r=>r.acct_code===code);
if (row) { row.sheet=sheet; row.sort_order=ord; row.head=head; }
sel.style.borderColor='#f59e0b';
try {
const r = await fetch('/api/costing/mapping',{method:'POST',headers:hdrs,body:JSON.stringify([{acct_code:code,sheet,sort_order:ord,head}])});
const j = await r.json();
if(j.success){sel.style.borderColor='#10b981';setTimeout(()=>{sel.style.borderColor='';renderAllSheets();renderSummary();},1500);}
else{sel.style.borderColor='#ef4444';showToast('Save failed: '+j.message,'error');}
} catch(err){sel.style.borderColor='#ef4444';showToast('Error: '+err.message,'error');}
}
window.autoSaveSingleMapping = autoSaveSingleMapping;
async function autoSaveOrderChange(inp) {
const code=inp.dataset.code, ord=parseInt(inp.value)||999;
const sheet=inp.closest('tr').querySelector('.cfg-sel')?.value||'Other';
const row=allData.find(r=>r.acct_code===code); if(row) row.sort_order=ord;
try { await fetch('/api/costing/mapping',{method:'POST',headers:hdrs,body:JSON.stringify([{acct_code:code,sheet,sort_order:ord}])}); }
catch(err){showToast('Order save error: '+err.message,'error');}
}
window.autoSaveOrderChange = autoSaveOrderChange;
async function autoAssign() {
const entries=[];
document.querySelectorAll('.cfg-sel').forEach(sel=>{
const code=sel.dataset.code, row=allData.find(r=>r.acct_code===code);
if(!row) return;
if(row.group_mask===4){sel.value='Revenue';row.sheet='Revenue';}
const ord=parseInt(sel.closest('tr').querySelector('.cfg-ord')?.value)||999;
entries.push({acct_code:code,sheet:sel.value,sort_order:ord});
});
try {
const r=await fetch('/api/costing/mapping',{method:'POST',headers:hdrs,body:JSON.stringify(entries)});
const j=await r.json();
if(j.success) showToast('Auto-assigned and saved!','success');
else showToast('Save failed: '+j.message,'error');
} catch(err){showToast('Error: '+err.message,'error');}
}
window.autoAssign = autoAssign;
// ── Excel Export ──────────────────────────────────────────────────────────────
document.getElementById('btnExport').addEventListener('click', () => {
if (!allData.length) { showToast('Load data first','error'); return; }
try { exportExcel(); } catch(e) { showToast('Download failed: '+e.message,'error'); }
});
function exportExcel() {
const wb = XLSX.utils.book_new();
const HDR = ['Head','Account Code','Account Name','Period Debit','Period Credit','Provision','Net','Notes'];
SHEETS.forEach(def => {
const rows = allData.filter(r=>r.sheet===def.key).sort((a,b)=>(a.sort_order-b.sort_order)||a.acct_code.localeCompare(b.acct_code));
if (!rows.length) return;
const aoa=[HDR]; let tPD=0,tPC=0,tProv=0,tNet=0;
const hasHeads = rows.some(r=>r.head&&r.head.trim());
let curHead = null;
let hPD=0,hPC=0,hProv=0,hNet=0;
rows.forEach((r,i)=>{
const net=finalVal(r,def.key), prov=r.provision!=null?Number(r.provision):0;
const h=(r.head||'').trim();
if(hasHeads && h !== curHead){
if(curHead!==null) aoa.push(['Sub-total — '+curHead,'','',hPD,hPC,hProv,hNet,'']);
if(h) aoa.push([h,'','','','','','','']);
curHead=h; hPD=0;hPC=0;hProv=0;hNet=0;
}
aoa.push([h,r.acct_code,r.acct_name,r.period_debit,r.period_credit,prov,net,r.note||'']);
tPD+=r.period_debit;tPC+=r.period_credit;tProv+=prov;tNet+=net;
hPD+=r.period_debit;hPC+=r.period_credit;hProv+=prov;hNet+=net;
});
if(hasHeads && curHead!==null) aoa.push(['Sub-total — '+curHead,'','',hPD,hPC,hProv,hNet,'']);
aoa.push(['TOTAL','','',tPD,tPC,tProv,tNet,'']);
const ws=XLSX.utils.aoa_to_sheet(aoa);
ws['!cols']=[{wch:20},{wch:16},{wch:40},{wch:16},{wch:16},{wch:14},{wch:16},{wch:30}];
XLSX.utils.book_append_sheet(wb,ws,xlsName(def.label));
});
const tot={}; SHEETS.forEach(s=>{tot[s.key]=0;});
allData.forEach(r=>{const s=r.sheet||'Other';if(tot[s]!==undefined)tot[s]+=finalVal(r,s);});
const matCost=openingStock+tot['Purchase']-closingStock;
const xlsConvChange=(stockDetail.open.conv||0)-(stockDetail.close.conv||0);
const gp=tot['Revenue']-(matCost+xlsConvChange);
const opE=(tot['Employee']||0)+(tot['Factory']||0)+(tot['Admin']||0)+(tot['SND']||0)+(tot['Finance']||0);
const oiT=-(tot['OtherIncome']||0);
const netOps=gp-opE;
const netP=netOps+oiT;
const pctR=v=>tot['Revenue']?+(Math.abs(v)/Math.abs(tot['Revenue'])*100).toFixed(1):0;
// Build head-level rows per sheet for Excel
function xlsHeadRows(sheetKey, indent, negate) {
const rows = allData.filter(r=>r.sheet===sheetKey);
const headMap={}, headOrder=[];
rows.forEach(r=>{
const h=(r.head||'').trim()||r.acct_name;
if(!headMap[h]){headMap[h]={total:0,minOrd:r.sort_order};headOrder.push(h);}
headMap[h].total+=negate?-finalVal(r,sheetKey):finalVal(r,sheetKey);
headMap[h].minOrd=Math.min(headMap[h].minOrd,r.sort_order);
});
headOrder.sort((a,b)=>headMap[a].minOrd-headMap[b].minOrd);
return headOrder.map(h=>[indent+h,'',headMap[h].total,pctR(headMap[h].total)+'%']);
}
const sum=[
['Particulars','','Total','% of Revenue'],[],
['Revenue from Operations (Inc Scrap)','',tot['Revenue'],pctR(tot['Revenue'])+'%'],[],
['Opening Stock','',openingStock,pctR(openingStock)+'%'],
['Purchases','',tot['Purchase'],pctR(tot['Purchase'])+'%'],
['Closing Stock','',closingStock,pctR(closingStock)+'%'],
['Consumption of Material','',matCost,pctR(matCost)+'%'],
['Change in Conversion Cost','',(stockDetail.open.conv||0)-(stockDetail.close.conv||0),pctR((stockDetail.open.conv||0)-(stockDetail.close.conv||0))+'%'],
['Total Consumption','',matCost+((stockDetail.open.conv||0)-(stockDetail.close.conv||0)),pctR(matCost+((stockDetail.open.conv||0)-(stockDetail.close.conv||0)))+'%'],[],
['Gross Profit','',gp,pctR(gp)+'%'],[],
['Employee Benefit Expenses','','',''],
...xlsHeadRows('Employee',' ',false),
['Total Employee Benefit Expenses','',tot['Employee'],pctR(tot['Employee'])+'%'],[],
['Factory / Utility Expenses','','',''],
...xlsHeadRows('Factory',' ',false),
['Total Factory Expenses','',tot['Factory'],pctR(tot['Factory'])+'%'],[],
['Administration Expenses','','',''],
...xlsHeadRows('Admin',' ',false),
['Total Administration Expenses','',tot['Admin'],pctR(tot['Admin'])+'%'],[],
['Selling & Distribution Expenses','','',''],
...xlsHeadRows('SND',' ',false),
['Total Selling & Distribution Expenses','',tot['SND'],pctR(tot['SND'])+'%'],[],
['Finance Cost','',tot['Finance'],pctR(tot['Finance'])+'%'],[],
['Total Expenses','',opE,pctR(opE)+'%'],[],
['Net Profit from Operations','',netOps,pctR(netOps)+'%'],
oiT!==0?['Other Income','',oiT,pctR(oiT)+'%']:null,
[netP>=0?'Net Profit before Tax':'Net Loss before Tax','',Math.abs(netP),pctR(netP)+'%'],
].filter(Boolean);
const ws2=XLSX.utils.aoa_to_sheet(sum);
ws2['!cols']=[{wch:40},{wch:8},{wch:18},{wch:14}];
XLSX.utils.book_append_sheet(wb,ws2,'PL Summary');
XLSX.writeFile(wb,`PL_${fromDate||'all'}_${toDate||''}.xlsx`);
showToast('Excel downloaded','success');
}
function xlsName(s){return s.replace(/:/g,'').replace(/\\/g,'').replace(/\//g,'').replace(/\?/g,'').replace(/\*/g,'').replace(/\[/g,'').replace(/\]/g,'').replace(/&/g,'and').trim().substring(0,31)||'Sheet';}
// ── Helpers ───────────────────────────────────────────────────────────────────
function raw(v){
const n=Number(v)||0;
return (n<0?'<span style="color:#dc2626">':'<span style="color:#047e7e">') +
'&#8377; '+Math.abs(n).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2})+'</span>';
}
function fmtAmt(v){const n=Number(v)||0;return '&#8377; '+Math.abs(n).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});}
function amtCls(v){if(!v||v===0)return 'amt-zero';return v>0?'amt-pos':'amt-neg';}
function fmtDateDisplay(d){if(!d)return '';const[y,m,day]=d.split('-');const mn=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];return `${day} ${mn[parseInt(m)-1]} ${y}`;}
function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function showToast(msg,type='info'){const t=document.getElementById('toast');t.textContent=msg;t.className='toast '+type+' show';setTimeout(()=>t.classList.remove('show'),3500);}
function printSection(id,title){
const el=document.getElementById(id);
if(!el||!el.querySelector('table')){showToast('No data to print','error');return;}
const w=window.open('','_blank','width=900,height=750');
w.document.write(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title}</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
<style>
@page{size:A4 landscape;margin:5mm}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:auto}
body{font-family:'Space Grotesk',sans-serif;font-size:7.5px;color:#1a1a2e;padding:2px 4px}
h2{font-size:9px;font-weight:700;margin-bottom:4px;color:#004d40;border-bottom:1px solid #047e7e;padding-bottom:2px}
div[style*="overflow"]{overflow:visible!important;max-height:none!important}
table{border-collapse:collapse;width:100%;table-layout:fixed}
th,td{font-size:7.5px!important;padding:2px 5px!important;line-height:1.25!important;position:static!important;white-space:nowrap}
col:first-child,th:first-child,td:first-child{width:55%}
thead th{background:#047e7e!important;color:#fff!important;font-weight:700;
-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr[style*="background:#e0f4f4"] td{background:#e0f4f4!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr[style*="background:#d1fae5"] td,tr[style*="background:#fee2e2"] td{-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr[style*="background:#004d40"] td,tr[style*="background:#7f1d1d"] td{-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr[style*="background:#f0fdf4"] td{background:#f0fdf4!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr[style*="background:#f8fafb"] td{background:#f8fafb!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr[style*="height:10px"]{display:none!important}
tr.blank{display:none!important}
.neg{color:#dc2626!important}
div[style*="font-size:10px;color:#9ca3af"]{display:none}
</style><link rel="stylesheet" href="/theme.css?v=3"/>
</head><body>
<h2>${title}</h2>
${el.innerHTML}
<script>window.onload=()=>{window.print();}<\/script>
</body></html>`);
w.document.close();
}
window.printSection=printSection;
function printPLSummary(){
if(!allData.length){showToast('Load data first','error');return;}
// Re-compute same values as renderSummary (fresh, no inline-style conflicts)
const tot={};
SHEETS.forEach(s=>{tot[s.key]=0;});
allData.forEach(r=>{const s=r.sheet||'Other';if(tot[s]!==undefined)tot[s]+=finalVal(r,s);});
const rev = tot['Revenue'] || 0;
const purchT = tot['Purchase'] || 0;
const matCost = openingStock + purchT - closingStock;
const openConv = stockDetail.open.conv || 0;
const closeConv = stockDetail.close.conv || 0;
const convChange = openConv - closeConv;
const totalCons = matCost + convChange;
const grossP = rev - totalCons;
const empT = tot['Employee'] || 0;
const factT = tot['Factory'] || 0;
const adminT = tot['Admin'] || 0;
const sndT = tot['SND'] || 0;
const finT = tot['Finance'] || 0;
const oiT = -(tot['OtherIncome'] || 0);
const totalExp = empT + factT + adminT + sndT + finT;
const netOps = grossP - totalExp;
const netTax = netOps + oiT;
const pct = v => rev!==0?(Math.abs(v)/Math.abs(rev)*100).toFixed(1)+'%':'—';
const n = v => Math.abs(v||0).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
const neg = v => v<0?' neg':'';
function getHeadTotals(sheetKey,negate){
const rows=allData.filter(r=>r.sheet===sheetKey);
const headMap={},headOrder=[];
rows.forEach(r=>{
const h=(r.head||'').trim()||r.acct_name;
if(!headMap[h]){headMap[h]={total:0,minOrd:r.sort_order};headOrder.push(h);}
const v=finalVal(r,sheetKey);
headMap[h].total+=negate?-v:v;
headMap[h].minOrd=Math.min(headMap[h].minOrd,r.sort_order);
});
headOrder.sort((a,b)=>headMap[a].minOrd-headMap[b].minOrd);
return headOrder.map(h=>({head:h,total:headMap[h].total}));
}
const TR = (cls,lbl,val) => `<tr class="${cls}${neg(val)}"><td class="lab">${esc(lbl)}</td><td class="pct">${pct(val)}</td><td class="amt">${n(val)}</td></tr>`;
const secHdr = lbl => `<tr class="sec"><td class="lab" colspan="3">${esc(lbl)}</td></tr>`;
const subR = (lbl,val) => `<tr class="sub${neg(val)}"><td class="lab ind">${esc(lbl)}</td><td class="pct">${pct(val)}</td><td class="amt">${n(val)}</td></tr>`;
const totR = (lbl,val) => TR('tot',lbl,val);
const buildSec=(sheetKey,secLbl,totLbl,negate=false)=>{
if(!allData.some(r=>r.sheet===sheetKey)) return '';
const heads=getHeadTotals(sheetKey,negate);
let h=secHdr(secLbl);
heads.forEach(({head,total})=>{h+=subR(head,total);});
h+=totR(totLbl,negate?-(tot[sheetKey]||0):(tot[sheetKey]||0));
return h;
};
const buildEmpSec=()=>{
const rows=allData.filter(r=>r.sheet==='Employee');
if(!rows.length) return '';
const headMap={},headOrder=[];
rows.forEach(r=>{
const h=(r.head||'').trim()||r.acct_name;
if(!headMap[h]){headMap[h]={accts:[],total:0,minOrd:r.sort_order};headOrder.push(h);}
headMap[h].accts.push(r);
headMap[h].total+=finalVal(r,'Employee');
headMap[h].minOrd=Math.min(headMap[h].minOrd,r.sort_order);
});
headOrder.sort((a,b)=>headMap[a].minOrd-headMap[b].minOrd);
let h=secHdr('Employee Benefit Expenses');
headOrder.forEach(hd=>{
if(hd==='Direct Expenses'){
headMap[hd].accts
.sort((a,b)=>(a.sort_order-b.sort_order)||a.acct_name.localeCompare(b.acct_name))
.forEach(r=>{h+=subR(r.acct_name,finalVal(r,'Employee'));});
} else {
h+=subR(hd,headMap[hd].total);
}
});
h+=totR('Total Employee Benefit Expenses',tot['Employee']||0);
return h;
};
const tbody=[
TR('rev','Revenue from Operations',rev),
subR('Opening Stock',openingStock),
subR('Purchases',purchT),
subR('Closing Stock',closingStock),
totR('Consumption of Material',matCost),
subR('Change in Conversion Cost',convChange),
totR('Total Consumption',totalCons),
TR('gp','Gross Profit',grossP),
buildEmpSec(),
buildSec('Factory','Factory / Utility Expenses','Total Factory Expenses'),
buildSec('Admin','Administration Expenses','Total Administration Expenses'),
buildSec('SND','Selling &amp; Distribution Expenses','Total S&amp;D Expenses'),
buildSec('Finance','Finance Cost','Total Finance Cost'),
totR('Total Expenses',totalExp),
TR('net',netOps>=0?'Net Profit from Operations':'Net Loss from Operations',netOps),
oiT!==0
? secHdr('Other Income')+getHeadTotals('OtherIncome',true).map(({head,total})=>subR(head,total)).join('')+TR('oi','Total Other Income',oiT)
: '',
TR('grand',netTax>=0?'Net Profit before Tax':'Net Loss before Tax',netTax),
].join('');
const title=`P&L Summary — ${fmtDateDisplay(fromDate)} to ${fmtDateDisplay(toDate)}`;
const w=window.open('','_blank','width=1100,height=800');
w.document.write(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title}</title>
<style>
@page{size:A4 portrait;margin:8mm}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Arial,Helvetica,sans-serif;font-size:8.5px;color:#1a1a2e;padding:2px}
h2{font-size:10px;font-weight:700;margin-bottom:5px;color:#004d40;padding-bottom:3px;border-bottom:1.5px solid #047e7e}
table{border-collapse:collapse;width:100%;table-layout:fixed}
thead tr{background:#047e7e;color:#fff;-webkit-print-color-adjust:exact;print-color-adjust:exact}
thead th{padding:3px 6px;font-weight:700;font-size:8.5px}
th.lab,td.lab{text-align:left;width:58%}
th.pct,td.pct{text-align:right;width:14%;color:#555;font-size:8px}
th.amt,td.amt{text-align:right;width:28%;font-variant-numeric:tabular-nums}
td{padding:2px 6px;border-bottom:1px solid #f0f2f4;font-size:8.5px;line-height:1.3}
thead th.pct{color:#e0f4f4}
tr.sec td{background:#f0f4f8;font-weight:700;font-size:8px;color:#374151;border-top:1px solid #d1dae3;padding:2.5px 6px;-webkit-print-color-adjust:exact;print-color-adjust:exact}
td.ind{padding-left:18px}
tr.sub td.lab{color:#047e7e}
tr.sub.neg td.lab{color:#dc2626}
tr.sub.neg td.amt{color:#dc2626}
tr.rev td,tr.tot td{background:#e0f4f4;font-weight:700;border-top:1px solid #80cbc4;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr.gp td{background:#bbf7d0;font-weight:800;border-top:2px solid #4ade80;font-size:9px;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr.gp.neg td{background:#fee2e2;border-top-color:#fca5a5}
tr.net td{background:#d1fae5;font-weight:800;border-top:2px solid #6ee7b7;font-size:9px;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr.net.neg td{background:#fee2e2;border-top-color:#fca5a5}
tr.oi td{background:#f0fdf4;font-weight:700;color:#166534;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr.grand td{background:#004d40;color:#fff;font-weight:800;font-size:9.5px;-webkit-print-color-adjust:exact;print-color-adjust:exact}
tr.grand.neg td{background:#7f1d1d}
</style><link rel="stylesheet" href="/theme.css?v=3"/>
</head><body>
<h2>${title}</h2>
<table>
<thead><tr>
<th class="lab">Particulars</th>
<th class="pct">% of Revenue</th>
<th class="amt">${esc(fmtDateDisplay(fromDate))} &mdash; ${esc(fmtDateDisplay(toDate))}</th>
</tr></thead>
<tbody>${tbody}</tbody>
</table>
<script>window.onload=()=>{window.print();}<\/script>
</body></html>`);
w.document.close();
}
window.printPLSummary=printPLSummary;
// Load snapshot list on page init
loadSnapshotList();
// ────────────────────────────────────────────────────────────────────────────
// ── Mode toggle: Monthly Accounts ↔ Trial Balance ───────────────────────────
// ────────────────────────────────────────────────────────────────────────────
let _tbData = [];
let _tbFiltered = [];
function setMode(mode) {
const isMA = mode === 'monthly';
document.getElementById('maCard').style.display = isMA ? '' : 'none';
document.getElementById('tbCard').style.display = isMA ? 'none' : '';
document.getElementById('btnModeMA').style.background = isMA ? '#047e7e' : '#fff';
document.getElementById('btnModeMA').style.color = isMA ? '#fff' : '#047e7e';
document.getElementById('btnModeTB').style.background = isMA ? '#fff' : '#047e7e';
document.getElementById('btnModeTB').style.color = isMA ? '#047e7e' : '#fff';
document.getElementById('btnPrint').style.display = isMA ? '' : 'none';
if (!isMA) {
const f = document.getElementById('dtFrom').value;
const t = document.getElementById('dtTo').value;
if (f) document.getElementById('tbFrom').value = f;
if (t) document.getElementById('tbTo').value = t;
}
}
function setTbStatus(msg, state) {
document.getElementById('tbStatusMsg').textContent = msg;
document.getElementById('tbStatusDot').className = 'status-dot' + (state ? ' ' + state : '');
}
async function loadTrialBalance() {
const from = document.getElementById('tbFrom').value;
const to = document.getElementById('tbTo').value;
if (!from || !to) { showToast('Please select a date range', 'error'); return; }
document.getElementById('tbWrap').innerHTML = '<div class="empty">Loading…</div>';
setTbStatus('Loading trial balance…', 'loading');
document.getElementById('btnTbExport').disabled = true;
try {
const r = await fetch(`/api/costing/trial-balance?from=${from}&to=${to}`, { headers: hdrs }).then(r => r.json());
if (!r.success) throw new Error(r.message);
_tbData = r.data;
_tbFiltered = _tbData;
if (r.opening_stock != null && r.opening_stock !== 0) document.getElementById('tbStockOpen').value = r.opening_stock;
if (r.closing_stock != null && r.closing_stock !== 0) document.getElementById('tbStockClose').value = r.closing_stock;
const provCount = _tbData.filter(a => a.provision != null && a.provision !== 0).length;
if (provCount > 0) document.getElementById('tbProvStatus').textContent = provCount + ' provisions loaded from Monthly Accounts';
renderTb(_tbFiltered);
document.getElementById('btnTbExport').disabled = false;
document.getElementById('btnTbSnapshot').disabled = false;
setTbStatus('Loaded ' + _tbData.length + ' accounts (' + from + ' to ' + to + ')', 'ok');
} catch(e) {
document.getElementById('tbWrap').innerHTML = '<div class="empty">' + e.message + '</div>';
setTbStatus(e.message, 'err');
showToast(e.message, 'error');
}
}
async function loadTbProvisions() {
const from = document.getElementById('tbFrom').value;
const to = document.getElementById('tbTo').value;
if (!from || !to) { showToast('Select a date range first', 'error'); return; }
if (!_tbData.length) { showToast('Load Trial Balance first', 'error'); return; }
try {
const r = await fetch(`/api/costing/trial-balance?from=${from}&to=${to}`, { headers: hdrs }).then(r => r.json());
if (!r.success) throw new Error(r.message);
const provMap = {};
r.data.forEach(d => { if (d.provision != null) provMap[d.acct_code] = { provision: d.provision, note: d.note }; });
_tbData = _tbData.map(a => Object.assign({}, a, provMap[a.acct_code] || {}));
if (r.opening_stock != null) document.getElementById('tbStockOpen').value = r.opening_stock || '';
if (r.closing_stock != null) document.getElementById('tbStockClose').value = r.closing_stock || '';
const q = document.getElementById('tbSearch').value.trim().toLowerCase();
_tbFiltered = q ? _tbData.filter(a => a.acct_code.toLowerCase().includes(q) || a.acct_name.toLowerCase().includes(q)) : _tbData;
const count = _tbData.filter(a => a.provision != null && a.provision !== 0).length;
document.getElementById('tbProvStatus').textContent = count + ' provisions loaded';
renderTb(_tbFiltered);
showToast('Provisions loaded (' + count + ' accounts)', 'success');
} catch(e) { showToast(e.message, 'error'); }
}
async function loadTbStock() {
const from = document.getElementById('tbFrom').value;
const to = document.getElementById('tbTo').value;
if (!from || !to) { showToast('Select a date range first', 'error'); return; }
try {
const r = await fetch(`/api/costing/stock?from=${from}&to=${to}`, { headers: hdrs }).then(r => r.json());
if (!r.success) throw new Error(r.message);
document.getElementById('tbStockOpen').value = r.opening_stock || '';
document.getElementById('tbStockClose').value = r.closing_stock || '';
renderTb(_tbFiltered);
showToast('Stock values loaded from Monthly Accounts', 'success');
} catch(e) { showToast(e.message, 'error'); }
}
function filterTb() {
const q = document.getElementById('tbSearch').value.trim().toLowerCase();
_tbFiltered = q
? _tbData.filter(r => r.acct_code.toLowerCase().includes(q) || r.acct_name.toLowerCase().includes(q) || (r.group_label||'').toLowerCase().includes(q))
: _tbData;
renderTb(_tbFiltered);
}
function renderTb(rows) {
const stockOpen = parseFloat(document.getElementById('tbStockOpen').value) || 0;
const stockClose = parseFloat(document.getElementById('tbStockClose').value) || 0;
const fmt = v => {
if (v == null || v === '') return '<span style="color:#9ca3af">—</span>';
const n = parseFloat(v); if (!n) return '<span style="color:#9ca3af">0.00</span>';
const s = Math.abs(n).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
return n < 0 ? '<span class="amt-neg">(' + s + ')</span>' : '<span class="amt-pos">' + s + '</span>';
};
const fmtProv = v => {
if (v == null) return '<span style="color:#9ca3af">—</span>';
const n = parseFloat(v); if (!n) return '<span style="color:#9ca3af">—</span>';
const s = Math.abs(n).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
return n < 0 ? '<span style="color:#dc2626;font-weight:600">(' + s + ')</span>'
: '<span style="color:#7c3aed;font-weight:600">' + s + '</span>';
};
// Closing Balance = Opening + Debit − Credit
const dispCB = r => r.opening_balance + r.period_debit - r.period_credit;
// Header (parent/title) accounts: trimming trailing zeros leaves ≤ 3 significant chars
// e.g. 2140000000 → '214' (len 3) = header; 2140040001 → no trailing zeros (len 10) = leaf
const trimCode = code => String(code).replace(/0+$/, '');
const isHdr = code => trimCode(code).length <= 3;
const pre4 = code => String(code).slice(0, 4);
// Build parent lookup: '2140' → parent row
const hdrByPre4 = {};
rows.forEach(r => { if (isHdr(r.acct_code)) hdrByPre4[pre4(r.acct_code)] = r; });
const hasProv = rows.some(r => r.provision != null && r.provision !== 0);
const MASK_ORDER = [1,2,3,4,5];
const byMask = {};
rows.forEach(r => {
if (!isHdr(r.acct_code)) (byMask[r.group_mask] = byMask[r.group_mask]||[]).push(r);
});
const masks = MASK_ORDER.filter(m => byMask[m]);
Object.keys(byMask).map(Number).filter(m => !MASK_ORDER.includes(m)).forEach(m => masks.push(m));
const totalCols = hasProv ? 7 : 6;
const leafRow = (r, indent) => {
const pad = indent ? 'padding-left:28px' : '';
return '<tr>'
+ '<td style="' + pad + '"><span class="code">' + r.acct_code + '</span></td>'
+ '<td style="' + pad + '">' + r.acct_name + (r.note ? '<span style="font-size:10px;color:#9ca3af;margin-left:6px">' + r.note + '</span>' : '') + '</td>'
+ '<td class="num">' + fmt(r.opening_balance) + '</td>'
+ '<td class="num">' + fmt(r.period_debit) + '</td>'
+ '<td class="num">' + fmt(r.period_credit) + '</td>'
+ '<td class="num">' + fmt(dispCB(r)) + '</td>'
+ (hasProv ? '<td class="num">' + fmtProv(r.provision) + '</td>' : '')
+ '</tr>';
};
let html = '<table class="pl-tbl" id="tbTable"><thead><tr>'
+ '<th style="min-width:110px">Acct Code</th>'
+ '<th style="min-width:260px">Account Name</th>'
+ '<th class="num" style="min-width:140px">Opening Balance</th>'
+ '<th class="num" style="min-width:130px">Period Debit</th>'
+ '<th class="num" style="min-width:130px">Period Credit</th>'
+ '<th class="num" style="min-width:130px">Closing Balance</th>'
+ (hasProv ? '<th class="num" style="min-width:120px;color:#7c3aed">Provision</th>' : '')
+ '</tr></thead><tbody>';
let grandOB=0, grandDr=0, grandCr=0, grandCBd=0, grandProv=0;
masks.forEach(mask => {
const leaves = byMask[mask];
const label = leaves[0]?.group_label || 'Group ' + mask;
let gOB=0, gDr=0, gCr=0, gCBd=0, gProv=0;
html += '<tr class="no-hover" style="background:#f0f7ff"><td colspan="' + totalCols
+ '" style="padding:8px 12px;font-size:11px;font-weight:800;color:#0369a1;text-transform:uppercase;letter-spacing:.6px;border-top:2px solid #bae6fd">' + label + '</td></tr>';
const grouped = {};
const ungrouped = [];
leaves.forEach(r => {
const p = pre4(r.acct_code);
if (hdrByPre4[p]) (grouped[p] = grouped[p]||[]).push(r);
else ungrouped.push(r);
});
Object.keys(grouped).sort().forEach(p => {
const parent = hdrByPre4[p];
const children = grouped[p];
let pOB=0, pDr=0, pCr=0, pCBd=0, pProv=0;
children.forEach(r => {
const prov = r.provision != null ? parseFloat(r.provision) : 0;
pOB += r.opening_balance; pDr += r.period_debit; pCr += r.period_credit;
pCBd += dispCB(r); pProv += prov;
gOB += r.opening_balance; gDr += r.period_debit; gCr += r.period_credit;
gCBd += dispCB(r); gProv += prov;
});
html += '<tr class="no-hover" style="background:#fff8e6;border-top:1.5px solid #f59e0b">'
+ '<td><span class="code" style="color:#92400e;font-weight:700">' + parent.acct_code + '</span></td>'
+ '<td style="font-weight:700;color:#92400e">' + parent.acct_name + '</td>'
+ '<td class="num">' + fmt(pOB) + '</td>'
+ '<td class="num">' + fmt(pDr) + '</td>'
+ '<td class="num">' + fmt(pCr) + '</td>'
+ '<td class="num">' + fmt(pCBd) + '</td>'
+ (hasProv ? '<td class="num">' + (pProv ? fmtProv(pProv) : '<span style="color:#9ca3af">—</span>') + '</td>' : '')
+ '</tr>';
children.forEach(r => { html += leafRow(r, true); });
});
ungrouped.forEach(r => {
const prov = r.provision != null ? parseFloat(r.provision) : 0;
gOB += r.opening_balance; gDr += r.period_debit; gCr += r.period_credit;
gCBd += dispCB(r); gProv += prov;
html += leafRow(r, false);
});
html += '<tr class="total-row no-hover"><td colspan="2">' + label + ' Sub-total</td>'
+ '<td class="num">' + fmt(gOB) + '</td><td class="num">' + fmt(gDr) + '</td>'
+ '<td class="num">' + fmt(gCr) + '</td><td class="num">' + fmt(gCBd) + '</td>'
+ (hasProv ? '<td class="num">' + fmtProv(gProv) + '</td>' : '')
+ '</tr>';
grandOB+=gOB; grandDr+=gDr; grandCr+=gCr; grandCBd+=gCBd; grandProv+=gProv;
});
if (stockOpen || stockClose) {
html += '<tr class="no-hover" style="background:#fffbeb"><td colspan="' + totalCols + '" style="padding:8px 12px;font-size:11px;font-weight:800;color:#92400e;text-transform:uppercase;letter-spacing:.6px;border-top:2px dashed #f59e0b">Stock Adjustments</td></tr>';
if (stockOpen) html += '<tr><td><span class="code">—</span></td><td>Opening Stock</td><td class="num">' + fmt(stockOpen) + '</td><td class="num">—</td><td class="num">—</td><td class="num">' + fmt(stockOpen) + '</td>' + (hasProv ? '<td class="num">—</td>' : '') + '</tr>';
if (stockClose) html += '<tr><td><span class="code">—</span></td><td>Closing Stock</td><td class="num">' + fmt(stockClose) + '</td><td class="num">—</td><td class="num">—</td><td class="num">' + fmt(-stockClose) + '</td>' + (hasProv ? '<td class="num">—</td>' : '') + '</tr>';
}
html += '<tr class="no-hover grand-total" style="background:#1a7f3c;color:#fff"><td colspan="2" style="font-weight:800;font-size:13px;padding:12px">GRAND TOTAL</td>'
+ '<td class="num" style="color:#fff;font-weight:800">' + fmt(grandOB) + '</td>'
+ '<td class="num" style="color:#fff;font-weight:800">' + fmt(grandDr) + '</td>'
+ '<td class="num" style="color:#fff;font-weight:800">' + fmt(grandCr) + '</td>'
+ '<td class="num" style="color:#fff;font-weight:800">' + fmt(grandCBd) + '</td>'
+ (hasProv ? '<td class="num" style="color:#fff;font-weight:800">' + fmtProv(grandProv) + '</td>' : '')
+ '</tr></tbody></table>';
document.getElementById('tbWrap').innerHTML = html;
document.getElementById('tbCount').textContent = rows.filter(r => !isHdr(r.acct_code)).length + ' accounts';
}
function exportTbXlsx() {
if (!_tbFiltered.length) return;
const from = document.getElementById('tbFrom').value;
const to = document.getElementById('tbTo').value;
const stockOpen = parseFloat(document.getElementById('tbStockOpen').value) || 0;
const stockClose = parseFloat(document.getElementById('tbStockClose').value) || 0;
const hasProv = _tbFiltered.some(r => r.provision != null && r.provision !== 0);
// Closing Balance = Opening + Debit − Credit
const xlCB = r => r.opening_balance + r.period_debit - r.period_credit;
const trimCode = code => String(code).replace(/0+$/, '');
const isHdr = code => trimCode(code).length <= 3;
const cols = ['Account Code','Account Name','Group','Opening Balance','Period Debit','Period Credit','Closing Balance'];
if (hasProv) cols.push('Provision');
const xlRows = [cols];
const MASK_ORDER=[1,2,3,4,5]; const byMask={};
_tbFiltered.forEach(r => { if(!isHdr(r.acct_code)) (byMask[r.group_mask]=byMask[r.group_mask]||[]).push(r); });
const masks = MASK_ORDER.filter(m=>byMask[m]);
Object.keys(byMask).map(Number).filter(m=>!MASK_ORDER.includes(m)).forEach(m=>masks.push(m));
let gOB=0,gDr=0,gCr=0,gCBd=0,gProv=0;
masks.forEach(mask => {
const group=byMask[mask], label=group[0]?.group_label||'Group '+mask;
xlRows.push([label]); let sOB=0,sDr=0,sCr=0,sCBd=0,sProv=0;
group.forEach(r => {
const prov=r.provision!=null?parseFloat(r.provision):0;
const cb=xlCB(r);
const row=[r.acct_code,r.acct_name,label,r.opening_balance,r.period_debit,r.period_credit,cb];
if(hasProv) row.push(prov||'');
xlRows.push(row);
sOB+=r.opening_balance;sDr+=r.period_debit;sCr+=r.period_credit;sCBd+=cb;sProv+=prov;
});
const sub=[label+' Sub-total','','',sOB,sDr,sCr,sCBd];
if(hasProv) sub.push(sProv);
xlRows.push(sub); xlRows.push([]);
gOB+=sOB;gDr+=sDr;gCr+=sCr;gCBd+=sCBd;gProv+=sProv;
});
if(stockOpen) xlRows.push(['Opening Stock','','',stockOpen,'','',stockOpen]);
if(stockClose) xlRows.push(['Closing Stock','','',stockClose,'','',-stockClose]);
const grand=['GRAND TOTAL','','',gOB,gDr,gCr,gCBd];
if(hasProv) grand.push(gProv);
xlRows.push(grand);
const ws=XLSX.utils.aoa_to_sheet(xlRows);
ws['!cols']=[14,36,14,18,16,16,18,14].map(w=>({wch:w}));
const wb=XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb,ws,'Trial Balance');
XLSX.writeFile(wb,'Trial_Balance_'+from+'_'+to+'.xlsx');
showToast('Excel exported successfully','success');
}
window.setMode = setMode;
window.loadTrialBalance = loadTrialBalance;
window.loadTbProvisions = loadTbProvisions;
window.loadTbStock = loadTbStock;
window.filterTb = filterTb;
window.renderTb = renderTb;
window.exportTbXlsx = exportTbXlsx;
// Init Trial Balance dates to current month
(function initTbDates(){
const now=new Date(), y=now.getFullYear(), m=String(now.getMonth()+1).padStart(2,'0');
document.getElementById('tbFrom').value = y+'-'+m+'-01';
const last=new Date(y,now.getMonth()+1,0);
document.getElementById('tbTo').value = y+'-'+m+'-'+String(last.getDate()).padStart(2,'0');
})();
})();
</script>
<!-- Snapshot name modal — outside all cards so it shows in both MA and TB modes -->
<div id="snapModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:9000;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:14px;padding:28px 32px;width:400px;box-shadow:0 8px 40px rgba(0,0,0,.2)">
<div style="font-size:15px;font-weight:700;margin-bottom:16px;color:#1a1a2e">Save Snapshot</div>
<div style="font-size:12px;color:#6b7280;margin-bottom:8px">Give this snapshot a name so you can identify it later.</div>
<input id="snapNameInput" type="text" placeholder="e.g. April 2026 Final" style="width:100%;height:38px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 12px;font-size:13px;font-family:inherit;outline:none;margin-bottom:16px">
<div style="display:flex;gap:10px;justify-content:flex-end">
<button class="btn btn-outline" onclick="closeSnapModal()">Cancel</button>
<button class="btn btn-primary" id="btnSnapConfirm" onclick="confirmSnapshot()">Save</button>
</div>
</div>
</div>
</body>
</html>