227 lines
12 KiB
JavaScript
227 lines
12 KiB
JavaScript
'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;
|