Kontakte/CRM (Directus): - Neue Collections `contacts` (Leads) + `contact_actions` (Rückruf/Angebot/Mail). - Hook `skrift-contacts`: Kontakte werden nur auf Anforderung angelegt (Auftrag "In Kontakte speichern"); Rückruf-Erinnerung (Zeitplan) und Angebots-/Mail-an-mich-Mails; Auto-Löschung nur bei Lead-Status "kein_interesse" nach 30 Tagen; optionaler DSGVO-Strip (per Env, Dry-Run). - Modul `skrift-kontakte`: handytaugliches Board (Anruf via groundwire:, Notizen mit Autosave, Durchblättern, CSV-Export, Aktionen als Buttons). API-Chargen & PDF-Sammeldruck: - Produktions-Board gruppiert API-Aufträge unter "Neu" zu Chargen (Kunde + Typ + Versandtag + Eingangstag), aufklappbar, mit "fällig bis" (Versandtag der übernächsten Woche). - Mehrfachauswahl -> Sammel-PDF (Backend `pdf-lib`, /api/charge/merge-pdf, Directus-Proxy /skrift-orders/charge/pdf), Kuverts an Plotter und Sammel-Status; PDF- und Kuvert-Reihenfolge nach Auftragsnummer (Kollation). - Mailer: keine Einzelmail je API-Auftrag mehr, stattdessen Tagesübersicht (18:00) pro Kunde, nach Versandtag aufgeschlüsselt. Status-API & Generierung: - Public-API-Status liest jetzt production_status (Board) statt nur kaufmännischem Status; neues Feld `status_since` in der Antwort. - Auftrag stempelt `production_status_changed_at` bei jedem Statuswechsel. - Generierung wird IMMER beim Eingang angestoßen (auch Datei-Produkte mit handgeschriebenem Kuvert), nicht nur bei letter/postcard. - Versandtag im Produktionsdashboard sichtbar. Rechte: Service-Rolle darf directus_files lesen (Backend lädt PDFs für den Sammeldruck über /assets). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
398 lines
25 KiB
JavaScript
398 lines
25 KiB
JavaScript
/**
|
||
* 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 }],
|
||
};
|