177 lines
11 KiB
JavaScript
177 lines
11 KiB
JavaScript
'use strict';
|
|
// routes/prePwo.js — "Pre-PWO" staging (Admin → System Settings → "Pre-PWO —
|
|
// Store Review Before SAP"). See services/prePwoStore.js for the full design
|
|
// note. This file is ONLY the staging record's CRUD + the Production<->Store
|
|
// review round trip. Actually creating the real SAP Production Order still
|
|
// goes through the existing, unmodified routes/sap.js + routes/productionOrders.js
|
|
// endpoints — the frontend, after a successful SAP creation, calls
|
|
// POST /:id/mark-converted here purely to lock the staging row.
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const { verifyToken, hasStepPerm, hasStepAssigned } = require('../middleware/auth');
|
|
|
|
const store = () => require('../services/prePwoStore');
|
|
const poStore = () => require('../services/productionOrderStore');
|
|
const woStore = () => require('../services/workOrderStore');
|
|
const appSettings = require('../services/appSettingsStore');
|
|
|
|
function requireAnyStep(fullKeys, perm) {
|
|
return (req, res, next) => {
|
|
if (fullKeys.some(k => hasStepPerm(req.user, k, perm))) return next();
|
|
res.status(403).json({ success: false, message: `You are not assigned "${perm}" on approval step: ${fullKeys.join(' or ')}` });
|
|
};
|
|
}
|
|
// For viewing (list/get), holding ANY perm on ANY of these steps is enough —
|
|
// e.g. a Store user who only has 'add' checked on prepwo_review (no 'view')
|
|
// still needs to reach the list to find what's been shared with them.
|
|
function requireAnyStepAssigned(fullKeys) {
|
|
return (req, res, next) => {
|
|
if (fullKeys.some(k => hasStepAssigned(req.user, k))) return next();
|
|
res.status(403).json({ success: false, message: `You are not assigned to any of: ${fullKeys.join(', ')}` });
|
|
};
|
|
}
|
|
const VIEW_STEPS = ['production_order:create', 'production_order:prepwo_share', 'production_order:prepwo_review'];
|
|
|
|
router.get('/', verifyToken, requireAnyStepAssigned(VIEW_STEPS), async (req, res) => {
|
|
try {
|
|
const { mine, company, workOrderId, status } = req.query;
|
|
const data = await store().listPrePwos({ mine: mine === '1' ? req.user.username : undefined, company, workOrderId, status });
|
|
res.json({ success: true, data });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
router.get('/:id', verifyToken, requireAnyStepAssigned(VIEW_STEPS), async (req, res) => {
|
|
try {
|
|
const r = await store().findById(req.params.id);
|
|
if (!r || r.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
|
|
res.json({ success: true, data: r });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// Create the staging record — the app-DB-only substitute for what would
|
|
// otherwise be an immediate SAP write. Only from a Work Order (per the
|
|
// original ask: "After approval of WO"); manual/standalone Production
|
|
// Orders are unaffected by this feature and keep writing straight to SAP.
|
|
router.post('/', verifyToken, (req, res, next) => {
|
|
if (hasStepPerm(req.user, 'production_order:create', 'add')) return next();
|
|
res.status(403).json({ success: false, message: 'You are not assigned "add" on approval step: production_order:create' });
|
|
}, async (req, res) => {
|
|
try {
|
|
if (!appSettings.preWoStoreReviewEnabled())
|
|
return res.status(403).json({ success: false, message: 'The Pre-PWO Store Review workflow is disabled in Admin → System Settings' });
|
|
const b = req.body || {};
|
|
if (!b.workOrderId) return res.status(400).json({ success: false, message: 'workOrderId is required' });
|
|
const wo = await woStore().findById(b.workOrderId);
|
|
if (!wo || wo.isDeleted) return res.status(404).json({ success: false, message: 'Work order not found' });
|
|
|
|
// Same "one Production Order per Work Order" guarantee as the direct-to-SAP
|
|
// path (routes/sap.js), checked against BOTH a real PO already existing
|
|
// AND an open (not-yet-converted) Pre-PWO already staged for this WO.
|
|
const existingPo = await poStore().listProductionOrders({ workOrderId: b.workOrderId });
|
|
if (existingPo.length)
|
|
return res.status(409).json({ success: false, message: `A Production Order (${existingPo[0].sapDocNum || '#' + existingPo[0].id}) has already been generated for this Work Order.` });
|
|
const existingPre = await store().findOpenByWorkOrderId(b.workOrderId);
|
|
if (existingPre)
|
|
return res.status(409).json({ success: false, message: `A Pre-PWO is already staged for this Work Order (${existingPre.reviewStage === 0 ? 'not yet shared' : existingPre.reviewStage === 1 ? 'awaiting Store review' : 'reviewed by Store'}).` });
|
|
|
|
if (!Array.isArray(b.lines) || !b.lines.length)
|
|
return res.status(400).json({ success: false, message: 'At least one component line is required' });
|
|
|
|
const saved = await store().insertPrePwo({
|
|
workOrderId: b.workOrderId,
|
|
itemCode: b.itemCode || wo.productCode, itemName: b.itemName || wo.productName,
|
|
plannedQty: b.plannedQty || wo.totalUnits, batchNumber: b.batchNumber || wo.batchNumber,
|
|
mfgDate: b.mfgDate || wo.mfgDate, expDate: b.expDate || wo.expDate,
|
|
warehouse: b.warehouse || '', fgWarehouse: b.fgWarehouse || '',
|
|
lines: b.lines, remarks: b.remarks || '', company: b.company || '',
|
|
createdBy: req.user.username, createdByName: req.user.name || req.user.username,
|
|
});
|
|
res.json({ success: true, data: saved });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// Production edits header/lines directly (with or without ever involving
|
|
// Store) — any time before the Pre-PWO is converted to a real SAP order.
|
|
router.put('/:id', verifyToken, requireAnyStep(['production_order:create'], 'edit'), async (req, res) => {
|
|
try {
|
|
const existing = await store().findById(req.params.id);
|
|
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
|
|
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — locked' });
|
|
const b = req.body || {};
|
|
if (!Array.isArray(b.lines) || !b.lines.length)
|
|
return res.status(400).json({ success: false, message: 'At least one component line is required' });
|
|
const updated = await store().updatePrePwo(req.params.id, {
|
|
itemCode: b.itemCode ?? existing.itemCode, itemName: b.itemName ?? existing.itemName,
|
|
plannedQty: b.plannedQty ?? existing.plannedQty, batchNumber: b.batchNumber ?? existing.batchNumber,
|
|
mfgDate: b.mfgDate ?? existing.mfgDate, expDate: b.expDate ?? existing.expDate,
|
|
warehouse: b.warehouse ?? existing.warehouse, fgWarehouse: b.fgWarehouse ?? existing.fgWarehouse,
|
|
lines: b.lines, remarks: b.remarks ?? existing.remarks,
|
|
});
|
|
res.json({ success: true, data: updated });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// Production → Store (optional — no hard gate anywhere downstream).
|
|
router.post('/:id/share-with-store', verifyToken, requireAnyStep(['production_order:prepwo_share'], 'add'), async (req, res) => {
|
|
try {
|
|
if (!appSettings.preWoStoreReviewEnabled())
|
|
return res.status(403).json({ success: false, message: 'The Pre-PWO Store Review workflow is disabled in Admin → System Settings' });
|
|
const existing = await store().findById(req.params.id);
|
|
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
|
|
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — locked' });
|
|
if (existing.reviewStage !== 0)
|
|
return res.status(409).json({ success: false, message: `Already ${existing.reviewStage === 1 ? 'shared with Store, awaiting their review' : 'been through Store review'}` });
|
|
const updated = await store().shareWithStore(req.params.id, { by: req.user.username, byName: req.user.name || req.user.username });
|
|
res.json({ success: true, data: updated });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// Store → Production: may add/remove/substitute component lines outright —
|
|
// single round trip, no further back-and-forth.
|
|
router.post('/:id/revert-to-production', verifyToken, requireAnyStep(['production_order:prepwo_review'], 'add'), async (req, res) => {
|
|
try {
|
|
if (!appSettings.preWoStoreReviewEnabled())
|
|
return res.status(403).json({ success: false, message: 'The Pre-PWO Store Review workflow is disabled in Admin → System Settings' });
|
|
const existing = await store().findById(req.params.id);
|
|
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
|
|
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — locked' });
|
|
if (existing.reviewStage !== 1)
|
|
return res.status(409).json({ success: false, message: existing.reviewStage === 0 ? 'This Pre-PWO has not been shared by Production yet' : 'Already reverted to Production' });
|
|
const b = req.body || {};
|
|
if (!Array.isArray(b.lines) || !b.lines.length)
|
|
return res.status(400).json({ success: false, message: 'At least one component line is required' });
|
|
const updated = await store().revertToProduction(req.params.id, {
|
|
by: req.user.username, byName: req.user.name || req.user.username, lines: b.lines, remarks: b.remarks || '',
|
|
});
|
|
res.json({ success: true, data: updated });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
// Called by the frontend immediately after it has successfully created the
|
|
// real SAP Production Order (via the existing /api/sap/production-order +
|
|
// /api/production-orders routes, unchanged) from this Pre-PWO's current
|
|
// lines. Purely locks the staging row — does not itself touch SAP.
|
|
router.post('/:id/mark-converted', verifyToken, requireAnyStep(['production_order:create'], 'add'), async (req, res) => {
|
|
try {
|
|
const existing = await store().findById(req.params.id);
|
|
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
|
|
if (existing.status === 'CONVERTED') return res.json({ success: true, data: existing });
|
|
const { convertedPoId } = req.body || {};
|
|
if (!convertedPoId) return res.status(400).json({ success: false, message: 'convertedPoId is required' });
|
|
const updated = await store().markConverted(req.params.id, { convertedPoId });
|
|
res.json({ success: true, data: updated });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
router.delete('/:id', verifyToken, requireAnyStep(['production_order:create'], 'delete'), async (req, res) => {
|
|
try {
|
|
const existing = await store().findById(req.params.id);
|
|
if (!existing) return res.status(404).json({ success: false, message: 'Not found' });
|
|
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — cannot delete' });
|
|
await store().softDelete(req.params.id);
|
|
res.json({ success: true });
|
|
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
|
|
});
|
|
|
|
module.exports = router;
|