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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user