/* ============================================================
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, '>')
.replace(/"/g, '"');
}
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]) => `
${_esc(label)}
${items.map(_noteCard).join('')}
`).join('');
_container.innerHTML = `
${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 `
${_kiOpen ? `
${_kiError ? `
${_esc(_kiError)}
` : ''}
${_kiSuggestions ? `
${_kiSuggestions.map(s => `- ${_esc(s)}
`).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 `
${_esc(rb.label)}
${note.parent_label
? `
${_esc(note.parent_label)}`
: ''
}
${_esc(_truncate(note.text))}
${microBadges.length ? `
${microBadges.map(b => `${_esc(b)}`).join('')}
` : ''}
${_esc(_formatTime(note.updated_at || note.created_at))}
${hasLocation ? ` ${_esc(note.location_name)}` : ''}
`;
}
// ----------------------------------------------------------
// 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 = `
${_esc(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);
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 };
})();