Initial commit: Inventory Barcode System

This commit is contained in:
2025-07-22 20:24:51 -04:00
commit 511b01748d
63 changed files with 26932 additions and 0 deletions

611
routes/codes.js Normal file
View 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;