Files
sap-erp/public/balance-sheet.html
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

655 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>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=31"></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>