Files
skrift-directus/extensions/directus-extension-skrift-kontakte/dist/index.js
s4luorth ba9c073bb9 WP-Feld-Mapping robuster + Logging; Muster ohne Direktdruck; Kontakte-Fixes
- skrift-orders: sample-/offer-request nehmen viele Feldnamen-Varianten an
  (feldWert-Helfer) und loggen eingehende + gefüllte Feldnamen zur Diagnose.
- skrift-produktion: „Direkt drucken" bei Musteranfragen entfernt (nur noch
  „In manuellen Druck übernehmen").
- skrift-kontakte: Bulk-Status-Dropdown lesbar (weiße Schrift auf weiß behoben);
  Shift-Klick wählt den ganzen Bereich zwischen zwei Checkboxen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-11 21:43:28 +02:00

468 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Skrift – Modul „Kontakte" (Mini-CRM)
* ===========================================================================
* Handytaugliches Lead-Board über die Collection `contacts`: alle Empfänger,
* denen ein Schriftstück gesendet wurde. Fokus auf Nachtelefonieren:
*
* • Liste mit Suche + Status-Filter, ein Tipp öffnet den Kontakt.
* • Detail: Name/Adresse, Telefon als GroundWire-Anruf-Button, E-Mail,
* Lead-Status, „Behalten" und ein Notizfeld – alles mit Auto-Speichern.
* • ◀/▶ blättert durch die gefilterte Liste, ohne zurückzuspringen.
* • CSV-Export der (gefilterten) Kontakte. Import über die normale
* Directus-Collection-Seite (Content → Kontakte → Import).
*
* Anruf-Schema fest GroundWire (groundwire://call?number=…). Die Konstante DIAL
* unten ist die einzige Stelle, falls ein anderes Format nötig wird.
*
* Von Hand als ESM (kein Build-Schritt) – Vue + SDK stellt Directus bereit.
*/
import { defineComponent, ref, reactive, computed, watch, onMounted, h, resolveComponent } from 'vue';
import { useApi } from '@directus/extensions-sdk';
const ACCENT = '#1E50A5';
// GroundWire-Anruf-Link (Acrobits „Call URI"): scheme:NUMMER?dialAction=…
// Die Nummer steht direkt hinter dem Doppelpunkt (kein „//"), ohne Leerzeichen.
// dialAction=voiceCall → sofortiger VoIP-Anruf über GroundWire. Für nur
// Vorbefüllen (Nummer eintippen, selbst tippen) den ?dialAction-Teil weglassen.
const cleanNr = (nr) => String(nr || '').replace(/[^\d+]/g, '');
const DIAL = (nr) => 'groundwire:' + cleanNr(nr) + '?dialAction=voiceCall';
const LEAD = [
{ key: 'neu', label: 'Neu', color: '#6b7280' },
{ key: 'kontaktiert', label: 'Kontaktiert', color: ACCENT },
{ key: 'interessiert', label: 'Interessiert', color: '#e07b00' },
{ key: 'kein_interesse', label: 'Kein Interesse', color: '#c62828' },
{ key: 'kunde', label: 'Kunde', color: '#2e7d32' },
];
const LEAD_LABEL = Object.fromEntries(LEAD.map((l) => [l.key, l.label]));
const LEAD_COLOR = Object.fromEntries(LEAD.map((l) => [l.key, l.color]));
const FIELDS = ['id', 'salutation', 'first_name', 'last_name', 'company', 'street', 'house_no',
'zip', 'city', 'country', 'email', 'phone', 'notes', 'lead_status', 'sent_at',
'source_order.order_number'];
// ── Stil-Helfer ──────────────────────────────────────────────────────────────
const CARD = 'border:1px solid var(--border-subdued);border-radius:10px;padding:16px 18px;background:var(--background-page,#fff);box-shadow:0 1px 3px rgba(0,0,0,.08);';
// Klar erkennbares Eingabefeld: kräftigere Umrandung + leicht getönter Grund,
// damit es sich deutlich von der weißen Karte abhebt.
const S_INPUT = 'width:100%;padding:11px 12px;border:1.5px solid var(--border-normal,#c4cdde);border-radius:8px;background:var(--background-subdued,#eef1f7);color:var(--foreground-normal);font:inherit;font-size:15px;box-sizing:border-box;';
const LBL = 'font-size:12px;font-weight:600;color:var(--foreground-subdued);margin:0 0 4px;text-transform:uppercase;letter-spacing:.03em;';
function btn(label, onClick, kind) {
const base = 'cursor:pointer;border-radius:8px;padding:10px 16px;font:inherit;font-size:14px;border:1px solid;';
const style = kind === 'primary'
? base + `border-color:${ACCENT};background:${ACCENT};color:#fff;`
: kind === 'ghost'
? base + 'border-color:transparent;background:transparent;color:var(--foreground-normal);'
: base + 'border-color:var(--border-normal);background:var(--background-subdued);color:var(--foreground-normal);';
return h('button', { type: 'button', onClick, style }, label);
}
function chip(text, farbe) {
return h('span', {
style: 'display:inline-flex;align-items:center;gap:4px;font-size:12px;padding:3px 10px;border-radius:20px;'
+ `background:${farbe || 'var(--background-subdued)'};color:#fff;white-space:nowrap;`,
}, text);
}
function subdued(text, extra) {
return h('div', { style: `color:var(--foreground-subdued);font-size:14px;${extra || ''}` }, text);
}
function feld(label, node) {
return h('div', { style: 'margin-bottom:14px;' }, [h('div', { style: LBL }, label), node]);
}
const anzeigeName = (k) => [k.salutation, k.first_name, k.last_name].filter(Boolean).join(' ')
|| k.company || k.email || k.phone || '(ohne Namen)';
const adresse = (k) => [[k.street, k.house_no].filter(Boolean).join(' '),
[k.zip, k.city].filter(Boolean).join(' '), k.country].filter(Boolean).join(', ');
const Board = defineComponent({
setup() {
const api = useApi();
const kontakte = ref([]);
const loading = ref(false);
const fehler = ref('');
const meldung = ref('');
const suche = ref('');
const statusFilter = ref('');
const aktivId = ref(null);
const savedFlash = ref(null); // Feld-Key, der zuletzt gespeichert wurde
const aktionen = ref([]); // offene/erledigte Aktionen des aktiven Kontakts
const rueckruf = reactive({ open: false, date: '', time: '09:00' });
const mailMich = reactive({ open: false, note: '' });
async function laden() {
loading.value = true; fehler.value = '';
try {
const r = await api.get('/items/contacts', {
params: { limit: -1, sort: ['-sent_at', '-date_created'], fields: FIELDS },
});
kontakte.value = r.data.data || [];
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Fehler beim Laden.'; }
finally { loading.value = false; }
}
onMounted(laden);
const gefiltert = computed(() => {
const q = suche.value.trim().toLowerCase();
return kontakte.value.filter((k) => {
if (statusFilter.value && (k.lead_status || 'neu') !== statusFilter.value) return false;
if (!q) return true;
const hay = [k.salutation, k.first_name, k.last_name, k.company, k.zip, k.city,
k.email, k.phone, k.source_order?.order_number].filter(Boolean).join(' ').toLowerCase();
return hay.includes(q);
});
});
const aktiv = computed(() => kontakte.value.find((k) => k.id === aktivId.value) || null);
const aktivIndex = computed(() => gefiltert.value.findIndex((k) => k.id === aktivId.value));
async function save(id, patch, feldKey) {
try {
await api.patch(`/items/contacts/${id}`, patch);
const k = kontakte.value.find((x) => x.id === id);
if (k) Object.assign(k, patch);
if (feldKey) { savedFlash.value = feldKey; setTimeout(() => { if (savedFlash.value === feldKey) savedFlash.value = null; }, 1500); }
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Speichern fehlgeschlagen.'; }
}
// Speichert nur, wenn sich der Wert wirklich geändert hat (Blur-Handler).
const saveIfChanged = (k, key) => (ev) => {
const val = ev.target.value;
if ((k[key] || '') !== val) save(k.id, { [key]: val || null }, key);
};
// ── Standardaktionen (Rückruf / Angebot) ─────────────────────────────────
async function ladeAktionen(id) {
if (!id) { aktionen.value = []; return; }
try {
const r = await api.get('/items/contact_actions', {
params: { filter: { contact: { _eq: id } }, sort: ['-date_created'], limit: -1,
fields: ['id', 'type', 'due_date', 'note', 'status', 'notified'] },
});
aktionen.value = r.data.data || [];
} catch { aktionen.value = []; }
}
watch(aktivId, (id) => { rueckruf.open = false; rueckruf.date = ''; rueckruf.time = '09:00'; mailMich.open = false; mailMich.note = ''; ladeAktionen(id); });
const isoDatum = (d) => { const x = new Date(); x.setHours(0, 0, 0, 0); x.setDate(x.getDate() + d); return x; };
const ymd = (dt) => `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
async function planeRueckruf(k) {
if (!rueckruf.date) { fehler.value = 'Bitte ein Datum wählen.'; return; }
try {
await api.post('/items/contact_actions', {
contact: k.id, type: 'rueckruf', status: 'offen',
due_date: new Date(rueckruf.date + 'T' + (rueckruf.time || '09:00') + ':00').toISOString(),
});
rueckruf.open = false; rueckruf.date = ''; rueckruf.time = '09:00';
await ladeAktionen(k.id);
meldung.value = 'Rückruf-Erinnerung gesetzt.'; setTimeout(() => { meldung.value = ''; }, 2500);
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Konnte Rückruf nicht anlegen.'; }
}
async function sendeAngebot(k) {
try {
await api.post('/items/contact_actions', { contact: k.id, type: 'angebot', status: 'erledigt' });
await ladeAktionen(k.id);
meldung.value = 'Angebot-Eckdaten wurden dir per Mail geschickt.'; setTimeout(() => { meldung.value = ''; }, 3500);
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Konnte Angebot nicht senden.'; }
}
// Legt eine Musterbestellung an: erscheint als Musteranfrage in
// Produktion → Neu und wird über den bestehenden „Muster drucken"-Workflow
// gedruckt. Keine Mail an den Kontakt.
async function sendeMuster(k) {
const name = [k.salutation, k.first_name, k.last_name].filter(Boolean).join(' ') || k.company || 'Kontakt';
try {
await api.post('/items/offer_requests', {
type: 'muster', status: 'neu', name, product: 'Muster',
email: k.email || 'muster@skrift.de', phone: k.phone || null,
address: {
company: k.company || '', salutation: k.salutation || '',
first_name: k.first_name || '', last_name: k.last_name || '',
street: [k.street, k.house_no].filter(Boolean).join(' '),
zip: k.zip || '', city: k.city || '', country: k.country || '',
},
});
meldung.value = 'Musterbestellung angelegt – erscheint in Produktion → Neu.'; setTimeout(() => { meldung.value = ''; }, 3500);
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Konnte Muster nicht anlegen.'; }
}
// Schickt dir (interne Adresse) die Eckdaten des Kontakts + deine Notiz per Mail.
async function sendeMail(k) {
if (!mailMich.note.trim()) { fehler.value = 'Bitte eine Notiz eingeben.'; return; }
try {
await api.post('/items/contact_actions', { contact: k.id, type: 'mail', status: 'erledigt', note: mailMich.note });
mailMich.open = false; mailMich.note = '';
meldung.value = 'Deine Notiz wurde dir per Mail geschickt.'; setTimeout(() => { meldung.value = ''; }, 3500);
} catch (e) { fehler.value = e?.response?.data?.errors?.[0]?.message || e.message || 'Konnte Mail nicht senden.'; }
}
async function erledige(a, k) {
try { await api.patch(`/items/contact_actions/${a.id}`, { status: 'erledigt' }); await ladeAktionen(k.id); }
catch (e) { fehler.value = e?.message || 'Konnte nicht als erledigt markieren.'; }
}
function exportCsv() {
const rows = gefiltert.value;
const cols = [['salutation', 'Anrede'], ['first_name', 'Vorname'], ['last_name', 'Nachname'],
['company', 'Firma'], ['street', 'Straße'], ['house_no', 'Hausnr'], ['zip', 'PLZ'], ['city', 'Ort'],
['country', 'Land'], ['email', 'E-Mail'], ['phone', 'Telefon'], ['lead_status', 'Status'],
['notes', 'Notizen'], ['sent_at', 'Versendet am']];
const esc = (v) => { const s = v == null ? '' : String(v); return /[",\n;]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
const csv = '' + cols.map((c) => c[1]).join(',') + '\n'
+ rows.map((r) => cols.map((c) => esc(r[c[0]])).join(',')).join('\n');
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
const a = document.createElement('a');
a.href = url; a.download = `kontakte-${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
}
// ── Mehrfachauswahl (Bulk: Status ändern / löschen) ──
const auswahl = ref(new Set());
const lastIdx = ref(-1); // zuletzt geklickte Zeile (für Shift-Bereichsauswahl)
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; };
// Klick auf eine Karten-Checkbox: mit Shift den ganzen Bereich seit dem letzten Klick.
const klickSel = (id, ev) => {
const list = gefiltert.value;
const idx = list.findIndex((x) => x.id === id);
if (ev && ev.shiftKey && lastIdx.value >= 0 && idx >= 0) {
const a = Math.min(lastIdx.value, idx);
const b = Math.max(lastIdx.value, idx);
const s = new Set(auswahl.value);
for (let i = a; i <= b; i += 1) s.add(list[i].id);
auswahl.value = s;
} else {
toggleSel(id);
}
lastIdx.value = idx;
};
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;'
+ (istGewaehlt(k.id) ? 'outline:2px solid var(--primary,#1E50A5);' : ''),
}, [
h('input', {
type: 'checkbox', checked: istGewaehlt(k.id),
onClick: (e) => { e.stopPropagation(); klickSel(k.id, e); },
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,
]),
h('div', { style: 'display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex-shrink:0;' }, [
chip(LEAD_LABEL[k.lead_status || 'neu'], LEAD_COLOR[k.lead_status || 'neu']),
]),
]);
}
function listenAnsicht() {
const kopf = h('div', { style: 'display:flex;flex-direction:column;gap:10px;margin-bottom:16px;' }, [
h('input', {
type: 'search', value: suche.value, placeholder: '🔎 Name, Ort, Telefon, E-Mail …',
style: S_INPUT, onInput: (e) => { suche.value = e.target.value; },
}),
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;' }, [
h('select', {
style: S_INPUT + 'width:auto;flex:1;min-width:150px;cursor:pointer;',
onChange: (e) => { statusFilter.value = e.target.value; },
}, [
h('option', { value: '', selected: statusFilter.value === '' }, 'Alle Status'),
...LEAD.map((l) => h('option', { value: l.key, selected: statusFilter.value === l.key }, l.label)),
]),
]),
h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:space-between;' }, [
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;background:#fff;color:#1a1a1a;', value: '',
onChange: (e) => { if (e.target.value) { bulkStatus(e.target.value); e.target.value = ''; } },
}, [h('option', { value: '', style: 'color:#1a1a1a;' }, 'ändern auf …'),
...LEAD.map((l) => h('option', { value: l.key, style: 'color:#1a1a1a;background:#fff;' }, 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, bulk, liste, hinweis].filter(Boolean);
}
// ── Detail ──
function detailAnsicht() {
const k = aktiv.value;
if (!k) { aktivId.value = null; return listenAnsicht(); }
const idx = aktivIndex.value;
const gesamt = gefiltert.value.length;
const geh = (d) => {
const l = gefiltert.value;
let i = aktivIndex.value; if (i < 0) return;
i += d; if (i < 0 || i >= l.length) return;
aktivId.value = l[i].id; fehler.value = '';
};
const flash = (key) => savedFlash.value === key ? h('span', { style: 'color:#2e7d32;font-size:12px;margin-left:8px;' }, '✓ gespeichert') : null;
const navRow = h('div', { style: 'display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:14px;' }, [
btn('◀ Liste', () => { aktivId.value = null; }, 'ghost'),
h('div', { style: 'display:flex;align-items:center;gap:8px;' }, [
h('button', { type: 'button', disabled: idx <= 0, onClick: () => geh(-1),
style: 'cursor:pointer;border:1px solid var(--border-normal);background:var(--background-subdued);border-radius:8px;padding:8px 12px;font-size:16px;' + (idx <= 0 ? 'opacity:.4;cursor:not-allowed;' : '') }, '◀'),
subdued(gesamt ? `${idx + 1} / ${gesamt}` : '–'),
h('button', { type: 'button', disabled: idx >= gesamt - 1, onClick: () => geh(1),
style: 'cursor:pointer;border:1px solid var(--border-normal);background:var(--background-subdued);border-radius:8px;padding:8px 12px;font-size:16px;' + (idx >= gesamt - 1 ? 'opacity:.4;cursor:not-allowed;' : '') }, '▶'),
]),
]);
const kopf = h('div', { style: 'display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:14px;' }, [
h('div', {}, [
h('div', { style: 'font-size:22px;font-weight:700;' }, anzeigeName(k)),
adresse(k) ? subdued(adresse(k), 'margin-top:2px;') : null,
k.source_order?.order_number ? subdued('aus Auftrag ' + k.source_order.order_number, 'font-size:12px;margin-top:2px;') : null,
]),
chip(LEAD_LABEL[k.lead_status || 'neu'], LEAD_COLOR[k.lead_status || 'neu']),
]);
// Anruf-Button (GroundWire) – nur wenn eine Nummer da ist.
const anruf = k.phone
? h('a', { href: DIAL(k.phone),
style: `display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none;background:#2e7d32;color:#fff;border-radius:10px;padding:14px;font-size:17px;font-weight:600;margin-bottom:14px;` },
[h('span', {}, '📞'), h('span', {}, 'Anrufen · ' + k.phone)])
: subdued('Keine Telefonnummer – unten eintragen, dann erscheint der Anruf-Button.', 'margin-bottom:14px;');
const telFeld = feld(h('span', {}, ['Telefon', flash('phone')]),
h('input', { type: 'tel', value: k.phone || '', placeholder: '+49 …', style: S_INPUT, onBlur: saveIfChanged(k, 'phone') }));
const mailFeld = feld(h('span', {}, ['E-Mail', flash('email')]),
h('div', { style: 'display:flex;gap:8px;align-items:center;' }, [
h('input', { type: 'email', value: k.email || '', placeholder: 'name@…', style: S_INPUT, onBlur: saveIfChanged(k, 'email') }),
k.email ? h('a', { href: 'mailto:' + k.email, style: 'text-decoration:none;font-size:20px;' }, '✉') : null,
]));
const statusFeld = feld('Lead-Status',
h('select', { style: S_INPUT + 'cursor:pointer;', onChange: (e) => save(k.id, { lead_status: e.target.value }, 'lead_status') },
LEAD.map((l) => h('option', { value: l.key, selected: (k.lead_status || 'neu') === l.key }, l.label))));
const notizFeld = feld(h('span', {}, ['Notizen', flash('notes')]),
h('textarea', { value: k.notes || '', rows: 5, placeholder: 'Gesprächsnotizen …',
style: S_INPUT + 'resize:vertical;line-height:1.5;', onBlur: saveIfChanged(k, 'notes') }));
const namensFelder = h('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:10px;' }, [
feld('Vorname', h('input', { value: k.first_name || '', style: S_INPUT, onBlur: saveIfChanged(k, 'first_name') })),
feld('Nachname', h('input', { value: k.last_name || '', style: S_INPUT, onBlur: saveIfChanged(k, 'last_name') })),
feld('Firma', h('input', { value: k.company || '', style: S_INPUT, onBlur: saveIfChanged(k, 'company') })),
feld('Ort', h('input', { value: k.city || '', style: S_INPUT, onBlur: saveIfChanged(k, 'city') })),
]);
// ── Standardaktionen (deutlich als farbige Buttons) ──
const aktBase = 'cursor:pointer;border-radius:8px;padding:11px 15px;font:inherit;font-size:14px;font-weight:600;'
+ 'border:1.5px solid;text-decoration:none;display:inline-flex;align-items:center;gap:6px;';
const aktStil = (rand, grund, text) => aktBase + `border-color:${rand};background:${grund};color:${text};`;
const S_RUECK = aktStil(ACCENT, 'rgba(30,80,165,.10)', ACCENT);
const S_ANGEBOT = aktStil('#e07b00', 'rgba(224,123,0,.12)', '#b35f00');
const S_MUSTER = aktStil('#6b3fa0', 'rgba(107,63,160,.12)', '#5a2f90');
const S_MAIL = aktStil('#0b7285', 'rgba(11,114,133,.12)', '#0b6273');
const aktionsLeiste = h('div', { class: 'sk-akt' }, [
h('button', { type: 'button', style: rueckruf.open ? aktStil(ACCENT, ACCENT, '#fff') : S_RUECK, onClick: () => { rueckruf.open = !rueckruf.open; if (rueckruf.open && !rueckruf.date) rueckruf.date = ymd(isoDatum(1)); } }, '📞 Rückruf planen'),
h('button', { type: 'button', style: S_ANGEBOT, onClick: () => sendeAngebot(k) }, '📧 Angebot senden'),
h('button', { type: 'button', style: S_MUSTER, onClick: () => sendeMuster(k) }, '🧪 Muster senden'),
h('button', { type: 'button', style: mailMich.open ? aktStil('#0b7285', '#0b7285', '#fff') : S_MAIL, onClick: () => { mailMich.open = !mailMich.open; } }, '✉️ Mail an mich'),
]);
const preset = (label, d) => h('button', { type: 'button',
style: 'cursor:pointer;border-radius:20px;padding:6px 12px;font:inherit;font-size:13px;border:1px solid var(--border-normal);'
+ (rueckruf.date === ymd(isoDatum(d)) ? `background:${ACCENT};color:#fff;` : 'background:var(--background-page);color:var(--foreground-normal);'),
onClick: () => { rueckruf.date = ymd(isoDatum(d)); } }, label);
const rueckrufForm = rueckruf.open ? h('div', { style: 'border:1px solid var(--border-normal);border-radius:10px;padding:14px;margin-top:10px;margin-bottom:12px;background:var(--background-subdued);' }, [
h('div', { style: 'display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;' }, [preset('Morgen', 1), preset('In 3 Tagen', 3), preset('Nächste Woche', 7)]),
h('div', { style: 'display:flex;gap:10px;flex-wrap:wrap;' }, [
h('div', { style: 'flex:2;min-width:150px;' }, feld('Datum', h('input', { type: 'date', value: rueckruf.date, style: S_INPUT, onInput: (e) => { rueckruf.date = e.target.value; } }))),
h('div', { style: 'flex:1;min-width:110px;' }, feld('Uhrzeit', h('input', { type: 'time', value: rueckruf.time, style: S_INPUT, onInput: (e) => { rueckruf.time = e.target.value; } }))),
]),
h('div', { style: 'display:flex;gap:8px;' }, [btn('Erinnerung setzen', () => planeRueckruf(k), 'primary'), btn('Abbrechen', () => { rueckruf.open = false; }, 'ghost')]),
]) : null;
const mailForm = mailMich.open ? h('div', { style: 'border:1px solid var(--border-normal);border-radius:10px;padding:14px;margin-top:10px;margin-bottom:12px;background:var(--background-subdued);' }, [
feld('Notiz an mich', h('textarea', { value: mailMich.note, rows: 3, placeholder: 'Was soll dir per Mail zugehen? (Eckdaten kommen automatisch mit)', style: S_INPUT + 'resize:vertical;', onInput: (e) => { mailMich.note = e.target.value; } })),
h('div', { style: 'display:flex;gap:8px;' }, [btn('Mail an mich senden', () => sendeMail(k), 'primary'), btn('Abbrechen', () => { mailMich.open = false; }, 'ghost')]),
]) : null;
const offene = aktionen.value.filter((a) => a.type === 'rueckruf' && a.status === 'offen');
const offeneListe = offene.length ? h('div', { style: 'margin-bottom:12px;display:flex;flex-direction:column;gap:6px;' }, offene.map((a) => h('div', {
style: 'display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--border-normal);border-radius:8px;padding:8px 12px;background:var(--background-page);',
}, [
h('div', { style: 'font-size:13px;' }, [
h('span', { style: 'font-weight:600;' }, '📞 Rückruf ' + (a.due_date ? new Date(a.due_date).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) : '')),
a.note ? h('div', { style: 'color:var(--foreground-subdued);margin-top:2px;white-space:pre-wrap;' }, a.note) : null,
]),
btn('✓ erledigt', () => erledige(a, k), 'default'),
]))) : null;
const aktionenBlock = h('div', { style: 'margin-bottom:14px;' }, [
h('div', { style: LBL }, 'Aktionen'), aktionsLeiste, rueckrufForm, mailForm, offeneListe,
]);
return [navRow, h('div', { style: CARD }, [kopf, anruf, notizFeld, statusFeld, aktionenBlock, telFeld, mailFeld, namensFelder])];
}
return () => {
const inhalt = h('div', { style: 'max-width:760px;margin:0 auto;padding:20px 16px 60px;' }, [
// Aktions-Buttons: am Desktop nebeneinander (gleich breit), am Handy gestapelt in voller Breite.
h('style', {}, '.sk-akt{display:flex;gap:8px;flex-wrap:wrap;}.sk-akt>*{flex:1 1 auto;justify-content:center;}@media(max-width:600px){.sk-akt{flex-direction:column;}.sk-akt>*{width:100%;flex:none;}}'),
fehler.value ? h('div', { style: 'background:var(--danger-25,rgba(198,40,40,.12));color:var(--danger,#c62828);padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, fehler.value) : null,
meldung.value ? h('div', { style: 'background:var(--success-25,rgba(46,125,50,.15));color:var(--success,#2e7d32);padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px;' }, meldung.value) : null,
aktivId.value ? detailAnsicht() : listenAnsicht(),
]);
return h(resolveComponent('private-view'), { title: 'Kontakte' }, {
default: () => h('div', { style: 'min-height:100%;background:var(--background-subdued,#f1f4f9);' }, [inhalt]),
});
};
},
});
export default {
id: 'skrift-kontakte',
name: 'Kontakte',
icon: 'contacts',
routes: [{ path: '', component: Board }],
};