switch printing to command pattern

This commit is contained in:
Sean Sube 2025-06-17 21:26:28 -05:00
parent 8f3c8ba80e
commit 7d680e469f
No known key found for this signature in database
GPG Key ID: 3EED7B957D362AF1
16 changed files with 1311 additions and 240 deletions

View File

@ -1,4 +1,5 @@
import { Knex } from 'knex';
import logger from '../../logger';
export abstract class BaseRepository<T extends { created_at?: Date; updated_at?: Date }> {
protected tableName: string;
@ -16,6 +17,7 @@ export abstract class BaseRepository<T extends { created_at?: Date; updated_at?:
async findById(id: number): Promise<T | null> {
const result = await this.db(this.tableName).where('id', id).first();
logger.info(`findById(${id}) for table ${this.tableName}:`, result);
return result || null;
}

View File

@ -1,6 +1,7 @@
import { Knex } from 'knex';
import { BaseRepository } from './base-repository';
import { Step, Task } from '@shared/index';
import logger from '../../logger';
export class StepRepository extends BaseRepository<Step> {
constructor(db: Knex) {
@ -8,10 +9,18 @@ export class StepRepository extends BaseRepository<Step> {
}
async findByTaskId(taskId: number): Promise<Step[]> {
return await this.db(this.tableName)
const steps = await this.db(this.tableName)
.where('task_id', taskId)
.orderBy('order')
.select('*');
logger.info(`Retrieved ${steps.length} steps for task ${taskId}`);
for (const step of steps) {
logger.info(`Step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
logger.info(`Full step data:`, JSON.stringify(step, null, 2));
}
return steps;
}
async incrementPrintCount(id: number): Promise<boolean> {

View File

@ -0,0 +1,98 @@
import { CommandBuilder } from '../printer-commands';
import { TestCommandExecutor } from '../command-executor';
import { BARCODE_CONFIG } from '../printer-constants';
describe('Barcode Debug', () => {
let commandExecutor: TestCommandExecutor;
beforeEach(() => {
commandExecutor = new TestCommandExecutor();
});
it('should test different barcode formats', async () => {
const testData = '123456789';
// Test basic barcode
const commands = [
CommandBuilder.text('Testing barcode:'),
CommandBuilder.barcode(testData),
CommandBuilder.newline(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
console.log('Barcode test output:', output);
expect(output).toContain('[BARCODE: 123456789]');
});
it('should test task barcode', async () => {
const taskId = 123;
const commands = [
CommandBuilder.text('Testing task barcode:'),
CommandBuilder.taskBarcode(taskId),
CommandBuilder.newline(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
console.log('Task barcode test output:', output);
expect(output).toContain('[BARCODE: 100000123]');
});
it('should test step barcode', async () => {
const stepId = 456;
const commands = [
CommandBuilder.text('Testing step barcode:'),
CommandBuilder.stepBarcode(stepId),
CommandBuilder.newline(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
console.log('Step barcode test output:', output);
expect(output).toContain('[BARCODE: 200000456]');
});
it('should test barcode with alignment', async () => {
const testData = '987654321';
const commands = [
CommandBuilder.text('Testing barcode with alignment:'),
...CommandBuilder.barcodeWithAlignment(testData),
CommandBuilder.newline(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
console.log('Barcode with alignment test output:', output);
expect(output).toContain('[BARCODE: 987654321]');
});
it('should test task barcode with alignment', async () => {
const taskId = 789;
const commands = [
CommandBuilder.text('Testing task barcode with alignment:'),
...CommandBuilder.taskBarcodeWithAlignment(taskId),
CommandBuilder.newline(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
console.log('Task barcode with alignment test output:', output);
expect(output).toContain('[BARCODE: 100000789]');
});
it('should test barcode configuration', () => {
console.log('Barcode config:', BARCODE_CONFIG);
expect(BARCODE_CONFIG.TYPE).toBe('CODE39');
expect(BARCODE_CONFIG.ALTERNATIVE_TYPES).toContain('CODE128');
});
});

View File

@ -0,0 +1,167 @@
import { BarcodeUtils, BarcodeData } from '../barcode-utils';
describe('BarcodeUtils', () => {
describe('parseBarcode', () => {
it('should parse valid task barcode', () => {
const result = BarcodeUtils.parseBarcode('100000123');
expect(result).toEqual({ type: 'TASK', id: 123 });
});
it('should parse valid step barcode', () => {
const result = BarcodeUtils.parseBarcode('200000456');
expect(result).toEqual({ type: 'STEP', id: 456 });
});
it('should parse barcode with non-numeric characters', () => {
const result = BarcodeUtils.parseBarcode('1ABC000123DEF');
expect(result).toEqual({ type: 'TASK', id: 123 });
});
it('should return null for invalid entity type', () => {
const result = BarcodeUtils.parseBarcode('300000123');
expect(result).toBeNull();
});
it('should return null for too short barcode', () => {
const result = BarcodeUtils.parseBarcode('1');
expect(result).toBeNull();
});
it('should return null for invalid ID', () => {
const result = BarcodeUtils.parseBarcode('100000000');
expect(result).toBeNull();
});
it('should return null for empty string', () => {
const result = BarcodeUtils.parseBarcode('');
expect(result).toBeNull();
});
it('should parse large IDs correctly', () => {
const result = BarcodeUtils.parseBarcode('199999999');
expect(result).toEqual({ type: 'TASK', id: 99999999 });
});
});
describe('generateTaskBarcode', () => {
it('should generate task barcode', () => {
const result = BarcodeUtils.generateTaskBarcode(123);
expect(result).toBe('100000123');
});
it('should generate task barcode with padding', () => {
const result = BarcodeUtils.generateTaskBarcode(1);
expect(result).toBe('100000001');
});
});
describe('generateStepBarcode', () => {
it('should generate step barcode', () => {
const result = BarcodeUtils.generateStepBarcode(456);
expect(result).toBe('200000456');
});
it('should generate step barcode with padding', () => {
const result = BarcodeUtils.generateStepBarcode(1);
expect(result).toBe('200000001');
});
});
describe('isValidBarcode', () => {
it('should return true for valid task barcode', () => {
const result = BarcodeUtils.isValidBarcode('100000123');
expect(result).toBe(true);
});
it('should return true for valid step barcode', () => {
const result = BarcodeUtils.isValidBarcode('200000456');
expect(result).toBe(true);
});
it('should return false for invalid barcode', () => {
const result = BarcodeUtils.isValidBarcode('300000123');
expect(result).toBe(false);
});
});
describe('getEntityType', () => {
it('should return TASK for task barcode', () => {
const result = BarcodeUtils.getEntityType('100000123');
expect(result).toBe('TASK');
});
it('should return STEP for step barcode', () => {
const result = BarcodeUtils.getEntityType('200000456');
expect(result).toBe('STEP');
});
it('should return null for invalid barcode', () => {
const result = BarcodeUtils.getEntityType('300000123');
expect(result).toBeNull();
});
});
describe('getEntityId', () => {
it('should return ID for task barcode', () => {
const result = BarcodeUtils.getEntityId('100000123');
expect(result).toBe(123);
});
it('should return ID for step barcode', () => {
const result = BarcodeUtils.getEntityId('200000456');
expect(result).toBe(456);
});
it('should return null for invalid barcode', () => {
const result = BarcodeUtils.getEntityId('300000123');
expect(result).toBeNull();
});
});
describe('getMaxId', () => {
it('should return maximum supported ID', () => {
const result = BarcodeUtils.getMaxId();
expect(result).toBe(99999999);
});
});
describe('isValidId', () => {
it('should return true for valid ID', () => {
const result = BarcodeUtils.isValidId(123);
expect(result).toBe(true);
});
it('should return false for zero ID', () => {
const result = BarcodeUtils.isValidId(0);
expect(result).toBe(false);
});
it('should return false for negative ID', () => {
const result = BarcodeUtils.isValidId(-1);
expect(result).toBe(false);
});
it('should return false for ID too large', () => {
const result = BarcodeUtils.isValidId(100000000);
expect(result).toBe(false);
});
});
describe('round-trip tests', () => {
it('should generate and parse task barcode correctly', () => {
const taskId = 789;
const generated = BarcodeUtils.generateTaskBarcode(taskId);
const parsed = BarcodeUtils.parseBarcode(generated);
expect(parsed).toEqual({ type: 'TASK', id: taskId });
});
it('should generate and parse step barcode correctly', () => {
const stepId = 101;
const generated = BarcodeUtils.generateStepBarcode(stepId);
const parsed = BarcodeUtils.parseBarcode(generated);
expect(parsed).toEqual({ type: 'STEP', id: stepId });
});
});
});

View File

@ -0,0 +1,133 @@
import { CommandBuilder } from '../printer-commands';
import { TestCommandExecutor } from '../command-executor';
describe('Command System (Standalone)', () => {
let commandExecutor: TestCommandExecutor;
beforeEach(() => {
commandExecutor = new TestCommandExecutor();
});
it('should execute text commands', async () => {
const commands = [
CommandBuilder.text('Hello World'),
CommandBuilder.newline(),
CommandBuilder.text('Test'),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('Hello World');
expect(output).toContain('Test');
});
it('should execute header commands', async () => {
const commands = [
CommandBuilder.header('Test Header'),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Test Header');
});
it('should execute banner commands', async () => {
const commands = [
CommandBuilder.banner('=', 10),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('==========');
});
it('should execute barcode commands', async () => {
const commands = [
CommandBuilder.barcode('123456'),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[BARCODE: 123456]');
});
it('should execute section commands', async () => {
const commands = [
CommandBuilder.section('Test Section', 'Test Content', '-', true),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Test Section');
expect(output).toContain('Test Content');
});
it('should execute step header commands', async () => {
const commands = [
CommandBuilder.stepHeader('Test Step', 1, 'Test Task', true),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Step 1: Test Step');
});
it('should execute cut commands', async () => {
const commands = [
CommandBuilder.cut(true, 4),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('--- CUT ---');
});
it('should execute complex command sequences', async () => {
const commands = [
CommandBuilder.header('Complex Test'),
CommandBuilder.banner('=', 20),
CommandBuilder.text('Some content'),
CommandBuilder.newline(),
CommandBuilder.barcode('123'),
CommandBuilder.cut(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Complex Test');
expect(output).toContain('====================');
expect(output).toContain('Some content');
expect(output).toContain('[BARCODE: 123]');
expect(output).toContain('--- CUT ---');
});
it('should handle empty command arrays', async () => {
const commands: any[] = [];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toBe('\n');
});
it('should handle list commands', async () => {
const commands = [
CommandBuilder.list(['Item 1', 'Item 2', 'Item 3'], 1, '- '),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('- [ ] 1: Item 1');
expect(output).toContain('- [ ] 2: Item 2');
expect(output).toContain('- [ ] 3: Item 3');
});
});

View File

@ -1,144 +1,51 @@
import { formatUtils } from '../format-utils';
import { Command } from '../printer-commands';
describe('formatUtils', () => {
describe('createBanner', () => {
it('should create a banner with default length', () => {
const banner = formatUtils.createBanner('=');
expect(banner).toBe('='.repeat(40));
});
it('should create a banner with custom length', () => {
const banner = formatUtils.createBanner('-', 32);
expect(banner).toBe('-'.repeat(32));
});
it('should handle empty character', () => {
const banner = formatUtils.createBanner('', 10);
expect(banner).toBe('');
});
it('should handle zero length', () => {
const banner = formatUtils.createBanner('*', 0);
expect(banner).toBe('');
});
it('banner returns a BANNER command', () => {
expect(formatUtils.banner('=', 10)).toEqual([Command.BANNER, '=', 10]);
});
describe('formatCheckbox', () => {
it('should format text with checkbox', () => {
const formatted = formatUtils.formatCheckbox('Test Item');
expect(formatted).toBe('[ ] Test Item');
});
it('should handle empty text', () => {
const formatted = formatUtils.formatCheckbox('');
expect(formatted).toBe('[ ] ');
});
it('should handle text with special characters', () => {
const formatted = formatUtils.formatCheckbox('Test: Item (123)');
expect(formatted).toBe('[ ] Test: Item (123)');
});
it('checkbox returns a CHECKBOX command', () => {
expect(formatUtils.checkbox('Test')).toEqual([Command.CHECKBOX, 'Test']);
});
describe('formatList', () => {
it('should format list without numbering', () => {
const items = ['Item 1', 'Item 2', 'Item 3'];
const formatted = formatUtils.formatList(items);
expect(formatted).toEqual([
'[ ] Item 1',
'[ ] Item 2',
'[ ] Item 3'
]);
});
it('should format list with numbering', () => {
const items = ['Item 1', 'Item 2', 'Item 3'];
const formatted = formatUtils.formatList(items, 1);
expect(formatted).toEqual([
'[ ] 1: Item 1',
'[ ] 2: Item 2',
'[ ] 3: Item 3'
]);
});
it('should handle empty list', () => {
const formatted = formatUtils.formatList([]);
expect(formatted).toEqual([]);
});
it('should handle list with empty items', () => {
const items = ['', 'Item 2', ''];
const formatted = formatUtils.formatList(items);
expect(formatted).toEqual([
'[ ] ',
'[ ] Item 2',
'[ ] '
]);
});
it('list returns a LIST command', () => {
expect(formatUtils.list(['A', 'B'], 1, '- ')).toEqual([[Command.LIST, ['A', 'B'], 1, '- ']]);
});
describe('formatSection', () => {
it('should format section with default banner', () => {
const section = formatUtils.formatSection('Header', 'Content');
expect(section).toEqual([
'[ ] Header',
'='.repeat(40),
'Content',
]);
});
it('should format section with custom banner', () => {
const section = formatUtils.formatSection('Header', 'Content', '-');
expect(section).toEqual([
'[ ] Header',
'-'.repeat(40),
'Content',
]);
});
it('should handle empty content', () => {
const section = formatUtils.formatSection('Header', '');
expect(section).toEqual([
'[ ] Header',
'='.repeat(40),
'',
]);
});
it('should handle empty header', () => {
const section = formatUtils.formatSection('', 'Content');
expect(section).toEqual([
'[ ] ',
'='.repeat(40),
'Content',
]);
});
it('stepHeader returns a CHECKBOX command with formatted text', () => {
expect(formatUtils.stepHeader('StepName', 2, 'TaskName', true)).toEqual([Command.CHECKBOX, 'Step 2: StepName']);
expect(formatUtils.stepHeader('StepName', 2, 'TaskName', false)).toEqual([Command.CHECKBOX, 'Step 2 of TaskName: StepName']);
});
describe('formatStepHeader', () => {
it('should format step header with just step name and number', () => {
const header = formatUtils.formatStepHeader('Test Step', 1);
expect(header).toBe('Step 1: Test Step');
});
it('section returns a list of commands', () => {
expect(formatUtils.section('Header', 'Content')).toEqual([
[Command.CHECKBOX, 'Header'],
[Command.BANNER, '='],
[Command.TEXT, 'Content'],
]);
expect(formatUtils.section('Header', 'Content', '-', true)).toEqual([
[Command.CHECKBOX, 'Header'],
[Command.BANNER, '-'],
[Command.TEXT, 'Content'],
[Command.NEWLINE],
]);
});
it('should format step header with step number and task name in single step view', () => {
const header = formatUtils.formatStepHeader('Test Step', 1, 'Test Task');
expect(header).toBe('Step 1 of Test Task: Test Step');
});
it('getCheckboxText extracts text from a CHECKBOX command', () => {
expect(formatUtils.getCheckboxText([Command.CHECKBOX, 'Hello'])).toBe('Hello');
expect(formatUtils.getCheckboxText([Command.BANNER, '='])).toBe('');
});
it('should format step header with step number but no task name in task view', () => {
const header = formatUtils.formatStepHeader('Test Step', 1, 'Test Task', true);
expect(header).toBe('Step 1: Test Step');
});
it('taskHeader returns a CHECKBOX command for the task', () => {
expect(formatUtils.taskHeader('My Task')).toEqual([Command.CHECKBOX, 'Task: My Task']);
});
it('should handle empty step name', () => {
const header = formatUtils.formatStepHeader('', 1);
expect(header).toBe('Step 1: ');
});
it('should handle zero step number', () => {
const header = formatUtils.formatStepHeader('Test Step', 0);
expect(header).toBe('Step 0: Test Step');
});
it('taskSection returns a list of commands for the task section', () => {
expect(formatUtils.taskSection('My Task')).toEqual([
[Command.CHECKBOX, 'Task: My Task'],
[Command.BANNER, '='],
]);
});
});

View File

@ -3,6 +3,8 @@ import { TestPrinter } from '../index';
import { Task, Step } from '@shared/index';
import { InMemoryPrintHistoryRepository, InMemoryStepRepository } from '../../db/repositories/in-memory-repository';
import { Knex } from 'knex';
import { CommandBuilder } from '../printer-commands';
import { TestCommandExecutor } from '../command-executor';
describe('Printer', () => {
let printHistoryRepo: InMemoryPrintHistoryRepository;
@ -40,4 +42,113 @@ describe('Printer', () => {
printed_at: expect.any(Date),
});
});
});
describe('Command System', () => {
let commandExecutor: TestCommandExecutor;
beforeEach(() => {
commandExecutor = new TestCommandExecutor();
});
it('should execute text commands', async () => {
const commands = [
CommandBuilder.text('Hello World'),
CommandBuilder.newline(),
CommandBuilder.text('Test'),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('Hello World');
expect(output).toContain('Test');
});
it('should execute header commands', async () => {
const commands = [
CommandBuilder.header('Test Header'),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Test Header');
});
it('should execute banner commands', async () => {
const commands = [
CommandBuilder.banner('=', 10),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('==========');
});
it('should execute barcode commands', async () => {
const commands = [
CommandBuilder.barcode('123456'),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[BARCODE: 123456]');
});
it('should execute section commands', async () => {
const commands = [
CommandBuilder.section('Test Section', 'Test Content', '-', true),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Test Section');
expect(output).toContain('Test Content');
});
it('should execute step header commands', async () => {
const commands = [
CommandBuilder.stepHeader('Test Step', 1, 'Test Task', true),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Step 1: Test Step');
});
it('should execute cut commands', async () => {
const commands = [
CommandBuilder.cut(true, 4),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('--- CUT ---');
});
it('should execute complex command sequences', async () => {
const commands = [
CommandBuilder.header('Complex Test'),
CommandBuilder.banner('=', 20),
CommandBuilder.text('Some content'),
CommandBuilder.newline(),
CommandBuilder.barcode('123'),
CommandBuilder.cut(),
];
await commandExecutor.executeCommands(commands);
const output = commandExecutor.getOutput();
expect(output).toContain('[ ] Complex Test');
expect(output).toContain('====================');
expect(output).toContain('Some content');
expect(output).toContain('[BARCODE: 123]');
expect(output).toContain('--- CUT ---');
});
});

View File

@ -0,0 +1,51 @@
import { Step } from '@shared/index';
describe('Step Data Debug', () => {
it('should test step data structure', () => {
// Test with sample step data that matches the database
const step: Step = {
id: 10,
name: 'Counter',
instructions: 'Was hit',
task_id: 20,
order: 1,
print_count: 9,
created_at: new Date('2025-06-15T01:55:41.121Z'),
updated_at: new Date('2025-06-15T01:55:41.121Z'),
};
console.log(`Step ${step.id}:`);
console.log(` name: "${step.name}"`);
console.log(` instructions: "${step.instructions}"`);
console.log(` task_id: ${step.task_id}`);
console.log(` order: ${step.order}`);
console.log(` print_count: ${step.print_count}`);
console.log(` created_at: ${step.created_at}`);
console.log(` updated_at: ${step.updated_at}`);
expect(step.instructions).toBe('Was hit');
expect(typeof step.instructions).toBe('string');
});
it('should test step data with different instructions', () => {
const step: Step = {
id: 11,
name: 'Sink',
instructions: 'asdf',
task_id: 20,
order: 2,
print_count: 2,
created_at: new Date('2025-06-15T01:55:46.466Z'),
updated_at: new Date('2025-06-15T01:55:46.466Z'),
};
console.log(`Step ${step.id}:`);
console.log(` name: "${step.name}"`);
console.log(` instructions: "${step.instructions}"`);
console.log(` task_id: ${step.task_id}`);
console.log(` order: ${step.order}`);
expect(step.instructions).toBe('asdf');
expect(typeof step.instructions).toBe('string');
});
});

View File

@ -0,0 +1,106 @@
export interface BarcodeData {
type: 'TASK' | 'STEP';
id: number;
}
export class BarcodeUtils {
// Entity type codes - using numeric prefixes
private static readonly TASK_PREFIX = 1;
private static readonly STEP_PREFIX = 2;
// Format: [entity_type][padding][id]
// Example: 100000123 for task 123, 200000456 for step 456
private static readonly ID_PADDING = 4; // 4 digits for ID
/**
* Parse barcode data to extract entity type and ID
* @param barcodeData The raw barcode data (e.g., "100000123" or "200000456")
* @returns Parsed barcode data with type and ID, or null if invalid
*/
static parseBarcode(barcodeData: string): BarcodeData | null {
const numericData = barcodeData.replace(/\D/g, ''); // Remove non-digits
if (numericData.length < 2) {
return null;
}
const entityType = parseInt(numericData.charAt(0), 10);
const id = parseInt(numericData.substring(1), 10);
if (isNaN(id) || id <= 0) {
return null;
}
if (entityType === this.TASK_PREFIX) {
return { type: 'TASK', id };
} else if (entityType === this.STEP_PREFIX) {
return { type: 'STEP', id };
}
return null;
}
/**
* Generate barcode data for a task
* @param taskId The task ID
* @returns Formatted barcode data (numeric only)
*/
static generateTaskBarcode(taskId: number): string {
return `${this.TASK_PREFIX}${taskId.toString().padStart(this.ID_PADDING, '0')}`;
}
/**
* Generate barcode data for a step
* @param stepId The step ID
* @returns Formatted barcode data (numeric only)
*/
static generateStepBarcode(stepId: number): string {
return `${this.STEP_PREFIX}${stepId.toString().padStart(this.ID_PADDING, '0')}`;
}
/**
* Validate if a barcode data string is valid
* @param barcodeData The barcode data to validate
* @returns True if valid, false otherwise
*/
static isValidBarcode(barcodeData: string): boolean {
return this.parseBarcode(barcodeData) !== null;
}
/**
* Get the entity type from barcode data
* @param barcodeData The barcode data
* @returns The entity type or null if invalid
*/
static getEntityType(barcodeData: string): 'TASK' | 'STEP' | null {
const parsed = this.parseBarcode(barcodeData);
return parsed?.type || null;
}
/**
* Get the entity ID from barcode data
* @param barcodeData The barcode data
* @returns The entity ID or null if invalid
*/
static getEntityId(barcodeData: string): number | null {
const parsed = this.parseBarcode(barcodeData);
return parsed?.id || null;
}
/**
* Get the maximum supported ID for the current format
* @returns Maximum ID value
*/
static getMaxId(): number {
return Math.pow(10, this.ID_PADDING) - 1;
}
/**
* Check if an ID is within the supported range
* @param id The ID to check
* @returns True if valid, false otherwise
*/
static isValidId(id: number): boolean {
return id > 0 && id <= this.getMaxId();
}
}

View File

@ -0,0 +1,266 @@
import { Printer as EscposPrinter } from '@node-escpos/core';
import { formatUtils } from './format-utils';
import { Command, CommandTuple, CommandArray } from './printer-commands';
import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG } from './printer-constants';
import logger from '../logger';
export interface CommandExecutor {
executeCommands(commands: CommandArray): Promise<void>;
}
export class SerialCommandExecutor implements CommandExecutor {
constructor(private printer: EscposPrinter<[]>) {}
async executeCommands(commands: CommandArray): Promise<void> {
for (const command of commands) {
await this.executeCommand(command);
}
}
private async executeCommand(commandTuple: CommandTuple): Promise<void> {
const [command, ...params] = commandTuple;
switch (command) {
case Command.TEXT:
await this.printer.text(params[0] as string);
break;
case Command.HEADER:
await this.printer
.font(FONT.DEFAULT)
.align(ALIGNMENT.CENTER)
.style(STYLE.BOLD)
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
.text(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
break;
case Command.BANNER:
const char = params[0] as string;
const length = params[1] as number;
await this.printer.text(formatUtils.bannerString(char, length));
break;
case Command.NEWLINE:
await this.printer.text('');
break;
case Command.BARCODE:
const barcodeData = params[0] as string;
logger.info(`Printing barcode: ${barcodeData} with type: ${BARCODE_CONFIG.TYPE}`);
// Try multiple barcode formats until one works
const barcodeTypes = [BARCODE_CONFIG.TYPE, ...BARCODE_CONFIG.ALTERNATIVE_TYPES];
let barcodePrinted = false;
for (const barcodeType of barcodeTypes) {
try {
logger.info(`Trying barcode type: ${barcodeType}`);
await this.printer.barcode(
barcodeData,
barcodeType,
BARCODE_CONFIG.DIMENSIONS
);
logger.info(`Successfully printed barcode with type: ${barcodeType}`);
barcodePrinted = true;
break;
} catch (error) {
logger.warn(`Barcode type ${barcodeType} failed: ${error}`);
continue;
}
}
if (!barcodePrinted) {
logger.error(`All barcode types failed for data: ${barcodeData}`);
// As a last resort, just print the data as text
await this.printer.text(`[BARCODE: ${barcodeData}]`);
}
break;
case Command.FONT_SIZE:
const size = params[0] as typeof FONT_SIZES[keyof typeof FONT_SIZES];
await this.printer.size(size.width, size.height);
break;
case Command.ALIGN:
await this.printer.align(params[0] as any);
break;
case Command.FONT_FAMILY:
await this.printer.font(params[0] as any);
break;
case Command.STYLE:
await this.printer.style(params[0] as any);
break;
case Command.CUT:
const partial = params[0] as boolean;
const lines = params[1] as number;
await this.printer.cut(partial, lines);
break;
case Command.SECTION:
const header = params[0] as string;
const content = params[1] as string;
const bannerChar = params[2] as string;
const trailingNewline = params[3] as boolean;
const section = formatUtils.section(header, content, bannerChar, trailingNewline);
for (const cmd of section) {
if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) {
await this.printer.text(cmd[1] as string);
} else if (cmd[0] === Command.BANNER) {
await this.printer.text(formatUtils.bannerString(cmd[1] as string, cmd[2] as number));
} else if (cmd[0] === Command.NEWLINE) {
await this.printer.text('');
}
}
break;
case Command.STEP_HEADER:
const stepName = params[0] as string;
const stepNumber = params[1] as number;
const taskName = params[2] as string;
const isTaskView = params[3] as boolean;
const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView);
await this.printer.text(formatUtils.getCheckboxText(stepHeader));
break;
case Command.CHECKBOX:
await this.printer.text(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
break;
case Command.LIST:
const items = params[0] as string[];
const startIndex = params[1] as number;
const prefix = params[2] as string;
const listCmds = formatUtils.list(items, startIndex, prefix);
for (const cmd of listCmds) {
if (cmd[0] === Command.LIST) {
// Render as text for test printer, or as needed for real printer
for (let i = 0; i < items.length; i++) {
const num = startIndex > 0 ? `${i + startIndex}: ` : '';
const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`));
await this.printer.text(`${prefix}${body}`);
}
}
}
break;
default:
throw new Error(`Unknown command: ${command}`);
}
}
}
export class TestCommandExecutor implements CommandExecutor {
private output: string[] = [];
async executeCommands(commands: CommandArray): Promise<void> {
this.output = [];
for (const command of commands) {
await this.executeCommand(command);
}
}
private async executeCommand(commandTuple: CommandTuple): Promise<void> {
const [command, ...params] = commandTuple;
switch (command) {
case Command.TEXT:
this.output.push(params[0] as string);
break;
case Command.HEADER:
this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
break;
case Command.BANNER:
const char = params[0] as string;
const length = params[1] as number;
this.output.push(formatUtils.banner(char, length)[1] as string);
break;
case Command.NEWLINE:
this.output.push('');
break;
case Command.BARCODE:
this.output.push(`[BARCODE: ${params[0] as string}]`);
break;
case Command.FONT_SIZE:
// Test printer ignores font size changes
break;
case Command.ALIGN:
// Test printer ignores alignment changes
break;
case Command.FONT_FAMILY:
// Test printer ignores font changes
break;
case Command.STYLE:
// Test printer ignores style changes
break;
case Command.CUT:
this.output.push('--- CUT ---');
break;
case Command.SECTION:
const header = params[0] as string;
const content = params[1] as string;
const bannerChar = params[2] as string;
const trailingNewline = params[3] as boolean;
const section = formatUtils.section(header, content, bannerChar, trailingNewline);
for (const cmd of section) {
if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) {
this.output.push(cmd[1] as string);
} else if (cmd[0] === Command.BANNER) {
this.output.push(formatUtils.bannerString(cmd[1] as string, cmd[2] as number));
} else if (cmd[0] === Command.NEWLINE) {
this.output.push('');
}
}
break;
case Command.STEP_HEADER:
const stepName = params[0] as string;
const stepNumber = params[1] as number;
const taskName = params[2] as string;
const isTaskView = params[3] as boolean;
const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView);
this.output.push(formatUtils.getCheckboxText(stepHeader));
break;
case Command.CHECKBOX:
this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
break;
case Command.LIST:
const items = params[0] as string[];
const startIndex = params[1] as number;
const prefix = params[2] as string;
const listCmds = formatUtils.list(items, startIndex, prefix);
for (const cmd of listCmds) {
if (cmd[0] === Command.LIST) {
for (let i = 0; i < items.length; i++) {
const num = startIndex > 0 ? `${i + startIndex}: ` : '';
const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`));
this.output.push(`${prefix}${body}`);
}
}
}
break;
default:
throw new Error(`Unknown command: ${command}`);
}
}
getOutput(): string {
return this.output.join('\n') + '\n';
}
}

View File

@ -1,48 +1,39 @@
import { PAPER_CONFIG } from "./printer-constants";
import { Command, CommandTuple } from './printer-commands';
export const formatUtils = {
/**
* Creates a banner line with the specified character
* @param char Character to repeat
* @param length Length of the banner
* @returns Formatted banner string
* Creates a banner command
*/
createBanner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string {
banner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): CommandTuple {
return [Command.BANNER, char, length];
},
/**
* Creates a banner string (repeated character)
*/
bannerString(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string {
return char.repeat(length);
},
/**
* Formats a checkbox item with the given text
* @param text Text to display after the checkbox
* @returns Formatted checkbox string
* Checkbox command
*/
formatCheckbox(text: string): string {
return `[ ] ${text}`;
checkbox(text: string): CommandTuple {
return [Command.CHECKBOX, text];
},
/**
* Formats a list of items into lines with optional numbering
* @param items List of items to format
* @param startIndex Starting index for numbering (0 for no numbers)
* @returns Array of formatted lines
* List of items as commands
*/
formatList(items: string[], startIndex: number = 0, prefix: string = ''): string[] {
return items.map((item, index) => {
const num = startIndex > 0 ? `${index + startIndex}: ` : '';
const body = this.formatCheckbox(`${num}${item}`);
return `${prefix}${body}`;
});
list(items: string[], startIndex: number = 0, prefix: string = ''): CommandTuple[] {
return [[Command.LIST, items, startIndex, prefix]];
},
/**
* Formats a step header with task context
* @param stepName Name of the step
* @param stepNumber Step number (1-based)
* @param taskName Name of the parent task (only used in single step view)
* @param isTaskView Whether this is being used in a task view
* @returns Formatted step header
* Step header as a command
*/
formatStepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): string {
stepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): CommandTuple {
const parts = ['Step'];
if (stepNumber !== undefined) {
parts.push(' ');
@ -53,25 +44,48 @@ export const formatUtils = {
}
parts.push(': ');
parts.push(stepName);
return parts.join('');
return [Command.CHECKBOX, parts.join('')];
},
/**
* Formats a section with a header and content
* @param header Section header
* @param content Section content
* @param bannerChar Character to use for the banner
* @returns Array of formatted lines
* Section as a list of commands
*/
formatSection(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): string[] {
const parts = [
this.formatCheckbox(header),
this.createBanner(bannerChar),
content,
section(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): CommandTuple[] {
const commands: CommandTuple[] = [
[Command.CHECKBOX, header],
[Command.BANNER, bannerChar],
[Command.TEXT, content],
];
if (trailingNewline) {
parts.push('');
commands.push([Command.NEWLINE]);
}
return parts;
}
return commands;
},
/**
* Extracts the text from a CHECKBOX CommandTuple
*/
getCheckboxText(cmd: CommandTuple): string {
if (cmd[0] === Command.CHECKBOX && typeof cmd[1] === 'string') {
return cmd[1];
}
return '';
},
/**
* Task header as a command
*/
taskHeader(taskName: string): CommandTuple {
return [Command.CHECKBOX, `Task: ${taskName}`];
},
/**
* Task section as a list of commands
*/
taskSection(taskName: string): CommandTuple[] {
return [
[Command.CHECKBOX, `Task: ${taskName}`],
[Command.BANNER, '='],
];
},
};

View File

@ -16,3 +16,6 @@ export function createPrinter(printHistoryRepo: PrintHistoryRepository, stepRepo
export { SerialPrinter } from './serial-printer';
export { TestPrinter } from './test-printer';
export { CommandBuilder, Command, type CommandArray, type CommandTuple } from './printer-commands';
export { SerialCommandExecutor, TestCommandExecutor, type CommandExecutor } from './command-executor';
export { BarcodeUtils, type BarcodeData } from './barcode-utils';

View File

@ -0,0 +1,148 @@
import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG, PAPER_CONFIG } from './printer-constants';
import { BarcodeUtils } from './barcode-utils';
// Command types for printer operations
export enum Command {
// Text commands
TEXT = 'TEXT',
HEADER = 'HEADER',
BANNER = 'BANNER',
NEWLINE = 'NEWLINE',
// Barcode commands
BARCODE = 'BARCODE',
// Formatting commands
FONT_SIZE = 'FONT_SIZE',
ALIGN = 'ALIGN',
FONT_FAMILY = 'FONT_FAMILY',
STYLE = 'STYLE',
// Paper commands
CUT = 'CUT',
// Section formatting
SECTION = 'SECTION',
STEP_HEADER = 'STEP_HEADER',
CHECKBOX = 'CHECKBOX',
// List formatting
LIST = 'LIST',
}
// Command parameter types
export type FontSize = typeof FONT_SIZES[keyof typeof FONT_SIZES];
export type Alignment = typeof ALIGNMENT[keyof typeof ALIGNMENT];
export type FontFamily = typeof FONT[keyof typeof FONT];
export type TextStyle = typeof STYLE[keyof typeof STYLE];
// Command tuple type - [Command, ...parameters]
export type CommandTuple =
| [Command.TEXT, string]
| [Command.HEADER, string]
| [Command.BANNER, string, number?]
| [Command.NEWLINE]
| [Command.BARCODE, string]
| [Command.FONT_SIZE, FontSize]
| [Command.ALIGN, Alignment]
| [Command.FONT_FAMILY, FontFamily]
| [Command.STYLE, TextStyle]
| [Command.CUT, boolean, number?]
| [Command.SECTION, string, string, string?, boolean?]
| [Command.STEP_HEADER, string, number, string?, boolean?]
| [Command.CHECKBOX, string]
| [Command.LIST, string[], number?, string?];
// Command array type
export type CommandArray = CommandTuple[];
// Command builder utility
export class CommandBuilder {
static text(text: string): CommandTuple {
return [Command.TEXT, text];
}
static header(text: string): CommandTuple {
return [Command.HEADER, text];
}
static banner(char: string = '=', length?: number): CommandTuple {
return [Command.BANNER, char, length];
}
static newline(): CommandTuple {
return [Command.NEWLINE];
}
static barcode(data: string): CommandTuple {
return [Command.BARCODE, data];
}
static taskBarcode(taskId: number): CommandTuple {
return [Command.BARCODE, BarcodeUtils.generateTaskBarcode(taskId)];
}
static stepBarcode(stepId: number): CommandTuple {
return [Command.BARCODE, BarcodeUtils.generateStepBarcode(stepId)];
}
static barcodeWithAlignment(data: string, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] {
return [
[Command.ALIGN, alignment],
[Command.BARCODE, data],
[Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment
];
}
static taskBarcodeWithAlignment(taskId: number, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] {
return [
[Command.ALIGN, alignment],
[Command.BARCODE, BarcodeUtils.generateTaskBarcode(taskId)],
[Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment
];
}
static stepBarcodeWithAlignment(stepId: number, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] {
return [
[Command.ALIGN, alignment],
[Command.BARCODE, BarcodeUtils.generateStepBarcode(stepId)],
[Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment
];
}
static fontSize(size: FontSize): CommandTuple {
return [Command.FONT_SIZE, size];
}
static align(alignment: Alignment): CommandTuple {
return [Command.ALIGN, alignment];
}
static font(font: FontFamily): CommandTuple {
return [Command.FONT_FAMILY, font];
}
static style(style: TextStyle): CommandTuple {
return [Command.STYLE, style];
}
static cut(partial: boolean = true, lines: number = PAPER_CONFIG.CUT_LINES): CommandTuple {
return [Command.CUT, partial, lines];
}
static section(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): CommandTuple {
return [Command.SECTION, header, content, bannerChar, trailingNewline];
}
static stepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): CommandTuple {
return [Command.STEP_HEADER, stepName, stepNumber, taskName, isTaskView];
}
static checkbox(text: string): CommandTuple {
return [Command.CHECKBOX, text];
}
static list(items: string[], startIndex: number = 0, prefix: string = ''): CommandTuple {
return [Command.LIST, items, startIndex, prefix];
}
}

View File

@ -5,13 +5,14 @@ export const PRINTER_CONFIG = {
export const FONT_SIZES = {
NORMAL: { width: 1, height: 1 }, // 0.08 x 2.13 mm
SMALL: { width: 1, height: 1 }, // Same as normal for now, but can be adjusted if needed
SMALL: { width: 0.5, height: 0.5 }, // Half size
LARGE: { width: 2, height: 2 }, // Double size
} as const;
export const BARCODE_CONFIG = {
TYPE: 'CODE128',
TYPE: 'CODE39',
DIMENSIONS: { width: 2, height: 50 },
ALTERNATIVE_TYPES: ['CODE128', 'EAN13', 'UPC_A'] as const,
} as const;
export const PAPER_CONFIG = {

View File

@ -5,6 +5,8 @@ import { StepRepository, PrintHistoryRepository } from '../db/repositories';
import { Knex } from 'knex';
import logger from '../logger';
import { formatUtils } from './format-utils';
import { CommandBuilder } from './printer-commands';
import { SerialCommandExecutor } from './command-executor';
import {
PRINTER_CONFIG,
FONT_SIZES,
@ -20,6 +22,7 @@ export class SerialPrinter implements PrinterInterface {
private printer: Printer<[]> | null = null;
private printHistoryRepo: PrintHistoryRepository;
private stepRepository: StepRepository;
private commandExecutor: SerialCommandExecutor | null = null;
constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) {
this.printHistoryRepo = printHistoryRepo;
@ -32,6 +35,7 @@ export class SerialPrinter implements PrinterInterface {
this.device = new USB();
const options = { encoding: PRINTER_CONFIG.ENCODING };
this.printer = new Printer(this.device, options);
this.commandExecutor = new SerialCommandExecutor(this.printer);
logger.info('Printer device initialized successfully');
} catch (error) {
logger.error('Failed to initialize printer device:', error);
@ -80,58 +84,51 @@ export class SerialPrinter implements PrinterInterface {
}
async printTask(task: Task, db: Knex): Promise<void> {
if (!this.printer || !this.device) {
if (!this.commandExecutor) {
throw new Error('Printer not initialized');
}
const taskSteps = await this.getTaskSteps(db, task);
logger.info(`Printing task ${task.id} with ${taskSteps.length} steps`);
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);
const commands = [
// Header with task name
CommandBuilder.header(`Task: ${task.name}`),
CommandBuilder.banner('=', PAPER_CONFIG.BANNER_LENGTH),
CommandBuilder.align(ALIGNMENT.LEFT),
// Print task ID as barcode
await this.printer
.barcode(task.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
// .text('')
// .text('');
// Task ID as barcode
...CommandBuilder.taskBarcodeWithAlignment(task.id),
CommandBuilder.newline(),
CommandBuilder.newline(),
];
// Print steps
// Add 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',
'-'
logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, i + 1, task.name, true));
commands.push(
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
CommandBuilder.text(headerText),
...formatUtils.section(
'',
step.instructions || 'No instructions provided',
'-'
),
CommandBuilder.newline(),
...CommandBuilder.stepBarcodeWithAlignment(step.id),
CommandBuilder.newline()
);
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);
commands.push(CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES));
await this.commandExecutor.executeCommands(commands);
await this.closePrinter();
logger.info(`Printed task ${task.id}`);
@ -148,10 +145,12 @@ export class SerialPrinter implements PrinterInterface {
}
async printStep(step: Step, db: Knex): Promise<void> {
if (!this.printer || !this.device) {
if (!this.commandExecutor) {
throw new Error('Printer not initialized');
}
logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
try {
await this.openPrinter();
@ -159,30 +158,30 @@ export class SerialPrinter implements PrinterInterface {
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'
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, stepNumber, task?.name));
const stepSection = formatUtils.section(
headerText,
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);
const commands = [
CommandBuilder.font(FONT.DEFAULT),
CommandBuilder.align(ALIGNMENT.CENTER),
CommandBuilder.style(STYLE.BOLD),
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
CommandBuilder.text(headerText),
CommandBuilder.newline(),
CommandBuilder.align(ALIGNMENT.LEFT),
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
CommandBuilder.text(step.instructions || 'No instructions provided'),
CommandBuilder.newline(),
CommandBuilder.newline(),
...CommandBuilder.stepBarcodeWithAlignment(step.id),
CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES),
];
await this.commandExecutor.executeCommands(commands);
await this.closePrinter();
logger.info(`Printed step ${step.id}`);

View File

@ -5,16 +5,27 @@ import { StepRepository, PrintHistoryRepository } from '../db/repositories';
import { Knex } from 'knex';
import logger from '../logger';
import { formatUtils } from './format-utils';
import { CommandBuilder } from './printer-commands';
import { TestCommandExecutor } from './command-executor';
import {
PAPER_CONFIG,
FONT_SIZES,
ALIGNMENT,
FONT,
STYLE,
} from './printer-constants';
export class TestPrinter implements Printer {
private readonly outputDir: string;
private printHistoryRepo: PrintHistoryRepository;
private stepRepository: StepRepository;
private commandExecutor: TestCommandExecutor;
constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) {
this.outputDir = path.join(process.cwd(), 'test-output');
this.printHistoryRepo = printHistoryRepo;
this.stepRepository = stepRepo;
this.commandExecutor = new TestCommandExecutor();
this.ensureOutputDir();
}
@ -35,12 +46,41 @@ export class TestPrinter implements Printer {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = path.join(this.outputDir, `task-${task.id}-${timestamp}.txt`);
const content = [
...formatUtils.formatSection(`Task: ${task.name}`, ''),
...formatUtils.formatList(taskSteps.map((step, index) =>
formatUtils.formatStepHeader(step.name, index + 1, task.name, true)
), 0, '- '),
].join('\n') + '\n';
const commands = [
// Header with task name
CommandBuilder.header(`Task: ${task.name}`),
CommandBuilder.banner('=', PAPER_CONFIG.BANNER_LENGTH),
CommandBuilder.align(ALIGNMENT.LEFT),
// Task ID as barcode
...CommandBuilder.taskBarcodeWithAlignment(task.id),
CommandBuilder.newline(),
];
// Add steps
for (let i = 0; i < taskSteps.length; i++) {
const step = taskSteps[i];
logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, i + 1, task.name, true));
commands.push(
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
CommandBuilder.text(headerText),
CommandBuilder.newline(),
...formatUtils.section(
'',
step.instructions || 'No instructions provided',
'-'
),
...CommandBuilder.stepBarcodeWithAlignment(step.id),
CommandBuilder.newline()
);
}
commands.push(CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES));
await this.commandExecutor.executeCommands(commands);
const content = this.commandExecutor.getOutput();
await fs.writeFile(filename, content);
logger.info(`Printed task ${task.id} to ${filename}`);
@ -61,10 +101,26 @@ export class TestPrinter implements Printer {
const task = await this.stepRepository.findTaskById(step.id);
const stepNumber = await this.stepRepository.findStepNumber(step.id);
const content = formatUtils.formatSection(
formatUtils.formatStepHeader(step.name, stepNumber, task?.name),
step.instructions
).join('\n') + '\n';
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, stepNumber, task?.name));
const commands = [
CommandBuilder.font(FONT.DEFAULT),
CommandBuilder.align(ALIGNMENT.CENTER),
CommandBuilder.style(STYLE.BOLD),
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
CommandBuilder.text(headerText),
CommandBuilder.newline(),
CommandBuilder.align(ALIGNMENT.LEFT),
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
CommandBuilder.text(step.instructions || 'No instructions provided'),
CommandBuilder.newline(),
CommandBuilder.newline(),
...CommandBuilder.stepBarcodeWithAlignment(step.id),
CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES),
];
await this.commandExecutor.executeCommands(commands);
const content = this.commandExecutor.getOutput();
await fs.writeFile(filename, content);
logger.info(`Printed step ${step.id} to ${filename}`);