'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;