banyaro/backend/static/js/pages/notes.js
rene 553e9e7854 Sprint 12+13: Tagebuch Day-One-Redesign, Notiz-Feature, Icon-Fixes, SW by-v405
Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations

Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil

Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware

Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
2026-04-25 20:44:46 +02:00

693 lines
30 KiB
JavaScript

/* ============================================================
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' },
];
function _rubrik(type) {
return RUBRIKEN.find(r => r.type === type) || { type, label: type, color: 'var(--c-text-muted)', icon: 'note' };
}
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 = 150) {
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]) => `
<div class="notes-group">
<div class="notes-group-label">${_esc(label)}</div>
${items.map(_noteCard).join('')}
</div>
`).join('');
_container.innerHTML = `
<div class="notes-page">
<!-- Header -->
<div class="notes-header">
<h2 class="notes-title">Notizblock</h2>
<span class="notes-count">${_notes.length} Notiz${_notes.length !== 1 ? 'en' : ''}</span>
</div>
<!-- KI-Panel -->
${kiEnabled ? _kiPanelHtml() : ''}
<!-- Filter-Chips -->
<div class="notes-filter-chips">
${RUBRIKEN.map(r => `
<button class="notes-chip ${_filterType === r.type ? 'notes-chip--active' : ''}"
data-type="${_esc(r.type)}"
style="${_filterType === r.type ? `--chip-color:${r.color}` : ''}">
${_esc(r.label)}
</button>
`).join('')}
</div>
<!-- Suche + Sortierung -->
<div class="notes-toolbar">
<div class="notes-search-wrap">
<i class="ph ph-magnifying-glass notes-search-icon"></i>
<input id="notes-search" type="search" class="notes-search-input"
placeholder="Suche…" value="${_esc(_searchQ)}">
</div>
<div class="notes-sort-btns">
<button class="notes-sort-btn ${_sortMode === 'newest' ? 'notes-sort-btn--active' : ''}"
data-sort="newest">Neueste</button>
<button class="notes-sort-btn ${_sortMode === 'type' ? 'notes-sort-btn--active' : ''}"
data-sort="type">Rubrik</button>
<button class="notes-sort-btn ${_sortMode === 'location' ? 'notes-sort-btn--active' : ''}"
data-sort="location">Ort</button>
</div>
</div>
<!-- Liste -->
<div class="notes-list">
${sorted.length === 0
? UI.emptyState({ icon: 'note', title: 'Keine Notizen', text: 'Füge Notizen zu Trainingseinheiten oder anderen Einträgen hinzu.' })
: groupHtml
}
</div>
</div>
<style>
.notes-page { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.notes-header { display: flex; align-items: center; justify-content: space-between; }
.notes-title { font-size: var(--text-lg); font-weight: var(--weight-bold); color: var(--c-text); margin: 0; }
.notes-count { font-size: var(--text-xs); color: var(--c-text-muted); }
/* KI-Panel */
.notes-ki-panel { background: var(--c-surface-2); border: 1.5px solid var(--c-border); border-radius: var(--radius-lg); overflow: hidden; }
.notes-ki-header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-3) var(--space-4); cursor: pointer; gap: var(--space-2); }
.notes-ki-header-left { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--c-text); }
.notes-ki-chevron { transition: transform .2s; color: var(--c-text-muted); }
.notes-ki-chevron--open { transform: rotate(180deg); }
.notes-ki-body { padding: var(--space-3) var(--space-4) var(--space-4); border-top: 1px solid var(--c-border); }
.notes-ki-btn { padding: var(--space-2) var(--space-4); border-radius: var(--radius-md); border: none; background: var(--c-primary); color: #fff; font-size: var(--text-sm); font-weight: var(--weight-semibold); cursor: pointer; }
.notes-ki-btn:disabled { opacity: .6; cursor: default; }
.notes-ki-suggestions { margin-top: var(--space-3); font-size: var(--text-sm); color: var(--c-text); line-height: 1.6; }
.notes-ki-suggestions ul { margin: var(--space-2) 0 0; padding-left: var(--space-4); }
.notes-ki-suggestions li { margin-bottom: var(--space-1); }
.notes-ki-error { margin-top: var(--space-2); font-size: var(--text-sm); color: var(--c-danger); }
/* Filter-Chips */
.notes-filter-chips { display: flex; gap: var(--space-2); overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
.notes-filter-chips::-webkit-scrollbar { display: none; }
.notes-chip { flex-shrink: 0; font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: 4px var(--space-3); border-radius: 999px; border: 1.5px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text-secondary); cursor: pointer; white-space: nowrap; transition: background .15s, color .15s, border-color .15s; }
.notes-chip--active { background: var(--chip-color, var(--c-primary)); color: #fff; border-color: var(--chip-color, var(--c-primary)); }
/* Toolbar */
.notes-toolbar { display: flex; gap: var(--space-2); align-items: center; }
.notes-search-wrap { position: relative; flex: 1; }
.notes-search-icon { position: absolute; left: var(--space-3); top: 50%; transform: translateY(-50%); color: var(--c-text-muted); font-size: 1rem; pointer-events: none; }
.notes-search-input { width: 100%; padding: var(--space-2) var(--space-3) var(--space-2) calc(var(--space-3) + 1.3rem); border: 1.5px solid var(--c-border); border-radius: var(--radius-md); font-size: var(--text-sm); background: var(--c-surface); color: var(--c-text); outline: none; box-sizing: border-box; }
.notes-search-input:focus { border-color: var(--c-primary); }
.notes-sort-btns { display: flex; border: 1.5px solid var(--c-border); border-radius: var(--radius-md); overflow: hidden; flex-shrink: 0; }
.notes-sort-btn { padding: var(--space-2) var(--space-3); font-size: var(--text-xs); font-weight: var(--weight-semibold); border: none; background: var(--c-surface-2); color: var(--c-text-secondary); cursor: pointer; transition: background .15s, color .15s; border-right: 1px solid var(--c-border); }
.notes-sort-btn:last-child { border-right: none; }
.notes-sort-btn--active { background: var(--c-primary); color: #fff; }
/* Gruppen */
.notes-group { display: flex; flex-direction: column; gap: var(--space-2); }
.notes-group-label { font-size: var(--text-xs); font-weight: var(--weight-semibold); color: var(--c-text-muted); text-transform: uppercase; letter-spacing: .05em; padding: var(--space-1) 0; }
/* Karten */
.notes-card { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
.notes-card-top { display: flex; align-items: flex-start; gap: var(--space-2); }
.notes-rubrik-chip { display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-xs); font-weight: var(--weight-semibold); padding: 2px var(--space-2); border-radius: 999px; flex-shrink: 0; }
.notes-parent-label { font-size: var(--text-xs); color: var(--c-text-secondary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; align-self: center; }
.notes-card-meta { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); color: var(--c-text-muted); }
.notes-card-actions { display: flex; gap: var(--space-2); margin-left: auto; flex-shrink: 0; }
.notes-card-text { font-size: var(--text-sm); color: var(--c-text); line-height: 1.55; white-space: pre-wrap; margin: 0; }
.notes-micro-badges { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.notes-micro-badge { font-size: var(--text-xs); padding: 2px 6px; border-radius: var(--radius-sm); background: var(--c-surface-2); color: var(--c-text-secondary); }
.notes-action-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text-muted); cursor: pointer; font-size: 1rem; transition: background .15s, color .15s; }
.notes-action-btn:hover { background: var(--c-surface); color: var(--c-text); }
.notes-action-btn--danger:hover { background: #fef2f2; color: var(--c-danger); border-color: var(--c-danger); }
.notes-list { display: flex; flex-direction: column; gap: var(--space-4); }
</style>
`;
_bindEvents();
}
// ----------------------------------------------------------
// KI-Panel HTML
// ----------------------------------------------------------
function _kiPanelHtml() {
return `
<div class="notes-ki-panel" id="notes-ki-panel">
<div class="notes-ki-header" id="notes-ki-toggle">
<div class="notes-ki-header-left">
<i class="ph ph-robot"></i>
Muster-Analyse
</div>
<i class="ph ph-caret-down notes-ki-chevron ${_kiOpen ? 'notes-ki-chevron--open' : ''}" id="notes-ki-chevron"></i>
</div>
${_kiOpen ? `
<div class="notes-ki-body" id="notes-ki-body">
<button class="notes-ki-btn" id="notes-ki-analyse-btn" ${_kiLoading ? 'disabled' : ''}>
${_kiLoading ? '<i class="ph ph-spinner-gap"></i> Analysiere…' : 'Analysieren'}
</button>
${_kiError ? `<div class="notes-ki-error"><i class="ph ph-warning-circle"></i> ${_esc(_kiError)}</div>` : ''}
${_kiSuggestions ? `
<div class="notes-ki-suggestions">
<ul>
${_kiSuggestions.map(s => `<li>${_esc(s)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
` : ''}
</div>
`;
}
// ----------------------------------------------------------
// 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 `
<div class="notes-card" data-id="${note.id}">
<!-- Top-Zeile: Rubrik-Chip + parent_label + Zeit + Buttons -->
<div class="notes-card-top">
<span class="notes-rubrik-chip"
style="background:${rb.color}22;color:${rb.color}">
<i class="ph ph-${rb.icon}"></i>
${_esc(rb.label)}
</span>
${note.parent_label
? `<span class="notes-parent-label" title="${_esc(note.parent_label)}">${_esc(note.parent_label)}</span>`
: ''
}
<div class="notes-card-actions">
<button class="notes-action-btn notes-edit-btn" data-id="${note.id}" title="Bearbeiten">
<i class="ph ph-pencil"></i>
</button>
<button class="notes-action-btn notes-action-btn--danger notes-delete-btn" data-id="${note.id}" title="Löschen">
<i class="ph ph-trash"></i>
</button>
</div>
</div>
<!-- Notiztext -->
<p class="notes-card-text">${_esc(_truncate(note.text))}</p>
<!-- Micro-Badges -->
${microBadges.length ? `
<div class="notes-micro-badges">
${microBadges.map(b => `<span class="notes-micro-badge">${_esc(b)}</span>`).join('')}
</div>
` : ''}
<!-- Meta: Zeit + Ort -->
<div class="notes-card-meta">
<i class="ph ph-clock"></i>
${_esc(_formatTime(note.updated_at || note.created_at))}
${hasLocation ? `<i class="ph ph-map-pin"></i> ${_esc(note.location_name)}` : ''}
</div>
</div>
`;
}
// ----------------------------------------------------------
// Event-Binding
// ----------------------------------------------------------
function _bindEvents() {
// 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.');
}
});
});
}
// ----------------------------------------------------------
// Laden + Re-Render
// ----------------------------------------------------------
async function _reload() {
_container.querySelector('.notes-list')?.classList.add('loading');
try {
_notes = await _load();
} catch (_) {
_notes = [];
}
_render();
}
// ----------------------------------------------------------
// 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 = `
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
width:100%;max-width:480px;max-height:85vh;overflow-y:auto;
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
<!-- Griff -->
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
<!-- Kopfzeile -->
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-4)">
<span style="display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);
font-weight:var(--weight-semibold);padding:2px var(--space-2);border-radius:999px;
background:${rb.color}22;color:${rb.color}">
<i class="ph ph-${rb.icon}"></i> ${_esc(rb.label)}
</span>
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0">
Notiz bearbeiten
</h3>
</div>
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
<!-- Freitext -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Text</label>
<textarea id="notes-edit-text" rows="5"
style="width:100%;padding:var(--space-3);border:1.5px solid var(--c-border);
border-radius:var(--radius-md);font-size:var(--text-sm);
font-family:var(--font-sans);background:var(--c-surface);
color:var(--c-text);resize:vertical;outline:none;line-height:1.5;
box-sizing:border-box">${_esc(note.text)}</textarea>
</div>
${note.parent_type === 'training_session' ? `
<!-- Bewertung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Bewertung</label>
<div style="display:flex;gap:var(--space-2)">
${[1,2,3,4,5].map(n => `
<button type="button" class="notes-pfote" data-val="${n}"
style="font-size:1.3rem;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
padding:3px 9px;cursor:pointer;
background:${(meta.erfolgsquote||0)===n?'var(--c-primary-subtle)':'var(--c-surface-2)'};
border-color:${(meta.erfolgsquote||0)===n?'var(--c-primary)':'var(--c-border)'}">🐾</button>
`).join('')}
</div>
</div>
<!-- Umgebung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Umgebung</label>
<div style="display:flex;gap:var(--space-2)">
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji,val]) => `
<button type="button" class="notes-umgebung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
padding:3px 10px;cursor:pointer;
background:${meta.umgebung===val?'var(--c-primary-subtle)':'var(--c-surface-2)'};
border-color:${meta.umgebung===val?'var(--c-primary)':'var(--c-border)'}">${emoji}</button>
`).join('')}
</div>
</div>
<!-- Stimmung -->
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes</label>
<div style="display:flex;gap:var(--space-2)">
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji,val]) => `
<button type="button" class="notes-stimmung" data-val="${val}"
style="font-size:1.2rem;border:1.5px solid var(--c-border);border-radius:var(--radius-md);
padding:3px 10px;cursor:pointer;
background:${meta.hund_stimmung===val?'var(--c-primary-subtle)':'var(--c-surface-2)'};
border-color:${meta.hund_stimmung===val?'var(--c-primary)':'var(--c-border)'}">${emoji}</button>
`).join('')}
</div>
</div>
` : ''}
</div>
<!-- Buttons -->
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
<button id="notes-edit-delete" type="button"
style="padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
border:1.5px solid var(--c-danger);background:none;
color:var(--c-danger);font-size:var(--text-sm);cursor:pointer">
Löschen
</button>
<button id="notes-edit-cancel" type="button"
style="flex:1;padding:var(--space-3);border-radius:var(--radius-md);
border:1.5px solid var(--c-border);background:none;
color:var(--c-text-secondary);font-size:var(--text-sm);cursor:pointer">
Abbrechen
</button>
<button id="notes-edit-save" type="button"
style="flex:2;padding:var(--space-3);border-radius:var(--radius-md);
border:none;background:var(--c-primary);
color:#fff;font-size:var(--text-sm);font-weight:var(--weight-semibold);cursor:pointer">
Speichern
</button>
</div>
</div>
`;
document.body.appendChild(overlay);
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() { 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) { UI.toast.warning('Notiz darf nicht leer sein.'); 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,
});
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 };
})();