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 api = useApi();
|
||||||
const tab = ref('neu');
|
const tab = ref('neu');
|
||||||
const suche = ref('');
|
const suche = ref('');
|
||||||
// Suche-Reiter: Objekttyp (Schriftstück/Kuvert/Druckobjekt), Standard-Templates
|
// Suche-Reiter: Objekttyp (Schriftstück/Kuvert/Druckobjekt) + ein flacher
|
||||||
// je Typ + Zielmaschine (im Reiter einstellbar, lokal gespeichert), und ein
|
// Empfänger-Index (eine Zeile je Empfänger, mit Briefnummer). Die Standard-
|
||||||
// flacher Empfänger-Index (eine Zeile je Empfänger, mit Briefnummer).
|
// Templates werden NICHT hier gewählt, sondern in den Einstellungen den
|
||||||
|
// Formaten zugeordnet (format_templates.default_format).
|
||||||
const sucheTyp = ref('brief');
|
const sucheTyp = ref('brief');
|
||||||
const sucheTpl = reactive({ brief: null, umschlag: null, machine: null });
|
|
||||||
const sucheEintraege = ref([]);
|
const sucheEintraege = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const fehler = ref('');
|
const fehler = ref('');
|
||||||
@@ -270,8 +270,18 @@ const Board = defineComponent({
|
|||||||
const nm = entryName(e);
|
const nm = entryName(e);
|
||||||
if (!namen[oid]) namen[oid] = { erste: nm, letzte: nm, anzahl: 1 };
|
if (!namen[oid]) namen[oid] = { erste: nm, letzte: nm, anzahl: 1 };
|
||||||
else { namen[oid].letzte = nm; namen[oid].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.
|
// 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;
|
empfIndex.value = idx;
|
||||||
empfNamen.value = namen;
|
empfNamen.value = namen;
|
||||||
@@ -401,7 +411,6 @@ const Board = defineComponent({
|
|||||||
} finally { if (!leise) loading.value = false; }
|
} finally { if (!leise) loading.value = false; }
|
||||||
}
|
}
|
||||||
onMounted(laden);
|
onMounted(laden);
|
||||||
onMounted(sucheDefaultsLaden);
|
|
||||||
|
|
||||||
// Auto-Refresh: hält das Board aktuell, ohne dass man die Seite neu laden muss.
|
// 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
|
// Pausiert bei offenem Sammel-Dialog, im Hintergrund-Tab und während bereits
|
||||||
@@ -473,30 +482,22 @@ const Board = defineComponent({
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
// Standard-Templates/Maschine für den Schnelldruck lokal sichern/laden.
|
// Schnelldruck: NUR den gewählten Empfänger (Briefnummer) des Typs in die
|
||||||
const SUCHE_KEY = 'skrift_suche_defaults';
|
// Warteschlange geben. Das Template kommt aus der in den Einstellungen dem
|
||||||
function sucheDefaultsLaden() {
|
// Auftragsformat zugeordneten Vorlage (default_format), sonst der Fallback.
|
||||||
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.
|
|
||||||
async function schnelldruck(o, nr, typ) {
|
async function schnelldruck(o, nr, typ) {
|
||||||
const tplId = typ === 'umschlag' ? sucheTpl.umschlag : sucheTpl.brief;
|
const machine = aktiveMaschinen.value[0]?.id || null;
|
||||||
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; }
|
|
||||||
if (!machine) { fehler.value = 'Keine aktive Maschine verfügbar.'; return; }
|
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 {
|
try {
|
||||||
const sections = {};
|
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 });
|
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.`);
|
flash(`#${o.order_number} · ${typ === 'umschlag' ? '✉ Kuvert' : '📄 Schriftstück'}${nr ? ` Nr. ${nr}` : ''} → Warteschlange.`);
|
||||||
await laden();
|
await laden();
|
||||||
@@ -1269,66 +1270,66 @@ const Board = defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Suche-Reiter ──────────────────────────────────────────────────────────
|
// ── 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() {
|
function sucheInhalt() {
|
||||||
const briefTpls = templates.value.filter((t) => t.type === 'brief');
|
const TYPEN = [
|
||||||
const umschlagTpls = templates.value.filter((t) => t.type === 'umschlag');
|
{ key: 'brief', label: '📄 Schriftstück' },
|
||||||
const tplSelect = (typ) => {
|
{ key: 'umschlag', label: '✉ Kuvert' },
|
||||||
const opts = typ === 'umschlag' ? umschlagTpls : briefTpls;
|
{ key: 'objekt', label: '📎 Druckobjekt' },
|
||||||
const val = typ === 'umschlag' ? sucheTpl.umschlag : sucheTpl.brief;
|
];
|
||||||
return h('select', { style: S_SELECT + 'width:100%;', value: val ?? '',
|
// Große, eindeutige Auswahlknöpfe: aktiv = blau/weiß, sonst weiß mit Rahmen.
|
||||||
onChange: (e) => { const v = e.target.value || null; if (typ === 'umschlag') sucheTpl.umschlag = v; else sucheTpl.brief = v; sucheDefaultsSpeichern(); } },
|
const typWahl = h('div', {}, [
|
||||||
[h('option', { value: '' }, '– Standard-Template –')]
|
h('div', { style: 'font-size:13px;font-weight:700;color:var(--foreground-normal);margin-bottom:8px;' }, '1. Was möchten Sie?'),
|
||||||
.concat(opts.map((t) => h('option', { value: t.id, selected: String(val) === String(t.id) }, t.name))));
|
h('div', { style: 'display:flex;gap:10px;flex-wrap:wrap;' }, TYPEN.map((t) => {
|
||||||
};
|
const aktiv = sucheTyp.value === t.key;
|
||||||
// Kopf: Typ-Umschalter + Standard-Templates/Maschine (im Reiter einstellbar).
|
return h('button', { type: 'button', onClick: () => { sucheTyp.value = t.key; },
|
||||||
const kopf = h('div', { style: SECTION }, [
|
style: 'cursor:pointer;border-radius:10px;padding:14px 22px;font:inherit;font-size:16px;font-weight:700;'
|
||||||
h('div', { style: 'max-width:560px;margin-bottom:14px;' }, [
|
+ (aktiv ? `background:${ACCENT};color:#fff;border:2px solid ${ACCENT};box-shadow:0 2px 6px rgba(30,80,165,.3);`
|
||||||
segToggle(sucheTyp.value, [
|
: 'background:#fff;color:#1a2a3a;border:2px solid #cdd6e2;') }, t.label);
|
||||||
{ key: 'brief', label: '📄 Schriftstück' },
|
})),
|
||||||
{ key: 'umschlag', label: '✉ Kuvert' },
|
]);
|
||||||
{ key: 'objekt', label: '📎 Druckobjekte' },
|
// Großes, unverkennbar weißes Suchfeld.
|
||||||
], (k) => { sucheTyp.value = k; }),
|
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('div', { style: 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;' }, [
|
h('input', { type: 'search', value: suche.value, placeholder: 'Name, Straße, Ort oder Auftragsnummer eingeben …',
|
||||||
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Standard-Template Schriftstück'), tplSelect('brief')]),
|
style: 'width:100%;box-sizing:border-box;background:#fff;color:#111;border:2px solid #cdd6e2;border-radius:10px;'
|
||||||
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Standard-Template Kuvert'), tplSelect('umschlag')]),
|
+ 'font:inherit;font-size:22px;padding:16px 20px;outline:none;box-shadow:0 1px 3px rgba(0,0,0,.08);',
|
||||||
h('div', {}, [h('label', { style: 'font-size:11px;color:var(--foreground-subdued);' }, 'Maschine'),
|
onInput: (e) => { suche.value = e.target.value; } }),
|
||||||
h('select', { style: S_SELECT + 'width:100%;', value: sucheTpl.machine ?? '',
|
]);
|
||||||
onChange: (e) => { sucheTpl.machine = e.target.value || null; sucheDefaultsSpeichern(); } },
|
const rahmen = (inhalt) => h('div', { style: 'display:flex;flex-direction:column;gap:0;' }, [
|
||||||
[h('option', { value: '' }, aktiveMaschinen.value[0] ? `Automatisch (${aktiveMaschinen.value[0].name})` : '– keine –')]
|
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]),
|
||||||
.concat(aktiveMaschinen.value.map((m) => h('option', { value: m.id, selected: String(sucheTpl.machine) === String(m.id) }, m.name)))),
|
inhalt,
|
||||||
]),
|
|
||||||
]),
|
|
||||||
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;'),
|
|
||||||
]);
|
]);
|
||||||
// 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;
|
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 zeile = ({ e, o }) => {
|
||||||
const gesamt = empfNamen.value[String(o.id)]?.anzahl || 1;
|
const gesamt = empfNamen.value[String(o.id)]?.anzahl || 1;
|
||||||
const pos = gesamt > 1 ? `Nr. ${e.nr} von ${gesamt}` : 'Einzelauftrag';
|
const adrLines = (e.adr && e.adr.length) ? e.adr : [e.name || '—'];
|
||||||
const statusLbl = STATUS_LABEL[o.production_status || 'eingegangen'] || o.production_status || '—';
|
const kannKuvert = !!(o.needs_envelope && o.envelope_labeling && o.envelope_labeling !== 'keine');
|
||||||
const aktion = sucheTyp.value === 'objekt'
|
const aktion = sucheTyp.value === 'objekt'
|
||||||
? btn('📎 Dateien', () => openEmpf(o), 'primary')
|
? btn('📎 Dateien öffnen', () => openEmpf(o), 'primary')
|
||||||
: btn('🖨 In Warteschlange', () => schnelldruck(o, e.nr, sucheTyp.value), '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;' }, [
|
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: 'min-width:0;' }, [
|
||||||
h('div', { style: 'font-weight:700;font-size:15px;' }, e.name || '—'),
|
// Vollständige Anschrift zuerst (erste Zeile = Name, hervorgehoben).
|
||||||
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;margin-top:3px;align-items:center;' }, [
|
...adrLines.map((z, i) => h('div', { style: i === 0 ? 'font-weight:700;font-size:16px;' : 'font-size:14px;color:var(--foreground-normal);' }, z)),
|
||||||
subdued(`#${o.order_number}`), chip(pos), chip(statusLbl), o.product?.name ? subdued(o.product.name) : null,
|
// 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,
|
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() {
|
function tabInhalt() {
|
||||||
|
|||||||
Reference in New Issue
Block a user