Files
sap-erp/routes/notifications.js
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

170 lines
9.1 KiB
JavaScript

'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=<AbsoluteEntry>&company=<co>, 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;