969 lines
26 KiB
JavaScript
969 lines
26 KiB
JavaScript
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; |