'use strict'; const { getPool } = require('./appSqlPool'); const COST_CENTRES = [ 'Administration overhead', 'BB & CAPD', 'BLOOD BAG', 'Boiler', 'CAPD', 'Diff', 'Equipment', 'FACTORY OVERHEAD', 'POWER & FUEL', 'QA', 'QC', 'R&D', 'Selling & distribution overhead', 'STENT', ]; async function bootstrap() { const pool = await getPool(); await pool.request().query(` IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='salary_uploads') CREATE TABLE salary_uploads ( id INT IDENTITY(1,1) PRIMARY KEY, period VARCHAR(7) NOT NULL, sheet_name NVARCHAR(100) NOT NULL, sr_no NVARCHAR(20) DEFAULT '', emp_code NVARCHAR(50) DEFAULT '', emp_name NVARCHAR(200) DEFAULT '', department NVARCHAR(100) DEFAULT '', gross_amt DECIMAL(15,2) DEFAULT 0, net_pay DECIMAL(15,2) DEFAULT 0, uploaded_at DATETIME DEFAULT GETDATE() )`); await pool.request().query(` IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='salary_cost_config') CREATE TABLE salary_cost_config ( id INT IDENTITY(1,1) PRIMARY KEY, sheet_name NVARCHAR(100) NOT NULL, cost_centre NVARCHAR(100) NOT NULL, alloc_pct DECIMAL(7,4) NOT NULL DEFAULT 0, updated_at DATETIME DEFAULT GETDATE(), CONSTRAINT uq_sal_cfg UNIQUE (sheet_name, cost_centre) )`); await pool.request().query(` IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='salary_bbcapd_ratio') CREATE TABLE salary_bbcapd_ratio ( id INT IDENTITY(1,1) PRIMARY KEY, sheet_name NVARCHAR(100) NOT NULL UNIQUE, bb_pct DECIMAL(7,4) NOT NULL DEFAULT 70, capd_pct DECIMAL(7,4) NOT NULL DEFAULT 30, updated_at DATETIME DEFAULT GETDATE() )`); await pool.request().query(` IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='salary_dept_mapping') CREATE TABLE salary_dept_mapping ( id INT IDENTITY(1,1) PRIMARY KEY, dept_name NVARCHAR(200) NOT NULL UNIQUE, cost_centre NVARCHAR(100) NOT NULL DEFAULT '', updated_at DATETIME DEFAULT GETDATE() )`); } async function clearPeriod(period) { const pool = await getPool(); await pool.request().input('p', period) .query(`DELETE FROM salary_uploads WHERE period=@p`); } async function saveSheetRows(period, sheetName, rows) { const pool = await getPool(); await pool.request() .input('p', period).input('s', sheetName) .query(`DELETE FROM salary_uploads WHERE period=@p AND sheet_name=@s`); for (const r of rows) { await pool.request() .input('p', period).input('s', sheetName) .input('sr', r.sr_no || '').input('ec', r.employee_code || '') .input('en', r.employee_name || '').input('dp', r.department || '') .input('gr', r.gross_salary || 0).input('np', r.net_pay || 0) .query(`INSERT INTO salary_uploads (period,sheet_name,sr_no,emp_code,emp_name,department,gross_amt,net_pay) VALUES(@p,@s,@sr,@ec,@en,@dp,@gr,@np)`); } } async function getPeriods() { const pool = await getPool(); const r = await pool.request() .query(`SELECT DISTINCT period FROM salary_uploads ORDER BY period DESC`); return (r.recordset || []).map(x => x.period); } async function getSheets(period) { const pool = await getPool(); const r = await pool.request().input('p', period).query(` SELECT sheet_name, COUNT(*) AS row_count, SUM(gross_amt) AS total_gross, SUM(net_pay) AS total_net FROM salary_uploads WHERE period=@p GROUP BY sheet_name ORDER BY sheet_name`); return r.recordset || []; } async function getRows(period, sheetName) { const pool = await getPool(); const r = await pool.request() .input('p', period).input('s', sheetName) .query(`SELECT sr_no,emp_code,emp_name,department,gross_amt,net_pay FROM salary_uploads WHERE period=@p AND sheet_name=@s ORDER BY id`); return r.recordset || []; } async function getConfig() { const pool = await getPool(); const r = await pool.request() .query(`SELECT sheet_name,cost_centre,alloc_pct FROM salary_cost_config ORDER BY sheet_name,cost_centre`); return r.recordset || []; } async function saveConfig(entries) { const pool = await getPool(); for (const e of entries) { await pool.request() .input('s', e.sheet_name).input('c', e.cost_centre).input('p', e.alloc_pct) .query(`MERGE salary_cost_config AS t USING (SELECT @s AS sheet_name,@c AS cost_centre) AS src ON t.sheet_name=src.sheet_name AND t.cost_centre=src.cost_centre WHEN MATCHED THEN UPDATE SET alloc_pct=@p,updated_at=GETDATE() WHEN NOT MATCHED THEN INSERT(sheet_name,cost_centre,alloc_pct) VALUES(@s,@c,@p);`); } } async function getBBRatios() { const pool = await getPool(); const r = await pool.request() .query(`SELECT sheet_name,bb_pct,capd_pct FROM salary_bbcapd_ratio`); return r.recordset || []; } async function saveBBRatio(sheetName, bbPct, capdPct) { const pool = await getPool(); await pool.request() .input('s', sheetName).input('b', bbPct).input('c', capdPct) .query(`MERGE salary_bbcapd_ratio AS t USING (SELECT @s AS sheet_name) AS src ON t.sheet_name=src.sheet_name WHEN MATCHED THEN UPDATE SET bb_pct=@b,capd_pct=@c,updated_at=GETDATE() WHEN NOT MATCHED THEN INSERT(sheet_name,bb_pct,capd_pct) VALUES(@s,@b,@c);`); } async function getDeptMappings() { const pool = await getPool(); const r = await pool.request() .query(`SELECT dept_name, cost_centre FROM salary_dept_mapping ORDER BY dept_name`); return r.recordset || []; } async function saveDeptMapping(deptName, costCentre) { const pool = await getPool(); await pool.request() .input('d', deptName).input('c', costCentre) .query(`MERGE salary_dept_mapping AS t USING (SELECT @d AS dept_name) AS src ON t.dept_name=src.dept_name WHEN MATCHED THEN UPDATE SET cost_centre=@c, updated_at=GETDATE() WHEN NOT MATCHED THEN INSERT(dept_name,cost_centre) VALUES(@d,@c);`); } async function getDistinctDepts(period) { const pool = await getPool(); const r = await pool.request().input('p', period).query(` SELECT department AS dept_name, COUNT(*) AS emp_count, SUM(net_pay) AS total_net FROM salary_uploads WHERE period=@p AND department <> '' GROUP BY department ORDER BY department`); return r.recordset || []; } function applyToCC(total, sheetAlloc, cc, amt, bbRatio) { if (cc === 'BB & CAPD') { const ratio = bbRatio || { bb_pct: 70, capd_pct: 30 }; const bbAmt = amt * (parseFloat(ratio.bb_pct) || 70) / 100; const capdAmt = amt * (parseFloat(ratio.capd_pct) || 30) / 100; total['BLOOD BAG'] += bbAmt; sheetAlloc['BLOOD BAG'] += bbAmt; total['CAPD'] += capdAmt; sheetAlloc['CAPD'] += capdAmt; } else if (COST_CENTRES.includes(cc)) { total[cc] += amt; sheetAlloc[cc] += amt; } } async function getSummary(period) { const pool = await getPool(); const sheets = await getSheets(period); const config = await getConfig(); const bbRatios = await getBBRatios(); const deptMaps = await getDeptMappings(); const cfgMap = {}; config.forEach(c => { if (!cfgMap[c.sheet_name]) cfgMap[c.sheet_name] = {}; cfgMap[c.sheet_name][c.cost_centre] = parseFloat(c.alloc_pct) || 0; }); const bbMap = {}; bbRatios.forEach(b => { bbMap[b.sheet_name] = b; }); const deptMap = {}; deptMaps.forEach(d => { if (d.cost_centre) deptMap[d.dept_name.toLowerCase().trim()] = d.cost_centre; }); const total = {}; COST_CENTRES.forEach(cc => { total[cc] = 0; }); const bySheet = {}; for (const sheet of sheets) { const sheetAlloc = {}; COST_CENTRES.forEach(cc => { sheetAlloc[cc] = 0; }); const bbRatio = bbMap[sheet.sheet_name]; const alloc = cfgMap[sheet.sheet_name] || {}; // Get per-employee rows to apply dept mapping individually const rows = await pool.request() .input('p', period).input('s', sheet.sheet_name) .query(`SELECT department, net_pay FROM salary_uploads WHERE period=@p AND sheet_name=@s`); const unmappedDepts = {}; (rows.recordset || []).forEach(row => { const net = parseFloat(row.net_pay) || 0; const dept = (row.department || '').toLowerCase().trim(); const mappedCC = deptMap[dept]; if (mappedCC) { applyToCC(total, sheetAlloc, mappedCC, net, bbRatio); } else { // Check if dept value directly matches a cost centre name (sheets 6-8 COST CENTER column) const directCC = COST_CENTRES.find(cc => cc.toLowerCase() === dept); if (directCC) { applyToCC(total, sheetAlloc, directCC, net, bbRatio); } else { // Fall back to sheet-level % allocation const allocated = Object.entries(alloc).reduce((sum, [cc, pct]) => { applyToCC(total, sheetAlloc, cc, net * pct / 100, bbRatio); return sum + pct; }, 0); const key = row.department || '(no department)'; if (!unmappedDepts[key]) unmappedDepts[key] = { net: 0, emp: 0 }; unmappedDepts[key].net += net; unmappedDepts[key].emp += 1; unmappedDepts[key].pct_applied = allocated; } } }); bySheet[sheet.sheet_name] = { total_net: parseFloat(sheet.total_net) || 0, row_count: sheet.row_count, allocation: sheetAlloc, unmapped_depts: unmappedDepts, }; } return { total, by_sheet: bySheet }; } module.exports = { bootstrap, clearPeriod, saveSheetRows, getPeriods, getSheets, getRows, getConfig, saveConfig, getBBRatios, saveBBRatio, getDeptMappings, saveDeptMapping, getDistinctDepts, getSummary, COST_CENTRES, };