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

View File

@ -0,0 +1,462 @@
const { jsPDF } = require('jspdf');
const CodeGenerationService = require('./CodeGenerationService');
/**
* Service for generating printable layouts with barcodes and QR codes
*/
class PrintableLayoutService {
constructor() {
this.codeGenService = new CodeGenerationService();
// Standard label sizes in mm
this.labelSizes = {
'avery-5160': { width: 66.7, height: 25.4, columns: 3, rows: 10 },
'avery-5161': { width: 101.6, height: 25.4, columns: 2, rows: 10 },
'avery-5162': { width: 101.6, height: 33.9, columns: 2, rows: 7 },
'avery-5163': { width: 101.6, height: 50.8, columns: 2, rows: 5 },
'avery-5164': { width: 101.6, height: 67.7, columns: 2, rows: 3 },
'custom': { width: 50, height: 25, columns: 4, rows: 8 }
};
this.defaultLayoutOptions = {
labelSize: 'avery-5160',
includeBarcode: true,
includeQRCode: false,
includeProductCode: true,
includeDescription: true,
fontSize: 8,
barcodeHeight: 15,
qrCodeSize: 20,
margin: 2,
orientation: 'portrait'
};
}
/**
* Generate printable PDF layout with barcodes/QR codes
* @param {Array} products - Array of product objects
* @param {Object} options - Layout options
* @returns {Promise<Buffer>} PDF buffer
*/
async generatePrintableLayout(products, options = {}) {
try {
if (!Array.isArray(products) || products.length === 0) {
throw new Error('Products array is required and cannot be empty');
}
const layoutOptions = { ...this.defaultLayoutOptions, ...options };
const labelSize = this.labelSizes[layoutOptions.labelSize] || this.labelSizes['custom'];
// Create PDF document
const pdf = new jsPDF({
orientation: layoutOptions.orientation,
unit: 'mm',
format: 'a4'
});
// Calculate page dimensions
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
// Calculate starting positions
const startX = (pageWidth - (labelSize.columns * labelSize.width)) / 2;
const startY = 10;
let currentProduct = 0;
let currentPage = 1;
while (currentProduct < products.length) {
if (currentPage > 1) {
pdf.addPage();
}
// Generate codes for current page products
const pageProducts = products.slice(
currentProduct,
currentProduct + (labelSize.columns * labelSize.rows)
);
const generatedCodes = await this._generateCodesForProducts(pageProducts, layoutOptions);
// Draw labels on current page
await this._drawLabelsOnPage(pdf, generatedCodes, labelSize, layoutOptions, startX, startY);
currentProduct += pageProducts.length;
currentPage++;
}
// Return PDF as buffer
const pdfBuffer = Buffer.from(pdf.output('arraybuffer'));
return {
success: true,
data: pdfBuffer,
metadata: {
totalProducts: products.length,
totalPages: currentPage - 1,
labelSize: layoutOptions.labelSize,
format: 'PDF'
}
};
} catch (error) {
return {
success: false,
error: error.message,
products: products
};
}
}
/**
* Generate custom layout template
* @param {Object} templateOptions - Template configuration
* @returns {Promise<Object>} Template configuration
*/
async generateCustomTemplate(templateOptions) {
try {
const {
width = 50,
height = 25,
columns = 4,
rows = 8,
name = 'custom-template'
} = templateOptions;
const template = {
name,
width,
height,
columns,
rows,
totalLabels: columns * rows,
pageSize: 'a4'
};
// Validate template fits on page
const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
if (width * columns > pageWidth - 10) {
throw new Error('Template width exceeds page width');
}
if (height * rows > pageHeight - 20) {
throw new Error('Template height exceeds page height');
}
return {
success: true,
template,
validation: {
fitsOnPage: true,
maxLabelsPerPage: columns * rows
}
};
} catch (error) {
return {
success: false,
error: error.message,
templateOptions
};
}
}
/**
* Get available label sizes
* @returns {Object} Available label sizes
*/
getAvailableLabelSizes() {
return { ...this.labelSizes };
}
/**
* Generate preview of layout
* @param {Array} sampleProducts - Sample products for preview
* @param {Object} options - Layout options
* @returns {Promise<Object>} Preview information
*/
async generateLayoutPreview(sampleProducts, options = {}) {
try {
const layoutOptions = { ...this.defaultLayoutOptions, ...options };
const labelSize = this.labelSizes[layoutOptions.labelSize] || this.labelSizes['custom'];
const preview = {
labelSize: layoutOptions.labelSize,
dimensions: labelSize,
labelsPerPage: labelSize.columns * labelSize.rows,
totalPages: Math.ceil(sampleProducts.length / (labelSize.columns * labelSize.rows)),
includeBarcode: layoutOptions.includeBarcode,
includeQRCode: layoutOptions.includeQRCode,
includeProductCode: layoutOptions.includeProductCode,
includeDescription: layoutOptions.includeDescription
};
return {
success: true,
preview
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Generate codes for products
* @private
* @param {Array} products - Products to generate codes for
* @param {Object} options - Generation options
* @returns {Promise<Array>} Generated codes
*/
async _generateCodesForProducts(products, options) {
const generatedCodes = [];
for (const product of products) {
const codeData = {
product,
barcode: null,
qrCode: null
};
if (options.includeBarcode) {
const barcodeResult = await this.codeGenService.generateBarcode(
product.product_code,
options.barcodeFormat || 'CODE128'
);
codeData.barcode = barcodeResult.success ? barcodeResult : null;
}
if (options.includeQRCode) {
const qrResult = await this.codeGenService.generateQRCode(product);
codeData.qrCode = qrResult.success ? qrResult : null;
}
generatedCodes.push(codeData);
}
return generatedCodes;
}
/**
* Draw labels on PDF page
* @private
* @param {jsPDF} pdf - PDF document
* @param {Array} generatedCodes - Generated codes data
* @param {Object} labelSize - Label dimensions
* @param {Object} options - Layout options
* @param {number} startX - Starting X position
* @param {number} startY - Starting Y position
*/
async _drawLabelsOnPage(pdf, generatedCodes, labelSize, options, startX, startY) {
let labelIndex = 0;
for (let row = 0; row < labelSize.rows && labelIndex < generatedCodes.length; row++) {
for (let col = 0; col < labelSize.columns && labelIndex < generatedCodes.length; col++) {
const x = startX + (col * labelSize.width);
const y = startY + (row * labelSize.height);
await this._drawSingleLabel(
pdf,
generatedCodes[labelIndex],
x,
y,
labelSize.width,
labelSize.height,
options
);
labelIndex++;
}
}
}
/**
* Draw a single label
* @private
* @param {jsPDF} pdf - PDF document
* @param {Object} codeData - Code data for the label
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} width - Label width
* @param {number} height - Label height
* @param {Object} options - Layout options
*/
async _drawSingleLabel(pdf, codeData, x, y, width, height, options) {
const { product, barcode, qrCode } = codeData;
const margin = options.margin;
// Draw label border (optional, for debugging)
if (options.showBorders) {
pdf.setDrawColor(200, 200, 200);
pdf.rect(x, y, width, height);
}
let currentY = y + margin;
const contentWidth = width - (2 * margin);
// Draw product code
if (options.includeProductCode && product.product_code) {
pdf.setFontSize(options.fontSize + 2);
pdf.setFont('helvetica', 'bold');
const codeText = this._truncateText(product.product_code, contentWidth, pdf);
pdf.text(codeText, x + margin, currentY + 4);
currentY += 6;
}
// Draw barcode
if (options.includeBarcode && barcode && barcode.data) {
try {
const barcodeHeight = options.barcodeHeight;
const barcodeWidth = Math.min(contentWidth, 40);
// Convert base64 to image and add to PDF
pdf.addImage(
barcode.data,
'PNG',
x + margin + (contentWidth - barcodeWidth) / 2,
currentY,
barcodeWidth,
barcodeHeight
);
currentY += barcodeHeight + 2;
} catch (error) {
console.warn('Failed to add barcode to PDF:', error.message);
}
}
// Draw QR code
if (options.includeQRCode && qrCode && qrCode.data) {
try {
const qrSize = options.qrCodeSize;
pdf.addImage(
qrCode.data,
'PNG',
x + margin + (contentWidth - qrSize) / 2,
currentY,
qrSize,
qrSize
);
currentY += qrSize + 2;
} catch (error) {
console.warn('Failed to add QR code to PDF:', error.message);
}
}
// Draw description
if (options.includeDescription && product.description) {
pdf.setFontSize(options.fontSize - 1);
pdf.setFont('helvetica', 'normal');
const descText = this._truncateText(product.description, contentWidth, pdf);
// Handle multi-line description
const lines = pdf.splitTextToSize(descText, contentWidth);
const maxLines = Math.floor((y + height - currentY - margin) / 3);
const displayLines = lines.slice(0, maxLines);
displayLines.forEach((line, index) => {
if (currentY + (index * 3) < y + height - margin) {
pdf.text(line, x + margin, currentY + (index * 3) + 3);
}
});
}
}
/**
* Truncate text to fit within specified width
* @private
* @param {string} text - Text to truncate
* @param {number} maxWidth - Maximum width in mm
* @param {jsPDF} pdf - PDF document for text measurement
* @returns {string} Truncated text
*/
_truncateText(text, maxWidth, pdf) {
if (!text) return '';
const textWidth = pdf.getTextWidth(text);
if (textWidth <= maxWidth) {
return text;
}
// Binary search for optimal length
let left = 0;
let right = text.length;
let result = text;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const truncated = text.substring(0, mid) + '...';
const truncatedWidth = pdf.getTextWidth(truncated);
if (truncatedWidth <= maxWidth) {
result = truncated;
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
/**
* Export layout configuration
* @param {Object} layoutConfig - Layout configuration to export
* @returns {Object} Exportable configuration
*/
exportLayoutConfiguration(layoutConfig) {
return {
version: '1.0',
timestamp: new Date().toISOString(),
configuration: {
...layoutConfig,
availableSizes: Object.keys(this.labelSizes)
}
};
}
/**
* Import layout configuration
* @param {Object} configData - Configuration data to import
* @returns {Object} Validation result
*/
importLayoutConfiguration(configData) {
try {
if (!configData.configuration) {
throw new Error('Invalid configuration format');
}
const config = configData.configuration;
// Validate required fields
const requiredFields = ['labelSize'];
for (const field of requiredFields) {
if (!(field in config)) {
throw new Error(`Missing required field: ${field}`);
}
}
// Validate label size
if (!this.labelSizes[config.labelSize]) {
throw new Error(`Unsupported label size: ${config.labelSize}`);
}
return {
success: true,
configuration: config,
message: 'Configuration imported successfully'
};
} catch (error) {
return {
success: false,
error: error.message,
configData
};
}
}
}
module.exports = PrintableLayoutService;