first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
'use strict';
// routes/manpower.js — "Man Power" module. CRUD over the SAP User-Defined
// Object "Production_Data" (a Document UDO): one header per Date, with 10
// child grids (one per production area / tab). Everything goes through the
// SAP Service Layer entity /Production_Data, so SAP keeps the keys, numbering
// and integrity. SAP has delete disabled for this UDO (CanDelete=N) — removal
// is done via Cancel (CanCancel=Y).
const express = require('express');
const router = express.Router();
const { verifyToken, requireApprovalStep } = require('../middleware/auth');
let _sap = null;
function sap() { if (!_sap) _sap = require('../services/sapServiceLayer'); return _sap; }
const { getPool } = require('../services/sqlPool');
async function hana(sqlText) { const pool = await getPool(); return (await pool.request().query(sqlText)).recordset || []; }
const cq = (req) => req.query?.company || req.body?.company || null;
// Each tab's Section valid-values live in UFD1 (SAP's valid-values table) for
// that UDT, on the Section user field (FieldID 0).
const SQL_TABLE = {
bb: '@PRODUCTION_BB', packing: '@PRODUCTION_PACKING', autoclave: '@PRODUCTION_AUTO',
capd: '@PRODUCTION_CAPD', moulding: '@PRODUCTION_MOULDING', sheet: '@PRODUCTION_SHEET',
lamination: '@PRO_NEEDLE_LAMI', stent: '@PRODUCTION_STENT', equipment: '@PRODUCTION_EQUIP',
needle: '@PRO_NEEDLE',
};
// Tab → Service-Layer child collection, and the (mostly-shared) "present
// manpower" field name — Autoclave uniquely uses U_Prst_mp_star.
const TABS = [
{ key: 'bb', label: 'BB Production', coll: 'PRODUCTION_BBCollection', present: 'U_Prst_manpower_star' },
{ key: 'packing', label: 'Packing', coll: 'PRODUCTION_PACKINGCollection', present: 'U_Prst_manpower_star' },
{ key: 'autoclave', label: 'Autoclave', coll: 'PRODUCTION_AUTOCollection', present: 'U_Prst_mp_star' },
{ key: 'capd', label: 'CAPD Production', coll: 'PRODUCTION_CAPDCollection', present: 'U_Prst_manpower_star' },
{ key: 'moulding', label: 'Moulding', coll: 'PRODUCTION_MOULDINGCollection', present: 'U_Prst_manpower_star' },
{ key: 'sheet', label: 'Sheet Plant', coll: 'PRODUCTION_SHEETCollection', present: 'U_Prst_manpower_star' },
{ key: 'lamination', label: 'Lamination', coll: 'PRO_NEEDLE_LAMICollection', present: 'U_Prst_manpower_star' },
{ key: 'stent', label: 'Stent', coll: 'PRODUCTION_STENTCollection', present: 'U_Prst_manpower_star' },
{ key: 'equipment', label: 'Equipment', coll: 'PRODUCTION_EQUIPCollection', present: 'U_Prst_manpower_star' },
{ key: 'needle', label: 'Needle', coll: 'PRO_NEEDLECollection', present: 'U_Prst_manpower_star' },
];
function num(v) { const n = parseFloat(v); return isNaN(n) ? 0 : n; }
function lineHasData(l) {
if (!l) return false;
if ((l.section || '').trim() || (l.remarks || '').trim()) return true;
return ['target','appManpower','prntWorker','totalSaction','approvedEt','present','overtime','actual','rejection']
.some(k => num(l[k]) !== 0);
}
// UI line → Service-Layer line for a given tab
function toSL(tab, l) {
const o = {
U_Section: (l.section || '') || null,
U_Prd_Target: num(l.target),
U_App_Manpower: num(l.appManpower),
U_Prnt_Worker: num(l.prntWorker),
U_Total_saction: num(l.totalSaction),
U_Approved_et: num(l.approvedEt),
U_Overtime_star: num(l.overtime),
U_Actual_Production: num(l.actual),
U_Rejection: num(l.rejection),
U_Remarks: (l.remarks || '') || null,
};
o[tab.present] = num(l.present);
return o;
}
// Service-Layer line → UI line
function fromSL(tab, l) {
return {
section: l.U_Section || '',
target: l.U_Prd_Target || 0,
appManpower: l.U_App_Manpower || 0,
prntWorker: l.U_Prnt_Worker || 0,
totalSaction: l.U_Total_saction || 0,
approvedEt: l.U_Approved_et || 0,
present: l[tab.present] || 0,
overtime: l.U_Overtime_star || 0,
actual: l.U_Actual_Production || 0,
rejection: l.U_Rejection || 0,
remarks: l.U_Remarks || '',
};
}
// A row is "in use" (must be saved) if it has a Section OR any data.
// Completely blank rows are ignored.
const lineInUse = (l) => !!(l && ((l.section || '').trim() || lineHasData(l)));
// Build the /Production_Data payload from a { date, tabs } body.
// Every in-use row MUST have a Section — throws otherwise (nothing is saved).
function buildPayload(body) {
const payload = {};
if (body.date) payload.U_Date = body.date; // 'YYYY-MM-DD'
if (body.remark !== undefined) payload.Remark = body.remark || null;
for (const tab of TABS) {
const src = (body.tabs?.[tab.key] || []).filter(lineInUse);
src.forEach((l, i) => {
if (!(l.section || '').trim())
throw new Error(`Section is required — ${tab.label}, row ${i + 1}`);
});
payload[tab.coll] = src.map(l => toSL(tab, l)); // always send (empty replaces on edit)
}
const totalLines = TABS.reduce((n, t) => n + (payload[t.coll]?.length || 0), 0);
if (totalLines === 0) throw new Error('Add at least one row with a Section before saving.');
return payload;
}
// Expose the tab metadata so the frontend renders the right tabs/labels.
router.get('/tabs', verifyToken, (req, res) => {
res.json({ success: true, data: TABS.map(t => ({ key: t.key, label: t.label })) });
});
// Section dropdown values per tab, from UFD1 (SAP-defined valid values).
router.get('/sections', verifyToken, requireApprovalStep('man_power:entry', 'view'), async (req, res) => {
try {
const list = TABS.map(t => `'${SQL_TABLE[t.key]}'`).join(',');
const rows = await hana(`SELECT "TableID","FldValue" FROM [dbo]."UFD1" WHERE "TableID" IN (${list}) AND "FieldID"=0 ORDER BY "IndexID"`);
const byTable = {};
rows.forEach(r => { (byTable[r.TableID] = byTable[r.TableID] || []).push(r.FldValue); });
const out = {};
TABS.forEach(t => { out[t.key] = byTable[SQL_TABLE[t.key]] || []; });
res.json({ success: true, data: out });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// ── List (view) ───────────────────────────────────────────────────
router.get('/', verifyToken, requireApprovalStep('man_power:entry', 'view'), async (req, res) => {
const co = cq(req);
const top = Math.min(Number(req.query.top) || 20, 100);
const skip = Number(req.query.skip) || 0;
const date = (req.query.date || '').trim();
try {
let filter = '';
if (date) filter = `&$filter=U_Date eq '${date}T00:00:00Z'`;
const r = await sap().sapRequest('GET',
`Production_Data?$select=DocEntry,DocNum,U_Date,Canceled,CreateDate&$orderby=DocEntry desc&$top=${top}&$skip=${skip}${filter}`, null, co);
const docs = (r?.value || []).map(d => ({
docEntry: d.DocEntry, docNum: d.DocNum, date: d.U_Date,
canceled: d.Canceled === 'tYES' || d.Canceled === 'Y' || d.Canceled === true,
createDate: d.CreateDate, tabCounts: {}, totalRows: 0,
}));
// Row count per tab (section rows) for each listed document — one bulk
// query across all child tables. Non-fatal: on failure counts stay 0.
const ids = docs.map(d => d.docEntry).filter(x => x != null);
if (ids.length) {
try {
const idList = ids.join(',');
const unions = TABS.map(t =>
`SELECT '${t.key}' AS tab, "DocEntry" AS de, COUNT(*) AS n FROM [dbo].[${SQL_TABLE[t.key]}] WHERE "DocEntry" IN (${idList}) GROUP BY "DocEntry"`
).join(' UNION ALL ');
const rows = await hana(unions);
const byDoc = {};
rows.forEach(x => { (byDoc[x.de] = byDoc[x.de] || {})[x.tab] = Number(x.n) || 0; });
docs.forEach(d => { d.tabCounts = byDoc[d.docEntry] || {}; d.totalRows = Object.values(d.tabCounts).reduce((a, b) => a + b, 0); });
} catch (e) { console.warn('[MANPOWER] tab counts failed (non-fatal):', e.message); }
}
res.json({ success: true, data: docs });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// ── Single document with all tabs (view) ──────────────────────────
router.get('/:docEntry', verifyToken, requireApprovalStep('man_power:entry', 'view'), async (req, res) => {
const co = cq(req);
try {
const d = await sap().sapRequest('GET', `Production_Data(${parseInt(req.params.docEntry)})`, null, co);
const tabs = {};
for (const t of TABS) tabs[t.key] = (d[t.coll] || []).map(l => fromSL(t, l));
res.json({ success: true, data: {
docEntry: d.DocEntry, docNum: d.DocNum, date: d.U_Date, remark: d.Remark || '',
canceled: d.Canceled === 'tYES' || d.Canceled === 'Y' || d.Canceled === true, tabs,
} });
} catch (err) { res.status(404).json({ success: false, message: err.message }); }
});
// This user's allowed Man Power tabs (Admin → user → Man Power). Returns a Set,
// or null = no restriction (all tabs). Read fresh from DB.
async function allowedTabsFor(userId) {
try {
const u = await require('../services/hanaUsers').findById(userId);
const t = Array.isArray(u && u.manpowerTabs) ? u.manpowerTabs.map(String) : [];
return t.length ? new Set(t) : null;
} catch (_e) { return null; }
}
// ── Create (add) ──────────────────────────────────────────────────
router.post('/', verifyToken, requireApprovalStep('man_power:entry', 'add'), async (req, res) => {
const co = cq(req);
if (!req.body.date) return res.status(400).json({ success: false, message: 'Date is required' });
try {
// Restrict to this user's allowed tabs — drop any others they sent.
const allow = await allowedTabsFor(req.user.id);
if (allow) { req.body.tabs = req.body.tabs || {}; for (const t of TABS) if (!allow.has(t.key)) delete req.body.tabs[t.key]; }
const payload = buildPayload(req.body);
const r = await sap().sapRequest('POST', 'Production_Data', payload, co);
res.json({ success: true, data: { docEntry: r?.DocEntry, docNum: r?.DocNum } });
} catch (err) { res.status(400).json({ success: false, message: err.message }); }
});
// ── Update (edit) ─────────────────────────────────────────────────
router.patch('/:docEntry', verifyToken, requireApprovalStep('man_power:entry', 'edit'), async (req, res) => {
const co = cq(req);
try {
// Restrict to allowed tabs. PATCH replaces whole collections, so for tabs
// this user CAN'T fill we re-inject the doc's EXISTING rows (never wipe, and
// ignore whatever the client sent for those tabs).
const allow = await allowedTabsFor(req.user.id);
if (allow) {
const existing = await sap().sapRequest('GET', `Production_Data(${parseInt(req.params.docEntry)})`, null, co);
req.body.tabs = req.body.tabs || {};
for (const t of TABS) if (!allow.has(t.key)) req.body.tabs[t.key] = (existing[t.coll] || []).map(l => fromSL(t, l));
}
const payload = buildPayload(req.body);
// Replace the whole child collections instead of merging line-by-line.
await sap().sapRequest('PATCH', `Production_Data(${parseInt(req.params.docEntry)})`, payload, co, true,
{ 'B1S-ReplaceCollectionsOnPatch': 'true' });
res.json({ success: true });
} catch (err) { res.status(400).json({ success: false, message: err.message }); }
});
// ── Cancel (delete is disabled in SAP for this UDO) ───────────────
router.post('/:docEntry/cancel', verifyToken, requireApprovalStep('man_power:entry', 'edit'), async (req, res) => {
const co = cq(req);
try {
await sap().sapRequest('POST', `Production_Data(${parseInt(req.params.docEntry)})/Cancel`, null, co);
res.json({ success: true });
} catch (err) { res.status(400).json({ success: false, message: err.message }); }
});
module.exports = router;