309 lines
19 KiB
JavaScript
309 lines
19 KiB
JavaScript
// services/approvalStepsStore.js
|
|
// Generic Approval Step registry: a catalogue of steps grouped by workflow,
|
|
// used to grant users fine-grained "which step can this person act on"
|
|
// permissions (services/hanaUsers.js stores the per-user assignment array).
|
|
// Built-in workflows are seeded on bootstrap (idempotent); admins can also
|
|
// add custom steps to a NEW workflow name via the API.
|
|
'use strict';
|
|
const { getPool } = require('./appSqlPool');
|
|
|
|
const TABLE = `[dbo].[ZAPPROVAL_STEPS]`;
|
|
|
|
// Built-in steps, seeded once. STEP_KEY must be unique within a workflow.
|
|
const BUILTIN_STEPS = [
|
|
// Work Order — 5-step QA/Production sign-off chain
|
|
{ workflow: 'work_order', key: 'prepared_qa', label: 'Prepared By QA', order: 1 },
|
|
{ workflow: 'work_order', key: 'checked_qc', label: 'Checked By QC', order: 2 },
|
|
{ workflow: 'work_order', key: 'checked_production', label: 'Checked By Store In-Charge', order: 3 },
|
|
{ workflow: 'work_order', key: 'checked_mgr_production', label: 'Checked By (Manager Production)', order: 4 },
|
|
{ workflow: 'work_order', key: 'approved_mgr_qa', label: 'Approved By (Manager QA)', order: 5 },
|
|
// Corrective, NOT part of the sequential 5-step chain above (same pattern as
|
|
// production_order's return_components): a portal-only sign-off performed
|
|
// AFTER a linked Production Order has been Issued in SAP — there is no such
|
|
// process in SAP itself. The verifier's signature is captured on the printed
|
|
// Work Order's "Verified By" column. Distinct from production_order:verify
|
|
// (kept, unused, for possible future use) — this is the one actually wired
|
|
// to the "Verify Work Order" screen (public/verify-work-order.html).
|
|
{ workflow: 'work_order', key: 'verify', label: 'Verify Work Order (post-Issuance sign-off)', order: 6 },
|
|
// Same idea as 'verify' above — a portal-only manual sign-off, independent
|
|
// of whoever performed the actual "Receipt from Production" SAP step. Lets
|
|
// a designated person (e.g. store keeper physically receiving the batch)
|
|
// record "Received By" for the printed Work Order, overriding the
|
|
// SAP-step-derived value. Managed from the same "Verify Work Order" screen.
|
|
{ workflow: 'work_order', key: 'receive', label: 'Received By (post-Issuance sign-off)', order: 7 },
|
|
// Per-row (per material line) sign-off — NOT order-wide. 'edit' lets the
|
|
// issuer fill in that row's Weighing Balance ID / AR No. at the moment of
|
|
// physically issuing it; 'approve' lets them stamp "Issued By/Date" on that
|
|
// one row. See public/verify-work-order.html — each row has its own
|
|
// Issued/Received/Verified stamps since different items can be issued,
|
|
// received and verified on different days by different people.
|
|
{ workflow: 'work_order', key: 'issue', label: 'Issued By (per-row post-Issuance sign-off)', order: 8 },
|
|
// Send back to QA — previously Admin/System Admin role ONLY, no
|
|
// assignable step at all: recalls a FULLY APPROVED Work Order back to the
|
|
// first QA step for editing (re-uses the normal Reject status, so once
|
|
// edited it restarts the full 5-step chain from Prepared By QA — see
|
|
// routes/workOrders.js's POST /:id/send-to-qa). Now also grantable to a
|
|
// regular user via 'approve' on this key; admin/system_admin still always
|
|
// bypass regardless.
|
|
{ workflow: 'work_order', key: 'send_to_qa', label: 'Send back to QA (recall a fully approved WO for editing)', order: 9 },
|
|
// BOM — up to 4 levels (BOM_APPROVAL_LEVELS controls how many are actually used)
|
|
{ workflow: 'bom', key: 'level1', label: 'Level 1 (Manager)', order: 1 },
|
|
{ workflow: 'bom', key: 'level2', label: 'Level 2 (Sr. Manager)', order: 2 },
|
|
{ workflow: 'bom', key: 'level3', label: 'Level 3 (SAP Adder)', order: 3 },
|
|
{ workflow: 'bom', key: 'level4', label: 'Level 4 (SAP Adder — Final)', order: 4 },
|
|
// Customer / Vendor registration — shared shape for both
|
|
{ workflow: 'customer_vendor', key: 'verify', label: 'Verify (Pending → Verified)', order: 1 },
|
|
{ workflow: 'customer_vendor', key: 'approve', label: 'Final Approval (Verified → SAP B1)', order: 2 },
|
|
// Project approvals — sequential department chain
|
|
{ workflow: 'project', key: 'purchase', label: 'Purchase', order: 1 },
|
|
{ workflow: 'project', key: 'engineering', label: 'Engineering', order: 2 },
|
|
{ workflow: 'project', key: 'qa', label: 'QA', order: 3 },
|
|
{ workflow: 'project', key: 'qc', label: 'QC', order: 4 },
|
|
{ workflow: 'project', key: 'legal', label: 'Legal', order: 5 },
|
|
{ workflow: 'project', key: 'owner', label: 'Project Owner', order: 6 },
|
|
{ workflow: 'project', key: 'plant', label: 'Plant Manager', order: 7 },
|
|
{ workflow: 'project', key: 'finance', label: 'Finance & Accounts', order: 8 },
|
|
// Production Order — created FROM a Work Order, then a 6-step SAP B1
|
|
// production lifecycle. Each step's 'approve' perm gates performing that
|
|
// step; 'add' on 'create'/'manual_create' gates who can originate an order.
|
|
// Split into two distinct steps so an admin can allow one without the
|
|
// other — e.g. force everyone through the Work Order-linked flow only,
|
|
// with no standalone/manual entry at all.
|
|
{ workflow: 'production_order', key: 'create', label: 'Create (from Work Order)', order: 1 },
|
|
{ workflow: 'production_order', key: 'manual_create', label: 'Create (Manual Entry, no Work Order)', order: 2 },
|
|
{ workflow: 'production_order', key: 'release', label: 'Release', order: 3 },
|
|
{ workflow: 'production_order', key: 'issuance', label: 'Issuance', order: 4 },
|
|
{ workflow: 'production_order', key: 'receipt', label: 'Receipt from Production', order: 5 },
|
|
{ workflow: 'production_order', key: 'transfer_fg', label: 'Transfer to Finished Goods', order: 6 },
|
|
{ workflow: 'production_order', key: 'close', label: 'Close', order: 7 },
|
|
// Corrective side-action, NOT part of the sequential stage chain above:
|
|
// after Issuance, defective component quantities can be returned to stock
|
|
// from the Receipt from Production page (SAP's "Return Components").
|
|
{ workflow: 'production_order', key: 'return_components', label: 'Return Components (defective, back to stock)', order: 8 },
|
|
// Portal-only sign-off (NOT a SAP transaction): after Issuance, a separate
|
|
// person verifies the issued production order. Their signature is captured on
|
|
// the printed Work Order's "Verified By" column. 'view' shows the Verify
|
|
// screen; 'approve' performs the verification.
|
|
{ workflow: 'production_order', key: 'verify', label: 'Verify (post-Issuance sign-off)', order: 9 },
|
|
// Corrective side-action, NOT part of the sequential stage chain: after
|
|
// Issuance, production may hit a Shortage (top up qty), need a
|
|
// Substitution (issue a different item instead), or a Damage/Loss
|
|
// (write off a damaged qty, optionally to a scrap warehouse) — without
|
|
// stopping the run. 'add' gates who can raise one; 'approve' gates the
|
|
// QA post-hoc sign-off (only relevant when Admin → System Settings →
|
|
// "Deviation — Require QA Approval" is on; the SAP posting itself always
|
|
// happens immediately on raise, regardless of that setting).
|
|
{ workflow: 'production_order', key: 'deviation', label: 'Deviation (Shortage / Substitution / Damage)', order: 10 },
|
|
// Pre-PWO Store Review (optional, Admin → System Settings → "Pre-PWO —
|
|
// Store Review Before SAP" — a whole-feature on/off switch, since it may
|
|
// not be needed at every site). When enabled, "Create (from Work Order)"
|
|
// first stages a portal-only Pre-PWO instead of writing to SAP; Production
|
|
// can then optionally share it with Store — who may add/remove/substitute
|
|
// component lines outright — before reverting it back (a single round
|
|
// trip). Actually creating the staging record AND pushing the final
|
|
// Pre-PWO to SAP both stay gated on the existing 'create' step above
|
|
// (same authority as today, just relocated); these two steps only gate
|
|
// the NEW capability of the Store detour itself. 'add' on 'prepwo_share'
|
|
// gates who can share; 'add' on 'prepwo_review' gates who can revert.
|
|
{ workflow: 'production_order', key: 'prepwo_share', label: 'Pre-PWO — Share with Store (before SAP)', order: 11 },
|
|
{ workflow: 'production_order', key: 'prepwo_review', label: 'Pre-PWO — Store Review & Revert', order: 12 },
|
|
// The "+ PWO" shortcut on a Released order's component table: raises a
|
|
// separate, standalone Production Order for a component that is itself an
|
|
// SFG with its own BOM, pre-filled with that line's exact required qty.
|
|
// Deliberately its own step (not reusing 'manual_create') so an admin can
|
|
// hand out this one narrow shortcut without granting full freeform Manual
|
|
// Entry creation, or vice versa.
|
|
{ workflow: 'production_order', key: 'create_from_component', label: '"+ PWO" — Create from a Component (SFG sub-assembly)', order: 13 },
|
|
// "Consumable Order" — a standalone manual Production Order restricted to
|
|
// Service items (SAP Item Group 132) only, e.g. for consumable/tooling
|
|
// items that need their own PWO but aren't part of a normal finished-goods
|
|
// BOM chain. Its own steps (not 'manual_create'/the general per-stage
|
|
// steps) so an admin can hand out each narrow capability independently —
|
|
// 'add' here = create one; Release/Issue/Close each get their OWN
|
|
// dedicated step below so a user can hold any subset (e.g. Issue only)
|
|
// without gaining the others or bleeding into the general
|
|
// production_order:release/issuance/close steps used by normal PWOs (and
|
|
// vice versa). No creator bypass — even the order's own creator needs the
|
|
// matching step to progress it further. Consumable Orders never go through
|
|
// Receipt (confirmed workflow is Release → Issue → Close only), so there
|
|
// is deliberately no consumable Receipt step at all.
|
|
{ workflow: 'production_order', key: 'consumable_create', label: 'Consumable Order — Add (Manual Entry, Service items only, Group 132)', order: 14 },
|
|
{ workflow: 'production_order', key: 'consumable_release', label: 'Consumable Order — Release', order: 15 },
|
|
{ workflow: 'production_order', key: 'consumable_issue', label: 'Consumable Order — Issue', order: 16 },
|
|
{ workflow: 'production_order', key: 'consumable_close', label: 'Consumable Order — Close', order: 17 },
|
|
// Cancel — previously Admin/System Admin role ONLY, no assignable step at
|
|
// all (an explicit, deliberate restriction — see routes/sap.js's
|
|
// isPoAdminOverride()). Now also grantable to a regular user like any
|
|
// other step, via 'approve' on this key; admin/system_admin still always
|
|
// bypass regardless, same as every other production_order step.
|
|
{ workflow: 'production_order', key: 'cancel', label: 'Cancel (bypasses all normal Close checks — irreversible)', order: 18 },
|
|
// Man Power — single entry step; its view/add/edit perms gate the whole
|
|
// Man Power module CRUD (routes/manpower.js). 'edit' also gates Cancel.
|
|
{ workflow: 'man_power', key: 'entry', label: 'Man Power Data Entry', order: 1 },
|
|
// OEE — single entry step; its view/add/edit perms gate the whole Overall
|
|
// Equipment Efficiency module CRUD (routes/oee.js). 'edit' also gates Cancel.
|
|
{ workflow: 'oee', key: 'entry', label: 'OEE Data Entry', order: 1 },
|
|
// Requirement — general CRUD gate (single entry step, same pattern as
|
|
// man_power:entry / oee:entry): 'view' gates seeing the Requirements page
|
|
// (list + Calculator) at all; 'add' gates creating a Requirement (and the
|
|
// Calculator's "send to New Requirement"); 'edit' gates editing one;
|
|
// 'delete' gates deleting one. Previously this was all-or-nothing via the
|
|
// 'production-requirements' module checkbox alone — see backfillRequirementManagePerm()
|
|
// in services/hanaUsers.js, which copies existing module access onto this
|
|
// step on deploy so nobody regresses.
|
|
{ workflow: 'requirement', key: 'manage', label: 'Create / Edit / Delete Requirements & Calculator', order: 1 },
|
|
// Requirement — Store Review Workflow (optional, Admin → System Settings →
|
|
// "Requirement — Store Review Workflow" — a whole-feature on/off switch
|
|
// separate from these permission steps, since it may not be needed at
|
|
// every site). A single round trip: Production shares a Requirement with
|
|
// Store, Store cross-checks it against SAP's own MRP Wizard output and
|
|
// edits the line items, then reverts it back to Production — who then
|
|
// raises the Batch Intimation as normal. 'add' on 'production_review'
|
|
// gates who can share; 'add' on 'store_review' gates who can revert.
|
|
{ workflow: 'requirement', key: 'production_review', label: 'Production — Share with Store', order: 2 },
|
|
{ workflow: 'requirement', key: 'store_review', label: 'Store — Review & Revert', order: 3 },
|
|
// Rejection Register — a line/shift user counts in-process rejections
|
|
// against a batch for one stage (EBB/Sheet Welding/Moulding/…), records
|
|
// rework, and submits; QA may then edit a submitted entry (the wizard
|
|
// itself is a one-way "submit" once done). Root-cause assignment (which
|
|
// physical/process cause each rejection cause rolls up to) is its own
|
|
// step so it can be handed to a QA lead separately from day-to-day QA
|
|
// editing. 'entry' with 'view' also gates seeing the Analytics screen —
|
|
// there's no separate analytics-only step, since anyone submitting/editing
|
|
// rejections needs to see the same numbers.
|
|
{ workflow: 'rejection_register', key: 'entry', label: 'Rejection Register — Entry (count & submit)', order: 1 },
|
|
{ workflow: 'rejection_register', key: 'qa_edit', label: 'Rejection Register — QA Edit After Submit', order: 2 },
|
|
{ workflow: 'rejection_register', key: 'causes_setup', label: 'Rejection Register — Root Cause Setup', order: 3 },
|
|
// Production Planning — entry step (view/add/edit/delete gate the plain
|
|
// CRUD list, routes/productionPlanning.js) plus a separate approve step:
|
|
// whoever holds 'approve' on production_planning:approve can sign off a
|
|
// plan, stamping Approved By/Date on it (used in the Excel export).
|
|
{ workflow: 'production_planning', key: 'entry', label: 'Production Planning Data Entry', order: 1 },
|
|
{ workflow: 'production_planning', key: 'approve', label: 'Production Planning Approval', order: 2 },
|
|
// Sales Order module (replacement for the msale portal) — order workflow,
|
|
// sample requests and masters. Defined in services/sales/constants.js
|
|
// (also the source of the per-"Sales User Type" default templates).
|
|
...require('./sales/constants').STEPS,
|
|
];
|
|
|
|
const WORKFLOW_LABELS = {
|
|
work_order: 'Work Order',
|
|
bom: 'BOM Approvals',
|
|
customer_vendor: 'Customer / Vendor Registration',
|
|
project: 'Project Approvals',
|
|
production_order: 'Production Order',
|
|
man_power: 'Man Power',
|
|
oee: 'Overall Equipment Efficiency',
|
|
requirement: 'Requirements',
|
|
rejection_register: 'Rejection Register',
|
|
production_planning: 'Production Planning',
|
|
sales_order: 'Sales Order',
|
|
sales_sample: 'Sample Request (Domestic)',
|
|
sales_export_sample: 'Sample Request (Export)',
|
|
sales_master: 'Sales Masters',
|
|
};
|
|
|
|
async function exec(sqlQuery, params = []) {
|
|
const pool = await getPool();
|
|
const req = pool.request();
|
|
let i = 0;
|
|
const text = sqlQuery.replace(/\?/g, () => { const n = `p${i}`; req.input(n, params[i]); i++; return `@${n}`; });
|
|
const result = await req.query(text);
|
|
return result.recordset || [];
|
|
}
|
|
|
|
function isAlreadyExists(e) {
|
|
const m = (e.message || '').toLowerCase();
|
|
return m.includes('already exists') || m.includes('there is already an object');
|
|
}
|
|
|
|
async function bootstrap() {
|
|
console.log('[APPROVAL-STEPS] Checking table', TABLE, '...');
|
|
await exec(`
|
|
CREATE TABLE ${TABLE} (
|
|
ID INT IDENTITY(1,1) PRIMARY KEY,
|
|
WORKFLOW NVARCHAR(50) NOT NULL,
|
|
STEP_KEY NVARCHAR(50) NOT NULL,
|
|
STEP_LABEL NVARCHAR(150) NOT NULL,
|
|
STEP_ORDER INT DEFAULT 1,
|
|
IS_CUSTOM BIT DEFAULT 0,
|
|
CREATED_BY NVARCHAR(50),
|
|
CREATED_AT DATETIME2,
|
|
CONSTRAINT UQ_APPROVAL_STEP UNIQUE (WORKFLOW, STEP_KEY)
|
|
)
|
|
`).catch(e => { if (!isAlreadyExists(e)) throw e; });
|
|
|
|
// Seed built-ins (idempotent — skip any that already exist)
|
|
for (const s of BUILTIN_STEPS) {
|
|
await exec(
|
|
`IF NOT EXISTS (SELECT 1 FROM ${TABLE} WHERE WORKFLOW=? AND STEP_KEY=?)
|
|
INSERT INTO ${TABLE} (WORKFLOW, STEP_KEY, STEP_LABEL, STEP_ORDER, IS_CUSTOM, CREATED_AT)
|
|
VALUES (?,?,?,?,0,SYSUTCDATETIME())`,
|
|
[s.workflow, s.key, s.workflow, s.key, s.label, s.order]
|
|
).catch(e => console.warn('[APPROVAL-STEPS] seed failed for', s.workflow, s.key, e.message));
|
|
}
|
|
|
|
// One-time relabel: "Checked By Production" → "Checked By Store In-Charge".
|
|
// The seed loop above only INSERTs when a row is missing, so an
|
|
// already-seeded row from before this rename would otherwise keep
|
|
// showing the old label forever in the admin permission checkboxes. Only
|
|
// touches the built-in row (IS_CUSTOM=0) and only if it still has the old
|
|
// text — never overwrites an admin's own custom step with this key.
|
|
await exec(
|
|
`UPDATE ${TABLE} SET STEP_LABEL=? WHERE WORKFLOW='work_order' AND STEP_KEY='checked_production' AND IS_CUSTOM=0 AND STEP_LABEL=?`,
|
|
['Checked By Store In-Charge', 'Checked By Production']
|
|
).catch(e => console.warn('[APPROVAL-STEPS] checked_production relabel failed:', e.message));
|
|
|
|
console.log('[APPROVAL-STEPS] ✅ Ready');
|
|
}
|
|
|
|
function fromRow(row) {
|
|
return {
|
|
id: row.ID,
|
|
workflow: row.WORKFLOW,
|
|
key: row.STEP_KEY,
|
|
label: row.STEP_LABEL,
|
|
order: row.STEP_ORDER,
|
|
isCustom: !!row.IS_CUSTOM,
|
|
fullKey: `${row.WORKFLOW}:${row.STEP_KEY}`,
|
|
};
|
|
}
|
|
|
|
async function listAll() {
|
|
const rows = await exec(`SELECT * FROM ${TABLE} ORDER BY WORKFLOW, STEP_ORDER, ID`);
|
|
return rows.map(fromRow);
|
|
}
|
|
|
|
async function listGrouped() {
|
|
const all = await listAll();
|
|
const workflows = [...new Set(all.map(s => s.workflow))];
|
|
return workflows.map(w => ({
|
|
workflow: w,
|
|
label: WORKFLOW_LABELS[w] || w,
|
|
steps: all.filter(s => s.workflow === w),
|
|
}));
|
|
}
|
|
|
|
// Create a custom step. `workflow` may be an existing one or a brand-new
|
|
// custom workflow name (admin-defined, for future use cases).
|
|
async function createCustomStep({ workflow, key, label, order, createdBy }) {
|
|
const wf = String(workflow || '').trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
const k = String(key || '').trim().toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
if (!wf || !k || !label) throw new Error('workflow, key and label are required');
|
|
await exec(
|
|
`INSERT INTO ${TABLE} (WORKFLOW, STEP_KEY, STEP_LABEL, STEP_ORDER, IS_CUSTOM, CREATED_BY, CREATED_AT)
|
|
VALUES (?,?,?,?,1,?,SYSUTCDATETIME())`,
|
|
[wf, k, label, parseInt(order) || 99, createdBy || null]
|
|
);
|
|
return { workflow: wf, key: k, label, fullKey: `${wf}:${k}` };
|
|
}
|
|
|
|
async function deleteCustomStep(id) {
|
|
await exec(`DELETE FROM ${TABLE} WHERE ID = ? AND IS_CUSTOM = 1`, [parseInt(id)]);
|
|
}
|
|
|
|
module.exports = {
|
|
bootstrap, listAll, listGrouped, createCustomStep, deleteCustomStep,
|
|
WORKFLOW_LABELS, BUILTIN_STEPS,
|
|
};
|