Files
skrift-directus/extensions/directus-extension-skrift-uebersicht/dist/index.js
s4luorth a1a2a615cc Punkt 3+4: Empfaenger-Fallbacks, "nur Kuvert"-Kuvertieren-Feld
Punkt 3:
- Board: kundeName faellt bei API-Auftraegen auf api_client.name/company zurueck (kein "-" mehr)
- Uebersicht: mehrseitige Briefe ("Seite 1/2/3") werden nicht als Empfaenger angezeigt
- empfKurz/entryName: totes company-Feld entfernt (gibt es auf order_entries nicht)

Punkt 4: neues Feld orders.kuvertieren (bool, default true) = "Blatt ins Kuvert legen".
- Board "Weiteres": getrennte Schalter "Kuvert schreiben" (needs_envelope) und
  "Blatt einlegen (kuvertieren)" (Blatt rein / Nur Kuvert)
- Ablauf-Schritt "Kuvertieren" nur noetig wenn Kuvert UND Blatt eingelegt wird
- kuvertieren in Order-Abfragen (Board + Uebersicht) ergaenzt

ACHTUNG Deploy: Bootstrap VOR Directus-Neustart laufen lassen, sonst 403 (neues Feld).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-01 13:21:24 +02:00

502 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Skrift – Modul „Übersicht" (Mitarbeiter)
* ===========================================================================
* Secondary-Sidebar mit zwei Ansichten:
* • Übersicht – kompakte, nach Status getrennte Tabellen (API-Aufträge
* gruppiert). Je Auftrag: Empfänger (erster/letzter), Format, Aufgabe,
* Versandart (umschaltbar), Versanddatum (editierbar), Druck Brief/Kuvert,
* eingeplant, Status. Maschinen-Panel: „Format einlegen → starten".
* • Suche – Empfänger suchen, Schriftstückart wählen, direkt in die
* Warteschlange (Nachdruck nach Fehlern).
*
* Von Hand als ESM (kein Build) – Vue + SDK stellt Directus bereit.
*/
import { ref, reactive, computed, onMounted, onUnmounted, h, resolveComponent } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const STATUS_LABEL = {
eingegangen: 'Eingegangen', im_druck: 'Im Druck', kuvertieren: 'Kuvertierung',
versandfertig: 'Versandfertig', versendet: 'Versendet', storniert: 'Storniert',
};
const STATUS_META = {
eingegangen: { bg: '#eef2f8', fg: '#566070', bd: '#d2dae6', icon: 'inbox' },
im_druck: { bg: '#e3ecfa', fg: '#1E50A5', bd: '#bcd0ef', icon: 'print' },
kuvertieren: { bg: '#ece4fb', fg: '#5b32b8', bd: '#d3c4f2', icon: 'mail' },
versandfertig: { bg: '#fbeccb', fg: '#8a5a00', bd: '#f0d79a', icon: 'inventory_2' },
versendet: { bg: '#e3f4e6', fg: '#1b6b2e', bd: '#bfe2c6', icon: 'local_shipping' },
storniert: { bg: '#fbe3e0', fg: '#c0392b', bd: '#f3c6bf', icon: 'cancel' },
};
const AKTIV_STATUS = ['im_druck', 'kuvertieren', 'versandfertig'];
const FORMAT_LABEL = { a4: 'A4', a5p: 'A5 hoch', a5l: 'A5 quer', dinlang_karte: 'DIN lang', a6_hoch: 'A6 hoch', a6_quer: 'A6 quer', dinlang: 'DIN Lang', c6: 'C6', c4: 'C4' };
// Format → Standard-Template-Schlüssel (Fallback, wie im Board).
const LETTER_TPL = { a4: 'a4_single', a5p: 'a5_v1', a5l: 'a5l_v1', dinlang_karte: 'dl_v1', a6_hoch: 'a6_v1', a6_quer: 'a6l_v1' };
const ENVELOPE_TPL = { dinlang: 'kuvert_a_v1', c6: 'kuvert_b_v1', c4: 'kuvert_c4_v1' };
const WOCHENTAG = { montag: 1, dienstag: 2, mittwoch: 3, donnerstag: 4, freitag: 5, samstag: 6, sonntag: 0 };
function faelligBis(dateCreated, shippingDay) {
const wd = WOCHENTAG[String(shippingDay || '').toLowerCase()];
if (wd === undefined || !dateCreated) return null;
const d = new Date(dateCreated); if (isNaN(d)) return null;
d.setHours(0, 0, 0, 0);
const tag = d.getDay();
const montag = new Date(d); montag.setDate(d.getDate() + (tag === 0 ? -6 : 1 - tag));
const ziel = new Date(montag); ziel.setDate(montag.getDate() + 14 + (wd === 0 ? 6 : wd - 1));
return ziel;
}
const dmY = (dt) => dt ? `${String(dt.getDate()).padStart(2, '0')}.${String(dt.getMonth() + 1).padStart(2, '0')}.${dt.getFullYear()}` : '—';
const Board = {
setup() {
const api = useApi();
const icon = (name, extra = {}) => h(resolveComponent('v-icon'), { name, small: true, ...extra });
const view = ref('uebersicht'); // 'uebersicht' | 'suche'
const orders = ref([]);
const jobs = ref([]);
const machines = ref([]);
const templates = ref([]);
const entriesByOrder = ref({}); // { orderId: {erster, letzter, anzahl} }
const sucheEintraege = ref([]); // flache Empfängerliste für die Suche
const expandedCharges = reactive({});
const loading = ref(false);
const msg = ref(''); const err = ref('');
const sucheQuery = ref(''); let sucheTimer = null;
const sucheSetzen = (v) => { clearTimeout(sucheTimer); sucheTimer = setTimeout(() => { sucheQuery.value = v; }, 220); };
const sucheTyp = ref('brief');
const aktiveMaschinen = computed(() => machines.value.filter((m) => m.active !== false));
function kundeName(o) { return [o.customer?.company, [o.customer?.first_name, o.customer?.last_name].filter(Boolean).join(' ')].filter(Boolean).join(' · '); }
// „Seite 1" o. Ä. = Seiten eines mehrseitigen Briefs, kein Empfänger.
const istSeitenmarker = (z) => /^seite\s*\d+$/i.test(String(z || '').trim());
function freitextName(ft) {
const z = ft ? String(ft).split('\n').map((s) => s.trim()).filter(Boolean)[0] : '';
return istSeitenmarker(z) ? '' : z;
}
function empfKurz(e) {
if (!e) return '';
const name = [e.first_name, e.last_name].filter(Boolean).join(' ') || freitextName(e.free_text);
return [name, e.city].filter(Boolean).join(', ');
}
function entryName(e) {
return [e.salutation, e.first_name, e.last_name].filter(Boolean).join(' ')
|| freitextName(e.free_text) || '—';
}
async function laden(leise = false) {
if (!leise) { loading.value = true; err.value = ''; }
try {
const [o, j, m, t] = await Promise.all([
api.get('/items/orders', { params: {
limit: -1, sort: 'date_created',
filter: { production_status: { _in: AKTIV_STATUS } },
fields: 'id,order_number,order_name,production_status,needs_envelope,kuvertieren,brief_gedruckt,kuvert_gedruckt,frankiert,'
+ 'format.key,format.name,shipping_day,shipping_type,versanddatum,date_created,entries_count,source,'
+ 'envelope_labeling,envelope_format,api_client.id,api_client.name,product.name,'
+ 'customer.first_name,customer.last_name,customer.company',
} }),
api.get('/items/jobs', { params: { limit: -1, filter: { status: { _in: ['queued', 'printing'] } }, fields: 'id,type,status,format,format_label,order.id' } }).catch(() => ({ data: { data: [] } })),
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) 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,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,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); });
onUnmounted(() => clearInterval(refreshTimer));
const orderById = computed(() => { const m = {}; for (const o of orders.value) m[String(o.id)] = o; return m; });
const eingeplantSet = computed(() => new Set(jobs.value.map((j) => String(j.order?.id)).filter((x) => x && x !== 'null')));
const queuedByFormat = computed(() => {
const map = {};
for (const j of jobs.value) { if (j.status !== 'queued') continue; const f = j.format || '?'; if (!map[f]) map[f] = { n: 0, label: j.format_label || FORMAT_LABEL[f] || f }; map[f].n += 1; }
return map;
});
const gefiltert = computed(() => {
const q = sucheQuery.value.trim().toLowerCase();
let list = orders.value;
if (q) list = list.filter((o) => {
const e = entriesByOrder.value[String(o.id)];
return [o.order_number, o.order_name, kundeName(o), empfKurz(e?.erster), empfKurz(e?.letzter), o.format?.name].filter(Boolean).join(' ').toLowerCase().includes(q);
});
return list;
});
function flash(t) { msg.value = t; setTimeout(() => { if (msg.value === t) msg.value = ''; }, 4000); }
async function patchOrder(o, felder) { try { await api.patch('/items/orders/' + o.id, felder); Object.assign(o, felder); } catch (e) { err.value = 'Speichern fehlgeschlagen.'; } }
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.'; }
}
// ── Charge-Bildung (API-Aufträge gruppieren) ──────────────────────────────
function bildeChargen(list) {
const map = new Map();
for (const o of list) {
const key = `${o.api_client?.id ?? 'x'}|${o.product?.name || '?'}|${o.shipping_day || ''}|${String(o.date_created || '').slice(0, 10)}`;
let c = map.get(key);
if (!c) { c = { key, kundeName: o.api_client?.name || 'API-Kunde', produkt: o.product?.name || '?', versandtag: o.shipping_day || '', orders: [] }; map.set(key, c); }
c.orders.push(o);
}
return [...map.values()];
}
// ── Reprint (Suche) ───────────────────────────────────────────────────────
const tplSuchStandard = (typ) => templates.value.find((t) => t.type === typ && t.search_default) || null;
const tplByKey = (key, typ) => templates.value.find((t) => t.key === key && t.type === typ) || templates.value.find((t) => t.type === typ) || null;
const tplForFormat = (fmt, typ) => (fmt && templates.value.find((t) => t.type === typ && t.default_format && t.default_format === fmt)) || null;
async function schnelldruck(o, nr, typ) {
const machine = aktiveMaschinen.value[0]?.id || null;
if (!machine) { err.value = 'Keine aktive Maschine verfügbar.'; return; }
const tpl = typ === 'umschlag'
? (tplSuchStandard('umschlag') || tplForFormat(o.envelope_format, 'umschlag') || tplByKey(ENVELOPE_TPL[o.envelope_format] || 'kuvert_a_v1', 'umschlag'))
: (tplSuchStandard('brief') || tplForFormat(o.format?.key, 'brief') || tplByKey(LETTER_TPL[o.format?.key] || 'a4_single', 'brief'));
if (!tpl?.id) { err.value = `Kein Standard-Template für ${typ === 'umschlag' ? 'Kuvert' : 'Schriftstück'} – bitte in den Einstellungen zuordnen.`; return; }
try {
const sections = {}; sections[typ === 'umschlag' ? 'umschlag' : 'brief'] = { enabled: true, template: tpl.id, numbers: nr ? String(nr) : '' };
await api.post('/skrift-orders/enqueue', { order: o.id, machine, mode: 'queue', sections });
flash(`#${o.order_number} · ${typ === 'umschlag' ? 'Kuvert' : 'Schriftstück'}${nr ? ` Nr. ${nr}` : ''} → Warteschlange.`);
await laden(true);
} catch (e) { err.value = e?.response?.data?.error || e?.message || 'Einreihen fehlgeschlagen.'; }
}
const sucheTreffer = computed(() => {
const q = sucheQuery.value.trim().toLowerCase(); if (!q) return [];
const res = [];
for (const e of sucheEintraege.value) {
const o = orderById.value[e.orderId]; if (!o) continue;
if (!(e.hay.includes(q) || String(o.order_number).toLowerCase().includes(q))) continue;
res.push({ e, o }); if (res.length >= 150) break;
}
return res;
});
// ── Render-Helfer ─────────────────────────────────────────────────────────
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};border:1px solid ${m.bd || '#d2dae6'};font-weight:600;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) => 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 ? '#1b6b2e' : '#c0392b'};` }, [icon(ok ? 'check' : 'close'), text]);
const th = (t, extra) => h('th', { style: `text-align:left;font-size:12px;font-weight:500;color:#566070;text-transform:uppercase;letter-spacing:.03em;padding:10px 12px;border-bottom:1px solid #b4c0d2;white-space:nowrap;${extra || ''}` }, t);
const td = (kids, extra) => h('td', { style: `padding:11px 12px;border-bottom:1px solid #d2dae6;font-size:14px;vertical-align:top;${extra || ''}` }, kids);
function aufgabe(o) {
const einzeln = o.shipping_type === 'einzeln';
switch (o.production_status) {
case 'eingegangen': return { t: 'Wartet auf Druck', c: '#566070', i: 'hourglass_empty' };
case 'im_druck': return { t: 'Im Druck', c: '#1E50A5', i: 'print' };
case 'kuvertieren': return { t: (o.needs_envelope ? 'Kuvertieren' : 'Vorbereiten') + ' + frankieren', c: '#6a3fd0', i: 'mail' };
case 'versandfertig': return { t: einzeln ? 'Einzelversand' : 'Sammelversand', c: '#8a6d00', i: 'local_shipping' };
default: return { t: STATUS_LABEL[o.production_status] || '', c: '#566070', i: 'info' };
}
}
// Nur-Lese-Anzeige für Mitarbeiter: Versandart darf hier nicht geändert werden.
function versandToggle(o) {
const einzeln = o.shipping_type === 'einzeln';
return h('span', { title: 'Versandart (nur Ansicht)',
style: 'border:1px solid #d2dae6;background:#eef2f8;border-radius:999px;padding:4px 11px;font-size:12.5px;font-weight:500;display:inline-flex;align-items:center;gap:5px;color:#1b2430;' },
[icon(einzeln ? 'person' : 'groups'), einzeln ? 'Einzeln' : 'Sammel']);
}
// Nur-Lese-Anzeige für Mitarbeiter: Versanddatum darf hier nicht geändert werden.
function datumFeld(o) {
const faellig = o.versanddatum && o.versanddatum <= new Date().toISOString().slice(0, 10);
const txt = o.versanddatum ? o.versanddatum : 'schnellstmöglich';
return h('span', { title: 'Versanddatum (nur Ansicht)',
style: 'display:inline-flex;align-items:center;gap:5px;font-size:13px;color:' + (faellig ? '#c0392b' : '#1b2430') + ';font-weight:' + (faellig ? '600' : '400') + ';' },
[icon('event'), txt]);
}
// Ein Ablauf-Schritt: nicht nötig (✗, grau) · erledigt (✓, grün) · offen (amber).
function stepPill(labelTxt, benoetigt, erledigt) {
let bg, fg, bd, ic, zustand;
if (!benoetigt) { bg = '#eef2f8'; fg = '#566070'; bd = '#d2dae6'; ic = 'close'; zustand = 'nicht nötig'; }
else if (erledigt) { bg = '#e3f4e6'; fg = '#1b6b2e'; bd = '#bfe2c6'; ic = 'check'; zustand = 'erledigt'; }
else { bg = '#fbeccb'; fg = '#8a5a00'; bd = '#f0d79a'; ic = 'radio_button_unchecked'; zustand = 'offen'; }
return h('span', { title: `${labelTxt}: ${zustand}`, style: `display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;padding:3px 9px;border-radius:999px;background:${bg};color:${fg};border:1px solid ${bd};white-space:nowrap;` }, [icon(ic), labelTxt]);
}
function ablaufStrip(o) {
const fertig = ['versandfertig', 'versendet'].includes(o.production_status);
const versendet = o.production_status === 'versendet';
const frankiert = (typeof o.frankiert === 'boolean') ? o.frankiert : (o.shipping_type !== 'sammel');
const kuvertieren = !!o.needs_envelope && o.kuvertieren !== false; // nur Kuvert ohne Blatt → kein Kuvertieren
const steps = [
['Druck Schriftstück', true, !!o.brief_gedruckt],
['Druck Kuvert', !!o.needs_envelope, !!o.kuvert_gedruckt],
['Kuvertieren', kuvertieren, fertig],
['Frankieren', !!frankiert, fertig],
['Versand', true, versendet],
];
return h('div', { style: 'display:flex;gap:5px;flex-wrap:wrap;' }, steps.map(([l, b, e]) => stepPill(l, b, e)));
}
function zeile(o, indent) {
const e = entriesByOrder.value[String(o.id)];
return h('tr', { key: 'o' + o.id }, [
td(h('div', { style: `line-height:1.45;${indent ? 'padding-left:18px;border-left:2px solid #d2dae6;' : ''}` }, [
h('div', { style: 'font-weight:500;' }, empfKurz(e?.erster) || '—'),
(e && e.anzahl > 1) ? h('div', { style: 'font-size:12.5px;color:#566070;' }, '… ' + empfKurz(e.letzter)) : null,
])),
td(h('div', { style: 'line-height:1.4;font-size:13px;' }, [
o.order_name ? h('div', { style: 'font-weight:500;color:#1E50A5;' }, o.order_name) : null,
h('div', {}, kundeName(o) || `#${o.order_number}`),
h('div', { style: 'color:#566070;' }, `#${o.order_number} · ${o.entries_count || 0} Empf.`),
])),
td(h('span', {}, o.format?.name || FORMAT_LABEL[o.format?.key] || o.format?.key || '—')),
td(ablaufStrip(o)),
td(versandToggle(o)),
td(datumFeld(o)),
td(statusPill(o)),
]);
}
// Aggregierter Ablauf über alle Aufträge einer Charge.
function ablaufStripAgg(orders) {
const needEnv = orders.filter((o) => o.needs_envelope);
const needKuv = orders.filter((o) => o.needs_envelope && o.kuvertieren !== false);
const needFrank = orders.filter((o) => (typeof o.frankiert === 'boolean') ? o.frankiert : (o.shipping_type !== 'sammel'));
const alle = (arr, fn) => arr.length > 0 && arr.every(fn);
const fertigOrder = (o) => ['versandfertig', 'versendet'].includes(o.production_status);
const steps = [
['Druck Schriftstück', true, alle(orders, (o) => !!o.brief_gedruckt)],
['Druck Kuvert', needEnv.length > 0, alle(needEnv, (o) => !!o.kuvert_gedruckt)],
['Kuvertieren', needKuv.length > 0, alle(needKuv, fertigOrder)],
['Frankieren', needFrank.length > 0, alle(needFrank, fertigOrder)],
['Versand', true, alle(orders, (o) => o.production_status === 'versendet')],
];
return h('div', { style: 'display:flex;gap:5px;flex-wrap:wrap;' }, steps.map(([l, b, e]) => stepPill(l, b, e)));
}
// 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 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:#566070;' }, '… ' + (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:#566070;' }, `${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 #d2dae6;background:#eef2f8;border-radius:999px;padding:4px 11px;font-size:12.5px;font-weight:500;display:inline-flex;align-items:center;gap:5px;color:#1b2430;' }, [icon(vIc), vTxt])),
td(h('span', { title: 'Versanddatum (nur Ansicht)', style: 'display:inline-flex;align-items:center;gap:5px;font-size:13px;color:' + (dFaellig ? '#c0392b' : '#1b2430') + ';font-weight:' + (dFaellig ? '600' : '400') + ';' }, [icon('event'), dTxt])),
td(statusPillCtrl(stat, (v) => setChargeStatus(c, v))),
]);
}
// 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(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;
}
const kopfRow = () => h('thead', {}, h('tr', {}, [th('Empfänger (erster · letzter)'), th('Auftrag'), th('Format'), th('Ablauf'), th('Versand'), th('Versanddatum'), th('Status')]));
const tabelle = (list) => h('div', { style: 'border:1px solid #b4c0d2;border-radius:12px;overflow:auto;background:#ffffff;' },
[h('table', { style: 'width:100%;border-collapse:collapse;' }, [kopfRow(), h('tbody', {}, gruppenZeilen(list))])]);
const sektionKopf = (label, m, n) => h('div', { style: 'display:flex;align-items:center;gap:10px;margin-bottom:10px;' }, [
h('span', { style: `display:inline-flex;align-items:center;gap:6px;font-size:16px;font-weight:500;color:${m.fg};` }, [icon(m.icon), label]),
h('span', { style: 'font-size:13px;color:#566070;' }, `${n} Auftrag/Aufträge`),
]);
function maschinenPanel() {
const qf = queuedByFormat.value; const formate = Object.keys(qf);
return h('div', { style: 'border:1px solid #b4c0d2;border-radius:12px;background:#ffffff;margin-bottom:18px;overflow:hidden;' }, [
h('div', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:#566070;padding:10px 14px;border-bottom:1px solid #d2dae6;background:#eef2f8;' }, [icon('print'), 'Maschinen · Format einlegen und starten']),
h('div', { style: 'padding:14px;display:flex;flex-direction:column;gap:14px;' },
aktiveMaschinen.value.length ? aktiveMaschinen.value.map((m) => h('div', { key: 'm' + m.id, style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;' }, [
h('div', { style: 'font-weight:500;min-width:150px;' }, [m.name, h('span', { style: 'color:#566070;font-weight:400;font-size:13px;' }, m.current_format ? ` · eingelegt: ${FORMAT_LABEL[m.current_format] || m.current_format}` : ' · kein Format eingelegt')]),
...(formate.length ? formate.map((f) => h('button', { type: 'button', onClick: () => formatStarten(m, f),
style: `cursor:pointer;border-radius:8px;padding:8px 14px;font:inherit;font-size:14px;font-weight:500;display:inline-flex;align-items:center;gap:6px;border:1px solid;${m.current_format === f ? 'border-color:#1E50A5;background:#1E50A5;color:#fff;' : 'border-color:#b4c0d2;background:#ffffff;color:#1b2430;'}`,
}, [icon('play_arrow'), `${qf[f].label} (${qf[f].n})`]))
: [h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine Jobs in der Warteschlange.')]),
])) : [h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine aktive Maschine.')]),
]);
}
function uebersichtInhalt() {
if (loading.value) return h('div', { style: 'color:#566070;' }, 'Lädt …');
const GRUPPEN = [['im_druck', 'Im Druck'], ['kuvertieren', 'Kuvertierung']];
const sek = [];
for (const [st, label] of GRUPPEN) {
const list = gefiltert.value.filter((o) => (o.production_status || 'eingegangen') === st);
if (!list.length) continue;
sek.push(h('div', { key: 'sec' + st, style: 'margin-bottom:22px;' }, [sektionKopf(label, STATUS_META[st], list.length), tabelle(list)]));
}
// Versandfertig: unterteilt in „schnellstmöglich" (ohne Datum) und „mit Datum".
const vf = gefiltert.value.filter((o) => o.production_status === 'versandfertig');
if (vf.length) {
const ohne = vf.filter((o) => !o.versanddatum);
const mit = vf.filter((o) => o.versanddatum).sort((a, b) => String(a.versanddatum).localeCompare(String(b.versanddatum)));
sek.push(h('div', { key: 'secvf', style: 'margin-bottom:22px;' }, [
sektionKopf('Versandfertig', STATUS_META.versandfertig, vf.length),
ohne.length ? h('div', { style: 'margin-bottom:12px;' }, [h('div', { style: 'font-size:13px;font-weight:500;color:#566070;margin-bottom:6px;' }, 'Schnellstmöglich (' + ohne.length + ')'), tabelle(ohne)]) : null,
mit.length ? h('div', {}, [h('div', { style: 'font-size:13px;font-weight:500;color:#566070;margin-bottom:6px;' }, 'Mit Versanddatum (' + mit.length + ')'), tabelle(mit)]) : null,
].filter(Boolean)));
}
return sek.length ? h('div', {}, [maschinenPanel(), ...sek]) : h('div', {}, [maschinenPanel(), h('div', { style: 'padding:28px;text-align:center;color:#566070;border:1px solid #b4c0d2;border-radius:12px;background:#ffffff;' }, 'Keine offenen Aufträge.')]);
}
function sucheInhalt() {
const TYPEN = [['brief', 'Schriftstück', 'description'], ['umschlag', 'Kuvert', 'mail'], ['objekt', 'Druckobjekt', 'attach_file']];
const typWahl = h('div', {}, [
h('div', { style: 'font-size:14px;font-weight:500;margin-bottom:8px;' }, '1. Was möchten Sie?'),
h('div', { style: 'display:flex;gap:10px;flex-wrap:wrap;' }, TYPEN.map(([k, l, ic]) => {
const aktiv = sucheTyp.value === k;
return h('button', { type: 'button', onClick: () => { sucheTyp.value = k; },
style: `cursor:pointer;border-radius:10px;padding:13px 20px;font:inherit;font-size:16px;font-weight:500;display:inline-flex;align-items:center;gap:8px;border:2px solid;${aktiv ? 'background:#1E50A5;color:#fff;border-color:#1E50A5;' : 'background:#ffffff;color:#1b2430;border-color:#b4c0d2;'}` }, [icon(ic), l]);
})),
]);
const feld = h('div', { style: 'margin-top:18px;' }, [
h('div', { style: 'font-size:14px;font-weight:500;margin-bottom:8px;' }, '2. Empfänger suchen'),
h('input', { type: 'search', placeholder: 'Name, Straße, Ort oder Auftragsnummer …',
style: 'width:100%;box-sizing:border-box;background:#ffffff;color:#1b2430;border:2px solid #b4c0d2;border-radius:12px;font:inherit;font-size:22px;padding:16px 20px;outline:none;',
onInput: (e) => sucheSetzen(e.target.value) }),
]);
const rahmen = (kinder) => h('div', {}, [
h('div', { style: 'background:#ffffff;border:1px solid #b4c0d2;border-radius:12px;padding:20px 22px;margin-bottom:16px;' }, [typWahl, feld]),
kinder,
]);
if (!sucheQuery.value.trim()) return rahmen(h('div', { style: 'color:#566070;' }, 'Suchbegriff eingeben – durchsucht die offenen Aufträge.'));
const treffer = sucheTreffer.value;
if (!treffer.length) return rahmen(h('div', { style: 'color:#566070;' }, 'Keine Treffer.'));
const trefferZeile = ({ e, o }) => {
const gesamt = entriesByOrder.value[String(o.id)]?.anzahl || 1;
const adrLines = (e.adr && e.adr.length) ? e.adr : [e.name || '—'];
const kannKuvert = !!(o.needs_envelope && o.envelope_labeling && o.envelope_labeling !== 'keine');
const aktion = sucheTyp.value === 'objekt'
? h('a', { href: `/admin/content/orders/${o.id}`, target: '_blank', rel: 'noopener', style: 'display:inline-flex;align-items:center;gap:6px;background:#1E50A5;color:#fff;border-radius:8px;padding:9px 16px;font-size:14px;font-weight:500;text-decoration:none;' }, [icon('open_in_new'), 'Auftrag öffnen'])
: (sucheTyp.value === 'umschlag' && !kannKuvert)
? h('span', { style: 'color:#566070;font-size:14px;' }, 'Kein Kuvert')
: h('button', { type: 'button', onClick: () => schnelldruck(o, e.nr, sucheTyp.value),
style: 'cursor:pointer;display:inline-flex;align-items:center;gap:6px;background:#1E50A5;color:#fff;border:none;border-radius:8px;padding:10px 18px;font:inherit;font-size:15px;font-weight:500;' }, [icon('print'), 'In Warteschlange']);
return h('div', { key: `s${o.id}-${e.nr}`, style: 'border:1px solid #b4c0d2;border-radius:12px;background:#ffffff;padding:14px 16px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;' }, [
h('div', { style: 'min-width:0;' }, [
...adrLines.map((z, i) => h('div', { style: i === 0 ? 'font-weight:500;font-size:16px;' : 'font-size:14px;color:#1b2430;' }, z)),
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;margin-top:5px;align-items:center;font-size:13px;color:#566070;' }, [
h('span', {}, `Auftrag #${o.order_number}`),
gesamt > 1 ? h('span', { style: 'background:#eef2f8;border-radius:999px;padding:2px 9px;' }, `Nr. ${e.nr} von ${gesamt}`) : null,
h('span', { style: `display:inline-flex;align-items:center;gap:4px;color:${(STATUS_META[o.production_status] || STATUS_META.eingegangen).fg};` }, [icon((STATUS_META[o.production_status] || STATUS_META.eingegangen).icon), STATUS_LABEL[o.production_status] || '']),
]),
]),
aktion,
]);
};
return rahmen(h('div', {}, treffer.map(trefferZeile)));
}
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:#1b2430;'),
}, [icon(ic), label]);
return () => {
const navi = h('div', { style: 'padding:14px 12px;' }, [
navItem('uebersicht', 'Übersicht', 'table_view'),
navItem('suche', 'Suche', 'search'),
]);
const inhalt = h('div', { style: 'padding:24px 28px;' }, [
view.value === 'uebersicht' ? h('div', { style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:18px;' }, [
h('input', { type: 'search', placeholder: 'Auftrag, Kunde, Empfänger, Format …', style: 'flex:1;min-width:240px;box-sizing:border-box;border:1px solid #b4c0d2;border-radius:10px;padding:11px 14px;font:inherit;font-size:15px;background:#ffffff;color:#1b2430;', onInput: (e) => sucheSetzen(e.target.value) }),
h('button', { type: 'button', onClick: () => laden(), style: 'cursor:pointer;border:1px solid #b4c0d2;background:#ffffff;border-radius:8px;padding:9px 16px;font:inherit;font-size:14px;font-weight:500;display:inline-flex;align-items:center;gap:6px;' }, [icon('refresh'), 'Aktualisieren']),
]) : null,
msg.value ? h('div', { style: 'background:#dff2e2;color:#1b6b2e;padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, msg.value) : null,
err.value ? h('div', { style: 'background:#fbe3e0;color:#c0392b;padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, err.value) : null,
view.value === 'suche' ? sucheInhalt() : uebersichtInhalt(),
]);
return h(resolveComponent('private-view'), { title: view.value === 'suche' ? 'Suche' : 'Übersicht' }, { navigation: () => navi, default: () => inhalt });
};
},
};
export default {
id: 'skrift-uebersicht',
name: 'Übersicht',
icon: 'table_view',
routes: [{ path: '', component: Board }],
};