Initial commit: Inventory Barcode System
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user