Mitarbeiter-Uebersicht: Empfaenger ausklappbar + Kommentar; Auftragsnr nur 1x; Board: Versanddatum setzbar

Uebersicht:
- Empfaenger-Zelle: erster Empfaenger + Umschalter "alle N Empfaenger" -> klappt eine
  Detailzeile mit der VOLLEN Empfaengerliste aus (kein gesondertes erster/letzter mehr)
- gleiche Detailzeile hat ein Kommentarfeld je Auftrag (internal_note)
- Auftragsnummer wird nur noch EINMAL angezeigt (keine Doppelung mehr)
- volle Empfaengerliste + internal_note werden mitgeladen

Produktionsboard:
- Versanddatum je Auftrag unter "Weiteres -> Aufgaben umstellen" setzbar (Datum, leer = schnellstmoeglich).
  Mitarbeiter-Uebersicht bleibt bei Versandart/-datum weiterhin nur lesbar (wie gewuenscht).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-10-01 18:03:02 +02:00
parent 850ba38578
commit 5196e30343
2 changed files with 55 additions and 16 deletions

View File

@@ -1405,6 +1405,9 @@ const Board = defineComponent({
h('div', { style: 'font-size:11.5px;font-weight:500;color:#566070;text-transform:uppercase;letter-spacing:.03em;margin-bottom:8px;' }, 'Aufgaben umstellen'), h('div', { style: 'font-size:11.5px;font-weight:500;color:#566070;text-transform:uppercase;letter-spacing:.03em;margin-bottom:8px;' }, 'Aufgaben umstellen'),
h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;' }, [ h('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px;' }, [
h('div', {}, [feldLabel('Versandart'), segToggle(o.shipping_type === 'einzeln' ? 'einzeln' : 'sammel', [{ key: 'einzeln', label: 'Einzelversand' }, { key: 'sammel', label: 'Sammelversand' }], (k) => setOrderFeld(o, { shipping_type: k }))]), h('div', {}, [feldLabel('Versandart'), segToggle(o.shipping_type === 'einzeln' ? 'einzeln' : 'sammel', [{ key: 'einzeln', label: 'Einzelversand' }, { key: 'sammel', label: 'Sammelversand' }], (k) => setOrderFeld(o, { shipping_type: k }))]),
h('div', {}, [feldLabel('Versanddatum (leer = schnellstmöglich)'), h('input', { type: 'date', value: o.versanddatum || '',
style: 'width:100%;box-sizing:border-box;border:1px solid #b4c0d2;border-radius:8px;padding:9px 11px;font:inherit;font-size:14px;background:#ffffff;color:#1b2430;',
onChange: (e) => setOrderFeld(o, { versanddatum: e.target.value || null }) })]),
h('div', {}, [feldLabel('Frankierung'), segToggle(((typeof o.frankiert === 'boolean') ? o.frankiert : (o.shipping_type !== 'sammel')) ? 'ja' : 'nein', [{ key: 'ja', label: 'Frankieren' }, { key: 'nein', label: 'Nicht' }], (k) => setOrderFeld(o, { frankiert: k === 'ja' }))]), h('div', {}, [feldLabel('Frankierung'), segToggle(((typeof o.frankiert === 'boolean') ? o.frankiert : (o.shipping_type !== 'sammel')) ? 'ja' : 'nein', [{ key: 'ja', label: 'Frankieren' }, { key: 'nein', label: 'Nicht' }], (k) => setOrderFeld(o, { frankiert: k === 'ja' }))]),
h('div', {}, [feldLabel('Kuvert schreiben'), segToggle(o.needs_envelope ? 'ja' : 'nein', [{ key: 'ja', label: 'Mit Kuvert' }, { key: 'nein', label: 'Ohne' }], (k) => setOrderFeld(o, { needs_envelope: k === 'ja' }))]), h('div', {}, [feldLabel('Kuvert schreiben'), segToggle(o.needs_envelope ? 'ja' : 'nein', [{ key: 'ja', label: 'Mit Kuvert' }, { key: 'nein', label: 'Ohne' }], (k) => setOrderFeld(o, { needs_envelope: k === 'ja' }))]),
h('div', {}, [feldLabel('Blatt einlegen (kuvertieren)'), segToggle((o.kuvertieren !== false) ? 'ja' : 'nein', [{ key: 'ja', label: 'Blatt rein' }, { key: 'nein', label: 'Nur Kuvert' }], (k) => setOrderFeld(o, { kuvertieren: k === 'ja' }))]), h('div', {}, [feldLabel('Blatt einlegen (kuvertieren)'), segToggle((o.kuvertieren !== false) ? 'ja' : 'nein', [{ key: 'ja', label: 'Blatt rein' }, { key: 'nein', label: 'Nur Kuvert' }], (k) => setOrderFeld(o, { kuvertieren: k === 'ja' }))]),

View File

@@ -58,6 +58,7 @@ const Board = {
const entriesByOrder = ref({}); // { orderId: {erster, letzter, anzahl} } const entriesByOrder = ref({}); // { orderId: {erster, letzter, anzahl} }
const sucheEintraege = ref([]); // flache Empfängerliste für die Suche const sucheEintraege = ref([]); // flache Empfängerliste für die Suche
const expandedCharges = reactive({}); const expandedCharges = reactive({});
const expandedEmpf = reactive({}); // je Auftrag: volle Empfängerliste + Kommentar ausklappen
const loading = ref(false); const loading = ref(false);
const msg = ref(''); const err = ref(''); const msg = ref(''); const err = ref('');
const sucheQuery = ref(''); let sucheTimer = null; const sucheQuery = ref(''); let sucheTimer = null;
@@ -92,7 +93,7 @@ const Board = {
// Sichere Basisfelder (alle auf orders/formats/products – von der // Sichere Basisfelder (alle auf orders/formats/products – von der
// Mitarbeiter-Rolle lesbar). api_client (API-Kundenname) und kuvertieren // Mitarbeiter-Rolle lesbar). api_client (API-Kundenname) und kuvertieren
// sind optional: fehlende Rechte/Felder dürfen die Ansicht NICHT blockieren. // sind optional: fehlende Rechte/Felder dürfen die Ansicht NICHT blockieren.
const basis = 'id,order_number,order_name,production_status,needs_envelope,brief_gedruckt,kuvert_gedruckt,frankiert,' const basis = 'id,order_number,order_name,production_status,needs_envelope,brief_gedruckt,kuvert_gedruckt,frankiert,internal_note,'
+ 'format.key,format.name,shipping_day,shipping_type,versanddatum,date_created,entries_count,source,' + 'format.key,format.name,shipping_day,shipping_type,versanddatum,date_created,entries_count,source,'
+ 'envelope_labeling,envelope_format,product.name'; + 'envelope_labeling,envelope_format,product.name';
const mitClient = basis + ',api_client.id,api_client.name'; const mitClient = basis + ',api_client.id,api_client.name';
@@ -137,8 +138,8 @@ const Board = {
const idx = {}; const idx = {};
for (const e of (ent.data?.data || [])) { for (const e of (ent.data?.data || [])) {
const k = String(e.order?.id ?? e.order); if (!k || k === 'null') continue; const k = String(e.order?.id ?? e.order); if (!k || k === 'null') continue;
if (!idx[k]) idx[k] = { erster: e, letzter: e, anzahl: 1 }; if (!idx[k]) idx[k] = { erster: e, letzter: e, anzahl: 1, alle: [e] };
else { idx[k].letzter = e; idx[k].anzahl += 1; } else { idx[k].letzter = e; idx[k].anzahl += 1; if (idx[k].alle.length < 2000) idx[k].alle.push(e); }
} }
entriesByOrder.value = idx; entriesByOrder.value = idx;
} catch { /* Empfänger optional */ } } catch { /* Empfänger optional */ }
@@ -192,6 +193,7 @@ const Board = {
const setStatus = (o, v) => patchOrder(o, { production_status: v }); const setStatus = (o, v) => patchOrder(o, { production_status: v });
const setVersand = (o, v) => patchOrder(o, { shipping_type: v }); const setVersand = (o, v) => patchOrder(o, { shipping_type: v });
const setVersanddatum = (o, v) => patchOrder(o, { versanddatum: v || null }); const setVersanddatum = (o, v) => patchOrder(o, { versanddatum: v || null });
const setNote = (o, v) => patchOrder(o, { internal_note: v });
// Status für einen ganzen Sammelauftrag (alle Teil-Aufträge) auf einmal. // Status für einen ganzen Sammelauftrag (alle Teil-Aufträge) auf einmal.
async function setChargeStatus(c, v) { async function setChargeStatus(c, v) {
try { try {
@@ -324,24 +326,58 @@ const Board = {
]; ];
return h('div', { style: 'display:flex;gap:5px;flex-wrap:wrap;' }, steps.map(([l, b, e]) => stepPill(l, b, e))); return h('div', { style: 'display:flex;gap:5px;flex-wrap:wrap;' }, steps.map(([l, b, e]) => stepPill(l, b, e)));
} }
// Gibt ein Zeilen-ARRAY zurück: Hauptzeile + (bei Ausklappen) Detailzeile mit
// voller Empfängerliste und Kommentarfeld.
function zeile(o, indent) { function zeile(o, indent) {
const e = entriesByOrder.value[String(o.id)]; const id = String(o.id);
return h('tr', { key: 'o' + o.id }, [ const e = entriesByOrder.value[id];
td(h('div', { style: `line-height:1.45;${indent ? 'padding-left:18px;border-left:2px solid #d2dae6;' : ''}` }, [ const auf = !!expandedEmpf[id];
h('div', { style: 'font-weight:500;' }, empfKurz(e?.erster) || '—'), const anzahl = e ? e.anzahl : (o.entries_count || 0);
(e && e.anzahl > 1) ? h('div', { style: 'font-size:12.5px;color:#566070;' }, '… ' + empfKurz(e.letzter)) : null, // Empfänger: erster + Umschalter „alle anzeigen" (kein gesondertes letzter mehr).
])), const empfZelle = h('div', { style: `line-height:1.45;${indent ? 'padding-left:18px;border-left:2px solid #d2dae6;' : ''}` }, [
td(h('div', { style: 'line-height:1.4;font-size:13px;' }, [ h('div', { style: 'font-weight:500;' }, empfKurz(e?.erster) || '—'),
o.order_name ? h('div', { style: 'font-weight:500;color:#1E50A5;' }, o.order_name) : null, (anzahl > 1) ? h('button', { type: 'button', onClick: () => { expandedEmpf[id] = !auf; },
h('div', {}, kundeName(o) || `#${o.order_number}`), style: 'cursor:pointer;border:none;background:transparent;padding:2px 0;margin-top:2px;color:#1E50A5;font:inherit;font-size:12.5px;font-weight:600;display:inline-flex;align-items:center;gap:3px;' },
h('div', { style: 'color:#566070;' }, `#${o.order_number} · ${o.entries_count || 0} Empf.`), [icon(auf ? 'expand_less' : 'expand_more'), auf ? 'Empfänger ausblenden' : `alle ${anzahl} Empfänger`]) : null,
])), ].filter(Boolean));
// Auftrag: Name/Kunde + Nummer NUR EINMAL.
const name = o.order_name || kundeName(o);
const auftragZelle = h('div', { style: 'line-height:1.4;font-size:13px;' }, [
name ? h('div', { style: 'font-weight:500;' + (o.order_name ? 'color:#1E50A5;' : '') }, name) : null,
h('div', { style: 'color:#566070;' }, `#${o.order_number} · ${o.entries_count || 0} Empf.`),
].filter(Boolean));
const haupt = h('tr', { key: 'o' + o.id }, [
td(empfZelle),
td(auftragZelle),
td(h('span', {}, o.format?.name || FORMAT_LABEL[o.format?.key] || o.format?.key || '—')), td(h('span', {}, o.format?.name || FORMAT_LABEL[o.format?.key] || o.format?.key || '—')),
td(ablaufStrip(o)), td(ablaufStrip(o)),
td(versandToggle(o)), td(versandToggle(o)),
td(datumFeld(o)), td(datumFeld(o)),
td(statusPill(o)), td(statusPill(o)),
]); ]);
if (!auf) return [haupt];
const liste = (e && e.alle) ? e.alle : [];
const detail = h('tr', { key: 'd' + o.id }, [
h('td', { colspan: 7, style: `padding:0 12px 14px;border-bottom:1px solid #d2dae6;${indent ? 'padding-left:30px;' : ''}` }, [
h('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:16px;background:#f7f9fc;border:1px solid #d2dae6;border-radius:10px;padding:12px 14px;' }, [
h('div', {}, [
h('div', { style: 'font-size:11.5px;font-weight:600;color:#566070;text-transform:uppercase;letter-spacing:.03em;margin-bottom:6px;' }, `Empfänger (${liste.length || anzahl})`),
h('div', { style: 'max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:3px;' },
liste.length ? liste.map((x, i) => h('div', { key: i, style: 'font-size:13px;display:flex;gap:8px;' }, [
h('span', { style: 'color:#566070;min-width:28px;' }, (i + 1) + '.'),
h('span', {}, empfKurz(x) || '—'),
])) : [h('span', { style: 'color:#566070;font-size:13px;' }, 'Keine Empfängerdaten geladen.')]),
]),
h('div', {}, [
h('div', { style: 'font-size:11.5px;font-weight:600;color:#566070;text-transform:uppercase;letter-spacing:.03em;margin-bottom:6px;' }, 'Kommentar'),
h('textarea', { rows: 4, placeholder: 'Kommentar zum Auftrag …',
style: 'width:100%;box-sizing:border-box;border:1px solid #b4c0d2;border-radius:8px;padding:9px 11px;font:inherit;font-size:13px;background:#ffffff;color:#1b2430;resize:vertical;',
value: o.internal_note || '', onChange: (ev) => setNote(o, ev.target.value) }),
]),
]),
]),
]);
return [haupt, detail];
} }
// Aggregierter Ablauf über alle Aufträge einer Charge. // Aggregierter Ablauf über alle Aufträge einer Charge.
function ablaufStripAgg(orders) { function ablaufStripAgg(orders) {
@@ -398,8 +434,8 @@ const Board = {
const api = list.filter((o) => o.source === 'api' || o.api_client); const api = list.filter((o) => o.source === 'api' || o.api_client);
const rest = list.filter((o) => !(o.source === 'api' || o.api_client)); const rest = list.filter((o) => !(o.source === 'api' || o.api_client));
const rows = []; 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 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)); for (const o of rest) rows.push(...zeile(o));
return rows; 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 kopfRow = () => h('thead', {}, h('tr', {}, [th('Empfänger (erster · letzter)'), th('Auftrag'), th('Format'), th('Ablauf'), th('Versand'), th('Versanddatum'), th('Status')]));