Files
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

915 lines
43 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>Cash Flow — 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}
.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:5px 11px;font-size:11px}
.btn-danger{background:#fef2f2;color:#dc2626;border:1.5px solid #fca5a5}.btn-danger:hover{background:#fee2e2}
.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-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}}
/* ── Statement of Cash Flow ── */
.stmt-wrap{overflow-x:auto;padding:0 20px 20px}
.stmt-tbl{width:100%;border-collapse:collapse;font-family:'Space Grotesk',sans-serif;min-width:560px}
.stmt-tbl thead th{padding:5px 10px;font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#004d40;border-bottom:2px solid #047e7e;background:#e0f4f4}
.stmt-tbl thead th.num{text-align:right}
.stmt-tbl td{padding:3px 10px;font-size:12px;border-top:1px solid #f0f2f4;vertical-align:middle;line-height:1.35}
.stmt-tbl td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;min-width:140px}
.stmt-tbl td.cr{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;min-width:90px;font-size:11.5px;color:#374151}
.stmt-tbl tr.s-sep td{border-top:2px solid #b2dfdf;padding-top:0;padding-bottom:0}
.stmt-tbl tr.s-total td{background:#e8f7f7;font-weight:800;border-top:2px solid #047e7e;border-bottom:1px solid #047e7e;color:#004d40}
.stmt-tbl tr.s-net td{background:#047e7e;color:#fff!important;font-weight:800;font-size:12.5px;border-top:2px solid #035b5b}
.stmt-tbl tr.s-cash-lbl td{background:#b2f0e8;font-weight:800;color:#004d40;font-size:12px;border-top:2px solid #80cbc4;padding-top:4px;padding-bottom:4px}
.stmt-tbl tr.s-cash-item td{background:#fff;font-size:12px}
.stmt-tbl tr.s-cash-item td:first-child{padding-left:22px;color:#374151}
.stmt-tbl tr.s-cash-tot td{background:#e0f4f4;font-weight:800;border-top:1.5px solid #80cbc4;color:#004d40}
.stmt-tbl tr.s-increase td{background:#047e7e;color:#fff!important;font-weight:800;font-size:12.5px;border-top:2px solid #035b5b}
.stmt-tbl tr.s-increase td span.amt-pos,.stmt-tbl tr.s-increase td span.amt-neg,.stmt-tbl tr.s-increase td span.amt-zero{color:#fff!important}
.stmt-tbl tr.s-net td span.amt-pos,.stmt-tbl tr.s-net td span.amt-neg,.stmt-tbl tr.s-net td span.amt-zero{color:#fff!important}
.stmt-tbl tr.s-closing td{background:#047e7e;color:#fff!important;font-weight:800;font-size:12.5px}
.amt-pos{color:#047e7e;font-weight:600}.amt-neg{color:#dc2626;font-weight:600}.amt-zero{color:#9ca3af}
.stmt-period{font-size:11px;font-weight:700;color:#047e7e;text-align:right;padding:2px 0}
.stmt-hdr-card{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-bottom:1px solid var(--border)}
/* ── Detailed Cash Flow Table ── */
.cf-tbl{width:100%;border-collapse:collapse;font-family:'Space Grotesk',sans-serif;background:#fff;border:1px solid #b2dfdf;border-radius:12px;overflow:hidden;margin-bottom:4px}
.cf-tbl thead th{background:#047e7e;color:#fff;padding:5px 10px;font-size:11px;font-weight:800;letter-spacing:.3px;text-align:left}
.cf-tbl thead th.num{text-align:right}
.cf-tbl td{padding:2px 10px;font-size:11.5px;border-top:1px solid #f0f2f4;vertical-align:middle;line-height:1.35}
.cf-tbl td.num{text-align:right;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap;min-width:120px}
.cf-tbl td.lbl{padding-left:20px;color:#374151}
.cf-tbl td.lbl.indent{padding-left:32px}
.cf-tbl tr.sect-hdr td{background:#e0f4f4;color:#004d40;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;border-top:2px solid #b2dfdf;padding:4px 10px}
.cf-tbl tr.sub td{background:#e0f4f4;border-top:2px solid #80cbc4;font-weight:700;color:#004d40;font-size:11.5px}
.cf-tbl tr.sub td.lbl{padding-left:10px}
.cf-tbl tr.grand td{background:#047e7e;border-top:2px solid #035b5b;color:#fff!important;font-weight:800;font-size:12.5px;padding:5px 10px}
.cf-tbl tr.item:hover td{background:#f0fafa}
.tag-util{display:inline-block;padding:2px 10px;border-radius:5px;font-size:10px;font-weight:800;letter-spacing:.5px;background:#dbeafe;color:#1d4ed8;text-transform:uppercase}
.tag-gen{display:inline-block;padding:2px 10px;border-radius:5px;font-size:10px;font-weight:800;letter-spacing:.5px;background:#dcfce7;color:#15803d;text-transform:uppercase}
.tag-nil{color:#9ca3af;font-size:11px;font-weight:600}
/* ── Collapsible detail section ── */
.detail-toggle{display:flex;align-items:center;gap:8px;padding:10px 20px;cursor:pointer;font-size:12px;font-weight:600;color:#047e7e;user-select:none;border-top:1px solid var(--border)}
.detail-toggle:hover{background:var(--surface3,#f8fafc)}
.detail-body{display:none;padding:0 20px 20px}
.detail-body.open{display:block}
/* ── Config 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:13px 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:18px 20px}
.cfg-body.open{display:block}
.add-form{background:var(--surface3,#f8fafc);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:18px}
.add-form-title{font-size:12px;font-weight:700;color:#047e7e;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px}
.form-row{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:12px}
.form-field{display:flex;flex-direction:column;gap:4px}
.form-field label{font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.5px}
.form-field input,.form-field select{padding:7px 10px;border:1px solid var(--border);border-radius:7px;font-size:12px;font-family:'Space Grotesk',sans-serif;outline:none;background:var(--surface);color:var(--text)}
.form-field input:focus,.form-field select:focus{border-color:#047e7e}
.acct-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;min-height:30px}
.acct-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}
.acct-chip button{background:none;border:none;cursor:pointer;font-size:13px;color:#047e7e;padding:0;line-height:1}.acct-chip button:hover{color:#dc2626}
.acct-search{display:flex;gap:6px;align-items:center}
.acct-search input{flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:7px;font-size:12px;font-family:'Space Grotesk',sans-serif;outline:none;background:var(--surface);color:var(--text)}
.acct-search input:focus{border-color:#047e7e}
.acct-dropdown{position:absolute;z-index:100;background:var(--surface);border:1px solid var(--border);border-radius:9px;box-shadow:0 8px 24px rgba(0,0,0,.12);max-height:220px;overflow-y:auto;min-width:340px}
.acct-dropdown-item{padding:8px 12px;font-size:12px;cursor:pointer;display:flex;gap:10px;align-items:center}
.acct-dropdown-item:hover{background:var(--surface3,#f8fafc)}
.acct-dropdown-item .code{font-weight:700;color:#047e7e;font-size:11px;white-space:nowrap}
.acct-dropdown-item .name{color:var(--text3)}
.items-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
@media(max-width:900px){.items-grid{grid-template-columns:1fr}}
.item-card{background:var(--surface3,#f8fafc);border:1px solid var(--border);border-radius:10px;padding:12px}
.item-card-top{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:8px}
.item-card-label{font-size:13px;font-weight:700;color:var(--text)}
.item-card-meta{font-size:10px;color:var(--text3);margin-top:2px}
.item-card-codes{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px;min-height:24px}
.item-card-code{font-size:10px;font-weight:600;background:#e0f4f4;color:#004d40;padding:2px 7px;border-radius:5px;border:1px solid #80cbc4}
.item-card-actions{display:flex;gap:6px}
.sect-tab{display:flex;gap:0;border:1px solid var(--border);border-radius:9px;overflow:hidden;margin-bottom:14px}
.sect-tab button{flex:1;padding:7px 0;border:none;font-size:12px;font-weight:600;font-family:'Space Grotesk',sans-serif;cursor:pointer;background:var(--surface);color:var(--text3);transition:all .15s}
.sect-tab button.active{background:#047e7e;color:#fff}
.toast{position:fixed;bottom:24px;right:24px;z-index:9999;padding:12px 20px;border-radius:10px;font-size:13px;font-weight:600;font-family:'Space Grotesk',sans-serif;opacity:0;pointer-events:none;transition:opacity .25s;box-shadow:0 4px 20px rgba(0,0,0,.15)}
.toast.ok{background:#10b981;color:#fff}.toast.err{background:#ef4444;color:#fff}
</style>
</head>
<body>
<script src="/sidebar.js?v=25"></script>
<div class="page">
<div class="hdr">
<div>
<div class="hdr-title">Cash Flow Statement</div>
<div style="font-size:12px;color:var(--text3);margin-top:3px">Statement of Cash Flow &amp; Working Capital Analysis</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-outline" id="btnExport" onclick="exportXlsx()" disabled>Download Excel</button>
</div>
</div>
<!-- ── Filter Card ── -->
<div class="card">
<div class="filter-bar">
<div class="fg">
<label>From Date</label>
<input type="date" id="fromDate" class="f-input" style="width:150px"/>
</div>
<div class="fg">
<label>To Date</label>
<input type="date" id="toDate" class="f-input" style="width:150px"/>
</div>
<div class="fg" style="flex:1;min-width:220px">
<label>Snapshot — Working Capital Items</label>
<select id="snapSelect" class="f-select" style="min-width:260px" onchange="onSnapChange()">
<option value="">— Live data (from DB) —</option>
</select>
</div>
<div class="fg" style="flex:1;min-width:220px">
<label>Monthly A/c Snapshot — Profit</label>
<select id="monthlySnapSelect" class="f-select" style="min-width:260px">
<option value="">— Same as above / Live —</option>
</select>
</div>
<button class="btn btn-primary" id="btnLoad" onclick="loadData()" style="margin-top:auto">Load Cash Flow</button>
</div>
<div class="status-bar">
<div class="status-dot" id="statusDot"></div>
<span id="statusMsg">Configure items below, then select a date range and click Load</span>
</div>
</div>
<!-- ── Output ── -->
<div id="cfOut"></div>
<!-- ── Config Panel ── -->
<div class="cfg-panel">
<div class="cfg-hdr" id="cfgHdr" onclick="toggleCfg()">
<span style="font-size:16px">&#9881;</span>
<span class="cfg-title">Configure Line Items</span>
<span style="font-size:11px;color:var(--text3);margin-left:8px" id="cfgCount"></span>
<span class="cfg-chev">&#9660;</span>
</div>
<div class="cfg-body" id="cfgBody">
<!-- Add / Edit form -->
<div class="add-form">
<div class="add-form-title" id="formTitle">Add New Line Item</div>
<input type="hidden" id="editId"/>
<div class="form-row">
<div class="form-field" style="flex:2;min-width:200px">
<label>Label</label>
<input type="text" id="fLabel" placeholder="e.g. Sundry Debtors"/>
</div>
<div class="form-field">
<label>Section</label>
<select id="fSection" onchange="onSectionChange()">
<option value="Assets">Assets (Working Capital)</option>
<option value="Liabilities">Liabilities (Working Capital)</option>
<option value="Capital">Capital / Fixed Assets</option>
<option value="Cash">Cash / Bank / Investment</option>
</select>
</div>
<div class="form-field" id="typeField">
<label>Type</label>
<select id="fType">
<option value="working">Working Capital</option>
<option value="capital">Capital / Fixed</option>
</select>
</div>
<div class="form-field" style="width:80px">
<label>Order</label>
<input type="number" id="fOrder" value="999" style="width:80px"/>
</div>
<div class="form-field" style="flex:2;min-width:180px">
<label>Notes</label>
<input type="text" id="fNotes" placeholder="optional note"/>
</div>
</div>
<!-- Account code search -->
<div style="margin-bottom:10px">
<div style="font-size:10px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Account Codes</div>
<div class="acct-chips" id="acctChips"></div>
<div style="position:relative">
<div class="acct-search">
<input type="text" id="acctSearch" placeholder="Search or paste multiple codes (space / comma / newline)&#8230;" oninput="searchAccounts()" onpaste="pasteAcctCodes(event)" autocomplete="off"/>
<button class="btn btn-sm btn-outline" onclick="clearAcctSearch()">Clear</button>
</div>
<div class="acct-dropdown" id="acctDropdown" style="display:none"></div>
</div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-primary btn-sm" onclick="saveItem()">Save Item</button>
<button class="btn btn-outline btn-sm" onclick="cancelEdit()">Cancel</button>
</div>
</div>
<!-- Section tabs + item list -->
<div class="sect-tab">
<button id="tabAssets" class="active" onclick="setCfgTab('Assets')">Assets</button>
<button id="tabLiabilities" onclick="setCfgTab('Liabilities')">Liabilities</button>
<button id="tabCapital" onclick="setCfgTab('Capital')">Capital</button>
<button id="tabCash" onclick="setCfgTab('Cash')">Cash/Bank</button>
</div>
<div class="items-grid" id="itemsGrid"></div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function(){
const tok = sessionStorage.getItem('portal_token');
if (!tok) { window.location.href = '/'; return; }
const hdrs = { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' };
let _items = [];
let _values = {};
let _periodValues = {};
let _openingValues = {};
let _summary = null;
let _cfgTab = 'Assets';
let _editChips = [];
let _snapMeta = {};
// ── Formatting ────────────────────────────────────────────────────────────────
function fmtRs(v) {
if (v == null || isNaN(v)) return '&#8212;';
const abs = Math.abs(v);
if (abs < 0.005) return '<span class="amt-zero">&#8212;</span>';
const s = abs.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return v < 0
? `<span class="amt-neg">(${s})</span>`
: `<span class="amt-pos">${s}</span>`;
}
function fmtCr(v) {
if (v == null || isNaN(v)) return '&#8212;';
const cr = v / 10000000;
const abs = Math.abs(cr);
if (abs < 0.005) return '<span class="amt-zero">&#8212;</span>';
const s = abs.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return cr < 0
? `<span class="amt-neg">(${s})</span>`
: `<span class="amt-pos">${s}</span>`;
}
function fmtN(v, lakh) {
if (v == null || isNaN(v)) return '&#8212;';
const n = lakh ? v / 100000 : v;
if (Math.abs(n) < 0.005) return '<span class="amt-zero">&#8212;</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>`;
}
function fmtTag(v) {
if (Math.abs(v) < 0.005) return '<span class="tag-nil">&#8212;</span>';
return v > 0
? '<span class="tag-util">Utilisation</span>'
: '<span class="tag-gen">Generation</span>';
}
// ── Status / Toast ────────────────────────────────────────────────────────────
function setStatus(msg, state) {
document.getElementById('statusMsg').textContent = msg;
document.getElementById('statusDot').className = 'status-dot' + (state ? ' ' + state : '');
}
function showToast(msg, type) {
const t = document.getElementById('toast');
t.textContent = msg; t.className = 'toast ' + (type === 'error' ? 'err' : 'ok');
t.style.opacity = '1';
setTimeout(() => { t.style.opacity = '0'; }, 3000);
}
// ── Snapshot list ─────────────────────────────────────────────────────────────
(async function loadSnaps() {
try {
const r = await fetch('/api/cash-flow/snapshots', { headers: hdrs }).then(r => r.json());
if (!r.success) return;
const sel = document.getElementById('snapSelect');
const msel = document.getElementById('monthlySnapSelect');
r.data.forEach(s => {
_snapMeta[s.id] = { from: s.period_from, to: s.period_to };
const dt = new Date(s.saved_at).toLocaleDateString('en-IN');
const txt = `${s.snap_name} (${s.period_from} → ${s.period_to}) — ${dt}`;
const opt1 = document.createElement('option'); opt1.value = s.id; opt1.textContent = txt;
const opt2 = document.createElement('option'); opt2.value = s.id; opt2.textContent = txt;
sel.appendChild(opt1);
msel.appendChild(opt2);
});
} catch(_) {}
})();
function onSnapChange() {
const id = document.getElementById('snapSelect').value;
const meta = _snapMeta[id];
if (meta) {
document.getElementById('fromDate').value = meta.from;
document.getElementById('toDate').value = meta.to;
}
}
window.onSnapChange = onSnapChange;
// ── Load config items ─────────────────────────────────────────────────────────
async function loadItems() {
try {
const r = await fetch('/api/cash-flow/items', { headers: hdrs }).then(r => r.json());
if (!r.success) return;
_items = r.data;
renderCfgItems();
document.getElementById('cfgCount').textContent = `(${_items.length} items)`;
} catch(_) {}
}
loadItems();
// ── Load Cash Flow data ───────────────────────────────────────────────────────
async function loadData() {
const from = document.getElementById('fromDate').value;
const to = document.getElementById('toDate').value;
const snapId = document.getElementById('snapSelect').value;
if (!from || !to) { showToast('Select date range', 'error'); return; }
if (!_items.length) { showToast('Add line items in the config panel first', 'error'); return; }
document.getElementById('btnLoad').disabled = true;
setStatus('Loading…', 'loading');
document.getElementById('cfOut').innerHTML = '';
try {
const monthlySnapId = document.getElementById('monthlySnapSelect').value;
const qs = `from=${from}&to=${to}${snapId ? `&snapshotId=${snapId}` : ''}${monthlySnapId ? `&monthlySnapId=${monthlySnapId}` : ''}`;
const r = await fetch(`/api/cash-flow/data?${qs}`, { headers: hdrs }).then(r => r.json());
if (!r.success) throw new Error(r.message);
_items = r.items;
_values = r.values;
_periodValues = r.period_values || {};
_openingValues = r.opening_values || {};
_summary = r.summary || null;
renderCF(from, to);
document.getElementById('btnExport').disabled = false;
const src = snapId ? 'snapshot' : 'live DB';
const profitSrc = monthlySnapId ? 'monthly A/c snapshot' : (snapId ? 'same snapshot' : 'live DB');
setStatus(`Loaded — ${from} to ${to} · WC: ${src} · Profit: ${profitSrc}`, 'ok');
} catch(e) {
setStatus(e.message, 'err');
showToast(e.message, 'error');
} finally {
document.getElementById('btnLoad').disabled = false;
}
}
window.loadData = loadData;
// ── Render Statement of Cash Flow ─────────────────────────────────────────────
function renderStatement(from, to) {
if (!_summary) return '';
const s = _summary;
const lbl = s.period_label || `${from} to ${to}`;
const cashItems = _items.filter(i => i.section === 'Cash' || i.item_type === 'cash')
.sort((a, b) => a.sort_order - b.sort_order);
function row(cls, label, val) {
return `<tr class="${cls}">
<td>${label}</td>
<td class="num">${fmtRs(val)}</td>
<td class="cr">${fmtCr(val)}</td>
</tr>`;
}
// Build opening/closing cash breakdown
let openRows = '', closeRows = '';
let totalOpen = 0, totalClose = 0;
if (cashItems.length) {
cashItems.forEach((item, idx) => {
const ov = _openingValues[item.id] || 0;
const cv = _values[item.id] || 0;
totalOpen += ov;
totalClose += cv;
// Closing label: replace "Opening" prefix with "Closing" for first item
const closeLbl = idx === 0 ? item.label.replace(/^Opening\b/i, 'Closing') : item.label;
if (idx === 0) {
openRows += `<tr class="s-cash-lbl"><td>${item.label}</td><td class="num">${fmtRs(ov)}</td><td class="cr">${fmtCr(ov)}</td></tr>`;
closeRows += `<tr class="s-cash-lbl"><td>${closeLbl}</td><td class="num">${fmtRs(cv)}</td><td class="cr">${fmtCr(cv)}</td></tr>`;
} else {
openRows += `<tr class="s-cash-item"><td>${item.label}</td><td class="num">${fmtRs(ov)}</td><td class="cr">${fmtCr(ov)}</td></tr>`;
closeRows += `<tr class="s-cash-item"><td>${item.label}</td><td class="num">${fmtRs(cv)}</td><td class="cr">${fmtCr(cv)}</td></tr>`;
}
});
} else {
// Fallback: no cash items configured yet
openRows = row('s-cash-lbl', 'Opening Balance of Cash / Bank / Investment', s.opening_cash);
closeRows = row('s-cash-lbl', 'Closing Balance of Cash / Bank / Investment', s.closing_cash);
totalOpen = s.opening_cash;
totalClose = s.closing_cash;
}
const increase = totalClose - totalOpen;
return `
<div class="card">
<div class="stmt-hdr-card">
<div style="font-size:15px;font-weight:800">Statement of Cash Flow</div>
<div class="stmt-period">${lbl}</div>
</div>
<div class="stmt-wrap">
<table class="stmt-tbl">
<thead><tr>
<th style="min-width:320px">Particulars</th>
<th class="num">Rs.</th>
<th class="num">Rs. In Crores</th>
</tr></thead>
<tbody>
${row('', 'Profit as per Monthly Accounts', s.net_profit)}
${row('', 'Changes in Working Capital', s.wc_change)}
${row('s-total', 'Total', s.total)}
${row('', 'Capital Expenditure', s.capex)}
${row('s-net', 'Net Increase / (Decrease) in Cash Flow', s.net_increase)}
${openRows}
${cashItems.length > 1 ? `<tr class="s-cash-tot"><td>Total Opening</td><td class="num">${fmtRs(totalOpen)}</td><td class="cr">${fmtCr(totalOpen)}</td></tr>` : ''}
${closeRows}
${cashItems.length > 1 ? `<tr class="s-cash-tot"><td>Total Closing</td><td class="num">${fmtRs(totalClose)}</td><td class="cr">${fmtCr(totalClose)}</td></tr>` : ''}
${row('s-increase', 'Increase in Cash', increase)}
</tbody>
</table>
</div>
</div>`;
}
// ── Render detailed working capital table ─────────────────────────────────────
function renderCF(from, to) {
const lakh = true;
const assets = _items.filter(i => i.section === 'Assets');
const liabs = _items.filter(i => i.section === 'Liabilities');
const capItems= _items.filter(i => i.section === 'Capital' || (i.section === 'Assets' && i.item_type === 'capital'));
const cashItems= _items.filter(i => i.section === 'Cash' || i.item_type === 'cash');
let assetsTot = 0, assetsWork = 0;
let liabsTot = 0;
let capTot = 0;
let aRows = '';
assets.forEach(item => {
const v = _values[item.id] || 0;
assetsTot += v;
if (item.item_type !== 'capital') assetsWork += v;
const col2 = item.item_type === 'capital' ? '<td class="num"></td>' : `<td class="num">${fmtN(v, lakh)}</td>`;
aRows += `<tr class="item">
<td class="lbl indent">${item.label}${item.notes ? ` <span style="font-size:10px;color:#9ca3af">${item.notes}</span>` : ''}</td>
<td class="num">${fmtN(v, lakh)}</td>
${col2}
<td style="width:110px"></td>
</tr>`;
});
let lRows = '';
liabs.forEach(item => {
const v = _values[item.id] || 0;
liabsTot += v;
const tag = Math.abs(v) > 0.005 ? (v < 0 ? '<span class="tag-gen">Generation</span>' : '<span class="tag-util">Utilisation</span>') : '';
lRows += `<tr class="item">
<td class="lbl indent">${item.label}${item.notes ? ` <span style="font-size:10px;color:#9ca3af">${item.notes}</span>` : ''}</td>
<td class="num">${fmtN(v, lakh)}</td>
<td class="num">${fmtN(v, lakh)}</td>
<td style="width:110px">${tag}</td>
</tr>`;
});
let cRows = '';
capItems.forEach(item => {
const v = _values[item.id] || 0;
const pv = _periodValues[item.id] || 0;
capTot += pv;
cRows += `<tr class="item">
<td class="lbl indent">${item.label}${item.notes ? ` <span style="font-size:10px;color:#9ca3af">${item.notes}</span>` : ''}</td>
<td class="num">${fmtN(v, lakh)}</td>
<td class="num" style="color:#374151;font-size:11px">Period: ${fmtN(pv, lakh)}</td>
<td style="width:110px"></td>
</tr>`;
});
let cashRows = '';
cashItems.forEach(item => {
const open = _openingValues[item.id] || 0;
const close = _values[item.id] || 0;
cashRows += `<tr class="item">
<td class="lbl indent">${item.label}${item.notes ? ` <span style="font-size:10px;color:#9ca3af">${item.notes}</span>` : ''}</td>
<td class="num">${fmtN(open, lakh)}</td>
<td class="num">${fmtN(close, lakh)}</td>
<td style="width:110px"></td>
</tr>`;
});
const grandTot = assetsTot + liabsTot;
const grandWork = assetsWork + liabsTot;
const detailHtml = `
<div style="padding:0 0 8px;font-size:11px;color:var(--text3);text-align:right">Amounts in &#8377; Lakhs &nbsp;|&nbsp; Period: ${from} to ${to}</div>
<table class="cf-tbl">
<thead><tr>
<th style="min-width:280px">Particulars</th>
<th class="num" style="min-width:150px">Amount / Closing (&#8377;L)</th>
<th class="num" style="min-width:150px">Working Capital / Period (&#8377;L)</th>
<th style="width:110px"></th>
</tr></thead>
<tbody>
${assets.length ? `
<tr class="sect-hdr"><td colspan="4">Working Capital &#8212; Assets</td></tr>
${aRows}
<tr class="sub">
<td class="lbl">Total &#8212; Assets Working Capital</td>
<td class="num">${fmtN(assetsTot, lakh)}</td>
<td class="num">${fmtN(assetsWork, lakh)}</td>
<td>${fmtTag(assetsWork)}</td>
</tr>` : ''}
${liabs.length ? `
<tr class="sect-hdr"><td colspan="4">Working Capital &#8212; Liabilities</td></tr>
${lRows}
<tr class="sub">
<td class="lbl">Total &#8212; Liabilities Working Capital</td>
<td class="num">${fmtN(liabsTot, lakh)}</td>
<td class="num">${fmtN(liabsTot, lakh)}</td>
<td>${fmtTag(liabsTot)}</td>
</tr>` : ''}
${(assets.length || liabs.length) ? `
<tr class="grand">
<td>Net Working Capital Change</td>
<td class="num" style="color:#fff">${fmtN(grandTot, lakh)}</td>
<td class="num" style="color:#fff">${fmtN(grandWork, lakh)}</td>
<td></td>
</tr>` : ''}
${capItems.length ? `
<tr class="sect-hdr"><td colspan="4">Capital / Fixed Assets</td></tr>
${cRows}
<tr class="sub">
<td class="lbl">Total Capital Items</td>
<td class="num"></td>
<td class="num">${fmtN(capTot, lakh)}</td>
<td>${fmtTag(-capTot)}</td>
</tr>` : ''}
${cashItems.length ? `
<tr class="sect-hdr"><td colspan="4">Cash / Bank / Investment</td></tr>
<tr class="sub" style="background:#e0f4f4">
<td class="lbl" colspan="2">Opening Balance (before period)</td>
<td class="lbl">Closing Balance</td>
<td></td>
</tr>
${cashRows}` : ''}
</tbody>
</table>`;
document.getElementById('cfOut').innerHTML =
renderStatement(from, to) +
`<div class="card" style="margin-bottom:16px">
<div class="detail-toggle" id="detailToggle" onclick="toggleDetail()">
&#9660; Working Capital &amp; Capital Details
</div>
<div class="detail-body" id="detailBody">
${detailHtml}
</div>
</div>`;
}
function toggleDetail() {
const body = document.getElementById('detailBody');
const tog = document.getElementById('detailToggle');
const open = body.classList.toggle('open');
tog.innerHTML = (open ? '&#9650;' : '&#9660;') + ' Working Capital &amp; Capital Details';
}
window.toggleDetail = toggleDetail;
// ── Config panel ──────────────────────────────────────────────────────────────
function toggleCfg() {
document.getElementById('cfgHdr').classList.toggle('open');
document.getElementById('cfgBody').classList.toggle('open');
}
window.toggleCfg = toggleCfg;
function onSectionChange() {
const sec = document.getElementById('fSection').value;
const typeField = document.getElementById('typeField');
if (sec === 'Cash') {
typeField.style.display = 'none';
} else {
typeField.style.display = '';
// Auto-set type based on section
const fType = document.getElementById('fType');
if (sec === 'Capital') fType.value = 'capital';
else fType.value = 'working';
}
}
window.onSectionChange = onSectionChange;
function setCfgTab(tab) {
_cfgTab = tab;
['Assets','Liabilities','Capital','Cash'].forEach(t => {
document.getElementById('tab' + t).classList.toggle('active', t === tab);
});
renderCfgItems();
}
window.setCfgTab = setCfgTab;
function typeLabel(item) {
if (item.item_type === 'cash') return '&#128994; Cash/Bank';
if (item.item_type === 'capital') return '&#127959; Capital';
return '&#128260; Working Capital';
}
function renderCfgItems() {
const visible = _items.filter(i => i.section === _cfgTab);
if (!visible.length) {
document.getElementById('itemsGrid').innerHTML = `<div style="color:var(--text3);font-size:13px;grid-column:1/-1;padding:12px 0">No items in ${_cfgTab} section. Add one above.</div>`;
return;
}
document.getElementById('itemsGrid').innerHTML = visible.map(item => `
<div class="item-card">
<div class="item-card-top">
<div>
<div class="item-card-label">${item.label}</div>
<div class="item-card-meta">
Order: ${item.sort_order} &nbsp;&middot;&nbsp;
${typeLabel(item)}
${item.notes ? ` &nbsp;&middot;&nbsp; ${item.notes}` : ''}
</div>
</div>
<div class="item-card-actions">
<button class="btn btn-sm btn-outline" onclick="startEdit(${item.id})">Edit</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">Del</button>
</div>
</div>
<div class="item-card-codes">
${item.acct_codes.length ? item.acct_codes.map(c => `<span class="item-card-code">${c}</span>`).join('') : '<span style="color:#9ca3af;font-size:11px">No accounts</span>'}
</div>
</div>
`).join('');
}
// ── Account search ────────────────────────────────────────────────────────────
function pasteAcctCodes(e) {
const text = (e.clipboardData || window.clipboardData).getData('text');
const codes = text.split(/[\s,;\n\r]+/).map(s => s.trim()).filter(s => s.length > 0);
if (codes.length > 1) {
e.preventDefault();
let added = 0;
codes.forEach(code => {
if (!_editChips.includes(code)) { _editChips.push(code); added++; }
});
renderChips();
document.getElementById('acctSearch').value = '';
document.getElementById('acctDropdown').style.display = 'none';
if (added > 0) showToast(`Added ${added} account code${added > 1 ? 's' : ''}`, 'success');
}
}
window.pasteAcctCodes = pasteAcctCodes;
let _searchTimer = null;
async function searchAccounts() {
const q = document.getElementById('acctSearch').value.trim();
clearTimeout(_searchTimer);
if (q.length < 2) { document.getElementById('acctDropdown').style.display = 'none'; return; }
_searchTimer = setTimeout(async () => {
try {
const r = await fetch(`/api/cash-flow/accounts?q=${encodeURIComponent(q)}`, { headers: hdrs }).then(r => r.json());
if (!r.success) return;
const dd = document.getElementById('acctDropdown');
if (!r.data.length) { dd.style.display = 'none'; return; }
dd.innerHTML = r.data.map(a =>
`<div class="acct-dropdown-item" onclick="addAcctCode('${a.AcctCode}','${a.AcctName.replace(/'/g,"\\'")}')">
<span class="code">${a.AcctCode}</span>
<span class="name">${a.AcctName}</span>
</div>`
).join('');
dd.style.display = 'block';
} catch(_) {}
}, 250);
}
window.searchAccounts = searchAccounts;
function addAcctCode(code, name) {
if (!_editChips.includes(code)) { _editChips.push(code); renderChips(); }
document.getElementById('acctSearch').value = '';
document.getElementById('acctDropdown').style.display = 'none';
}
window.addAcctCode = addAcctCode;
function removeChip(code) {
_editChips = _editChips.filter(c => c !== code);
renderChips();
}
window.removeChip = removeChip;
function renderChips() {
document.getElementById('acctChips').innerHTML = _editChips.map(c =>
`<span class="acct-chip">${c} <button onclick="removeChip('${c}')" title="Remove">&times;</button></span>`
).join('') || '<span style="color:#9ca3af;font-size:11px">No accounts added</span>';
}
function clearAcctSearch() {
document.getElementById('acctSearch').value = '';
document.getElementById('acctDropdown').style.display = 'none';
}
window.clearAcctSearch = clearAcctSearch;
document.addEventListener('click', e => {
if (!e.target.closest('#acctSearch') && !e.target.closest('#acctDropdown'))
document.getElementById('acctDropdown').style.display = 'none';
});
// ── Form: Save / Edit / Cancel ────────────────────────────────────────────────
async function saveItem() {
const id = document.getElementById('editId').value || null;
const section = document.getElementById('fSection').value;
const label = document.getElementById('fLabel').value.trim();
let type = document.getElementById('fType').value;
const order = document.getElementById('fOrder').value;
const notes = document.getElementById('fNotes').value.trim();
if (!label) { showToast('Label is required', 'error'); return; }
if (section === 'Cash') type = 'cash';
try {
const r = await fetch('/api/cash-flow/items', {
method: 'POST', headers: hdrs,
body: JSON.stringify({ id, section, item_type: type, label, acct_codes: _editChips, sort_order: order, notes })
}).then(r => r.json());
if (!r.success) throw new Error(r.message);
showToast(id ? 'Item updated' : 'Item added', 'success');
cancelEdit();
await loadItems();
_cfgTab = section;
setCfgTab(section);
} catch(e) { showToast(e.message, 'error'); }
}
window.saveItem = saveItem;
function cancelEdit() {
document.getElementById('editId').value = '';
document.getElementById('fLabel').value = '';
document.getElementById('fSection').value = 'Assets';
document.getElementById('fType').value = 'working';
document.getElementById('fOrder').value = '999';
document.getElementById('fNotes').value = '';
document.getElementById('typeField').style.display = '';
_editChips = [];
renderChips();
document.getElementById('formTitle').textContent = 'Add New Line Item';
}
window.cancelEdit = cancelEdit;
function startEdit(id) {
const item = _items.find(i => i.id === id);
if (!item) return;
document.getElementById('editId').value = item.id;
document.getElementById('fLabel').value = item.label;
document.getElementById('fSection').value = item.section;
document.getElementById('fType').value = item.item_type === 'cash' ? 'working' : (item.item_type || 'working');
document.getElementById('fOrder').value = item.sort_order;
document.getElementById('fNotes').value = item.notes || '';
document.getElementById('typeField').style.display = item.section === 'Cash' ? 'none' : '';
_editChips = [...item.acct_codes];
renderChips();
document.getElementById('formTitle').textContent = 'Edit Line Item';
document.getElementById('cfgBody').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
window.startEdit = startEdit;
async function deleteItem(id) {
if (!confirm('Delete this item?')) return;
try {
const r = await fetch(`/api/cash-flow/items/${id}`, { method: 'DELETE', headers: hdrs }).then(r => r.json());
if (!r.success) throw new Error(r.message);
showToast('Deleted', 'success');
await loadItems();
} catch(e) { showToast(e.message, 'error'); }
}
window.deleteItem = deleteItem;
// ── Excel Export ──────────────────────────────────────────────────────────────
function exportXlsx() {
if (!_summary) return;
const from = document.getElementById('fromDate').value;
const to = document.getElementById('toDate').value;
const s = _summary;
const cr = v => parseFloat((v / 10000000).toFixed(2));
const cashItems2 = _items.filter(i => i.section === 'Cash' || i.item_type === 'cash').sort((a,b)=>a.sort_order-b.sort_order);
let xTotalOpen = 0, xTotalClose = 0;
const openRows2 = cashItems2.map((item, idx) => {
const v = _openingValues[item.id] || 0; xTotalOpen += v;
return [idx===0 ? item.label : ` ${item.label}`, v, cr(v)];
});
const closeRows2 = cashItems2.map((item, idx) => {
const v = _values[item.id] || 0; xTotalClose += v;
return [idx===0 ? item.label : ` ${item.label}`, v, cr(v)];
});
if (!cashItems2.length) { xTotalOpen = s.opening_cash; xTotalClose = s.closing_cash; }
const rows = [
['Statement of Cash Flow', '', ''],
[s.period_label || `${from} to ${to}`, '', ''],
[],
['Particulars', 'Rs.', 'Rs. In Crores'],
['Profit as per Monthly Accounts', s.net_profit, cr(s.net_profit)],
['Changes in Working Capital', s.wc_change, cr(s.wc_change)],
['Total', s.total, cr(s.total)],
[],
['Capital Expenditure', s.capex, cr(s.capex)],
[],
['Net Increase / (Decrease) in Cash Flow', s.net_increase, cr(s.net_increase)],
[],
...(cashItems2.length ? openRows2 : [['Opening Balance of Cash / Bank / Investment', s.opening_cash, cr(s.opening_cash)]]),
...(cashItems2.length > 1 ? [['Total Opening', xTotalOpen, cr(xTotalOpen)]] : []),
[],
...(cashItems2.length ? closeRows2 : [['Closing Balance of Cash / Bank / Investment', s.closing_cash, cr(s.closing_cash)]]),
...(cashItems2.length > 1 ? [['Total Closing', xTotalClose, cr(xTotalClose)]] : []),
[],
['Increase in Cash', xTotalClose - xTotalOpen, cr(xTotalClose - xTotalOpen)],
[],
[],
['Working Capital Details', '', ''],
['Particulars', 'Amount (Rs. L)', 'Working Capital (Rs. L)'],
];
const addSection = (label, items) => {
rows.push([label]);
let tot = 0, work = 0;
items.forEach(item => {
const v = (_values[item.id] || 0) / 100000;
tot += v;
if (item.item_type !== 'capital') work += v;
rows.push([' ' + item.label, parseFloat(v.toFixed(2)), item.item_type === 'capital' ? '' : parseFloat(v.toFixed(2))]);
});
rows.push(['Total', parseFloat(tot.toFixed(2)), parseFloat(work.toFixed(2))]);
rows.push([]);
};
addSection('Working Capital — Assets', _items.filter(i => i.section === 'Assets'));
addSection('Working Capital — Liabilities', _items.filter(i => i.section === 'Liabilities'));
addSection('Capital / Fixed Assets', _items.filter(i => i.section === 'Capital'));
const ws = XLSX.utils.aoa_to_sheet(rows);
ws['!cols'] = [40, 18, 16].map(w => ({ wch: w }));
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Cash Flow');
XLSX.writeFile(wb, `Cash_Flow_${from}_to_${to}.xlsx`);
showToast('Excel exported', 'success');
}
window.exportXlsx = exportXlsx;
// Init date range to current month
(function initDates() {
const now = new Date(), y = now.getFullYear(), m = String(now.getMonth()+1).padStart(2,'0');
document.getElementById('fromDate').value = `${y}-${m}-01`;
const last = new Date(y, now.getMonth()+1, 0);
document.getElementById('toDate').value = `${y}-${m}-${String(last.getDate()).padStart(2,'0')}`;
})();
})();
</script>
</body>
</html>