Format-Templates nicht mehr re-seeden, Signatur-Kuverts entfernt, Weiteres-Menü

- bootstrap: format_templates nur bei LEERER Collection anlegen (Betreiber
  pflegt sie selbst; kein Wieder-Anlegen bei jedem Bootstrap).
- Unterschriftenservice: Inline-Kuverts ("Kuverts mitdrucken") + Verknüpfen im
  Signatur-Tab wieder entfernt; signature-order auf Seiten-only zurückgesetzt.
- Produktion: Verknüpfen, In Kontakte speichern, Neu generieren und In manuellen
  Druck übernehmen hinter "Weiteres"-Button gebündelt; Notiz bleibt vorne.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-07 08:44:21 +02:00
parent ae7ef2422e
commit 08fe21a158
4 changed files with 50 additions and 121 deletions

View File

@@ -915,10 +915,16 @@ async function applySeed() {
{ 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) {
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); }
// Nur bei LEERER Collection anlegen (Erstinstallation). Der Betreiber pflegt die
// Templates danach selbst (löschen/umbenennen/eigene) – Bootstrap fügt sie NICHT
// wieder hinzu. Vorhandene werden bewusst nicht überschrieben.
const ftVorhanden = await api('GET', `/items/format_templates?limit=1&fields=id`);
if (!ftVorhanden || !ftVorhanden.length) {
for (const t of formatTemplates) {
await api('POST', '/items/format_templates', t); log('seed + format_templates.' + t.key);
}
} else {
log('format_templates vorhanden → Seed übersprungen (vom Betreiber gepflegt)');
}
// Rendering-Formate (zentral fürs Backend). Werte = bisherige Defaults.

View File

