/* ============================================================ BAN YARO — Notizblock Seiten-Modul: Alle Notizen mit Filter, Suche, Sortierung und KI-Analyse. ============================================================ */ window.Page_notes = (() => { let _container = null; let _appState = null; let _notes = []; // Aktueller Filter-/Such-Zustand let _filterType = ''; // '' = alle let _sortMode = 'newest'; // newest | type | location let _searchQ = ''; let _searchTimer = null; // KI-Panel let _kiOpen = false; let _kiLoading = false; let _kiSuggestions = null; let _kiError = null; // ---------------------------------------------------------- // Rubrik-Konfiguration // ---------------------------------------------------------- const RUBRIKEN = [ { type: '', label: 'Alle', color: 'var(--c-text-muted)', icon: 'note' }, { type: 'health', label: 'Gesundheit', color: '#e74c3c', icon: 'heart' }, { type: 'diary', label: 'Tagebuch', color: '#C4843A', icon: 'book-open' }, { type: 'training_session', label: 'Training', color: '#27ae60', icon: 'target' }, { type: 'route', label: 'Routen', color: '#2980b9', icon: 'path' }, { type: 'event', label: 'Events', color: '#8e44ad', icon: 'calendar' }, { type: 'walk', label: 'Gassi-Treffen',color: '#f39c12', icon: 'paw-print' }, { type: 'sitting', label: 'Sitting', color: '#16a085', icon: 'house-line' }, { type: 'erste_hilfe', label: 'Erste Hilfe', color: '#c0392b', icon: 'first-aid' }, { type: 'trainingsplan', label: 'Trainingsplan',color: '#059669', icon: 'clipboard-text' }, { type: 'friends', label: 'Freunde', color: '#7c3aed', icon: 'users' }, { type: 'poison', label: 'Giftköder', color: '#dc2626', icon: 'warning-octagon' }, { type: 'lost', label: 'Vermisste', color: '#b45309', icon: 'magnifying-glass' }, ]; function _rubrik(type) { return RUBRIKEN.find(r => r.type === type) || { type, label: type, color: 'var(--c-text-muted)', icon: 'note' }; } // ---------------------------------------------------------- // Hilfsfunktionen // ---------------------------------------------------------- function _formatTime(isoStr) { if (!isoStr) return ''; try { const d = new Date(isoStr.replace(' ', 'T') + (isoStr.includes('T') || isoStr.endsWith('Z') ? '' : 'Z')); return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } catch (_) { return ''; } } function _dateGroup(isoStr) { if (!isoStr) return 'Älteres'; try { const d = new Date(isoStr.replace(' ', 'T') + (isoStr.includes('T') || isoStr.endsWith('Z') ? '' : 'Z')); const now = new Date(); const diffDays = (now - d) / 86400000; if (diffDays < 1 && d.getDate() === now.getDate()) return 'Heute'; if (diffDays < 7) return 'Diese Woche'; return 'Älteres'; } catch (_) { return 'Älteres'; } } function _truncate(str, max = 600) { // Karten zeigen max 5 Zeilen via CSS-Clamp — Text muss lang genug // sein dass die Clamp greift. Bei sehr langen Notes: vor Clamp abschneiden // damit der String nicht riesig in der DOM-Page steht. if (!str) return ''; return str.length > max ? str.slice(0, max) + '…' : str; } // ---------------------------------------------------------- // Daten laden // ---------------------------------------------------------- async function _load() { const params = {}; if (_filterType) params.parent_type = _filterType; if (_sortMode !== 'newest') params.sort = _sortMode; if (_searchQ) params.q = _searchQ; return await API.notes.getAll(params); } // ---------------------------------------------------------- // Filter/Sortierung anwenden (client-seitig falls API alles zurückgibt) // ---------------------------------------------------------- function _applySort(list) { const copy = [...list]; if (_sortMode === 'newest') { copy.sort((a, b) => new Date(b.updated_at || b.created_at) - new Date(a.updated_at || a.created_at)); } else if (_sortMode === 'type') { copy.sort((a, b) => (a.parent_type || '').localeCompare(b.parent_type || '', 'de')); } else if (_sortMode === 'location') { copy.sort((a, b) => (a.location_name || '').localeCompare(b.location_name || '', 'de')); } return copy; } // ---------------------------------------------------------- // Rendern // ---------------------------------------------------------- function _render() { const kiEnabled = _appState?.user?.notes_ki_enabled !== 0; const sorted = _applySort(_notes); // Gruppen aufbauen const groups = { 'Heute': [], 'Diese Woche': [], 'Älteres': [] }; sorted.forEach(n => { const g = _dateGroup(n.updated_at || n.created_at); groups[g].push(n); }); const groupHtml = Object.entries(groups) .filter(([, items]) => items.length > 0) .map(([label, items]) => `
${UI.escape(label)}
${items.map(_noteCard).join('')}
`).join(''); _container.innerHTML = `

Notizblock

${_notes.length} Notiz${_notes.length !== 1 ? 'en' : ''}
${localStorage.getItem('by_notes_privacy_ack') ? '' : `
Alle Notizen sind privat — nur du kannst sie lesen.
`} ${kiEnabled ? _kiPanelHtml() : ''}
${RUBRIKEN.map(r => ` `).join('')}
${sorted.length === 0 ? UI.emptyState({ icon: 'note', title: 'Keine Notizen', text: 'Füge Notizen zu Trainingseinheiten oder anderen Einträgen hinzu.' }) : groupHtml }
`; _bindEvents(); } // ---------------------------------------------------------- // KI-Panel HTML // ---------------------------------------------------------- function _kiPanelHtml() { return `
Muster-Analyse
${_kiOpen ? `
${_kiError ? `
${UI.escape(_kiError)}
` : ''} ${_kiSuggestions ? `
    ${_kiSuggestions.map(s => `
  • ${UI.escape(s)}
  • `).join('')}
` : ''}
` : ''}
`; } // ---------------------------------------------------------- // Medien-Helfer: Preview-URL ableiten + Indikator-Strip für die Karte // ---------------------------------------------------------- function _notePreview(url) { if (!url) return url; const dot = url.lastIndexOf('.'); if (dot < 0) return url; if (/\.(mp4|webm|mov|avi|m4v)$/i.test(url)) return url.slice(0, dot) + '_thumb.jpg'; if (/\.(m4a|aac|mp3|ogg|oga|wav|pdf)$/i.test(url)) return url; return url.slice(0, dot) + '_preview.webp'; } function _noteMediaStrip(note) { const items = note.media_items || []; if (!items.length) return ''; const n = { image: 0, video: 0, audio: 0, pdf: 0, file: 0 }; items.forEach(m => { n[m.media_type] = (n[m.media_type] || 0) + 1; }); const parts = []; if (n.image) parts.push(['image', n.image]); if (n.video) parts.push(['video-camera', n.video]); if (n.audio) parts.push(['microphone', n.audio]); if (n.pdf + n.file) parts.push(['paperclip', n.pdf + n.file]); if (!parts.length) return ''; return `
${parts.map(([icon, count]) => `${UI.icon(icon)} ${count}`).join('')}
`; } // ---------------------------------------------------------- // Notiz-Karte HTML // ---------------------------------------------------------- function _noteCard(note) { const rb = _rubrik(note.parent_type); const meta = note.meta_json || {}; const microBadges = []; if (meta.erfolgsquote) microBadges.push('🐾'.repeat(meta.erfolgsquote)); if (meta.umgebung) microBadges.push({ zuhause: '🏠 Zuhause', natur: '🌿 Natur', stadt: '🌆 Stadt' }[meta.umgebung] || meta.umgebung); if (meta.hund_stimmung) microBadges.push({ super: '😊 Super', ok: '😐 Ok', mude: '😔 Müde' }[meta.hund_stimmung] || meta.hund_stimmung); const hasLocation = !!note.location_name; return `
${UI.escape(rb.label)} ${note.parent_label ? `${UI.escape(note.parent_label)}` : '' }
${note.text ? `

${UI.escape(_truncate(note.text))}

` : ''} ${_noteMediaStrip(note)} ${microBadges.length ? `
${microBadges.map(b => `${UI.escape(b)}`).join('')}
` : ''}
${UI.escape(_formatTime(note.updated_at || note.created_at))} ${hasLocation ? ` ${UI.escape(note.location_name)}` : ''}
`; } // ---------------------------------------------------------- // Event-Binding // ---------------------------------------------------------- function _bindEvents() { // Datenschutz-Hinweis wegklicken _container.querySelector('#notes-privacy-notice')?.addEventListener('click', () => { localStorage.setItem('by_notes_privacy_ack', '1'); _container.querySelector('#notes-privacy-notice')?.remove(); }); // Neue Notiz _container.querySelector('#notes-new-btn')?.addEventListener('click', () => { _openCreateModal(_filterType || ''); }); // Filter-Chips _container.querySelectorAll('.notes-chip').forEach(btn => { btn.addEventListener('click', () => { _filterType = btn.dataset.type; _reload(); }); }); // Sortierung _container.querySelectorAll('.notes-sort-btn').forEach(btn => { btn.addEventListener('click', () => { _sortMode = btn.dataset.sort; _render(); // nur neu rendern, keine API-Last }); }); // Suche (debounced) const searchInput = _container.querySelector('#notes-search'); if (searchInput) { searchInput.addEventListener('input', () => { clearTimeout(_searchTimer); _searchTimer = setTimeout(() => { _searchQ = searchInput.value.trim(); _reload(); }, 300); }); } // KI-Toggle const kiToggle = _container.querySelector('#notes-ki-toggle'); if (kiToggle) { kiToggle.addEventListener('click', () => { _kiOpen = !_kiOpen; _render(); }); } // KI-Analyse-Button const kiBtn = _container.querySelector('#notes-ki-analyse-btn'); if (kiBtn) { kiBtn.addEventListener('click', async () => { _kiLoading = true; _kiError = null; _kiSuggestions = null; _render(); try { const res = await API.notes.analyse(); if (res && Array.isArray(res.suggestions)) { _kiSuggestions = res.suggestions; } else if (res && res.text) { _kiSuggestions = res.text.split('\n').filter(Boolean); } else { _kiSuggestions = ['Keine Vorschläge verfügbar.']; } } catch (err) { _kiError = err?.message || 'KI-Analyse nicht verfügbar.'; } finally { _kiLoading = false; _render(); } }); } // Edit-Buttons _container.querySelectorAll('.notes-edit-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const note = _notes.find(n => n.id === parseInt(btn.dataset.id, 10)); if (note) _openEditModal(note); }); }); // Delete-Buttons _container.querySelectorAll('.notes-delete-btn').forEach(btn => { btn.addEventListener('click', async e => { e.stopPropagation(); const noteId = parseInt(btn.dataset.id, 10); if (!window.confirm('Notiz wirklich löschen?')) return; try { await API.notes.delete(noteId); _notes = _notes.filter(n => n.id !== noteId); _render(); UI.toast.success('Notiz gelöscht.'); } catch (_) { UI.toast.error('Löschen fehlgeschlagen.'); } }); }); // Karte selbst klickbar → Detail-Modal mit vollem Text _container.querySelectorAll('.notes-card').forEach(card => { card.addEventListener('click', e => { // Klicks auf Action-Buttons nicht doppelt verarbeiten if (e.target.closest('.list-item-action-btn')) return; const note = _notes.find(n => n.id === parseInt(card.dataset.id, 10)); if (note) _openDetailModal(note); }); }); } // ---------------------------------------------------------- // Detail-Modal: voller Notiz-Text + Meta + Bearbeiten/Löschen // ---------------------------------------------------------- function _openDetailModal(note) { const rb = RUBRIKEN.find(r => r.id === note.rubrik) || RUBRIKEN[0]; const meta = (() => { try { return JSON.parse(note.meta || '{}'); } catch { return {}; } })(); const microBadges = []; if (meta.erfolg) microBadges.push(`🐾 ${meta.erfolg}/5`); if (meta.umgebung) microBadges.push({ zuhause: '🏠 Zuhause', natur: '🌿 Natur', stadt: '🌆 Stadt' }[meta.umgebung] || meta.umgebung); if (meta.hund_stimmung) microBadges.push({ super: '😊 Super', ok: '😐 Ok', mude: '😔 Müde' }[meta.hund_stimmung] || meta.hund_stimmung); UI.modal.open({ title: `${UI.icon(rb.icon)} ${UI.escape(rb.label)}`, body: `
${note.parent_label ? `
${UI.escape(note.parent_label)}
` : ''} ${note.text ? `

${UI.escape(note.text)}

` : ''} ${(note.media_items && note.media_items.length) ? `
${note.media_items.map(m => { if (m.media_type === 'image') return ``; if (m.media_type === 'video') return ``; if (m.media_type === 'audio') return ``; return `${UI.icon('file-text')} ${m.media_type === 'pdf' ? 'PDF öffnen' : 'Datei öffnen'}`; }).join('')}
` : ''} ${microBadges.length ? `
${microBadges.map(b => `${UI.escape(b)}`).join('')}
` : ''}
${UI.escape(_formatTime(note.updated_at || note.created_at))} ${note.location_name ? ` ${UI.escape(note.location_name)}` : ''}
`, footer: `
`, }); document.getElementById('notes-detail-edit')?.addEventListener('click', () => { UI.modal.close(); _openEditModal(note); }); // Bild-Thumbnails → Lightbox; Preview→Original-Fallback (CSP-konform) const _imgItems = (note.media_items || []).filter(m => m.media_type === 'image').map(m => ({ url: m.url, type: 'image' })); document.querySelectorAll('.notes-detail-media-img').forEach(img => { img.addEventListener('error', () => { if (img.src !== img.dataset.full) img.src = img.dataset.full; }, { once: true }); img.addEventListener('click', () => { const idx = _imgItems.findIndex(it => it.url === img.dataset.full); UI.lightbox?.show(_imgItems, Math.max(0, idx)); }); }); } // ---------------------------------------------------------- // Laden + Re-Render // ---------------------------------------------------------- async function _reload() { _container.querySelector('.notes-list')?.classList.add('loading'); try { _notes = await _load(); } catch (_) { _notes = []; } _render(); } // ---------------------------------------------------------- // Create-Modal — neue Notiz mit vorausgewählter Kategorie // ---------------------------------------------------------- function _openCreateModal(preselectedType = '') { const ERSTELL_RUBRIKEN = RUBRIKEN.filter(r => r.type !== ''); // ohne "Alle" let _selType = preselectedType || ERSTELL_RUBRIKEN[0].type; const modalId = 'notes-create-modal'; document.getElementById(modalId)?.remove(); const overlay = document.createElement('div'); overlay.id = modalId; overlay.style.cssText = `position:fixed;inset:0;z-index:9999;display:flex;align-items:flex-end;justify-content:center;background:rgba(0,0,0,0.45)`; const _buildContent = () => { const rb = _rubrik(_selType); return `

Neue Notiz

${ERSTELL_RUBRIKEN.map(r => ` `).join('')}
`; }; overlay.innerHTML = _buildContent(); document.body.appendChild(overlay); const _media = UI.noteMediaAttacher({ containerId: 'nc-media' }); const _remove = () => { _media.destroy(); overlay.remove(); }; // Kategorie-Wechsel: nur Auswahl + Button-Styles aktualisieren — KEIN // innerHTML-Rebuild, sonst gingen eingegebener Text & angehängte Medien // (und eine laufende Sprachaufnahme) verloren. overlay.querySelectorAll('.nc-cat').forEach(btn => { btn.addEventListener('click', () => { _selType = btn.dataset.type; overlay.querySelectorAll('.nc-cat').forEach(b => { const r = _rubrik(b.dataset.type); const active = b.dataset.type === _selType; b.style.borderColor = active ? r.color : 'var(--c-border)'; b.style.background = active ? r.color + '22' : 'var(--c-surface-2)'; b.style.color = active ? r.color : 'var(--c-text-secondary)'; }); }); }); overlay.querySelector('#nc-cancel')?.addEventListener('click', _remove); overlay.addEventListener('click', e => { if (e.target === overlay) _remove(); }); overlay.querySelector('#nc-save')?.addEventListener('click', async () => { const text = overlay.querySelector('#nc-text')?.value?.trim(); if (!text && !_media.hasPending()) { UI.toast.warning('Bitte einen Text eingeben oder ein Medium anhängen.'); return; } const btn = overlay.querySelector('#nc-save'); await UI.asyncButton(btn, async () => { const rb = _rubrik(_selType); const created = await API.notes.create(_selType, 'standalone', { text: text || '', parent_label: rb.label, }); if (created?.id && _media.hasPending()) { await _media.uploadAll(created.id, (d, t) => { btn.textContent = `${d}/${t} hochgeladen…`; }); } _remove(); _filterType = _selType; await _reload(); UI.toast.success('Notiz gespeichert.'); }); }); setTimeout(() => overlay.querySelector('#nc-text')?.focus(), 100); } // ---------------------------------------------------------- // Edit-Modal (Bottom-Sheet Stil) // ---------------------------------------------------------- function _openEditModal(note) { const meta = note.meta_json || {}; const rb = _rubrik(note.parent_type); const modalId = 'notes-edit-modal'; document.getElementById(modalId)?.remove(); const overlay = document.createElement('div'); overlay.id = modalId; overlay.style.cssText = ` position:fixed;inset:0;z-index:9999; display:flex;align-items:flex-end;justify-content:center; background:rgba(0,0,0,0.45); `; overlay.innerHTML = `
${UI.escape(rb.label)}

Notiz bearbeiten

${note.parent_type === 'training_session' ? `
${[1,2,3,4,5].map(n => ` `).join('')}
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji,val]) => ` `).join('')}
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji,val]) => ` `).join('')}
` : ''}
`; document.body.appendChild(overlay); const _media = UI.noteMediaAttacher({ containerId: 'notes-edit-media', noteId: note.id, existingMedia: note.media_items || [], }); let selErfolgsquote = meta.erfolgsquote || null; let selUmgebung = meta.umgebung || null; let selStimmung = meta.hund_stimmung || null; function _toggleBtn(group, val, getter, setter) { overlay.querySelectorAll(`.notes-${group}`).forEach(b => { const match = (group === 'pfote') ? parseInt(b.dataset.val, 10) === val : b.dataset.val === val; b.style.background = match ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'; b.style.borderColor = match ? 'var(--c-primary)' : 'var(--c-border)'; }); } overlay.querySelectorAll('.notes-pfote').forEach(btn => { btn.addEventListener('click', () => { const v = parseInt(btn.dataset.val, 10); selErfolgsquote = selErfolgsquote === v ? null : v; _toggleBtn('pfote', selErfolgsquote, null, null); }); }); overlay.querySelectorAll('.notes-umgebung').forEach(btn => { btn.addEventListener('click', () => { selUmgebung = selUmgebung === btn.dataset.val ? null : btn.dataset.val; _toggleBtn('umgebung', selUmgebung, null, null); }); }); overlay.querySelectorAll('.notes-stimmung').forEach(btn => { btn.addEventListener('click', () => { selStimmung = selStimmung === btn.dataset.val ? null : btn.dataset.val; _toggleBtn('stimmung', selStimmung, null, null); }); }); function _close() { _media.destroy(); overlay.remove(); } overlay.addEventListener('click', e => { if (e.target === overlay) _close(); }); overlay.querySelector('#notes-edit-cancel').addEventListener('click', _close); // Speichern overlay.querySelector('#notes-edit-save').addEventListener('click', async () => { const text = overlay.querySelector('#notes-edit-text').value.trim(); if (!text && !_media.hasPending() && !(note.media_items || []).length) { UI.toast.warning('Bitte einen Text eingeben oder ein Medium anhängen.'); return; } const saveBtn = overlay.querySelector('#notes-edit-save'); saveBtn.disabled = true; saveBtn.textContent = 'Speichern…'; const metaObj = {}; if (selErfolgsquote) metaObj.erfolgsquote = selErfolgsquote; if (selUmgebung) metaObj.umgebung = selUmgebung; if (selStimmung) metaObj.hund_stimmung = selStimmung; try { const updated = await API.notes.update(note.id, { text, meta_json: Object.keys(metaObj).length > 0 ? metaObj : null, }); if (_media.hasPending()) { const { uploaded } = await _media.uploadAll(note.id, (d, t) => { saveBtn.textContent = `${d}/${t} hochgeladen…`; }); updated.media_items = (updated.media_items || []).concat(uploaded); } const idx = _notes.findIndex(n => n.id === note.id); if (idx >= 0) _notes[idx] = updated; _render(); _close(); UI.toast.success('Notiz aktualisiert.'); } catch (_) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; UI.toast.error('Speichern fehlgeschlagen.'); } }); // Löschen overlay.querySelector('#notes-edit-delete').addEventListener('click', async () => { if (!window.confirm('Notiz wirklich löschen?')) return; try { await API.notes.delete(note.id); _notes = _notes.filter(n => n.id !== note.id); _render(); _close(); UI.toast.success('Notiz gelöscht.'); } catch (_) { UI.toast.error('Löschen fehlgeschlagen.'); } }); } // ---------------------------------------------------------- // INIT / REFRESH // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; // Zustand zurücksetzen _filterType = ''; _sortMode = 'newest'; _searchQ = ''; _kiOpen = false; _kiLoading = false; _kiSuggestions = null; _kiError = null; _notes = []; _container.innerHTML = UI.skeleton(3); try { _notes = await _load(); } catch (_) { _notes = []; } _render(); } async function refresh() { if (!_container) return; _container.innerHTML = UI.skeleton(3); try { _notes = await _load(); } catch (_) { _notes = []; } _render(); } return { init, refresh }; })();