655 lines
32 KiB
HTML
655 lines
32 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>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&L From (For Net Profit)</label>
|
||
<input type="date" id="plFrom" class="f-input" onchange="autoMatchSnapshot()">
|
||
</div>
|
||
<div class="fg">
|
||
<label>P&L To</label>
|
||
<input type="date" id="plTo" class="f-input" onchange="autoMatchSnapshot()">
|
||
</div>
|
||
<div class="fg" style="min-width:260px">
|
||
<label>Profit & 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 & 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 ·
|
||
Closing Stock: ₹${(bsData.meta.closing_stock/100000).toFixed(2)}L ·
|
||
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>
|