'use strict'; // routes/notifications.js — aggregates "pending on me" items across the // workflows that already have clear, per-user pending logic (Work Order, // Production Order, Batch Issuance, BOM Requests, Production Deviations, // and — admin only — Password Reset requests), for the sidebar bell + // dashboard "Pending Actions" widget. Every item is gated by the same // approval-step/role check its own workflow route enforces, so a user only // ever sees what they're actually permitted to act on. Other approval // workflows (Purchase Requests, Customer/Vendor registration, Project // approvals, etc.) aren't included yet. const express = require('express'); const router = express.Router(); const { verifyToken, hasStepPerm, hasStepAssigned } = require('../middleware/auth'); const appSettings = require('../services/appSettingsStore'); // Mirrors routes/workOrders.js's WORK_ORDER_STEP_KEYS (index-aligned with // services/workOrderStore.js's STEPS) — duplicated here rather than // exported, matching how work-order.html itself already keeps its own copy. const WO_STEP_KEYS = ['prepared_qa', 'checked_qc', 'checked_production', 'checked_mgr_production', 'approved_mgr_qa']; const WO_STEPS = ['Prepared By QA', 'Checked By QC', 'Checked By Store In-Charge', 'Checked By (Manager Production)', 'Approved By (Manager QA)']; // Mirrors server.js's /api/config approvalMap for BOM Requests — which // ROLE is on turn for a given status, at the admin-configured BOM levels // (Settings → bomApprovalLevels; see services/appSettingsStore.js). function bomApprovalMap() { const levels = appSettings.bomApprovalLevels(); return { 2: { PENDING: 'manager', L1_APPROVED: 'sap_adder' }, 3: { PENDING: 'manager', L1_APPROVED: 'sr_manager', L2_APPROVED: 'sap_adder' }, 4: { PENDING: 'manager', L1_APPROVED: 'sr_manager', L2_APPROVED: 'sap_adder', L3_APPROVED: 'sap_adder' }, }[levels] || {}; } router.get('/pending', verifyToken, async (req, res) => { const user = req.user; const items = []; try { const wos = await require('../services/workOrderStore').listWorkOrders({ status: 'IN_PROGRESS' }); wos.forEach(w => { const key = WO_STEP_KEYS[w.stage]; if (key && hasStepPerm(user, `work_order:${key}`, 'approve')) { items.push({ module: 'work_order', label: 'Work Order', title: w.woNo, detail: `${WO_STEPS[w.stage]} — ${w.productName || w.productCode || ''}`, link: `/work-order?open=${w.id}`, id: w.id, cardHref: '/work-order', }); } }); } catch (e) { console.warn('[notifications] work order scan failed:', e.message); } try { const poStore = require('../services/productionOrderStore'); // Which standalone page each stage's action actually happens on — // Issue/Receipt/Close each have their own dedicated page (deep-linked // via #order=&company=, same hash convention // production.html's own action buttons already use to get there). // Release and Transfer to Finished Goods have no standalone page of // their own — those stay on production.html's detail popup. const STAGE_CARD_HREF = { release: '/production', issuance: '/issue-production', receipt: '/receipt-production', transfer_fg: '/production', close: '/close-production' }; // Consumable Orders (Release → Issue → Close only, no Receipt/Transfer to // FG) are gated by their OWN dedicated steps, never the general // production_order:* ones — mirrors services/productionOrderStore.js's // own notifyStepFor()/CONSUMABLE_STEP_FOR (not exported, so duplicated // here, same convention as WORK_ORDER_STEP_KEYS above). Without this, a // user holding the general 'production_order:release' step (but NOT // 'production_order:consumable_release') incorrectly saw every pending // Consumable Order in the bell too, even though the Production Order // page itself already correctly hides Consumable Orders from them. const CONSUMABLE_STEP_FOR = { release: 'consumable_release', issuance: 'consumable_issue', close: 'consumable_close' }; const pos = await poStore.listProductionOrders({ status: 'IN_PROGRESS' }); // REJECTED orders (receipt posted with rejection lines) still need to be // CLOSED — surface them to the close-step users too. const rejected = await poStore.listProductionOrders({ status: 'REJECTED' }); rejected.forEach(p => { pos.push(Object.assign({}, p, { stage: 4 })); }); // stage 4 = Close pos.forEach(p => { const generalKey = poStore.STEP_KEYS[p.stage]; const key = p.isConsumable ? (CONSUMABLE_STEP_FOR[generalKey] || null) : generalKey; // Same rule as the Issue/Receipt/Close pages themselves: being ASSIGNED // the step (any perm — view/add/edit/approve) means the action is yours, // so it must show in the bell too. (Was 'approve'-only, which hid e.g. // pending Receipts from users holding view/add/edit.) if (key && hasStepAssigned(user, `production_order:${key}`)) { const cardHref = STAGE_CARD_HREF[key] || '/production'; const link = p.sapAbsEntry ? (cardHref === '/production' ? `/production?open=${p.sapAbsEntry}` : `${cardHref}#order=${p.sapAbsEntry}&company=${encodeURIComponent(p.company || '')}`) : '/production'; items.push({ module: 'production_order', label: 'Production Order', title: p.sapDocNum ? `#${p.sapDocNum}` : `Item ${p.itemCode}`, detail: `${poStore.STEPS[p.stage]} — ${p.itemName || p.itemCode || ''}`, link, id: p.id, cardHref, }); } }); } catch (e) { console.warn('[notifications] production order scan failed:', e.message); } // Batch Issuance intimations awaiting a Work Order — pending for whoever // holds the "Prepared By QA" (create Work Order) step, same 'add' perm the // WO create route itself requires. An intimation counts as pending until // some Work Order references it (ZWORK_ORDERS.INTIMATION_ID). try { if (hasStepPerm(user, 'work_order:prepared_qa', 'add')) { const ints = await require('../services/batchIntimationStore').listIntimations({ status: 'SENT_TO_QA' }); const wos = await require('../services/workOrderStore').listWorkOrders({}); const used = new Set(wos.filter(w => w.intimationId).map(w => String(w.intimationId))); ints.forEach(bi => { if (used.has(String(bi.id))) return; const firstProd = Array.isArray(bi.products) && bi.products[0] ? (bi.products[0].productName || bi.products[0].name || bi.products[0].productCode || '') : ''; items.push({ module: 'batch_issuance', label: 'Batch Issuance', title: bi.docNo || ('#' + bi.id), detail: `${firstProd ? firstProd + ' — ' : ''}awaiting Work Order`, link: '/work-order', id: bi.id, cardHref: '/batch-issuance', }); }); } } catch (e) { console.warn('[notifications] batch issuance scan failed:', e.message); } try { const boms = await require('../services/bomRequestStore').listRequests({ status: 'ALL' }); const map = bomApprovalMap(); boms.forEach(b => { const turnRole = map[b.status]; if (turnRole && turnRole === user.role) { items.push({ module: 'bom', label: 'BOM Request', title: b.itemCode, detail: `${b.itemName || ''} — awaiting your review`, link: `/approvals?open=${b.id}`, id: b.id, cardHref: '/bom', }); } }); } catch (e) { console.warn('[notifications] bom scan failed:', e.message); } // Production Deviations awaiting QA sign-off — pending for whoever holds // 'approve' on production_order:deviation (see [[production-deviation-workflow]]). // Only QA_STATUS='PENDING' counts as pending; N_A (QA not required for this // one) / APPROVED / REJECTED are already resolved, nothing left to do. try { if (hasStepPerm(user, 'production_order:deviation', 'approve')) { const devs = await require('../services/deviationStore').listDeviations({}); devs.filter(d => d.qaStatus === 'PENDING').forEach(d => { items.push({ module: 'deviation', label: 'Deviation', title: d.sapDocNum ? `PWO #${d.sapDocNum}` : `Item ${d.itemCode}`, detail: `${d.type.charAt(0) + d.type.slice(1).toLowerCase()} — ${d.itemCode}${d.newItemCode ? ' → ' + d.newItemCode : ''} — awaiting QA`, link: d.sapAbsEntry ? `/production?open=${d.sapAbsEntry}` : '/deviations', id: d.id, cardHref: '/deviations', }); }); } } catch (e) { console.warn('[notifications] deviation scan failed:', e.message); } if (user.role === 'admin' || user.role === 'sap_adder') { try { const pending = await require('../services/passwordResetStore').listRequests('PENDING'); pending.forEach(r => { items.push({ module: 'password_reset', label: 'Password Reset', title: '@' + r.username, detail: r.note || 'Requested a password reset', link: `/admin?tab=pwresets&open=${r.id}`, id: r.id, cardHref: '/admin', }); }); } catch (e) { console.warn('[notifications] password reset scan failed:', e.message); } } res.json({ success: true, count: items.length, data: items }); }); module.exports = router;