/** * 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 }], };