173 lines
7.0 KiB
JavaScript
173 lines
7.0 KiB
JavaScript
'use strict';
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const { verifyToken } = require('../middleware/auth');
|
|
const { getPool } = require('../services/sqlPool');
|
|
|
|
const store = () => require('../services/cashFlowStore');
|
|
const bsStore = () => require('../services/balanceSheetStore');
|
|
|
|
store().bootstrap().catch(e => console.error('[CashFlow] bootstrap error:', e));
|
|
|
|
function dayBefore(dateStr) {
|
|
const d = new Date(dateStr);
|
|
d.setDate(d.getDate() - 1);
|
|
return d.toISOString().split('T')[0];
|
|
}
|
|
|
|
function periodLabel(to) {
|
|
const d = new Date(to);
|
|
const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
|
|
return `Upto ${months[d.getMonth()]} ${d.getFullYear()}`;
|
|
}
|
|
|
|
// GET /api/cash-flow/items
|
|
router.get('/items', verifyToken, async (req, res) => {
|
|
try { res.json({ success: true, data: await store().getItems() }); }
|
|
catch(err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// POST /api/cash-flow/items — create or update
|
|
router.post('/items', verifyToken, async (req, res) => {
|
|
try {
|
|
const { id, section, item_type, label, acct_codes, sort_order, notes } = req.body;
|
|
if (!label || !section) return res.status(400).json({ success: false, message: 'section and label required' });
|
|
// cash section always forces item_type=cash
|
|
const type = section === 'Cash' ? 'cash' : (item_type || 'working');
|
|
await store().saveItem({ id: id || null, section, item_type: type, label, acct_codes, sort_order, notes });
|
|
res.json({ success: true });
|
|
} catch(err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// DELETE /api/cash-flow/items/:id
|
|
router.delete('/items/:id', verifyToken, async (req, res) => {
|
|
try {
|
|
await store().deleteItem(parseInt(req.params.id));
|
|
res.json({ success: true });
|
|
} catch(err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// GET /api/cash-flow/data?from=&to=[&snapshotId=][&monthlySnapId=]
|
|
router.get('/data', verifyToken, async (req, res) => {
|
|
try {
|
|
const { from, to, snapshotId, monthlySnapId } = req.query;
|
|
if (!from || !to) return res.status(400).json({ success: false, message: 'from and to required' });
|
|
|
|
const s = store();
|
|
const items = await s.getItems();
|
|
|
|
const values = {}; // detailed table values
|
|
const period_values = {}; // period D-C for all items (used for statement CapEx/WC)
|
|
const opening_values = {}; // opening balance for cash items
|
|
|
|
const openDate = dayBefore(from);
|
|
|
|
if (snapshotId) {
|
|
// pl_snapshots lives on the app DB — read via costingStore, not the SAP pool.
|
|
const snap = await require('../services/costingStore').getSnapshot(parseInt(snapshotId));
|
|
if (!snap) return res.status(404).json({ success: false, message: 'Snapshot not found' });
|
|
const snapData = JSON.parse(snap.data_json);
|
|
const accounts = snapData.accounts || snapData;
|
|
|
|
items.forEach(item => {
|
|
if (item.item_type === 'cash') {
|
|
opening_values[item.id] = s.fetchOpeningBalanceFromSnapshot(accounts, item.acct_codes);
|
|
values[item.id] = s.fetchItemClosingBalanceFromSnapshot(accounts, item.acct_codes);
|
|
period_values[item.id] = s.fetchItemValueFromSnapshot(accounts, item.acct_codes);
|
|
} else if (item.item_type === 'capital') {
|
|
values[item.id] = s.fetchItemClosingBalanceFromSnapshot(accounts, item.acct_codes);
|
|
period_values[item.id] = s.fetchItemValueFromSnapshot(accounts, item.acct_codes);
|
|
} else {
|
|
values[item.id] = s.fetchItemValueFromSnapshot(accounts, item.acct_codes);
|
|
period_values[item.id] = values[item.id];
|
|
}
|
|
});
|
|
} else {
|
|
const pool = await getPool();
|
|
await Promise.all(items.map(async item => {
|
|
if (item.item_type === 'cash') {
|
|
[opening_values[item.id], values[item.id], period_values[item.id]] = await Promise.all([
|
|
s.fetchItemClosingBalance(pool, item.acct_codes, openDate),
|
|
s.fetchItemClosingBalance(pool, item.acct_codes, to),
|
|
s.fetchItemValue(pool, item.acct_codes, from, to),
|
|
]);
|
|
} else if (item.item_type === 'capital') {
|
|
[values[item.id], period_values[item.id]] = await Promise.all([
|
|
s.fetchItemClosingBalance(pool, item.acct_codes, to),
|
|
s.fetchItemValue(pool, item.acct_codes, from, to),
|
|
]);
|
|
} else {
|
|
values[item.id] = await s.fetchItemValue(pool, item.acct_codes, from, to);
|
|
period_values[item.id] = values[item.id];
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Net profit — always from Monthly Accounts snapshot if provided,
|
|
// otherwise from the main snapshot (if selected), otherwise live DB
|
|
const pool = await getPool();
|
|
const profitSnapId = monthlySnapId || (!snapshotId ? null : snapshotId);
|
|
const netProfit = profitSnapId
|
|
? await bsStore().fetchNetProfitFromSnapshot(pool, profitSnapId)
|
|
: await bsStore().fetchNetProfit(pool, from, to);
|
|
|
|
// Aggregate summary components
|
|
let wc_change = 0, capex = 0, opening_cash = 0, closing_cash = 0;
|
|
items.forEach(item => {
|
|
const pv = period_values[item.id] || 0;
|
|
if (item.item_type === 'cash') {
|
|
opening_cash += opening_values[item.id] || 0;
|
|
closing_cash += values[item.id] || 0;
|
|
} else if (item.item_type === 'working') {
|
|
// WC increase = cash decrease → negate period D-C
|
|
wc_change -= pv;
|
|
} else if (item.item_type === 'capital') {
|
|
// CapEx: asset D-C > 0 (bought) = cash out = negative
|
|
capex -= pv;
|
|
}
|
|
});
|
|
|
|
const total = netProfit + wc_change;
|
|
const net_increase = total + capex;
|
|
|
|
const summary = {
|
|
net_profit: netProfit,
|
|
wc_change,
|
|
total,
|
|
capex,
|
|
net_increase,
|
|
opening_cash,
|
|
closing_cash,
|
|
period_label: periodLabel(to),
|
|
};
|
|
|
|
res.json({ success: true, from, to, items, values, period_values, opening_values, summary });
|
|
} catch(err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// GET /api/cash-flow/snapshots
|
|
router.get('/snapshots', verifyToken, async (req, res) => {
|
|
try {
|
|
const snaps = await require('../services/costingStore').getSnapshots();
|
|
res.json({ success: true, data: snaps });
|
|
} catch(err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// GET /api/cash-flow/accounts?q= — search accounts for config panel
|
|
router.get('/accounts', verifyToken, async (req, res) => {
|
|
try {
|
|
const { q } = req.query;
|
|
const pool = await getPool();
|
|
const req2 = pool.request();
|
|
let where = `WHERE T0.GroupMask IN (1,2,3,4,5)`;
|
|
if (q) { req2.input('q', `%${q}%`); where += ` AND (T0.AcctCode LIKE @q OR T0.AcctName LIKE @q)`; }
|
|
const r = await req2.query(`
|
|
SELECT TOP 100 T0.AcctCode, T0.AcctName, T0.GroupMask
|
|
FROM OACT T0 ${where} ORDER BY T0.AcctCode
|
|
`);
|
|
res.json({ success: true, data: r.recordset });
|
|
} catch(err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
module.exports = router;
|