Compare commits

..

No commits in common. "6e45369855d96d5116d6cec28cbc4ef5af1b2541" and "267b09946e3b429a2124af8434adcec6a869c13d" have entirely different histories.

7 changed files with 186 additions and 821 deletions

766
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,18 +23,12 @@
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^6.16.4",
"@bull-board/express": "^6.16.4",
"@bull-board/nestjs": "^6.16.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.0.17", "@nestjs/common": "^11.0.17",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.5", "@nestjs/swagger": "^11.2.5",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"bullmq": "^5.66.6",
"multer": "^2.0.2", "multer": "^2.0.2",
"node-printer": "^1.0.4", "node-printer": "^1.0.4",
"node-thermal-printer": "^4.5.0", "node-thermal-printer": "^4.5.0",

View File

@ -1,21 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './config/typeorm.config'; import { typeOrmConfig } from './config/typeorm.config';
import { ListPrinterController } from './controller/list-printer.controller'; import { ListPrinterController } from './controller/list-printer.controller';
import { ListPrinterService } from './services/create-printer'; import { ListPrinterService } from './services/create-printer';
import { PrinterProcessor } from './services/printer.processor';
import { BullQueuesModule } from './config/BullModule';
@Module({ @Module({
imports: [ imports: [TypeOrmModule.forRoot(typeOrmConfig)],
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot(typeOrmConfig),
BullQueuesModule
],
controllers: [AppController, ListPrinterController], controllers: [AppController, ListPrinterController],
providers: [AppService, ListPrinterService, PrinterProcessor], providers: [AppService, ListPrinterService],
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,47 +0,0 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';
import { BullBoardModule } from '@bull-board/nestjs';
import { ExpressAdapter } from '@bull-board/express';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
connection: {
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT') || 6379,
password: configService.get<string>('REDIS_PASSWORD'),
},
}),
inject: [ConfigService],
}),
BullBoardModule.forRoot({
route: '/queues',
adapter: ExpressAdapter,
}),
BullModule.registerQueue({
name: 'printer',
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
removeOnComplete: false,
removeOnFail: { count: 500 },
},
}),
BullBoardModule.forFeature({
name: 'printer',
adapter: BullMQAdapter,
}),
],
exports: [BullModule],
})
export class BullQueuesModule {}

View File

@ -18,11 +18,4 @@ export class PrintHtmlDto {
@ApiProperty({ example: '40mm', required: false, description: 'Altura da etiqueta (default: auto)' }) @ApiProperty({ example: '40mm', required: false, description: 'Altura da etiqueta (default: auto)' })
height?: string; height?: string;
@ApiProperty({
example: 'impresso-12345',
required: false,
description: 'ID único do job para idempotência (evita duplicados)'
})
jobId?: string;
} }

View File

@ -1,16 +1,12 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { PrintDataDto } from '../dto/print-data.dto'; import { PrintDataDto } from '../dto/print-data.dto';
import { PrintHtmlDto } from '../dto/print-html.dto'; import { PrintHtmlDto } from '../dto/print-html.dto';
import { ThermalPrinter, PrinterTypes } from 'node-thermal-printer'; const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer');
@Injectable() @Injectable()
export class ListPrinterService { export class ListPrinterService {
private readonly printerType = PrinterTypes.EPSON; private readonly printerType = PrinterTypes.EPSON;
constructor(@InjectQueue('printer') private printerQueue: Queue) {}
async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> { async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> {
if (!data.lines?.length) { if (!data.lines?.length) {
return { success: false, message: 'No lines provided' }; return { success: false, message: 'No lines provided' };
@ -41,6 +37,7 @@ export class ListPrinterService {
} }
printer.drawLine(); printer.drawLine();
printer.cut();
printer.beep(); printer.beep();
await printer.execute(); await printer.execute();
@ -66,20 +63,81 @@ export class ListPrinterService {
return printer.alignLeft(); return printer.alignLeft();
} }
async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string; jobId?: string }> { async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string }> {
const jobOptions: any = {}; const puppeteer = require('puppeteer');
const pdfPrinter = require('pdf-to-printer');
const path = require('path');
const fs = require('fs');
const os = require('os');
if (data.jobId) { const tempFilePath = path.join(os.tmpdir(), `print-${Date.now()}.pdf`);
const safeJobId = /^\d+$/.test(data.jobId) ? `print-${data.jobId}` : data.jobId;
jobOptions.jobId = safeJobId; try {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const width = data.width || '80mm';
const height = data.height;
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<style>
html, body {
margin: 0;
padding: 0;
-webkit-print-color-adjust: exact;
width: 100%;
height: 100%;
} }
const job = await this.printerQueue.add('print-html-job', data, jobOptions); body {
transform-origin: top left;
zoom: 1.25;
}
@page { size: ${width} ${height || 'auto'}; margin: 0; }
</style>
</head>
<body>
${data.html}
</body>
</html>
`;
return { await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
success: true,
message: 'Tarefa enviada para a fila de impressão', const pdfOptions: any = {
jobId: job.id path: tempFilePath,
printBackground: true,
width: width,
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }
}; };
if (height) {
pdfOptions.height = height;
}
await page.pdf(pdfOptions);
await browser.close();
await pdfPrinter.print(tempFilePath, { printer: data.printerName });
return { success: true, message: 'HTML convertido (com Tailwind) e enviado para impressão!' };
} catch (error) {
console.error('Erro na impressão HTML:', error);
return { success: false, message: `Erro ao imprimir HTML: ${error.message}` };
} finally {
try {
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath);
}
} catch (e) {
console.warn('Não foi possível deletar arquivo temp:', e);
}
}
} }
} }

View File

@ -1,74 +0,0 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import * as puppeteer from 'puppeteer';
import * as pdfPrinter from 'pdf-to-printer';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
@Processor('printer')
export class PrinterProcessor extends WorkerHost {
async process(job: Job<any>): Promise<any> {
const data = job.data;
const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`);
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
const width = data.width || '80mm';
const height = data.height;
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<style>
html, body { margin: 0; padding: 0; }
body { zoom: 1.4; transform-origin: top left; }
@page { size: ${width} ${height || 'auto'}; margin: 0; }
</style>
</head>
<body>${data.html}</body>
</html>
`;
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
const pdfOptions: any = {
path: tempFilePath,
printBackground: true,
width: width,
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }
};
if (height) pdfOptions.height = height;
await page.pdf(pdfOptions);
await browser.close();
await pdfPrinter.print(tempFilePath, {
printer: data.printerName,
silent: true
});
return { status: 'success', jobId: job.id };
} catch (error) {
if (browser) await browser.close();
console.error(`Erro no Job ${job.id}:`, error);
throw error;
} finally {
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath);
}
}
}
}