refactor: remove comments and improve printer service

This commit is contained in:
Joelbrit0 2026-01-22 12:15:23 -03:00
parent 853aeddb75
commit 267b09946e
14 changed files with 614 additions and 924 deletions

109
API_CONTRACTS.md Normal file
View File

@ -0,0 +1,109 @@
# Documentação da API de Impressão
Esta API permite listar impressoras disponíveis no servidor (Windows) e enviar comandos de impressão de texto simples ou layouts HTML (etiquetas).
URL Base: `http://localhost:3000`
Swagger UI: `http://localhost:3000/api`
## Endpoints
### 1. Listar Impressoras
Retorna a lista de impressoras instaladas no servidor onde a API está rodando.
- **Método:** `GET`
- **Rota:** `/printer/list`
- **Exemplo de Resposta:**
```json
[
{
"name": "POS-80",
"portName": "USB001",
"driverName": "Generic / Text Only",
"printerStatus": 0,
"deviceId": "POS-80"
},
{
"name": "Microsoft Print to PDF",
"portName": "PORTPROMPT:",
"driverName": "Microsoft Print To PDF",
"printerStatus": 0
}
]
```
---
### 2. Imprimir HTML (Etiquetas/Layouts)
Converte um código HTML (com suporte a Tailwind CSS) para PDF e imprime na impressora selecionada. Ideal para etiquetas.
- **Método:** `POST`
- **Rota:** `/printer/print-html`
- **Corpo da Requisição (JSON):**
```json
{
"html": "<div class='text-xl font-bold'>Minha Etiqueta</div>...",
"printerName": "POS-80", // Nome exato da impressora (conforme retornado na listagem)
"width": "60mm", // Opcional. Largura do papel/etiqueta. Default: 80mm
"height": "40mm" // Opcional. Altura da etiqueta. Default: auto
}
```
- **Detalhes:**
- O backend injeta automaticamente o script do Tailwind CSS.
- O HTML é renderizado via Puppeteer (Chrome headless).
---
### 3. Imprimir Texto Simples (ESC/POS)
Envia comandos de texto diretamente para a impressora. Ideal para cupons simples e rápidos, sem formatação complexa.
- **Método:** `POST`
- **Rota:** `/printer/print`
- **Corpo da Requisição (JSON):**
```json
{
"lines": [
"--------------------------------",
" CUPOM FISCAL ",
"--------------------------------",
"Item 1 ................. R$ 10,00"
],
"alignment": "center", // "left", "center", "right". Default: left
"upsideDown": false, // Se true, imprime de cabeça para baixo
"printerInterface": "POS-80" // Pode ser nome da impressora (USB) ou "tcp://192.168.1.100" (Rede)
}
```
## Tipos (TypeScript Interfaces)
Se estiver usando TypeScript no Frontend, utilize estas interfaces:
```typescript
export interface Printer {
name: string;
portName?: string;
driverName?: string;
printerStatus?: number;
deviceId?: string;
}
export interface PrintHtmlPayload {
html: string;
printerName: string;
width?: string;
height?: string;
}
export interface PrintTextPayload {
lines: string[];
alignment?: 'left' | 'center' | 'right';
upsideDown?: boolean;
printerInterface?: string;
}
```

View File

@ -3,6 +3,7 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": true,
"plugins": ["@nestjs/swagger"]
}
}

1070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,17 +26,20 @@
"@nestjs/common": "^11.0.17",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.5",
"@nestjs/typeorm": "^11.0.0",
"@types/multer": "^2.0.0",
"multer": "^2.0.2",
"node-printer": "^1.0.4",
"node-thermal-printer": "^4.5.0",
"oracledb": "^6.10.0",
"pdf-to-printer": "^5.6.1",
"printer": "^0.4.0",
"puppeteer": "^24.35.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.28"
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.28",
"unix-print": "^1.3.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",