@@ -59,11 +59,6 @@ const NeuAuftrag = defineComponent({
modus: 'auftrag', // 'auftrag' (aus Auftrag laden) oder 'manuell' (direkt hochladen)
orderNr: '', order: null, docs: [], docId: null, docName: '', signaturen: [],
customerEmail: '', laden: false, pdf: '', msg: '', err: '',
// Kuverts zum Unterschriftenservice (wie manueller Druck): je Empfänger ein Umschlag.
kuverts: false, kuvertFormat: 'dinlang', empfBlocks: '',
absenderAuf: false, absender: '', absenderScale: 100,
// Verknüpfung mit vorhandenem Auftrag (dessen Kuverts genutzt werden).
linkNr: '',
});
// Datei nach Directus hochladen (liefert die Datei-ID).
async function sigUpload(file) {
@@ -768,20 +763,10 @@ const NeuAuftrag = defineComponent({
async function sigAuftrag() {
if (!sig.docId) { sig.err = 'Bitte ein Dokument wählen/hochladen.'; return; }
if (!sigAktive().length) { sig.err = 'Mindestens eine Unterschrift aktivieren.'; return; }
if (sig.kuverts && !sig.empfBlocks.trim()) { sig.err = 'Kuverts gewählt, aber keine Empfänger angegeben.'; return; }
sig.err = ''; sig.msg = 'Auftrag wird angelegt …';
try {
const res = await api.post('/skrift-orders/signature-order', {
...sigPayload(), customer_email: sig.customerEmail || undefined,
kuverts: sig.kuverts,
recipients_text: sig.kuverts ? sig.empfBlocks : undefined,
envelope_format: sig.kuverts ? sig.kuvertFormat : undefined,
envelope_sender: sig.kuverts && sig.absenderAuf ? sig.absender.trim() : undefined,
envelope_sender_scale: (Number(sig.absenderScale) || 100) / 100,
link_order_number: sig.linkNr.trim() || undefined,
});
const kt = res.data.kuverts ? `, ${res.data.kuverts} Kuvert/s` : '';
sig.msg = `✔ Auftrag ${res.data.order_number} angelegt (${res.data.pages} Seite/n${kt}). Im Produktions-Board unter „Neu" → Drucken.`;
const res = await api.post('/skrift-orders/signature-order', { ...sigPayload(), customer_email: sig.customerEmail || undefined });
sig.msg = `✔ Auftrag ${res.data.order_number} angelegt (${res.data.pages} Seite/n). Im Produktions-Board unter „Neu" → Drucken.`;
} catch (e) { sig.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Auftrag konnte nicht angelegt werden.'; sig.msg = ''; }
}
@@ -856,44 +841,11 @@ const NeuAuftrag = defineComponent({
btn('⤓ A4-Signaturen (ZIP)', sigDownload),
]),
]);
// ── Kuverts & Verknüpfung ──────────────────────────────────────────────
const b3 = h('div', { style: BEREICH }, [
bereichKopf('✉ Kuverts & Verknüpfung'),
subdued('Optional: adressierte Kuverts zum Unterschriftenservice erzeugen (wie beim manuellen Druck) – oder den Auftrag mit einem vorhandenen Auftrag verknüpfen, dessen Kuverts genutzt werden.', 'margin-bottom:10px;'),
h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-bottom:8px;' }, [
h('input', { type: 'checkbox', checked: sig.kuverts, onChange: (e) => { sig.kuverts = e.target.checked; }, style: 'width:16px;height:16px;' }),
'Kuverts mitdrucken (je Empfänger ein Umschlag)',
]),
sig.kuverts ? h('div', { style: 'padding-left:24px;display:flex;flex-direction:column;gap:12px;margin-bottom:12px;' }, [
h('div', { style: 'max-width:240px;' }, [label('Kuvertformat'), sel(sig.kuvertFormat, [{ v: 'dinlang', t: 'DIN Lang' }, { v: 'c6', t: 'C6' }], (e) => { sig.kuvertFormat = e.target.value; })]),
h('div', {}, [
label('Empfänger (ein Block je Empfänger, durch Leerzeile getrennt)'),
h('textarea', { value: sig.empfBlocks, rows: 6, placeholder: 'Max Mustermann\nMusterstr. 1\n12345 Musterstadt\n\nErika Beispiel\nBeispielweg 2\n54321 Beispielort',
style: WFELD + 'width:100%;font-family:var(--family-monospace,monospace);', onInput: (e) => { sig.empfBlocks = e.target.value; } }),
]),
h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;' }, [
h('input', { type: 'checkbox', checked: sig.absenderAuf, onChange: (e) => { sig.absenderAuf = e.target.checked; }, style: 'width:16px;height:16px;' }),
'Absender aufs Kuvert',
]),
sig.absenderAuf ? h('div', { style: 'display:flex;gap:12px;flex-wrap:wrap;' }, [
h('div', { style: 'flex:1;min-width:200px;' }, [label('Absender (max. 5 Zeilen)'),
h('textarea', { value: sig.absender, rows: 5, style: WFELD + 'width:100%;',
onInput: (e) => { sig.absender = e.target.value.split('\n').slice(0, 5).join('\n'); } })]),
h('div', { style: 'width:120px;' }, [label('Größe (%)'),
h('input', { type: 'number', step: 5, min: 50, max: 200, value: sig.absenderScale, style: WFELD + 'width:100%;', onInput: (e) => { sig.absenderScale = e.target.value; } })]),
]) : null,
]) : null,
h('div', { style: 'max-width:360px;' }, [
label('Mit vorhandenem Auftrag verknüpfen (Nr., optional)'),
h('input', { value: sig.linkNr, placeholder: 'z. B. 07-09-26-003', style: WFELD + 'width:100%;', onInput: (e) => { sig.linkNr = e.target.value; } }),
subdued('Dessen Kuverts werden für diesen Auftrag genutzt. Änderbar im Produktions-Board.', 'margin-top:4px;'),
]),
]);
const meldung = h('div', {}, [
sig.err ? h('div', { style: 'color:var(--danger,#c62828);font-size:13px;margin-top:8px;' }, sig.err) : null,
sig.msg ? subdued(sig.msg, 'margin-top:8px;') : null,
]);
const links = h('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:16px;' }, [b1, b2, b3, meldung]);
const links = h('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:16px;' }, [b1, b2, meldung]);
const rechts = h('div', { style: BEREICH + 'flex:0 0 420px;position:sticky;top:20px;align-self:flex-start;min-height:200px;' }, [
h('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:8px;' }, [
bereichKopf('👁 Vorschau'),

View File

@@ -310,20 +310,6 @@ const handler = (router, { services, getSchema, logger }) => {
const pages = (await r.json())?.pages || [];
if (!pages.length) return res.status(422).json({ error: 'Keine Seiten erzeugt.' });
// Optionale Kuverts (wie manueller Druck): je Empfänger ein Umschlag. Empfänger
// als Blöcke (durch Leerzeile getrennt) im Freitext. Absender optional.
const kuvertsAn = !!body.kuverts;
const empf = kuvertsAn
? String(body.recipients_text || '')
.split(/\n\s*\n/).map((s) => s.trim()).filter(Boolean)
.map((free_text) => ({ free_text }))
: [];
if (kuvertsAn && !empf.length) return res.status(400).json({ error: 'Kuverts gewählt, aber keine Empfänger angegeben.' });
const envFormat = ['c6', 'dinlang'].includes(body.envelope_format) ? body.envelope_format : 'dinlang';
const absender = String(body.envelope_sender || '').trim();
const absenderScale = Math.max(0.5, Math.min(2, Number(body.envelope_sender_scale) || 1));
const countAusland = empf.filter((e) => !zielInland(e)).length;
const jt = new Date();
const praefix = `${String(jt.getDate()).padStart(2, '0')}-${String(jt.getMonth() + 1).padStart(2, '0')}-${String(jt.getFullYear()).slice(2)}`;
const naechsteNummer = async () => {
@@ -334,14 +320,9 @@ const handler = (router, { services, getSchema, logger }) => {
customer: customerId, customer_email: customerEmail, product: product.id,
person_type: 'privat', source: 'operator', status: 'in_queue', production_status: 'eingegangen',
payment_status: 'bezahlt', payment_method: 'rechnung', shipping_type: 'sammel',
needs_envelope: kuvertsAn, envelope_labeling: kuvertsAn ? 'empfaenger' : 'keine',
envelope_format: kuvertsAn ? envFormat : null,
envelope_sender: kuvertsAn && absender ? absender : null,
envelope_sender_scale: absenderScale,
needs_envelope: false, envelope_labeling: 'keine',
source_file: pdfId, source_files: [pdfId],
entries_count: kuvertsAn ? empf.length : pages.length,
count_inland: kuvertsAn ? (empf.length - countAusland) : pages.length,
count_ausland: kuvertsAn ? countAusland : 0,
entries_count: pages.length, count_inland: pages.length, count_ausland: 0,
};
let orderId, orderNumber;
for (let v = 0; v < 6; v += 1) {
@@ -351,28 +332,11 @@ const handler = (router, { services, getSchema, logger }) => {
}
const entriesSvc = svc('order_entries');
if (kuvertsAn) {
// Empfänger als Zeilen – daraus erzeugt die Pipeline je einen Umschlag.
for (let i = 0; i < empf.length; i += 1) {
await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: empf[i].free_text });
}
} else {
for (let i = 0; i < pages.length; i += 1) {
await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` });
}
for (let i = 0; i < pages.length; i += 1) {
await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` });
}
await svc('status_history').createOne({ order: orderId, status: 'eingegangen', note: 'Unterschriftenservice – manuell angelegt.' }).catch(() => {});
// Verknüpfung mit vorhandenem Auftrag (dessen Kuverts genutzt werden) – optional.
if (body.link_order_number) {
try {
const linked = (await svc('orders').readByQuery({
filter: { order_number: { _eq: String(body.link_order_number).trim() } }, limit: 1, fields: ['id'],
}))?.[0];
if (linked) await svc('orders').updateOne(orderId, { linked_order: linked.id });
} catch (e) { logger.warn(`[skrift-orders] signature-order Verknüpfung: ${e.message}`); }
}
// Seiten-SVGs als Schriftstück in den Auftragsordner schreiben.
const ordner = path.join(OUTPUT_DIR, orderNumber);
fs.mkdirSync(ordner, { recursive: true });
@@ -380,16 +344,7 @@ const handler = (router, { services, getSchema, logger }) => {
fs.writeFileSync(path.join(ordner, `letter_${String(i + 1).padStart(3, '0')}.svg`), p.svg, 'utf8');
});
// Kuverts über die Standard-Pipeline erzeugen (Datei-Produkt → NUR Umschläge;
// die Seiten-SVGs bleiben erhalten, da runGeneration den Ordner nicht leert).
if (kuvertsAn) {
fetch(`${BACKEND_URL}/api/order/from-directus`, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Token': BACKEND_TOKEN },
body: JSON.stringify({ orderId }),
}).catch((e) => logger.warn(`[skrift-orders] signature-order Kuverts nicht gestartet: ${e.message}`));
}
return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length, kuverts: empf.length });
return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length });
} catch (err) {
logger.error(`[skrift-orders] signature-order: ${err.stack || err.message}`);
return res.status(500).json({ error: 'Auftrag konnte nicht angelegt werden.', detail: String(err.message || err).slice(0, 400) });

View File

@@ -272,6 +272,9 @@ const Board = defineComponent({
// Notizen je Charge (charge_key → Notiz) + Speichern von Auftrags-/Charge-Notizen.
const chargeNotizen = ref({});
// Ausgeklappter „Weiteres"-Bereich je Auftrag (Verknüpfen/Kontakte/Neu gen./Übernehmen).
const weiteresOffen = ref({});
const toggleWeiteres = (id) => { weiteresOffen.value = { ...weiteresOffen.value, [id]: !weiteresOffen.value[id] }; };
async function ladeChargeNotizen() {
try {
const r = await api.get('/items/charge_notes', { params: { limit: -1, fields: 'charge_key,note' } });
@@ -804,8 +807,8 @@ const Board = defineComponent({
// Auftrag in den manuellen Druck (neuauftrag-Modul) übernehmen: Text, Schrift,
// Format, Kuvert-Option und ALLE Empfänger (free_text + Platzhalter) per
// localStorage übergeben und dorthin navigieren – dort editierbar/neu druckbar.
async function uebernehmenManuell() {
const o = empf.orderObj;
async function uebernehmenManuell(order) {
const o = order || empf.orderObj;
if (!o) return;
try {
const full = (await api.get('/items/orders/' + o.id, { params: {
@@ -914,15 +917,32 @@ const Board = defineComponent({
// ── Render-Bausteine ──────────────────────────────────────────────────
function actionButtons(order) {
// Drucken + Empfänger (Empfänger enthält die Vorschau je Dokument).
// Drucken + Empfänger immer sichtbar; Rest hinter „Weiteres".
const offen = !!weiteresOffen.value[order.id];
return h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [
btn('🖨 Drucken', () => openDruck(order), 'primary'),
btn('👥 Empfänger', () => openEmpf(order)),
orderDateiIds(order).length ? btn('📎 Dateien', () => openDateien(order)) : null,
btn(order.keep_contacts ? '📇 In Kontakte gespeichert ✓' : '📇 In Kontakte speichern',
() => toggleKeepContacts(order), order.keep_contacts ? 'primary' : 'default'),
btn(offen ? '⚙ Weiteres ▴' : '⚙ Weiteres ▾', () => toggleWeiteres(order.id)),
].filter(Boolean));
}
// „Weiteres": Verknüpfen · In Kontakte speichern · Neu generieren · In manuellen Druck.
function weiteresPanel(o) {
return h('div', { style: 'margin-top:10px;padding:12px;border:1px solid var(--border-subdued);border-radius:8px;background:var(--background-subdued);display:flex;flex-direction:column;gap:10px;' }, [
h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;' }, [
subdued('🔗 Verknüpfter Auftrag:', ''),
h('input', { placeholder: 'Auftragsnr. (leer = keine)', value: o.linked_order?.order_number || '',
style: 'flex:1;min-width:140px;max-width:220px;border:1px solid var(--border-subdued);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);',
onChange: (e) => setOrderLink(o, e.target.value) }),
]),
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [
btn(o.keep_contacts ? '📇 In Kontakte gespeichert ✓' : '📇 In Kontakte speichern',
() => toggleKeepContacts(o), o.keep_contacts ? 'primary' : 'default'),
btn('🔄 Neu generieren', () => regenerieren(o.id), 'danger'),
btn('✍ In manuellen Druck übernehmen', () => uebernehmenManuell(o), 'primary'),
]),
]);
}
function statusSelect(order) {
return h('select', {
style: S_SELECT + 'font-size:12px;padding:4px 8px;',
@@ -958,15 +978,6 @@ const Board = defineComponent({
o.payment_status === 'bezahlt' ? 'var(--success-25,rgba(46,125,50,.15))' : 'var(--warning-25,rgba(230,160,0,.15))'),
o.linked_order?.order_number ? chip('🔗 Kuverts: #' + o.linked_order.order_number, 'var(--primary-25,rgba(30,80,165,.14))') : null,
]);
// Verknüpfter Auftrag (Kuverts zum Unterschriftenservice) – setzen/ändern/entfernen.
const linkFeld = h('div', { style: 'display:flex;align-items:center;gap:8px;margin:2px 0 6px;flex-wrap:wrap;' }, [
subdued('🔗 Verknüpfter Auftrag:', ''),
h('input', {
placeholder: 'Auftragsnr. (leer = keine)', value: o.linked_order?.order_number || '',
style: 'flex:1;min-width:140px;max-width:220px;border:1px solid var(--border-subdued);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);',
onChange: (e) => setOrderLink(o, e.target.value),
}),
]);
// „Auf einen Blick": welche Nummern wurden schon in den Druck gesendet
// (aus den Jobs, inkl. bereits gedruckter). Kein Status, nur die Eingabe.
const send = sendIndex.value[String(o.id)];
@@ -975,7 +986,12 @@ const Board = defineComponent({
send.brief.length ? chip('🖨 Brief gesendet: ' + send.brief.join(' · '), 'var(--primary-25,rgba(30,80,165,.14))') : null,
send.umschlag.length ? chip('✉ Kuvert gesendet: ' + send.umschlag.join(' · '), 'var(--primary-25,rgba(30,80,165,.14))') : null,
]) : null;
return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, meta, badges, linkFeld, notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'), actionButtons(o)]);
return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [
kopf, meta, badges,
notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'),
actionButtons(o),
weiteresOffen.value[o.id] ? weiteresPanel(o) : null,
]);
}
function requestCard(req) {
const istMuster = req.type === 'muster';
@@ -1330,7 +1346,7 @@ const Board = defineComponent({
const uebernahme = istOrder ? h('div', { style: 'padding:12px 20px;border-bottom:1px solid var(--border-subdued);display:flex;justify-content:flex-end;align-items:center;gap:10px;flex-wrap:wrap;' }, [
subdued('Neu generieren erzeugt Schriftstück + Kuvert frisch.'),
btn('🔄 Neu generieren', () => regenerieren(empf.orderObj?.id), 'danger'),
btn('✍ In manuellen Druck übernehmen', uebernehmenManuell, 'primary'),
btn('✍ In manuellen Druck übernehmen', () => uebernehmenManuell(), 'primary'),
]) : null;
const body = h('div', { style: 'padding:16px 20px;display:flex;gap:20px;flex-wrap:wrap;' }, [liste, vorschau]);
return overlay([modalKopf(empf.titel, () => { empf.open = false; }), uebernahme, body].filter(Boolean), () => { empf.open = false; }, istOrder ? '980px' : '620px');