diff --git a/extensions/directus-extension-skrift-neuauftrag/dist/index.js b/extensions/directus-extension-skrift-neuauftrag/dist/index.js index d34cc9e..c955168 100644 --- a/extensions/directus-extension-skrift-neuauftrag/dist/index.js +++ b/extensions/directus-extension-skrift-neuauftrag/dist/index.js @@ -13,11 +13,11 @@ import { defineComponent, ref, reactive, onMounted, h, resolveComponent, Telepor import { useApi } from '@directus/extensions-sdk'; const ACCENT = '#1E50A5'; -// Klar umrandetes, theme-sicheres Eingabefeld (sieht aus wie ein Feld). -const WFELD = 'background:var(--background-page);border:1px solid var(--border-normal);border-radius:8px;padding:9px 12px;font:inherit;font-size:14px;color:var(--foreground-normal);outline:none;'; +// Klar umrandetes, theme-sicheres Eingabefeld (dezente Füllung → sieht wie ein Feld aus). +const WFELD = 'background:var(--background-subdued,#f4f6fa);border:1px solid var(--border-normal);border-radius:8px;padding:10px 12px;font:inherit;font-size:14px;color:var(--foreground-normal);outline:none;'; const WSELECT = WFELD + 'cursor:pointer;'; -// Getönter Bereich mit Haarlinie. -const BEREICH = 'background:var(--background-subdued,#f6f8fb);border:1px solid var(--border-subdued);border-radius:12px;padding:18px 20px;'; +// Bereich = klar abgegrenzte weiße Karte mit sichtbarem Rahmen + feinem Schatten. +const BEREICH = 'background:var(--background-page,#fff);border:1px solid var(--border-normal);border-radius:12px;padding:20px 22px;box-shadow:0 1px 2px rgba(16,24,40,.05);'; const STANDARD_PLATZHALTER = ['Anrede', 'Vorname', 'Nachname', 'Strasse', 'PLZ', 'Ort', 'Land', 'Briefnummer']; // Echtes Directus-Icon (Material Symbols) statt Emoji. @@ -37,8 +37,8 @@ function segToggle(value, opts, onSel) { + (value === o.key ? `background:${ACCENT};color:#fff;font-weight:500;` : `background:transparent;color:var(--foreground-normal);`), }, o.label))); } -function bereichKopf(ic, txt) { return h('div', { style: 'display:flex;align-items:center;gap:7px;font-size:16px;font-weight:500;color:var(--foreground-normal);margin-bottom:14px;' }, [icon(ic), txt]); } -function label(txt) { return h('label', { style: 'display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-subdued);margin-bottom:4px;' }, txt); } +function bereichKopf(ic, txt) { return h('div', { style: 'display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;color:var(--foreground-normal);margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--border-subdued);' }, [h('span', { style: `color:${ACCENT};display:inline-flex;align-items:center;` }, icon(ic)), txt]); } +function label(txt) { return h('label', { style: 'display:block;font-size:11.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--foreground-subdued);margin-bottom:5px;' }, txt); } const NeuAuftrag = defineComponent({ setup() { diff --git a/extensions/directus-extension-skrift-produktion/dist/index.js b/extensions/directus-extension-skrift-produktion/dist/index.js index 1727c4a..148ba98 100644 --- a/extensions/directus-extension-skrift-produktion/dist/index.js +++ b/extensions/directus-extension-skrift-produktion/dist/index.js @@ -1552,11 +1552,11 @@ const Board = defineComponent({ const sumKuvert = c.orders.reduce((a, o) => a + (o.needs_envelope ? (Number(o.entries_count) || 0) : 0), 0); const fmts = [...new Set(c.orders.map((o) => o.format?.name || o.format?.key).filter(Boolean))]; const versands = [...new Set(c.orders.map((o) => o.shipping_type === 'einzeln' ? 'Einzel' : 'Sammel'))]; - const cTrenn = () => h('span', { style: 'color:var(--border-normal);', 'aria-hidden': 'true' }, '·'); - const chargeSpecs = h('div', { style: 'display:flex;gap:11px;align-items:center;flex-wrap:wrap;margin-top:10px;color:var(--foreground-subdued);font-size:13.5px;' }, [ - metaItem(`${sumSchrift} Schriftstücke`), cTrenn(), metaItem(`${sumKuvert} Kuverts`), cTrenn(), - metaItem('Format: ' + (fmts.length ? fmts.join(', ') : '—')), cTrenn(), - metaItem('Versand: ' + (versands.length === 1 ? versands[0] : versands.join('/'))), + const chargeSpecs = h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:12px;' }, [ + tile('Schriftstücke', String(sumSchrift), true), + tile('Kuverts', sumKuvert ? String(sumKuvert) : 'ohne', true), + tile('Format', fmts.length ? fmts.join(', ') : '—'), + tile('Versand', versands.length === 1 ? versands[0] : versands.join('/')), ]); const body = auf ? h('div', { style: 'margin-top:12px;display:flex;flex-direction:column;gap:2px;' }, c.orders.map((o) => orderCard(o, opts))) : null; const chargeNotiz = notizFeld(chargeNotizen.value[c.key], (v) => setChargeNotiz(c.key, v), 'Notiz zur Charge …'); diff --git a/extensions/directus-extension-skrift-uebersicht/dist/index.js b/extensions/directus-extension-skrift-uebersicht/dist/index.js index c2c6ece..a2fdc72 100644 --- a/extensions/directus-extension-skrift-uebersicht/dist/index.js +++ b/extensions/directus-extension-skrift-uebersicht/dist/index.js @@ -93,41 +93,63 @@ const Board = { api.get('/items/machines', { params: { limit: -1, filter: { active: { _eq: true } }, fields: 'id,name,current_format,status,active' } }).catch(() => ({ data: { data: [] } })), api.get('/items/format_templates', { params: { limit: -1, fields: 'id,key,name,type,default_format,search_default' } }).catch(() => ({ data: { data: [] } })), ]); + const vorherAnzahl = orders.value.length; orders.value = o.data?.data || []; jobs.value = j.data?.data || []; machines.value = m.data?.data || []; templates.value = t.data?.data || []; - // Empfänger (erster/letzter + flache Suchliste) aus order_entries. - const idx = {}; const flach = []; - const ids = orders.value.map((x) => x.id); - try { - // Relationsfilter (kurze URL) statt _in mit hunderten IDs (sonst 414). - const ent = ids.length ? await api.get('/items/order_entries', { params: { - limit: -1, sort: 'order', - filter: { order: { production_status: { _in: AKTIV_STATUS } } }, - fields: 'order,letter_number,salutation,first_name,last_name,company,street,house_no,zip,city,country,free_text', - } }) : { data: { data: [] } }; - for (const e of (ent.data?.data || [])) { - const k = String(e.order?.id ?? e.order); if (!k || k === 'null') continue; - if (!idx[k]) idx[k] = { erster: e, letzter: e, anzahl: 1 }; - else { idx[k].letzter = e; idx[k].anzahl += 1; } - const adr = []; - const person = [e.salutation, e.first_name, e.last_name].filter(Boolean).join(' '); - if (person) adr.push(person); else if (e.company) adr.push(e.company); - const strasse = [e.street, e.house_no].filter(Boolean).join(' '); if (strasse) adr.push(strasse); - const ortZeile = [e.zip, e.city].filter(Boolean).join(' '); if (ortZeile) adr.push(ortZeile); - if (e.country && !/^(de|d|deu|deutschland|germany)$/i.test(String(e.country).trim())) adr.push(e.country); - if (!adr.length && e.free_text) adr.push(...String(e.free_text).split('\n').map((z) => z.trim()).filter(Boolean)); - const hay = [e.salutation, e.first_name, e.last_name, e.company, e.street, e.house_no, e.zip, e.city, e.country, e.free_text].filter(Boolean).join(' ').toLowerCase(); - flach.push({ orderId: k, nr: Number(e.letter_number) || (idx[k].anzahl), name: entryName(e), adr, hay }); - } - } catch { /* Empfänger optional */ } - entriesByOrder.value = idx; - sucheEintraege.value = flach; + // Empfänger (erster/letzter) separat & schlank laden – nur beim vollen + // Laden oder wenn sich die Auftragsmenge geändert hat (sonst unnötige + // DB-Last beim 20-Sekunden-Refresh). + if (!leise || orders.value.length !== vorherAnzahl) await ladeEmpfaenger(); } catch (e) { if (!leise) err.value = e?.response?.data?.errors?.[0]?.message || e?.message || 'Laden fehlgeschlagen.'; } finally { if (!leise) loading.value = false; } } + // Schlanke Empfänger-Indexierung (nur Name/Ort) für erster/letzter + Filter. + async function ladeEmpfaenger() { + try { + const ent = await api.get('/items/order_entries', { params: { + limit: -1, sort: ['order', 'letter_number'], + filter: { order: { production_status: { _in: AKTIV_STATUS } } }, + fields: 'order,letter_number,first_name,last_name,company,city,free_text', + } }); + const idx = {}; + for (const e of (ent.data?.data || [])) { + const k = String(e.order?.id ?? e.order); if (!k || k === 'null') continue; + if (!idx[k]) idx[k] = { erster: e, letzter: e, anzahl: 1 }; + else { idx[k].letzter = e; idx[k].anzahl += 1; } + } + entriesByOrder.value = idx; + } catch { /* Empfänger optional */ } + } + // Volle Empfängerliste (mit Adresse) für die Suche – erst bei Bedarf. + let sucheGeladen = false; + async function ladeSuche() { + if (sucheGeladen) return; + try { + const ent = await api.get('/items/order_entries', { params: { + limit: -1, sort: ['order', 'letter_number'], + filter: { order: { production_status: { _in: AKTIV_STATUS } } }, + fields: 'order,letter_number,salutation,first_name,last_name,company,street,house_no,zip,city,country,free_text', + } }); + const flach = []; const cnt = {}; + for (const e of (ent.data?.data || [])) { + const k = String(e.order?.id ?? e.order); if (!k || k === 'null') continue; + cnt[k] = (cnt[k] || 0) + 1; + const adr = []; + const person = [e.salutation, e.first_name, e.last_name].filter(Boolean).join(' '); + if (person) adr.push(person); else if (e.company) adr.push(e.company); + const strasse = [e.street, e.house_no].filter(Boolean).join(' '); if (strasse) adr.push(strasse); + const ortZeile = [e.zip, e.city].filter(Boolean).join(' '); if (ortZeile) adr.push(ortZeile); + if (e.country && !/^(de|d|deu|deutschland|germany)$/i.test(String(e.country).trim())) adr.push(e.country); + if (!adr.length && e.free_text) adr.push(...String(e.free_text).split('\n').map((z) => z.trim()).filter(Boolean)); + const hay = [e.salutation, e.first_name, e.last_name, e.company, e.street, e.house_no, e.zip, e.city, e.country, e.free_text].filter(Boolean).join(' ').toLowerCase(); + flach.push({ orderId: k, nr: Number(e.letter_number) || cnt[k], name: entryName(e), adr, hay }); + } + sucheEintraege.value = flach; sucheGeladen = true; + } catch { /* optional */ } + } onMounted(laden); let refreshTimer = null; onMounted(() => { refreshTimer = setInterval(() => laden(true), 20000); }); @@ -155,6 +177,14 @@ const Board = { const setStatus = (o, v) => patchOrder(o, { production_status: v }); const setVersand = (o, v) => patchOrder(o, { shipping_type: v }); const setVersanddatum = (o, v) => patchOrder(o, { versanddatum: v || null }); + // Status für einen ganzen Sammelauftrag (alle Teil-Aufträge) auf einmal. + async function setChargeStatus(c, v) { + try { + await api.patch('/items/orders', { keys: c.orders.map((o) => o.id), data: { production_status: v } }); + for (const o of c.orders) o.production_status = v; + await laden(true); + } catch (e) { err.value = 'Speichern fehlgeschlagen.'; } + } async function formatStarten(machine, fmt) { try { await api.patch('/items/machines/' + machine.id, { current_format: fmt }); machine.current_format = fmt; flash(`${machine.name}: Format „${FORMAT_LABEL[fmt] || fmt}" eingelegt → Warteschlange läuft.`); } catch (e) { err.value = 'Format konnte nicht gestartet werden.'; } @@ -202,14 +232,15 @@ const Board = { }); // ── Render-Helfer ───────────────────────────────────────────────────────── - const statusPill = (o) => { - const k = o.production_status || 'eingegangen'; const m = STATUS_META[k] || STATUS_META.eingegangen; + const statusPillCtrl = (k0, onCh) => { + const k = k0 || 'eingegangen'; const m = STATUS_META[k] || STATUS_META.eingegangen; return h('div', { style: `display:inline-flex;align-items:center;gap:5px;border-radius:8px;padding:5px 8px 5px 10px;background:${m.bg};color:${m.fg};font-weight:500;font-size:13px;` }, [ icon(m.icon), - h('select', { style: `border:none;background:transparent;color:${m.fg};font:inherit;font-size:13px;font-weight:500;cursor:pointer;`, value: k, onChange: (e) => setStatus(o, e.target.value) }, + h('select', { style: `border:none;background:transparent;color:${m.fg};font:inherit;font-size:13px;font-weight:500;cursor:pointer;`, value: k, onChange: (e) => onCh(e.target.value) }, Object.entries(STATUS_LABEL).map(([kk, v]) => h('option', { value: kk, selected: k === kk }, v))), ]); }; + const statusPill = (o) => statusPillCtrl(o.production_status, (v) => setStatus(o, v)); const druckBadge = (ok, text) => h('span', { style: `display:inline-flex;align-items:center;gap:4px;font-size:12.5px;font-weight:500;padding:2px 8px;border-radius:999px;background:${ok ? 'rgba(46,125,50,.14)' : 'rgba(198,40,40,.12)'};color:${ok ? 'var(--success,#2e7d32)' : 'var(--danger,#c62828)'};` }, [icon(ok ? 'check' : 'close'), text]); const th = (t, extra) => h('th', { style: `text-align:left;font-size:12px;font-weight:500;color:var(--foreground-subdued);text-transform:uppercase;letter-spacing:.03em;padding:10px 12px;border-bottom:1px solid var(--border-normal);white-space:nowrap;${extra || ''}` }, t); const td = (kids, extra) => h('td', { style: `padding:11px 12px;border-bottom:1px solid var(--border-subdued);font-size:14px;vertical-align:top;${extra || ''}` }, kids); @@ -273,19 +304,12 @@ const Board = { h('div', { style: 'color:var(--foreground-subdued);' }, `#${o.order_number} · ${o.entries_count || 0} Empf.`), ])), td(h('span', {}, o.format?.name || FORMAT_LABEL[o.format?.key] || o.format?.key || '—')), - td(indent ? h('span', { style: 'color:var(--foreground-subdued);font-size:12px;' }, '— (s. Zusammenfassung)') : ablaufStrip(o)), + td(ablaufStrip(o)), td(versandToggle(o)), td(datumFeld(o)), td(statusPill(o)), ]); } - // Große Kachel für die Chargen-Zusammenfassung. - function kachel(label, wert, ic) { - return h('div', { style: 'flex:1;min-width:104px;border:1px solid var(--border-normal);border-radius:10px;padding:9px 12px;background:var(--background-page,#fff);' }, [ - h('div', { style: 'display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--foreground-subdued);text-transform:uppercase;letter-spacing:.04em;' }, [icon(ic), label]), - h('div', { style: 'font-size:19px;font-weight:600;color:var(--foreground-normal);margin-top:3px;line-height:1.2;' }, String(wert)), - ]); - } // Aggregierter Ablauf über alle Aufträge einer Charge. function ablaufStripAgg(orders) { const needEnv = orders.filter((o) => o.needs_envelope); @@ -301,40 +325,46 @@ const Board = { ]; return h('div', { style: 'display:flex;gap:5px;flex-wrap:wrap;' }, steps.map(([l, b, e]) => stepPill(l, b, e))); } - function chargeHeaderRow(c) { + // Sammelauftrag (Charge) = EINE normale Zeile, optisch wie jeder andere + // Auftrag. Empfänger = erster der Charge · letzter der Charge. Status + // schaltet den ganzen Sammelauftrag um. Unterelemente optional aufklappbar. + function chargeZeile(c) { const auf = !!expandedCharges[c.key]; - const sumS = c.orders.reduce((a, o) => a + (Number(o.entries_count) || 0), 0); - const sumK = c.orders.reduce((a, o) => a + (o.needs_envelope ? (Number(o.entries_count) || 0) : 0), 0); - const fmts = [...new Set(c.orders.map((o) => o.format?.name || o.format?.key).filter(Boolean))]; - const versands = [...new Set(c.orders.map((o) => o.shipping_type === 'einzeln' ? 'Einzel' : 'Sammel'))]; - return h('tr', { key: 'c' + c.key, style: 'background:var(--background-subdued,#f6f8fb);' }, [ - h('td', { colspan: 7, style: 'padding:0;border-bottom:1px solid var(--border-normal);' }, [ - h('div', { style: 'padding:12px 14px;display:flex;flex-direction:column;gap:11px;' }, [ - // Kopfzeile: aufklappbar - h('div', { style: 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;cursor:pointer;', onClick: () => { expandedCharges[c.key] = !auf; } }, [ - icon(auf ? 'expand_more' : 'chevron_right'), - h('span', { style: 'font-weight:600;font-size:15px;display:inline-flex;align-items:center;gap:6px;' }, [icon('inbox'), `${c.kundeName} · ${c.produkt}`]), - h('span', { style: 'font-size:13px;color:var(--foreground-subdued);' }, `${c.orders.length} Aufträge`), - ]), - // Große Kacheln - h('div', { style: 'display:flex;gap:9px;flex-wrap:wrap;' }, [ - kachel('Schriftstücke', sumS, 'description'), - kachel('Kuverts', sumK, 'mail'), - kachel('Format', fmts.join(', ') || '—', 'crop_free'), - kachel('Versand', versands.length === 1 ? versands[0] : versands.join(' / '), 'local_shipping'), - ]), - // Aggregierter Ablauf - ablaufStripAgg(c.orders), - ]), - ]), + const sumEmpf = c.orders.reduce((a, o) => a + (Number(o.entries_count) || 0), 0); + const fmts = [...new Set(c.orders.map((o) => o.format?.name || FORMAT_LABEL[o.format?.key] || o.format?.key).filter(Boolean))]; + const first = entriesByOrder.value[String(c.orders[0]?.id)]; + const last = entriesByOrder.value[String(c.orders[c.orders.length - 1]?.id)]; + const einzelN = c.orders.filter((o) => o.shipping_type === 'einzeln').length; + const vTxt = einzelN === 0 ? 'Sammel' : (einzelN === c.orders.length ? 'Einzeln' : 'gemischt'); + const vIc = einzelN === c.orders.length ? 'person' : 'groups'; + const daten = [...new Set(c.orders.map((o) => o.versanddatum || '').filter(Boolean))]; + const dTxt = daten.length === 0 ? 'schnellstmöglich' : (daten.length === 1 ? daten[0] : 'mehrere'); + const dFaellig = daten.length === 1 && daten[0] <= new Date().toISOString().slice(0, 10); + const stat = c.orders[0]?.production_status; + return h('tr', { key: 'c' + c.key }, [ + td(h('div', { style: 'line-height:1.45;' }, [ + h('div', { style: 'font-weight:500;' }, empfKurz(first?.erster) || '—'), + sumEmpf > 1 ? h('div', { style: 'font-size:12.5px;color:var(--foreground-subdued);' }, '… ' + (empfKurz(last?.letzter) || '—')) : null, + ])), + td(h('div', { style: 'line-height:1.4;font-size:13px;' }, [ + h('div', { style: 'font-weight:500;display:inline-flex;align-items:center;gap:6px;cursor:pointer;', title: auf ? 'Zuklappen' : 'Teil-Aufträge zeigen', onClick: () => { expandedCharges[c.key] = !auf; } }, + [icon(auf ? 'expand_more' : 'chevron_right'), icon('inbox'), c.kundeName]), + h('div', { style: 'color:var(--foreground-subdued);' }, `${c.produkt} · ${c.orders.length} Aufträge · ${sumEmpf} Empf.`), + ])), + td(h('span', {}, fmts.join(', ') || '—')), + td(ablaufStripAgg(c.orders)), + td(h('span', { title: 'Versandart (nur Ansicht)', style: 'border:1px solid var(--border-subdued);background:var(--background-subdued,#f2f4f8);border-radius:999px;padding:4px 11px;font-size:12.5px;font-weight:500;display:inline-flex;align-items:center;gap:5px;color:var(--foreground-normal);' }, [icon(vIc), vTxt])), + td(h('span', { title: 'Versanddatum (nur Ansicht)', style: 'display:inline-flex;align-items:center;gap:5px;font-size:13px;color:' + (dFaellig ? 'var(--danger,#c62828)' : 'var(--foreground-normal)') + ';font-weight:' + (dFaellig ? '600' : '400') + ';' }, [icon('event'), dTxt])), + td(statusPillCtrl(stat, (v) => setChargeStatus(c, v))), ]); } - // Zeilen einer Status-Gruppe: API gruppiert (Charge-Kopf + bei Aufklappen Mitglieder), Rest einzeln. + // Zeilen einer Status-Gruppe: API-Aufträge als Sammelzeile (optional + // aufklappbar), Rest einzeln. function gruppenZeilen(list) { const api = list.filter((o) => o.source === 'api' || o.api_client); const rest = list.filter((o) => !(o.source === 'api' || o.api_client)); const rows = []; - for (const c of bildeChargen(api)) { rows.push(chargeHeaderRow(c)); if (expandedCharges[c.key]) for (const o of c.orders) rows.push(zeile(o, true)); } + for (const c of bildeChargen(api)) { rows.push(chargeZeile(c)); if (expandedCharges[c.key]) for (const o of c.orders) rows.push(zeile(o, true)); } for (const o of rest) rows.push(zeile(o)); return rows; } @@ -432,7 +462,7 @@ const Board = { return rahmen(h('div', {}, treffer.map(trefferZeile))); } - const navItem = (key, label, ic) => h('button', { type: 'button', onClick: () => { clearTimeout(sucheTimer); sucheQuery.value = ''; view.value = key; }, + const navItem = (key, label, ic) => h('button', { type: 'button', onClick: () => { clearTimeout(sucheTimer); sucheQuery.value = ''; view.value = key; if (key === 'suche') ladeSuche(); }, style: 'display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;border:none;border-radius:8px;padding:11px 13px;margin-bottom:3px;font:inherit;font-size:14px;' + (view.value === key ? 'background:#1E50A5;color:#fff;font-weight:500;' : 'background:transparent;color:var(--foreground-normal);'), }, [icon(ic), label]);