From ae7ef2422e05d2a99685be9d0e7a4d241ecc1e1c Mon Sep 17 00:00:00 2001 From: s4luorth Date: Mon, 7 Sep 2026 08:27:44 +0200 Subject: [PATCH] =?UTF-8?q?Statistik-Tab,=20Unterschriftenservice-Kuverts,?= =?UTF-8?q?=20Auftragsverkn=C3=BCpfung=20&=20Notizen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Produktion: Statistik-Tab (versendete Briefe je Kunde/Monat), verknüpfter Auftrag je Karte anzeigen/ändern/entfernen, Datei-Aufträge werden beim Übernehmen wieder in den Signatur-Tab überführt. - Unterschriftenservice (neuauftrag): optionale Kuverts (je Empfänger ein Umschlag, Absender möglich) und Verknüpfung mit vorhandenem Auftrag. - orders: signature-order erzeugt Kuverts über die Standard-Pipeline (Datei-Produkt -> nur Umschläge, Seiten bleiben erhalten) und speichert linked_order; nachträgliches Verknüpfen im Board. - bootstrap: orders.linked_order (Self-Relation). Co-Authored-By: Claude Opus 4.8 --- bootstrap/bootstrap.mjs | 1 + .../dist/index.js | 62 +++++++++++++- .../dist/index.js | 55 +++++++++++-- .../dist/index.js | 82 ++++++++++++++++++- 4 files changed, 188 insertions(+), 12 deletions(-) diff --git a/bootstrap/bootstrap.mjs b/bootstrap/bootstrap.mjs index 79a50ea..406c353 100644 --- a/bootstrap/bootstrap.mjs +++ b/bootstrap/bootstrap.mjs @@ -311,6 +311,7 @@ const collections = [ f('payment_ref', 'string', { interface: 'input' }), f('artifact_path', 'string', { interface: 'input', note: 'Verweis auf Backend-Output.' }), f('internal_note', 'text', { interface: 'input-multiline', note: 'Interne Notiz zum Auftrag (Produktion).' }), + m2o('linked_order', 'orders', { onDelete: 'SET NULL', note: 'Verknüpfter Auftrag (z. B. Kuverts zum Unterschriftenservice). Nachträglich änderbar.' }), { field: 'date_created', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-created'] }, schema: {} }, { field: 'date_updated', type: 'timestamp', meta: { interface: 'datetime', readonly: true, special: ['date-updated'] }, schema: {} }, // Produktionsansicht: durchsuchbare Empfängertabelle (Briefnummer + volle diff --git a/extensions/directus-extension-skrift-neuauftrag/dist/index.js b/extensions/directus-extension-skrift-neuauftrag/dist/index.js index e67c86e..0bb9d11 100644 --- a/extensions/directus-extension-skrift-neuauftrag/dist/index.js +++ b/extensions/directus-extension-skrift-neuauftrag/dist/index.js @@ -59,6 +59,11 @@ 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) { @@ -118,7 +123,13 @@ const NeuAuftrag = defineComponent({ if (raw) { localStorage.removeItem('skrift_neuauftrag_uebernahme'); const d = JSON.parse(raw); - if (d && typeof d === 'object') { + if (d && d.zielSignatur) { + // Datei-/Unterschriftenservice-Auftrag → Signatur-Tab, per Nr. laden. + ansicht.value = 'signatur'; + sig.modus = 'auftrag'; + sig.orderNr = String(d.orderNr || ''); + if (sig.orderNr) { sigLaden().catch(() => {}); } + } else if (d && typeof d === 'object') { if (d.product) neu.product = d.product; if (d.format) neu.format = d.format; if (d.font) neu.font = d.font; @@ -757,10 +768,20 @@ 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 }); - sig.msg = `✔ Auftrag ${res.data.order_number} angelegt (${res.data.pages} Seite/n). Im Produktions-Board unter „Neu" → Drucken.`; + 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.`; } catch (e) { sig.err = e?.response?.data?.detail || e?.response?.data?.error || e?.message || 'Auftrag konnte nicht angelegt werden.'; sig.msg = ''; } } @@ -835,11 +856,44 @@ 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, meldung]); + const links = h('div', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;gap:16px;' }, [b1, b2, b3, 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'), diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index b7c501d..e482840 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -310,6 +310,20 @@ 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 () => { @@ -320,9 +334,14 @@ 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: false, envelope_labeling: 'keine', + needs_envelope: kuvertsAn, envelope_labeling: kuvertsAn ? 'empfaenger' : 'keine', + envelope_format: kuvertsAn ? envFormat : null, + envelope_sender: kuvertsAn && absender ? absender : null, + envelope_sender_scale: absenderScale, source_file: pdfId, source_files: [pdfId], - entries_count: pages.length, count_inland: pages.length, count_ausland: 0, + entries_count: kuvertsAn ? empf.length : pages.length, + count_inland: kuvertsAn ? (empf.length - countAusland) : pages.length, + count_ausland: kuvertsAn ? countAusland : 0, }; let orderId, orderNumber; for (let v = 0; v < 6; v += 1) { @@ -332,11 +351,28 @@ const handler = (router, { services, getSchema, logger }) => { } const entriesSvc = svc('order_entries'); - for (let i = 0; i < pages.length; i += 1) { - await entriesSvc.createOne({ order: orderId, letter_number: i + 1, free_text: `Seite ${i + 1}` }); + 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}` }); + } } 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 }); @@ -344,7 +380,16 @@ const handler = (router, { services, getSchema, logger }) => { fs.writeFileSync(path.join(ordner, `letter_${String(i + 1).padStart(3, '0')}.svg`), p.svg, 'utf8'); }); - return res.json({ ok: true, order_number: orderNumber, id: orderId, pages: pages.length }); + // 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 }); } 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) }); diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index a0394bd..71bbd44 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -60,6 +60,7 @@ const TABS = [ { key: 'versandfertig', label: '📦 Versandfertig' }, { key: 'versendet', label: '🚚 Versendet' }, { key: 'archiv', label: '🗄 Archiv' }, + { key: 'statistik', label: '📊 Statistik' }, ]; const STATUS_LABEL = { eingegangen: 'Eingegangen', im_druck: 'Im Druck', kuvertieren: 'Kuvertierung', @@ -196,7 +197,7 @@ const Board = defineComponent({ + 'envelope_labeling,shipping_type,shipping_day,keep_contacts,entries_count,count_inland,count_ausland,brief_gedruckt,kuvert_gedruckt,' + 'date_updated,date_created,format.key,format.name,product.name,product.input_mode,' + 'source,api_client.id,api_client.name,' - + 'source_file,internal_note,' + + 'source_file,internal_note,linked_order.id,linked_order.order_number,' + 'customer.first_name,customer.last_name,customer.email,customer.phone'; async function ladeOrders() { try { @@ -285,6 +286,19 @@ const Board = defineComponent({ try { await api.patch('/items/orders/' + order.id, { internal_note: val }); order.internal_note = val; } catch { fehler.value = 'Notiz konnte nicht gespeichert werden.'; } } + // Verknüpften Auftrag (Kuverts) setzen/ändern/entfernen – per Auftragsnummer. + async function setOrderLink(order, nr) { + const num = String(nr || '').trim(); + if (num === (order.linked_order?.order_number || '')) return; + try { + if (!num) { await api.patch('/items/orders/' + order.id, { linked_order: null }); order.linked_order = null; flash(`${order.order_number}: Verknüpfung entfernt`); return; } + const ziel = (await api.get('/items/orders', { params: { filter: { order_number: { _eq: num } }, limit: 1, fields: 'id,order_number' } })).data?.data?.[0]; + if (!ziel) { fehler.value = `Auftrag ${num} nicht gefunden.`; return; } + await api.patch('/items/orders/' + order.id, { linked_order: ziel.id }); + order.linked_order = { id: ziel.id, order_number: ziel.order_number }; + flash(`${order.order_number} ↔ ${ziel.order_number} verknüpft`); + } catch { fehler.value = 'Verknüpfung konnte nicht gespeichert werden.'; } + } async function setChargeNotiz(key, v) { const val = String(v || ''); if (val === (chargeNotizen.value[key] || '')) return; @@ -371,6 +385,26 @@ const Board = defineComponent({ || (o.production_status === 'versendet' && alterTage(o.versandt_am || o.date_updated) > ARCHIV_TAGE); const versendetAktiv = () => orders.value.filter((o) => o.production_status === 'versendet' && !istArchiviert(o) && trifftSuche(orderHay(o))); const archivListe = () => orders.value.filter((o) => istArchiviert(o) && trifftSuche(orderHay(o))); + + // ── Statistik: versendete Briefe je Kunde und Monat ────────────────────── + const statMonat = ref(new Date().toISOString().slice(0, 7)); // YYYY-MM + const versandMonat = (o) => String(o.versandt_am || o.date_updated || o.date_created || '').slice(0, 7); + const statistik = computed(() => { + const map = new Map(); + let gesamt = 0; + for (const o of orders.value) { + if (o.production_status !== 'versendet') continue; + if (versandMonat(o) !== statMonat.value) continue; + const email = o.customer?.email || ''; + const key = email || kundeName(o) || '—'; + const cur = map.get(key) || { kunde: kundeName(o) || email || '—', email, briefe: 0, auftraege: 0 }; + cur.briefe += Number(o.entries_count) || 0; + cur.auftraege += 1; + map.set(key, cur); + gesamt += Number(o.entries_count) || 0; + } + return { zeilen: [...map.values()].sort((a, b) => b.briefe - a.briefe), gesamt }; + }); const queuedByMachine = computed(() => { const groups = {}; for (const m of aktiveMaschinen.value) groups[m.id] = { machine: m, jobs: [] }; @@ -775,8 +809,16 @@ const Board = defineComponent({ if (!o) return; try { const full = (await api.get('/items/orders/' + o.id, { params: { - fields: 'text_template,font,multi_page,realistic,font_scale_letter,font_scale_envelope,needs_envelope,envelope_sender,envelope_sender_scale,product.key,format.key', + fields: 'order_number,text_template,font,multi_page,realistic,font_scale_letter,font_scale_envelope,needs_envelope,envelope_sender,envelope_sender_scale,product.key,product.input_mode,format.key', } })).data?.data || {}; + // Datei-Produkt (Unterschriftenservice) → zurück in den Signatur-Tab, + // dort per Auftragsnummer erneut laden (statt normaler manueller Druck). + if (full.product?.input_mode === 'datei') { + localStorage.setItem('skrift_neuauftrag_uebernahme', JSON.stringify({ zielSignatur: true, orderNr: full.order_number || o.order_number || '' })); + const base = window.location.pathname.split('/admin/')[0] + '/admin/'; + window.location.href = base + 'skrift-neuauftrag'; + return; + } const ent = (await api.get('/items/order_entries', { params: { 'filter[order][_eq]': o.id, sort: 'letter_number', limit: -1, fields: 'free_text,placeholders,letter_number', } })).data?.data || []; @@ -914,6 +956,16 @@ const Board = defineComponent({ chip(o.format?.name || o.format?.key || 'Format?'), chip(o.payment_status === 'bezahlt' ? '✓ bezahlt' : (o.payment_status === 'storniert' ? 'storniert' : 'offen'), 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. @@ -923,7 +975,7 @@ 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, 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, linkFeld, notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'), actionButtons(o)]); } function requestCard(req) { const istMuster = req.type === 'muster'; @@ -1047,6 +1099,30 @@ const Board = defineComponent({ const list = archivListe(); return list.length ? renderGruppiert(list) : subdued('Archiv leer.'); } + if (tab.value === 'statistik') { + const s = statistik.value; + const kopfzeile = h('div', { style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px;' }, [ + h('label', { style: 'font-weight:600;' }, 'Monat:'), + h('input', { type: 'month', value: statMonat.value, style: S_INPUT + 'width:auto;', + onInput: (e) => { statMonat.value = e.target.value || statMonat.value; } }), + chip(`${s.gesamt} versendete Briefe gesamt`, 'var(--primary-25,rgba(30,80,165,.14))'), + ]); + if (!s.zeilen.length) return h('div', {}, [kopfzeile, subdued('Keine versendeten Briefe in diesem Monat.')]); + const th = (t) => h('th', { style: 'text-align:left;padding:8px 12px;border-bottom:2px solid var(--border-subdued);font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-subdued);' }, t); + const td = (t, extra) => h('td', { style: `padding:8px 12px;border-bottom:1px solid var(--border-subdued);${extra || ''}` }, t); + return h('div', {}, [ + kopfzeile, + h('div', { style: CARD + 'overflow-x:auto;' }, [ + h('table', { style: 'width:100%;border-collapse:collapse;font-size:14px;' }, [ + h('thead', {}, h('tr', {}, [th('Kunde'), th('E-Mail'), th('Aufträge'), th('Briefe versendet')])), + h('tbody', {}, s.zeilen.map((z) => h('tr', {}, [ + td(z.kunde, 'font-weight:600;'), td(z.email || '—', 'color:var(--foreground-subdued);'), + td(String(z.auftraege)), td(String(z.briefe), 'font-weight:700;color:' + ACCENT + ';'), + ]))), + ]), + ]), + ]); + } const map = { kuvertierung: 'kuvertieren', versandfertig: 'versandfertig' }; const st = map[tab.value]; const list = ordersByStatus(st);