feat: add label preview functionality to barcode templates with backend rendering and UI modal
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s
This commit is contained in:
@@ -24,6 +24,19 @@ import { LabelPrintAgentService } from './label-print-agent.service';
|
|||||||
export class LabelPrintAgentController {
|
export class LabelPrintAgentController {
|
||||||
constructor(private readonly service: LabelPrintAgentService) {}
|
constructor(private readonly service: LabelPrintAgentService) {}
|
||||||
|
|
||||||
|
@Post('preview')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@RequirePermissions(Permission.VIEW_SCANNER)
|
||||||
|
async preview(
|
||||||
|
@Body() body: { templateId: number; fieldValues?: Record<string, string> },
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<StreamableFile> {
|
||||||
|
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)
|
// Manuell einen Job anlegen (Frontend → Backend)
|
||||||
@Post('jobs')
|
@Post('jobs')
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
|||||||
@@ -155,6 +155,39 @@ export class LabelPrintAgentService {
|
|||||||
await this.callUrl('RELEASE', job);
|
await this.callUrl('RELEASE', job);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async renderPreview(templateId: number, fieldValues: Record<string, string>): Promise<Buffer> {
|
||||||
|
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<string, string> = { ...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<void> {
|
private async callUrl(type: 'PRINTED' | 'RELEASE', job: LabelPrintJob): Promise<void> {
|
||||||
const template = job.BarcodeTemplateId
|
const template = job.BarcodeTemplateId
|
||||||
? await this.templateRepo.findOne({ where: { Id: job.BarcodeTemplateId } })
|
? await this.templateRepo.findOne({ where: { Id: job.BarcodeTemplateId } })
|
||||||
|
|||||||
@@ -5,4 +5,9 @@ export const labelPrintAgentApi = {
|
|||||||
api
|
api
|
||||||
.post<{ jobId: string }>('/api/label-print-agent/jobs', { templateId, fieldValues })
|
.post<{ jobId: string }>('/api/label-print-agent/jobs', { templateId, fieldValues })
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
previewLabel: async (templateId: number, fieldValues: Record<string, string>): Promise<string> => {
|
||||||
|
const res = await api.post('/api/label-print-agent/preview', { templateId, fieldValues }, { responseType: 'blob' });
|
||||||
|
return URL.createObjectURL(res.data as Blob);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
UserOutlined, FileTextOutlined, ThunderboltOutlined,
|
UserOutlined, FileTextOutlined, ThunderboltOutlined,
|
||||||
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
|
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
|
||||||
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
|
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
|
||||||
QrcodeOutlined, UnorderedListOutlined,
|
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
type BarcodeTemplate,
|
type BarcodeTemplate,
|
||||||
type LabelInputField,
|
type LabelInputField,
|
||||||
} from '../api/barcode-templates';
|
} from '../api/barcode-templates';
|
||||||
|
import { labelPrintAgentApi } from '../api/labelPrintAgent';
|
||||||
import {
|
import {
|
||||||
paperlessApi, type PaperlessTag, type PaperlessDocType,
|
paperlessApi, type PaperlessTag, type PaperlessDocType,
|
||||||
type PaperlessCustomField, type PaperlessCorrespondent,
|
type PaperlessCustomField, type PaperlessCorrespondent,
|
||||||
@@ -1845,6 +1846,8 @@ function BarcodeTemplatesTab() {
|
|||||||
const [editing, setEditing] = useState<BarcodeTemplate | null>(null);
|
const [editing, setEditing] = useState<BarcodeTemplate | null>(null);
|
||||||
const [isNew, setIsNew] = useState(false);
|
const [isNew, setIsNew] = useState(false);
|
||||||
const [testValue, setTestValue] = useState('');
|
const [testValue, setTestValue] = useState('');
|
||||||
|
const [testPrinting, setTestPrinting] = useState(false);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
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<string, string> = {};
|
||||||
|
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) => {
|
const handleDelete = async (id: number) => {
|
||||||
await barcodeTemplatesApi.remove(id);
|
await barcodeTemplatesApi.remove(id);
|
||||||
message.success('Vorlage gelöscht');
|
message.success('Vorlage gelöscht');
|
||||||
@@ -1981,11 +2009,17 @@ function BarcodeTemplatesTab() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={editing ? 'Eingangsdokumentart bearbeiten' : 'Neue Eingangsdokumentart'}
|
title={editing ? 'Eingangsdokumentart bearbeiten' : 'Neue Eingangsdokumentart'}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
onOk={handleSave}
|
|
||||||
onCancel={() => { setModalOpen(false); setEditing(null); }}
|
onCancel={() => { setModalOpen(false); setEditing(null); }}
|
||||||
okText="Speichern"
|
|
||||||
cancelText="Abbrechen"
|
|
||||||
width={720}
|
width={720}
|
||||||
|
footer={[
|
||||||
|
...(editing && !isNew ? [
|
||||||
|
<Button key="test" icon={<PrinterOutlined />} loading={testPrinting} onClick={handleTestLabel}>
|
||||||
|
Testetikett erstellen
|
||||||
|
</Button>,
|
||||||
|
] : []),
|
||||||
|
<Button key="cancel" onClick={() => { setModalOpen(false); setEditing(null); }}>Abbrechen</Button>,
|
||||||
|
<Button key="save" type="primary" onClick={handleSave}>Speichern</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item name="Name" label="Name" rules={[{ required: true, message: 'Name ist erforderlich' }]}>
|
<Form.Item name="Name" label="Name" rules={[{ required: true, message: 'Name ist erforderlich' }]}>
|
||||||
@@ -2164,6 +2198,24 @@ function BarcodeTemplatesTab() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Etikett-Vorschau"
|
||||||
|
open={!!previewUrl}
|
||||||
|
onCancel={() => { URL.revokeObjectURL(previewUrl!); setPreviewUrl(null); }}
|
||||||
|
footer={<Button onClick={() => { URL.revokeObjectURL(previewUrl!); setPreviewUrl(null); }}>Schließen</Button>}
|
||||||
|
width={520}
|
||||||
|
>
|
||||||
|
{previewUrl && (
|
||||||
|
<div style={{ background: '#e0e0e0', padding: 24, display: 'flex', justifyContent: 'center', borderRadius: 4 }}>
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Etikett-Vorschau"
|
||||||
|
style={{ maxWidth: '100%', boxShadow: '0 2px 8px rgba(0,0,0,0.3)', display: 'block' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user