Initial commit: Inventory Barcode System
This commit is contained in:
462
services/PrintableLayoutService.js
Normal file
462
services/PrintableLayoutService.js
Normal 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;
|
||||
Reference in New Issue
Block a user