From 71b447154de12f8a53796a1c1f42980419a1b950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Thu, 7 May 2026 23:52:39 +0200 Subject: [PATCH] feat: add label preview functionality to barcode templates with backend rendering and UI modal --- .../label-print-agent.controller.ts | 13 ++++ .../label-print-agent.service.ts | 33 ++++++++++ paperless-frontend/src/api/labelPrintAgent.ts | 5 ++ paperless-frontend/src/pages/SettingsPage.tsx | 60 +++++++++++++++++-- 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/paperless-backend/src/label-print-agent/label-print-agent.controller.ts b/paperless-backend/src/label-print-agent/label-print-agent.controller.ts index bef4bf0..3a26d5d 100644 --- a/paperless-backend/src/label-print-agent/label-print-agent.controller.ts +++ b/paperless-backend/src/label-print-agent/label-print-agent.controller.ts @@ -24,6 +24,19 @@ import { LabelPrintAgentService } from './label-print-agent.service'; export class LabelPrintAgentController { constructor(private readonly service: LabelPrintAgentService) {} + @Post('preview') + @HttpCode(HttpStatus.OK) + @RequirePermissions(Permission.VIEW_SCANNER) + async preview( + @Body() body: { templateId: number; fieldValues?: Record }, + @Res({ passthrough: true }) res: Response, + ): Promise { + const buf = await this.service.renderPreview(body.templateId, body.fieldValues ?? {}); + const { Readable } = await import('stream'); + res.setHeader('Content-Type', 'image/png'); + return new StreamableFile(Readable.from(buf)); + } + // Manuell einen Job anlegen (Frontend → Backend) @Post('jobs') @HttpCode(HttpStatus.CREATED) diff --git a/paperless-backend/src/label-print-agent/label-print-agent.service.ts b/paperless-backend/src/label-print-agent/label-print-agent.service.ts index 1623ddd..49be412 100644 --- a/paperless-backend/src/label-print-agent/label-print-agent.service.ts +++ b/paperless-backend/src/label-print-agent/label-print-agent.service.ts @@ -155,6 +155,39 @@ export class LabelPrintAgentService { 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}.year`] = parts[0]; + vars[`${field.name}.month`] = parts[1]; + vars[`${field.name}.day`] = parts[2]; + } + } + } + if (template.LabelGetUrl) { + const url = applyVars(template.LabelGetUrl, vars); + try { + vars['number'] = (await (await fetch(url)).text()).trim(); + } catch (err: any) { + this.logger.warn(`GET-URL fehlgeschlagen (${url}): ${err.message}`); + } + } + + 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 } }) diff --git a/paperless-frontend/src/api/labelPrintAgent.ts b/paperless-frontend/src/api/labelPrintAgent.ts index de725b7..9a061ee 100644 --- a/paperless-frontend/src/api/labelPrintAgent.ts +++ b/paperless-frontend/src/api/labelPrintAgent.ts @@ -5,4 +5,9 @@ export const labelPrintAgentApi = { api .post<{ jobId: string }>('/api/label-print-agent/jobs', { templateId, fieldValues }) .then((r) => r.data), + + previewLabel: async (templateId: number, fieldValues: Record): Promise => { + const res = await api.post('/api/label-print-agent/preview', { templateId, fieldValues }, { responseType: 'blob' }); + return URL.createObjectURL(res.data as Blob); + }, }; diff --git a/paperless-frontend/src/pages/SettingsPage.tsx b/paperless-frontend/src/pages/SettingsPage.tsx index 3714cb5..be81853 100644 --- a/paperless-frontend/src/pages/SettingsPage.tsx +++ b/paperless-frontend/src/pages/SettingsPage.tsx @@ -8,7 +8,7 @@ import { UserOutlined, FileTextOutlined, ThunderboltOutlined, PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined, HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined, - QrcodeOutlined, UnorderedListOutlined, + QrcodeOutlined, UnorderedListOutlined, PrinterOutlined, } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { @@ -26,6 +26,7 @@ import { type BarcodeTemplate, type LabelInputField, } from '../api/barcode-templates'; +import { labelPrintAgentApi } from '../api/labelPrintAgent'; import { paperlessApi, type PaperlessTag, type PaperlessDocType, type PaperlessCustomField, type PaperlessCorrespondent, @@ -1845,6 +1846,8 @@ function BarcodeTemplatesTab() { const [editing, setEditing] = useState(null); const [isNew, setIsNew] = useState(false); const [testValue, setTestValue] = useState(''); + const [testPrinting, setTestPrinting] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); const [form] = Form.useForm(); const load = useCallback(async () => { @@ -1915,6 +1918,31 @@ function BarcodeTemplatesTab() { } }; + const handleTestLabel = async () => { + if (!editing) return; + setTestPrinting(true); + try { + const values = await form.validateFields(); + await barcodeTemplatesApi.update(editing.Id, values); + const inputFields: LabelInputField[] = values.LabelInputFields ?? []; + const testFieldValues: Record = {}; + const today = new Date().toISOString().slice(0, 10); + for (const f of inputFields) { + if (f.type === 'date') testFieldValues[f.name] = today; + else if (f.type === 'number') testFieldValues[f.name] = '1'; + else testFieldValues[f.name] = 'Test'; + } + const url = await labelPrintAgentApi.previewLabel(editing.Id, testFieldValues); + if (previewUrl) URL.revokeObjectURL(previewUrl); + setPreviewUrl(url); + load(); + } catch (err: any) { + message.error(err?.response?.data?.message ?? 'Vorschau fehlgeschlagen'); + } finally { + setTestPrinting(false); + } + }; + const handleDelete = async (id: number) => { await barcodeTemplatesApi.remove(id); message.success('Vorlage gelöscht'); @@ -1981,11 +2009,17 @@ function BarcodeTemplatesTab() { { setModalOpen(false); setEditing(null); }} - okText="Speichern" - cancelText="Abbrechen" width={720} + footer={[ + ...(editing && !isNew ? [ + , + ] : []), + , + , + ]} >
@@ -2164,6 +2198,24 @@ function BarcodeTemplatesTab() { )} + + { URL.revokeObjectURL(previewUrl!); setPreviewUrl(null); }} + footer={} + width={520} + > + {previewUrl && ( +
+ Etikett-Vorschau +
+ )} +
); }