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
693 lines
30 KiB
JavaScript
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, '<')
|
|
.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]) => `
|
|
<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 };
|
|
|
|
})();
|