/** * Modul: Kontakte (Contacts) * Zweck: Kontaktliste mit Kategorie-Filter, Suche, CRUD, tel:/mailto:/maps-Links * Abhängigkeiten: /api.js, /router.js (window.oikos) */ import { api } from '/api.js'; // -------------------------------------------------------- // Konstanten // -------------------------------------------------------- const CATEGORIES = ['Arzt', 'Schule/Kita', 'Behörde', 'Versicherung', 'Handwerker', 'Notfall', 'Sonstiges']; const CATEGORY_ICONS = { 'Arzt': '🏥', 'Schule/Kita': '🏫', 'Behörde': '🏛️', 'Versicherung': '🛡️', 'Handwerker': '🔧', 'Notfall': '🚨', 'Sonstiges': '📋', }; // -------------------------------------------------------- // State // -------------------------------------------------------- let state = { contacts: [], activeCategory: null, searchQuery: '', }; // -------------------------------------------------------- // Entry Point // -------------------------------------------------------- export async function render(container, { user }) { container.innerHTML = `
${CATEGORIES.map((c) => ` `).join('')}
`; if (window.lucide) lucide.createIcons(); const res = await api.get('/contacts'); state.contacts = res.data; renderList(); // Suche let searchTimer; document.getElementById('contacts-search').addEventListener('input', (e) => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { state.searchQuery = e.target.value.trim(); renderList(); }, 200); }); // Kategorie-Filter document.getElementById('contacts-filters').addEventListener('click', (e) => { const chip = e.target.closest('[data-cat]'); if (!chip) return; document.querySelectorAll('.contact-filter-chip').forEach((c) => c.classList.toggle('contact-filter-chip--active', c === chip) ); state.activeCategory = chip.dataset.cat || null; renderList(); }); // Neu document.getElementById('contacts-add-btn').addEventListener('click', () => openModal({ mode: 'create' }) ); } // -------------------------------------------------------- // Liste rendern // -------------------------------------------------------- function filterContacts() { let list = state.contacts; if (state.activeCategory) { list = list.filter((c) => c.category === state.activeCategory); } if (state.searchQuery) { const q = state.searchQuery.toLowerCase(); list = list.filter((c) => c.name.toLowerCase().includes(q) || (c.phone && c.phone.toLowerCase().includes(q)) || (c.email && c.email.toLowerCase().includes(q)) ); } return list; } function renderList() { const container = document.getElementById('contacts-list'); if (!container) return; const contacts = filterContacts(); if (!contacts.length) { container.innerHTML = `
Keine Kontakte gefunden
`; if (window.lucide) lucide.createIcons(); return; } // Nach Kategorie gruppieren const groups = {}; for (const c of contacts) { if (!groups[c.category]) groups[c.category] = []; groups[c.category].push(c); } container.innerHTML = Object.entries(groups) .sort(([a], [b]) => CATEGORIES.indexOf(a) - CATEGORIES.indexOf(b)) .map(([cat, items]) => `
${CATEGORY_ICONS[cat] || ''} ${escHtml(cat)}
${items.map((c) => renderContactItem(c)).join('')}
`).join(''); if (window.lucide) lucide.createIcons(); // Event-Delegation container.addEventListener('click', async (e) => { if (e.target.closest('[data-action="delete"]')) { const id = parseInt(e.target.closest('[data-action="delete"]').dataset.id, 10); await deleteContact(id); return; } const item = e.target.closest('.contact-item[data-id]'); if (item && !e.target.closest('a') && !e.target.closest('[data-action]')) { const c = state.contacts.find((c) => c.id === parseInt(item.dataset.id, 10)); if (c) openModal({ mode: 'edit', contact: c }); } }); } function renderContactItem(c) { const phone = c.phone ? `` : ''; const email = c.email ? `` : ''; const maps = c.address ? `` : ''; const meta = [c.phone, c.email].filter(Boolean).join(' · '); return `
${CATEGORY_ICONS[c.category] || '📋'}
${escHtml(c.name)}
${meta ? `
${escHtml(meta)}
` : ''}
${phone}${email}${maps}
`; } // -------------------------------------------------------- // Modal // -------------------------------------------------------- function openModal({ mode, contact = null }) { document.getElementById('contact-modal-overlay')?.remove(); const isEdit = mode === 'edit'; const v = (field) => escHtml(isEdit && contact[field] ? contact[field] : ''); const catOpts = CATEGORIES.map((c) => `` ).join(''); const overlay = document.createElement('div'); overlay.id = 'contact-modal-overlay'; overlay.className = 'contact-modal-overlay'; overlay.innerHTML = ` `; document.body.appendChild(overlay); if (window.lucide) lucide.createIcons(); overlay.querySelector('#cm-close').addEventListener('click', () => overlay.remove()); overlay.querySelector('#cm-cancel').addEventListener('click', () => overlay.remove()); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); overlay.querySelector('#cm-delete')?.addEventListener('click', async () => { if (!confirm(`"${contact.name}" wirklich löschen?`)) return; overlay.remove(); await deleteContact(contact.id); }); overlay.querySelector('#cm-save').addEventListener('click', async () => { const saveBtn = overlay.querySelector('#cm-save'); const name = overlay.querySelector('#cm-name').value.trim(); const category = overlay.querySelector('#cm-category').value; const phone = overlay.querySelector('#cm-phone').value.trim() || null; const email = overlay.querySelector('#cm-email').value.trim() || null; const address = overlay.querySelector('#cm-address').value.trim() || null; const notes = overlay.querySelector('#cm-notes').value.trim() || null; if (!name) { window.oikos?.showToast('Name ist erforderlich', 'error'); return; } saveBtn.disabled = true; saveBtn.textContent = '…'; try { const body = { name, category, phone, email, address, notes }; if (mode === 'create') { const res = await api.post('/contacts', body); state.contacts.push(res.data); state.contacts.sort((a, b) => CATEGORIES.indexOf(a.category) - CATEGORIES.indexOf(b.category) || a.name.localeCompare(b.name) ); } else { const res = await api.put(`/contacts/${contact.id}`, body); const idx = state.contacts.findIndex((c) => c.id === contact.id); if (idx !== -1) state.contacts[idx] = res.data; } overlay.remove(); renderList(); window.oikos?.showToast(mode === 'create' ? 'Kontakt gespeichert' : 'Kontakt aktualisiert', 'success'); } catch (err) { window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error'); saveBtn.disabled = false; saveBtn.textContent = isEdit ? 'Speichern' : 'Erstellen'; } }); overlay.querySelector('#cm-name').focus(); } async function deleteContact(id) { if (!confirm('Kontakt wirklich löschen?')) return; try { await api.delete(`/contacts/${id}`); state.contacts = state.contacts.filter((c) => c.id !== id); renderList(); window.oikos?.showToast('Kontakt gelöscht', 'success'); } catch (err) { window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error'); } } function escHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }