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

201 lines
5.7 KiB
TypeScript
Raw Normal View History

2025-06-15 00:22:52 +00:00
import { Printer } from '@node-escpos/core';
import USB from '@node-escpos/usb-adapter';
2025-06-14 22:27:33 +00:00
import { Task, Step, Printer as PrinterInterface } from '@shared/index';
2025-06-14 23:04:05 +00:00
import { StepRepository, PrintHistoryRepository } from '../db/repositories';
2025-06-14 22:27:33 +00:00
import { Knex } from 'knex';
import logger from '../logger';
2025-06-15 01:24:42 +00:00
import { formatUtils } from './format-utils';
2025-06-15 02:02:57 +00:00
import {
PRINTER_CONFIG,
FONT_SIZES,
BARCODE_CONFIG,
PAPER_CONFIG,
ALIGNMENT,
FONT,
STYLE,
} from './printer-constants';
2025-06-14 22:27:33 +00:00
export class SerialPrinter implements PrinterInterface {
private device: USB | null = null;
private printer: Printer<[]> | null = null;
2025-06-14 23:04:05 +00:00
private printHistoryRepo: PrintHistoryRepository;
private stepRepository: StepRepository;
2025-06-14 22:27:33 +00:00
2025-06-14 23:04:05 +00:00
constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) {
this.printHistoryRepo = printHistoryRepo;
this.stepRepository = stepRepo;
2025-06-17 23:14:26 +00:00
this.initializeDevice();
2025-06-14 22:27:33 +00:00
}
2025-06-17 23:14:26 +00:00
private initializeDevice() {
2025-06-14 22:27:33 +00:00
try {
this.device = new USB();
2025-06-17 23:14:26 +00:00
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 {
2025-06-14 22:27:33 +00:00
await new Promise<void>((resolve, reject) => {
this.device?.open((err) => {
if (err) {
logger.error('Failed to open printer:', err);
reject(err);
return;
}
resolve();
});
});
2025-06-17 23:14:26 +00:00
logger.info('Printer opened successfully');
} catch (error) {
logger.error('Failed to open printer:', error);
throw error;
}
}
2025-06-14 22:27:33 +00:00
2025-06-17 23:14:26 +00:00
private async closePrinter(): Promise<void> {
if (!this.printer) {
throw new Error('Printer not initialized');
}
try {
await this.printer.close();
logger.info('Printer closed successfully');
2025-06-14 22:27:33 +00:00
} catch (error) {
2025-06-17 23:14:26 +00:00
logger.error('Failed to close printer:', error);
throw error;
2025-06-14 22:27:33 +00:00
}
}
2025-06-15 00:22:52 +00:00
async getTaskSteps(_db: Knex, task: Task): Promise<Step[]> {
2025-06-14 23:04:05 +00:00
return await this.stepRepository.findByTaskId(task.id);
2025-06-14 22:27:33 +00:00
}
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 {
2025-06-17 23:14:26 +00:00
await this.openPrinter();
2025-06-14 22:27:33 +00:00
// Print header with task ID as barcode
await this.printer
2025-06-15 02:02:57 +00:00
.font(FONT.DEFAULT)
.align(ALIGNMENT.CENTER)
.style(STYLE.BOLD)
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
2025-06-15 01:24:42 +00:00
.text(formatUtils.formatCheckbox(`Task: ${task.name}`))
2025-06-15 02:02:57 +00:00
.text(formatUtils.createBanner('=', PAPER_CONFIG.BANNER_LENGTH))
2025-06-17 23:14:26 +00:00
// .text('')
2025-06-15 02:02:57 +00:00
.align(ALIGNMENT.LEFT);
2025-06-14 22:27:33 +00:00
// Print task ID as barcode
await this.printer
2025-06-17 23:14:26 +00:00
.barcode(task.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
// .text('')
// .text('');
2025-06-14 22:27:33 +00:00
// Print steps
for (let i = 0; i < taskSteps.length; i++) {
const step = taskSteps[i];
2025-06-15 01:49:56 +00:00
const stepSection = formatUtils.formatSection(
formatUtils.formatStepHeader(step.name, i + 1, task.name, true),
2025-06-18 01:18:04 +00:00
step.instructions || 'No instructions provided',
2025-06-15 01:49:56 +00:00
'-'
);
2025-06-15 01:24:42 +00:00
2025-06-14 22:27:33 +00:00
await this.printer
2025-06-15 02:02:57 +00:00
.size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height)
.text(stepSection[0])
.text(stepSection[1])
.size(FONT_SIZES.SMALL.width, FONT_SIZES.SMALL.height)
2025-06-17 23:14:26 +00:00
.text(stepSection[3]);
// .text('');
2025-06-14 22:27:33 +00:00
// Print step ID as barcode
await this.printer
2025-06-17 23:14:26 +00:00
.barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
// .text('');
2025-06-14 22:27:33 +00:00
}
await this.printer
2025-06-17 23:14:26 +00:00
.cut(true, PAPER_CONFIG.CUT_LINES);
await this.closePrinter();
2025-06-14 22:27:33 +00:00
logger.info(`Printed task ${task.id}`);
2025-06-14 23:04:05 +00:00
await this.printHistoryRepo.create({
2025-06-15 02:02:57 +00:00
user_id: PRINTER_CONFIG.DEFAULT_USER_ID,
2025-06-14 23:04:05 +00:00
task_id: task.id,
printed_at: new Date(),
});
2025-06-14 22:27:33 +00:00
} catch (error) {
logger.error('Failed to print task:', error);
throw error;
}
}
2025-06-15 01:49:56 +00:00
async printStep(step: Step, db: Knex): Promise<void> {
2025-06-14 22:27:33 +00:00
if (!this.printer || !this.device) {
throw new Error('Printer not initialized');
}
try {
2025-06-17 23:14:26 +00:00
await this.openPrinter();
2025-06-15 01:49:56 +00:00
// 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),
2025-06-18 01:18:04 +00:00
step.instructions || 'No instructions provided'
2025-06-15 01:49:56 +00:00
);
2025-06-15 01:24:42 +00:00
2025-06-14 22:27:33 +00:00
await this.printer
2025-06-15 02:02:57 +00:00
.font(FONT.DEFAULT)
.align(ALIGNMENT.CENTER)
.style(STYLE.BOLD)
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
.text(stepSection[0])
.text(stepSection[1])
2025-06-14 22:27:33 +00:00
.text('')
2025-06-15 02:02:57 +00:00
.align(ALIGNMENT.LEFT)
.size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height)
.text(stepSection[3])
2025-06-14 22:27:33 +00:00
.text('')
.text('');
// Print step ID as barcode
await this.printer
2025-06-15 02:02:57 +00:00
.barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS)
2025-06-17 23:14:26 +00:00
.cut(true, PAPER_CONFIG.CUT_LINES);
await this.closePrinter();
2025-06-14 22:27:33 +00:00
logger.info(`Printed step ${step.id}`);
2025-06-14 23:04:05 +00:00
await this.printHistoryRepo.create({
2025-06-15 02:02:57 +00:00
user_id: PRINTER_CONFIG.DEFAULT_USER_ID,
2025-06-14 23:04:05 +00:00
step_id: step.id,
printed_at: new Date(),
});
2025-06-14 22:27:33 +00:00
} catch (error) {
logger.error('Failed to print step:', error);
throw error;
}
}
2025-06-17 23:14:26 +00:00
}