Formate zentral aus Directus + Kuvert-/Agent-/Login-Fixes
Formate zentralisiert (Phase 1)
- Neue Collection render_formats (Seitengröße, Ränder, Zeilenhöhe/-limit +
Plotter-Template) als einzige Quelle. Backend liest sie gecacht (Start +
stündlich, synchroner Zugriff → keine Latenz) mit eingebauten Defaults als
Fallback; manueller Trigger via POST /api/formats/refresh.
- Ermöglicht frei definierbare Formate ohne Codeänderung.
Kuvert-Templates
- Werte 1:1 aus dem alten Job-Manager übernommen (falsche/vertauschte Maße
korrigiert, per Upsert) und je Format die zweite Variante ergänzt
(individuelle Beschriftung). Druck-Popup schlägt bei freiem Text Variante 2 vor.
Produktions-Agent
- Datei-Endpunkt liefert {files} ohne data-Hülle → eigener get_raw statt des
data-auspackenden get; Dokumentauswahl positionsbasiert (robust gegen 0-/1-
basierte Dateinamen).
Bootstrap
- Ungültiger DIRECTUS_TOKEN wird erkannt und fällt auf ADMIN_EMAIL/PASSWORD
zurück, statt mit einem 401 abzubrechen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,10 +56,27 @@ async function api(method, path, body) {
|
||||
return data?.data ?? data;
|
||||
}
|
||||
|
||||
/** Prüft, ob der gesetzte Token wirklich gültig ist. */
|
||||
async function tokenGueltig() {
|
||||
try {
|
||||
const res = await fetch(DIRECTUS_URL + '/users/me?fields=id', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return res.ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (token) return;
|
||||
// Ein mitgegebener DIRECTUS_TOKEN (z. B. versehentlich aus der .env
|
||||
// übernommen) wird geprüft – sonst schlägt erst der nächste Aufruf mit einem
|
||||
// wenig hilfreichen 401 fehl.
|
||||
if (token) {
|
||||
if (await tokenGueltig()) return;
|
||||
console.warn(' DIRECTUS_TOKEN ist ungültig – versuche Login mit ADMIN_EMAIL/ADMIN_PASSWORD.');
|
||||
token = '';
|
||||
}
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
console.error('Weder DIRECTUS_TOKEN noch ADMIN_EMAIL/ADMIN_PASSWORD gesetzt.');
|
||||
console.error('DIRECTUS_TOKEN ungültig bzw. nicht gesetzt und ADMIN_EMAIL/ADMIN_PASSWORD fehlen.');
|
||||
process.exit(1);
|
||||
}
|
||||
const res = await fetch(DIRECTUS_URL + '/auth/login', {
|
||||
@@ -349,6 +366,25 @@ const collections = [
|
||||
f('rotation', 'integer', { interface: 'input', default: 0 }),
|
||||
] },
|
||||
|
||||
// Zentrale Rendering-Formate: Seitengröße, Ränder, Zeilenhöhe/-limit. Das
|
||||
// Backend liest diese (gecacht) statt hartkodierter Werte – so lassen sich
|
||||
// eigene Formate anlegen, ohne Code zu ändern (eine Quelle für App + Backend).
|
||||
{ collection: 'render_formats', meta: { icon: 'crop_free', note: 'Rendering-Formate (Seitengröße, Ränder, Zeilenlimit) – zentral fürs Backend.' }, fields: [
|
||||
f('key', 'string', { interface: 'input', unique: true, required: true, note: 'Technischer Schlüssel, z. B. a4, a6p, din_lang, oder ein eigener.' }),
|
||||
f('name', 'string', { interface: 'input', required: true }),
|
||||
dropdown('kind', ['brief', 'umschlag'], { default: 'brief' }),
|
||||
f('page_width_mm', 'float', { interface: 'input', required: true, note: 'Seitenbreite in mm.' }),
|
||||
f('page_height_mm', 'float', { interface: 'input', required: true, note: 'Seitenhöhe in mm.' }),
|
||||
f('margin_top_mm', 'float', { interface: 'input', default: 25 }),
|
||||
f('margin_left_mm', 'float', { interface: 'input', default: 20 }),
|
||||
f('margin_right_mm', 'float', { interface: 'input', note: 'Optional (v. a. Querformat).' }),
|
||||
f('line_height_factor', 'float', { interface: 'input', default: 1.35, note: 'Zeilenhöhe = Schriftgröße × Faktor.' }),
|
||||
f('line_limit', 'integer', { interface: 'input', note: 'Max. Zeilen. Leer = automatisch aus Höhe.' }),
|
||||
// Zugehöriges Plotter-Druck-Template (Platzierung an der Maschine).
|
||||
m2o('print_template', 'format_templates'),
|
||||
f('active', 'boolean', { interface: 'boolean', default: true }),
|
||||
] },
|
||||
|
||||
{ collection: 'jobs', meta: { icon: 'print', note: 'Produktions-Job (Druck-Warteschlange je Maschine).' }, fields: [
|
||||
m2o('order', 'orders', { onDelete: 'CASCADE' }),
|
||||
f('output_ref', 'string', { interface: 'input', note: 'Ausgabe-Ordner ohne Auftrag (z. B. Testdruck).' }),
|
||||
@@ -664,15 +700,38 @@ async function applySeed() {
|
||||
];
|
||||
for (const t of templates) await seedItem('email_templates', 'key', { enabled: true, ...t });
|
||||
|
||||
// Druck-Templates (aus der bisherigen config.json der Produktions-App).
|
||||
// Druck-Templates – Werte 1:1 aus der config.json des alten Job-Managers.
|
||||
// Je Format gibt es zwei Varianten (v1/v2); die zweite ist die zusätzliche
|
||||
// Platzierung (u. a. für individuelle Kuvert-Beschriftung). Werte werden per
|
||||
// Upsert gesetzt, damit falsche Altwerte beim Bootstrap korrigiert werden.
|
||||
const formatTemplates = [
|
||||
{ key: 'a4_single', name: 'A4 Brief – Einzeln', type: 'brief', width: 8.27, height: 11.69, xpos: 0.0, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v1', name: 'A6 Brief – Hochformat', type: 'brief', width: 4.1, height: 5.8, xpos: -0.3, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v2', name: 'A6 Brief – Querformat', type: 'brief', width: 4.1, height: 5.8, xpos: 4.0, ypos: -0.3, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_a_v1', name: 'Kuvert DIN Lang', type: 'umschlag', width: 4.49, height: 6.38, xpos: 4.2, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_b_v1', name: 'Kuvert C6', type: 'umschlag', width: 3.9, height: 8.27, xpos: 4.49, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'a4_single', name: 'A4 Brief', type: 'brief', width: 8.27, height: 11.69, xpos: 0.0, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v1', name: 'A6 Hochformat – Variante 1', type: 'brief', width: 4.1, height: 5.8, xpos: -0.3, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6_v2', name: 'A6 Hochformat – Variante 2', type: 'brief', width: 4.1, height: 5.8, xpos: 4.0, ypos: -0.3, scale: 1.0, rotation: 90 },
|
||||
{ key: 'a6l_v1', name: 'A6 Querformat – Variante 1', type: 'brief', width: 5.8, height: 4.1, xpos: -0.3, ypos: -0.3, scale: 1.0, rotation: 0 },
|
||||
{ key: 'a6l_v2', name: 'A6 Querformat – Variante 2', type: 'brief', width: 5.8, height: 4.1, xpos: 4.0, ypos: -0.3, scale: 1.0, rotation: 90 },
|
||||
// DIN Lang – v1 = Empfängeradresse, v2 = individuelle Beschriftung.
|
||||
{ key: 'kuvert_a_v1', name: 'Kuvert DIN Lang – Variante 1', type: 'umschlag', width: 4.33, height: 8.27, xpos: 4.0, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_a_v2', name: 'Kuvert DIN Lang – Variante 2', type: 'umschlag', width: 3.9, height: 8.27, xpos: 4.49, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
// C6 – v1 = Empfängeradresse, v2 = individuelle Beschriftung.
|
||||
{ key: 'kuvert_b_v1', name: 'Kuvert C6 – Variante 1', type: 'umschlag', width: 4.49, height: 6.38, xpos: 4.2, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
{ key: 'kuvert_b_v2', name: 'Kuvert C6 – Variante 2', type: 'umschlag', width: 4.33, height: 6.38, xpos: 4.0, ypos: -0.2, scale: 1.0, rotation: 90 },
|
||||
];
|
||||
for (const t of formatTemplates) await seedItem('format_templates', 'key', t);
|
||||
for (const t of formatTemplates) {
|
||||
const vorhanden = await api('GET', `/items/format_templates?filter[key][_eq]=${encodeURIComponent(t.key)}&limit=1&fields=id`);
|
||||
if (vorhanden && vorhanden.length) await api('PATCH', `/items/format_templates/${vorhanden[0].id}`, t);
|
||||
else { await api('POST', '/items/format_templates', t); log('seed + format_templates.' + t.key); }
|
||||
}
|
||||
|
||||
// Rendering-Formate (zentral fürs Backend). Werte = bisherige Defaults.
|
||||
const renderFormats = [
|
||||
{ key: 'a4', name: 'A4', kind: 'brief', page_width_mm: 210, page_height_mm: 297, margin_top_mm: 25, margin_left_mm: 20, line_height_factor: 1.35, line_limit: 27, active: true },
|
||||
{ key: 'a6p', name: 'A6 Hochformat', kind: 'brief', page_width_mm: 105, page_height_mm: 148, margin_top_mm: 8, margin_left_mm: 8, line_height_factor: 1.3, line_limit: 14, active: true },
|
||||
{ key: 'a6l', name: 'A6 Querformat', kind: 'brief', page_width_mm: 148, page_height_mm: 105, margin_top_mm: 8, margin_left_mm: 8, margin_right_mm: 14, line_height_factor: 1.2, line_limit: 11, active: true },
|
||||
{ key: 'din_lang', name: 'DIN Lang Kuvert', kind: 'umschlag', page_width_mm: 220, page_height_mm: 110, margin_top_mm: 50, margin_left_mm: 10, line_height_factor: 1.2, active: true },
|
||||
{ key: 'c6', name: 'C6 Kuvert', kind: 'umschlag', page_width_mm: 162, page_height_mm: 114, margin_top_mm: 60, margin_left_mm: 10, line_height_factor: 1.2, active: true },
|
||||
];
|
||||
for (const rf of renderFormats) await seedItem('render_formats', 'key', rf);
|
||||
|
||||
// email_settings (Singleton) – Signatur nur setzen, wenn leer.
|
||||
const es = await api('GET', '/items/email_settings');
|
||||
@@ -702,7 +761,7 @@ async function applySeed() {
|
||||
const BIZ = ['products', 'formats', 'motifs', 'price_items', 'pricing_settings',
|
||||
'vouchers', 'voucher_redemptions', 'orders', 'order_entries', 'order_addons', 'jobs', 'status_history',
|
||||
'offer_requests', 'signatures', 'customer_discounts', 'customer_products',
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'test_prints'];
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'render_formats', 'test_prints'];
|
||||
// Kunde = Login-Konto: Aufträge hängen direkt am Nutzer.
|
||||
const OWN_ORDER = { customer: { _eq: '$CURRENT_USER' } };
|
||||
const OWN_VIA_ORDER = { order: { customer: { _eq: '$CURRENT_USER' } } };
|
||||
@@ -817,7 +876,7 @@ async function applyPermissions() {
|
||||
const svcPol = await ensurePolicy('Service', { app: false });
|
||||
await ensureAccess(svcRole.id, svcPol.id);
|
||||
for (const col of ['orders', 'order_entries', 'order_addons', 'jobs', 'products', 'formats',
|
||||
'motifs', 'price_items', 'machines', 'format_templates'])
|
||||
'motifs', 'price_items', 'machines', 'format_templates', 'render_formats'])
|
||||
await ensurePermission(svcPol.id, col, 'read');
|
||||
await ensurePermission(svcPol.id, 'orders', 'update',
|
||||
['status', 'artifact_path', 'payment_status', 'payment_ref', 'production_status', 'brief_gedruckt', 'kuvert_gedruckt']);
|
||||
@@ -888,7 +947,7 @@ async function organizeNav() {
|
||||
await patch(col, { group: null, hidden: false });
|
||||
// Unter „Einstellungen"
|
||||
for (const col of ['products', 'formats', 'motifs', 'price_items', 'pricing_settings', 'offer_requests',
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'test_prints'])
|
||||
'email_templates', 'email_settings', 'muster_settings', 'machines', 'format_templates', 'render_formats', 'test_prints'])
|
||||
await patch(col, { group: FOLDER, hidden: false });
|
||||
// Reine Detail-/Technik-Collections aus der Navigation nehmen (Signaturen und
|
||||
// Rabatte werden direkt am Kundenkonto gepflegt, nicht als eigener Menüpunkt).
|
||||
|
||||
@@ -630,7 +630,7 @@ const handler = (router, { services, getSchema, logger }) => {
|
||||
// ── Druckauftrag einreihen (manueller Trigger aus dem Admin) ───────────────
|
||||
// Legt pro Typ (Schriftstück / Umschlag) einen Job mit aufgelöstem Template
|
||||
// für die gewählte Maschine an. Kein Statuswechsel am Auftrag (bewusst).
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6_v2' };
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6l_v1' };
|
||||
const ENVELOPE_TPL = { dinlang: 'kuvert_a_v1', c6: 'kuvert_b_v1' };
|
||||
|
||||
router.post('/enqueue', async (req, res) => {
|
||||
|
||||
@@ -23,7 +23,9 @@ import { defineComponent, ref, reactive, computed, onMounted, h, Teleport, resol
|
||||
import { useApi } from '@directus/extensions-sdk';
|
||||
|
||||
// Auftragsformat → Standard-Template-Schlüssel (wie im Backend /enqueue).
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6_v2' };
|
||||
const LETTER_TPL = { a4: 'a4_single', a6_hoch: 'a6_v1', a6_quer: 'a6l_v1' };
|
||||
// v1 = Empfängeradresse (Standardvorschlag). Für individuelle Beschriftung
|
||||
// wählt der Operator im Popup die Variante 2 (kuvert_a_v2 / kuvert_b_v2).
|
||||
const ENVELOPE_TPL = { dinlang: 'kuvert_a_v1', c6: 'kuvert_b_v1' };
|
||||
const INLAND = /^(de|deutschland|germany)$/i;
|
||||
|
||||
@@ -256,7 +258,10 @@ const Board = defineComponent({
|
||||
function openDruck(order) {
|
||||
const bt = tplByKey(LETTER_TPL[order.format?.key] || 'a4_single', 'brief');
|
||||
const wantsEnv = order.needs_envelope && order.envelope_labeling && order.envelope_labeling !== 'keine';
|
||||
const et = wantsEnv ? tplByKey(ENVELOPE_TPL[order.envelope_format] || 'kuvert_a_v1', 'umschlag') : null;
|
||||
// Bei individuellem Text die Variante 2 des Kuverts vorschlagen (v1 = Empfängeradresse).
|
||||
let envKey = ENVELOPE_TPL[order.envelope_format] || 'kuvert_a_v1';
|
||||
if (wantsEnv && order.envelope_labeling === 'freitext') envKey = envKey.replace('_v1', '_v2');
|
||||
const et = wantsEnv ? (tplByKey(envKey, 'umschlag') || tplByKey(ENVELOPE_TPL[order.envelope_format], 'umschlag')) : null;
|
||||
const n = Math.max(1, order.entries_count || 1);
|
||||
druck.order = order;
|
||||
druck.machine = aktiveMaschinen.value[0]?.id ?? null;
|
||||
|
||||
Reference in New Issue
Block a user