task_receipts/server/src/printer/serial-printer.ts

201 lines
5.7 KiB
TypeScript

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';
import {
PRINTER_CONFIG,
FONT_SIZES,
BARCODE_CONFIG,
PAPER_CONFIG,
ALIGNMENT,
FONT,
STYLE,
} from './printer-constants';
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.initializeDevice();
}
private initializeDevice() {
try {
this.device = new USB();
const options = { encoding: PRINTER_CONFIG.ENCODING };
this.printer = new Printer(this.device, options);
logger.info('Printer device initialized successfully');
} catch (error) {
logger.error('Failed to initialize printer device:', error);
}
}
private async openPrinter(): Promise<void> {
if (!this.device) {
throw new Error('Printer device not initialized');
}
try {
await new Promise<void>((resolve, reject) => {
this.device?.open((err) => {
if (err) {
logger.error('Failed to open printer:', err);
reject(err);
return;
}
resolve();
});
});
logger.info('Printer opened successfully');
} catch (error) {
logger.error('Failed to open printer:', error);
throw error;
}
}
private async closePrinter(): Promise<void> {
if (!this.printer) {
throw new Error('Printer not initialized');
}
try {
await this.printer.close();
logger.info('Printer closed successfully');
} catch (error) {
logger.error('Failed to close printer:', error);
throw error;
}
}
async getTaskSteps(_db: Knex, task: Task): Promise<Step[]> {
return await this.stepRepository.findByTaskId(task.id);
}
async printTask(task: Task, db: Knex): Promise<void> {
if (!this.printer || !this.device) {
throw new Error('Printer not initialized');
}
const taskSteps = await this.getTaskSteps(db, task);
try {
await this.openPrinter();
// Print header with task ID as barcode
await this.printer
.font(FONT.DEFAULT)
.align(ALIGNMENT.CENTER)
.style(STYLE.BOLD)
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
.text(formatUtils.formatCheckbox(`Task: ${task.name}`))
.text(formatUtils.createBanner('=', PAPER_CONFIG.BANNER_LENGTH))
// .text('')
.align(ALIGNMENT.LEFT);
// Print task ID as barcode
await this.printer
.barcode(task.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
// .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 || 'No instructions provided',
'-'
);
await this.printer
.size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height)
.text(stepSection[0])
.text(stepSection[1])
.size(FONT_SIZES.SMALL.width, FONT_SIZES.SMALL.height)
.text(stepSection[3]);
// .text('');
// Print step ID as barcode
await this.printer
.barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
// .text('');
}
await this.printer
.cut(true, PAPER_CONFIG.CUT_LINES);
await this.closePrinter();
logger.info(`Printed task ${task.id}`);
await this.printHistoryRepo.create({
user_id: PRINTER_CONFIG.DEFAULT_USER_ID,
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<void> {
if (!this.printer || !this.device) {
throw new Error('Printer not initialized');
}
try {
await this.openPrinter();
// 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 || 'No instructions provided'
);
await this.printer
.font(FONT.DEFAULT)
.align(ALIGNMENT.CENTER)
.style(STYLE.BOLD)
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
.text(stepSection[0])
.text(stepSection[1])
.text('')
.align(ALIGNMENT.LEFT)
.size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height)
.text(stepSection[3])
.text('')
.text('');
// Print step ID as barcode
await this.printer
.barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS)
.cut(true, PAPER_CONFIG.CUT_LINES);
await this.closePrinter();
logger.info(`Printed step ${step.id}`);
await this.printHistoryRepo.create({
user_id: PRINTER_CONFIG.DEFAULT_USER_ID,
step_id: step.id,
printed_at: new Date(),
});
} catch (error) {
logger.error('Failed to print step:', error);
throw error;
}
}
}