Statistik-Modul, Kontakt-Bulk, Notiz-Fix, Ausland-Erkennung, Weiteres-Kontakt

- Neues Modul „Statistik": versendete Briefe je Kunde/Monat, getrennt Inland/
  Ausland, Zeilen aufklappbar (Auftragsliste), mit Suche. Raus aus dem Board.
- Produktion: Auto-Refresh pausiert, während ein Feld bearbeitet wird (Notizen
  gingen sonst beim 15-s-Reload/Statuswechsel verloren). „Neu generieren“/
  „In manuellen Druck“ aus dem Empfänger-Dialog entfernt (liegen unter Weiteres).
  Unter Weiteres jetzt auch Kontakt/Kunde verknüpfen (per E-Mail) und mit × lösen
  – ohne Kundenmail.
- Ausland-Erkennung (zielInland) mit Wortgrenzen (\b) + unbekannter Ländertext →
  Inland: „Schweizer Straße“, „Neusass“, „Polenz“ werden nicht mehr als Ausland
  geflaggt (orders + produktion).
- Kontakte-Modul: Mehrfachauswahl → Status ändern / löschen.
- contacts-Hook: Abwahl von „In Kontakte speichern“ löscht die Kontakte wieder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorthandClaude Opus 4.8 committed 2026-09-11 14:16:07 +02:00
1 parent 6ccd4f3c53
commit 4aa3fd04e6
6 files changed
+299 -62

No files matched your search

+57 -4
View File
@@ -212,13 +212,46 @@ const Board = defineComponent({
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
}
// ── Mehrfachauswahl (Bulk: Status ändern / löschen) ──
const auswahl = ref(new Set());
const istGewaehlt = (id) => auswahl.value.has(id);
const toggleSel = (id) => { const s = new Set(auswahl.value); s.has(id) ? s.delete(id) : s.add(id); auswahl.value = s; };
const selAlle = () => { auswahl.value = new Set(gefiltert.value.map((k) => k.id)); };
const selKeine = () => { auswahl.value = new Set(); };
async function bulkStatus(status) {
const ids = [...auswahl.value]; if (!ids.length || !status) return;
try {
await api.patch('/items/contacts', { keys: ids, data: { lead_status: status } });
for (const k of kontakte.value) if (auswahl.value.has(k.id)) k.lead_status = status;
meldung.value = `Status für ${ids.length} Kontakt(e) geändert.`; setTimeout(() => { meldung.value = ''; }, 2500);
selKeine();
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Status-Änderung fehlgeschlagen.'; }
}
async function bulkLoeschen() {
const ids = [...auswahl.value]; if (!ids.length) return;
if (typeof window !== 'undefined' && !window.confirm(`${ids.length} Kontakt(e) endgültig löschen?`)) return;
try {
await api.delete('/items/contacts', { data: ids });
const raus = auswahl.value;
kontakte.value = kontakte.value.filter((k) => !raus.has(k.id));
selKeine();
meldung.value = `${ids.length} Kontakt(e) gelöscht.`; setTimeout(() => { meldung.value = ''; }, 2500);
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Löschen fehlgeschlagen.'; }
}
// ── Liste ──
function karte(k) {
return h('div', {
onClick: () => { aktivId.value = k.id; fehler.value = ''; },
style: CARD + 'cursor:pointer;margin-bottom:10px;display:flex;justify-content:space-between;align-items:flex-start;gap:12px;',
style: CARD + 'cursor:pointer;margin-bottom:10px;display:flex;justify-content:space-between;align-items:flex-start;gap:12px;'
+ (istGewaehlt(k.id) ? 'outline:2px solid var(--primary,#1E50A5);' : ''),
}, [
h('div', { style: 'min-width:0;' }, [
h('input', {
type: 'checkbox', checked: istGewaehlt(k.id),
onClick: (e) => { e.stopPropagation(); toggleSel(k.id); },
title: 'Auswählen', style: 'width:18px;height:18px;margin-top:3px;cursor:pointer;flex-shrink:0;',
}),
h('div', { style: 'min-width:0;flex:1;' }, [
h('div', { style: 'font-size:16px;font-weight:600;margin-bottom:2px;' }, anzeigeName(k)),
subdued(adresse(k) || '—', 'font-size:13px;'),
k.phone ? subdued('📞 ' + k.phone, 'font-size:13px;margin-top:2px;') : null,
@@ -245,18 +278,38 @@ const Board = defineComponent({
]),
]),
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:space-between;' }, [
subdued(`${gefiltert.value.length} Kontakt(e)`),
h('label', { style: 'display:flex;align-items:center;gap:6px;font-size:13px;color:var(--foreground-subdued);cursor:pointer;' }, [
h('input', { type: 'checkbox',
checked: gefiltert.value.length > 0 && auswahl.value.size >= gefiltert.value.length,
onChange: (e) => { e.target.checked ? selAlle() : selKeine(); }, style: 'width:16px;height:16px;cursor:pointer;' }),
`${gefiltert.value.length} Kontakt(e)`,
]),
h('div', { style: 'display:flex;gap:8px;' }, [
btn('↻ Aktualisieren', laden),
btn('⭳ CSV-Export', exportCsv),
]),
]),
]);
// Bulk-Leiste, sobald mindestens ein Kontakt ausgewählt ist.
const bulk = auswahl.value.size ? h('div', {
style: 'position:sticky;top:8px;z-index:5;background:var(--primary,#1E50A5);color:#fff;border-radius:10px;padding:10px 14px;margin-bottom:12px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;',
}, [
h('span', { style: 'font-weight:600;' }, `${auswahl.value.size} ausgewählt`),
h('span', {}, 'Status:'),
h('select', {
style: 'border-radius:6px;padding:5px 8px;font:inherit;font-size:13px;border:none;cursor:pointer;', value: '',
onChange: (e) => { if (e.target.value) { bulkStatus(e.target.value); e.target.value = ''; } },
}, [h('option', { value: '' }, 'ändern auf …'), ...LEAD.map((l) => h('option', { value: l.key }, l.label))]),
h('button', { type: 'button', onClick: bulkLoeschen,
style: 'margin-left:auto;cursor:pointer;border:1px solid #fff;background:transparent;color:#fff;border-radius:6px;padding:5px 12px;font:inherit;font-size:13px;font-weight:600;' }, '🗑 Löschen'),
h('button', { type: 'button', onClick: selKeine,
style: 'cursor:pointer;border:none;background:rgba(255,255,255,.2);color:#fff;border-radius:6px;padding:5px 12px;font:inherit;font-size:13px;' }, 'Aufheben'),
]) : null;
const liste = loading.value ? subdued('Lädt …')
: gefiltert.value.length ? h('div', {}, gefiltert.value.map(karte))
: subdued('Keine Kontakte. Sobald ein Auftrag versendet wird, erscheinen die Empfänger hier automatisch.');
const hinweis = subdued('CSV-Import: Content → Kontakte → Import (oben rechts).', 'margin-top:16px;font-size:12px;');
return [kopf, liste, hinweis];
return [kopf, bulk, liste, hinweis].filter(Boolean);
}
// ── Detail ──