Produktion: Suche-Reiter Usability – weißes Suchfeld, klare Typ-Auswahl

Hoher Kontrast für die Bedienung: große, eindeutige Typ-Auswahlknöpfe
(Schriftstück/Kuvert/Druckobjekt, aktiv blau, sonst weiß mit Rahmen)
direkt über einem unverkennbar weißen, großen Suchfeld. Standard-
Templates werden nicht mehr im Reiter gewählt, sondern in den
Einstellungen den Formaten zugeordnet (format_templates.default_format);
der Schnelldruck leitet das Template daraus ab. Ergebnisse zeigen die
vollständige Anschrift über der Auftragsnummer – ohne Status/Produkttyp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-21 17:16:01 +02:00
parent a364f07035
commit abf60cda0e

View File

@@ -170,11 +170,11 @@ const Board = defineComponent({
const api = useApi();
const tab = ref('neu');
const suche = ref('');
// Suche-Reiter: Objekttyp (Schriftstück/Kuvert/Druckobjekt), Standard-Templates
// je Typ + Zielmaschine (im Reiter einstellbar, lokal gespeichert), und ein
// flacher Empfänger-Index (eine Zeile je Empfänger, mit Briefnummer).
// Suche-Reiter: Objekttyp (Schriftstück/Kuvert/Druckobjekt) + ein flacher
// Empfänger-Index (eine Zeile je Empfänger, mit Briefnummer). Die Standard-
// Templates werden NICHT hier gewählt, sondern in den Einstellungen den
// Formaten zugeordnet (format_templates.default_format).
const sucheTyp = ref('brief');
const sucheTpl = reactive({ brief: null, umschlag: null, machine: null });
const sucheEintraege = ref([]);
const loading = ref(false);
const fehler = ref('');
@@ -270,8 +270,18 @@ const Board = defineComponent({
const nm = entryName(e);
if (!namen[oid]) namen[oid] = { erste: nm, letzte: nm, anzahl: 1 };
else { namen[oid].letzte = nm; namen[oid].anzahl += 1; }
// Vollständige Anschrift (mehrzeilig) für die Anzeige im Suche-Reiter.
const adr = [];
const person = [e.salutation, e.first_name, e.last_name].filter(Boolean).join(' ');
if (person) adr.push(person);
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));
// Eine durchsuchbare Zeile je Empfänger (mit Briefnummer) für den Suche-Reiter.
flach.push({ orderId: oid, nr: Number(e.letter_number) || flach.filter((x) => x.orderId === oid).length + 1, name: nm, hay: txt });
flach.push({ orderId: oid, nr: Number(e.letter_number) || flach.filter((x) => x.orderId === oid).length + 1, name: nm, adr, hay: txt });
}
empfIndex.value = idx;
empfNamen.value = namen;
@@ -401,7 +411,6 @@ const Board = defineComponent({
} finally { if (!leise) loading.value = false; }
}
onMounted(laden);
onMounted(sucheDefaultsLaden);
// Auto-Refresh: hält das Board aktuell, ohne dass man die Seite neu laden muss.
// Pausiert bei offenem Sammel-Dialog, im Hintergrund-Tab und während bereits
@@ -473,30 +482,22 @@ const Board = defineComponent({
}
return res;
});
// Standard-Templates/Maschine für den Schnelldruck lokal sichern/laden.
const SUCHE_KEY = 'skrift_suche_defaults';
function sucheDefaultsLaden() {
try {
const s = JSON.parse(localStorage.getItem(SUCHE_KEY) || '{}');
if (s.brief) sucheTpl.brief = s.brief;
if (s.umschlag) sucheTpl.umschlag = s.umschlag;
if (s.machine) sucheTpl.machine = s.machine;
} catch { /* ohne gespeicherte Defaults weiter */ }
}
function sucheDefaultsSpeichern() {
try { localStorage.setItem(SUCHE_KEY, JSON.stringify({ brief: sucheTpl.brief, umschlag: sucheTpl.umschlag, machine: sucheTpl.machine })); }
catch { /* egal */ }
}
// Schnelldruck: NUR den gewählten Empfänger (Briefnummer) des Typs mit dem im
// Reiter gesetzten Standard-Template in die Warteschlange geben.
// Schnelldruck: NUR den gewählten Empfänger (Briefnummer) des Typs in die
// Warteschlange geben. Das Template kommt aus der in den Einstellungen dem
// Auftragsformat zugeordneten Vorlage (default_format), sonst der Fallback.
async function schnelldruck(o, nr, typ) {
const tplId = typ === 'umschlag' ? sucheTpl.umschlag : sucheTpl.brief;
const machine = sucheTpl.machine || aktiveMaschinen.value[0]?.id || null;
if (!tplId) { fehler.value = `Bitte oben ein Standard-Template für ${typ === 'umschlag' ? 'Kuvert' : 'Schriftstück'} wählen.`; return; }
const machine = aktiveMaschinen.value[0]?.id || null;
if (!machine) { fehler.value = 'Keine aktive Maschine verfügbar.'; return; }
const tpl = typ === 'umschlag'
? (tplForFormat(o.envelope_format, 'umschlag') || tplByKey(ENVELOPE_TPL[o.envelope_format] || 'kuvert_a_v1', 'umschlag'))
: (tplForFormat(o.format?.key, 'brief') || tplByKey(LETTER_TPL[o.format?.key] || 'a4_single', 'brief'));
if (!tpl?.id) {
fehler.value = `Kein Standard-Template für ${typ === 'umschlag' ? 'Kuvert' : 'Schriftstück'} hinterlegt – bitte in den Einstellungen (Format-Templates) ein Standardformat zuordnen.`;
return;
}
try {
const sections = {};
sections[typ === 'umschlag' ? 'umschlag' : 'brief'] = { enabled: true, template: tplId, numbers: nr ? String(nr) : '' };
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();
@@ -1269,66 +1270,66 @@ const Board = defineComponent({
}
// ── Suche-Reiter ──────────────────────────────────────────────────────────
// Bewusst hoher Kontrast (weißes Suchfeld, klar erkennbare Typ-Auswahl) – wird
// auch von weniger technikaffinen Personen bedient. Standard-Templates werden
// NICHT hier gewählt, sondern in den Einstellungen den Formaten zugeordnet.
function sucheInhalt() {
const briefTpls = templates.value.filter((t) => t.type === 'brief');
const umschlagTpls = templates.value.filter((t) => t.type === 'umschlag');
const tplSelect = (typ) => {
const opts = typ === 'umschlag' ? umschlagTpls : briefTpls;
const val = typ === 'umschlag' ? sucheTpl.umschlag : sucheTpl.brief;
return h('select', { style: S_SELECT + 'width:100%;', value: val ?? '',
onChange: (e) => { const v = e.target.value || null; if (typ === 'umschlag') sucheTpl.umschlag = v; else sucheTpl.brief = v; sucheDefaultsSpeichern(); } },
[h('option', { value: '' }, '– Standard-Template –')]
.concat(opts.map((t) => h('option', { value: t.id, selected: String(val) === String(t.id) }, t.name))));
};
// Kopf: Typ-Umschalter + Standard-Templates/Maschine (im Reiter einstellbar).
const kopf = h('div', { style: SECTION }, [
h('div', { style: 'max-width:560px;margin-bottom:14px;' }, [
segToggle(sucheTyp.value, [
{ key: 'brief', label: '📄 Schriftstück' },
{ key: 'umschlag', label: '✉ Kuvert' },
{ key: 'objekt', label: '📎 Druckobjekte' },
], (k) => { sucheTyp.value = k; }),
]),
h('div', { style: 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;' }, [
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Standard-Template Schriftstück'), tplSelect('brief')]),
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Standard-Template Kuvert'), tplSelect('umschlag')]),
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Maschine'),
h('select', { style: S_SELECT + 'width:100%;', value: sucheTpl.machine ?? '',
onChange: (e) => { sucheTpl.machine = e.target.value || null; sucheDefaultsSpeichern(); } },
[h('option', { value: '' }, aktiveMaschinen.value[0] ? `Automatisch (${aktiveMaschinen.value[0].name})` : '– keine –')]
.concat(aktiveMaschinen.value.map((m) => h('option', { value: m.id, selected: String(sucheTpl.machine) === String(m.id) }, m.name)))),
]),
]),
subdued(sucheTyp.value === 'objekt'
? 'Druckobjekte: fertige Dateien des Auftrags zum manuellen Herunterladen.'
: 'Klick auf „In Warteschlange" gibt genau diesen Empfänger mit dem gewählten Standard-Template in die Queue.', 'margin-top:10px;'),
const TYPEN = [
{ key: 'brief', label: '📄 Schriftstück' },
{ key: 'umschlag', label: '✉ Kuvert' },
{ key: 'objekt', label: '📎 Druckobjekt' },
];
// Große, eindeutige Auswahlknöpfe: aktiv = blau/weiß, sonst weiß mit Rahmen.
const typWahl = h('div', {}, [
h('div', { style: 'font-size:13px;font-weight:700;color:var(--foreground-normal);margin-bottom:8px;' }, '1. Was möchten Sie?'),
h('div', { style: 'display:flex;gap:10px;flex-wrap:wrap;' }, TYPEN.map((t) => {
const aktiv = sucheTyp.value === t.key;
return h('button', { type: 'button', onClick: () => { sucheTyp.value = t.key; },
style: 'cursor:pointer;border-radius:10px;padding:14px 22px;font:inherit;font-size:16px;font-weight:700;'
+ (aktiv ? `background:${ACCENT};color:#fff;border:2px solid ${ACCENT};box-shadow:0 2px 6px rgba(30,80,165,.3);`
: 'background:#fff;color:#1a2a3a;border:2px solid #cdd6e2;') }, t.label);
})),
]);
// Großes, unverkennbar weißes Suchfeld.
const feld = h('div', { style: 'margin-top:18px;' }, [
h('div', { style: 'font-size:13px;font-weight:700;color:var(--foreground-normal);margin-bottom:8px;' }, '2. Empfänger suchen'),
h('input', { type: 'search', value: suche.value, placeholder: 'Name, Straße, Ort oder Auftragsnummer eingeben …',
style: 'width:100%;box-sizing:border-box;background:#fff;color:#111;border:2px solid #cdd6e2;border-radius:10px;'
+ 'font:inherit;font-size:22px;padding:16px 20px;outline:none;box-shadow:0 1px 3px rgba(0,0,0,.08);',
onInput: (e) => { suche.value = e.target.value; } }),
]);
const rahmen = (inhalt) => h('div', { style: 'display:flex;flex-direction:column;gap:0;' }, [
h('div', { style: 'background:#fff;border:1px solid #cdd6e2;border-radius:12px;padding:20px 22px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.06);' }, [typWahl, feld]),
inhalt,
]);
// Großes Suchfeld (nutzt dasselbe Such-Feld wie oben).
const feld = h('input', { type: 'search', value: suche.value, placeholder: '🔎 Name, Straße, Ort, Auftragsnummer …',
style: S_INPUT + 'width:100%;font-size:20px;padding:14px 18px;margin:14px 0;', onInput: (e) => { suche.value = e.target.value; } });
if (!suche.value.trim()) return [kopf, feld, subdued('Suchbegriff eingeben – durchsucht Neu, Warteschlange, Im Druck, Kuvertierung und Versandfertig.')];
if (!suche.value.trim()) return rahmen(subdued('Suchbegriff eingeben – durchsucht Neu, Warteschlange, Im Druck, Kuvertierung und Versandfertig.'));
const treffer = sucheTreffer.value;
if (!treffer.length) return [kopf, feld, subdued('Keine Treffer in den aktiven Aufträgen.')];
if (!treffer.length) return rahmen(subdued('Keine Treffer in den aktiven Aufträgen.'));
const zeile = ({ e, o }) => {
const gesamt = empfNamen.value[String(o.id)]?.anzahl || 1;
const pos = gesamt > 1 ? `Nr. ${e.nr} von ${gesamt}` : 'Einzelauftrag';
const statusLbl = STATUS_LABEL[o.production_status || 'eingegangen'] || o.production_status || '—';
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'
? btn('📎 Dateien', () => openEmpf(o), 'primary')
: btn('🖨 In Warteschlange', () => schnelldruck(o, e.nr, sucheTyp.value), 'primary');
? btn('📎 Dateien öffnen', () => openEmpf(o), 'primary')
: (sucheTyp.value === 'umschlag' && !kannKuvert)
? subdued('Kein Kuvert')
: btn('🖨 In Warteschlange', () => schnelldruck(o, e.nr, sucheTyp.value), 'primary');
return h('div', { key: `s${o.id}-${e.nr}`, style: CARD + 'margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;' }, [
h('div', { style: 'min-width:0;' }, [
h('div', { style: 'font-weight:700;font-size:15px;' }, e.name || '—'),
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;margin-top:3px;align-items:center;' }, [
subdued(`#${o.order_number}`), chip(pos), chip(statusLbl), o.product?.name ? subdued(o.product.name) : null,
// Vollständige Anschrift zuerst (erste Zeile = Name, hervorgehoben).
...adrLines.map((z, i) => h('div', { style: i === 0 ? 'font-weight:700;font-size:16px;' : 'font-size:14px;color:var(--foreground-normal);' }, z)),
// Darunter die Auftragsnummer (+ Position bei mehreren Empfängern).
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;margin-top:5px;align-items:center;' }, [
h('span', { style: 'font-size:13px;color:var(--foreground-subdued);' }, `Auftrag #${o.order_number}`),
gesamt > 1 ? chip(`Nr. ${e.nr} von ${gesamt}`) : null,
]),
]),
aktion,
]);
};
return [kopf, feld, h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile))];
return rahmen(h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, treffer.map(zeile)));
}
function tabInhalt() {