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
This commit is contained in:
parent
95f91fdc00
commit
553e9e7854
35 changed files with 4558 additions and 370 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -28,6 +28,10 @@ window.Page_dog_profile = (() => {
|
|||
if (e.target.closest('#profile-goto-login')) {
|
||||
App.navigate('settings');
|
||||
}
|
||||
if (e.target.closest('[data-action="goto-weight"]')) {
|
||||
App.navigate('health', true, { tab: 'gewicht', openForm: true });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
await _render();
|
||||
|
|
@ -119,7 +123,7 @@ window.Page_dog_profile = (() => {
|
|||
</div>
|
||||
` : ''}
|
||||
${dog.gewicht_kg ? `
|
||||
<div class="card" style="padding:var(--space-3)">
|
||||
<div class="card" style="padding:var(--space-3);cursor:pointer" data-action="goto-weight">
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg> Gewicht</div>
|
||||
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ window.Page_erste_hilfe = (() => {
|
|||
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold)">Bedeutung</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Rosa, feucht</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:#22c55e;font-weight:var(--weight-semibold)">Normal</td></tr>
|
||||
<tr><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Rosa, feucht</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-success);font-weight:var(--weight-semibold)">Normal</td></tr>
|
||||
<tr style="background:var(--c-surface-2)"><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Blass / weiß</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-danger)">Schock, Blutverlust, Vergiftung</td></tr>
|
||||
<tr><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Blau / grau</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-danger);font-weight:var(--weight-semibold)">Sauerstoffmangel — NOTFALL</td></tr>
|
||||
<tr style="background:var(--c-surface-2)"><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">Gelb</td><td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border);color:var(--c-warning)">Leberprobleme</td></tr>
|
||||
|
|
@ -239,6 +239,7 @@ window.Page_erste_hilfe = (() => {
|
|||
`;
|
||||
_bindTabs();
|
||||
_bindAccordions();
|
||||
_bindNoteButtons();
|
||||
_activateTab('lebensgefahr');
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +341,10 @@ window.Page_erste_hilfe = (() => {
|
|||
${massnahmenHtml}
|
||||
${warnHtml}
|
||||
${e.extra || ''}
|
||||
<div style="margin-top:var(--space-3);text-align:right">
|
||||
<button class="btn btn-ghost btn-xs eh-note-btn" style="font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 8px"
|
||||
data-kat-id="${katId}" data-titel="${e.titel.replace(/"/g,'"')}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -382,6 +387,102 @@ window.Page_erste_hilfe = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function _bindNoteButtons() {
|
||||
_container.querySelectorAll('.eh-note-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const katId = btn.dataset.katId;
|
||||
const titel = btn.dataset.titel;
|
||||
const kat = KATEGORIEN.find(k => k.id === katId);
|
||||
const label = kat ? `${kat.label} — ${titel}` : titel;
|
||||
_openNoteModal('erste_hilfe', katId, label, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
const _esc = s => s ? String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : '';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, parentId);
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, parentId, payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ----------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -226,7 +226,14 @@ window.Page_events = (() => {
|
|||
</a>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten">${_icon('pencil-simple')}</button>` : ''}
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-1)">
|
||||
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten" onclick="event.stopPropagation()">${_icon('pencil-simple')}</button>` : ''}
|
||||
${_state.user ? `<button class="btn-icon ev-note-btn" data-ev-note-id="${ev.id}"
|
||||
data-ev-note-label="${UI.escape(ev.titel + ' ' + ev.datum)}"
|
||||
data-ev-note-ort="${UI.escape(ev.ort_name || '')}"
|
||||
title="Notiz" style="color:var(--c-text-muted)" onclick="event.stopPropagation()">
|
||||
${_icon('note-pencil')}</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -268,7 +275,7 @@ window.Page_events = (() => {
|
|||
const popup = `
|
||||
<div style="min-width:180px">
|
||||
<strong>${UI.escape(ev.titel)}</strong><br>
|
||||
<span style="color:#666;font-size:12px">${datum}</span><br>
|
||||
<span style="color:var(--c-text-muted);font-size:12px">${datum}</span><br>
|
||||
${ev.ort_name ? `<span style="font-size:12px">📍 ${UI.escape(ev.ort_name)}</span><br>` : ''}
|
||||
${ev.beschreibung ? `<span style="font-size:12px">${UI.escape(ev.beschreibung.slice(0, 80))}${ev.beschreibung.length > 80 ? '…' : ''}</span><br>` : ''}
|
||||
<a href="#" onclick="event.preventDefault();Page_events._openDetail(${ev.id})"
|
||||
|
|
@ -634,11 +641,77 @@ window.Page_events = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Notiz-Button
|
||||
const noteBtn = e.target.closest('.ev-note-btn');
|
||||
if (noteBtn) {
|
||||
e.stopPropagation();
|
||||
_openNoteModal(
|
||||
'event',
|
||||
parseInt(noteBtn.dataset.evNoteId),
|
||||
noteBtn.dataset.evNoteLabel,
|
||||
noteBtn.dataset.evNoteOrt || null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Karten-Klick → Detail
|
||||
const card = e.target.closest('[data-ev-id]');
|
||||
if (card) { _showDetail(parseInt(card.dataset.evId)); }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="ev-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="ev-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu diesem Event…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="ev-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="ev-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#ev-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#ev-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#ev-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#ev-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, openNew, _openDetail: _showDetail };
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ window.Page_friends = (() => {
|
|||
<div style="width:36px;height:36px;border-radius:50%;flex-shrink:0;
|
||||
background:var(--c-primary-subtle);
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
||||
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#link"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,17 @@ window.Page_health = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// LIFECYCLE
|
||||
// ----------------------------------------------------------
|
||||
async function init(container, appState) {
|
||||
async function init(container, appState, params) {
|
||||
_container = container;
|
||||
_appState = appState;
|
||||
if (params?.tab) {
|
||||
const valid = _getTabs().some(t => t.key === params.tab);
|
||||
if (valid) _activeTab = params.tab;
|
||||
}
|
||||
await _render();
|
||||
if (params?.openForm) {
|
||||
setTimeout(() => _showForm(null, _activeTab), 200);
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
|
|
@ -400,6 +407,10 @@ window.Page_health = (() => {
|
|||
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
|
||||
</div>` : ''}
|
||||
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="${_esc(e.bezeichnung)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -445,6 +456,10 @@ window.Page_health = (() => {
|
|||
</div>` : ''}
|
||||
${e.diagnose ? `<div class="health-card-note"><b>Diagnose:</b> ${_esc(e.diagnose)}</div>` : ''}
|
||||
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="${_esc(e.bezeichnung)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -493,6 +508,10 @@ window.Page_health = (() => {
|
|||
</span>
|
||||
</div>
|
||||
${e.notiz ? `<div class="health-card-note" style="padding-top:var(--space-1)">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-1);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="Gewicht ${_esc(e.datum)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
|
|
@ -726,6 +745,10 @@ window.Page_health = (() => {
|
|||
${interval ? ` · Abstand zur Vorherigen: ${interval} Tage` : ''}
|
||||
</div>
|
||||
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="Läufigkeit ${_esc(e.datum)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
|
@ -760,6 +783,10 @@ window.Page_health = (() => {
|
|||
${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''}
|
||||
</div>
|
||||
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="${_esc(e.bezeichnung)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
|
@ -797,6 +824,10 @@ window.Page_health = (() => {
|
|||
</div>
|
||||
${e.reaktion ? `<div class="health-card-note"><b>Reaktion:</b> ${_esc(e.reaktion)}</div>` : ''}
|
||||
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="${_esc(e.bezeichnung)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
|
@ -837,6 +868,10 @@ window.Page_health = (() => {
|
|||
${count > 1 ? ` · ${count} Dateien` : ''}
|
||||
</div>
|
||||
${e.notiz ? `<div class="health-card-note">${_esc(e.notiz)}</div>` : ''}
|
||||
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
|
||||
data-action="open-note" data-entry-id="${e.id}"
|
||||
data-label="${_esc(e.bezeichnung)}"
|
||||
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
|
||||
${count
|
||||
? `<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);align-items:center;flex-wrap:wrap">
|
||||
${mediaList.slice(0, 3).map(m => m.media_type === 'pdf'
|
||||
|
|
@ -874,6 +909,14 @@ window.Page_health = (() => {
|
|||
const entry = (_data[_activeTab] || []).find(e => e.id === id);
|
||||
if (entry) card.addEventListener('click', () => _openDetail(entry));
|
||||
});
|
||||
content.querySelectorAll('[data-action="open-note"]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const id = parseInt(btn.dataset.entryId);
|
||||
const label = btn.dataset.label || '';
|
||||
_openNoteModal('health', id, label, null);
|
||||
});
|
||||
});
|
||||
// Praxis öffnen
|
||||
content.querySelectorAll('[data-action="open-praxis"]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
|
|
@ -1166,6 +1209,9 @@ window.Page_health = (() => {
|
|||
if (!_data[t]) _data[t] = [];
|
||||
_data[t].unshift(saved);
|
||||
UI.toast.success('Eintrag erstellt.');
|
||||
if (t === 'gewicht' && saved.wert) {
|
||||
_appState.activeDog.gewicht_kg = saved.wert;
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-File-Upload
|
||||
|
|
@ -1830,6 +1876,89 @@ window.Page_health = (() => {
|
|||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NOTIZ-MODAL (custom DOM, kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
// Vorhandenes Modal entfernen falls noch offen
|
||||
document.getElementById('by-note-modal')?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'by-note-modal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-xl) var(--radius-xl) 0 0;
|
||||
width:100%;max-width:640px;max-height:90vh;display:flex;flex-direction:column;
|
||||
padding-bottom:env(safe-area-inset-bottom,0px)">
|
||||
<div style="padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div>
|
||||
</div>
|
||||
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div style="padding:var(--space-4) var(--space-5);flex:1;overflow-y:auto">
|
||||
<form id="by-note-form">
|
||||
<textarea id="by-note-text" class="form-control" rows="5"
|
||||
placeholder="Notiz eingeben…"
|
||||
style="width:100%;resize:vertical"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-5);border-top:1px solid var(--c-border);
|
||||
display:flex;gap:var(--space-2);flex-shrink:0">
|
||||
<button type="button" id="by-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button type="submit" form="by-note-form" id="by-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const textarea = document.getElementById('by-note-text');
|
||||
const saveBtn = document.getElementById('by-note-save');
|
||||
const cancelBtn = document.getElementById('by-note-cancel');
|
||||
const closeBtn = document.getElementById('by-note-close');
|
||||
|
||||
let existingNoteId = null;
|
||||
|
||||
// Vorhandene Notiz laden
|
||||
try {
|
||||
const existing = await API.notes.get(parentType, parentId);
|
||||
if (existing?.id) {
|
||||
existingNoteId = existing.id;
|
||||
textarea.value = existing.text || '';
|
||||
}
|
||||
} catch (_) { /* keine Notiz vorhanden — ok */ }
|
||||
|
||||
setTimeout(() => textarea.focus(), 100);
|
||||
|
||||
const _close = () => overlay.remove();
|
||||
closeBtn.addEventListener('click', _close);
|
||||
cancelBtn.addEventListener('click', _close);
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _close(); });
|
||||
|
||||
document.getElementById('by-note-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const text = textarea.value.trim();
|
||||
UI.setLoading(saveBtn, true);
|
||||
try {
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName };
|
||||
if (existingNoteId) {
|
||||
await API.notes.update(existingNoteId, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, parentId, payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
_close();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
UI.setLoading(saveBtn, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, openNew, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
|
|||
693
backend/static/js/pages/notes.js
Normal file
693
backend/static/js/pages/notes.js
Normal file
|
|
@ -0,0 +1,693 @@
|
|||
/* ============================================================
|
||||
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 };
|
||||
|
||||
})();
|
||||
|
|
@ -126,7 +126,7 @@ window.Page_onboarding = (() => {
|
|||
<div style="width:36px;height:36px;border-radius:var(--radius-md);
|
||||
background:var(--c-primary-subtle);flex-shrink:0;
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
||||
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -167,7 +167,7 @@ window.Page_onboarding = (() => {
|
|||
<div style="width:64px;height:64px;border-radius:50%;
|
||||
background:var(--c-primary-subtle);margin:0 auto var(--space-4);
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
|
||||
<svg style="fill:currentColor;width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#dog"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -262,7 +262,7 @@ window.Page_onboarding = (() => {
|
|||
<div style="width:80px;height:80px;border-radius:50%;
|
||||
background:var(--c-success-subtle,#dcfce7);margin:0 auto var(--space-4);
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:40px;height:40px;color:var(--c-success)" aria-hidden="true">
|
||||
<svg style="fill:currentColor;width:40px;height:40px;color:var(--c-success)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#check-circle"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1805,6 +1805,7 @@ window.Page_routes = (() => {
|
|||
${_actionBtn('rd-gpx', 'download-simple', 'GPX')}
|
||||
${_actionBtn('rd-share', 'arrow-square-out', 'Teilen')}
|
||||
${_actionBtn('rd-navi', 'map-pin', 'Navi')}
|
||||
${_appState.user ? _actionBtn('rd-note', 'note-pencil', 'Notiz') : ''}
|
||||
</div>
|
||||
${ownerRow}
|
||||
<button type="button" class="btn btn-primary w-full" id="rd-close">Schließen</button>
|
||||
|
|
@ -1920,6 +1921,12 @@ window.Page_routes = (() => {
|
|||
} catch (err) { UI.toast.error(err.message); }
|
||||
});
|
||||
|
||||
// Notiz-Button
|
||||
document.getElementById('rd-note')?.addEventListener('click', () => {
|
||||
const label = route.name || (route.distanz_km ? route.distanz_km.toFixed(1) + ' km' : 'Route');
|
||||
_openNoteModal('route', route.id, label, null);
|
||||
});
|
||||
|
||||
// Mini-Map
|
||||
let _detailMap = null;
|
||||
setTimeout(() => {
|
||||
|
|
@ -2504,6 +2511,59 @@ window.Page_routes = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="rk-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="rk-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu dieser Route…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="rk-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="rk-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#rk-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#rk-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#rk-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#rk-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -266,6 +266,30 @@ window.Page_settings = (() => {
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!-- KI-Notiz-Assistent -->
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#brain"></use></svg>
|
||||
<div style="flex:1">
|
||||
<div style="font-weight:500">KI-Notiz-Assistent</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
|
||||
Erkennt Muster in deinen Notizen und macht Vorschläge
|
||||
</div>
|
||||
</div>
|
||||
<label style="position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0">
|
||||
<input type="checkbox" id="toggle-notes-ki"
|
||||
style="opacity:0;width:0;height:0;position:absolute"
|
||||
${u.notes_ki_enabled ? 'checked' : ''}>
|
||||
<span style="position:absolute;cursor:pointer;inset:0;border-radius:12px;
|
||||
background:var(--c-border);transition:.2s"
|
||||
id="toggle-notes-ki-track"></span>
|
||||
<span id="toggle-notes-ki-thumb"
|
||||
style="position:absolute;top:2px;left:${u.notes_ki_enabled ? '22px' : '2px'};
|
||||
width:20px;height:20px;border-radius:50%;
|
||||
background:#fff;transition:.2s;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.3)"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -635,6 +659,25 @@ window.Page_settings = (() => {
|
|||
: 'Pocket-Modus deaktiviert.');
|
||||
});
|
||||
|
||||
document.getElementById('toggle-notes-ki')?.addEventListener('change', async e => {
|
||||
const enabled = e.target.checked;
|
||||
const track = document.getElementById('toggle-notes-ki-track');
|
||||
const thumb = document.getElementById('toggle-notes-ki-thumb');
|
||||
if (track) track.style.background = enabled ? 'var(--c-primary)' : 'var(--c-border)';
|
||||
if (thumb) thumb.style.left = enabled ? '22px' : '2px';
|
||||
try {
|
||||
await API.patch('/profile', { notes_ki_enabled: enabled ? 1 : 0 });
|
||||
_appState.user.notes_ki_enabled = enabled ? 1 : 0;
|
||||
UI.toast.success(enabled ? 'KI-Notiz-Assistent aktiviert.' : 'KI-Notiz-Assistent deaktiviert.');
|
||||
} catch (err) {
|
||||
UI.toast.error(err?.message || 'Einstellung konnte nicht gespeichert werden.');
|
||||
// Revert UI
|
||||
e.target.checked = !enabled;
|
||||
if (track) track.style.background = !enabled ? 'var(--c-primary)' : 'var(--c-border)';
|
||||
if (thumb) thumb.style.left = !enabled ? '22px' : '2px';
|
||||
}
|
||||
});
|
||||
|
||||
_loadReferral();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,12 @@ window.Page_sitting = (() => {
|
|||
<div class="sitting-card-side">
|
||||
<div class="sitting-price">${s.preis_pro_tag > 0 ? s.preis_pro_tag.toFixed(0) + ' €/Tag' : 'Preis anfragen'}</div>
|
||||
<div class="sitting-dogs">max. ${s.max_hunde} Hund${s.max_hunde !== 1 ? 'e' : ''}</div>
|
||||
${_state.user ? `<button class="btn-icon sit-note-btn"
|
||||
data-sit-note-id="${s.id}"
|
||||
data-sit-note-label="${UI.escHtml(s.sitter_name + ' ' + (s.datum || ''))}"
|
||||
title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)"
|
||||
onclick="event.stopPropagation()">
|
||||
${UI.icon('note-pencil')}</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -704,6 +710,19 @@ window.Page_sitting = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Notiz-Button auf Sitter-Karte
|
||||
const noteBtn = e.target.closest('.sit-note-btn');
|
||||
if (noteBtn) {
|
||||
e.stopPropagation();
|
||||
_openNoteModal(
|
||||
'sitting',
|
||||
parseInt(noteBtn.dataset.sitNoteId),
|
||||
noteBtn.dataset.sitNoteLabel,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sitter-Karte
|
||||
const sitterCard = e.target.closest('[data-sit-id]');
|
||||
if (sitterCard && !e.target.closest('button')) {
|
||||
|
|
@ -741,6 +760,59 @@ window.Page_sitting = (() => {
|
|||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="sit-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="sit-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu diesem Sitter…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="sit-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="sit-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#sit-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#sit-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#sit-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#sit-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh };
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -895,6 +895,7 @@ window.Page_uebungen = (() => {
|
|||
_bindAccordions();
|
||||
_bindStatusButtons();
|
||||
_bindLogButtons();
|
||||
_bindNotizButtons();
|
||||
if (_activeTab === 'ki-trainer') _loadKiTrainerFeedback();
|
||||
}
|
||||
|
||||
|
|
@ -965,6 +966,19 @@ window.Page_uebungen = (() => {
|
|||
Einheit
|
||||
</button>
|
||||
${_sessionStatsChip(_activeTab, u.name)}
|
||||
<button class="ueb-notiz-btn"
|
||||
data-tab="${_esc(_activeTab)}"
|
||||
data-name="${_esc(u.name)}"
|
||||
title="Notiz hinzufügen"
|
||||
style="background:none;border:1px solid var(--c-border);cursor:pointer;
|
||||
padding:3px 7px;border-radius:var(--radius-sm);
|
||||
display:flex;align-items:center;gap:3px;
|
||||
font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#note-pencil"></use>
|
||||
</svg>
|
||||
Notiz
|
||||
</button>
|
||||
<button class="ueb-status-btn"
|
||||
data-tab="${_esc(_activeTab)}"
|
||||
data-name="${_esc(u.name)}"
|
||||
|
|
@ -1006,7 +1020,7 @@ window.Page_uebungen = (() => {
|
|||
background:#78350f22;border:1px solid #d9770644;border-radius:var(--radius-sm);
|
||||
font-size:var(--text-xs);color:var(--c-text);line-height:1.4;
|
||||
display:flex;align-items:flex-start;gap:var(--space-2)">
|
||||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||||
<span>${_esc(u.hinweis)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
|
|
@ -1039,7 +1053,7 @@ window.Page_uebungen = (() => {
|
|||
${u.fehler.length ? `
|
||||
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
||||
<svg class="ph-icon" style="width:12px;height:12px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||||
<svg class="ph-icon" style="width:12px;height:12px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||||
Häufige Fehler
|
||||
</p>
|
||||
<ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
|
|
@ -1100,6 +1114,252 @@ window.Page_uebungen = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function _bindNotizButtons() {
|
||||
_container.querySelectorAll('.ueb-notiz-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const exerciseId = `${btn.dataset.tab}_${btn.dataset.name.replace(/[\s/]+/g, '_')}`;
|
||||
_openNotizModal(exerciseId, btn.dataset.name, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function _openNotizModal(exerciseId, exerciseName, triggerBtn) {
|
||||
const modalId = 'ueb-notiz-modal';
|
||||
document.getElementById(modalId)?.remove();
|
||||
|
||||
// Lade bestehende Notiz
|
||||
let existingNote = null;
|
||||
if (_appState?.user) {
|
||||
try {
|
||||
const notes = await API.notes.get('training_session', exerciseId.length > 0 ? exerciseId : 0);
|
||||
if (notes && notes.length > 0) existingNote = notes[0];
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
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 noteText = existingNote?.text || '';
|
||||
const meta = existingNote?.meta_json || {};
|
||||
const currentErfolgsquote = meta.erfolgsquote || null;
|
||||
const currentUmgebung = meta.umgebung || null;
|
||||
const currentStimmung = meta.hund_stimmung || null;
|
||||
|
||||
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)">
|
||||
<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:var(--weight-semibold);color:var(--c-text);
|
||||
margin:0 0 var(--space-4);text-align:center">
|
||||
Notiz: ${_esc(exerciseName)}
|
||||
</h3>
|
||||
|
||||
<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)">Notiz</label>
|
||||
<textarea id="ueb-notiz-text" rows="3"
|
||||
placeholder="Was ist dir aufgefallen? Tipps für nächstes Mal…"
|
||||
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">${_esc(noteText)}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Erfolgsquote -->
|
||||
<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 (optional)</label>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
${[1,2,3,4,5].map(n => `
|
||||
<button type="button" class="ueb-notiz-pfote" data-val="${n}"
|
||||
style="font-size:1.4rem;border:1.5px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:4px 10px;cursor:pointer;
|
||||
background:${currentErfolgsquote === n ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
|
||||
border-color:${currentErfolgsquote === n ? 'var(--c-primary)' : 'var(--c-border)'};
|
||||
transition:all 0.15s">🐾</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 (optional)</label>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
${[['🏠','zuhause'],['🌿','natur'],['🌆','stadt']].map(([emoji, val]) => `
|
||||
<button type="button" class="ueb-notiz-umgebung" data-val="${val}"
|
||||
style="font-size:1.2rem;border:1.5px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
|
||||
background:${currentUmgebung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
|
||||
border-color:${currentUmgebung === val ? 'var(--c-primary)' : 'var(--c-border)'};
|
||||
transition:all 0.15s">${emoji}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hund-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 (optional)</label>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
${[['😊','super'],['😐','ok'],['😔','mude']].map(([emoji, val]) => `
|
||||
<button type="button" class="ueb-notiz-stimmung" data-val="${val}"
|
||||
style="font-size:1.2rem;border:1.5px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:4px 12px;cursor:pointer;
|
||||
background:${currentStimmung === val ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)'};
|
||||
border-color:${currentStimmung === val ? 'var(--c-primary)' : 'var(--c-border)'};
|
||||
transition:all 0.15s">${emoji}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
|
||||
${existingNote ? `
|
||||
<button id="ueb-notiz-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="ueb-notiz-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="ueb-notiz-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">
|
||||
${existingNote ? 'Aktualisieren' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// State
|
||||
let selectedErfolgsquote = currentErfolgsquote;
|
||||
let selectedUmgebung = currentUmgebung;
|
||||
let selectedStimmung = currentStimmung;
|
||||
|
||||
// Pfoten-Buttons
|
||||
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const val = parseInt(btn.dataset.val, 10);
|
||||
selectedErfolgsquote = selectedErfolgsquote === val ? null : val;
|
||||
overlay.querySelectorAll('.ueb-notiz-pfote').forEach(b => {
|
||||
const active = parseInt(b.dataset.val, 10) === selectedErfolgsquote;
|
||||
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
|
||||
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Umgebung-Buttons
|
||||
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
selectedUmgebung = selectedUmgebung === btn.dataset.val ? null : btn.dataset.val;
|
||||
overlay.querySelectorAll('.ueb-notiz-umgebung').forEach(b => {
|
||||
const active = b.dataset.val === selectedUmgebung;
|
||||
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
|
||||
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Stimmung-Buttons
|
||||
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
selectedStimmung = selectedStimmung === btn.dataset.val ? null : btn.dataset.val;
|
||||
overlay.querySelectorAll('.ueb-notiz-stimmung').forEach(b => {
|
||||
const active = b.dataset.val === selectedStimmung;
|
||||
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-surface-2)';
|
||||
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function _closeNotizModal() {
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _closeNotizModal(); });
|
||||
overlay.querySelector('#ueb-notiz-cancel').addEventListener('click', _closeNotizModal);
|
||||
|
||||
// Speichern
|
||||
overlay.querySelector('#ueb-notiz-save').addEventListener('click', async () => {
|
||||
const text = overlay.querySelector('#ueb-notiz-text').value.trim();
|
||||
if (!text) { UI.toast.warning('Bitte gib eine Notiz ein.'); return; }
|
||||
|
||||
const saveBtn = overlay.querySelector('#ueb-notiz-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichern…';
|
||||
|
||||
const meta = {};
|
||||
if (selectedErfolgsquote) meta.erfolgsquote = selectedErfolgsquote;
|
||||
if (selectedUmgebung) meta.umgebung = selectedUmgebung;
|
||||
if (selectedStimmung) meta.hund_stimmung = selectedStimmung;
|
||||
|
||||
const payload = {
|
||||
text,
|
||||
meta_json: Object.keys(meta).length > 0 ? meta : null,
|
||||
};
|
||||
|
||||
try {
|
||||
if (existingNote) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create('training_session', exerciseId, payload);
|
||||
}
|
||||
_closeNotizModal();
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
// Notiz-Button leicht hervorheben
|
||||
if (triggerBtn) {
|
||||
triggerBtn.style.borderColor = 'var(--c-primary)';
|
||||
triggerBtn.style.color = 'var(--c-primary)';
|
||||
}
|
||||
} catch (err) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = existingNote ? 'Aktualisieren' : 'Speichern';
|
||||
UI.toast.error('Speichern fehlgeschlagen.');
|
||||
}
|
||||
});
|
||||
|
||||
// Löschen
|
||||
overlay.querySelector('#ueb-notiz-delete')?.addEventListener('click', async () => {
|
||||
if (!existingNote) return;
|
||||
try {
|
||||
await API.notes.delete(existingNote.id);
|
||||
_closeNotizModal();
|
||||
UI.toast.success('Notiz gelöscht.');
|
||||
if (triggerBtn) {
|
||||
triggerBtn.style.borderColor = '';
|
||||
triggerBtn.style.color = '';
|
||||
}
|
||||
} catch (_) {
|
||||
UI.toast.error('Löschen fehlgeschlagen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _openLogModal(tab, exerciseName, initialReps) {
|
||||
// Build the modal HTML
|
||||
const modalId = 'ueb-log-modal';
|
||||
|
|
|
|||
|
|
@ -192,6 +192,18 @@ window.Page_walks = (() => {
|
|||
el.querySelectorAll('.walks-card').forEach(card => {
|
||||
card.addEventListener('click', () => _openDetail(parseInt(card.dataset.id)));
|
||||
});
|
||||
|
||||
el.querySelectorAll('.wk-note-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_openNoteModal(
|
||||
'walk',
|
||||
parseInt(btn.dataset.wkNoteId),
|
||||
btn.dataset.wkNoteLabel,
|
||||
btn.dataset.wkNoteOrt || null
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _walkCardHTML(w) {
|
||||
|
|
@ -217,7 +229,16 @@ window.Page_walks = (() => {
|
|||
${isOwn ? '<span class="walks-badge walks-badge--own">Mein Treffen</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="walks-card-arrow">›</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-1)">
|
||||
<div class="walks-card-arrow">›</div>
|
||||
${_appState.user ? `<button class="btn-icon wk-note-btn"
|
||||
data-wk-note-id="${w.id}"
|
||||
data-wk-note-label="${UI.escape(w.titel + ' ' + w.datum)}"
|
||||
data-wk-note-ort="${UI.escape(w.ort_name || '')}"
|
||||
title="Notiz" style="color:var(--c-text-muted);font-size:var(--text-xs)"
|
||||
onclick="event.stopPropagation()">
|
||||
${UI.icon('note-pencil')}</button>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -964,6 +985,59 @@ window.Page_walks = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden)
|
||||
// ----------------------------------------------------------
|
||||
async function _openNoteModal(parentType, parentId, parentLabel, locationName) {
|
||||
let existingNote = null;
|
||||
try { existingNote = await API.notes.get(parentType, String(parentId)); } catch {}
|
||||
|
||||
const ovl = document.createElement('div');
|
||||
ovl.style.cssText = 'position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
|
||||
ovl.innerHTML = `
|
||||
<div style="width:100%;max-width:600px;background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
padding:var(--space-4);box-sizing:border-box;max-height:80vh;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" aria-hidden="true" style="color:var(--c-primary)"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
|
||||
<span style="font-weight:600;flex:1"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz — ${UI.escape(parentLabel)}</span>
|
||||
<button id="wk-note-close" style="background:none;border:none;cursor:pointer;color:var(--c-text-muted);padding:4px">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="wk-note-text" rows="5"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;
|
||||
background:var(--c-bg);color:var(--c-text);resize:vertical;flex:1"
|
||||
placeholder="Deine Notiz zu diesem Gassi-Treffen…">${UI.escape(existingNote?.text || '')}</textarea>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button id="wk-note-cancel" class="btn btn-secondary flex-1">Abbrechen</button>
|
||||
<button id="wk-note-save" class="btn btn-primary flex-1">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(ovl);
|
||||
|
||||
const close = () => ovl.remove();
|
||||
ovl.querySelector('#wk-note-close')?.addEventListener('click', close);
|
||||
ovl.querySelector('#wk-note-cancel')?.addEventListener('click', close);
|
||||
ovl.addEventListener('click', e => { if (e.target === ovl) close(); });
|
||||
|
||||
ovl.querySelector('#wk-note-save')?.addEventListener('click', async () => {
|
||||
const text = ovl.querySelector('#wk-note-text')?.value?.trim() || '';
|
||||
const payload = { text, parent_label: parentLabel, location_name: locationName || null };
|
||||
try {
|
||||
if (existingNote?.id) {
|
||||
await API.notes.update(existingNote.id, payload);
|
||||
} else {
|
||||
await API.notes.create(parentType, String(parentId), payload);
|
||||
}
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
close();
|
||||
} catch (err) { UI.toast.error(err.message || 'Fehler beim Speichern.'); }
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange, openNew, openDetail: _openDetail };
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ window.Page_welcome = (() => {
|
|||
style="width:36px;height:36px;border-radius:var(--radius-md);
|
||||
background:var(--c-primary);flex-shrink:0;
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:20px;height:20px;color:#fff" aria-hidden="true">
|
||||
<svg style="fill:currentColor;width:20px;height:20px;color:#fff" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#list"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -237,7 +237,7 @@ window.Page_welcome = (() => {
|
|||
<div style="width:34px;height:34px;border-radius:var(--radius-md);
|
||||
background:var(--c-primary-subtle);flex-shrink:0;
|
||||
display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
||||
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue