import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, LessThan, IsNull } from 'typeorm'; import { Subject, Observable } from 'rxjs'; import { LabelPrintJob } from '../database/entities/label-print-job.entity'; import { BarcodeTemplate } from '../database/entities/barcode-template.entity'; import { LabelRendererService } from './label-renderer.service'; function isSafeUrl(url: string): boolean { try { const parsed = new URL(url); return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } catch { return false; } } function applyVars(template: string, vars: Record): string { return template.replace(/\{([^}]+)\}/g, (_, key: string) => { const colonIdx = key.indexOf(':'); if (colonIdx !== -1) { const varName = key.slice(0, colonIdx); const width = parseInt(key.slice(colonIdx + 1), 10); if (!isNaN(width) && varName in vars) { return vars[varName].padStart(width, '0'); } } return vars[key] ?? ''; }); } function lockExpiry(): Date { const d = new Date(); d.setMinutes(d.getMinutes() - 5); return d; } @Injectable() export class LabelPrintAgentService { private readonly logger = new Logger(LabelPrintAgentService.name); private readonly jobCreated$ = new Subject(); get newJob$(): Observable { return this.jobCreated$.asObservable(); } constructor( @InjectRepository(LabelPrintJob) private readonly jobRepo: Repository, @InjectRepository(BarcodeTemplate) private readonly templateRepo: Repository, private readonly renderer: LabelRendererService, ) {} async createJob( templateId: number, fieldValues: Record, ): Promise { const template = await this.templateRepo.findOne({ where: { Id: templateId } }); if (!template) throw new NotFoundException('Template nicht gefunden'); if (!template.LabelEnabled) throw new BadRequestException('Etikett-Druck für dieses Template nicht aktiviert'); // Variablen aufbauen const vars: Record = { ...fieldValues }; // Datum-Felder: year/month/day separat ablegen for (const field of template.LabelInputFields ?? []) { if (field.type === 'date' && fieldValues[field.name]) { const raw = fieldValues[field.name]; // erwartet YYYY-MM-DD const parts = raw.split('-'); if (parts.length === 3) { vars[field.name] = `${parts[2]}.${parts[1]}.${parts[0]}`; // dd.MM.yyyy vars[`${field.name}.year`] = parts[0]; vars[`${field.name}.month`] = parts[1]; vars[`${field.name}.day`] = parts[2]; } } } // GET-URL aufrufen → {number} if (template.LabelGetUrl) { const url = applyVars(template.LabelGetUrl, vars); if (isSafeUrl(url)) { try { const res = await fetch(url); const text = (await res.text()).trim(); vars['number'] = text; } catch (err: any) { this.logger.warn(`GET-URL fehlgeschlagen (${url}): ${err.message}`); } } else { this.logger.warn(`GET-URL übersprungen (ungültiges Protokoll): ${url}`); } } const job = this.jobRepo.create({ Status: 'pending', LabelImageData: null, LabelWidthMm: template.LabelWidthMm ?? 57, LabelHeightMm: template.LabelHeightMm ?? 32, BarcodeTemplateId: template.Id, LabelVariables: vars, LockedAt: null, LockedByAgent: null, }); const saved = await this.jobRepo.save(job); this.jobCreated$.next(); return saved; } async claimNextJob(agentId: string): Promise { // Kandidat: pending und kein Lock oder Lock abgelaufen (> 5 Min) const candidate = await this.jobRepo.findOne({ where: [ { Status: 'pending', LockedAt: IsNull() }, { Status: 'pending', LockedAt: LessThan(lockExpiry()) }, ], order: { CreatedAt: 'ASC' }, }); if (!candidate) return null; // Lock setzen candidate.LockedAt = new Date(); candidate.LockedByAgent = agentId; await this.jobRepo.save(candidate); // Lazy render if (!candidate.LabelImageData) { const template = await this.templateRepo.findOne({ where: { Id: candidate.BarcodeTemplateId ?? undefined } }); if (template?.LabelLayout?.length) { try { candidate.LabelImageData = await this.renderer.render( template.LabelLayout, candidate.LabelWidthMm, candidate.LabelHeightMm, candidate.LabelVariables ?? {}, ); await this.jobRepo.save(candidate); } catch (err: any) { this.logger.error(`Label-Rendering fehlgeschlagen für Job ${candidate.Id}: ${err.message}`); } } } return candidate; } async getJobImage(jobId: number): Promise { const job = await this.jobRepo.findOne({ where: { Id: jobId } }); return job?.LabelImageData ?? null; } async markPrinted(jobId: number, agentId: string, printerName: string): Promise { const job = await this.jobRepo.findOne({ where: { Id: jobId } }); if (!job) throw new NotFoundException('Job nicht gefunden'); job.Status = 'printed'; job.PrintedAt = new Date(); job.PrintedByAgent = agentId; job.PrinterName = printerName; await this.jobRepo.save(job); await this.callUrl('PRINTED', job); } async markError(jobId: number, agentId: string, printerName: string, errorMessage: string): Promise { const job = await this.jobRepo.findOne({ where: { Id: jobId } }); if (!job) throw new NotFoundException('Job nicht gefunden'); job.Status = 'error'; job.PrintedByAgent = agentId; job.PrinterName = printerName; job.ErrorMessage = errorMessage; await this.jobRepo.save(job); await this.callUrl('RELEASE', job); } async renderPreview(templateId: number, fieldValues: Record): Promise { const template = await this.templateRepo.findOne({ where: { Id: templateId } }); if (!template) throw new NotFoundException('Template nicht gefunden'); if (!template.LabelLayout?.length) throw new BadRequestException('Kein Layout definiert'); const vars: Record = { ...fieldValues }; for (const field of template.LabelInputFields ?? []) { if (field.type === 'date' && fieldValues[field.name]) { const parts = fieldValues[field.name].split('-'); if (parts.length === 3) { vars[field.name] = `${parts[2]}.${parts[1]}.${parts[0]}`; // dd.MM.yyyy vars[`${field.name}.year`] = parts[0]; vars[`${field.name}.month`] = parts[1]; vars[`${field.name}.day`] = parts[2]; } } } vars['number'] = '1'; return this.renderer.render( template.LabelLayout, template.LabelWidthMm ?? 57, template.LabelHeightMm ?? 32, vars, ); } private async callUrl(type: 'PRINTED' | 'RELEASE', job: LabelPrintJob): Promise { const template = job.BarcodeTemplateId ? await this.templateRepo.findOne({ where: { Id: job.BarcodeTemplateId } }) : null; if (!template) return; const urlTemplate = type === 'PRINTED' ? template.LabelPrintedUrl : template.LabelReleaseUrl; if (!urlTemplate) return; const url = applyVars(urlTemplate, job.LabelVariables ?? {}); if (!isSafeUrl(url)) { this.logger.warn(`${type}-URL übersprungen (ungültiges Protokoll): ${url}`); return; } try { await fetch(url, { method: 'POST' }); } catch (err: any) { this.logger.warn(`${type}-URL fehlgeschlagen (${url}): ${err.message}`); } } }