'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); } } // Genuine total (same filter, no paging) — drives "Page X of Y" on the // client instead of an open-ended "Page X". let total = null; try { const c = await sap().sapRequest('GET', `Production_Data/$count${filter ? '?' + filter.slice(1) : ''}`, null, co); const n = Number(c); if (!isNaN(n)) total = n; } catch (e) { console.warn('[MANPOWER] $count failed — pager will show no total:', e.message); } res.json({ success: true, data: docs, total }); } 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;