ROOT CAUSE Empfaenger-"-" + tote Suche: order_entries hat KEIN company-Feld. Die Abfrage forderte company an -> 403/FORBIDDEN -> ganzer Request scheiterte -> Empfaenger-Index leer -> "-" ueberall + Suche ohne Daten. company aus allen order_entries-Abfragen entfernt. Echte Empfaenger stehen in free_text (wird gelesen). Farben/Barrierefreiheit: Extensions nutzten alte Directus-10-Variablen (--border-normal etc.), die im aktuellen Theme nicht definiert sind -> blasse/ unsichtbare Rahmen, verschwimmende Farben. Alle unzuverlaessigen Theme-Variablen durch feste, kontraststarke Farben ersetzt (WCAG-AA, helles Weiss-Blau-Theme): sichtbare Rahmen (#b4c0d2), dunklere Labels (#566070), Text (#1b2430). Status- und Ablauf-Pillen mit soliden Farben + Rahmen + kraeftigerer Schrift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
495 lines
35 KiB
JavaScript
495 lines
35 KiB
JavaScript
/**
|
||
* 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(' · '); }
|
||
function empfKurz(e) {
|
||
if (!e) return '';
|
||
const name = e.company || [e.first_name, e.last_name].filter(Boolean).join(' ')
|
||
|| (e.free_text ? String(e.free_text).split('\n').map((z) => z.trim()).filter(Boolean)[0] : '');
|
||
return [name, e.city].filter(Boolean).join(', ');
|
||
}
|
||
function entryName(e) {
|
||
return [e.salutation, e.first_name, e.last_name].filter(Boolean).join(' ')
|
||
|| e.company || (e.free_text ? String(e.free_text).split('\n')[0].trim() : '') || '—';
|
||
}
|
||
|
||
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,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 steps = [
|
||
['Druck Schriftstück', true, !!o.brief_gedruckt],
|
||
['Druck Kuvert', !!o.needs_envelope, !!o.kuvert_gedruckt],
|
||
['Kuvertieren', !!o.needs_envelope, 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 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', needEnv.length > 0, alle(needEnv, 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 }],
|
||
};
|