2081 lines
109 KiB
HTML
2081 lines
109 KiB
HTML
<!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=31"></script>
|
||
|
||
<div class="pg">
|
||
<div class="hdr">
|
||
<h1 id="pageTitle"><span>Monthly Accounts</span> P&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&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&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&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&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&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 (₹)</th>
|
||
<th class="num">Period Credit (₹)</th>
|
||
<th class="num">Provision</th>
|
||
<th class="num">Net ₹ <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&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))} — ${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 & Distribution Expenses','Total Selling & 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 & 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"> </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>→ ${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 —
|
||
<span style="font-weight:400;color:#6b7280">${sorted.length} accounts • 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">') +
|
||
'₹ '+Math.abs(n).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2})+'</span>';
|
||
}
|
||
function fmtAmt(v){const n=Number(v)||0;return '₹ '+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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
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 & Distribution Expenses','Total S&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))} — ${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>
|