first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+286
View File
@@ -0,0 +1,286 @@
// 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 },
// 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 },
// 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 },
];
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',
};
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,
};