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} 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} 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} 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} 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;