/* ============================================================ 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; let _searchQuery = ''; let _filterMilestone = false; const LIMIT = 20; function _sourceIcon(source) { if (source === 'places') return 'star'; if (source === 'osm') return 'map-pin'; return 'map-trifold'; } // ---------------------------------------------------------- // DATUM-HILFSFUNKTIONEN (Day One Style) // ---------------------------------------------------------- const _WOCHENTAG = ['SO.', 'MO.', 'DI.', 'MI.', 'DO.', 'FR.', 'SA.']; function _weekday(datum) { if (!datum) return ''; return _WOCHENTAG[new Date(datum + 'T12:00').getDay()]; } function _dayNum(datum) { return datum ? String(parseInt(datum.slice(8, 10), 10)) : ''; } function _timeStr(createdAt) { if (!createdAt) return ''; const t = createdAt.includes('T') ? createdAt : createdAt.replace(' ', 'T'); const d = new Date(t); return isNaN(d) ? '' : d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } function _formatDateLong(datum) { if (!datum) return ''; const d = new Date(datum + 'T12:00'); return d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); } // WMO-Wettercodes → Emoji für Badges const _WMO_EMOJI = { 0: '☀️', 1: '🌤️', 2: '⛅', 3: '☁️', 45: '🌫️', 48: '🌫️', 51: '🌦️', 53: '🌦️', 55: '🌧️', 61: '🌦️', 63: '🌧️', 65: '🌧️', 71: '🌨️', 73: '❄️', 75: '❄️', 77: '🌨️', 80: '🌦️', 81: '🌧️', 82: '⛈️', 85: '🌨️', 86: '❄️', 95: '⛈️', 96: '⛈️', 99: '⛈️', }; function _weatherEmoji(code, isDay) { if (code === 0 && isDay === false) return '🌙'; return _WMO_EMOJI[code] || '🌡️'; } /** Wetter-Badge HTML für Karten (kompakt) */ function _weatherBadgeHtml(entry) { if (!entry.weather_json) return ''; let w; try { w = typeof entry.weather_json === 'string' ? JSON.parse(entry.weather_json) : entry.weather_json; } catch (_) { return ''; } if (!w || w.temp_c == null) return ''; const emoji = _weatherEmoji(w.weathercode, w.is_day); const temp = Math.round(w.temp_c); return `${emoji} ${temp}°`; } /** POI-Chips HTML (max. 3 Items) */ function _poiChipsHtml(entry, maxItems = 3) { if (!entry.poi_json) return ''; let pois; try { pois = typeof entry.poi_json === 'string' ? JSON.parse(entry.poi_json) : entry.poi_json; } catch (_) { return ''; } if (!Array.isArray(pois) || pois.length === 0) return ''; const _poiEmoji = (type) => { if (!type) return '📍'; const t = type.toLowerCase(); if (t === 'veterinary' || t === 'hospital') return '🏥'; if (t === 'pet_shop' || t === 'shop') return '🛒'; if (t === 'park' || t === 'leisure' || t === 'garden') return '🌳'; if (t === 'restaurant' || t === 'cafe' || t === 'bar') return '☕'; if (t === 'historic' || t === 'monument') return '🏛️'; if (t === 'tourism' || t === 'attraction') return '🗺️'; if (t === 'playground') return '🛝'; return '📍'; }; const chips = pois.slice(0, maxItems) .map(p => `${_poiEmoji(p.type)} ${UI.escape(p.name)}`) .join(' · '); return `

${chips}

`; } const _VIDEO_EXT = new Set(['.mp4','.mov','.webm','.m4v','.avi']); function _isVideo(url) { if (!url) return false; return _VIDEO_EXT.has(url.slice(url.lastIndexOf('.')).toLowerCase()); } function _videoPoster(url) { return url.replace(/\.[^.]+$/, '_thumb.jpg'); } function _mediaHtml(url, style = '') { if (!url) return ''; return _isVideo(url) ? `` : `Foto`; } /** Alle Mediendateien eines Eintrags normalisiert als Array zurückgeben. * Rückwärtskompatibel: wenn media_items leer, aber media_url gesetzt → altes Format. */ function _allMedia(entry) { const items = entry.media_items || []; if (items.length > 0) return items; if (entry.media_url) { return [{ id: null, url: entry.media_url, media_type: _isVideo(entry.media_url) ? 'video' : 'image', sort_order: 0 }]; } return []; } const TYPEN = { eintrag: { label: 'Eintrag', icon: '' }, foto: { label: 'Foto', icon: '' }, meilenstein:{ label: 'Meilenstein',icon: '' }, training: { label: 'Training', icon: '' }, gesundheit: { label: 'Gesundheit', icon: '' }, spaziergang:{ label: 'Spaziergang', icon: '' }, ausflug: { label: 'Ausflug', icon: '' }, }; // ---------------------------------------------------------- // INIT — erster Aufruf, Container leer // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; UI.loadLeaflet(); // Leaflet im Hintergrund vorladen — kein await await _render(); } // ---------------------------------------------------------- // REFRESH — erneuter Navigations-Aufruf (Tap auf Tab) // ---------------------------------------------------------- async function refresh() { if (!_appState.activeDog) return; _offset = 0; _entries = []; _totalStats = null; await _renderDiary(); } // ---------------------------------------------------------- // ON DOG CHANGE — vom Header-Switcher ausgelöst // ---------------------------------------------------------- async function onDogChange(dog) { _offset = 0; _entries = []; _searchQuery = ''; await _renderDiary(); } // ---------------------------------------------------------- // OPEN NEW — vom + Button oder Quick-Add // ---------------------------------------------------------- function openNew() { _showForm(null); } // ---------------------------------------------------------- // RENDER — Einstieg: Picker bei mehreren Hunden, sonst direkt // ---------------------------------------------------------- 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; } await _renderDiary(); } // ---------------------------------------------------------- // DIARY-ANSICHT — Timeline mit Einträgen // ---------------------------------------------------------- async function _renderDiary() { _container.innerHTML = ` ${UI.dogChip(_appState)}
`; UI.bindDogChip(_container, _appState); _container.querySelector('#diary-milestone-filter') ?.addEventListener('click', async () => { _filterMilestone = !_filterMilestone; _offset = 0; _entries = []; const btn = _container.querySelector('#diary-milestone-filter'); btn?.classList.toggle('btn-active', _filterMilestone); await _load(); _renderList(); }); _container.querySelector('#diary-import-btn') ?.addEventListener('click', _showImport); _container.querySelector('#diary-fab') ?.addEventListener('click', () => _showForm(null)); _container.querySelector('#diary-btn-more') ?.addEventListener('click', () => _loadMore()); // Suche mit Debounce let _searchTimer = null; _container.querySelector('#diary-search-input') ?.addEventListener('input', e => { clearTimeout(_searchTimer); _searchTimer = setTimeout(async () => { _offset = 0; _entries = []; _searchQuery = e.target.value.trim(); await _load(); _renderList(); }, 350); }); await Promise.all([_load(), _loadStats()]); _renderList(); _renderStatsBar(); _loadPraise(); } // ---------------------------------------------------------- // FORTSCHRITTS-LOBER // ---------------------------------------------------------- async function _loadPraise() { const dog = _appState.activeDog; if (!dog) return; const existing = _container.querySelector('#diary-praise-card'); if (existing) existing.remove(); let data; try { const r = await fetch(`/api/praise/current?dog_id=${dog.id}`, {credentials: 'include'}); data = r.ok ? await r.json() : null; } catch (_) { return; } if (!data?.praise) return; const card = document.createElement('div'); card.id = 'diary-praise-card'; card.style.cssText = ` margin: var(--space-3) var(--space-4) 0; background: linear-gradient(135deg, var(--c-primary-subtle), #fdf6ef); border: 1px solid var(--c-primary-light, #e8c99a); border-radius: var(--radius-xl); padding: var(--space-4) var(--space-5); display: flex; gap: var(--space-3); align-items: flex-start; `; card.innerHTML = `
🐾
Rückblick der Woche

${data.praise}

`; const list = _container.querySelector('#diary-list'); if (list) _container.insertBefore(card, list); card.querySelector('#diary-praise-close')?.addEventListener('click', () => { card.style.opacity = '0'; card.style.transition = 'opacity .2s'; setTimeout(() => card.remove(), 200); }); } // ---------------------------------------------------------- // DATEN LADEN // ---------------------------------------------------------- async function _load() { const dog = _appState.activeDog; if (!dog) return; try { const params = { limit: LIMIT, offset: _offset }; if (_searchQuery) params.q = _searchQuery; if (_filterMilestone) params.milestone = 1; const batch = await API.diary.list(dog.id, params); _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'; } // Stats-Bar befüllen _renderStatsBar(); } catch (err) { UI.toast.error('Einträge konnten nicht geladen werden.'); } } let _currentView = 'list'; // 'list' | 'media' | 'calendar' | 'map' let _totalStats = null; // {entries, photos, days} — Gesamtstatistik aus API async function _loadStats() { const dog = _appState.activeDog; if (!dog) return; try { _totalStats = await API.diary.stats(dog.id); } catch (_) {} } function _fmt(n) { if (n >= 10000) return (n / 1000).toFixed(0) + 'k'; if (n >= 1000) return (n / 1000).toFixed(1).replace('.0','') + 'k'; return String(n); } function _renderStatsBar() { const bar = _container.querySelector('#diary-stats-bar'); if (!bar) return; const s = _totalStats; if (!s && _entries.length === 0) { bar.style.display = 'none'; return; } const entries = s?.entries ?? _entries.length; const photos = s?.photos ?? _entries.reduce((n, e) => n + _allMedia(e).length, 0); const days = s?.days ?? new Set(_entries.map(e => e.datum).filter(Boolean)).size; bar.innerHTML = `
${_fmt(entries)} Einträge
${_fmt(photos)} Medien
${_fmt(days)} Tage
`; bar.style.display = 'flex'; bar.querySelectorAll('.diary-view-btn').forEach(btn => { btn.addEventListener('click', () => { _currentView = btn.dataset.view; _renderStatsBar(); _renderCurrentView(); }); }); } function _renderCurrentView() { const content = _container.querySelector('#diary-view-content'); const loadMore = _container.querySelector('#diary-load-more'); if (!content) return; // "Weitere laden" nur in der Listenansicht sinnvoll if (loadMore) loadMore.style.display = 'none'; if (_currentView === 'list') { content.innerHTML = '
'; _renderList(); // Sichtbarkeit des "Weitere laden"-Buttons nach _load() steuern (bereits in _load()) } else if (_currentView === 'media') { _renderMediaGrid(content); } else if (_currentView === 'calendar') { _renderCalendarView(content); } else if (_currentView === 'map') { _renderMapView(content); } } async function _renderMapView(content) { const dog = _appState.activeDog; if (!dog) return; content.innerHTML = `
Karte wird geladen…
`; let locations; try { locations = await API.diary.locations(dog.id); } catch (e) { content.innerHTML = `

Standorte konnten nicht geladen werden.

`; return; } if (!locations.length) { content.innerHTML = UI.emptyState({ icon: UI.icon('map-pin'), title: 'Keine Standorte', text: 'Füge GPS-Koordinaten zu Tagebucheinträgen hinzu.' }); return; } // Leaflet laden if (!window.L) { await new Promise((res, rej) => { const s = document.createElement('script'); s.src = `/js/leaflet.js?v=${APP_VER}`; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); } const mapEl = content.querySelector('#diary-map-view'); if (!mapEl) return; // Bounds aus allen Punkten berechnen const lats = locations.map(l => l.gps_lat); const lons = locations.map(l => l.gps_lon); const bounds = [[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]]; const map = L.map(mapEl, { zoomControl: true, attributionControl: false }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); // Marker für jeden Eintrag locations.forEach(loc => { const hasPhoto = !!loc.cover_url; const dateStr = loc.datum ? new Date(loc.datum+'T12:00').toLocaleDateString('de-DE', {day:'numeric',month:'short',year:'numeric'}) : ''; const title = UI.escape(loc.titel || loc.location_name || dateStr); const icon = L.divIcon({ html: hasPhoto ? `
` : `
`, iconSize: hasPhoto ? [44, 44] : [32, 32], iconAnchor: hasPhoto ? [22, 22] : [16, 16], className: '', }); const marker = L.marker([loc.gps_lat, loc.gps_lon], { icon }); marker.bindPopup(`
${hasPhoto ? `` : ''}
${title}
${dateStr}
${loc.media_count > 1 ? `
📷 ${loc.media_count} Medien
` : ''}
→ Öffnen
`, { maxWidth: 200 }); marker.on('popupopen', () => { setTimeout(() => { document.querySelectorAll('.diary-map-popup').forEach(el => { el.addEventListener('click', async () => { map.closePopup(); const id = parseInt(el.dataset.id); // Eintrag aus _entries holen oder per API nachladen if (!_entries.find(e => e.id === id)) { try { const fresh = await API.diary.get(_appState.activeDog.id, id); _entries.unshift(fresh); } catch { return; } } _openDetail(id); }); }); }, 50); }); marker.addTo(map); }); // Karte auf alle Punkte zoomen if (locations.length === 1) { map.setView([locations[0].gps_lat, locations[0].gps_lon], 14); } else { map.fitBounds(bounds, { padding: [40, 40] }); } setTimeout(() => map.invalidateSize(), 100); } function _renderMediaGrid(content) { const allMedia = []; _entries.forEach(e => { _allMedia(e).forEach(m => { if (m.media_type === 'image') allMedia.push({ url: m.url, preview_url: m.preview_url, entryId: e.id, datum: e.datum }); }); }); if (allMedia.length === 0) { content.innerHTML = UI.emptyState({ icon: UI.icon('images'), title: 'Keine Medien', text: 'Füge Fotos oder Videos zu Tagebucheinträgen hinzu.' }); return; } content.innerHTML = `
${ allMedia.map(m => `
`).join('') }
`; content.querySelectorAll('.diary-mosaic-item').forEach(el => { el.addEventListener('click', () => _openDetail(parseInt(el.dataset.entryId))); }); } async function _renderCalendarView(content) { const dog = _appState.activeDog; if (!dog) return; const today = new Date().toISOString().slice(0, 10); const now = new Date(); let year = now.getFullYear(), month = now.getMonth(); content.innerHTML = `
Kalender wird geladen…
`; let byDate = {}; try { const all = await API.diary.calendar(dog.id); if (!Array.isArray(all)) throw new Error('Keine Array-Antwort: ' + typeof all); all.forEach(e => { if (e && e.datum) byDate[e.datum] = e; }); } catch (err) { content.innerHTML = `

Kalender-Fehler: ${UI.escape(String(err))}

`; return; } // Debug: Anzahl geladener Einträge kurz anzeigen const _total = Object.keys(byDate).length; if (_total === 0) { content.innerHTML = `

Keine Einträge mit Datum gefunden.

`; return; } const render = () => { const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const monthName = new Date(year, month).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); const monthPrefix = `${year}-${String(month+1).padStart(2,'0')}`; const monthCount = Object.keys(byDate).filter(k => k.startsWith(monthPrefix)).length; const DAYS = ['Mo','Di','Mi','Do','Fr','Sa','So']; const offset = firstDay === 0 ? 6 : firstDay - 1; const cells = []; for (let i = 0; i < offset; i++) cells.push('
'); for (let d = 1; d <= daysInMonth; d++) { const key = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`; const entry = byDate[key]; cells.push(`
${entry?.cover_url ? `` : ''} ${d}
`); } // Nächsten/vorherigen Monat MIT Einträgen finden für Sprungbuttons const allMonths = [...new Set(Object.keys(byDate).map(k => k.slice(0,7)))].sort(); const curM = monthPrefix; const prevM = allMonths.filter(m => m < curM).at(-1) || null; const nextM = allMonths.filter(m => m > curM)[0] || null; content.innerHTML = `
${prevM ? `` : '
'}
${monthName}${monthCount > 0 ? `${monthCount}` : ''}
${nextM ? `` : '
'}
${DAYS.map(n=>`
${n}
`).join('')}
${cells.join('')}
`; }; // Event-Delegation auf content — überlebt innerHTML-Erneuerungen content.addEventListener('click', async e => { const navBtn = e.target.closest('.cal-nav-btn'); if (navBtn) { // Sprungbutton: direkt zu Monat mit Einträgen if (navBtn.dataset.jump) { const [y, m] = navBtn.dataset.jump.split('-').map(Number); year = y; month = m - 1; render(); return; } const dir = parseInt(navBtn.dataset.dir); month += dir; if (month < 0) { month = 11; year--; } if (month > 11) { month = 0; year++; } render(); return; } const cell = e.target.closest('.diary-cal-cell.has-entry'); if (cell) { const id = parseInt(cell.dataset.entryId); if (!id) return; if (!_entries.find(en => en.id === id)) { try { const fresh = await API.diary.get(dog.id, id); _entries.unshift(fresh); } catch { return; } } _openDetail(id); } }); render(); } 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 (Day One Style) // ---------------------------------------------------------- function _renderList() { const listEl = _container.querySelector('#diary-list'); if (!listEl) return; const dog = _appState.activeDog; const isSitter = dog?.is_guest === true; // Sitter: Einträge grundsätzlich ausgeblendet — nur Hinweis + FAB bleibt aktiv if (isSitter) { listEl.innerHTML = UI.emptyState({ icon: UI.icon('lock-simple'), title: 'Einträge nicht sichtbar', text: 'Du kannst neue Einträge hinzufügen, aber keine bestehenden Einträge sehen.', }); return; } if (_entries.length === 0) { listEl.innerHTML = UI.emptyState({ icon: UI.icon('book-open'), title: 'Noch keine Tagebucheinträge', text: 'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Erlebnisse, Erinnerungen.', action: ``, }); listEl.querySelector('#diary-first-entry') ?.addEventListener('click', () => _showForm(null)); return; } // Datenschutz-Hinweis: einmalig anzeigen, per Klick wegklicken const privacyNotice = localStorage.getItem('by_diary_privacy_ack') ? '' : `
Deine Tagebucheinträge sind privat — nur du kannst sie sehen.
`; // 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 = privacyNotice; groups.forEach((items, key) => { const monthLabel = key === 'unbekannt' ? 'Datum unbekannt' : _formatMonth(key); html += `
${monthLabel}
`; html += `
`; html += items.map(e => _entryCard(e)).join(''); html += `
`; }); listEl.innerHTML = html; // Datenschutz-Hinweis wegklicken listEl.querySelector('#diary-privacy-notice')?.addEventListener('click', () => { localStorage.setItem('by_diary_privacy_ack', '1'); listEl.querySelector('#diary-privacy-notice')?.remove(); }); // Events an Karten binden listEl.querySelectorAll('[data-entry-id]').forEach(card => { const id = parseInt(card.dataset.entryId); card.addEventListener('click', () => _openDetail(id)); }); listEl.querySelectorAll('[data-action="open-note"]').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const id = parseInt(btn.dataset.entryId); const label = btn.dataset.label || ''; const location = btn.dataset.location || null; _openNoteModal('diary', id, label, location || null); }); }); } // ---------------------------------------------------------- // ENTRY CARD — Day One Row-Style // ---------------------------------------------------------- function _entryCard(e) { const isMile = e.is_milestone || e.typ === 'meilenstein'; const allMedia = _allMedia(e); const coverMedia = allMedia.find(m => m.is_cover) || allMedia[0] || null; const mediaCount = allMedia.length; // Thumbnail rechts (72×72) let photoHtml = ''; if (coverMedia) { if (coverMedia.media_type === 'video') { photoHtml = `
${mediaCount > 1 ? `${mediaCount}` : ''}
`; } else { photoHtml = `
Foto ${mediaCount > 1 ? `${mediaCount}` : ''}
`; } } // Vorschautext (max 2 Zeilen via CSS clamp) const cleanedText = e.text ? _cleanText(e.text) : ''; const textPreview = cleanedText ? `

${UI.escape(cleanedText.slice(0, 160))}

` : ''; // Meta-Zeile: Zeit · 📍 Ort · Wetter const metaParts = []; if (e.created_at) { const t = _timeStr(e.created_at); if (t) metaParts.push(`${t}`); } if (e.location_name) { metaParts.push(`${UI.escape(e.location_name)}`); } if (e.weather_json) { try { const w = typeof e.weather_json === 'string' ? JSON.parse(e.weather_json) : e.weather_json; const temp = w?.temp_c ?? w?.temperature_2m; if (temp != null) { metaParts.push(`${_weatherEmoji(w.weathercode ?? w.weather_code, w.is_day)} ${Math.round(temp)}°`); } } catch (_) {} } const metaRow = metaParts.length ? `
${metaParts.join(' · ')}
` : ''; // Meilenstein-Icon auf der Datum-Spalte const mileIcon = isMile ? `` : ''; // Titel oder Typ als Fallback const typObj = TYPEN[e.typ] || TYPEN.eintrag; const titleText = e.titel ? `
${UI.escape(e.titel)}
` : `
${typObj.label}
`; const noteLabel = e.titel || e.datum || ''; return `
${_weekday(e.datum)} ${_dayNum(e.datum)} ${mileIcon}
${titleText} ${textPreview} ${metaRow}
${photoHtml}
`; } function _dogAvatarRow(dogIds) { if (!dogIds || dogIds.length <= 1) return ''; const avatars = dogIds.map(did => { const dog = _appState.dogs.find(d => d.id === did); if (!dog) return ''; return `
${dog.foto_url ? `` : `${UI.icon('dog')}`}
`; }).join(''); return `
${avatars}
`; } // ---------------------------------------------------------- // LIGHTBOX // ---------------------------------------------------------- // ---------------------------------------------------------- // LIGHTBOX — Fotos mit Vor/Zurück-Navigation // ---------------------------------------------------------- function _showLightbox(urls, startIdx = 0) { const photos = Array.isArray(urls) ? urls : [urls]; let idx = startIdx; const lb = document.createElement('div'); lb.id = 'diary-lightbox'; lb.style.cssText = 'position:fixed;inset:0;z-index:1100;background:#000;display:flex;flex-direction:column'; const render = () => { lb.innerHTML = `
${photos.length > 1 ? `${idx+1} / ${photos.length}` : ''} ${photos.length > 1 ? `
` : '
'}
`; lb.querySelector('#lb-close').addEventListener('click', () => lb.remove()); lb.querySelector('#lb-prev')?.addEventListener('click', () => { if (idx > 0) { idx--; render(); } }); lb.querySelector('#lb-next')?.addEventListener('click', () => { if (idx < photos.length-1) { idx++; render(); } }); }; render(); document.body.appendChild(lb); } // ---------------------------------------------------------- // DETAIL-ANSICHT — Fullscreen Day One-Stil // ---------------------------------------------------------- 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 || []).filter(t => t && t.trim()); const allMedia = _allMedia(entry); const dogIds = entry.dog_ids || [entry.dog_id]; // Hunde-Chips (bei mehreren Hunden) const dogsHtml = dogIds.length > 1 ? `
${dogIds.map(did => { const dog = _appState.dogs.find(d => d.id === did); return dog ? `
${dog.foto_url ? `` : `${UI.icon('dog')}`}
${UI.escape(dog.name)}
` : ''; }).join('')}
` : ''; // Detail-View im Content-Bereich rendern (gleiche Breite wie Liste/Kalender) const content = _container.querySelector('#diary-view-content'); if (!content) return; // FAB + "Weitere laden" ausblenden während Detail offen _container.querySelector('#diary-fab')?.style.setProperty('display','none'); const _lm = _container.querySelector('#diary-load-more'); if (_lm) _lm.style.display = 'none'; const view = document.createElement('div'); view.id = 'diary-detail-view'; view.className = 'diary-detail-view-inner'; // Medien-HTML für Hero-Bereich (45vh) const _heroHtml = (m) => { if (m.media_type === 'pdf') { return ` ${UI.escape(m.url.split('/').pop())} PDF öffnen `; } if (m.media_type === 'video') { return ``; } return ``; }; // Hero-Sektion let heroSection = ''; if (allMedia.length >= 1) { const thumbsHtml = allMedia.length > 1 ? `
${allMedia.map((m, i) => `
${m.media_type === 'pdf' ? `
` : m.media_type === 'video' ? `` : ``}
`).join('')}
` : ''; heroSection = `
${_heroHtml(allMedia[0])}
${thumbsHtml}`; } // Datum lang formatiert: "Montag, 21. April 2026" const datumLang = _formatDateLong(entry.datum); // Meta-Bar: Zeit · Ort · Wetter (korrekte Feldnamen aus Open-Meteo) const metaItems = []; if (entry.created_at) { const t = _timeStr(entry.created_at); if (t) metaItems.push(`${t}`); } if (entry.location_name) { const locContent = entry.gps_lat ? `${UI.escape(entry.location_name)}` : UI.escape(entry.location_name); metaItems.push(`${locContent}`); } if (entry.weather_json) { try { const w = typeof entry.weather_json === 'string' ? JSON.parse(entry.weather_json) : entry.weather_json; const temp = w?.temp_c ?? w?.temperature_2m; if (w && temp != null) { const wind = w.wind_kmh ?? w.wind_speed_10m; const precip = w.precip_prob; const parts = [ `${_weatherEmoji(w.weathercode ?? w.weather_code, w.is_day)} ${Math.round(temp)}°C`, wind != null ? `${Math.round(wind)} km/h Wind` : null, precip != null ? `${precip}% Regen` : null, ].filter(Boolean).join(' · '); metaItems.push(`${parts}`); } } catch (_) {} } const metaBar = metaItems.length ? `
${metaItems.join('')}
` : ''; // Tags const tagsSection = tags.length ? `
${tags.map(t => `${t}`).join('')}
` : ''; // POI-Liste (wie Routen) const POI_ICON = { restaurant:'fork-knife', cafe:'coffee', bar:'beer-bottle', pharmacy:'first-aid', hospital:'first-aid', park:'tree', playground:'soccer-ball', supermarket:'shopping-cart', shop:'shopping-bag', attraction:'star', viewpoint:'binoculars', museum:'buildings', hotel:'bed', church:'church', school:'graduation-cap', default:'map-pin' }; let poiListHtml = ''; if (entry.poi_json) { try { const pois = typeof entry.poi_json === 'string' ? JSON.parse(entry.poi_json) : entry.poi_json; if (pois?.length) { poiListHtml = `
In der Nähe
${pois.map(p => { const icon = POI_ICON[p.type] || POI_ICON.default; const dist = p.distance_m < 1000 ? `${p.distance_m} m` : `${(p.distance_m/1000).toFixed(1)} km`; return `
${UI.escape(p.name)} ${dist}
`; }).join('')}
`; } } catch (_) {} } // Karte (wenn GPS vorhanden) — Platzhalter-Div, wird nach DOM-Insert befüllt const hasGps = entry.gps_lat != null && entry.gps_lon != null; const mapSection = hasGps ? `
${poiListHtml}
` : ''; view.innerHTML = `
${datumLang}
${!_appState?.activeDog?.is_guest ? ` ` : '
'}
${heroSection}
${isMile ? `
Meilenstein
` : ''} ${entry.titel ? `

${UI.escape(entry.titel)}

` : ''} ${metaBar} ${dogsHtml} ${entry.text ? `

${UI.escape(_cleanText(entry.text))}

` : ''} ${metaItems.length || entry.text ? '
' : ''}
${typ.icon} ${typ.label}
${tagsSection}
${mapSection}
`; // In Content-Bereich einsetzen statt als Fixed-Overlay content.innerHTML = ''; content.appendChild(view); UI.scrollTop(); // Seite nach oben scrollen // Leaflet-Karte initialisieren (wenn GPS vorhanden) if (hasGps) { setTimeout(async () => { const mapEl = view.querySelector('#diary-dv-map'); if (!mapEl) return; if (!window.L) { await new Promise((res, rej) => { const s = document.createElement('script'); s.src = `/js/leaflet.js?v=${APP_VER}`; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); } const map = L.map(mapEl, { zoomControl: true, attributionControl: false }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); const svgIcon = L.divIcon({ html: ` `, iconSize: [32, 32], iconAnchor: [16, 16], className: '', }); L.marker([entry.gps_lat, entry.gps_lon], { icon: svgIcon }).addTo(map); map.setView([entry.gps_lat, entry.gps_lon], 15); map.invalidateSize(); }, 150); } // Zurück → vorherige Ansicht wiederherstellen const _closeDetail = () => { _container.querySelector('#diary-fab')?.style.removeProperty('display'); _renderCurrentView(); _renderStatsBar(); }; view.querySelector('#diary-dv-back').addEventListener('click', _closeDetail); // Notiz-Button in Detailansicht view.querySelector('#diary-dv-note')?.addEventListener('click', e => { e.stopPropagation(); const label = entry.titel || entry.datum || String(entry.id); _openNoteModal('diary', entry.id, label, entry.location_name || null); }); // Bearbeiten view.querySelector('#diary-dv-edit')?.addEventListener('click', async () => { _container.querySelector('#diary-fab')?.style.removeProperty('display'); if (entry.location_name !== undefined || entry.gps_lat !== undefined) { _showForm(entry); } else { try { const fresh = await API.diary.get(_appState.activeDog.id, entry.id); const idx = _entries.findIndex(e => e.id === entry.id); if (idx !== -1) _entries[idx] = fresh; _showForm(fresh); } catch { _showForm(entry); } } }); // Foto in Hero → Lightbox const photoUrls = allMedia.filter(m => m.media_type !== 'video').map(m => m.url); view.querySelector('#diary-dv-hero')?.querySelector('img')?.addEventListener('click', ev => { const clickedIdx = parseInt(ev.target.dataset.idx ?? 0); const photoIdx = allMedia.slice(0, clickedIdx + 1).filter(m => m.media_type !== 'video').length - 1; _showLightbox(photoUrls, Math.max(0, photoIdx)); }); // Thumbnail-Strip → Hero wechseln view.querySelector('#diary-dv-thumbs')?.addEventListener('click', ev => { const thumb = ev.target.closest('[data-idx]'); if (!thumb) return; const i = parseInt(thumb.dataset.idx); const hero = view.querySelector('#diary-dv-hero'); if (hero) hero.innerHTML = _heroHtml(allMedia[i]); // Foto in neuem Hero → Lightbox hero?.querySelector('img')?.addEventListener('click', ev2 => { const clickedIdx = parseInt(ev2.target.dataset.idx ?? i); const photoIdx = allMedia.slice(0, clickedIdx + 1).filter(m => m.media_type !== 'video').length - 1; _showLightbox(photoUrls, Math.max(0, photoIdx)); }); // Aktive Markierung view.querySelectorAll('#diary-dv-thumbs .diary-detail-thumb').forEach((t, j) => { t.classList.toggle('diary-detail-thumb--active', j === i); }); }); } // ---------------------------------------------------------- // FORMULAR — Neu erstellen / Bearbeiten // ---------------------------------------------------------- function _showForm(entry) { const isEdit = !!entry; const today = new Date().toISOString().slice(0, 10); const activeDog = _appState.activeDog; const typOpts = Object.entries(TYPEN) .map(([val, { icon, label }]) => ``) .join(''); // Weitere Hunde: alle außer dem aktiven const otherDogs = _appState.dogs.filter(d => d.id !== activeDog?.id); const entryDogIds = entry?.dog_ids || [activeDog?.id]; const dogPickerHtml = otherDogs.length > 0 ? `
${otherDogs.map(d => ` `).join('')}
` : ''; const body = `
${UI.escape(entry?.location_name || '')}
${dogPickerHtml}
`; const footer = `
${isEdit ? `` : ''}
`; UI.modal.open({ title: isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag', body, footer }); const form = document.getElementById('diary-form'); // Fokus auf Titel-Feld → öffnet Keyboard auf Mobile, zeigt dem User was zu tun ist setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150); // ---- Multi-Media-Verwaltung ---- const mediaInput = document.getElementById('diary-media-input'); // Neue Dateien die noch nicht hochgeladen wurden const _newFiles = []; function _renderNewGrid() { const grid = document.getElementById('diary-new-media-grid'); if (!grid) return; if (_newFiles.length === 0) { grid.style.display = 'none'; grid.innerHTML = ''; return; } grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:8px;margin-bottom:8px'; grid.innerHTML = _newFiles.map((f, i) => { const objUrl = URL.createObjectURL(f); const thumb = f.type === 'application/pdf' || f.name?.endsWith('.pdf') ? `
${f.name}
` : f.type.startsWith('video/') ? `` : ``; return `
${thumb}
`; }).join(''); grid.querySelectorAll('.diary-media-thumb-del').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.newIdx); _newFiles.splice(idx, 1); _renderNewGrid(); }); }); } // Bestehende Medien im Edit-Modus rendern function _renderExistingMedia() { const wrap = document.getElementById('diary-existing-media'); if (!wrap) return; const items = isEdit ? _allMedia(entry) : []; if (items.length === 0) { wrap.innerHTML = ''; return; } const GRID_STYLE = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:8px;margin-bottom:8px'; const grid = `
${items.map((m, idx) => `
${m.media_type === 'video' ? `` : ``} ${m.id != null ? ` ` : ''}
`).join('')}
`; wrap.innerHTML = grid; wrap.querySelectorAll('.diary-media-thumb-del').forEach(btn => { btn.addEventListener('click', async () => { const wrap2 = btn.closest('.diary-media-thumb-wrap'); const mediaId = btn.dataset.mediaId ? parseInt(btn.dataset.mediaId) : null; const isLegacy = !!btn.dataset.legacy; btn.disabled = true; try { if (mediaId != null) { await API.diary.deleteMediaItem(_appState.activeDog.id, entry.id, mediaId); // aus entry.media_items entfernen if (entry.media_items) entry.media_items = entry.media_items.filter(m => m.id !== mediaId); } else if (isLegacy) { await API.diary.deleteMedia(_appState.activeDog.id, entry.id); entry.media_url = null; } wrap2.remove(); UI.toast.success('Medium entfernt.'); } catch (e) { btn.disabled = false; UI.toast.error(e.message || 'Fehler beim Löschen.'); } }); }); // Stern-Buttons im Edit-Formular wrap.querySelectorAll('.diary-cover-btn--form').forEach(btn => { btn.addEventListener('click', async () => { const mediaId = parseInt(btn.dataset.mediaId); try { await API.diary.setCover(_appState.activeDog.id, entry.id, mediaId); if (entry.media_items) { entry.media_items.forEach(m => { m.is_cover = m.id === mediaId ? 1 : 0; }); } entry.cover_url = (entry.media_items || []).find(m => m.id === mediaId)?.url || null; _updateEntryInList(entry); // Alle Sterne in diesem Formular aktualisieren wrap.querySelectorAll('.diary-cover-btn--form').forEach(b => { const active = parseInt(b.dataset.mediaId) === mediaId; b.classList.toggle('diary-cover-btn--active', active); b.style.background = active ? '#f5c518' : 'rgba(0,0,0,.45)'; b.style.color = active ? '#fff' : 'rgba(255,255,255,.7)'; b.setAttribute('aria-label', active ? 'Cover-Bild' : 'Als Cover setzen'); b.setAttribute('title', active ? 'Cover-Bild' : 'Als Cover setzen'); const use = b.querySelector('use'); if (use) use.setAttribute('href', `/icons/phosphor.svg#${active ? 'star-fill' : 'star'}`); }); UI.toast.success('Cover-Bild gesetzt.'); } catch { UI.toast.error('Cover konnte nicht gesetzt werden.'); } }); }); } _renderExistingMedia(); function _addFiles(fileList) { for (const f of fileList) _newFiles.push(f); _renderNewGrid(); } function _openPicker(opts = {}) { const tmp = document.createElement('input'); tmp.type = 'file'; tmp.accept = 'image/*,video/*'; tmp.style.display = 'none'; if (opts.capture) tmp.setAttribute('capture', opts.capture); if (opts.noAccept) tmp.removeAttribute('accept'); tmp.addEventListener('change', () => { _addFiles(tmp.files); tmp.remove(); }); document.body.appendChild(tmp); tmp.click(); } mediaInput?.addEventListener('change', () => { if (mediaInput.files.length) { _addFiles(mediaInput.files); mediaInput.value = ''; } }); document.getElementById('diary-form-cancel')?.addEventListener('click', UI.modal.close); // Milestone-Toggle document.getElementById('diary-milestone-btn')?.addEventListener('click', () => { const cb = document.getElementById('diary-milestone-cb'); const btn = document.getElementById('diary-milestone-btn'); cb.checked = !cb.checked; btn.classList.toggle('diary-milestone-toggle--active', cb.checked); btn.querySelector('span').textContent = cb.checked ? 'Meilenstein ✓' : 'Als Meilenstein markieren'; }); // --- Location Picker --- let _locLat = (entry?.gps_lat != null) ? entry.gps_lat : null; let _locLon = (entry?.gps_lon != null) ? entry.gps_lon : null; let _locName = entry?.location_name || null; let _miniMap = null, _miniMarker = null; const _pinSvg = ''; const _mkIcon = () => L.divIcon({ html: _pinSvg, className: '', iconSize: [32,40], iconAnchor: [16,40] }); function _setName(name) { _locName = name; document.getElementById('diary-location-label').textContent = name; document.getElementById('diary-location-chip-wrap').style.display = ''; document.getElementById('diary-location-suggestions').style.display = 'none'; } function _placeMarker(lat, lon) { if (_miniMarker) { _miniMarker.setLatLng([lat, lon]); return; } _miniMarker = L.marker([lat, lon], { draggable: false, icon: _mkIcon() }).addTo(_miniMap); _miniMarker.on('dragend', () => { const p = _miniMarker.getLatLng(); _locLat = p.lat; _locLon = p.lng; document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; }); } document.getElementById('diary-location-clear')?.addEventListener('click', () => { _locName = null; document.getElementById('diary-location-chip-wrap').style.display = 'none'; }); const _clearBtn = document.getElementById('diary-coords-clear'); let _clearPending = false; _clearBtn?.addEventListener('click', () => { if (!_clearPending) { _clearPending = true; _clearBtn.textContent = 'Wirklich entfernen?'; _clearBtn.style.color = 'var(--c-danger)'; setTimeout(() => { if (_clearPending) { _clearPending = false; _clearBtn.textContent = 'Ort entfernen'; _clearBtn.style.color = 'var(--c-text-muted)'; } }, 3000); return; } _clearPending = false; _clearBtn.textContent = 'Ort entfernen'; _clearBtn.style.color = 'var(--c-text-muted)'; _locLat = null; _locLon = null; _locName = null; document.getElementById('diary-location-chip-wrap').style.display = 'none'; document.getElementById('diary-location-suggestions').style.display = 'none'; document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; if (_miniMarker) { _miniMarker.remove(); _miniMarker = null; } if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); } }); let _mapEditing = false; function _setMapEditing(on) { _mapEditing = on; const lbl = document.getElementById('diary-map-edit-label'); if (lbl) lbl.textContent = on ? 'Fertig' : 'Position ändern'; if (!_miniMap) return; if (on) { if (_miniMarker) _miniMarker.dragging.enable(); } else { if (_miniMarker) _miniMarker.dragging.disable(); } } document.getElementById('diary-map-edit-btn')?.addEventListener('click', () => { _setMapEditing(!_mapEditing); }); // Karte beim Formular-Open automatisch laden UI.loadLeaflet().then(() => { setTimeout(() => { const lat = _locLat || 48.0, lon = _locLon || 11.9, zoom = _locLat ? 15 : 7; _miniMap = L.map('diary-map-wrap', { zoomControl: true, attributionControl: false, dragging: true, scrollWheelZoom: false, }).setView([lat, lon], zoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }) .addTo(_miniMap); _miniMap.invalidateSize(); if (_locLat) { _placeMarker(lat, lon); _miniMarker.dragging.disable(); // Lesemodus: kein Drag } // Klick nur im Edit-Modus _miniMap.on('click', e => { if (!_mapEditing) return; _locLat = e.latlng.lat; _locLon = e.latlng.lng; _placeMarker(_locLat, _locLon); if (!_mapEditing) _miniMarker.dragging.disable(); document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; }); }, 150); }); async function _showSuggestions() { const btn = document.getElementById('diary-location-btn'); UI.setLoading(btn, true); try { let lat = _locLat, lon = _locLon; if (lat == null || lon == null) { const pos = await API.getLocation(); lat = pos.lat; lon = pos.lon; _locLat = lat; _locLon = lon; if (_miniMap) { _miniMap.setView([lat, lon], 15); _placeMarker(lat, lon); } document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; } const suggestions = await API.diary.nearby(_appState.activeDog.id, lat, lon); const sugEl = document.getElementById('diary-location-suggestions'); if (suggestions.length === 0) { sugEl.innerHTML = '

Keine Orte in der Nähe gefunden.

'; } else { sugEl.innerHTML = suggestions.map(s => ` `).join(''); sugEl.querySelectorAll('.diary-location-suggestion').forEach(el => { el.addEventListener('click', () => _setName(el.dataset.name)); }); } sugEl.style.display = ''; } catch (err) { UI.toast.error(err?.message?.includes('GPS') || lat == null ? 'GPS nicht verfügbar.' : 'Ortssuche fehlgeschlagen.'); } finally { UI.setLoading(btn, false); } } document.getElementById('diary-location-btn')?.addEventListener('click', _showSuggestions); document.getElementById('diary-form-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(entry.id); }); // Checked-Klasse auf Dog-Picker-Items toggeln form.querySelectorAll('.diary-dog-pick-item input').forEach(cb => { cb.addEventListener('change', () => { cb.closest('.diary-dog-pick-item').classList.toggle('checked', cb.checked); }); }); form.addEventListener('submit', async e => { e.preventDefault(); const submitBtn = document.querySelector('[form="diary-form"][type="submit"]') || form.querySelector('[type="submit"]'); const fd = UI.formData(form); // dog_ids zusammenbauen: aktiver Hund + gewählte weitere const dogIds = [_appState.activeDog.id]; form.querySelectorAll('.diary-dog-pick-item input:checked').forEach(cb => { const id = parseInt(cb.value); if (!dogIds.includes(id)) dogIds.push(id); }); await UI.asyncButton(submitBtn, async () => { // Auto-Wetter: nur bei neuem Eintrag ohne GPS-Standort let _clientWeather = null; if (!isEdit && _locLat == null) { try { const pos = await API.getLocation(); const wd = await API.weather.get(pos.lat, pos.lon); if (wd && wd.temp_c != null) _clientWeather = JSON.stringify(wd); } catch (_) { /* GPS oder Wetter nicht verfügbar → kein Problem */ } } const payload = { datum: fd.datum || null, typ: fd.typ, titel: fd.titel || null, text: fd.text || null, is_milestone: 'is_milestone' in fd, dog_ids: dogIds, gps_lat: _locLat, gps_lon: _locLon, location_name: _locName, client_time: API.clientNow(), weather_json: _clientWeather, }; async function _uploadNewFiles(entryId) { let failCount = 0; const uploaded = []; let exifGps = null; for (const file of _newFiles) { try { const formData = new FormData(); formData.append('file', file); const m = await API.diary.uploadMedia(_appState.activeDog.id, entryId, formData); uploaded.push(m); if (m.exif_lat != null && m.exif_lon != null && !exifGps) { exifGps = { lat: m.exif_lat, lon: m.exif_lon }; } } catch { failCount++; } } if (failCount > 0) { UI.toast.warning(`${failCount} Medium${failCount > 1 ? 'en' : ''} konnte${failCount > 1 ? 'n' : ''} nicht hochgeladen werden.`); } if (exifGps) { UI.toast.success(`📍 Standort aus Foto-GPS übernommen`); } return { uploaded, exifGps }; } if (isEdit) { const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload); if (_newFiles.length > 0) { const { uploaded, exifGps } = await _uploadNewFiles(entry.id); if (!updated.media_items) updated.media_items = []; updated.media_items.push(...uploaded); if (exifGps && !updated.gps_lat) { updated.gps_lat = exifGps.lat; updated.gps_lon = exifGps.lon; } } else { updated.media_items = entry.media_items || updated.media_items || []; updated.media_url = entry.media_url ?? updated.media_url; } _updateEntryInList(updated); UI.toast.success('Eintrag gespeichert.'); } else { const created = await API.diary.create(_appState.activeDog.id, payload); if (_newFiles.length > 0) { const { uploaded, exifGps } = await _uploadNewFiles(created.id); created.media_items = uploaded; if (exifGps && !created.gps_lat) { created.gps_lat = exifGps.lat; created.gps_lon = exifGps.lon; } } _entries.unshift(created); UI.toast.success('Eintrag erstellt.'); } UI.modal.close(); _renderList(); _loadStats().then(() => _renderStatsBar()); }); }); } // ---------------------------------------------------------- // 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(); _loadStats().then(() => _renderStatsBar()); 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)); } // ---------------------------------------------------------- // IMPORT // ---------------------------------------------------------- function _showImport() { UI.modal.open({ title: 'Tagebuch importieren', body: `

Importiere Einträge aus einer anderen App in das Tagebuch von ${UI.escape(_appState.activeDog?.name || 'deinem Hund')}.

`, footer: ` `, }); // Format-Karten klickbar machen document.querySelectorAll('.import-format-card').forEach(card => { card.addEventListener('click', () => { document.querySelectorAll('.import-format-card').forEach(c => c.classList.remove('import-format-card--active')); card.classList.add('import-format-card--active'); card.querySelector('input[type=radio]').checked = true; // Accept-Attribut anpassen const fmt = card.querySelector('input').value; document.getElementById('import-file-input').accept = fmt === 'nsx' ? '.nsx' : '.csv'; }); }); // Erste Karte direkt aktiv setzen document.getElementById('fmt-nsx')?.classList.add('import-format-card--active'); document.getElementById('import-start-btn').addEventListener('click', async () => { const fileInput = document.getElementById('import-file-input'); const fmt = document.querySelector('input[name="import-fmt"]:checked')?.value; const btn = document.getElementById('import-start-btn'); const resultEl = document.getElementById('import-result'); if (!fileInput.files.length) { UI.toast('Bitte zuerst eine Datei auswählen.', 'warning'); return; } const file = fileInput.files[0]; const dogId = _appState.activeDog?.id; UI.setLoading(btn, true); resultEl.style.display = 'none'; try { const res = fmt === 'nsx' ? await API.importData.notestation(dogId, file) : await API.importData.csv(dogId, file); const errHtml = res.errors?.length ? `
${res.errors.length} Fehler anzeigen
${UI.escape(res.errors.join('\n'))}
` : ''; resultEl.innerHTML = `
${res.imported} Einträge importiert ${res.skipped ? ` · ${res.skipped} übersprungen` : ''} ${errHtml}
`; resultEl.style.display = 'block'; UI.setLoading(btn, false); // Diary neu laden falls etwas importiert wurde if (res.imported > 0) { _offset = 0; _entries = []; await _load(); _renderList(); } } catch (e) { resultEl.innerHTML = `
Fehler: ${UI.escape(e.message || String(e))}
`; resultEl.style.display = 'block'; UI.setLoading(btn, false); } }); } // ---------------------------------------------------------- // NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden) // ---------------------------------------------------------- async function _openNoteModal(parentType, parentId, parentLabel, locationName) { document.getElementById('by-note-modal')?.remove(); const overlay = document.createElement('div'); overlay.id = 'by-note-modal'; overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center'; overlay.innerHTML = `
Notiz
${UI.escape(parentLabel)}
`; document.body.appendChild(overlay); const textarea = document.getElementById('by-note-text'); const saveBtn = document.getElementById('by-note-save'); const cancelBtn = document.getElementById('by-note-cancel'); const closeBtn = document.getElementById('by-note-close'); let existingNoteId = null; try { const existing = await API.notes.get(parentType, parentId); if (existing?.id) { existingNoteId = existing.id; textarea.value = existing.text || ''; } } catch (_) { /* keine Notiz vorhanden — ok */ } setTimeout(() => textarea.focus(), 100); const _close = () => overlay.remove(); closeBtn.addEventListener('click', _close); cancelBtn.addEventListener('click', _close); overlay.addEventListener('click', e => { if (e.target === overlay) _close(); }); document.getElementById('by-note-form').addEventListener('submit', async e => { e.preventDefault(); const text = textarea.value.trim(); UI.setLoading(saveBtn, true); try { const payload = { text, parent_label: parentLabel, location_name: locationName, client_time: API.clientNow() }; if (existingNoteId) { await API.notes.update(existingNoteId, payload); } else { await API.notes.create(parentType, parentId, payload); } UI.toast.success('Notiz gespeichert.'); _close(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); UI.setLoading(saveBtn, false); } }); } // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- function _cleanText(text) { if (!text) return text; return text .replace(/!\[([^\]]*)\]\([^\)]*\)/g, '') // Markdown-Bilder ![]() .replace(/\[([^\]]*)\]\([^\)]*\)/g, '$1') // Markdown-Links [text](url) → text .replace(/^\[\]\s*$/gm, '') // leere [] auf eigener Zeile .replace(/\n{3,}/g, '\n\n') // mehrfache Leerzeilen kürzen .trim(); } return { init, refresh, openNew, onDogChange, openDetail: _openDetail }; })();