Files
skrift-directus/extensions/directus-extension-skrift-uebersicht/dist/index.js
s4luorth d2559de22c Uebersicht: Empfaenger als Hauptinfo, Status-getrennte Tabellen, Aufgabe, Versand-Toggle
- Empfaenger (erster/letzter) ist die fuehrende Spalte; Ladefehler behoben
  (order _in statt Relationsfilter). Auftrag/Kunde sekundaer.
- Je Status eine eigene Tabelle (Im Druck / Kuvertierung / Versandfertig /
  Eingegangen).
- Neue Spalte "Aufgabe" (kuvertieren/frankieren/Versandart aus Status abgeleitet).
- Versandart pro Auftrag umschaltbar (Sammel/Einzel).
- Maschinen-Panel zaehlt nur WARTENDE Jobs (nicht "im Druck").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-01 11:08:29 +02:00

277 lines
18 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, read-only)
* ===========================================================================
* Kompakte Tabelle aller offenen Aufträge – ohne Blättern direkt ersichtlich:
* Auftrag (Name/Nummer) · erster+letzter Empfänger · Format · fällig ·
* Status · Druck (Brief/Kuvert getrennt) · eingeplant.
* Mitarbeiter planen NICHT, dürfen aber: Status setzen, „Format liegt ein →
* Warteschlange starten" (setzt das Maschinenformat) und suchen/filtern.
*
* 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: 'var(--background-subdued,#eef1f6)', fg: 'var(--foreground-subdued)', icon: 'inbox' },
im_druck: { bg: 'rgba(30,80,165,.12)', fg: 'var(--primary,#1e50a5)', icon: 'print' },
kuvertieren: { bg: 'rgba(106,63,208,.12)', fg: '#6a3fd0', icon: 'mail' },
versandfertig: { bg: 'rgba(230,160,0,.16)', fg: '#8a6d00', icon: 'inventory_2' },
versendet: { bg: 'rgba(46,125,50,.14)', fg: 'var(--success,#2e7d32)', icon: 'local_shipping' },
storniert: { bg: 'rgba(198,40,40,.12)', fg: 'var(--danger,#c62828)', icon: 'cancel' },
};
// Aktiv = noch nicht abgeschlossen.
const AKTIV_STATUS = ['eingegangen', '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' };
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 orders = ref([]);
const jobs = ref([]);
const machines = ref([]);
const entriesByOrder = ref({});
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; }, 250); };
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(', ');
}
async function laden(leise = false) {
if (!leise) { loading.value = true; err.value = ''; }
try {
const [o, j, m] = 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,'
+ 'format.key,format.name,shipping_day,date_created,entries_count,'
+ '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' } }).catch(() => ({ data: { data: [] } })),
]);
orders.value = o.data?.data || [];
jobs.value = j.data?.data || [];
machines.value = m.data?.data || [];
// Erster/letzter Empfänger je Auftrag (für die Adressspalte).
const idx = {};
const ids = orders.value.map((x) => x.id);
try {
const ent = ids.length ? await api.get('/items/order_entries', { params: {
limit: -1, sort: 'letter_number',
filter: { order: { _in: ids } },
fields: 'order,first_name,last_name,company,city,letter_number,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; }
}
} catch { /* Empfänger optional */ }
entriesByOrder.value = idx;
} catch (e) {
if (!leise) err.value = e?.response?.data?.errors?.[0]?.message || e?.message || 'Laden fehlgeschlagen.';
} finally { if (!leise) loading.value = false; }
}
onMounted(laden);
let refreshTimer = null;
onMounted(() => { refreshTimer = setInterval(() => laden(true), 20000); });
onUnmounted(() => clearInterval(refreshTimer));
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; // nur wartende Jobs (nicht „im Druck")
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)];
const hay = [o.order_number, o.order_name, kundeName(o), empfKurz(e?.erster), empfKurz(e?.letzter), o.format?.name].filter(Boolean).join(' ').toLowerCase();
return hay.includes(q);
});
return list;
});
function flash(t) { msg.value = t; setTimeout(() => { if (msg.value === t) msg.value = ''; }, 4000); }
async function setStatus(o, v) {
try { await api.patch('/items/orders/' + o.id, { production_status: v }); o.production_status = v; }
catch (e) { err.value = 'Status konnte nicht gesetzt werden.'; }
}
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.'; }
}
// ── Render-Helfer ─────────────────────────────────────────────────────────
const statusPill = (o) => {
const k = o.production_status || '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) },
Object.entries(STATUS_LABEL).map(([kk, v]) => h('option', { value: kk, selected: k === kk }, 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);
// Nächste konkrete Aufgabe für den Mitarbeiter (aus Status + Kuvert + Versandart).
function aufgabe(o) {
const einzeln = o.shipping_type === 'einzeln';
switch (o.production_status) {
case 'eingegangen': return { t: 'Wartet auf Druck', c: 'var(--foreground-subdued)', i: 'hourglass_empty' };
case 'im_druck': return { t: 'Im Druck', c: 'var(--primary,#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: 'var(--foreground-subdued)', i: 'info' };
}
}
async function setVersand(o, v) {
try { await api.patch('/items/orders/' + o.id, { shipping_type: v }); o.shipping_type = v; }
catch (e) { err.value = 'Versandart konnte nicht gesetzt werden.'; }
}
function versandToggle(o) {
const einzeln = o.shipping_type === 'einzeln';
return h('button', { type: 'button', title: 'Umschalten Sammel/Einzel', onClick: () => setVersand(o, einzeln ? 'sammel' : 'einzeln'),
style: 'cursor:pointer;border:1px solid var(--border-normal);background:var(--background-page,#fff);border-radius:999px;padding:4px 11px;font:inherit;font-size:12.5px;font-weight:500;display:inline-flex;align-items:center;gap:5px;' },
[icon(einzeln ? 'person' : 'groups'), einzeln ? 'Einzeln' : 'Sammel']);
}
function zeile(o) {
const e = entriesByOrder.value[String(o.id)];
const faellig = faelligBis(o.date_created, o.shipping_day);
const ag = aufgabe(o);
const eingeplant = eingeplantSet.value.has(String(o.id));
return h('tr', { key: 'o' + o.id }, [
// Empfänger = Hauptinfo.
td(h('div', { style: 'line-height:1.45;' }, [
h('div', { style: 'font-weight:500;' }, empfKurz(e?.erster) || '—'),
(e && e.anzahl > 1) ? h('div', { style: 'font-size:12.5px;color:var(--foreground-subdued);' }, '… ' + empfKurz(e.letzter)) : null,
])),
// Auftrag = sekundär.
td(h('div', { style: 'line-height:1.4;font-size:13px;' }, [
o.order_name ? h('div', { style: 'font-weight:500;color:var(--primary,#1e50a5);' }, o.order_name) : null,
h('div', {}, kundeName(o) || `#${o.order_number}`),
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(h('span', { style: `display:inline-flex;align-items:center;gap:5px;font-weight:500;color:${ag.c};` }, [icon(ag.i), ag.t])),
td(versandToggle(o)),
td(h('span', { style: faellig && faellig.getTime() < Date.now() ? 'color:var(--danger,#c62828);font-weight:500;' : '' }, dmY(faellig))),
td(h('div', { style: 'display:flex;flex-direction:column;gap:4px;align-items:flex-start;' }, [
druckBadge(!!o.brief_gedruckt, 'Brief'),
o.needs_envelope ? druckBadge(!!o.kuvert_gedruckt, 'Kuvert') : null,
eingeplant ? h('span', { style: 'display:inline-flex;align-items:center;gap:3px;font-size:11.5px;font-weight:500;color:var(--primary,#1e50a5);' }, [icon('check'), 'eingeplant']) : null,
].filter(Boolean))),
td(statusPill(o)),
]);
}
function maschinenPanel() {
const qf = queuedByFormat.value;
const formate = Object.keys(qf);
return h('div', { style: 'border:1px solid var(--border-normal);border-radius:12px;background:var(--background-page,#fff);margin-bottom:18px;overflow:hidden;' }, [
h('div', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:var(--foreground-subdued);padding:10px 14px;border-bottom:1px solid var(--border-subdued);background:var(--background-subdued,#f6f8fb);' }, [icon('print'), 'Maschinen · Format einlegen und starten']),
h('div', { style: 'padding:14px;display:flex;flex-direction:column;gap:14px;' },
machines.value.length ? machines.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, m.current_format ? h('span', { style: 'color:var(--foreground-subdued);font-weight:400;font-size:13px;' }, ` · eingelegt: ${FORMAT_LABEL[m.current_format] || m.current_format}`) : h('span', { style: 'color:var(--foreground-subdued);font-weight:400;font-size:13px;' }, ' · 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:var(--primary,#1e50a5);background:var(--primary,#1e50a5);color:#fff;' : 'border-color:var(--border-normal);background:var(--background-page,#fff);color:var(--foreground-normal);'}`,
}, [icon('play_arrow'), `${qf[f].label} (${qf[f].n})`]))
: [h('span', { style: 'color:var(--foreground-subdued);font-size:13px;' }, 'Keine Jobs in der Warteschlange.')]),
]))
: [h('span', { style: 'color:var(--foreground-subdued);font-size:13px;' }, 'Keine aktive Maschine.')]),
]);
}
return () => {
const inhalt = h('div', { style: 'padding:24px 28px;' }, [
h('div', { style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:18px;' }, [
h('div', { style: 'position:relative;flex:1;min-width:240px;' }, [
h('input', { type: 'search', placeholder: 'Auftrag, Kunde, Empfänger, Format …',
style: 'width:100%;box-sizing:border-box;border:1px solid var(--border-normal);border-radius:10px;padding:11px 14px;font:inherit;font-size:15px;background:var(--background-page);color:var(--foreground-normal);',
onInput: (e) => sucheSetzen(e.target.value) }),
]),
h('button', { type: 'button', onClick: () => laden(), style: 'cursor:pointer;border:1px solid var(--border-normal);background:var(--background-page,#fff);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']),
]),
msg.value ? h('div', { style: 'background:var(--success-25,rgba(46,125,50,.14));color:var(--success,#2e7d32);padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, msg.value) : null,
err.value ? h('div', { style: 'background:var(--danger-25,rgba(198,40,40,.12));color:var(--danger,#c62828);padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, err.value) : null,
maschinenPanel(),
loading.value
? h('div', { style: 'color:var(--foreground-subdued);' }, 'Lädt …')
: (() => {
const GRUPPEN = [['im_druck', 'Im Druck'], ['kuvertieren', 'Kuvertierung'], ['versandfertig', 'Versandfertig'], ['eingegangen', 'Eingegangen']];
const kopf = () => h('thead', {}, h('tr', {}, [th('Empfänger (erster · letzter)'), th('Auftrag'), th('Format'), th('Aufgabe'), th('Versand'), th('Fällig'), th('Stand'), th('Status')]));
const sek = GRUPPEN.map(([st, label]) => {
const list = gefiltert.value.filter((o) => (o.production_status || 'eingegangen') === st);
if (!list.length) return null;
const m = STATUS_META[st];
return h('div', { key: 'sec' + st, style: 'margin-bottom:22px;' }, [
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:var(--foreground-subdued);' }, `${list.length} Auftrag/Aufträge`),
]),
h('div', { style: 'border:1px solid var(--border-normal);border-radius:12px;overflow:auto;background:var(--background-page,#fff);' },
[h('table', { style: 'width:100%;border-collapse:collapse;' }, [kopf(), h('tbody', {}, list.map(zeile))])]),
]);
}).filter(Boolean);
return sek.length ? h('div', {}, sek) : h('div', { style: 'padding:28px;text-align:center;color:var(--foreground-subdued);border:1px solid var(--border-normal);border-radius:12px;background:var(--background-page,#fff);' }, 'Keine offenen Aufträge.');
})(),
]);
return h(resolveComponent('private-view'), { title: 'Übersicht' }, { default: () => inhalt });
};
},
};
export default {
id: 'skrift-uebersicht',
name: 'Übersicht',
icon: 'table_view',
routes: [{ path: '', component: Board }],
};