first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+654
View File
@@ -0,0 +1,654 @@
<!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>Balance Sheet — Finance</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>document.documentElement.setAttribute('data-theme',localStorage.getItem('portal_theme')||'obsidian')</script>
<link rel="stylesheet" href="/theme.css?v=3"/>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Space Grotesk',sans-serif;background:var(--bg,#f8fafc);color:var(--text,#0f172a);-webkit-font-smoothing:antialiased}
.page{padding:20px 24px 40px}
.hdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px}
.hdr-title{font-size:20px;font-weight:800;letter-spacing:-.4px;color:var(--text)}
.hdr-sub{font-size:12px;color:var(--text3);margin-top:3px}
.btn{padding:9px 18px;border-radius:9px;border:none;font-size:13px;font-weight:600;font-family:'Space Grotesk',sans-serif;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
.btn-primary{background:#047e7e;color:#fff}.btn-primary:hover{background:#035b5b}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-outline{background:var(--surface);color:#047e7e;border:1.5px solid #047e7e}.btn-outline:hover{background:#f0fafa}
.btn-sm{padding:6px 12px;font-size:11px}
/* ── Filter card ── */
.card{background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-bottom:16px}
.filter-bar{display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;padding:16px 20px}
.fg{display:flex;flex-direction:column;gap:5px}
.fg label{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text3)}
.f-input,.f-select{padding:8px 11px;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text);font-size:13px;font-family:'Space Grotesk',sans-serif;outline:none}
.f-input:focus,.f-select:focus{border-color:#047e7e}
/* ── Status ── */
.status-bar{display:flex;align-items:center;gap:10px;padding:10px 20px;background:var(--surface3,#f8fafc);border-top:1px solid var(--border);font-size:12px;color:var(--text3)}
.status-dot{width:8px;height:8px;border-radius:50%;background:#d1d5db;flex-shrink:0}
.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}}
/* ── Balance Sheet 4-column table ── */
.bs-tbl{width:100%;border-collapse:collapse;border:1px solid #b2dfdf;border-radius:12px;overflow:hidden;background:#fff;margin-bottom:16px;font-family:'Space Grotesk',sans-serif}
.bs-tbl thead th{background:#047e7e;color:#fff;padding:10px 16px;font-size:13px;font-weight:800;letter-spacing:.3px;text-align:left}
.bs-tbl thead th.rh{border-left:2px solid #035b5b}
.bs-tbl td{padding:6px 16px;font-size:12.5px;border-top:1px solid #f0f2f4;vertical-align:middle}
.bs-tbl td.rd{border-left:2px solid #b2dfdf}
/* Section header */
.bs-tbl td.sect{background:#e0f4f4;color:#004d40;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.6px;border-top:1px solid #b2dfdf}
/* Items */
.bs-tbl td.lbl{color:#374151;padding-left:26px}
.bs-tbl td.amt{text-align:right;font-weight:700;font-variant-numeric:tabular-nums;min-width:110px;white-space:nowrap}
.bs-tbl td.amt .pos{color:#047e7e}.bs-tbl td.amt .neg{color:#dc2626}.bs-tbl td.amt .zero{color:#9ca3af}
/* Hover */
.bs-tbl tr.item:hover td{background:#f0fafa}
/* Sub-total */
.bs-tbl tr.sub td{background:#e0f4f4;border-top:2px solid #80cbc4;font-weight:700;color:#004d40;font-size:13px}
.bs-tbl tr.sub td.lbl{padding-left:16px}
/* Grand total */
.bs-tbl tr.grand td{background:#047e7e;border-top:3px solid #035b5b;color:#fff!important;font-weight:800;font-size:13.5px;padding:11px 16px}
.bs-tbl tr.grand td.amt .pos,.bs-tbl tr.grand td.amt .neg,.bs-tbl tr.grand td.amt .zero{color:#fff!important}
/* ── Configure Panel ── */
.cfg-panel{background:var(--surface);border:1px solid var(--border);border-radius:14px;margin-bottom:16px;overflow:hidden}
.cfg-hdr{display:flex;align-items:center;gap:10px;padding:12px 18px;cursor:pointer;user-select:none;border-bottom:1px solid transparent}
.cfg-hdr:hover{background:var(--surface3,#f8fafc)}
.cfg-hdr.open{border-bottom-color:var(--border)}
.cfg-title{font-size:14px;font-weight:700;color:var(--text)}
.cfg-chev{margin-left:auto;font-size:14px;transition:transform .2s}
.cfg-hdr.open .cfg-chev{transform:rotate(180deg)}
.cfg-body{display:none;padding:16px 20px}
.cfg-body.open{display:block}
.cfg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px}
.cfg-item{background:var(--surface3,#f8fafc);border:1px solid var(--border);border-radius:10px;padding:14px}
.cfg-item-title{font-size:12px;font-weight:700;color:#047e7e;margin-bottom:10px;display:flex;align-items:center;gap:6px}
.cfg-item-src{font-size:10px;color:var(--text3);font-weight:400;background:var(--surface);padding:1px 7px;border-radius:5px;border:1px solid var(--border)}
.cfg-codes{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;min-height:28px}
.cfg-code-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:6px;font-size:11px;font-weight:600;background:#e0f4f4;color:#004d40;border:1px solid #80cbc4}
.cfg-code-chip button{background:none;border:none;cursor:pointer;font-size:13px;line-height:1;color:#047e7e;padding:0}
.cfg-code-chip button:hover{color:#dc2626}
.cfg-add{display:flex;gap:6px;margin-top:4px}
.cfg-add input{flex:1;padding:5px 8px;border:1px solid var(--border);border-radius:7px;font-size:12px;font-family:'Space Grotesk',sans-serif;outline:none;background:var(--surface)}
.cfg-add input:focus{border-color:#047e7e}
.cfg-fixed{margin-top:8px;display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text2)}
.cfg-fixed input{width:120px;padding:5px 8px;border:1px solid var(--border);border-radius:7px;font-size:12px;font-family:'Space Grotesk',sans-serif;outline:none;background:var(--surface)}
.cfg-fixed input:focus{border-color:#047e7e}
.cfg-save-btn{margin-top:10px;padding:5px 14px;border-radius:7px;font-size:11px;font-weight:700;font-family:'Space Grotesk',sans-serif;border:none;cursor:pointer;background:#047e7e;color:#fff}
.cfg-save-btn:hover{background:#035b5b}
/* ── Toast ── */
.toast-c{position:fixed;bottom:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px}
.toast{padding:11px 18px;border-radius:10px;font-size:13px;font-weight:600;color:#fff;box-shadow:0 4px 20px rgba(0,0,0,.15);animation:slideIn .3s ease}
@keyframes slideIn{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
.toast.ok{background:#10b981}.toast.err{background:#ef4444}.toast.info{background:#047e7e}
@media print{.cfg-panel,.hdr .btn,.filter-bar button{display:none!important} .bs-tbl{page-break-inside:avoid}}
</style>
</head>
<body>
<script src="/sidebar.js?v=25"></script>
<div class="page">
<div class="hdr">
<div>
<div class="hdr-title">Balance Sheet</div>
<div class="hdr-sub" id="bsSub">As per Companies Act (Schedule III) — Figures in Lakh (₹)</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<label style="font-size:12px;font-weight:600;color:var(--text2);display:flex;align-items:center;gap:6px">
<input type="checkbox" id="chkLakh" checked onchange="renderBS()"> Figures in Lakh
</label>
<button class="btn btn-outline" onclick="window.print()">Print</button>
<button class="btn btn-outline" id="btnExport" onclick="exportXlsx()" disabled>Excel</button>
<button class="btn btn-primary" id="btnLoad" onclick="loadBS()">Load Balance Sheet</button>
</div>
</div>
<!-- Filter -->
<div class="card">
<div class="filter-bar">
<div class="fg">
<label>Balance Sheet Date (As Of)</label>
<input type="date" id="asOf" class="f-input">
</div>
<div class="fg">
<label>P&amp;L From (For Net Profit)</label>
<input type="date" id="plFrom" class="f-input" onchange="autoMatchSnapshot()">
</div>
<div class="fg">
<label>P&amp;L To</label>
<input type="date" id="plTo" class="f-input" onchange="autoMatchSnapshot()">
</div>
<div class="fg" style="min-width:260px">
<label>Profit &amp; Loss — Select Snapshot</label>
<select id="snapSelect" class="f-select" style="min-width:260px" onchange="onSnapChange()">
<option value="">— Select snapshot —</option>
</select>
</div>
<div class="fg" style="min-width:200px">
<label>Net Profit (₹) <span id="npLabel" style="font-size:9px;color:var(--text3)">— select snapshot to auto-fill</span></label>
<input type="number" id="netProfitOverride" class="f-input" placeholder="auto-fills from snapshot"
style="min-width:200px" step="0.01"
title="Auto-fills from selected snapshot, or enter manually to override">
</div>
</div>
<div class="status-bar">
<div class="status-dot" id="statusDot"></div>
<span id="statusMsg">Select dates and click Load Balance Sheet</span>
</div>
</div>
<!-- Configure Panel (admin only) -->
<div class="cfg-panel" id="cfgPanel" style="display:none">
<div class="cfg-hdr" id="cfgHdr" onclick="toggleCfg()">
<span style="font-size:16px">⚙</span>
<span class="cfg-title">Configure Account Groups</span>
<span style="font-size:11px;color:var(--text3);font-weight:400">Map ledger accounts to each Balance Sheet line item</span>
<span class="cfg-chev">▼</span>
</div>
<div class="cfg-body" id="cfgBody">
<div class="cfg-grid" id="cfgGrid">
<div style="color:var(--text3);font-size:12px">Loading configuration…</div>
</div>
<div style="margin-top:14px;padding-top:14px;border-top:1px solid var(--border);display:flex;gap:8px">
<button class="btn btn-primary btn-sm" onclick="saveAllCfg()">Save All Changes</button>
<span style="font-size:11px;color:var(--text3);align-self:center">Changes take effect after next Load</span>
</div>
</div>
</div>
<!-- Balance Sheet Output -->
<div id="bsOut"></div>
</div>
<div class="toast-c" id="toastC"></div>
<script>
'use strict';
const tok = sessionStorage.getItem('portal_token') || '';
const hdrs = { 'Content-Type':'application/json', Authorization:'Bearer '+tok };
if (!tok) window.location.href = '/';
let bsData = null; // latest API response
let bsItems = []; // structure from server
let bsCfg = {}; // config from server
let cfgDirty = {}; // local edits
// ── Auth: show configure panel for admin ─────────────────────────────────────
(function initAuth() {
let user = null;
try { user = JSON.parse(sessionStorage.getItem('portal_user')); } catch(_) {}
if (user?.role === 'admin' || user?.role === 'sap_adder')
document.getElementById('cfgPanel').style.display = '';
})();
// ── Default dates ─────────────────────────────────────────────────────────────
(function initDates() {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth(); // 0-based
// Last day of previous month for BS date
const lastDay = new Date(y, m, 0);
document.getElementById('asOf').value = lastDay.toISOString().slice(0,10);
document.getElementById('plFrom').value = `${y}-${String(m).padStart(2,'0')}-01`;
document.getElementById('plTo').value = lastDay.toISOString().slice(0,10);
})();
// ── Helpers ───────────────────────────────────────────────────────────────────
function toast(msg, type='ok') {
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = msg;
document.getElementById('toastC').appendChild(el);
setTimeout(() => el.remove(), 3500);
}
function setStatus(msg, state='') {
document.getElementById('statusMsg').textContent = msg;
document.getElementById('statusDot').className = 'status-dot' + (state ? ' '+state : '');
}
function fmtAmt(v, inLakh) {
if (v == null) return '—';
const n = inLakh ? v / 100000 : v;
if (Math.abs(n) < 0.005) return '<span class="zero">—</span>';
const s = Math.abs(n).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
if (n < 0) return `<span class="neg">(${s})</span>`;
return `<span class="pos">${s}</span>`;
}
function fmtN(v, inLakh) {
if (v == null || Math.abs(inLakh ? v/100000 : v) < 0.005) return 0;
return inLakh ? v / 100000 : v;
}
let _snapMeta = {}; // id → {period_from, period_to}
async function autoMatchSnapshot() {
const plFrom = document.getElementById('plFrom').value;
const plTo = document.getElementById('plTo').value;
if (!plFrom || !plTo) return;
const sel = document.getElementById('snapSelect');
const matchId = Object.keys(_snapMeta).find(id => {
const m = _snapMeta[id];
return m.period_from === plFrom && m.period_to === plTo;
});
if (matchId && sel.value !== matchId) {
sel.value = matchId;
await onSnapChange();
}
}
window.autoMatchSnapshot = autoMatchSnapshot;
async function onSnapChange() {
const snapId = document.getElementById('snapSelect').value;
if (!snapId) {
document.getElementById('npLabel').textContent = '— select snapshot to auto-fill';
document.getElementById('netProfitOverride').value = '';
return;
}
const meta = _snapMeta[snapId];
if (meta) {
document.getElementById('plFrom').value = meta.period_from;
document.getElementById('plTo').value = meta.period_to;
}
// Fetch net profit from this snapshot and auto-fill the field
const npLabel = document.getElementById('npLabel');
npLabel.textContent = '— loading from snapshot…';
try {
const r = await fetch(`/api/balance-sheet/snapshot-profit?id=${snapId}`, { headers: hdrs }).then(r => r.json());
if (r.success && r.net_profit != null) {
document.getElementById('netProfitOverride').value = r.net_profit.toFixed(2);
npLabel.textContent = `— auto-filled from snapshot (₹${(r.net_profit/100000).toFixed(2)}L)`;
toast(`Net profit loaded: ₹${(r.net_profit/100000).toFixed(2)}L`, 'info');
} else {
npLabel.textContent = '— could not fetch from snapshot';
}
} catch(_) { npLabel.textContent = '— error loading snapshot'; }
}
window.onSnapChange = onSnapChange;
// ── Load snapshot list and auto-select the one matching the P&L period ───────
(async function loadSnapshots() {
try {
const r = await fetch('/api/balance-sheet/snapshots', { headers: hdrs }).then(r => r.json());
if (!r.success || !r.data.length) return;
const sel = document.getElementById('snapSelect');
r.data.forEach(s => {
_snapMeta[s.id] = { period_from: s.period_from, period_to: s.period_to };
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = `${s.snap_name || 'Snapshot'} (${s.period_from} → ${s.period_to})`;
sel.appendChild(opt);
});
// Auto-select snapshot matching current P&L period (most recent first)
const plFrom = document.getElementById('plFrom').value;
const plTo = document.getElementById('plTo').value;
const match = r.data.find(s => s.period_from === plFrom && s.period_to === plTo);
if (match) {
sel.value = String(match.id);
await onSnapChange();
}
} catch(_) {}
})();
async function loadBS() {
const asOf = document.getElementById('asOf').value;
const plFrom = document.getElementById('plFrom').value;
const plTo = document.getElementById('plTo').value;
if (!asOf || !plFrom || !plTo) { toast('Please fill all date fields', 'err'); return; }
document.getElementById('btnLoad').disabled = true;
document.getElementById('btnExport').disabled = true;
setStatus('Loading balance sheet…', 'loading');
document.getElementById('bsOut').innerHTML = '';
try {
const snapId = document.getElementById('snapSelect').value;
const manualProfit = document.getElementById('netProfitOverride').value.trim();
const snapParam = snapId ? `&snapshotId=${snapId}` : '';
const overrideParam = manualProfit !== '' ? `&netProfitOverride=${encodeURIComponent(manualProfit)}` : '';
const r = await fetch(`/api/balance-sheet/data?asOf=${asOf}&from=${plFrom}&to=${plTo}${snapParam}${overrideParam}`, { headers: hdrs }).then(r => r.json());
if (!r.success) throw new Error(r.message);
bsData = r;
// ── Force balance: set st_provisions so Assets = Liabilities ──────────
const aTotal = ['fixed_assets','non_current_invest','lt_loans_advances','other_non_current',
'current_invest','inventories','trade_receivables','cash_equivalents',
'st_loans_advances','other_current_assets']
.reduce((s, k) => s + Math.abs(bsData.values[k] || 0), 0);
const lTotalWithoutProv = ['share_capital','reserves_surplus','profit_loss',
'lt_borrowings','other_lt_liab','lt_provisions',
'st_borrowings','trade_payables','other_current_liab']
.reduce((s, k) => {
const v = bsData.values[k] || 0;
return s + (k === 'profit_loss' ? v : Math.abs(v));
}, 0);
bsData.values['st_provisions'] = aTotal - lTotalWithoutProv;
// ──────────────────────────────────────────────────────────────────────
renderBS();
document.getElementById('btnExport').disabled = false;
const snapLabel = snapId
? document.getElementById('snapSelect').options[document.getElementById('snapSelect').selectedIndex].text
: 'live P&L';
setStatus(`Balance Sheet as at ${asOf} · P&L: ${snapLabel}`, 'ok');
document.getElementById('bsSub').textContent = `As at ${asOf} · P&L: ${snapLabel} · Figures in ${document.getElementById('chkLakh').checked ? 'Lakh (₹)' : 'Rupees (₹)'}`;
} catch(e) {
setStatus(e.message, 'err');
toast(e.message, 'err');
} finally {
document.getElementById('btnLoad').disabled = false;
}
}
// ── Render Balance Sheet ──────────────────────────────────────────────────────
const STRUCTURE = {
L: [
{ key:'SF', label:"Shareholders' Funds", items:['share_capital','reserves_surplus','profit_loss'] },
{ key:'NCL', label:'Non-Current Liabilities', items:['lt_borrowings','other_lt_liab','lt_provisions'] },
{ key:'CL', label:'Current Liabilities', items:['st_borrowings','trade_payables','other_current_liab','st_provisions'] },
],
A: [
{ key:'NCA', label:'Non-Current Assets', items:['fixed_assets','non_current_invest','lt_loans_advances','other_non_current'] },
{ key:'CA', label:'Current Assets', items:['current_invest','inventories','trade_receivables','cash_equivalents','st_loans_advances','other_current_assets'] },
],
};
function renderBS() {
if (!bsData) return;
const inLakh = document.getElementById('chkLakh').checked;
const vals = bsData.values;
const itemMap = {};
bsItems.forEach(i => { itemMap[i.key] = i; });
// Build rows for one side — grand total returned separately
function buildRows(sideKey) {
const rows = [];
let total = 0;
STRUCTURE[sideKey].forEach(sub => {
rows.push({ t:'sect', label: sub.label });
let st = 0;
sub.items.forEach(key => {
// profit_loss shows actual sign; all other items show absolute value
const raw = key === 'profit_loss' ? (vals[key] || 0) : Math.abs(vals[key] || 0);
st += raw;
rows.push({ t:'item', label: (itemMap[key]?.label || key), val: raw });
});
total += st;
rows.push({ t:'sub', label: 'Total ' + sub.label, val: st });
});
return { rows, total };
}
const { rows: lRows, total: lTotal } = buildRows('L');
const { rows: aRows, total: aTotal } = buildRows('A');
const diff = aTotal - lTotal;
const maxLen = Math.max(lRows.length, aRows.length);
const emp = { t:'empty' };
while (lRows.length < maxLen) lRows.push(emp);
while (aRows.length < maxLen) aRows.push(emp);
function cells(row, right) {
const d = right ? ' rd' : '';
if (!row || row.t === 'empty') return `<td class="${d}"></td><td></td>`;
if (row.t === 'sect') return `<td class="sect${d}" colspan="2">${row.label}</td>`;
return `<td class="lbl${d}">${row.label}</td><td class="amt">${fmtAmt(row.val, inLakh)}</td>`;
}
let html = `<table class="bs-tbl">
<thead><tr>
<th colspan="2">I. Equity and Liabilities</th>
<th colspan="2" class="rh">II. Assets</th>
</tr></thead><tbody>`;
for (let i = 0; i < maxLen; i++) {
const l = lRows[i], r = aRows[i];
const isSub = l.t === 'sub' || r.t === 'sub';
html += `<tr class="${isSub ? 'sub' : (l.t === 'item' || r.t === 'item' ? 'item' : '')}">${cells(l,false)}${cells(r,true)}</tr>`;
}
// Grand total row
html += `<tr class="grand">
<td class="lbl">Total Equity &amp; Liabilities</td><td class="amt">${fmtAmt(lTotal,inLakh)}</td>
<td class="lbl rd">Total Assets</td><td class="amt">${fmtAmt(aTotal,inLakh)}</td>
</tr>`;
// Difference row
const diffColor = Math.abs(diff) < 0.005 ? '#10b981' : '#f59e0b';
const diffLabel = Math.abs(diff) < 0.005 ? 'Balanced ✓' : 'Difference (Assets − Liabilities)';
const diffVal = inLakh ? (diff / 100000).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2})
: diff.toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
html += `<tr class="no-hover" style="background:#fffbeb;border-top:2px dashed ${diffColor}">
<td colspan="3" style="padding:8px 16px;font-size:12px;font-weight:700;color:${diffColor}">${diffLabel}</td>
<td class="amt" style="font-weight:800;color:${diffColor}">${diff === 0 ? '—' : diffVal}</td>
</tr>`;
html += `</tbody></table>
<div style="font-size:11px;color:var(--text3);text-align:right;padding:4px 8px">
Net Profit: ₹${(bsData.meta.net_profit/100000).toFixed(2)}L &nbsp;·&nbsp;
Closing Stock: ₹${(bsData.meta.closing_stock/100000).toFixed(2)}L &nbsp;·&nbsp;
Total Provisions: ₹${(bsData.meta.total_provisions/100000).toFixed(2)}L
</div>`;
document.getElementById('bsOut').innerHTML = html;
}
// ── Load Config ───────────────────────────────────────────────────────────────
async function loadCfg() {
try {
const r = await fetch('/api/balance-sheet/config', { headers: hdrs }).then(r => r.json());
if (!r.success) return;
bsItems = r.items || [];
bsCfg = r.data || {};
renderCfgGrid();
} catch(_) {}
}
loadCfg();
function toggleCfg() {
const hdr = document.getElementById('cfgHdr');
const body = document.getElementById('cfgBody');
hdr.classList.toggle('open');
body.classList.toggle('open');
}
const SUB_LABELS = { SF:'Shareholders Funds', NCL:'Non-Current Liabilities', CL:'Current Liabilities', NCA:'Non-Current Assets', CA:'Current Assets' };
const SRC_LABELS = { fixed:'Fixed Value', ledger:'Ledger', pl_net:'P&L Net Profit', closing_stock:'Closing Stock (MA)', provisions:'MA Provisions + Ledger', reserves_formula:'Ledger + Opening Stock − P&L Opening' };
function renderCfgGrid() {
const itemMap = {};
bsItems.forEach(i => { itemMap[i.key] = i; });
const allItems = [...STRUCTURE.L, ...STRUCTURE.A].flatMap(sub =>
sub.items.map(k => ({ ...itemMap[k], subLabel: SUB_LABELS[itemMap[k]?.sub] || '' }))
).filter(Boolean);
document.getElementById('cfgGrid').innerHTML = allItems.map(item => {
const cfg = cfgDirty[item.key] || bsCfg[item.key] || {};
const codes = cfg.acct_codes || [];
const ocodes = cfg.opening_acct_codes || [];
const fv = cfg.fixed_value != null ? cfg.fixed_value : '';
const chipsHtml = codes.map(c =>
`<span class="cfg-code-chip">${c}<button onclick="removeCfgCode('${item.key}','${c}','main')">×</button></span>`
).join('');
const ochipsHtml = ocodes.map(c =>
`<span class="cfg-code-chip" style="background:#ede9fe;color:#5b21b6;border-color:#c4b5fd">${c}<button onclick="removeCfgCode('${item.key}','${c}','opening')">×</button></span>`
).join('');
const fixedHtml = item.src === 'fixed' ? `
<div class="cfg-fixed">
<label>Fixed Value (₹ Lakh):</label>
<input type="number" id="cfg-fv-${item.key}" value="${fv}" placeholder="e.g. 100"
onchange="setDirty('${item.key}',null,null,this.value)">
</div>` : '';
const showAccts = item.src === 'ledger' || item.src === 'provisions' || item.src === 'reserves_formula';
const showOpening = item.src === 'reserves_formula';
return `<div class="cfg-item">
<div class="cfg-item-title">
${item.label}
<span class="cfg-item-src">${SRC_LABELS[item.src]||item.src}</span>
<span style="font-size:10px;color:var(--text3);font-weight:400">${item.subLabel}</span>
</div>
${showAccts ? `
<div style="font-size:10px;font-weight:700;color:#047e7e;margin-bottom:4px">Main Account (2120020001)</div>
<div class="cfg-codes" id="chips-${item.key}">${chipsHtml||'<span style="font-size:11px;color:var(--text3)">None</span>'}</div>
<div class="cfg-add">
<input type="text" id="newacc-${item.key}" placeholder="e.g. 2120020001"
onkeydown="if(event.key==='Enter')addCfgCode('${item.key}','main')">
<button class="btn btn-outline btn-sm" onclick="addCfgCode('${item.key}','main')">+ Add</button>
</div>` : ''}
${showOpening ? `
<div style="font-size:10px;font-weight:700;color:#7c3aed;margin:10px 0 4px">Opening Balance Account (5100001001)</div>
<div style="font-size:10px;color:var(--text3);margin-bottom:6px">Total Opening = balance(this account, day before period) − Opening Stock</div>
<div class="cfg-codes" id="ochips-${item.key}">${ochipsHtml||'<span style="font-size:11px;color:var(--text3)">None</span>'}</div>
<div class="cfg-add">
<input type="text" id="onewacc-${item.key}" placeholder="e.g. 5100001001"
onkeydown="if(event.key==='Enter')addCfgCode('${item.key}','opening')">
<button class="btn btn-outline btn-sm" style="border-color:#7c3aed;color:#7c3aed" onclick="addCfgCode('${item.key}','opening')">+ Add</button>
</div>` : ''}
${fixedHtml}
<button class="cfg-save-btn" onclick="saveSingleCfg('${item.key}')">Save</button>
</div>`;
}).join('');
}
function setDirty(key, codes, ocodes, fv) {
if (!cfgDirty[key]) cfgDirty[key] = { ...(bsCfg[key] || {}) };
if (codes !== null) cfgDirty[key].acct_codes = codes;
if (ocodes !== null) cfgDirty[key].opening_acct_codes = ocodes;
if (fv !== null) cfgDirty[key].fixed_value = fv;
}
function addCfgCode(key, type) {
const inp = document.getElementById(type === 'opening' ? `onewacc-${key}` : `newacc-${key}`);
const code = (inp.value || '').trim();
if (!code) return;
if (type === 'opening') {
const cur = (cfgDirty[key]?.opening_acct_codes || bsCfg[key]?.opening_acct_codes || []).filter(Boolean);
if (cur.includes(code)) { toast('Code already added', 'err'); return; }
setDirty(key, null, [...cur, code], null);
} else {
const cur = (cfgDirty[key]?.acct_codes || bsCfg[key]?.acct_codes || []).filter(Boolean);
if (cur.includes(code)) { toast('Code already added', 'err'); return; }
setDirty(key, [...cur, code], null, null);
}
inp.value = '';
renderCfgGrid();
}
function removeCfgCode(key, code, type) {
if (type === 'opening') {
const cur = (cfgDirty[key]?.opening_acct_codes || bsCfg[key]?.opening_acct_codes || []);
setDirty(key, null, cur.filter(c => c !== code), null);
} else {
const cur = (cfgDirty[key]?.acct_codes || bsCfg[key]?.acct_codes || []);
setDirty(key, cur.filter(c => c !== code), null, null);
}
renderCfgGrid();
}
async function saveSingleCfg(key) {
const cfg = cfgDirty[key] || bsCfg[key] || {};
try {
const r = await fetch('/api/balance-sheet/config', {
method: 'POST', headers: hdrs,
body: JSON.stringify({
group_key: key,
acct_codes: cfg.acct_codes || [],
opening_acct_codes: cfg.opening_acct_codes || [],
fixed_value: cfg.fixed_value,
notes: cfg.notes,
}),
}).then(r => r.json());
if (!r.success) throw new Error(r.message);
bsCfg[key] = { ...cfg };
delete cfgDirty[key];
toast(`Saved: ${key}`, 'ok');
loadCfg();
} catch(e) { toast(e.message, 'err'); }
}
async function saveAllCfg() {
const keys = Object.keys(cfgDirty);
if (!keys.length) { toast('No unsaved changes', 'info'); return; }
for (const key of keys) await saveSingleCfg(key);
toast('All changes saved', 'ok');
}
window.toggleCfg = toggleCfg;
window.addCfgCode = addCfgCode;
window.removeCfgCode= removeCfgCode;
window.saveSingleCfg= saveSingleCfg;
window.saveAllCfg = saveAllCfg;
window.renderBS = renderBS;
// ── Excel Export ──────────────────────────────────────────────────────────────
function exportXlsx() {
if (!bsData) return;
const inLakh = document.getElementById('chkLakh').checked;
const asOf = document.getElementById('asOf').value;
const vals = bsData.values;
const itemMap = {};
bsItems.forEach(i => { itemMap[i.key] = i; });
const rows = [
['BALANCE SHEET'],
[`As at ${asOf}`],
[inLakh ? 'Figures in Lakh (₹)' : 'Figures in Rupees (₹)'],
[],
['I. EQUITY AND LIABILITIES','','','II. ASSETS',''],
['Line Item','Amount','','Line Item','Amount'],
];
const lItems = STRUCTURE.L.flatMap(s => [
[s.label,'','','',''],
...s.items.map(k => {
const n = fmtN(Math.abs(vals[k]||0), inLakh);
return [(itemMap[k]?.label||k), n];
}),
['Total '+s.label, s.items.reduce((t,k)=>t+Math.abs(vals[k]||0),0) / (inLakh?100000:1),'','',''],
]);
const aItems = STRUCTURE.A.flatMap(s => [
[s.label,'','','',''],
...s.items.map(k => {
const n = fmtN(Math.abs(vals[k]||0), inLakh);
return ['', '', '', (itemMap[k]?.label||k), n];
}),
['','','','Total '+s.label, s.items.reduce((t,k)=>t+Math.abs(vals[k]||0),0) / (inLakh?100000:1)],
]);
const maxLen = Math.max(lItems.length, aItems.length);
for (let i = 0; i < maxLen; i++) {
const l = lItems[i] || ['','','','',''];
const a = aItems[i] || ['','','','',''];
rows.push([l[0]||'', l[1]||'', '', a[3]||a[0]||'', a[4]||a[1]||'']);
}
rows.push([]);
rows.push(['Total Equity & Liabilities',
STRUCTURE.L.flatMap(s=>s.items).reduce((t,k)=>t+(vals[k]||0),0) / (inLakh?100000:1),
'',
'Total Assets',
STRUCTURE.A.flatMap(s=>s.items).reduce((t,k)=>t+(vals[k]||0),0) / (inLakh?100000:1),
]);
const ws = XLSX.utils.aoa_to_sheet(rows);
ws['!cols'] = [36,16,4,36,16].map(w=>({wch:w}));
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Balance Sheet');
XLSX.writeFile(wb, `Balance_Sheet_${asOf}.xlsx`);
toast('Excel exported', 'ok');
}
window.loadBS = loadBS;
window.exportXlsx = exportXlsx;
</script>
</body>
</html>