From 6ccd4f3c53136075a50bc92bf09ebb4f775e5e58 Mon Sep 17 00:00:00 2001 From: s4luorth Date: Fri, 11 Sep 2026 13:58:28 +0200 Subject: [PATCH] Postkarten-Format (nur manueller Druck): Text links, Adresse rechts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bootstrap: render_formats a6_postkarte + dinlang_postkarte (kind 'postkarte'), passende formats-Einträge (active=false → nicht in der App, nur manueller Druck), kind-Dropdown um 'postkarte' erweitert. - skrift-orders: Vorschau erkennt Postkarten (render_formats.kind) und baut EIN Dokument mit Text + Absender + Empfänger (Marker [[pk_abs]]/[[pk_adr]]). - neuauftrag: Postkarten-Formate erkannt; Absenderfeld direkt (unabhängig von Kuverts), Kuvert-Optionen ausgeblendet, Hinweis aufs Kartenlayout. Co-Authored-By: Claude Opus 4.8 --- bootstrap/bootstrap.mjs | 9 +++- .../dist/index.js | 45 +++++++++++++------ .../dist/index.js | 20 +++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/bootstrap/bootstrap.mjs b/bootstrap/bootstrap.mjs index 30c9e18..5dba4dd 100644 --- a/bootstrap/bootstrap.mjs +++ b/bootstrap/bootstrap.mjs @@ -415,7 +415,7 @@ const collections = [ { 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' }), + dropdown('kind', ['brief', 'umschlag', 'postkarte'], { 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 }), @@ -783,6 +783,9 @@ async function applySeed() { { product: productByKey.briefe.id, key: 'a4', name: 'A4', active: true }, { product: productByKey.postkarten.id, key: 'a6_hoch', name: 'A6 Hochformat', active: true }, { product: productByKey.postkarten.id, key: 'a6_quer', name: 'A6 Querformat', active: true }, + // Postkarten-Layout (Text links, Adresse rechts) – NUR manueller Druck, nicht in der App. + { product: productByKey.postkarten.id, key: 'a6_postkarte', name: 'A6 Postkarte (Text + Adresse)', active: false, show_manual: true }, + { product: productByKey.postkarten.id, key: 'dinlang_postkarte', name: 'DIN lang Postkarte (Text + Adresse)', active: false, show_manual: true }, ]; for (const fmt of formats) await seedItem('formats', 'key', fmt); @@ -934,6 +937,10 @@ async function applySeed() { { 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 }, + // Postkarten (nur manueller Druck): Text links, Absender+Empfänger rechts, + // Briefmarkenfeld oben rechts frei. Ein SVG je Karte (ein Plotter-Durchlauf). + { key: 'a6_postkarte', name: 'A6 Postkarte (Text + Adresse)', kind: 'postkarte', page_width_mm: 148, page_height_mm: 105, margin_top_mm: 8, margin_left_mm: 8, line_height_factor: 1.2, active: true }, + { key: 'dinlang_postkarte', name: 'DIN lang Postkarte (Text + Adresse)', kind: 'postkarte', page_width_mm: 210, page_height_mm: 105, margin_top_mm: 8, margin_left_mm: 8, line_height_factor: 1.2, active: true }, ]; // Upsert (nicht nur anlegen), damit falsche Alt-Maße korrigiert werden – z. B. // Postkarten (a6p/a6l), die sonst nicht aufs Kartenformat passen. diff --git a/extensions/directus-extension-skrift-neuauftrag/dist/index.js b/extensions/directus-extension-skrift-neuauftrag/dist/index.js index cd8088a..0b8ab82 100644 --- a/extensions/directus-extension-skrift-neuauftrag/dist/index.js +++ b/extensions/directus-extension-skrift-neuauftrag/dist/index.js @@ -44,6 +44,8 @@ const NeuAuftrag = defineComponent({ const api = useApi(); const produkte = ref([]); const formate = ref([]); + // Format-Schlüssel, die Postkarten sind (render_formats.kind === 'postkarte'). + const pkFormate = ref(new Set()); const verbotenRe = ref(null); const musterTexte = ref({ text_du: '', text_sie: '' }); // Frei anlegbare Mustertexte (Collection muster_texts). Fallback: Du/Sie aus muster_settings. @@ -82,25 +84,38 @@ const NeuAuftrag = defineComponent({ importOpen: false, importText: '', docs: [], previewSession: null, pLaedt: false, msg: '', err: '', hinweis: '', lb: '', lbDoc: null, }); + // Ist das gewählte Format eine Postkarte (Text links, Adresse rechts, ein SVG)? + const istPostkarte = () => pkFormate.value.has(neu.format); // Kuverts nur generieren, wenn angehakt (Beschriftung aus der Empfängeradresse). - const kuvertPayload = () => (neu.kuverts - ? { needs_envelope: true, envelope_labeling: 'empfaenger', envelope_text: '', - envelope_sender: neu.absenderAufKuvert ? (neu.envelopeSender || '').trim() : '', - envelope_sender_scale: (Number(neu.envelopeSenderScale) || 100) / 100 } - : { needs_envelope: false, envelope_labeling: 'keine', envelope_text: '', envelope_sender: '', envelope_sender_scale: 1 }); + // Postkarte: KEIN separater Umschlag – der Absender kommt über envelope_sender + // aufs Kartenlayout (rechte Hälfte). + const kuvertPayload = () => { + if (istPostkarte()) { + return { needs_envelope: false, envelope_labeling: 'keine', envelope_text: '', + envelope_sender: (neu.envelopeSender || '').trim(), + envelope_sender_scale: (Number(neu.envelopeSenderScale) || 100) / 100 }; + } + return neu.kuverts + ? { needs_envelope: true, envelope_labeling: 'empfaenger', envelope_text: '', + envelope_sender: neu.absenderAufKuvert ? (neu.envelopeSender || '').trim() : '', + envelope_sender_scale: (Number(neu.envelopeSenderScale) || 100) / 100 } + : { needs_envelope: false, envelope_labeling: 'keine', envelope_text: '', envelope_sender: '', envelope_sender_scale: 1 }; + }; async function laden() { try { - const [p, f, ps, mt, mtx, sg] = await Promise.all([ + const [p, f, ps, mt, mtx, sg, rf] = await Promise.all([ api.get('/items/products', { params: { limit: -1, filter: { active: { _eq: true }, type: { _in: ['letter', 'postcard'] } }, fields: 'key,name,type' } }), api.get('/items/formats', { params: { limit: -1, fields: '*,product.key' } }), api.get('/items/pricing_settings', { params: { fields: 'char_whitelist' } }).catch(() => ({ data: { data: {} } })), api.get('/items/muster_settings', { params: { fields: 'text_du,text_sie' } }).catch(() => ({ data: { data: {} } })), api.get('/items/muster_texts', { params: { limit: -1, filter: { active: { _eq: true } }, sort: 'sort', fields: 'id,label,text' } }).catch(() => ({ data: { data: [] } })), api.get('/items/signatures', { params: { limit: -1, sort: '-date_created', fields: 'id,label,file,marker_key,offset_x_mm,offset_y_mm,width_mm,customer.first_name,customer.last_name' } }).catch(() => ({ data: { data: [] } })), + api.get('/items/render_formats', { params: { limit: -1, filter: { kind: { _eq: 'postkarte' } }, fields: 'key' } }).catch(() => ({ data: { data: [] } })), ]); produkte.value = p.data?.data || []; formate.value = f.data?.data || []; + pkFormate.value = new Set((rf.data?.data || []).map((r) => r.key)); musterTexte.value = mt.data?.data || { text_du: '', text_sie: '' }; musterListe.value = mtx.data?.data || []; signaturListe.value = sg.data?.data || []; @@ -286,7 +301,8 @@ const NeuAuftrag = defineComponent({ async function preview() { // Vorschau ohne Empfänger erlaubt – das Backend nutzt dann eine Blankozeile. // Ohne Text nur erlaubt, wenn Kuverts generiert werden (reiner Kuvert-Druck). - if (!neu.text.trim() && !neu.kuverts) { neu.err = 'Bitte Text eingeben – oder „Kuverts generieren" aktivieren.'; return; } + // Postkarte braucht immer Text (linke Kartenhälfte). + if (!neu.text.trim() && (istPostkarte() || !neu.kuverts)) { neu.err = 'Bitte Text eingeben' + (istPostkarte() ? '.' : ' – oder „Kuverts generieren" aktivieren.'); return; } neu.pLaedt = true; neu.err = ''; neu.msg = ''; try { const kv = kuvertPayload(); @@ -533,16 +549,19 @@ const NeuAuftrag = defineComponent({ btn('+ Empfänger', addZeile), btn(neu.importOpen ? 'Import schließen' : '⇪ Aus Excel einfügen', () => { neu.importOpen = !neu.importOpen; }), ]), - h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-top:12px;' }, [ + // Postkarte: Hinweis auf das Kartenlayout; kein separates Kuvert. + istPostkarte() ? h('div', { style: 'font-size:12px;color:var(--foreground-subdued,#888);margin-top:12px;line-height:1.5;' }, + 'Postkarte: Text auf der linken Hälfte, Absender + Empfänger rechts, Briefmarkenfeld oben rechts frei. Ein separates Kuvert wird nicht erzeugt.') : null, + !istPostkarte() ? h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-top:12px;' }, [ h('input', { type: 'checkbox', checked: neu.kuverts, onChange: (e) => { neu.kuverts = e.target.checked; neu.previewSession = null; }, style: 'width:16px;height:16px;' }), 'Kuverts generieren', - ]), - neu.kuverts ? h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-top:8px;' }, [ + ]) : null, + (!istPostkarte() && neu.kuverts) ? h('label', { style: 'display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;margin-top:8px;' }, [ h('input', { type: 'checkbox', checked: neu.absenderAufKuvert, onChange: (e) => { neu.absenderAufKuvert = e.target.checked; neu.previewSession = null; }, style: 'width:16px;height:16px;' }), 'Absender aufs Kuvert (oben links)', ]) : null, - (neu.kuverts && neu.absenderAufKuvert) ? h('div', { style: 'margin-top:10px;' }, [ - label('Absender (max. 5 Zeilen)'), + (istPostkarte() || (neu.kuverts && neu.absenderAufKuvert)) ? h('div', { style: 'margin-top:10px;' }, [ + label(istPostkarte() ? 'Absender (rechte Kartenhälfte, max. 5 Zeilen)' : 'Absender (max. 5 Zeilen)'), h('textarea', { value: neu.envelopeSender, rows: 5, placeholder: 'Max Mustermann\nMusterstraße 1\n12345 Musterstadt', @@ -556,7 +575,7 @@ const NeuAuftrag = defineComponent({ onBlur: (e) => { const n = Math.round(Number(e.target.value)); neu.envelopeSenderScale = (Number.isFinite(n) && n) ? Math.max(50, Math.min(200, n)) : 100; } }), ]), h('div', { style: 'font-size:11px;color:var(--foreground-subdued,#888);margin-top:4px;line-height:1.5;' }, - 'Der Empfänger rutscht dann auf dem Kuvert automatisch nach unten rechts.'), + istPostkarte() ? 'Empfänger steht darunter in der Anschriftzone (rechte Hälfte).' : 'Der Empfänger rutscht dann auf dem Kuvert automatisch nach unten rechts.'), ]) : null, neu.importOpen ? h('div', { style: 'margin-top:12px;' }, [ subdued('Aus Excel kopieren und einfügen – eine Zeile pro Empfänger, Spalten mit Tab/Semikolon/Komma. ' diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index b7c501d..3f7afa1 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -678,6 +678,10 @@ const handler = (router, { services, getSchema, logger }) => { const briefFormat = LETTER_FORMAT[body.format] || body.format || 'a4'; // A4 → DIN Lang, A6 → C6 const umschlagFormat = briefFormat === 'a4' ? 'din_lang' : 'c6'; + // Postkarte (Text links, Adresse rechts, ein SVG) – Erkennung über render_formats.kind. + const istPk = (await svc('render_formats').readByQuery({ + filter: { key: { _eq: briefFormat } }, limit: 1, fields: ['kind'], + }))?.[0]?.kind === 'postkarte'; const platzhalterVon = (e, i) => ({ Anrede: clean(e.salutation || ''), @@ -717,6 +721,22 @@ const handler = (router, { services, getSchema, logger }) => { const nr = i + 1; // Abweichender Text je Schriftstück (#5) hat Vorrang; sonst der Standardtext. const briefText = (e.text && String(e.text).trim()) ? String(e.text) : String(body.text); + + // Postkarte: EIN Dokument mit Text + Absender + Empfänger (Marker-getrennt). + if (istPk) { + const sender = clean(String(body.envelope_sender || '')).trim(); + const recip = adressBlock(e); + let combined = String(clean(briefText) || ''); + if (sender) combined += `[[pk_abs]]${sender}`; + if (recip) combined += `[[pk_adr]]${recip}`; + letters.push({ + index: nr, type: 'postcard', text: combined, format: briefFormat, font, + realisticHandwriting: realistisch, placeholders: platzhalterVon(e, i), + fontScale: fsLetter, senderScale: Number(body.envelope_sender_scale) || 1, + }); + return; + } + // Kein Schriftstück, wenn kein Text da ist (reiner Kuvert-Druck). if (String(briefText).trim()) { letters.push({