915 lines
43 KiB
HTML
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 & 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">⚙</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">▼</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)…" 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 '—';
|
|
const abs = Math.abs(v);
|
|
if (abs < 0.005) return '<span class="amt-zero">—</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 '—';
|
|
const cr = v / 10000000;
|
|
const abs = Math.abs(cr);
|
|
if (abs < 0.005) return '<span class="amt-zero">—</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 '—';
|
|
const n = lakh ? v / 100000 : v;
|
|
if (Math.abs(n) < 0.005) return '<span class="amt-zero">—</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">—</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 ₹ Lakhs | 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 (₹L)</th>
|
|
<th class="num" style="min-width:150px">Working Capital / Period (₹L)</th>
|
|
<th style="width:110px"></th>
|
|
</tr></thead>
|
|
<tbody>
|
|
|
|
${assets.length ? `
|
|
<tr class="sect-hdr"><td colspan="4">Working Capital — Assets</td></tr>
|
|
${aRows}
|
|
<tr class="sub">
|
|
<td class="lbl">Total — 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 — Liabilities</td></tr>
|
|
${lRows}
|
|
<tr class="sub">
|
|
<td class="lbl">Total — 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()">
|
|
▼ Working Capital & 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 ? '▲' : '▼') + ' Working Capital & 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 '🟢 Cash/Bank';
|
|
if (item.item_type === 'capital') return '🏗 Capital';
|
|
return '🔄 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} ·
|
|
${typeLabel(item)}
|
|
${item.notes ? ` · ${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">×</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>
|