View File

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ListPrinterController } from './controller/list-printer.controller';
import { ListPrinterService } from './services/list-printer';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './config/typeorm.config';
import { ListPrinterController } from './controller/list-printer.controller';
import { ListPrinterService } from './services/create-printer';
@Module({
imports: [TypeOrmModule.forRoot(typeOrmConfig)],

View File

@ -1,17 +1,62 @@
import { Controller, Get, Post, Body } from '@nestjs/common';
import { ListPrinterService } from '../services/list-printer';
import { Controller, Post, Body, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ListPrinterService } from '../services/create-printer';
import { PrintDataDto } from '../dto/print-data.dto';
import { PrintHtmlDto } from '../dto/print-html.dto';
import { PrinterDto } from '../dto/printer.dto';
import { getPrinters } from '../services/get-printer';
@Controller('printers')
@ApiTags('Printer')
@Controller('printer')
export class ListPrinterController {
constructor(private readonly listPrinterService: ListPrinterService) {}
constructor(private readonly printerService: ListPrinterService) {}
@Get()
async getPrinters() {
return this.listPrinterService.listPrinters();
@Post(':printerName/print')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Envia um comando de impressão de texto simples (ESC/POS)' })
@ApiResponse({ status: 200, description: 'Impressão enviada com sucesso' })
async print(
@Param('printerName') printerName: string,
@Body() printData: PrintDataDto
) {
printData.printerInterface = printerName;
return this.printerService.printReceipt(printData);
}
@Post('print')
async print(@Body() body: { printerName: string; filePath: string }) {
return this.listPrinterService.printDocument(body.printerName, body.filePath);
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Envia um comando de impressão (Compatibilidade)' })
async printLegacy(@Body() printData: PrintDataDto) {
return this.printerService.printReceipt(printData);
}
@Post(':printerName/print-html')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Converte HTML para PDF e imprime na impressora especificada na URL' })
@ApiResponse({ status: 200, description: 'HTML enviado para impressão' })
async printHtml(
@Param('printerName') printerName: string,
@Body() printHtmlDto: PrintHtmlDto
) {
printHtmlDto.printerName = printerName;
return this.printerService.printHtml(printHtmlDto);
}
@Post('print-html')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Converte HTML para PDF (Compatibilidade)' })
async printHtmlLegacy(@Body() printHtmlDto: PrintHtmlDto) {
return this.printerService.printHtml(printHtmlDto);
}
@Get('list')
@ApiOperation({ summary: 'Lista impressoras disponíveis no sistema' })
@ApiResponse({
status: 200,
description: 'Lista de impressoras encontradas',
type: [PrinterDto]
})
async listPrinters() {
return getPrinters();
}
}

31
src/dto/print-data.dto.ts Normal file
View File

@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
export class PrintDataDto {
@ApiProperty({
example: ['--------------------------------', ' TESTE DE IMPRESSAO ', '--------------------------------'],
description: 'Lista de linhas de texto para imprimir'
})
lines: string[];
@ApiProperty({
required: false,
enum: ['left', 'center', 'right'],
example: 'center',
description: 'Alinhamento do texto'
})
alignment?: 'left' | 'center' | 'right';
@ApiProperty({
required: false,
example: false,
description: 'Se verdadeiro, imprime de cabeça para baixo (se suportado)'
})
upsideDown?: boolean;
@ApiProperty({
required: false,
example: 'tcp://10.1.119.13',
description: 'Interface da impressora. Ex: tcp://ip:porta ou printer:NomeDaImpressora'
})
printerInterface?: string;
}

21
src/dto/print-html.dto.ts Normal file
View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
export class PrintHtmlDto {
@ApiProperty({
example: '<h1>Titulo</h1><p>Conteudo do pedido...</p>',
description: 'Conteúdo HTML para impressão'
})
html: string;
@ApiProperty({
example: 'POS-80',
description: 'Nome da impressora onde será impresso o PDF gerado'
})
printerName: string;
@ApiProperty({ example: '60mm', required: false, description: 'Largura da etiqueta (default: 80mm)' })
width?: string;
@ApiProperty({ example: '40mm', required: false, description: 'Altura da etiqueta (default: auto)' })
height?: string;
}

18
src/dto/printer.dto.ts Normal file
View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
export class PrinterDto {
@ApiProperty({ example: 'POS-80', description: 'Nome da impressora' })
name: string;
@ApiProperty({ example: 'USB001', description: 'Porta da impressora (Win32)' })
portName?: string;
@ApiProperty({ example: 'Generic / Text Only', description: 'Nome do driver' })
driverName?: string;
@ApiProperty({ example: 0, description: 'Status da impressora (0 = Idle)' })
printerStatus?: number;
@ApiProperty({ example: 'POS-80', description: 'ID do dispositivo (usado para impressão)' })
deviceId?: string;
}

View File

@ -0,0 +1,5 @@
export interface PrintData {
lines: string[];
alignment?: 'left' | 'center' | 'right';
upsideDown?: boolean;
}

View File

@ -1,10 +1,20 @@
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { initializeOracleClient } from './helpers/oracle.helper';
async function bootstrap() {
initializeOracleClient();
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('API Documentation')
.setDescription('The API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();

View File

@ -0,0 +1,143 @@
import { Injectable } from '@nestjs/common';
import { PrintDataDto } from '../dto/print-data.dto';
import { PrintHtmlDto } from '../dto/print-html.dto';
const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer');
@Injectable()
export class ListPrinterService {
private readonly printerType = PrinterTypes.EPSON;
async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> {
if (!data.lines?.length) {
return { success: false, message: 'No lines provided' };
}
try {
let printerInterface = data.printerInterface;
if (printerInterface && !printerInterface.includes('://') && !printerInterface.startsWith('printer:')) {
printerInterface = `printer:${printerInterface}`;
}
const printer = new ThermalPrinter({
type: this.printerType,
interface: printerInterface,
});
this.applyAlignment(printer, data.alignment);
for (const line of data.lines) {
if (data.upsideDown) {
printer.upsideDown(true);
}
printer.println(line);
if (data.upsideDown) {
printer.upsideDown(false);
}
}
printer.drawLine();
printer.cut();
printer.beep();
await printer.execute();
return { success: true, message: 'Print successful!' };
} catch (error) {
return {
success: false,
message: `Print failed: ${error.message || error}`
};
}
}
private applyAlignment(printer: any, alignment?: string): void {
if (alignment === 'center') {
return printer.alignCenter();
}
if (alignment === 'right') {
return printer.alignRight();
}
return printer.alignLeft();
}
async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string }> {
const puppeteer = require('puppeteer');
const pdfPrinter = require('pdf-to-printer');
const path = require('path');
const fs = require('fs');
const os = require('os');
const tempFilePath = path.join(os.tmpdir(), `print-${Date.now()}.pdf`);
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%;
}
body {
transform-origin: top left;
zoom: 1.25;
}
@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 });
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

@ -0,0 +1,17 @@
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export const getPrinters = async () => {
try {
const command = `powershell "Get-Printer | Select-Object Name, DriverName, PrinterStatus | ConvertTo-Json"`;
const { stdout } = await execAsync(command);
if (!stdout) return [];
const data = JSON.parse(stdout);
return Array.isArray(data) ? data : [data];
} catch (error) {
console.error("Erro ao listar impressoras via PowerShell:", error);
return [];
}
};

View File

@ -1,37 +0,0 @@
import { Injectable } from '@nestjs/common';
import { exec } from 'child_process';
import { promisify } from 'util';
import { print } from 'pdf-to-printer';
const execAsync = promisify(exec);
@Injectable()
export class ListPrinterService {
async listPrinters(): Promise<string[]> {
try {
const { stdout } = await execAsync(
'powershell -Command "Get-Printer | Select-Object -ExpandProperty Name"',
{ encoding: 'utf-8' }
);
const printers = stdout
.split('\r\n')
.map(line => line.trim())
.filter(line => line && line !== 'Name');
return printers;
} catch (error) {
throw new Error(`Failed to list printers: ${(error as Error).message}`);
}
}
async printDocument(printerName: string, filePath: string): Promise<{ success: boolean; message: string }> {
try {
await print(filePath, { printer: printerName });
return { success: true, message: `Document sent to printer: ${printerName}` };
} catch (error) {
throw new Error(`Failed to print document: ${(error as Error).message}`);
}
}
}