Import aus Monorepo (frischer Start)

This commit is contained in:
Lucas Orth committed 2026-08-24 12:13:58 +02:00
commit 94e79f12ac
34 files changed
+7438

No files matched your search

@@ -0,0 +1,397 @@
/**
* 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);
}
// ── 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;',
}, [
h('div', { style: 'min-width:0;' }, [
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;' }, [
subdued(`${gefiltert.value.length} Kontakt(e)`),
h('div', { style: 'display:flex;gap:8px;' }, [
btn('↻ Aktualisieren', laden),
btn('⭳ CSV-Export', exportCsv),
]),
]),
]);
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];
}
// ── 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 }],
};