import { Printer } from '@node-escpos/core'; import USB from '@node-escpos/usb-adapter'; import { Task, Step, Printer as PrinterInterface } from '@shared/index'; import { StepRepository, PrintHistoryRepository } from '../db/repositories'; import { Knex } from 'knex'; import logger from '../logger'; import { formatUtils } from './format-utils'; export class SerialPrinter implements PrinterInterface { private device: USB | null = null; private printer: Printer<[]> | null = null; private printHistoryRepo: PrintHistoryRepository; private stepRepository: StepRepository; constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) { this.printHistoryRepo = printHistoryRepo; this.stepRepository = stepRepo; this.initializePrinter(); } private async initializePrinter() { try { this.device = new USB(); await new Promise((resolve, reject) => { this.device?.open((err) => { if (err) { logger.error('Failed to open printer:', err); reject(err); return; } resolve(); }); }); const options = { encoding: 'CP437' }; this.printer = new Printer(this.device, options); logger.info('Printer initialized successfully'); } catch (error) { logger.error('Failed to initialize printer:', error); } } async getTaskSteps(_db: Knex, task: Task): Promise { return await this.stepRepository.findByTaskId(task.id); } async printTask(task: Task, db: Knex): Promise { if (!this.printer || !this.device) { throw new Error('Printer not initialized'); } const taskSteps = await this.getTaskSteps(db, task); try { // Print header with task ID as barcode await this.printer .font('a') .align('ct') .style('b') .size(1, 1) // Normal size (0.08 x 2.13 mm) .text(formatUtils.formatCheckbox(`Task: ${task.name}`)) .text(formatUtils.createBanner('=', 32)) .text('') .align('lt'); // Print task ID as barcode await this.printer .barcode(task.id.toString(), 'CODE128', { width: 2, height: 50 }) .text('') .text(''); // Print steps for (let i = 0; i < taskSteps.length; i++) { const step = taskSteps[i]; const stepSection = formatUtils.formatSection( formatUtils.formatStepHeader(step.name, i + 1, task.name, true), step.instructions, '-' ); await this.printer .size(1, 1) // Normal size for step header .text(stepSection[0]) // Header with checkbox .text(stepSection[1]) // Banner .size(0, 0) // Smaller size for instructions .text(stepSection[3]) // Instructions .text(''); // Print step ID as barcode await this.printer .barcode(step.id.toString(), 'CODE128', { width: 2, height: 50 }) .text(''); } await this.printer .cut(true, 2) .close(); logger.info(`Printed task ${task.id}`); await this.printHistoryRepo.create({ user_id: 1, // Replace with actual user ID if available task_id: task.id, printed_at: new Date(), }); } catch (error) { logger.error('Failed to print task:', error); throw error; } } async printStep(step: Step, db: Knex): Promise { if (!this.printer || !this.device) { throw new Error('Printer not initialized'); } try { // Get the task name for context const task = await this.stepRepository.findTaskById(step.id); const stepNumber = await this.stepRepository.findStepNumber(step.id); const stepSection = formatUtils.formatSection( formatUtils.formatStepHeader(step.name, stepNumber, task?.name), step.instructions ); await this.printer .font('a') .align('ct') .style('b') .size(1, 1) // Normal size (0.08 x 2.13 mm) .text(stepSection[0]) // Header with checkbox .text(stepSection[1]) // Banner .text('') .align('lt') .size(0, 0) // Smaller size for instructions .text(stepSection[3]) // Instructions .text('') .text(''); // Print step ID as barcode await this.printer .barcode(step.id.toString(), 'CODE128', { width: 2, height: 50 }) .cut(true, 2) .close(); logger.info(`Printed step ${step.id}`); await this.printHistoryRepo.create({ user_id: 1, // Replace with actual user ID if available step_id: step.id, printed_at: new Date(), }); } catch (error) { logger.error('Failed to print step:', error); throw error; } } }