Board strukturiert + neue Mitarbeiter-Uebersicht (A)
produktion: - Karte in klaren Bloecken: getoenter Kopf (Identitaet + Status), Specs als beschriftete Kacheln, Druckstatus-Leiste (Brief/Kuvert getrennt), beschriftete Notiz, strukturiertes "Weiteres" mit beschrifteten Feldern. Kein linker Farbrand mehr. Groessere Schrift. Suche aus dem Board entfernt. Neue Extension skrift-uebersicht (Modul, read-only fuer Mitarbeiter): - Kompakte Tabelle: Auftrag/Name, erster+letzter Empfaenger, Format, faellig, Druck Brief/Kuvert getrennt, eingeplant, Status. Suche/Filter. - Maschinen-Panel: "Format einlegen -> starten" (setzt current_format). access: Staff sieht jetzt nur noch skrift-uebersicht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,15 +2,14 @@
|
||||
* Skrift – Zugriffsbeschränkung Modulleiste
|
||||
* ---------------------------------------------------------------------------
|
||||
* Directus 11 kennt keine rollenbasierte Modulleiste (nur global). Dieser
|
||||
* settings.read-Filter blendet für NICHT-Admins (= Rolle Staff im Admin) alle
|
||||
* settings.read-Filter blendet für NICHT-Admins (= Rolle Staff/Mitarbeiter) alle
|
||||
* Module aus außer:
|
||||
* - skrift-produktion (Produktions-Dashboard)
|
||||
* - skrift-neuauftrag (Neuer Auftrag)
|
||||
* - skrift-uebersicht (Mitarbeiter-Übersicht + Suche; read-only)
|
||||
*
|
||||
* Admins bleiben unberührt. Kunden nutzen die separate App (app.skrift.de),
|
||||
* nicht die Directus-Admin-Oberfläche – daher reicht der Admin-Check.
|
||||
*/
|
||||
const ERLAUBT = ['skrift-produktion', 'skrift-neuauftrag'];
|
||||
const ERLAUBT = ['skrift-uebersicht'];
|
||||
|
||||
function nurProduktion(bar) {
|
||||
const vorhanden = (Array.isArray(bar) ? bar : [])
|
||||
|
||||
@@ -53,7 +53,6 @@ function faelligBis(dateCreated, shippingDay) {
|
||||
const dmY = (dt) => dt ? `${String(dt.getDate()).padStart(2, '0')}.${String(dt.getMonth() + 1).padStart(2, '0')}.${dt.getFullYear()}` : '';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'suche', label: 'Suche', icon: 'search' },
|
||||
{ key: 'neu', label: 'Neu', icon: 'inbox' },
|
||||
{ key: 'warteschlange', label: 'Warteschlange', icon: 'hourglass_empty' },
|
||||
{ key: 'druck', label: 'Im Druck', icon: 'print' },
|
||||
@@ -128,11 +127,22 @@ function subdued(text, extra) {
|
||||
function metaItem(text) {
|
||||
return h('span', { style: 'display:inline-flex;align-items:center;gap:5px;color:var(--foreground-subdued);font-size:13.5px;white-space:nowrap;' }, text);
|
||||
}
|
||||
// Editierbares Notizfeld (speichert beim Verlassen/Blur).
|
||||
// Beschriftete Eckdaten-Kachel (Label oben, Wert darunter) – klare Struktur.
|
||||
function tile(labelTxt, value, gross) {
|
||||
return h('div', { style: 'background:var(--background-subdued,#f1f4f9);border-radius:8px;padding:9px 12px;min-width:0;' }, [
|
||||
h('div', { style: 'font-size:11.5px;color:var(--foreground-subdued);' }, labelTxt),
|
||||
h('div', { style: `font-weight:500;overflow:hidden;text-overflow:ellipsis;${gross ? 'font-size:20px;line-height:1.1;margin-top:2px;' : 'font-size:15px;margin-top:3px;'}` }, value),
|
||||
]);
|
||||
}
|
||||
// Feld-Beschriftung über einem Eingabefeld.
|
||||
function feldLabel(t) {
|
||||
return h('label', { style: 'display:block;font-size:13px;font-weight:500;color:var(--foreground-subdued);margin-bottom:5px;' }, t);
|
||||
}
|
||||
// Editierbares Notizfeld (speichert beim Verlassen/Blur) – klar sichtbares Feld.
|
||||
function notizFeld(wert, onSave, ph) {
|
||||
return h('textarea', {
|
||||
rows: 2, placeholder: ph || 'Notiz …',
|
||||
style: 'width:100%;margin:6px 0 2px;border:1px solid var(--border-subdued);border-radius:6px;padding:6px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);resize:vertical;',
|
||||
style: 'width:100%;box-sizing:border-box;border:1px solid var(--border-normal);border-radius:8px;padding:9px 11px;font:inherit;font-size:14px;background:var(--background-page);color:var(--foreground-normal);resize:vertical;min-height:44px;',
|
||||
value: wert || '',
|
||||
onChange: (e) => onSave(e.target.value),
|
||||
}, wert || '');
|
||||
@@ -1359,34 +1369,38 @@ const Board = defineComponent({
|
||||
function weiteresPanel(o) {
|
||||
const dz = druckIndex.value[String(o.id)];
|
||||
const nm = empfNamen.value[String(o.id)];
|
||||
return h('div', { style: 'margin-top:10px;padding:12px;border:1px solid var(--border-subdued);border-radius:8px;background:var(--background-subdued);display:flex;flex-direction:column;gap:10px;' }, [
|
||||
// Druckdatum + Erster/Letzter Empfänger – zum Wiederfinden/Auseinandersortieren.
|
||||
h('div', { style: 'font-size:12px;color:var(--foreground-normal);line-height:1.6;' }, [
|
||||
h('div', {}, dz ? `In den Druck: ${dmYhm(dz.erste)}${dz.letzte && dz.letzte.slice(0, 16) !== dz.erste.slice(0, 16) ? ' – ' + dmYhm(dz.letzte) : ''}` : 'Noch nicht gedruckt.'),
|
||||
nm ? h('div', {}, `Erster: ${nm.erste}${nm.anzahl > 1 ? ` · Letzter: ${nm.letzte}` : ''}`) : null,
|
||||
]),
|
||||
h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;' }, [
|
||||
subdued('Verknüpfter Auftrag:', ''),
|
||||
h('input', { placeholder: 'Auftragsnr. (leer = keine)', value: o.linked_order?.order_number || '',
|
||||
style: 'flex:1;min-width:140px;max-width:220px;border:1px solid var(--border-subdued);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);',
|
||||
onChange: (e) => setOrderLink(o, e.target.value) }),
|
||||
]),
|
||||
// Kontakt/Kunde verknüpfen (per E-Mail) oder mit „×" lösen. Keine Kundenmail.
|
||||
h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;' }, [
|
||||
subdued('Kontakt:', ''),
|
||||
o.customer ? h('span', { style: 'font-size:12px;' }, `${kundeName(o)}${o.customer.email ? ' · ' + o.customer.email : ''}`) : null,
|
||||
o.customer
|
||||
? h('button', { type: 'button', title: 'Kontakt lösen', onClick: () => setOrderCustomer(o, ''),
|
||||
style: 'cursor:pointer;border:1px solid var(--border-subdued);background:var(--background-page);border-radius:6px;padding:3px 9px;font:inherit;font-size:13px;color:var(--danger,#c62828);' }, '×')
|
||||
: h('input', { placeholder: 'E-Mail verknüpfen …',
|
||||
style: 'flex:1;min-width:160px;max-width:240px;border:1px solid var(--border-subdued);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px;background:var(--background-page);color:var(--foreground-normal);',
|
||||
onChange: (e) => setOrderCustomer(o, e.target.value) }),
|
||||
]),
|
||||
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;' }, [
|
||||
btn(o.keep_contacts ? 'In Kontakte gespeichert' : 'In Kontakte speichern',
|
||||
() => toggleKeepContacts(o), o.keep_contacts ? 'primary' : 'default', o.keep_contacts ? 'how_to_reg' : 'contacts'),
|
||||
btn('Neu generieren', () => regenerieren(o.id), 'danger', 'autorenew'),
|
||||
btn('In manuellen Druck übernehmen', () => uebernehmenManuell(o), 'primary', 'edit_note'),
|
||||
const feld = 'width:100%;box-sizing:border-box;border:1px solid var(--border-normal);border-radius:8px;padding:9px 11px;font:inherit;font-size:14px;background:var(--background-page);color:var(--foreground-normal);';
|
||||
return h('div', { style: 'border:1px solid var(--border-normal);border-radius:10px;overflow:hidden;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:var(--foreground-subdued);padding:9px 14px;border-bottom:1px solid var(--border-subdued);background:var(--background-subdued,#f6f8fb);' }, [icon('tune'), 'Weiteres']),
|
||||
h('div', { style: 'padding:14px;display:flex;flex-direction:column;gap:14px;' }, [
|
||||
// Druckverlauf: wann gedruckt + erster/letzter Empfänger.
|
||||
h('div', {}, [
|
||||
h('div', { style: 'font-size:11.5px;font-weight:500;color:var(--foreground-subdued);text-transform:uppercase;letter-spacing:.03em;margin-bottom:5px;' }, 'Druckverlauf'),
|
||||
h('div', { style: 'font-size:14px;line-height:1.6;' }, [
|
||||
h('div', {}, dz ? `In den Druck: ${dmYhm(dz.erste)}${dz.letzte && dz.letzte.slice(0, 16) !== dz.erste.slice(0, 16) ? ' – ' + dmYhm(dz.letzte) : ''}` : 'Noch nicht gedruckt.'),
|
||||
nm ? h('div', { style: 'color:var(--foreground-subdued);' }, `Erster: ${nm.erste}${nm.anzahl > 1 ? ` · Letzter: ${nm.letzte}` : ''}`) : null,
|
||||
]),
|
||||
]),
|
||||
// Verknüpfungen: beschriftete Felder.
|
||||
h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;' }, [
|
||||
h('div', {}, [feldLabel('Verknüpfter Auftrag'),
|
||||
h('input', { placeholder: 'Auftragsnr. (leer = keine)', value: o.linked_order?.order_number || '', style: feld, onChange: (e) => setOrderLink(o, e.target.value) })]),
|
||||
h('div', {}, [feldLabel('Kunde verknüpfen (E-Mail)'),
|
||||
o.customer
|
||||
? h('div', { style: 'display:flex;align-items:center;gap:8px;' }, [
|
||||
h('span', { style: 'font-size:14px;' }, `${kundeName(o)}${o.customer.email ? ' · ' + o.customer.email : ''}`),
|
||||
h('button', { type: 'button', title: 'Kontakt lösen', onClick: () => setOrderCustomer(o, ''), style: 'cursor:pointer;border:1px solid var(--border-normal);background:var(--background-page);border-radius:8px;padding:5px 10px;font:inherit;font-size:13px;color:var(--danger,#c62828);' }, 'lösen'),
|
||||
])
|
||||
: h('input', { placeholder: 'kunde@example.de', style: feld, onChange: (e) => setOrderCustomer(o, e.target.value) }),
|
||||
]),
|
||||
]),
|
||||
// Aktionen (abgetrennt).
|
||||
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;border-top:1px solid var(--border-subdued);padding-top:14px;' }, [
|
||||
btn(o.keep_contacts ? 'In Kontakte gespeichert' : 'In Kontakte speichern',
|
||||
() => toggleKeepContacts(o), o.keep_contacts ? 'primary' : 'default', o.keep_contacts ? 'how_to_reg' : 'contacts'),
|
||||
btn('Neu generieren', () => regenerieren(o.id), 'danger', 'autorenew'),
|
||||
btn('In manuellen Druck übernehmen', () => uebernehmenManuell(o), 'primary', 'edit_note'),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -1417,40 +1431,40 @@ const Board = defineComponent({
|
||||
const o = order;
|
||||
const opts2 = opts || {};
|
||||
const bezahlt = o.payment_status === 'bezahlt';
|
||||
// Kopf: Name = Hero, darunter Anzahl Schriftstücke groß + Eckdaten. Rechts: Status-Pill.
|
||||
const spec = [
|
||||
o.needs_envelope ? `${o.entries_count || 0} Kuverts` : 'ohne Kuvert',
|
||||
o.format?.name || o.format?.key || 'Format?',
|
||||
o.shipping_type === 'einzeln' ? 'Einzelversand' : 'Sammelversand',
|
||||
].join(' · ');
|
||||
const kopf = h('div', { style: 'display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;' }, [
|
||||
const identName = kundeName(o);
|
||||
// ── KOPF (getönt): Auswahl/Griff + Identität + Status ──────────────────
|
||||
const kopf = h('div', { style: 'display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;padding:14px 16px;background:var(--background-subdued,#f6f8fb);border-bottom:1px solid var(--border-subdued);' }, [
|
||||
h('div', { style: 'display:flex;gap:12px;align-items:flex-start;min-width:0;flex:1;' }, [
|
||||
opts2.dragHandle
|
||||
? h('span', { style: 'color:var(--foreground-subdued);cursor:grab;margin-top:5px;flex-shrink:0;', title: 'Ziehen zum Sortieren' }, icon('drag_indicator'))
|
||||
? h('span', { style: 'color:var(--foreground-subdued);cursor:grab;margin-top:3px;flex-shrink:0;', title: 'Ziehen zum Sortieren' }, icon('drag_indicator'))
|
||||
: h('input', { type: 'checkbox', checked: istGewaehlt(o.id), onClick: (e) => toggleAuswahl(o.id, e),
|
||||
title: 'Für Sammel-Druck auswählen', style: 'width:18px;height:18px;margin-top:5px;cursor:pointer;flex-shrink:0;' }),
|
||||
title: 'Für Sammel-Druck auswählen', style: 'width:19px;height:19px;margin-top:3px;cursor:pointer;flex-shrink:0;' }),
|
||||
h('div', { style: 'min-width:0;' }, [
|
||||
o.order_name ? h('div', { style: 'font-size:12px;font-weight:500;color:var(--primary,#1e50a5);text-transform:uppercase;letter-spacing:.03em;margin-bottom:1px;' }, o.order_name) : null,
|
||||
h('div', { style: 'font-size:19px;font-weight:500;letter-spacing:-.01em;line-height:1.25;' }, kundeName(o) || `#${o.order_number}`),
|
||||
h('div', { style: 'display:flex;align-items:baseline;gap:8px;margin-top:4px;flex-wrap:wrap;' }, [
|
||||
h('span', { style: `font-size:22px;font-weight:500;color:${ACCENT};line-height:1;` }, String(o.entries_count || 0)),
|
||||
h('span', { style: 'font-size:14px;color:var(--foreground-subdued);' }, 'Schriftstücke · ' + spec),
|
||||
]),
|
||||
h('div', { style: 'font-size:20px;font-weight:500;letter-spacing:-.01em;line-height:1.2;' }, identName || `#${o.order_number}`),
|
||||
h('div', { style: 'font-size:14px;color:var(--foreground-subdued);margin-top:2px;' }, (identName ? `#${o.order_number} · ` : '') + `${o.entries_count || 0} Empfänger`),
|
||||
]),
|
||||
]),
|
||||
statusSelect(o),
|
||||
]);
|
||||
// Druckfortschritt: klar sehen, was/wann gedruckt wurde.
|
||||
// ── SPECS-Kacheln ──────────────────────────────────────────────────────
|
||||
const specs = h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;' }, [
|
||||
tile('Schriftstücke', String(o.entries_count || 0), true),
|
||||
tile('Kuverts', o.needs_envelope ? String(o.entries_count || 0) : 'ohne', true),
|
||||
tile('Format', o.format?.name || o.format?.key || '—'),
|
||||
tile('Versand', o.shipping_type === 'einzeln' ? 'Einzeln' : 'Sammel'),
|
||||
]);
|
||||
// ── DRUCKSTATUS-Leiste (Brief/Kuvert getrennt) ─────────────────────────
|
||||
const send = sendIndex.value[String(o.id)];
|
||||
const hatDruck = send && (send.brief.length || send.umschlag.length);
|
||||
const druckZeile = h('div', { style: 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:12px 0;padding:10px 0;border-top:1px solid var(--border-subdued);border-bottom:1px solid var(--border-subdued);font-size:13.5px;' },
|
||||
hatDruck ? [
|
||||
h('span', { style: 'display:inline-flex;align-items:center;gap:5px;color:var(--success,#2e7d32);font-weight:500;' }, [icon('check_circle'), 'gedruckt']),
|
||||
send.brief.length ? h('span', { style: 'color:var(--foreground-subdued);' }, 'Brief: ' + send.brief.join(' · ')) : null,
|
||||
send.umschlag.length ? h('span', { style: 'color:var(--foreground-subdued);' }, '· Kuvert: ' + send.umschlag.join(' · ')) : null,
|
||||
].filter(Boolean)
|
||||
: [h('span', { style: 'display:inline-flex;align-items:center;gap:5px;color:var(--foreground-subdued);' }, [icon('print_disabled'), 'Noch nicht gedruckt'])]);
|
||||
// Warn-/Hinweis-Pills: nur Aussagekräftiges (Motiv, Freigabe, verknüpfte Kuverts).
|
||||
const briefOk = !!o.brief_gedruckt || !!(send && send.brief.length);
|
||||
const kuvOk = !!o.kuvert_gedruckt || !!(send && send.umschlag.length);
|
||||
const statusWort = (ok, ranges) => h('span', { style: `font-weight:500;color:${ok ? 'var(--success,#2e7d32)' : 'var(--danger,#c62828)'};` },
|
||||
ok ? ('gedruckt' + (ranges && ranges.length ? ' · ' + ranges.join(', ') : '')) : 'nicht gedruckt');
|
||||
const druckStrip = h('div', { style: 'display:flex;gap:20px;flex-wrap:wrap;padding:10px 12px;background:var(--background-subdued,#f6f8fb);border-radius:8px;font-size:14px;' }, [
|
||||
h('span', { style: 'display:inline-flex;align-items:center;gap:6px;' }, [icon('description'), 'Brief: ', statusWort(briefOk, send && send.brief)]),
|
||||
o.needs_envelope ? h('span', { style: 'display:inline-flex;align-items:center;gap:6px;' }, [icon('mail'), 'Kuvert: ', statusWort(kuvOk, send && send.umschlag)]) : null,
|
||||
].filter(Boolean));
|
||||
// ── Warn-/Hinweis-Pills (nur Aussagekräftiges) ─────────────────────────
|
||||
const pills = [
|
||||
(o.motif?.name || (o.motif_mode && !['ohne', ''].includes(o.motif_mode)))
|
||||
? chip('Motiv' + (o.motif?.name ? ': ' + o.motif.name : (o.motif_mode === 'eigene_karten' ? ': eigene Karten' : (o.motif_mode === 'upload' ? ': Upload' : ''))), 'rgba(230,160,0,.18)', '#8a6d00')
|
||||
@@ -1460,14 +1474,14 @@ const Board = defineComponent({
|
||||
o.approval_status === 'approved' ? chip('Motiv freigegeben', 'rgba(46,125,50,.14)', 'var(--success,#2e7d32)') : null,
|
||||
o.approval_status === 'changes' ? chip('Änderungswunsch', 'rgba(198,40,40,.14)', 'var(--danger,#c62828)') : null,
|
||||
].filter(Boolean);
|
||||
const pillRow = pills.length ? h('div', { style: 'display:flex;gap:7px;flex-wrap:wrap;margin-bottom:10px;' }, pills) : null;
|
||||
// Sammelversand: Lieferadresse direkt auf der Karte.
|
||||
const pillRow = pills.length ? h('div', { style: 'display:flex;gap:7px;flex-wrap:wrap;' }, pills) : null;
|
||||
// ── Sammelversand: Lieferadresse ───────────────────────────────────────
|
||||
const liefer = (o.shipping_type !== 'einzeln') ? lieferAdresse(o) : null;
|
||||
const lieferBox = liefer ? h('div', { style: 'margin:0 0 10px;padding:11px 13px;background:var(--background-subdued,#f1f4f9);border-radius:10px;font-size:13.5px;line-height:1.55;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:5px;font-weight:500;color:var(--foreground-subdued);font-size:11px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px;' }, [icon('local_shipping', { small: false, style: 'font-size:15px;' }), 'Versand an · ' + liefer.quelle]),
|
||||
const lieferBox = liefer ? h('div', { style: 'padding:11px 13px;background:var(--background-subdued,#f6f8fb);border-radius:8px;font-size:14px;line-height:1.55;' }, [
|
||||
h('div', { style: 'display:flex;align-items:center;gap:5px;font-weight:500;color:var(--foreground-subdued);font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px;' }, [icon('local_shipping'), 'Versand an · ' + liefer.quelle]),
|
||||
...liefer.zeilen.map((z) => h('div', {}, z)),
|
||||
]) : null;
|
||||
// Subzeile: kleine, ruhige Sekundär-Infos (Nummer, Zahlung, Versandtag, Link).
|
||||
// ── Fußzeile: Nummer · Zahlung · Versandtag · Link ─────────────────────
|
||||
const sub = [
|
||||
h('span', { style: 'font-variant-numeric:tabular-nums;' }, '#' + o.order_number),
|
||||
h('span', { style: `color:${bezahlt ? 'var(--success,#2e7d32)' : '#8a6d00'};font-weight:500;` }, bezahlt ? 'bezahlt' : (o.payment_status === 'storniert' ? 'storniert' : 'offen')),
|
||||
@@ -1475,14 +1489,17 @@ const Board = defineComponent({
|
||||
o.shipping_day ? h('span', {}, 'Versandtag ' + o.shipping_day.charAt(0).toUpperCase() + o.shipping_day.slice(1)) : null,
|
||||
h('a', { href: `/admin/content/orders/${o.id}`, target: '_blank', rel: 'noopener', style: 'display:inline-flex;align-items:center;gap:3px;color:var(--primary,#1e50a5);text-decoration:none;font-weight:500;' }, [icon('open_in_new'), 'Auftrag öffnen']),
|
||||
].filter(Boolean);
|
||||
const subline = h('div', { style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:12.5px;color:var(--foreground-subdued);margin-bottom:12px;' },
|
||||
const subline = h('div', { style: 'display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:13px;color:var(--foreground-subdued);' },
|
||||
sub.reduce((acc, el, i) => (i === 0 ? [el] : acc.concat(h('span', { style: 'color:var(--border-normal);', 'aria-hidden': 'true' }, '·'), el)), []));
|
||||
return h('div', { key: 'o' + o.id, style: CARD + `margin-bottom:14px;border-left:3px solid ${(STATUS_META[o.production_status] || STATUS_META.eingegangen).fg};` }, [
|
||||
kopf, druckZeile, pillRow, lieferBox, subline,
|
||||
notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz zum Auftrag …'),
|
||||
// ── BODY ───────────────────────────────────────────────────────────────
|
||||
const body = h('div', { style: 'padding:14px 16px;display:flex;flex-direction:column;gap:14px;' }, [
|
||||
specs, druckStrip, pillRow, lieferBox,
|
||||
h('div', {}, [feldLabel('Notiz zum Auftrag'), notizFeld(o.internal_note, (v) => setOrderNotiz(o, v), 'Notiz eingeben …')]),
|
||||
actionButtons(o),
|
||||
weiteresOffen.value[o.id] ? weiteresPanel(o) : null,
|
||||
]);
|
||||
subline,
|
||||
].filter(Boolean));
|
||||
return h('div', { key: 'o' + o.id, style: 'border:1px solid var(--border-normal);border-radius:12px;overflow:hidden;background:var(--background-page,#fff);margin-bottom:14px;' }, [kopf, body]);
|
||||
}
|
||||
function requestCard(req) {
|
||||
const istMuster = req.type === 'muster';
|
||||
@@ -1507,7 +1524,7 @@ const Board = defineComponent({
|
||||
istMuster ? btn('In manuellen Druck', () => musterUebernehmen(req), 'primary', 'edit_note') : null,
|
||||
btn('Details', () => openReqInfo(req), 'default', 'person'),
|
||||
]);
|
||||
return h('div', { key: 'r' + req.id, style: CARD + `margin-bottom:14px;border-left:3px solid ${akzent};` }, [kopf, metaRow, nachricht, aktionen]);
|
||||
return h('div', { key: 'r' + req.id, style: 'border:1px solid var(--border-normal);border-radius:12px;background:var(--background-page,#fff);padding:16px 18px;margin-bottom:14px;' }, [kopf, metaRow, nachricht, aktionen]);
|
||||
}
|
||||
|
||||
// Charge-Karte: fasst API-Aufträge (Kunde+Typ+Versandtag+Eingangstag) zusammen.
|
||||
@@ -1532,7 +1549,7 @@ const Board = defineComponent({
|
||||
]);
|
||||
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 …');
|
||||
return h('div', { key: 'c' + c.key, style: CARD + `margin-bottom:14px;border-left:4px solid ${ACCENT};` }, [kopf, chargeNotiz, body]);
|
||||
return h('div', { key: 'c' + c.key, style: 'border:1px solid var(--border-normal);border-radius:12px;background:var(--background-page,#fff);padding:16px 18px;margin-bottom:14px;' }, [kopf, chargeNotiz, body]);
|
||||
}
|
||||
|
||||
// Aufträge einer Ansicht rendern: bei aktiver Suche als EINZELaufträge (damit
|
||||
|
||||
239
extensions/directus-extension-skrift-uebersicht/dist/index.js
vendored
Normal file
239
extensions/directus-extension-skrift-uebersicht/dist/index.js
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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 = {};
|
||||
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,first_name,last_name,company,city,letter_number,free_text',
|
||||
} });
|
||||
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) { 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);
|
||||
|
||||
function zeile(o) {
|
||||
const e = entriesByOrder.value[String(o.id)];
|
||||
const faellig = faelligBis(o.date_created, o.shipping_day);
|
||||
const empf = e
|
||||
? h('div', { style: 'line-height:1.5;' }, [
|
||||
h('div', {}, empfKurz(e.erster) || '—'),
|
||||
e.anzahl > 1 ? h('div', { style: 'color:var(--foreground-subdued);' }, '… ' + empfKurz(e.letzter)) : null,
|
||||
])
|
||||
: h('span', { style: 'color:var(--foreground-subdued);' }, '—');
|
||||
const briefOk = !!o.brief_gedruckt;
|
||||
const kuvOk = !!o.kuvert_gedruckt;
|
||||
return h('tr', { key: 'o' + o.id }, [
|
||||
td(h('div', { style: 'line-height:1.4;' }, [
|
||||
o.order_name ? h('div', { style: 'font-weight:500;color:var(--primary,#1e50a5);' }, o.order_name) : null,
|
||||
h('div', { style: 'font-weight:500;' }, kundeName(o) || `#${o.order_number}`),
|
||||
h('div', { style: 'font-size:12.5px;color:var(--foreground-subdued);' }, `#${o.order_number} · ${o.entries_count || 0} Empf.`),
|
||||
])),
|
||||
td(empf),
|
||||
td(h('span', {}, o.format?.name || FORMAT_LABEL[o.format?.key] || o.format?.key || '—')),
|
||||
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(briefOk, 'Brief'),
|
||||
o.needs_envelope ? druckBadge(kuvOk, 'Kuvert') : h('span', { style: 'font-size:12px;color:var(--foreground-subdued);' }, 'ohne Kuvert'),
|
||||
])),
|
||||
td(eingeplantSet.value.has(String(o.id))
|
||||
? h('span', { style: 'display:inline-flex;align-items:center;gap:4px;font-size:12.5px;font-weight:500;color:var(--primary,#1e50a5);' }, [icon('check'), 'eingeplant'])
|
||||
: h('span', { style: 'font-size:12.5px;color:var(--foreground-subdued);' }, 'nein')),
|
||||
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 …')
|
||||
: 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;' }, [
|
||||
h('thead', {}, h('tr', {}, [th('Auftrag'), th('Empfänger (erster · letzter)'), th('Format'), th('Fällig'), th('Druck'), th('Eingeplant'), th('Status')])),
|
||||
h('tbody', {}, gefiltert.value.length ? gefiltert.value.map(zeile) : [h('tr', {}, h('td', { colspan: 7, style: 'padding:24px;text-align:center;color:var(--foreground-subdued);' }, 'Keine Aufträge.'))]),
|
||||
]),
|
||||
]),
|
||||
h('div', { style: 'margin-top:10px;font-size:12.5px;color:var(--foreground-subdued);' }, `${gefiltert.value.length} von ${orders.value.length} offenen Aufträgen`),
|
||||
]);
|
||||
return h(resolveComponent('private-view'), { title: 'Übersicht' }, { default: () => inhalt });
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
id: 'skrift-uebersicht',
|
||||
name: 'Übersicht',
|
||||
icon: 'table_view',
|
||||
routes: [{ path: '', component: Board }],
|
||||
};
|
||||
13
extensions/directus-extension-skrift-uebersicht/package.json
Normal file
13
extensions/directus-extension-skrift-uebersicht/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "directus-extension-skrift-uebersicht",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Modul: Mitarbeiter-Übersicht (read-only) + Suche. Zeigt Aufträge kompakt (Empfänger, Status, Druck Brief/Kuvert, eingeplant, fällig); Mitarbeiter können Format einlegen/starten, Status setzen, Vorschau sehen.",
|
||||
"directus:extension": {
|
||||
"type": "module",
|
||||
"path": "dist/index.js",
|
||||
"source": "dist/index.js",
|
||||
"host": "^11.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user