/* ============================================================ 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 _loadLeaflet() { if (window.L) return Promise.resolve(); return new Promise((resolve, reject) => { const cssLoaded = document.querySelector('link[href*="leaflet"]') ? Promise.resolve() : new Promise(res => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/css/leaflet.css'; link.onload = res; link.onerror = res; document.head.appendChild(link); }); cssLoaded.then(() => { if (document.querySelector('script[src*="leaflet.js"]')) { resolve(); return; } const s = document.createElement('script'); s.src = '/js/leaflet.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); }); } function _sourceIcon(source) { if (source === 'places') return 'star'; if (source === 'osm') return 'map-pin'; return 'map-trifold'; } 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 _mediaHtml(url, style = '') { if (!url) return ''; return _isVideo(url) ? `` : `Foto`; } 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; _loadLeaflet(); // Leaflet im Hintergrund vorladen — kein await await _render(); } // ---------------------------------------------------------- // REFRESH — erneuter Navigations-Aufruf (Tap auf Tab) // ---------------------------------------------------------- async function refresh() { if (!_appState.activeDog) return; // Mehrere Hunde → Picker zeigen (User kann Hund wählen) if (_appState.dogs.length > 1) { _renderDogPicker(); return; } // Einzelner Hund → Diary direkt neu laden _offset = 0; _entries = []; 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; } if (_appState.dogs.length > 1) { _renderDogPicker(); } else { await _renderDiary(); } } // ---------------------------------------------------------- // HUNDE-PICKER — Einstiegsseite bei mehreren Hunden // ---------------------------------------------------------- function _renderDogPicker() { const activeDogId = _appState.activeDog?.id; const cards = _appState.dogs.map(dog => { const isActive = dog.id === activeDogId; const av = dog.foto_url ? `${_escape(dog.name)}` : `${UI.icon('dog')}`; return `
${av}
${_escape(dog.name)}
${dog.rasse ? `
${_escape(dog.rasse)}
` : ''}
`; }).join(''); _container.innerHTML = `

Wessen Tagebuch?

${cards}
`; _container.querySelectorAll('.diary-picker-card').forEach(el => { el.addEventListener('click', async () => { const id = parseInt(el.dataset.dogId); if (id === _appState.activeDog?.id) { // Bereits aktiver Hund → direkt Diary laden _offset = 0; _entries = []; await _renderDiary(); } else { App.setActiveDog(id); // onDogChange() → _renderDiary() via _notifyDogChange() } }); }); } // ---------------------------------------------------------- // DIARY-ANSICHT — Timeline mit Einträgen // ---------------------------------------------------------- async function _renderDiary() { _container.innerHTML = `
`; _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-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 _load(); _renderList(); } // ---------------------------------------------------------- // 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'; } } 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); } // ---------------------------------------------------------- // EMPTY-STATE HELPER // ---------------------------------------------------------- function _emptyState(icon, title, text, cta = '') { return `
${title}
${text ? `

${text}

` : ''} ${cta ? `
${cta}
` : ''}
`; } // ---------------------------------------------------------- // LISTE RENDERN — Timeline gruppiert nach Monat // ---------------------------------------------------------- function _renderList() { const listEl = _container.querySelector('#diary-list'); if (!listEl) return; if (_entries.length === 0) { listEl.innerHTML = _emptyState( 'book-open', 'Noch keine Tagebucheinträge', 'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Erlebnisse, Erinnerungen.', `` ); 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 ? `
${_isVideo(e.media_url) ? `
` : `Foto`}
` : ''; const tagsHtml = tags.length ? `
${tags.map(t => `${t}`).join('')}
` : ''; const locationHtml = e.location_name ? `

${_escape(e.location_name)}

` : ''; const textPreview = e.text ? `

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

` : ''; // Meilenstein-Badge (nur bei is_milestone=1, nicht bei manuell gewähltem Typ 'meilenstein') const milestoneBadge = e.is_milestone ? `
${UI.icon('calendar-dots')} Meilenstein
` : ''; // Mehrere Hunde: kleine Avatare in der Karte const dogAvatars = _dogAvatarRow(e.dog_ids || []); return `
${photo}
${milestoneBadge}
${typ.icon} ${typ.label} ${dateStr}
${e.titel ? `
${_escape(e.titel)}
` : ''} ${locationHtml} ${textPreview} ${tagsHtml} ${dogAvatars}
`; } 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}
`; } // ---------------------------------------------------------- // 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 ? _mediaHtml(entry.media_url, 'margin-bottom:var(--space-4)') : ''; // Hunde-Anzeige wenn mehrere beteiligt const dogIds = entry.dog_ids || [entry.dog_id]; 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')}`}
${_escape(dog.name)}
` : ''; }).join('')}
` : ''; const body = ` ${isMile ? `
${UI.icon('trophy')} Meilenstein
` : ''} ${photo}
${typ.icon} ${typ.label} ${entry.datum ? UI.time.format(entry.datum + 'T00:00:00') : ''}
${entry.location_name ? `
${entry.gps_lat ? `${_escape(entry.location_name)}` : _escape(entry.location_name)}
` : ''} ${dogsHtml} ${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', async () => { UI.modal.close(); // Nur nachladen wenn location_name/gps_lat fehlen (älterer In-Memory-Eintrag) 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); } } }); } // ---------------------------------------------------------- // 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 = `
${_escape(entry?.location_name || '')}
${dogPickerHtml}
${isEdit && entry.media_url ? `
${_mediaHtml(entry.media_url, 'max-height:200px;object-fit:cover')}
` : ''}
`; 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); // Media-Inputs + Vorschau const mediaInput = document.getElementById('diary-media-input'); const cameraInput = document.getElementById('diary-camera-input'); const photoPreview = document.getElementById('diary-photo-preview'); const videoPreview = document.getElementById('diary-video-preview'); const previewWrap = document.getElementById('diary-media-preview'); const mediaBtns = document.getElementById('diary-media-btns'); function _showPreview(file) { if (!file) return; previewWrap.style.display = ''; if (file.type.startsWith('video/')) { photoPreview.style.display = 'none'; videoPreview.style.display = ''; videoPreview.src = URL.createObjectURL(file); } else { videoPreview.style.display = 'none'; photoPreview.style.display = ''; photoPreview.src = URL.createObjectURL(file); } } mediaInput?.addEventListener('change', () => _showPreview(mediaInput.files[0])); cameraInput?.addEventListener('change', () => { // Auswahl in mediaInput spiegeln damit Submit-Handler nur einen Ort abfragt const dt = new DataTransfer(); if (cameraInput.files[0]) dt.items.add(cameraInput.files[0]); mediaInput.files = dt.files; _showPreview(cameraInput.files[0]); }); document.getElementById('diary-btn-camera') ?.addEventListener('click', () => cameraInput.click()); document.getElementById('diary-btn-library')?.addEventListener('click', () => { // Kein capture → iOS zeigt Mediathek-Auswahl, Android zeigt Galerie const tmp = document.createElement('input'); tmp.type = 'file'; tmp.accept = 'image/*,video/*'; tmp.style.display = 'none'; tmp.addEventListener('change', () => { const dt = new DataTransfer(); if (tmp.files[0]) dt.items.add(tmp.files[0]); mediaInput.files = dt.files; _showPreview(tmp.files[0]); tmp.remove(); }); document.body.appendChild(tmp); tmp.click(); }); document.getElementById('diary-btn-file')?.addEventListener('click', () => { mediaInput.removeAttribute('accept'); mediaInput.click(); mediaInput.setAttribute('accept', 'image/*,video/*'); }); document.getElementById('diary-preview-clear')?.addEventListener('click', () => { previewWrap.style.display = 'none'; photoPreview.src = ''; videoPreview.src = ''; mediaInput.value = ''; }); // "Entfernen"-Button löscht Medium direkt document.getElementById('diary-media-delete')?.addEventListener('click', async () => { const ok = await UI.modal.confirm({ title: `${_isVideo(entry.media_url) ? 'Video' : 'Foto'} entfernen?`, message: 'Das Medium wird dauerhaft gelöscht.', confirmText: 'Entfernen', danger: true, }); if (!ok) return; try { await API.diary.deleteMedia(_appState.activeDog.id, entry.id); entry.media_url = null; const mediaDiv = document.getElementById('diary-current-media'); if (mediaDiv) mediaDiv.remove(); const replaceBtn = document.getElementById('diary-media-replace'); if (replaceBtn) replaceBtn.remove(); mediaInput.style.display = ''; UI.toast.success('Medium entfernt.'); } catch (e) { UI.toast.error(e.message || 'Fehler.'); } }); 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 = 'GPS → 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 _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 () => { 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, }; const mediaFile = mediaInput?.files[0]; if (isEdit) { const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload); if (mediaFile) { try { const fd2 = new FormData(); fd2.append('file', mediaFile); const media = await API.diary.uploadMedia(_appState.activeDog.id, entry.id, fd2); updated.media_url = media.media_url; } catch { UI.toast.warning('Gespeichert, Medium konnte nicht hochgeladen werden.'); } } _updateEntryInList(updated); UI.toast.success('Eintrag gespeichert.'); } else { const created = await API.diary.create(_appState.activeDog.id, payload); if (mediaFile) { try { const fd2 = new FormData(); fd2.append('file', mediaFile); const media = await API.diary.uploadMedia(_appState.activeDog.id, created.id, fd2); created.media_url = media.media_url; } catch { UI.toast.warning('Eintrag erstellt, Medium 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, '"'); } // ---------------------------------------------------------- // IMPORT // ---------------------------------------------------------- function _showImport() { UI.modal.open({ title: 'Tagebuch importieren', body: `

Importiere Einträge aus einer anderen App in das Tagebuch von ${_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
${_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: ${_escape(e.message || String(e))}
`; resultEl.style.display = 'block'; UI.setLoading(btn, false); } }); } // ---------------------------------------------------------- // PUBLIC // ---------------------------------------------------------- return { init, refresh, openNew, onDogChange }; })();