switch printing to command pattern
This commit is contained in:
parent
8f3c8ba80e
commit
7d680e469f
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
98
server/src/printer/__tests__/barcode-debug.test.ts
Normal file
98
server/src/printer/__tests__/barcode-debug.test.ts
Normal 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');
|
||||
});
|
||||
});
|
167
server/src/printer/__tests__/barcode-utils.test.ts
Normal file
167
server/src/printer/__tests__/barcode-utils.test.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
});
|
133
server/src/printer/__tests__/command-system.test.ts
Normal file
133
server/src/printer/__tests__/command-system.test.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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('banner returns a BANNER command', () => {
|
||||
expect(formatUtils.banner('=', 10)).toEqual([Command.BANNER, '=', 10]);
|
||||
});
|
||||
|
||||
it('should create a banner with custom length', () => {
|
||||
const banner = formatUtils.createBanner('-', 32);
|
||||
expect(banner).toBe('-'.repeat(32));
|
||||
it('checkbox returns a CHECKBOX command', () => {
|
||||
expect(formatUtils.checkbox('Test')).toEqual([Command.CHECKBOX, 'Test']);
|
||||
});
|
||||
|
||||
it('should handle empty character', () => {
|
||||
const banner = formatUtils.createBanner('', 10);
|
||||
expect(banner).toBe('');
|
||||
it('list returns a LIST command', () => {
|
||||
expect(formatUtils.list(['A', 'B'], 1, '- ')).toEqual([[Command.LIST, ['A', 'B'], 1, '- ']]);
|
||||
});
|
||||
|
||||
it('should handle zero length', () => {
|
||||
const banner = formatUtils.createBanner('*', 0);
|
||||
expect(banner).toBe('');
|
||||
});
|
||||
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('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)');
|
||||
});
|
||||
});
|
||||
|
||||
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('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 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('getCheckboxText extracts text from a CHECKBOX command', () => {
|
||||
expect(formatUtils.getCheckboxText([Command.CHECKBOX, 'Hello'])).toBe('Hello');
|
||||
expect(formatUtils.getCheckboxText([Command.BANNER, '='])).toBe('');
|
||||
});
|
||||
|
||||
it('taskHeader returns a CHECKBOX command for the task', () => {
|
||||
expect(formatUtils.taskHeader('My Task')).toEqual([Command.CHECKBOX, 'Task: My Task']);
|
||||
});
|
||||
|
||||
it('taskSection returns a list of commands for the task section', () => {
|
||||
expect(formatUtils.taskSection('My Task')).toEqual([
|
||||
[Command.CHECKBOX, 'Task: My Task'],
|
||||
[Command.BANNER, '='],
|
||||
]);
|
||||
});
|
||||
|
||||
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',
|
||||
'[ ] '
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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('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('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('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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
@ -41,3 +43,112 @@ describe('Printer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 ---');
|
||||
});
|
||||
});
|
51
server/src/printer/__tests__/step-data-debug.test.ts
Normal file
51
server/src/printer/__tests__/step-data-debug.test.ts
Normal 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');
|
||||
});
|
||||
});
|
106
server/src/printer/barcode-utils.ts
Normal file
106
server/src/printer/barcode-utils.ts
Normal 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();
|
||||
}
|
||||
}
|
266
server/src/printer/command-executor.ts
Normal file
266
server/src/printer/command-executor.ts
Normal 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';
|
||||
}
|
||||
}
|
@ -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, '='],
|
||||
];
|
||||
},
|
||||
};
|
@ -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';
|
||||
|
148
server/src/printer/printer-commands.ts
Normal file
148
server/src/printer/printer-commands.ts
Normal 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];
|
||||
}
|
||||
}
|
@ -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 = {
|
||||
|
@ -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),
|
||||
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}`);
|
||||
|
@ -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}`);
|
||||
|
Loading…
Reference in New Issue
Block a user