Wiederverwendbarer UI.noteMediaAttacher für beide Notiz-Stellen (UI.noteModal
+ Notizblock-Seite). note_media-Tabelle + POST/DELETE /api/notes/{id}/media
(vor der gierigen /{parent_type}/{parent_id}-Route). Audio per MediaRecorder,
serverseitig nach m4a/AAC transkodiert (ffmpeg) — iOS spielt Chrome-Opus-webm
nicht ab. UI.lightbox global eingeführt. Mikrofon-Policy microphone=(self) +
CSP media-src 'self' blob:, Datenschutz v6. Disk-Cleanup für note_media bei
Notiz-, Account- und Admin-User-Delete. Reine Medien-Notiz ohne Text erlaubt.
noteModal-Bug gefixt: notes.get() liefert Array -> existing[0] statt
existing?.id (verhinderte Bearbeiten, erzeugte Duplikate). 12 neue Tests.
admin.py enthält außerdem KI-Vision-Statusfelder aus paralleler Arbeit
(nicht sauber trennbar ohne interaktives Staging).
981 lines
47 KiB
JavaScript
981 lines
47 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' },
|
|
{ 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]) => `
|
|
<div class="notes-group">
|
|
<div class="list-group-header">${UI.escape(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>
|
|
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
|
<span class="notes-count">${_notes.length} Notiz${_notes.length !== 1 ? 'en' : ''}</span>
|
|
<button id="notes-new-btn" class="btn btn-primary btn-sm" style="gap:4px">
|
|
<svg class="ph-icon" aria-hidden="true" style="width:15px;height:15px"><use href="/icons/phosphor.svg#plus"></use></svg>
|
|
${_filterType && _filterType !== '' ? _rubrik(_filterType).label : 'Neue Notiz'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Datenschutz-Hinweis (einmalig, wegklickbar) -->
|
|
${localStorage.getItem('by_notes_privacy_ack') ? '' : `
|
|
<div id="notes-privacy-notice" style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
|
background:var(--c-surface-2);border-radius:var(--radius-md);
|
|
padding:var(--space-3) var(--space-3);display:flex;align-items:center;
|
|
gap:var(--space-2);cursor:pointer;border:1px solid var(--c-border-light)"
|
|
title="Klicken zum Schließen">
|
|
<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px;flex-shrink:0;color:var(--c-primary)">
|
|
<use href="/icons/phosphor.svg#lock-simple"></use></svg>
|
|
<span>Alle Notizen sind privat — nur du kannst sie lesen.</span>
|
|
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px;margin-left:auto;flex-shrink:0;opacity:0.4">
|
|
<use href="/icons/phosphor.svg#x"></use></svg>
|
|
</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="${UI.escape(r.type)}"
|
|
style="${_filterType === r.type ? `--chip-color:${r.color}` : ''}">
|
|
${UI.escape(r.label)}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<!-- Suche + Sortierung -->
|
|
<div class="notes-toolbar">
|
|
<div class="notes-search-wrap">
|
|
<svg class="ph-icon notes-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
|
<input id="notes-search" type="search" class="notes-search-input"
|
|
placeholder="Suche…" value="${UI.escape(_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; }
|
|
@media (min-width: 1024px) {
|
|
.notes-filter-chips { flex-wrap: wrap; overflow-x: visible; }
|
|
}
|
|
.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); width: 16px; height: 16px; 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); }
|
|
/* TODO nach Migration entfernen: ersetzt durch .list-group-header in lists.css */
|
|
.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-spezifischer Override: vertikales Layout statt horizontalem .list-item-card */
|
|
.notes-card { flex-direction: column; gap: var(--space-2); }
|
|
.notes-card-top { display: flex; align-items: flex-start; gap: var(--space-2); width: 100%; }
|
|
/* TODO nach Migration entfernen: ersetzt durch .list-item-chip */
|
|
/* .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; }
|
|
/* TODO nach Migration entfernen: ersetzt durch .list-item-meta-row */
|
|
/* .notes-card-meta { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); color: var(--c-text-muted); } */
|
|
/* Notes-Override: Actions in Top-Zeile rechts ausrichten (statt align-self:center bei list-item-actions) */
|
|
.notes-card-actions { margin-left: auto; align-self: flex-start; }
|
|
/* Notes-Override: Newlines (pre-wrap) + max 5 Zeilen mit "…", Rest in Detail-Modal */
|
|
.notes-card-text { line-height: 1.55; white-space: pre-wrap; margin: 0; color: var(--c-text);
|
|
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5; overflow: hidden; }
|
|
/* Detail-Modal: voller Notiz-Text scrollbar */
|
|
.notes-detail-text { white-space: pre-wrap; line-height: 1.6; font-size: var(--text-base);
|
|
color: var(--c-text); margin: 0; max-height: 60vh; overflow-y: auto; }
|
|
/* TODO nach Migration entfernen: ersetzt durch .list-item-micro-badges / .list-item-micro-badge */
|
|
/* .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); } */
|
|
/* TODO nach Migration entfernen: ersetzt durch .list-item-action-btn / --danger */
|
|
/* .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); }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
</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">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>
|
|
Muster-Analyse
|
|
</div>
|
|
<svg class="ph-icon notes-ki-chevron ${_kiOpen ? 'notes-ki-chevron--open' : ''}" id="notes-ki-chevron" aria-hidden="true"><use href="/icons/phosphor.svg#caret-down"></use></svg>
|
|
</div>
|
|
${_kiOpen ? `
|
|
<div class="notes-ki-body" id="notes-ki-body">
|
|
<button class="notes-ki-btn" id="notes-ki-analyse-btn" ${_kiLoading ? 'disabled' : ''}>
|
|
${_kiLoading ? '<svg class="ph-icon" aria-hidden="true" style="animation:spin 1s linear infinite"><use href="/icons/phosphor.svg#spinner-gap"></use></svg> Analysiere…' : 'Analysieren'}
|
|
</button>
|
|
${_kiError ? `<div class="notes-ki-error"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-circle"></use></svg> ${UI.escape(_kiError)}</div>` : ''}
|
|
${_kiSuggestions ? `
|
|
<div class="notes-ki-suggestions">
|
|
<ul>
|
|
${_kiSuggestions.map(s => `<li>${UI.escape(s)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// 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 `<div class="notes-media-strip" style="display:flex;align-items:center;gap:12px;margin-top:6px;font-size:12px;color:var(--c-text-secondary)">
|
|
${parts.map(([icon, count]) => `<span style="display:inline-flex;align-items:center;gap:4px">${UI.icon(icon)} ${count}</span>`).join('')}
|
|
</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="list-item-card list-item-card--clickable notes-card" data-id="${note.id}">
|
|
<!-- Top-Zeile: Rubrik-Chip + parent_label + Zeit + Buttons -->
|
|
<div class="notes-card-top">
|
|
<span class="list-item-chip" style="--chip-color:${rb.color}">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${rb.icon}"></use></svg>
|
|
${UI.escape(rb.label)}
|
|
</span>
|
|
${note.parent_label
|
|
? `<span class="notes-parent-label" title="${UI.escape(note.parent_label)}">${UI.escape(note.parent_label)}</span>`
|
|
: ''
|
|
}
|
|
<div class="list-item-actions notes-card-actions">
|
|
<button class="list-item-action-btn notes-edit-btn" data-id="${note.id}" title="Bearbeiten">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil"></use></svg>
|
|
</button>
|
|
<button class="list-item-action-btn list-item-action-btn--danger notes-delete-btn" data-id="${note.id}" title="Löschen">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notiztext -->
|
|
${note.text ? `<p class="list-item-text notes-card-text">${UI.escape(_truncate(note.text))}</p>` : ''}
|
|
|
|
${_noteMediaStrip(note)}
|
|
|
|
<!-- Micro-Badges -->
|
|
${microBadges.length ? `
|
|
<div class="list-item-micro-badges">
|
|
${microBadges.map(b => `<span class="list-item-micro-badge">${UI.escape(b)}</span>`).join('')}
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Meta: Zeit + Ort -->
|
|
<div class="list-item-meta-row">
|
|
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#clock"></use></svg>
|
|
${UI.escape(_formatTime(note.updated_at || note.created_at))}
|
|
${hasLocation ? `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#map-pin"></use></svg> ${UI.escape(note.location_name)}` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// 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: `
|
|
<div class="flex-col-gap-3">
|
|
${note.parent_label
|
|
? `<div class="text-sm-secondary"><strong>${UI.escape(note.parent_label)}</strong></div>` : ''}
|
|
|
|
${note.text ? `<p class="notes-detail-text">${UI.escape(note.text)}</p>` : ''}
|
|
|
|
${(note.media_items && note.media_items.length) ? `
|
|
<div style="display:flex;flex-direction:column;gap:8px">
|
|
${note.media_items.map(m => {
|
|
if (m.media_type === 'image')
|
|
return `<img class="notes-detail-media-img" data-full="${m.url}" src="${_notePreview(m.url)}" alt="" style="width:100%;max-height:320px;object-fit:cover;border-radius:var(--radius-md);cursor:pointer">`;
|
|
if (m.media_type === 'video')
|
|
return `<video src="${m.url}" controls playsinline style="width:100%;max-height:320px;border-radius:var(--radius-md);background:#000"></video>`;
|
|
if (m.media_type === 'audio')
|
|
return `<audio controls src="${m.url}" style="width:100%"></audio>`;
|
|
return `<a href="${m.url}" target="_blank" rel="noopener" class="btn btn-secondary" style="justify-content:flex-start;gap:8px">${UI.icon('file-text')} ${m.media_type === 'pdf' ? 'PDF öffnen' : 'Datei öffnen'}</a>`;
|
|
}).join('')}
|
|
</div>` : ''}
|
|
|
|
${microBadges.length ? `
|
|
<div class="list-item-micro-badges">
|
|
${microBadges.map(b => `<span class="list-item-micro-badge">${UI.escape(b)}</span>`).join('')}
|
|
</div>` : ''}
|
|
|
|
<div class="list-item-meta-row" style="margin-top:var(--space-2)">
|
|
<svg class="ph-icon icon-sm" aria-hidden="true"><use href="/icons/phosphor.svg#clock"></use></svg>
|
|
${UI.escape(_formatTime(note.updated_at || note.created_at))}
|
|
${note.location_name
|
|
? `<svg class="ph-icon icon-sm" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg> ${UI.escape(note.location_name)}` : ''}
|
|
</div>
|
|
</div>
|
|
`,
|
|
footer: `
|
|
<div class="flex-gap-2" style="width:100%">
|
|
<button class="btn btn-ghost flex-1" id="notes-detail-edit">
|
|
${UI.icon('pencil')} Bearbeiten
|
|
</button>
|
|
<button class="btn btn-secondary" data-modal-close>Schließen</button>
|
|
</div>
|
|
`,
|
|
});
|
|
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 `
|
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
|
width:100%;max-width:480px;max-height:90vh;overflow-y:auto;
|
|
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
|
|
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
|
|
<h3 style="font-size:var(--text-base);font-weight:700;margin:0 0 var(--space-4)">Neue Notiz</h3>
|
|
|
|
<!-- Kategorie-Auswahl -->
|
|
<div class="mb-4">
|
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;color:var(--c-text);margin-bottom:var(--space-2)">Kategorie</label>
|
|
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2)">
|
|
${ERSTELL_RUBRIKEN.map(r => `
|
|
<button class="nc-cat" data-type="${r.type}"
|
|
style="font-size:var(--text-xs);font-weight:600;padding:4px var(--space-3);
|
|
border-radius:999px;border:1.5px solid ${_selType===r.type ? r.color : 'var(--c-border)'};
|
|
background:${_selType===r.type ? r.color+'22' : 'var(--c-surface-2)'};
|
|
color:${_selType===r.type ? r.color : 'var(--c-text-secondary)'};cursor:pointer">
|
|
${UI.escape(r.label)}
|
|
</button>`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Text -->
|
|
<div class="mb-4">
|
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
|
|
<textarea id="nc-text" rows="5" placeholder="Was möchtest du festhalten…"
|
|
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"></textarea>
|
|
</div>
|
|
|
|
<!-- Medien -->
|
|
<div class="mb-4">
|
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;color:var(--c-text);margin-bottom:var(--space-2)">Medien</label>
|
|
<div id="nc-media"></div>
|
|
</div>
|
|
|
|
<div class="flex-gap-3">
|
|
<button id="nc-cancel" class="btn btn-ghost flex-1">Abbrechen</button>
|
|
<button id="nc-save" class="btn btn-primary flex-1">Speichern</button>
|
|
</div>
|
|
</div>`;
|
|
};
|
|
|
|
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 = `
|
|
<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}">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${rb.icon}"></use></svg> ${UI.escape(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">${UI.escape(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 class="flex-gap-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 class="flex-gap-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 class="flex-gap-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>
|
|
` : ''}
|
|
|
|
<!-- Medien -->
|
|
<div>
|
|
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text);margin-bottom:var(--space-2)">Medien</label>
|
|
<div id="notes-edit-media"></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);
|
|
|
|
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 };
|
|
|
|
})();
|