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

259 lines
9.7 KiB
JavaScript

'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,
};