/* ============================================================ BAN YARO — Tagebuch (Sprint 1) Seiten-Modul: Timeline aller Einträge, Erstellen, Bearbeiten, Löschen, Foto-Upload, Meilensteine. ============================================================ */ window.Page_diary = (() => { // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- let _container = null; let _appState = null; let _entries = []; let _offset = 0; const LIMIT = 20; const TYPEN = { eintrag: { label: 'Eintrag', icon: '📖' }, foto: { label: 'Foto', icon: '📷' }, meilenstein:{ label: 'Meilenstein',icon: '🏆' }, training: { label: 'Training', icon: '🎯' }, gesundheit: { label: 'Gesundheit', icon: '💉' }, ausflug: { label: 'Ausflug', icon: '🚗' }, }; // ---------------------------------------------------------- // INIT — erster Aufruf, Container leer // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; await _render(); } // ---------------------------------------------------------- // REFRESH — erneuter Navigations-Aufruf // ---------------------------------------------------------- async function refresh() { if (!_appState.activeDog) return; _offset = 0; _entries = []; await _load(); _renderList(); } // ---------------------------------------------------------- // ON DOG CHANGE // ---------------------------------------------------------- async function onDogChange(dog) { _offset = 0; _entries = []; await _render(); } // ---------------------------------------------------------- // OPEN NEW — vom + Button oder Quick-Add // ---------------------------------------------------------- function openNew() { _showForm(null); } // ---------------------------------------------------------- // RENDER — Hauptstruktur // ---------------------------------------------------------- async function _render() { if (!_appState.activeDog) { _container.innerHTML = UI.emptyState({ icon: '🐕', title: 'Noch kein Hund angelegt', text: 'Erstelle zuerst ein Hundeprofil, um das Tagebuch zu nutzen.', action: ``, }); _container.querySelector('#diary-goto-profile') ?.addEventListener('click', () => App.navigate('dog-profile')); return; } _container.innerHTML = `
`; _container.querySelector('#diary-btn-more') ?.addEventListener('click', () => _loadMore()); await _load(); _renderList(); } // ---------------------------------------------------------- // DATEN LADEN // ---------------------------------------------------------- async function _load() { const dog = _appState.activeDog; if (!dog) return; try { const batch = await API.diary.list(dog.id, { limit: LIMIT, offset: _offset }); _entries = _entries.concat(batch); // "Mehr laden" anzeigen wenn volle Page geladen wurde const loadMore = _container.querySelector('#diary-load-more'); if (loadMore) { loadMore.style.display = batch.length === LIMIT ? 'block' : 'none'; } } catch (err) { UI.toast.error('Einträge konnten nicht geladen werden.'); } } async function _loadMore() { _offset += LIMIT; const btn = _container.querySelector('#diary-btn-more'); UI.setLoading(btn, true); await _load(); _renderList(); UI.setLoading(btn, false); } // ---------------------------------------------------------- // LISTE RENDERN — Timeline gruppiert nach Monat // ---------------------------------------------------------- function _renderList() { const listEl = _container.querySelector('#diary-list'); if (!listEl) return; if (_entries.length === 0) { listEl.innerHTML = UI.emptyState({ icon: '📖', title: 'Noch keine Einträge', text: 'Halte besondere Momente mit deinem Hund fest.', action: ``, }); listEl.querySelector('#diary-first-entry') ?.addEventListener('click', () => _showForm(null)); return; } // Gruppieren nach Jahr-Monat (Anzeigereihenfolge: chronologisch absteigend) const groups = new Map(); _entries.forEach(e => { const key = e.datum ? e.datum.slice(0, 7) : 'unbekannt'; // "2025-04" if (!groups.has(key)) groups.set(key, []); groups.get(key).push(e); }); let html = ''; groups.forEach((items, key) => { const monthLabel = key === 'unbekannt' ? 'Datum unbekannt' : _formatMonth(key); html += `
${monthLabel}
`; html += items.map(e => _entryCard(e)).join(''); }); listEl.innerHTML = html; // Events an Karten binden listEl.querySelectorAll('[data-entry-id]').forEach(card => { const id = parseInt(card.dataset.entryId); card.addEventListener('click', () => _openDetail(id)); }); } // ---------------------------------------------------------- // ENTRY CARD // ---------------------------------------------------------- function _entryCard(e) { const typ = TYPEN[e.typ] || TYPEN.eintrag; const isMile = e.is_milestone || e.typ === 'meilenstein'; const dateStr = e.datum ? UI.time.format(e.datum + 'T00:00:00') : ''; const tags = (e.tags || []).slice(0, 4); const photo = e.media_url ? `
Foto
` : ''; const tagsHtml = tags.length ? `
${tags.map(t => `${t}`).join('')}
` : ''; const textPreview = e.text ? `

${_escape(e.text.slice(0, 140))}${e.text.length > 140 ? '…' : ''}

` : ''; return `
${photo}
${typ.icon} ${typ.label} ${dateStr}
${e.titel ? `
${_escape(e.titel)}
` : ''} ${textPreview} ${tagsHtml}
`; } // ---------------------------------------------------------- // DETAIL-ANSICHT // ---------------------------------------------------------- function _openDetail(entryId) { const entry = _entries.find(e => e.id === entryId); if (!entry) return; const typ = TYPEN[entry.typ] || TYPEN.eintrag; const isMile = entry.is_milestone || entry.typ === 'meilenstein'; const tags = (entry.tags || []); const photo = entry.media_url ? `Foto` : ''; const body = ` ${isMile ? '
🏆 Meilenstein
' : ''} ${photo}
${typ.icon} ${typ.label} ${entry.datum ? UI.time.format(entry.datum + 'T00:00:00') : ''}
${entry.text ? `

${_escape(entry.text)}

` : ''} ${tags.length ? `
${tags.map(t => `${t}`).join('')}
` : ''}
`; UI.modal.open({ title: entry.titel || typ.label, body }); document.getElementById('detail-edit')?.addEventListener('click', () => { UI.modal.close(); _showForm(entry); }); document.getElementById('detail-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: 'Eintrag löschen?', message: 'Dieser Vorgang kann nicht rückgängig gemacht werden.', confirmText: 'Löschen', danger: true, }); if (ok) { await _deleteEntry(entryId); } }); } // ---------------------------------------------------------- // FORMULAR — Neu erstellen / Bearbeiten // ---------------------------------------------------------- function _showForm(entry) { const isEdit = !!entry; const today = new Date().toISOString().slice(0, 10); const typOpts = Object.entries(TYPEN) .map(([val, { icon, label }]) => ``) .join(''); const body = `
${!isEdit ? `
` : ''}
`; UI.modal.open({ title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag', body }); const form = document.getElementById('diary-form'); // Foto-Vorschau const photoInput = form.querySelector('[name="photo"]'); const photoPreview = document.getElementById('diary-photo-preview'); if (photoInput && photoPreview) { UI.setupPhotoPreview(photoInput, photoPreview); photoInput.addEventListener('change', () => { photoPreview.style.display = photoInput.files[0] ? 'block' : 'none'; }); } document.getElementById('diary-form-cancel')?.addEventListener('click', UI.modal.close); form.addEventListener('submit', async e => { e.preventDefault(); const submitBtn = form.querySelector('[type="submit"]'); const fd = UI.formData(form); await UI.asyncButton(submitBtn, async () => { const payload = { datum: fd.datum || null, typ: fd.typ, titel: fd.titel || null, text: fd.text || null, is_milestone: 'is_milestone' in fd, }; if (isEdit) { const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload); _updateEntryInList(updated); UI.toast.success('Eintrag gespeichert.'); } else { const created = await API.diary.create(_appState.activeDog.id, payload); // Foto hochladen wenn vorhanden if (photoInput?.files[0]) { try { const formData = new FormData(); formData.append('file', photoInput.files[0]); const media = await API.diary.uploadMedia( _appState.activeDog.id, created.id, formData ); created.media_url = media.media_url; } catch { UI.toast.warning('Eintrag erstellt, Foto konnte nicht hochgeladen werden.'); } } _entries.unshift(created); UI.toast.success('Eintrag erstellt.'); } UI.modal.close(); _renderList(); }); }); } // ---------------------------------------------------------- // EINTRAG LÖSCHEN // ---------------------------------------------------------- async function _deleteEntry(entryId) { try { await API.diary.delete(_appState.activeDog.id, entryId); _entries = _entries.filter(e => e.id !== entryId); UI.modal.close(); _renderList(); UI.toast.success('Eintrag gelöscht.'); } catch (err) { UI.toast.error(err.message || 'Fehler beim Löschen.'); } } // ---------------------------------------------------------- // HELPER // ---------------------------------------------------------- function _updateEntryInList(updated) { const i = _entries.findIndex(e => e.id === updated.id); if (i !== -1) _entries[i] = updated; } function _formatMonth(yearMonth) { const [y, m] = yearMonth.split('-'); return new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' }) .format(new Date(+y, +m - 1, 1)); } function _escape(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"'); } // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- return { init, refresh, openNew, onDogChange }; })();