feat: implement backend label print agent system for remote label rendering and job management
Build and Push Multi-Platform Images / build-and-push (push) Successful in 33s

This commit is contained in:
2026-05-07 22:46:29 +02:00
parent 0c94e7b999
commit 80f862a0c0
15 changed files with 995 additions and 15 deletions
+198 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
import dayjs from 'dayjs';
import {
Tabs, Typography, Table, Button, Modal, Form, Input, Select,
Switch, Checkbox, Popconfirm, message, Card, Tag, Space, Divider, InputNumber, Badge,
Switch, Checkbox, Popconfirm, message, Card, Tag, Space, Divider, InputNumber, Badge, Row, Col,
} from 'antd';
import {
UserOutlined, FileTextOutlined, ThunderboltOutlined,
@@ -24,6 +24,7 @@ import { apiKeysApi, type ApiKey } from '../api/api-keys';
import {
barcodeTemplatesApi,
type BarcodeTemplate,
type LabelInputField,
} from '../api/barcode-templates';
import {
paperlessApi, type PaperlessTag, type PaperlessDocType,
@@ -1779,6 +1780,64 @@ function InboxActionsForTemplateEditor({ templateId }: { templateId: number }) {
// Eingangsdokumentarten Tab (ehemals Barcode-Vorlagen)
// ═══════════════════════════════════════════════════════════════════
function LabelElementRow({ listName, remove }: { listName: number; remove: (n: number) => void }) {
return (
<Card
size="small"
style={{ marginBottom: 8 }}
extra={<Button size="small" danger icon={<DeleteOutlined />} onClick={() => remove(listName)} />}
>
<Form.Item name={[listName, 'type']} label="Typ" style={{ marginBottom: 8 }}>
<Select style={{ width: 120 }} options={[
{ value: 'text', label: 'Text' },
{ value: 'qr', label: 'QR-Code' },
{ value: 'line', label: 'Linie' },
]} />
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) => {
const type = getFieldValue(['LabelLayout', listName, 'type']);
if (type === 'text') return (
<Row gutter={8}>
<Col span={4}><Form.Item name={[listName, 'x']} label="X (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'y']} label="Y (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'fontSize']} label="Schrift (mm)"><InputNumber min={0.5} step={0.5} style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'maxWidth']} label="Max. B. (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'bold']} label="Fett" valuePropName="checked"><Checkbox /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'align']} label="Ausrichtung">
<Select allowClear style={{ width: '100%' }} options={[
{ value: 'left', label: 'Links' },
{ value: 'center', label: 'Mitte' },
{ value: 'right', label: 'Rechts' },
]} />
</Form.Item></Col>
<Col span={24}><Form.Item name={[listName, 'content']} label="Inhalt"><Input placeholder="{nummer} oder {datum}" /></Form.Item></Col>
</Row>
);
if (type === 'qr') return (
<Row gutter={8}>
<Col span={4}><Form.Item name={[listName, 'x']} label="X (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'y']} label="Y (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'sizeMm']} label="Größe (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={12}><Form.Item name={[listName, 'content']} label="Inhalt"><Input placeholder="{number}" /></Form.Item></Col>
</Row>
);
if (type === 'line') return (
<Row gutter={8}>
<Col span={4}><Form.Item name={[listName, 'x1']} label="X1 (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'y1']} label="Y1 (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'x2']} label="X2 (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'y2']} label="Y2 (mm)"><InputNumber style={{ width: '100%' }} /></Form.Item></Col>
<Col span={4}><Form.Item name={[listName, 'lineWidth']} label="Stärke (mm)"><InputNumber step={0.1} style={{ width: '100%' }} /></Form.Item></Col>
</Row>
);
return null;
}}
</Form.Item>
</Card>
);
}
function BarcodeTemplatesTab() {
const [data, setData] = useState<BarcodeTemplate[]>([]);
const [loading, setLoading] = useState(true);
@@ -1806,7 +1865,15 @@ function BarcodeTemplatesTab() {
setEditing(null);
setTestValue('');
form.resetFields();
form.setFieldsValue({ SplitBefore: false, DateinameTemplate: '' });
form.setFieldsValue({
SplitBefore: false,
DateinameTemplate: '',
LabelEnabled: false,
LabelWidthMm: 57,
LabelHeightMm: 32,
LabelInputFields: [],
LabelLayout: [],
});
setModalOpen(true);
};
@@ -1814,7 +1881,20 @@ function BarcodeTemplatesTab() {
setIsNew(false);
setEditing(row);
setTestValue('');
form.setFieldsValue({ Name: row.Name, Regex: row.Regex, SplitBefore: row.SplitBefore, DateinameTemplate: row.DateinameTemplate ?? '' });
form.setFieldsValue({
Name: row.Name,
Regex: row.Regex,
SplitBefore: row.SplitBefore,
DateinameTemplate: row.DateinameTemplate ?? '',
LabelEnabled: row.LabelEnabled ?? false,
LabelWidthMm: row.LabelWidthMm ?? 57,
LabelHeightMm: row.LabelHeightMm ?? 32,
LabelInputFields: row.LabelInputFields ?? [],
LabelGetUrl: row.LabelGetUrl ?? '',
LabelPrintedUrl: row.LabelPrintedUrl ?? '',
LabelReleaseUrl: row.LabelReleaseUrl ?? '',
LabelLayout: row.LabelLayout ?? [],
});
setModalOpen(true);
};
@@ -1960,6 +2040,121 @@ function BarcodeTemplatesTab() {
>
<Input placeholder="{barcode}_{datum}" />
</Form.Item>
<Divider>Etikett</Divider>
<Form.Item name="LabelEnabled" valuePropName="checked" label="Etikett-Druck aktivieren">
<Switch />
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.LabelEnabled !== curr.LabelEnabled}>
{({ getFieldValue }) =>
getFieldValue('LabelEnabled') ? (
<>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="LabelWidthMm" label="Breite (mm)">
<InputNumber min={10} max={300} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="LabelHeightMm" label="Höhe (mm)">
<InputNumber min={10} max={300} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item label="Eingabefelder">
<Form.List name="LabelInputFields">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...rest }) => (
<Space key={key} style={{ display: 'flex', marginBottom: 6 }} align="baseline">
<Form.Item {...rest} name={[name, 'name']} rules={[{ required: true, message: 'Feldname' }]} noStyle>
<Input placeholder="Feldname (z. B. datum)" style={{ width: 160 }} />
</Form.Item>
<Form.Item {...rest} name={[name, 'label']} noStyle>
<Input placeholder="Bezeichnung" style={{ width: 160 }} />
</Form.Item>
<Form.Item {...rest} name={[name, 'type']} noStyle>
<Select style={{ width: 110 }} options={[
{ value: 'text', label: 'Text' },
{ value: 'number', label: 'Nummer' },
{ value: 'date', label: 'Datum' },
]} />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
))}
<Button type="dashed" onClick={() => add({ type: 'text' })} icon={<PlusOutlined />} size="small">
Feld hinzufügen
</Button>
</>
)}
</Form.List>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue: gfv }) => {
const inputFields: LabelInputField[] = gfv('LabelInputFields') ?? [];
const chips: string[] = [];
for (const f of inputFields) {
if (!f?.name) continue;
chips.push(`{${f.name}}`);
if (f.type === 'date') {
chips.push(`{${f.name}.year}`, `{${f.name}.month}`, `{${f.name}.day}`);
}
}
const chipsWithNumber = [...chips, '{number}'];
return (
<>
<Form.Item name="LabelGetUrl" label="GET-URL (liefert Nummer)"
extra={chips.length > 0 ? `Platzhalter: ${chips.join(' ')}` : undefined}>
<Input placeholder="https://example.com/nummer?feld={feldname}" />
</Form.Item>
<Form.Item name="LabelPrintedUrl" label="PRINTED-URL (nach erfolgreichem Druck)"
extra={chipsWithNumber.length > 0 ? `Platzhalter: ${chipsWithNumber.join(' ')}` : undefined}>
<Input placeholder="https://example.com/gedruckt?nr={number}" />
</Form.Item>
<Form.Item name="LabelReleaseUrl" label="RELEASE-URL (bei Druckfehler)"
extra={chipsWithNumber.length > 0 ? `Platzhalter: ${chipsWithNumber.join(' ')}` : undefined}>
<Input placeholder="https://example.com/freigeben?nr={number}" />
</Form.Item>
<Form.Item label="Layout-Elemente"
extra={chipsWithNumber.length > 0 ? `Platzhalter für Inhalt: ${chipsWithNumber.join(' ')}` : undefined}>
<Form.List name="LabelLayout">
{(layoutFields, { add: addEl, remove: removeEl }) => (
<>
{layoutFields.map(({ key, name: elName }) => (
<LabelElementRow key={key} listName={elName} remove={removeEl} />
))}
<Space>
<Button size="small" type="dashed" icon={<PlusOutlined />}
onClick={() => addEl({ type: 'text', x: 0, y: 0, fontSize: 3, content: '', bold: false })}>
Text
</Button>
<Button size="small" type="dashed" icon={<PlusOutlined />}
onClick={() => addEl({ type: 'qr', x: 0, y: 0, sizeMm: 20, content: '' })}>
QR-Code
</Button>
<Button size="small" type="dashed" icon={<PlusOutlined />}
onClick={() => addEl({ type: 'line', x1: 0, y1: 0, x2: 50, y2: 0 })}>
Linie
</Button>
</Space>
</>
)}
</Form.List>
</Form.Item>
</>
);
}}
</Form.Item>
</>
) : null
}
</Form.Item>
</Form>
{editing && !isNew && (