Initial commit: Inventory Barcode System
This commit is contained in:
795
services/ExcelExportService.js
Normal file
795
services/ExcelExportService.js
Normal file
@ -0,0 +1,795 @@
|
||||
const XLSX = require('xlsx');
|
||||
const database = require('../models/database');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
/**
|
||||
* Service for exporting inventory data to Excel files
|
||||
*/
|
||||
class ExcelExportService {
|
||||
constructor() {
|
||||
this.exportDirectory = path.join(__dirname, '..', 'data', 'exports');
|
||||
this.ensureExportDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure export directory exists
|
||||
*/
|
||||
async ensureExportDirectory() {
|
||||
try {
|
||||
await fs.mkdir(this.exportDirectory, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to create export directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export inventory data to Excel format
|
||||
* @param {Object} options - Export options
|
||||
* @returns {Object} Export results with file path and metadata
|
||||
*/
|
||||
async exportInventoryToExcel(options = {}) {
|
||||
try {
|
||||
const {
|
||||
format = 'xlsx',
|
||||
includeHistory = false,
|
||||
filters = {},
|
||||
filename,
|
||||
originalFileBuffer = null,
|
||||
preserveFormatting = true
|
||||
} = options;
|
||||
|
||||
// Get inventory data
|
||||
const inventoryData = await this.getInventoryData(filters);
|
||||
|
||||
let workbook;
|
||||
let exportResult;
|
||||
|
||||
if (originalFileBuffer && preserveFormatting) {
|
||||
// Preserve original Excel structure and update data
|
||||
exportResult = await this.updateOriginalExcelFile(originalFileBuffer, inventoryData, options);
|
||||
} else {
|
||||
// Create new Excel file with standard format
|
||||
exportResult = await this.createNewExcelFile(inventoryData, options);
|
||||
}
|
||||
|
||||
// Generate filename if not provided
|
||||
const exportFilename = filename || this.generateExportFilename(format);
|
||||
const exportPath = path.join(this.exportDirectory, exportFilename);
|
||||
|
||||
// Write file to disk
|
||||
await this.writeExcelFile(exportResult.workbook, exportPath, format);
|
||||
|
||||
// Create export session record
|
||||
const sessionId = await this.createExportSession({
|
||||
filename: exportFilename,
|
||||
totalRecords: inventoryData.length,
|
||||
filters: filters,
|
||||
includeHistory: includeHistory
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filePath: exportPath,
|
||||
filename: exportFilename,
|
||||
sessionId: sessionId,
|
||||
recordCount: inventoryData.length,
|
||||
exportDate: new Date().toISOString(),
|
||||
metadata: exportResult.metadata
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
filePath: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventory data with optional filtering
|
||||
* @param {Object} filters - Filter options
|
||||
* @returns {Array} Array of inventory records with product information
|
||||
*/
|
||||
async getInventoryData(filters = {}) {
|
||||
const db = database.getDatabase();
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
p.product_code,
|
||||
p.description,
|
||||
p.category,
|
||||
p.unit_of_measure,
|
||||
i.current_level,
|
||||
i.minimum_level,
|
||||
i.maximum_level,
|
||||
i.last_updated,
|
||||
i.updated_by,
|
||||
p.created_at as product_created_at,
|
||||
p.updated_at as product_updated_at,
|
||||
CASE
|
||||
WHEN i.current_level <= i.minimum_level THEN 'Low Stock'
|
||||
WHEN i.current_level <= i.minimum_level * 1.5 THEN 'Warning'
|
||||
ELSE 'Normal'
|
||||
END as stock_status
|
||||
FROM products p
|
||||
LEFT JOIN inventory i ON p.id = i.product_id
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
const conditions = [];
|
||||
|
||||
// Apply filters
|
||||
if (filters.category) {
|
||||
conditions.push('p.category = ?');
|
||||
params.push(filters.category);
|
||||
}
|
||||
|
||||
if (filters.stockStatus) {
|
||||
switch (filters.stockStatus) {
|
||||
case 'low':
|
||||
conditions.push('i.current_level <= i.minimum_level');
|
||||
break;
|
||||
case 'warning':
|
||||
conditions.push('i.current_level <= i.minimum_level * 1.5 AND i.current_level > i.minimum_level');
|
||||
break;
|
||||
case 'normal':
|
||||
conditions.push('i.current_level > i.minimum_level * 1.5');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.updatedSince) {
|
||||
conditions.push('i.last_updated >= ?');
|
||||
params.push(filters.updatedSince);
|
||||
}
|
||||
|
||||
if (filters.productCodes && filters.productCodes.length > 0) {
|
||||
const placeholders = filters.productCodes.map(() => '?').join(',');
|
||||
conditions.push(`p.product_code IN (${placeholders})`);
|
||||
params.push(...filters.productCodes);
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query += ' WHERE ' + conditions.join(' AND ');
|
||||
}
|
||||
|
||||
query += ' ORDER BY p.product_code';
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update original Excel file with current inventory data
|
||||
* @param {Buffer} originalFileBuffer - Original Excel file buffer
|
||||
* @param {Array} inventoryData - Current inventory data
|
||||
* @param {Object} options - Update options
|
||||
* @returns {Object} Updated workbook and metadata
|
||||
*/
|
||||
async updateOriginalExcelFile(originalFileBuffer, inventoryData, options = {}) {
|
||||
try {
|
||||
// Read original workbook
|
||||
const workbook = XLSX.read(originalFileBuffer, { type: 'buffer', cellStyles: true });
|
||||
const sheetName = options.sheetName || workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
|
||||
if (!worksheet) {
|
||||
throw new Error(`Sheet "${sheetName}" not found in original file`);
|
||||
}
|
||||
|
||||
// Get original data structure
|
||||
const originalData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
||||
|
||||
if (originalData.length === 0) {
|
||||
throw new Error('Original Excel file appears to be empty');
|
||||
}
|
||||
|
||||
// Detect column mappings from original file
|
||||
const headerRow = originalData[0];
|
||||
const columnMapping = this.detectColumnMappings(headerRow);
|
||||
|
||||
// Create lookup map for current inventory data
|
||||
const inventoryMap = new Map();
|
||||
inventoryData.forEach(item => {
|
||||
inventoryMap.set(item.product_code, item);
|
||||
});
|
||||
|
||||
// Update data rows while preserving structure
|
||||
const updatedData = [...originalData];
|
||||
let updatedCount = 0;
|
||||
let addedCount = 0;
|
||||
|
||||
// Update existing rows
|
||||
for (let i = 1; i < originalData.length; i++) {
|
||||
const row = [...originalData[i]];
|
||||
const productCode = this.getCellValue(row, columnMapping.productCode);
|
||||
|
||||
if (productCode && inventoryMap.has(productCode)) {
|
||||
const inventoryItem = inventoryMap.get(productCode);
|
||||
|
||||
// Update quantity/current level
|
||||
if (columnMapping.quantity !== null) {
|
||||
row[columnMapping.quantity] = inventoryItem.current_level;
|
||||
}
|
||||
|
||||
// Update description if it exists and is different
|
||||
if (columnMapping.description !== null && inventoryItem.description) {
|
||||
row[columnMapping.description] = inventoryItem.description;
|
||||
}
|
||||
|
||||
// Update category if it exists
|
||||
if (columnMapping.category !== null && inventoryItem.category) {
|
||||
row[columnMapping.category] = inventoryItem.category;
|
||||
}
|
||||
|
||||
// Add timestamp column if requested
|
||||
if (options.includeTimestamp) {
|
||||
if (columnMapping.lastUpdated === null) {
|
||||
// Add new column for timestamp
|
||||
if (i === 1) {
|
||||
// Add header
|
||||
updatedData[0].push('Last Updated');
|
||||
columnMapping.lastUpdated = updatedData[0].length - 1;
|
||||
}
|
||||
row.push(inventoryItem.last_updated || '');
|
||||
} else {
|
||||
row[columnMapping.lastUpdated] = inventoryItem.last_updated || '';
|
||||
}
|
||||
}
|
||||
|
||||
updatedData[i] = row;
|
||||
updatedCount++;
|
||||
inventoryMap.delete(productCode); // Remove from map to track what's been processed
|
||||
}
|
||||
}
|
||||
|
||||
// Add new products that weren't in original file
|
||||
if (options.includeNewProducts && inventoryMap.size > 0) {
|
||||
inventoryMap.forEach(inventoryItem => {
|
||||
const newRow = new Array(headerRow.length).fill('');
|
||||
|
||||
if (columnMapping.productCode !== null) {
|
||||
newRow[columnMapping.productCode] = inventoryItem.product_code;
|
||||
}
|
||||
if (columnMapping.description !== null) {
|
||||
newRow[columnMapping.description] = inventoryItem.description || '';
|
||||
}
|
||||
if (columnMapping.category !== null) {
|
||||
newRow[columnMapping.category] = inventoryItem.category || '';
|
||||
}
|
||||
if (columnMapping.quantity !== null) {
|
||||
newRow[columnMapping.quantity] = inventoryItem.current_level;
|
||||
}
|
||||
if (columnMapping.lastUpdated !== null) {
|
||||
newRow[columnMapping.lastUpdated] = inventoryItem.last_updated || '';
|
||||
}
|
||||
|
||||
updatedData.push(newRow);
|
||||
addedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
// Convert back to worksheet
|
||||
const newWorksheet = XLSX.utils.aoa_to_sheet(updatedData);
|
||||
|
||||
// Preserve column widths and formatting where possible
|
||||
if (worksheet['!cols']) {
|
||||
newWorksheet['!cols'] = worksheet['!cols'];
|
||||
}
|
||||
if (worksheet['!rows']) {
|
||||
newWorksheet['!rows'] = worksheet['!rows'];
|
||||
}
|
||||
|
||||
// Replace the worksheet in workbook
|
||||
workbook.Sheets[sheetName] = newWorksheet;
|
||||
|
||||
return {
|
||||
workbook: workbook,
|
||||
metadata: {
|
||||
originalRows: originalData.length - 1,
|
||||
updatedRows: updatedCount,
|
||||
addedRows: addedCount,
|
||||
preservedFormatting: true,
|
||||
sheetName: sheetName
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update original Excel file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new Excel file with inventory data
|
||||
* @param {Array} inventoryData - Inventory data to export
|
||||
* @param {Object} options - Export options
|
||||
* @returns {Object} New workbook and metadata
|
||||
*/
|
||||
async createNewExcelFile(inventoryData, options = {}) {
|
||||
const { includeHistory = false, includeAuditInfo = true } = options;
|
||||
|
||||
// Create main inventory sheet
|
||||
const inventorySheet = this.createInventorySheet(inventoryData, includeAuditInfo);
|
||||
|
||||
// Create workbook
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, inventorySheet, 'Inventory');
|
||||
|
||||
// Add history sheet if requested
|
||||
if (includeHistory) {
|
||||
const historySheet = await this.createHistorySheet(inventoryData);
|
||||
XLSX.utils.book_append_sheet(workbook, historySheet, 'History');
|
||||
}
|
||||
|
||||
// Add summary sheet
|
||||
const summarySheet = this.createSummarySheet(inventoryData);
|
||||
XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary');
|
||||
|
||||
return {
|
||||
workbook: workbook,
|
||||
metadata: {
|
||||
sheets: ['Inventory', ...(includeHistory ? ['History'] : []), 'Summary'],
|
||||
recordCount: inventoryData.length,
|
||||
includeHistory: includeHistory,
|
||||
includeAuditInfo: includeAuditInfo
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create inventory worksheet
|
||||
* @param {Array} inventoryData - Inventory data
|
||||
* @param {boolean} includeAuditInfo - Whether to include audit information
|
||||
* @returns {Object} Worksheet object
|
||||
*/
|
||||
createInventorySheet(inventoryData, includeAuditInfo = true) {
|
||||
const headers = [
|
||||
'Product Code',
|
||||
'Description',
|
||||
'Category',
|
||||
'Unit of Measure',
|
||||
'Current Level',
|
||||
'Minimum Level',
|
||||
'Maximum Level',
|
||||
'Stock Status'
|
||||
];
|
||||
|
||||
if (includeAuditInfo) {
|
||||
headers.push('Last Updated', 'Updated By');
|
||||
}
|
||||
|
||||
// Create data rows
|
||||
const rows = [headers];
|
||||
|
||||
inventoryData.forEach(item => {
|
||||
const row = [
|
||||
item.product_code || '',
|
||||
item.description || '',
|
||||
item.category || '',
|
||||
item.unit_of_measure || '',
|
||||
item.current_level || 0,
|
||||
item.minimum_level || 0,
|
||||
item.maximum_level || '',
|
||||
item.stock_status || 'Normal'
|
||||
];
|
||||
|
||||
if (includeAuditInfo) {
|
||||
row.push(
|
||||
item.last_updated ? new Date(item.last_updated).toLocaleString() : '',
|
||||
item.updated_by || ''
|
||||
);
|
||||
}
|
||||
|
||||
rows.push(row);
|
||||
});
|
||||
|
||||
// Create worksheet
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(rows);
|
||||
|
||||
// Set column widths
|
||||
const columnWidths = [
|
||||
{ wch: 15 }, // Product Code
|
||||
{ wch: 30 }, // Description
|
||||
{ wch: 15 }, // Category
|
||||
{ wch: 12 }, // Unit of Measure
|
||||
{ wch: 12 }, // Current Level
|
||||
{ wch: 12 }, // Minimum Level
|
||||
{ wch: 12 }, // Maximum Level
|
||||
{ wch: 12 } // Stock Status
|
||||
];
|
||||
|
||||
if (includeAuditInfo) {
|
||||
columnWidths.push(
|
||||
{ wch: 18 }, // Last Updated
|
||||
{ wch: 15 } // Updated By
|
||||
);
|
||||
}
|
||||
|
||||
worksheet['!cols'] = columnWidths;
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create history worksheet
|
||||
* @param {Array} inventoryData - Inventory data for getting history
|
||||
* @returns {Object} History worksheet
|
||||
*/
|
||||
async createHistorySheet(inventoryData) {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Get product IDs for history lookup
|
||||
const productCodes = inventoryData.map(item => item.product_code);
|
||||
const placeholders = productCodes.map(() => '?').join(',');
|
||||
|
||||
const historyQuery = `
|
||||
SELECT
|
||||
p.product_code,
|
||||
p.description,
|
||||
h.old_level,
|
||||
h.new_level,
|
||||
h.change_reason,
|
||||
h.updated_by,
|
||||
h.updated_at
|
||||
FROM inventory_history h
|
||||
JOIN products p ON h.product_id = p.id
|
||||
WHERE p.product_code IN (${placeholders})
|
||||
ORDER BY h.updated_at DESC, p.product_code
|
||||
LIMIT 1000
|
||||
`;
|
||||
|
||||
const historyData = db.prepare(historyQuery).all(...productCodes);
|
||||
|
||||
const headers = [
|
||||
'Product Code',
|
||||
'Description',
|
||||
'Old Level',
|
||||
'New Level',
|
||||
'Change Reason',
|
||||
'Updated By',
|
||||
'Updated At'
|
||||
];
|
||||
|
||||
const rows = [headers];
|
||||
|
||||
historyData.forEach(record => {
|
||||
rows.push([
|
||||
record.product_code,
|
||||
record.description || '',
|
||||
record.old_level || 0,
|
||||
record.new_level || 0,
|
||||
record.change_reason || '',
|
||||
record.updated_by || '',
|
||||
new Date(record.updated_at).toLocaleString()
|
||||
]);
|
||||
});
|
||||
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(rows);
|
||||
|
||||
// Set column widths
|
||||
worksheet['!cols'] = [
|
||||
{ wch: 15 }, // Product Code
|
||||
{ wch: 30 }, // Description
|
||||
{ wch: 10 }, // Old Level
|
||||
{ wch: 10 }, // New Level
|
||||
{ wch: 20 }, // Change Reason
|
||||
{ wch: 15 }, // Updated By
|
||||
{ wch: 18 } // Updated At
|
||||
];
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create summary worksheet
|
||||
* @param {Array} inventoryData - Inventory data for summary
|
||||
* @returns {Object} Summary worksheet
|
||||
*/
|
||||
createSummarySheet(inventoryData) {
|
||||
const summary = {
|
||||
totalProducts: inventoryData.length,
|
||||
lowStockItems: inventoryData.filter(item => item.stock_status === 'Low Stock').length,
|
||||
warningItems: inventoryData.filter(item => item.stock_status === 'Warning').length,
|
||||
normalItems: inventoryData.filter(item => item.stock_status === 'Normal').length,
|
||||
totalInventoryValue: inventoryData.reduce((sum, item) => sum + (item.current_level || 0), 0),
|
||||
categories: {}
|
||||
};
|
||||
|
||||
// Calculate category breakdown
|
||||
inventoryData.forEach(item => {
|
||||
const category = item.category || 'Uncategorized';
|
||||
if (!summary.categories[category]) {
|
||||
summary.categories[category] = {
|
||||
count: 0,
|
||||
totalStock: 0,
|
||||
lowStock: 0
|
||||
};
|
||||
}
|
||||
summary.categories[category].count++;
|
||||
summary.categories[category].totalStock += item.current_level || 0;
|
||||
if (item.stock_status === 'Low Stock') {
|
||||
summary.categories[category].lowStock++;
|
||||
}
|
||||
});
|
||||
|
||||
// Create summary data
|
||||
const rows = [
|
||||
['Inventory Export Summary'],
|
||||
[''],
|
||||
['Export Date', new Date().toLocaleString()],
|
||||
['Total Products', summary.totalProducts],
|
||||
['Total Inventory Items', summary.totalInventoryValue],
|
||||
[''],
|
||||
['Stock Status Breakdown'],
|
||||
['Low Stock Items', summary.lowStockItems],
|
||||
['Warning Items', summary.warningItems],
|
||||
['Normal Stock Items', summary.normalItems],
|
||||
[''],
|
||||
['Category Breakdown'],
|
||||
['Category', 'Product Count', 'Total Stock', 'Low Stock Items']
|
||||
];
|
||||
|
||||
// Add category data
|
||||
Object.entries(summary.categories).forEach(([category, data]) => {
|
||||
rows.push([category, data.count, data.totalStock, data.lowStock]);
|
||||
});
|
||||
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(rows);
|
||||
|
||||
// Set column widths
|
||||
worksheet['!cols'] = [
|
||||
{ wch: 25 },
|
||||
{ wch: 15 },
|
||||
{ wch: 15 },
|
||||
{ wch: 15 }
|
||||
];
|
||||
|
||||
return worksheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect column mappings from header row
|
||||
* @param {Array} headerRow - Header row from Excel
|
||||
* @returns {Object} Column mapping object
|
||||
*/
|
||||
detectColumnMappings(headerRow) {
|
||||
const mapping = {
|
||||
productCode: null,
|
||||
description: null,
|
||||
quantity: null,
|
||||
category: null,
|
||||
lastUpdated: null
|
||||
};
|
||||
|
||||
const patterns = {
|
||||
productCode: ['product_code', 'productcode', 'product code', 'code', 'item_code', 'sku'],
|
||||
description: ['description', 'desc', 'product_description', 'name', 'product_name'],
|
||||
quantity: ['quantity', 'qty', 'current_quantity', 'inventory', 'stock', 'current_level'],
|
||||
category: ['category', 'cat', 'product_category', 'type', 'group'],
|
||||
lastUpdated: ['last_updated', 'lastupdated', 'updated', 'timestamp', 'date_updated']
|
||||
};
|
||||
|
||||
const normalizedHeaders = headerRow.map(header =>
|
||||
typeof header === 'string' ? header.toLowerCase().trim() : ''
|
||||
);
|
||||
|
||||
Object.keys(patterns).forEach(columnType => {
|
||||
const columnPatterns = patterns[columnType];
|
||||
|
||||
for (let i = 0; i < normalizedHeaders.length; i++) {
|
||||
const header = normalizedHeaders[i];
|
||||
|
||||
if (columnPatterns.includes(header)) {
|
||||
mapping[columnType] = i;
|
||||
break;
|
||||
}
|
||||
|
||||
const partialMatch = columnPatterns.find(pattern =>
|
||||
header.includes(pattern) || pattern.includes(header)
|
||||
);
|
||||
|
||||
if (partialMatch && mapping[columnType] === null) {
|
||||
mapping[columnType] = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cell value safely
|
||||
* @param {Array} row - Data row
|
||||
* @param {number} columnIndex - Column index
|
||||
* @returns {string} Cell value or empty string
|
||||
*/
|
||||
getCellValue(row, columnIndex) {
|
||||
if (columnIndex === null || columnIndex >= row.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const value = row[columnIndex];
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write Excel file to disk
|
||||
* @param {Object} workbook - Excel workbook object
|
||||
* @param {string} filePath - Output file path
|
||||
* @param {string} format - File format (xlsx, xls, csv)
|
||||
*/
|
||||
async writeExcelFile(workbook, filePath, format = 'xlsx') {
|
||||
try {
|
||||
const writeOptions = {
|
||||
bookType: format,
|
||||
type: 'buffer'
|
||||
};
|
||||
|
||||
const buffer = XLSX.write(workbook, writeOptions);
|
||||
await fs.writeFile(filePath, buffer);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write Excel file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate export filename with timestamp
|
||||
* @param {string} format - File format
|
||||
* @returns {string} Generated filename
|
||||
*/
|
||||
generateExportFilename(format = 'xlsx') {
|
||||
const timestamp = new Date().toISOString()
|
||||
.replace(/[:.]/g, '-')
|
||||
.replace('T', '_')
|
||||
.substring(0, 19);
|
||||
|
||||
return `inventory_export_${timestamp}.${format}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create export session record for tracking
|
||||
* @param {Object} sessionData - Export session data
|
||||
* @returns {number} Session ID
|
||||
*/
|
||||
async createExportSession(sessionData) {
|
||||
try {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Create export_sessions table if it doesn't exist
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS export_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename VARCHAR(255),
|
||||
total_records INTEGER,
|
||||
export_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
filters TEXT,
|
||||
include_history BOOLEAN DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'completed'
|
||||
)
|
||||
`);
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO export_sessions (filename, total_records, filters, include_history)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
sessionData.filename,
|
||||
sessionData.totalRecords,
|
||||
JSON.stringify(sessionData.filters || {}),
|
||||
sessionData.includeHistory ? 1 : 0
|
||||
);
|
||||
|
||||
return result.lastInsertRowid;
|
||||
} catch (error) {
|
||||
console.error('Failed to create export session:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get export history
|
||||
* @param {Object} options - Query options
|
||||
* @returns {Array} Export history records
|
||||
*/
|
||||
async getExportHistory(options = {}) {
|
||||
try {
|
||||
const db = database.getDatabase();
|
||||
const { limit = 50, offset = 0 } = options;
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM export_sessions
|
||||
ORDER BY export_date DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
return stmt.all(limit, offset);
|
||||
} catch (error) {
|
||||
console.error('Failed to get export history:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old export files
|
||||
* @param {number} maxAgeHours - Maximum age in hours
|
||||
* @returns {Object} Cleanup results
|
||||
*/
|
||||
async cleanupOldExports(maxAgeHours = 24) {
|
||||
try {
|
||||
const files = await fs.readdir(this.exportDirectory);
|
||||
const cutoffTime = Date.now() - (maxAgeHours * 60 * 60 * 1000);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(this.exportDirectory, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
if (stats.mtime.getTime() < cutoffTime) {
|
||||
await fs.unlink(filePath);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: deletedCount,
|
||||
message: `Cleaned up ${deletedCount} old export files`
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear export history
|
||||
* @returns {Object} Result with success status and deleted count
|
||||
*/
|
||||
async clearExportHistory() {
|
||||
const db = database.getDatabase();
|
||||
|
||||
try {
|
||||
// Get count before deletion
|
||||
const countStmt = db.prepare('SELECT COUNT(*) as count FROM export_sessions');
|
||||
const countResult = countStmt.get();
|
||||
const deletedCount = countResult.count;
|
||||
|
||||
// Clear export sessions table
|
||||
const deleteStmt = db.prepare('DELETE FROM export_sessions');
|
||||
deleteStmt.run();
|
||||
|
||||
// Clean up export files directory
|
||||
try {
|
||||
const files = await fs.readdir(this.exportDirectory);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.xlsx') || file.endsWith('.xls')) {
|
||||
await fs.unlink(path.join(this.exportDirectory, file));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Warning: Could not clean up export files:', error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error clearing export history:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExcelExportService;
|
||||
Reference in New Issue
Block a user