// backend/routes/itemApproval.js 'use strict'; const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); let _store = null; function getStore() { if (!_store) _store = require('../services/hanaItemStore'); return _store; } let _sapSvc = null; function getSap() { if (!_sapSvc) _sapSvc = require('../services/sapServiceLayer'); return _sapSvc; } const appSettings = () => require('../services/appSettingsStore'); // admin/sap_adder/system_admin may push directly and approve/reject others' // submissions. Kept as one helper so the client (public/approvals.html's // canManageItems()) and server always agree on who this is. function isItemApprover(user) { return user?.role === 'admin' || user?.role === 'sap_adder' || user?.role === 'system_admin'; } const cq = (req) => req.query?.company || req.body?.company || null; async function pushItemToSap(data, co) { const sap = getSap(); const payload = buildSapPayload(data); await sap.sapRequest('POST', 'Items', payload, co); if (data.DefaultWarehouse) { try { await sap.sapRequest('PATCH', `Items('${encodeURIComponent(payload.ItemCode)}')`, { DefaultWarehouse: data.DefaultWarehouse }, co ); } catch (we) { console.warn('[ITEM-APPROVAL] DefaultWarehouse PATCH failed (non-fatal):', we.message); } } return payload; } // ── Build SAP item payload from stored itemData ──────────────────────────────── function buildSapPayload(data) { const d = data || {}; const payload = { ItemCode: (d.ItemCode || '').trim().toUpperCase(), ItemName: (d.ItemName || '').trim(), ItemType: d.ItemType || 'itItems', ItemClass: d.ItemClass || 'itcMaterial', InventoryItem: d.InventoryItem || 'tYES', SalesItem: d.SalesItem || 'tYES', PurchaseItem: d.PurchaseItem || 'tYES', VatLiable: 'tYES', Valid: 'tYES', Frozen: d.Frozen || 'tNO', ManageSerialNumbers: d.ManageSerialNumbers || 'tNO', ManageBatchNumbers: d.ManageBatchNumbers || 'tNO', SRIAndBatchManageMethod: d.SRIAndBatchManageMethod || 'bomm_OnEveryTransaction', WTLiable: d.WTLiable || 'tNO', NoDiscounts: d.NoDiscounts || 'tNO', TreeType: d.TreeType || 'iNotATree', AssetItem: 'tNO', GLMethod: d.GLMethod || 'glm_ItemClass', CostAccountingMethod: d.CostAccountingMethod || 'bis_MovingAverage', TaxType: 'tt_Yes', IssueMethod: d.IssueMethod || 'im_Manual', PlanningSystem: d.PlanningSystem || 'bop_None', ProcurementMethod: d.ProcurementMethod || 'bom_Buy', ComponentWarehouse: d.ComponentWarehouse || 'bomcw_BOM', MaterialType: d.MaterialType || 'mt_FinishedGoods', ProductSource: d.ProductSource || '0', ManageStockByWarehouse: 'tNO', InCostRollup: d.InCostRollup || 'tYES', }; if (d.ItemsGroupCode) payload.ItemsGroupCode = parseInt(d.ItemsGroupCode); if (d.UoMGroupEntry) payload.UoMGroupEntry = parseInt(d.UoMGroupEntry); if (d.InventoryUOM) payload.InventoryUOM = String(d.InventoryUOM).toUpperCase(); if (d.SalesUnit) payload.SalesUnit = String(d.SalesUnit).toUpperCase(); if (d.PurchaseUnit) payload.PurchaseUnit = String(d.PurchaseUnit).toUpperCase(); // accept both form-style keys and frontend SAP-style keys const gst = d.GSTRelevant || d.GSTRelevnt; if (gst) payload.GSTRelevnt = gst; if (d.GSTTaxCategory) payload.GSTTaxCategory = d.GSTTaxCategory; if (d.Excisable) payload.Excisable = d.Excisable; if (d.AssessableValue) payload.AssessableValue = Number(d.AssessableValue) || 0; if (d.HsnCode && /^\d+$/.test(String(d.HsnCode))) payload.ChapterID = parseInt(d.HsnCode); else if (d.ChapterID) payload.ChapterID = d.ChapterID; if (d.DefaultWarehouse) payload.DefaultWarehouse = d.DefaultWarehouse; if (d.DefaultVendor) payload.Mainsupplier = d.DefaultVendor; else if (d.Mainsupplier) payload.Mainsupplier = d.Mainsupplier; if (d.LeadTime) payload.LeadTime = parseInt(d.LeadTime); if (d.UserText) payload.User_Text = d.UserText; else if (d.User_Text) payload.User_Text = d.User_Text; if (d.ProductionStandardCost) payload.ProdStdCost = Number(d.ProductionStandardCost) || 0; else if (d.ProdStdCost) payload.ProdStdCost = Number(d.ProdStdCost) || 0; if (d.PurchaseQtyPerPackage) payload.PurchaseQtyPerPackUnit = Number(d.PurchaseQtyPerPackage) || 1; else if (d.PurchaseQtyPerPackUnit) payload.PurchaseQtyPerPackUnit = Number(d.PurchaseQtyPerPackUnit) || 1; if (d.PurchaseItemsPerUnit) payload.PurchaseItemsPerUnit = Number(d.PurchaseItemsPerUnit) || 1; if (d.SalesQtyPerPackage) payload.SalesQtyPerPackUnit = Number(d.SalesQtyPerPackage) || 1; else if (d.SalesQtyPerPackUnit) payload.SalesQtyPerPackUnit = Number(d.SalesQtyPerPackUnit) || 1; if (d.MinimumOrderQuantity) payload.MinOrderQuantity = Number(d.MinimumOrderQuantity) || 0; else if (d.MinOrderQuantity) payload.MinOrderQuantity = Number(d.MinOrderQuantity) || 0; if (d.MinInventory !== undefined) payload.MinInventory = Number(d.MinInventory) || 0; if (d.MaxInventory !== undefined) payload.MaxInventory = Number(d.MaxInventory) || 0; if (d.DesiredInventory !== undefined) payload.DesiredInventory = Number(d.DesiredInventory) || 0; // Remove blank/null keys Object.keys(payload).forEach(k => { if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k]; }); return payload; } // ── POST /api/item-approvals/submit ───────────────────────────────────────── // admin/sap_adder/system_admin → push directly to SAP (no approval queue) // all other roles → save as PENDING for admin review // Admin → System Settings → "Item Approval Workflow": when OFF, the whole // queue is bypassed and EVERYONE pushes directly (there's no one left who'd // review a pending item) — same as an approver submitting, regardless of role. router.post('/submit', verifyToken, async (req, res) => { try { const store = getStore(); const b = req.body; if (!b.ItemCode || !b.ItemName) { return res.status(400).json({ success: false, message: 'ItemCode and ItemName are required' }); } const { company, ...itemData } = b; const itemCode = b.ItemCode.trim().toUpperCase(); const itemName = b.ItemName.trim(); const co = company || null; const baseItem = { itemCode, itemName, itemData, submittedBy: req.user.username, submittedByName: req.user.name || req.user.username, company: company || '', }; if (isItemApprover(req.user) || !appSettings().itemApprovalEnabled()) { const payload = await pushItemToSap(itemData, co); const logEntry = { username: req.user.username, name: req.user.name || req.user.username, role: req.user.role, action: 'approve', comment: 'Direct push', timestamp: new Date().toISOString(), }; const request = await store.insertItem({ ...baseItem, status: 'PENDING', approvalLog: [logEntry] }); await store.updateItem(request.id, { status: 'SAP_PUSHED', approvedBy: req.user.username, approvedAt: new Date().toISOString(), sapItemCode: payload.ItemCode, }); console.log(`[ITEM-APPROVAL] ✅ Direct push — Item ${payload.ItemCode} created in SAP`); return res.json({ success: true, message: `Item ${payload.ItemCode} created in SAP B1!`, id: request.id, status: 'SAP_PUSHED', itemCode: payload.ItemCode }); } // user / manager / sr_manager — queue for approval const request = await store.insertItem({ ...baseItem, status: 'PENDING', approvalLog: [] }); res.json({ success: true, message: 'Item submitted for admin approval', id: request.id, status: 'PENDING' }); } catch (err) { console.error('[ITEM-APPROVAL] submit error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); // ── GET /api/item-approvals ────────────────────────────────────────────────── router.get('/', verifyToken, async (req, res) => { try { const store = getStore(); const status = (req.query.status || 'ALL').toUpperCase(); const company = cq(req); // 'user' role can only see their own submissions const submittedBy = req.user.role === 'user' ? req.user.username : null; const data = await store.listByStatus(status, company, submittedBy); res.json({ success: true, data }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ── GET /api/item-approvals/:id ────────────────────────────────────────────── router.get('/:id', verifyToken, async (req, res) => { try { const item = await getStore().findById(req.params.id); if (!item) return res.status(404).json({ success: false, message: 'Not found' }); res.json({ success: true, data: item }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } }); // ── PATCH /api/item-approvals/:id/action ──────────────────────────────────── // action: 'approve' | 'reject' // Admin can also pass editedData to override item fields before pushing router.patch('/:id/action', verifyToken, async (req, res) => { if (!isItemApprover(req.user)) { return res.status(403).json({ success: false, message: 'Only admin/SAP Adder/system admin can approve items' }); } try { const store = getStore(); const item = await store.findById(req.params.id); if (!item) return res.status(404).json({ success: false, message: 'Not found' }); const { action, comment, editedData } = req.body; if (!['approve', 'reject'].includes(action)) { return res.status(400).json({ success: false, message: 'action must be approve or reject' }); } if (item.status === 'SAP_PUSHED') { return res.status(400).json({ success: false, message: 'Item already pushed to SAP' }); } if (item.status === 'REJECTED') { return res.status(400).json({ success: false, message: 'Item is already rejected. Ask submitter to re-submit.' }); } if (item.status !== 'PENDING') { return res.status(400).json({ success: false, message: `Cannot act on item with status: ${item.status}` }); } const logEntry = { username: req.user.username, name: req.user.name || req.user.username, role: req.user.role, action, comment: comment || '', timestamp: new Date().toISOString(), }; if (action === 'reject') { await store.updateItem(item.id, { status: 'REJECTED', approvalLog: [...(item.approvalLog || []), logEntry], rejectedBy: req.user.username, rejectedAt: new Date().toISOString(), adminNotes: comment || '', }); return res.json({ success: true, message: 'Item request rejected', status: 'REJECTED' }); } // ── APPROVE: merge editedData if admin changed fields, then push to SAP ── const baseData = item.itemData || {}; const mergedData = editedData ? { ...baseData, ...editedData } : baseData; const co = cq(req) || item.company || null; console.log('[ITEM-APPROVAL] Pushing to SAP:', (mergedData.ItemCode || '').trim().toUpperCase()); const payload = await pushItemToSap(mergedData, co); await store.updateItem(item.id, { status: 'SAP_PUSHED', approvalLog: [...(item.approvalLog || []), logEntry], adminNotes: comment || '', adminEditedData: editedData || null, approvedBy: req.user.username, approvedAt: new Date().toISOString(), sapItemCode: payload.ItemCode, }); console.log(`[ITEM-APPROVAL] ✅ Item ${payload.ItemCode} created in SAP`); res.json({ success: true, message: `Item ${payload.ItemCode} created in SAP B1!`, status: 'SAP_PUSHED', itemCode: payload.ItemCode }); } catch (err) { console.error('[ITEM-APPROVAL] action error:', err.message); res.status(500).json({ success: false, message: err.message }); } }); module.exports = router;