Initial commit: Inventory Barcode System
This commit is contained in:
2
routes/.gitkeep
Normal file
2
routes/.gitkeep
Normal file
@ -0,0 +1,2 @@
|
||||
# Routes directory
|
||||
This directory contains Express.js route handlers
|
||||
611
routes/codes.js
Normal file
611
routes/codes.js
Normal file
@ -0,0 +1,611 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const XLSX = require('xlsx');
|
||||
const CodeGenerationService = require('../services/CodeGenerationService');
|
||||
const PrintableLayoutService = require('../services/PrintableLayoutService');
|
||||
const Product = require('../models/Product');
|
||||
const Inventory = require('../models/Inventory');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Configure multer for file uploads (for export functionality)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB limit
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/codes/formats
|
||||
* Get supported barcode formats
|
||||
*/
|
||||
router.get('/formats', (req, res) => {
|
||||
try {
|
||||
const codeGenService = new CodeGenerationService();
|
||||
const supportedFormats = codeGenService.getSupportedFormats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
barcodeFormats: supportedFormats,
|
||||
qrCodeSupported: true
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve supported formats',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/barcode
|
||||
* Generate barcode for a product code
|
||||
*/
|
||||
router.post('/barcode', async (req, res) => {
|
||||
try {
|
||||
const { productCode, format = 'CODE128', options = {} } = req.body;
|
||||
|
||||
if (!productCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Missing product code',
|
||||
message: 'Product code is required'
|
||||
});
|
||||
}
|
||||
|
||||
const codeGenService = new CodeGenerationService();
|
||||
const result = await codeGenService.generateBarcode(productCode, format, options);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Barcode generation failed',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: 'Barcode generated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate barcode',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/qrcode
|
||||
* Generate QR code for product data
|
||||
*/
|
||||
router.post('/qrcode', async (req, res) => {
|
||||
try {
|
||||
const { productData, options = {} } = req.body;
|
||||
|
||||
if (!productData || !productData.product_code) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product data',
|
||||
message: 'Product data with product_code is required'
|
||||
});
|
||||
}
|
||||
|
||||
const codeGenService = new CodeGenerationService();
|
||||
const result = await codeGenService.generateQRCode(productData, options);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'QR code generation failed',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: 'QR code generated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate QR code',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/both
|
||||
* Generate both barcode and QR code for product data
|
||||
*/
|
||||
router.post('/both', async (req, res) => {
|
||||
try {
|
||||
const { productData, options = {} } = req.body;
|
||||
|
||||
if (!productData || !productData.product_code) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product data',
|
||||
message: 'Product data with product_code is required'
|
||||
});
|
||||
}
|
||||
|
||||
const codeGenService = new CodeGenerationService();
|
||||
const result = await codeGenService.generateBothCodes(productData, options);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: 'Codes generated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate codes',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/product/:productId
|
||||
* Generate codes for a specific product by ID
|
||||
*/
|
||||
router.post('/product/:productId', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
// Get product data
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
const { codeType = 'both', format = 'CODE128', options = {} } = req.body;
|
||||
const codeGenService = new CodeGenerationService();
|
||||
|
||||
let result;
|
||||
const productData = product.toJSON();
|
||||
|
||||
switch (codeType.toLowerCase()) {
|
||||
case 'barcode':
|
||||
result = await codeGenService.generateBarcode(productData.name, format, options);
|
||||
break;
|
||||
case 'qrcode':
|
||||
result = await codeGenService.generateQRCode({
|
||||
product_code: productData.name,
|
||||
description: productData.description,
|
||||
category: productData.category,
|
||||
unit_of_measure: productData.unit
|
||||
}, options);
|
||||
break;
|
||||
case 'both':
|
||||
default:
|
||||
result = await codeGenService.generateBothCodes({
|
||||
product_code: productData.name,
|
||||
description: productData.description,
|
||||
category: productData.category,
|
||||
unit_of_measure: productData.unit
|
||||
}, options);
|
||||
break;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
product: productData,
|
||||
codes: result
|
||||
},
|
||||
message: 'Codes generated successfully for product'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate codes for product',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/codes/layouts/sizes
|
||||
* Get available label sizes for printable layouts
|
||||
*/
|
||||
router.get('/layouts/sizes', (req, res) => {
|
||||
try {
|
||||
const printService = new PrintableLayoutService();
|
||||
const labelSizes = printService.getAvailableLabelSizes();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: labelSizes
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve label sizes',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/layouts/preview
|
||||
* Generate preview of printable layout
|
||||
*/
|
||||
router.post('/layouts/preview', async (req, res) => {
|
||||
try {
|
||||
const { productIds = [], options = {} } = req.body;
|
||||
|
||||
if (!Array.isArray(productIds) || productIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product IDs',
|
||||
message: 'Product IDs array is required and cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Get sample products (limit to first 5 for preview)
|
||||
const sampleIds = productIds.slice(0, 5);
|
||||
const sampleProducts = [];
|
||||
|
||||
for (const productId of sampleIds) {
|
||||
const product = await Product.findById(productId);
|
||||
if (product) {
|
||||
sampleProducts.push({
|
||||
product_code: product.name,
|
||||
description: product.description,
|
||||
category: product.category,
|
||||
unit_of_measure: product.unit
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sampleProducts.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'No valid products found',
|
||||
message: 'None of the provided product IDs exist'
|
||||
});
|
||||
}
|
||||
|
||||
const printService = new PrintableLayoutService();
|
||||
const preview = await printService.generateLayoutPreview(sampleProducts, options);
|
||||
|
||||
if (!preview.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Preview generation failed',
|
||||
message: preview.error
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...preview.preview,
|
||||
sampleProducts: sampleProducts,
|
||||
totalRequestedProducts: productIds.length
|
||||
},
|
||||
message: 'Layout preview generated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate layout preview',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/layouts/generate
|
||||
* Generate printable PDF layout with codes
|
||||
*/
|
||||
router.post('/layouts/generate', async (req, res) => {
|
||||
try {
|
||||
const { productIds = [], options = {} } = req.body;
|
||||
|
||||
if (!Array.isArray(productIds) || productIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product IDs',
|
||||
message: 'Product IDs array is required and cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Limit to reasonable number of products
|
||||
if (productIds.length > 1000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Too many products',
|
||||
message: 'Maximum 1000 products allowed per layout'
|
||||
});
|
||||
}
|
||||
|
||||
// Get products data
|
||||
const products = [];
|
||||
for (const productId of productIds) {
|
||||
const product = await Product.findById(productId);
|
||||
if (product) {
|
||||
products.push({
|
||||
product_code: product.name,
|
||||
description: product.description,
|
||||
category: product.category,
|
||||
unit_of_measure: product.unit
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (products.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'No valid products found',
|
||||
message: 'None of the provided product IDs exist'
|
||||
});
|
||||
}
|
||||
|
||||
const printService = new PrintableLayoutService();
|
||||
const result = await printService.generatePrintableLayout(products, options);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Layout generation failed',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
|
||||
// Set response headers for PDF download
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="product-labels.pdf"');
|
||||
res.setHeader('Content-Length', result.data.length);
|
||||
|
||||
res.send(result.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate printable layout',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/codes/export/excel
|
||||
* Export inventory data to Excel with updated levels
|
||||
*/
|
||||
router.get('/export/excel', async (req, res) => {
|
||||
try {
|
||||
const filters = {};
|
||||
|
||||
// Extract query parameters for filtering
|
||||
if (req.query.category) {
|
||||
filters.category = req.query.category;
|
||||
}
|
||||
|
||||
if (req.query.lowStock === 'true') {
|
||||
filters.lowStock = true;
|
||||
}
|
||||
|
||||
// Get inventory summary
|
||||
const inventoryData = await Inventory.getInventorySummary(filters);
|
||||
|
||||
if (inventoryData.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'No data to export',
|
||||
message: 'No inventory data found matching the specified filters'
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare data for Excel export
|
||||
const excelData = inventoryData.map(item => ({
|
||||
'Product Code': item.product_code || '',
|
||||
'Description': item.description || '',
|
||||
'Category': item.category || '',
|
||||
'Current Level': item.current_level || 0,
|
||||
'Minimum Level': item.minimum_level || 0,
|
||||
'Maximum Level': item.maximum_level || '',
|
||||
'Stock Status': item.stock_status || 'unknown',
|
||||
'Last Updated': item.last_updated || '',
|
||||
'Updated By': item.updated_by || ''
|
||||
}));
|
||||
|
||||
// Create workbook and worksheet
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||
|
||||
// Add worksheet to workbook
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Inventory');
|
||||
|
||||
// Add metadata sheet
|
||||
const metadata = [
|
||||
{ Field: 'Export Date', Value: new Date().toISOString() },
|
||||
{ Field: 'Total Records', Value: inventoryData.length },
|
||||
{ Field: 'Filters Applied', Value: Object.keys(filters).length > 0 ? JSON.stringify(filters) : 'None' }
|
||||
];
|
||||
const metadataSheet = XLSX.utils.json_to_sheet(metadata);
|
||||
XLSX.utils.book_append_sheet(workbook, metadataSheet, 'Export Info');
|
||||
|
||||
// Generate Excel buffer
|
||||
const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
// Set response headers for Excel download
|
||||
const filename = `inventory-export-${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Length', excelBuffer.length);
|
||||
|
||||
res.send(excelBuffer);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to export Excel file',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/export/excel/custom
|
||||
* Export custom inventory data to Excel with specified columns
|
||||
*/
|
||||
router.post('/export/excel/custom', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
productIds = [],
|
||||
columns = ['product_code', 'description', 'current_level'],
|
||||
includeHistory = false,
|
||||
filename
|
||||
} = req.body;
|
||||
|
||||
if (!Array.isArray(productIds) || productIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product IDs',
|
||||
message: 'Product IDs array is required and cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Get products and their inventory data
|
||||
const exportData = [];
|
||||
|
||||
for (const productId of productIds) {
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) continue;
|
||||
|
||||
const inventory = await Inventory.getByProductId(productId);
|
||||
|
||||
const rowData = {};
|
||||
|
||||
// Add requested columns
|
||||
if (columns.includes('product_code')) rowData['Product Code'] = product.name;
|
||||
if (columns.includes('description')) rowData['Description'] = product.description;
|
||||
if (columns.includes('category')) rowData['Category'] = product.category;
|
||||
if (columns.includes('current_level')) rowData['Current Level'] = inventory?.current_level || 0;
|
||||
if (columns.includes('minimum_level')) rowData['Minimum Level'] = inventory?.minimum_level || 0;
|
||||
if (columns.includes('maximum_level')) rowData['Maximum Level'] = inventory?.maximum_level || '';
|
||||
if (columns.includes('last_updated')) rowData['Last Updated'] = inventory?.last_updated || '';
|
||||
if (columns.includes('updated_by')) rowData['Updated By'] = inventory?.updated_by || '';
|
||||
|
||||
exportData.push(rowData);
|
||||
}
|
||||
|
||||
if (exportData.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'No data to export',
|
||||
message: 'No valid products found for export'
|
||||
});
|
||||
}
|
||||
|
||||
// Create workbook
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Custom Export');
|
||||
|
||||
// Add history sheet if requested
|
||||
if (includeHistory) {
|
||||
const historyData = [];
|
||||
|
||||
for (const productId of productIds.slice(0, 10)) { // Limit history to first 10 products
|
||||
const history = await Inventory.getInventoryHistory(productId, { limit: 50 });
|
||||
historyData.push(...history.map(h => ({
|
||||
'Product Code': h.product_code,
|
||||
'Old Level': h.old_level,
|
||||
'New Level': h.new_level,
|
||||
'Change Reason': h.change_reason,
|
||||
'Updated By': h.updated_by,
|
||||
'Updated At': h.updated_at
|
||||
})));
|
||||
}
|
||||
|
||||
if (historyData.length > 0) {
|
||||
const historySheet = XLSX.utils.json_to_sheet(historyData);
|
||||
XLSX.utils.book_append_sheet(workbook, historySheet, 'History');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Excel buffer
|
||||
const excelBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
// Set response headers
|
||||
const exportFilename = filename || `custom-export-${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${exportFilename}"`);
|
||||
res.setHeader('Content-Length', excelBuffer.length);
|
||||
|
||||
res.send(excelBuffer);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to export custom Excel file',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/codes/qr/parse
|
||||
* Parse QR code data back to product information
|
||||
*/
|
||||
router.post('/qr/parse', (req, res) => {
|
||||
try {
|
||||
const { qrData } = req.body;
|
||||
|
||||
if (!qrData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Missing QR data',
|
||||
message: 'QR code data is required'
|
||||
});
|
||||
}
|
||||
|
||||
const codeGenService = new CodeGenerationService();
|
||||
const result = codeGenService.parseQRCodeData(qrData);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'QR code parsing failed',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: 'QR code parsed successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to parse QR code',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
969
routes/inventory.js
Normal file
969
routes/inventory.js
Normal file
@ -0,0 +1,969 @@
|
||||
const express = require('express');
|
||||
const Inventory = require('../models/Inventory');
|
||||
const Product = require('../models/Product');
|
||||
const ExcelExportService = require('../services/ExcelExportService');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/inventory
|
||||
* Get inventory summary for all products with optional filtering
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const filters = {};
|
||||
|
||||
// Extract query parameters for filtering
|
||||
if (req.query.category) {
|
||||
filters.category = req.query.category;
|
||||
}
|
||||
|
||||
if (req.query.lowStock === 'true') {
|
||||
filters.lowStock = true;
|
||||
}
|
||||
|
||||
const inventorySummary = await Inventory.getInventorySummary(filters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: inventorySummary,
|
||||
count: inventorySummary.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve inventory summary',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/inventory/low-stock
|
||||
* Get products with low stock levels
|
||||
*/
|
||||
router.get('/low-stock', async (req, res) => {
|
||||
try {
|
||||
const lowStockItems = await Inventory.getLowStockItems();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: lowStockItems,
|
||||
count: lowStockItems.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve low stock items',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/inventory/product/:productId
|
||||
* Get inventory details for a specific product
|
||||
*/
|
||||
router.get('/product/:productId', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
const inventory = await Inventory.getByProductId(productId);
|
||||
|
||||
if (!inventory) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Inventory not found',
|
||||
message: `Inventory record for product ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: inventory.toJSON()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve inventory',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/inventory/product/:productId/level
|
||||
* Get current inventory level for a specific product
|
||||
*/
|
||||
router.get('/product/:productId/level', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
const currentLevel = await Inventory.getCurrentLevel(productId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
product_id: productId,
|
||||
current_level: currentLevel
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve inventory level',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/inventory/product/:productId/history
|
||||
* Get inventory history for a specific product
|
||||
*/
|
||||
router.get('/product/:productId/history', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse query parameters for pagination and filtering
|
||||
const options = {
|
||||
limit: parseInt(req.query.limit) || 50,
|
||||
offset: parseInt(req.query.offset) || 0,
|
||||
startDate: req.query.startDate,
|
||||
endDate: req.query.endDate
|
||||
};
|
||||
|
||||
// Validate limit and offset
|
||||
if (options.limit < 1 || options.limit > 1000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid limit',
|
||||
message: 'Limit must be between 1 and 1000'
|
||||
});
|
||||
}
|
||||
|
||||
if (options.offset < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid offset',
|
||||
message: 'Offset must be non-negative'
|
||||
});
|
||||
}
|
||||
|
||||
const history = await Inventory.getInventoryHistory(productId, options);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: history,
|
||||
count: history.length,
|
||||
pagination: {
|
||||
limit: options.limit,
|
||||
offset: options.offset,
|
||||
hasMore: history.length === options.limit
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve inventory history',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/inventory/product/:productId/level
|
||||
* Update inventory level for a specific product
|
||||
*/
|
||||
router.put('/product/:productId/level', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
const { newLevel, changeReason, updatedBy } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (typeof newLevel !== 'number') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid new level',
|
||||
message: 'New level must be a number'
|
||||
});
|
||||
}
|
||||
|
||||
if (newLevel < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid new level',
|
||||
message: 'New level cannot be negative'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify product exists
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
const updatedInventory = await Inventory.updateInventoryLevel(
|
||||
productId,
|
||||
newLevel,
|
||||
changeReason || 'Manual update',
|
||||
updatedBy || 'api-user'
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updatedInventory.toJSON(),
|
||||
message: 'Inventory level updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('Concurrent update detected')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'Concurrent update conflict',
|
||||
message: error.message
|
||||
});
|
||||
} else if (error.message.includes('not found')) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Inventory not found',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update inventory level',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/inventory/product/:productId
|
||||
* Update inventory settings (minimum/maximum levels) for a specific product
|
||||
*/
|
||||
router.put('/product/:productId', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify product exists
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
// Get existing inventory record
|
||||
let inventory = await Inventory.getByProductId(productId);
|
||||
|
||||
if (!inventory) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Inventory not found',
|
||||
message: `Inventory record for product ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
// Update inventory settings
|
||||
const { minimum_level, maximum_level, updatedBy } = req.body;
|
||||
|
||||
if (minimum_level !== undefined) {
|
||||
if (typeof minimum_level !== 'number' || minimum_level < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid minimum level',
|
||||
message: 'Minimum level must be a non-negative number'
|
||||
});
|
||||
}
|
||||
inventory.minimum_level = minimum_level;
|
||||
}
|
||||
|
||||
if (maximum_level !== undefined) {
|
||||
if (maximum_level !== null && (typeof maximum_level !== 'number' || maximum_level < 0)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid maximum level',
|
||||
message: 'Maximum level must be a non-negative number or null'
|
||||
});
|
||||
}
|
||||
inventory.maximum_level = maximum_level;
|
||||
}
|
||||
|
||||
if (updatedBy) {
|
||||
inventory.updated_by = updatedBy;
|
||||
}
|
||||
|
||||
// Validate the updated inventory
|
||||
const validation = inventory.validate();
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
message: 'Updated inventory data is invalid',
|
||||
details: validation.errors
|
||||
});
|
||||
}
|
||||
|
||||
await inventory.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: inventory.toJSON(),
|
||||
message: 'Inventory settings updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('Concurrent update detected')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'Concurrent update conflict',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update inventory settings',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/inventory/product/:productId
|
||||
* Create inventory record for a product
|
||||
*/
|
||||
router.post('/product/:productId', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify product exists
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
// Check if inventory record already exists
|
||||
const existingInventory = await Inventory.getByProductId(productId);
|
||||
if (existingInventory) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'Inventory already exists',
|
||||
message: `Inventory record for product ID ${productId} already exists`
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
initialLevel = 0,
|
||||
minimumLevel = 0,
|
||||
maximumLevel = null,
|
||||
updatedBy = 'api-user'
|
||||
} = req.body;
|
||||
|
||||
// Validate input
|
||||
if (typeof initialLevel !== 'number' || initialLevel < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid initial level',
|
||||
message: 'Initial level must be a non-negative number'
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof minimumLevel !== 'number' || minimumLevel < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid minimum level',
|
||||
message: 'Minimum level must be a non-negative number'
|
||||
});
|
||||
}
|
||||
|
||||
if (maximumLevel !== null && (typeof maximumLevel !== 'number' || maximumLevel < 0)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid maximum level',
|
||||
message: 'Maximum level must be a non-negative number or null'
|
||||
});
|
||||
}
|
||||
|
||||
const inventory = await Inventory.createForProduct(
|
||||
productId,
|
||||
initialLevel,
|
||||
minimumLevel,
|
||||
maximumLevel,
|
||||
updatedBy
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: inventory.toJSON(),
|
||||
message: 'Inventory record created successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create inventory record',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/inventory/bulk-update
|
||||
* Bulk update inventory levels for multiple products
|
||||
*/
|
||||
router.post('/bulk-update', async (req, res) => {
|
||||
try {
|
||||
const { updates } = req.body;
|
||||
|
||||
if (!Array.isArray(updates) || updates.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid input',
|
||||
message: 'Updates array is required and cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate each update
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
const update = updates[i];
|
||||
|
||||
if (!update.productId || typeof update.productId !== 'number') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid update data',
|
||||
message: `Update at index ${i}: productId is required and must be a number`
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof update.newLevel !== 'number' || update.newLevel < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid update data',
|
||||
message: `Update at index ${i}: newLevel must be a non-negative number`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedInventories = await Inventory.bulkUpdateInventory(updates);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updatedInventories.map(inv => inv.toJSON()),
|
||||
count: updatedInventories.length,
|
||||
message: `Successfully updated ${updatedInventories.length} inventory records`
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('Concurrent update detected')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'Concurrent update conflict',
|
||||
message: error.message
|
||||
});
|
||||
} else if (error.message.includes('not found')) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Inventory not found',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to bulk update inventory',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/inventory/adjust/:productId
|
||||
* Adjust inventory level (add or subtract from current level)
|
||||
*/
|
||||
router.post('/adjust/:productId', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.productId);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
const { adjustment, changeReason, updatedBy } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (typeof adjustment !== 'number') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid adjustment',
|
||||
message: 'Adjustment must be a number (positive to add, negative to subtract)'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify product exists
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
// Get current level and calculate new level
|
||||
const currentLevel = await Inventory.getCurrentLevel(productId);
|
||||
const newLevel = currentLevel + adjustment;
|
||||
|
||||
if (newLevel < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid adjustment',
|
||||
message: `Adjustment would result in negative inventory (current: ${currentLevel}, adjustment: ${adjustment})`
|
||||
});
|
||||
}
|
||||
|
||||
const updatedInventory = await Inventory.updateInventoryLevel(
|
||||
productId,
|
||||
newLevel,
|
||||
changeReason || `Inventory adjustment: ${adjustment > 0 ? '+' : ''}${adjustment}`,
|
||||
updatedBy || 'api-user'
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...updatedInventory.toJSON(),
|
||||
adjustment: adjustment,
|
||||
previous_level: currentLevel
|
||||
},
|
||||
message: 'Inventory level adjusted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('Concurrent update detected')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'Concurrent update conflict',
|
||||
message: error.message
|
||||
});
|
||||
} else if (error.message.includes('not found')) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Inventory not found',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to adjust inventory level',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Configure multer for file uploads
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel', // .xls
|
||||
'application/octet-stream' // fallback
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(xlsx|xls)$/i)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only Excel files (.xlsx, .xls) are allowed'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/inventory/export
|
||||
* Export inventory data to Excel format
|
||||
*/
|
||||
router.get('/export', async (req, res) => {
|
||||
try {
|
||||
const exportService = new ExcelExportService();
|
||||
|
||||
// Parse query parameters for filtering and options
|
||||
const filters = {};
|
||||
const options = {
|
||||
format: req.query.format || 'xlsx',
|
||||
includeHistory: req.query.includeHistory === 'true',
|
||||
includeAuditInfo: req.query.includeAuditInfo !== 'false', // default true
|
||||
filename: req.query.filename
|
||||
};
|
||||
|
||||
// Apply filters
|
||||
if (req.query.category) {
|
||||
filters.category = req.query.category;
|
||||
}
|
||||
|
||||
if (req.query.stockStatus) {
|
||||
filters.stockStatus = req.query.stockStatus;
|
||||
}
|
||||
|
||||
if (req.query.updatedSince) {
|
||||
filters.updatedSince = req.query.updatedSince;
|
||||
}
|
||||
|
||||
if (req.query.productCodes) {
|
||||
filters.productCodes = req.query.productCodes.split(',').map(code => code.trim());
|
||||
}
|
||||
|
||||
// Validate format
|
||||
const allowedFormats = ['xlsx', 'xls', 'csv'];
|
||||
if (!allowedFormats.includes(options.format)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid format',
|
||||
message: `Format must be one of: ${allowedFormats.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
// Export inventory data
|
||||
const exportResult = await exportService.exportInventoryToExcel({
|
||||
...options,
|
||||
filters: filters
|
||||
});
|
||||
|
||||
if (!exportResult.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Export failed',
|
||||
message: exportResult.error
|
||||
});
|
||||
}
|
||||
|
||||
// Set response headers for file download
|
||||
const contentType = options.format === 'csv'
|
||||
? 'text/csv'
|
||||
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${exportResult.filename}"`);
|
||||
res.setHeader('X-Export-Session-Id', exportResult.sessionId);
|
||||
res.setHeader('X-Record-Count', exportResult.recordCount);
|
||||
|
||||
// Send file
|
||||
res.sendFile(exportResult.filePath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending export file:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send export file',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Export failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/inventory/export/with-original
|
||||
* Export inventory data while preserving original Excel file structure
|
||||
*/
|
||||
router.post('/export/with-original', upload.single('originalFile'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Missing original file',
|
||||
message: 'Original Excel file is required for structure preservation'
|
||||
});
|
||||
}
|
||||
|
||||
const exportService = new ExcelExportService();
|
||||
|
||||
// Parse options from request body
|
||||
const options = {
|
||||
format: req.body.format || 'xlsx',
|
||||
includeHistory: req.body.includeHistory === 'true',
|
||||
includeTimestamp: req.body.includeTimestamp !== 'false', // default true
|
||||
includeNewProducts: req.body.includeNewProducts === 'true',
|
||||
preserveFormatting: req.body.preserveFormatting !== 'false', // default true
|
||||
filename: req.body.filename,
|
||||
originalFileBuffer: req.file.buffer,
|
||||
sheetName: req.body.sheetName
|
||||
};
|
||||
|
||||
// Parse filters
|
||||
const filters = {};
|
||||
if (req.body.category) {
|
||||
filters.category = req.body.category;
|
||||
}
|
||||
|
||||
if (req.body.stockStatus) {
|
||||
filters.stockStatus = req.body.stockStatus;
|
||||
}
|
||||
|
||||
if (req.body.updatedSince) {
|
||||
filters.updatedSince = req.body.updatedSince;
|
||||
}
|
||||
|
||||
if (req.body.productCodes) {
|
||||
const codes = typeof req.body.productCodes === 'string'
|
||||
? req.body.productCodes.split(',').map(code => code.trim())
|
||||
: req.body.productCodes;
|
||||
filters.productCodes = codes;
|
||||
}
|
||||
|
||||
// Export with original file structure
|
||||
const exportResult = await exportService.exportInventoryToExcel({
|
||||
...options,
|
||||
filters: filters
|
||||
});
|
||||
|
||||
if (!exportResult.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Export failed',
|
||||
message: exportResult.error
|
||||
});
|
||||
}
|
||||
|
||||
// Set response headers for file download
|
||||
const contentType = options.format === 'csv'
|
||||
? 'text/csv'
|
||||
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${exportResult.filename}"`);
|
||||
res.setHeader('X-Export-Session-Id', exportResult.sessionId);
|
||||
res.setHeader('X-Record-Count', exportResult.recordCount);
|
||||
res.setHeader('X-Preserved-Formatting', exportResult.metadata?.preservedFormatting || false);
|
||||
|
||||
// Send file
|
||||
res.sendFile(exportResult.filePath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending export file:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to send export file',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Export failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/inventory/export/history
|
||||
* Get export history with pagination
|
||||
*/
|
||||
router.get('/export/history', async (req, res) => {
|
||||
try {
|
||||
const exportService = new ExcelExportService();
|
||||
|
||||
const options = {
|
||||
limit: parseInt(req.query.limit) || 50,
|
||||
offset: parseInt(req.query.offset) || 0
|
||||
};
|
||||
|
||||
// Validate pagination parameters
|
||||
if (options.limit < 1 || options.limit > 1000) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid limit',
|
||||
message: 'Limit must be between 1 and 1000'
|
||||
});
|
||||
}
|
||||
|
||||
if (options.offset < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid offset',
|
||||
message: 'Offset must be non-negative'
|
||||
});
|
||||
}
|
||||
|
||||
const history = await exportService.getExportHistory(options);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: history,
|
||||
count: history.length,
|
||||
pagination: {
|
||||
limit: options.limit,
|
||||
offset: options.offset,
|
||||
hasMore: history.length === options.limit
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve export history',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/inventory/export/cleanup
|
||||
* Clean up old export files
|
||||
*/
|
||||
router.delete('/export/cleanup', async (req, res) => {
|
||||
try {
|
||||
const exportService = new ExcelExportService();
|
||||
|
||||
const maxAgeHours = parseInt(req.query.maxAgeHours) || 24;
|
||||
|
||||
// Validate maxAgeHours
|
||||
if (maxAgeHours < 1 || maxAgeHours > 168) { // 1 hour to 1 week
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid maxAgeHours',
|
||||
message: 'maxAgeHours must be between 1 and 168 (1 week)'
|
||||
});
|
||||
}
|
||||
|
||||
const cleanupResult = await exportService.cleanupOldExports(maxAgeHours);
|
||||
|
||||
if (!cleanupResult.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Cleanup failed',
|
||||
message: cleanupResult.error
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
deletedCount: cleanupResult.deletedCount,
|
||||
maxAgeHours: maxAgeHours
|
||||
},
|
||||
message: cleanupResult.message
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Cleanup failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/inventory/export/history
|
||||
* Clear export history
|
||||
*/
|
||||
router.delete('/export/history', async (req, res) => {
|
||||
try {
|
||||
const exportService = new ExcelExportService();
|
||||
|
||||
const result = await exportService.clearExportHistory();
|
||||
|
||||
if (result.success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Export history cleared successfully',
|
||||
deletedCount: result.deletedCount || 0
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to clear export history',
|
||||
message: result.error || 'Unknown error'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error clearing export history:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to clear export history',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
776
routes/products.js
Normal file
776
routes/products.js
Normal file
@ -0,0 +1,776 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const Product = require('../models/Product');
|
||||
const ExcelImportService = require('../services/ExcelImportService');
|
||||
const logger = require('../utils/logger');
|
||||
const {
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
asyncHandler
|
||||
} = require('../middleware/errorHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/products/test
|
||||
* Test endpoint to verify API is working - MUST BE FIRST to avoid /:id conflict
|
||||
*/
|
||||
router.get('/test', (req, res) => {
|
||||
console.log('Test endpoint accessed from:', req.ip, req.get('User-Agent'));
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Products API is working',
|
||||
timestamp: new Date().toISOString(),
|
||||
server: {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
baseUrl: req.baseUrl,
|
||||
originalUrl: req.originalUrl,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
},
|
||||
endpoints: [
|
||||
'GET /api/products',
|
||||
'GET /api/products/:id',
|
||||
'POST /api/products',
|
||||
'PUT /api/products/:id',
|
||||
'DELETE /api/products/:id',
|
||||
'POST /api/products/import/excel/preview',
|
||||
'POST /api/products/import/excel',
|
||||
'POST /api/products/bulk'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// Configure multer for file uploads
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB limit
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Accept Excel files
|
||||
const allowedMimes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel', // .xls
|
||||
];
|
||||
|
||||
if (allowedMimes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only Excel files (.xlsx, .xls) are allowed'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/products
|
||||
* Get all products with optional filtering
|
||||
*/
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.info('Retrieving products', {
|
||||
requestId: req.requestId,
|
||||
filters: req.query
|
||||
});
|
||||
|
||||
const filters = {};
|
||||
|
||||
// Extract and validate query parameters for filtering
|
||||
if (req.query.category) {
|
||||
if (typeof req.query.category !== 'string' || req.query.category.length > 100) {
|
||||
throw new ValidationError('Invalid category filter');
|
||||
}
|
||||
filters.category = req.query.category.trim();
|
||||
}
|
||||
|
||||
if (req.query.name) {
|
||||
if (typeof req.query.name !== 'string' || req.query.name.length > 200) {
|
||||
throw new ValidationError('Invalid name filter');
|
||||
}
|
||||
filters.name = req.query.name.trim();
|
||||
}
|
||||
|
||||
const products = await Product.findAll(filters);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
logger.info('Products retrieved successfully', {
|
||||
requestId: req.requestId,
|
||||
count: products.length,
|
||||
duration: `${duration}ms`,
|
||||
filters
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: products.map(product => product.toJSON()),
|
||||
count: products.length,
|
||||
filters: Object.keys(filters).length > 0 ? filters : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
logger.logError(error, {
|
||||
operation: 'get_products',
|
||||
requestId: req.requestId,
|
||||
filters: req.query
|
||||
});
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/products/api-test
|
||||
* Test endpoint to verify API is working
|
||||
*/
|
||||
router.get('/api-test', (req, res) => {
|
||||
console.log('Test endpoint accessed from:', req.ip, req.get('User-Agent'));
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Products API is working',
|
||||
timestamp: new Date().toISOString(),
|
||||
server: {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
baseUrl: req.baseUrl,
|
||||
originalUrl: req.originalUrl,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
},
|
||||
endpoints: [
|
||||
'GET /api/products',
|
||||
'GET /api/products/:id',
|
||||
'POST /api/products',
|
||||
'PUT /api/products/:id',
|
||||
'DELETE /api/products/:id',
|
||||
'POST /api/products/import/excel/preview',
|
||||
'POST /api/products/import/excel',
|
||||
'POST /api/products/bulk'
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/products/:id
|
||||
* Get a specific product by ID
|
||||
*/
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
const product = await Product.findById(productId);
|
||||
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: product.toJSON()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve product',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/products/barcode/:barcode
|
||||
* Get a product by barcode
|
||||
*/
|
||||
router.get('/barcode/:barcode', async (req, res) => {
|
||||
try {
|
||||
const barcode = decodeURIComponent(req.params.barcode);
|
||||
|
||||
if (!barcode || barcode.trim() === '' || barcode.trim() === ' ') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid barcode',
|
||||
message: 'Barcode cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
const product = await Product.findByBarcode(barcode);
|
||||
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with barcode ${barcode} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: product.toJSON()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve product',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/products
|
||||
* Create a new product
|
||||
*/
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const productData = req.body;
|
||||
|
||||
// Create new product instance
|
||||
const product = new Product(productData);
|
||||
|
||||
// Validate the product
|
||||
const validation = product.validate();
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
message: 'Product data is invalid',
|
||||
details: validation.errors
|
||||
});
|
||||
}
|
||||
|
||||
// Save the product
|
||||
await product.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: product.toJSON(),
|
||||
message: 'Product created successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'Conflict',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create product',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/products/:id
|
||||
* Update an existing product
|
||||
*/
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
// Find existing product
|
||||
const existingProduct = await Product.findById(productId);
|
||||
if (!existingProduct) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
// Update product data
|
||||
const updatedData = { ...existingProduct.toJSON(), ...req.body, id: productId };
|
||||
const product = new Product(updatedData);
|
||||
|
||||
// Validate the updated product
|
||||
const validation = product.validate();
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
message: 'Updated product data is invalid',
|
||||
details: validation.errors
|
||||
});
|
||||
}
|
||||
|
||||
// Save the updated product
|
||||
await product.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: product.toJSON(),
|
||||
message: 'Product updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: 'Conflict',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update product',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/products/:id
|
||||
* Delete a product
|
||||
*/
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const productId = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(productId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid product ID',
|
||||
message: 'Product ID must be a valid number'
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await Product.deleteById(productId);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
message: `Product with ID ${productId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Product deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to delete product',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/products/import/excel
|
||||
* Import products from Excel file
|
||||
*/
|
||||
router.post('/import/excel', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No file uploaded',
|
||||
message: 'Please upload an Excel file'
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
filename: req.file.originalname,
|
||||
checkExisting: req.body.checkExisting !== 'false',
|
||||
duplicateStrategy: req.body.duplicateStrategy || 'skip',
|
||||
importToDatabase: req.body.importToDatabase !== 'false',
|
||||
forceImport: req.body.forceImport === 'true',
|
||||
updatedBy: req.body.updatedBy || 'api-user'
|
||||
};
|
||||
|
||||
const excelImportService = new ExcelImportService();
|
||||
const results = await excelImportService.processImport(req.file.buffer, options);
|
||||
|
||||
if (!results.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Import failed',
|
||||
message: 'Failed to process Excel file',
|
||||
details: results.errors
|
||||
});
|
||||
}
|
||||
|
||||
// Determine response status based on validation results
|
||||
let statusCode = 200;
|
||||
if (results.validationResults && !results.validationResults.isValid) {
|
||||
statusCode = 422; // Unprocessable Entity
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: true,
|
||||
data: {
|
||||
parseResults: results.parseResults,
|
||||
validationResults: results.validationResults,
|
||||
importResults: results.importResults
|
||||
},
|
||||
message: 'Excel file processed successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('Only Excel files')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid file type',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Import failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/products/import/excel/preview
|
||||
* Preview Excel file import without saving to database
|
||||
*/
|
||||
/**
|
||||
* Direct import preview endpoint that doesn't rely on ExcelImportService
|
||||
* This is a simplified version for testing
|
||||
*/
|
||||
router.post('/direct-import-preview', upload.single('file'), async (req, res) => {
|
||||
console.log('Direct import preview endpoint hit:', {
|
||||
hasFile: !!req.file,
|
||||
filename: req.file?.originalname,
|
||||
size: req.file?.size,
|
||||
mimetype: req.file?.mimetype
|
||||
});
|
||||
|
||||
try {
|
||||
if (!req.file) {
|
||||
console.log('No file uploaded');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No file uploaded',
|
||||
message: 'Please upload an Excel file'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Processing file:', req.file.originalname);
|
||||
|
||||
// Simple mock response for testing
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
preview: {
|
||||
totalRows: 3,
|
||||
validProducts: 2,
|
||||
invalidProducts: 1,
|
||||
duplicateProducts: 0,
|
||||
existingProducts: 0,
|
||||
sampleProducts: [
|
||||
{ product_code: 'TEST001', description: 'Test Product 1', quantity: 10, isValid: true },
|
||||
{ product_code: 'TEST002', description: 'Test Product 2', quantity: 20, isValid: true },
|
||||
{ product_code: '', description: 'Invalid Product', quantity: 0, isValid: false, validationErrors: [{ message: 'Missing product code' }] }
|
||||
]
|
||||
},
|
||||
validationResults: {
|
||||
isValid: false,
|
||||
errors: [
|
||||
{ row: 3, message: 'Missing product code' }
|
||||
],
|
||||
statistics: {
|
||||
validProducts: 2,
|
||||
invalidProducts: 1,
|
||||
duplicateProducts: 0,
|
||||
existingProducts: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
message: 'Excel file preview generated successfully (direct endpoint)'
|
||||
};
|
||||
|
||||
console.log('Sending direct import response');
|
||||
res.json(mockResponse);
|
||||
} catch (error) {
|
||||
console.error('Direct import preview error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Import preview failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Regular import preview endpoint
|
||||
*/
|
||||
router.post('/import/excel/preview', upload.single('file'), async (req, res) => {
|
||||
console.log('Import preview endpoint hit:', {
|
||||
hasFile: !!req.file,
|
||||
filename: req.file?.originalname,
|
||||
size: req.file?.size,
|
||||
mimetype: req.file?.mimetype
|
||||
});
|
||||
|
||||
try {
|
||||
if (!req.file) {
|
||||
console.log('No file uploaded');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'No file uploaded',
|
||||
message: 'Please upload an Excel file'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Processing file:', req.file.originalname);
|
||||
|
||||
// For now, return a simple mock response to test the endpoint
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
preview: {
|
||||
totalRows: 3,
|
||||
validProducts: 2,
|
||||
invalidProducts: 1,
|
||||
duplicateProducts: 0,
|
||||
existingProducts: 0,
|
||||
sampleProducts: [
|
||||
{ product_code: 'TEST001', description: 'Test Product 1', quantity: 10, isValid: true },
|
||||
{ product_code: 'TEST002', description: 'Test Product 2', quantity: 20, isValid: true },
|
||||
{ product_code: '', description: 'Invalid Product', quantity: 0, isValid: false, validationErrors: [{ message: 'Missing product code' }] }
|
||||
]
|
||||
},
|
||||
validationResults: {
|
||||
isValid: false,
|
||||
errors: [
|
||||
{ row: 3, message: 'Missing product code' }
|
||||
],
|
||||
statistics: {
|
||||
validProducts: 2,
|
||||
invalidProducts: 1,
|
||||
duplicateProducts: 0,
|
||||
existingProducts: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
message: 'Excel file preview generated successfully (mock data)'
|
||||
};
|
||||
|
||||
console.log('Sending mock response');
|
||||
res.json(mockResponse);
|
||||
|
||||
/* Original code - commented out for testing
|
||||
const options = {
|
||||
filename: req.file.originalname,
|
||||
checkExisting: req.body.checkExisting !== 'false',
|
||||
importToDatabase: false // Never import for preview
|
||||
};
|
||||
|
||||
const excelImportService = new ExcelImportService();
|
||||
const results = await excelImportService.processImport(req.file.buffer, options);
|
||||
|
||||
if (!results.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Preview failed',
|
||||
message: 'Failed to process Excel file',
|
||||
details: results.errors
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
parseResults: results.parseResults,
|
||||
validationResults: results.validationResults,
|
||||
preview: {
|
||||
totalRows: results.parseResults.data.totalRows,
|
||||
validProducts: results.validationResults.statistics.validProducts,
|
||||
invalidProducts: results.validationResults.statistics.invalidProducts,
|
||||
duplicateProducts: results.validationResults.statistics.duplicateProducts,
|
||||
existingProducts: results.validationResults.statistics.existingProducts,
|
||||
sampleProducts: results.validationResults.validatedProducts.slice(0, 5) // First 5 for preview
|
||||
}
|
||||
},
|
||||
message: 'Excel file preview generated successfully'
|
||||
});
|
||||
*/
|
||||
} catch (error) {
|
||||
console.error('Import preview error:', error);
|
||||
if (error.message.includes('Only Excel files')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid file type',
|
||||
message: error.message
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Preview failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/products/bulk
|
||||
* Create multiple products in bulk
|
||||
*/
|
||||
router.post('/bulk', async (req, res) => {
|
||||
try {
|
||||
const { products } = req.body;
|
||||
|
||||
if (!Array.isArray(products) || products.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid input',
|
||||
message: 'Products array is required and cannot be empty'
|
||||
});
|
||||
}
|
||||
|
||||
const results = {
|
||||
success: true,
|
||||
created: 0,
|
||||
failed: 0,
|
||||
errors: [],
|
||||
createdProducts: []
|
||||
};
|
||||
|
||||
// Process each product
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
try {
|
||||
const productData = products[i];
|
||||
const product = new Product(productData);
|
||||
|
||||
// Validate the product
|
||||
const validation = product.validate();
|
||||
if (!validation.isValid) {
|
||||
results.failed++;
|
||||
results.errors.push({
|
||||
index: i,
|
||||
productData: productData,
|
||||
errors: validation.errors
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save the product
|
||||
await product.save();
|
||||
results.created++;
|
||||
results.createdProducts.push(product.toJSON());
|
||||
|
||||
} catch (error) {
|
||||
results.failed++;
|
||||
results.errors.push({
|
||||
index: i,
|
||||
productData: products[i],
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Determine response status
|
||||
let statusCode = 200;
|
||||
if (results.failed > 0 && results.created === 0) {
|
||||
statusCode = 400; // All failed
|
||||
results.success = false;
|
||||
} else if (results.failed > 0) {
|
||||
statusCode = 207; // Partial success
|
||||
} else {
|
||||
statusCode = 201; // All created
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
...results,
|
||||
message: `Bulk operation completed: ${results.created} created, ${results.failed} failed`
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Bulk operation failed',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/products/categories
|
||||
* Get all unique product categories
|
||||
*/
|
||||
router.get('/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await Product.getCategories();
|
||||
|
||||
res.json(categories);
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch categories',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Catch-all route for debugging
|
||||
*/
|
||||
router.all('*', (req, res) => {
|
||||
console.log('Unmatched products route:', {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
originalUrl: req.originalUrl,
|
||||
baseUrl: req.baseUrl
|
||||
});
|
||||
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Route not found',
|
||||
message: `Route ${req.method} ${req.originalUrl} not found in products router`,
|
||||
availableRoutes: [
|
||||
'GET /api/products',
|
||||
'GET /api/products/api-test',
|
||||
'POST /api/products/import/excel/preview',
|
||||
'POST /api/products/import/excel'
|
||||
],
|
||||
debug: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
originalUrl: req.originalUrl,
|
||||
baseUrl: req.baseUrl
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user