269 lines
13 KiB
JavaScript
269 lines
13 KiB
JavaScript
// 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; |