diff --git a/VERSION b/VERSION index a624bd7..2d0ce9f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1133 \ No newline at end of file +1131 \ No newline at end of file diff --git a/backend/routes/health.py b/backend/routes/health.py index f16645e..7b9ed35 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -15,9 +15,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"} # Erlaubte Typen -# Routine-/Pflege-Typen (wiederkehrend mit intervall_tage): parasit, krallen, fellpflege -TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument", - "laeufigkeit", "parasit", "krallen", "fellpflege"} +TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument", "laeufigkeit"} # ------------------------------------------------------------------ @@ -166,15 +164,15 @@ async def create_health(dog_id: int, data: HealthCreate, (dog_id, typ, bezeichnung, datum, naechstes, notiz, wert, einheit, charge_nr, tierarzt_name, kosten, diagnose, dosierung, haeufigkeit, aktiv, bis_datum, - schweregrad, reaktion, erinnerung, intervall_tage, tierarzt_id, + schweregrad, reaktion, erinnerung, tierarzt_id, deckdatum, wurftermin) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", (dog_id, data.typ, data.bezeichnung, data.datum, data.naechstes, data.notiz, data.wert, data.einheit, data.charge_nr, data.tierarzt_name, data.kosten, data.diagnose, data.dosierung, data.haeufigkeit, data.aktiv, data.bis_datum, - data.schweregrad, data.reaktion, data.erinnerung, data.intervall_tage, - data.tierarzt_id, data.deckdatum, data.wurftermin) + data.schweregrad, data.reaktion, data.erinnerung, data.tierarzt_id, + data.deckdatum, data.wurftermin) ) row = conn.execute( "SELECT * FROM health WHERE dog_id=? ORDER BY id DESC LIMIT 1", @@ -214,34 +212,6 @@ async def update_health(dog_id: int, entry_id: int, data: HealthUpdate, return _entry_with_media(row, media_map) -# ------------------------------------------------------------------ -# POST /api/dogs/{dog_id}/health/{id}/erledigt -# Markiert eine wiederkehrende Routine als heute erledigt und schreibt -# bei gesetztem intervall_tage das nächste Fälligkeitsdatum automatisch fort. -# ------------------------------------------------------------------ -@router.post("/{dog_id}/health/{entry_id}/erledigt") -async def complete_health(dog_id: int, entry_id: int, user=Depends(get_current_user)): - from datetime import timedelta - today = date.today() - with db() as conn: - _check_dog_owner(conn, dog_id, user["id"]) - entry = conn.execute( - "SELECT * FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id) - ).fetchone() - if not entry: - raise HTTPException(404, "Eintrag nicht gefunden.") - - intervall = entry["intervall_tage"] - naechstes = (today + timedelta(days=intervall)).isoformat() if intervall else None - conn.execute( - "UPDATE health SET datum=?, naechstes=? WHERE id=?", - (today.isoformat(), naechstes, entry_id), - ) - row = conn.execute("SELECT * FROM health WHERE id=?", (entry_id,)).fetchone() - media_map = _fetch_media_items(conn, [entry_id]) - return _entry_with_media(row, media_map) - - # ------------------------------------------------------------------ # DELETE /api/dogs/{dog_id}/health/{id} # ------------------------------------------------------------------ @@ -530,9 +500,6 @@ _TERMIN_TYPEN = { 'tierarzt': {'label': 'Tierarztbesuch','beim_tierarzt': True, 'icon': 'first-aid'}, 'medikament': {'label': 'Medikament', 'beim_tierarzt': False, 'icon': 'pill'}, 'laeufigkeit': {'label': 'Läufigkeit', 'beim_tierarzt': False, 'icon': 'calendar'}, - 'parasit': {'label': 'Zecken-/Flohschutz', 'beim_tierarzt': False, 'icon': 'bug-beetle'}, - 'krallen': {'label': 'Krallen schneiden', 'beim_tierarzt': False, 'icon': 'scissors'}, - 'fellpflege': {'label': 'Fellpflege', 'beim_tierarzt': False, 'icon': 'wind'}, } @router.get("/{dog_id}/health/terminvorschlaege") diff --git a/backend/routes/jobs.py b/backend/routes/jobs.py index 0b678e5..59c73c2 100644 --- a/backend/routes/jobs.py +++ b/backend/routes/jobs.py @@ -268,20 +268,14 @@ async def update_application( "UPDATE users SET is_social_media=1 WHERE id=?", (row["user_id"],) ) - # Atomare Gründer-Vergabe inkl. founder_number — Race-frei via Sub-Query - # (konsistent mit dogs.py / partner.py). - conn.execute( - """UPDATE users - SET is_founder = 1, - founder_number = ( - SELECT IFNULL(MAX(founder_number), 0) + 1 - FROM users WHERE is_founder = 1 - ) - WHERE id = ? - AND (is_founder IS NULL OR is_founder = 0) - AND (SELECT COUNT(*) FROM users WHERE is_founder = 1) < 100""", - (row["user_id"],) - ) + founder_count = conn.execute( + "SELECT COUNT(*) FROM users WHERE is_founder=1" + ).fetchone()[0] + if founder_count < 100: + conn.execute( + "UPDATE users SET is_founder=1 WHERE id=? AND is_founder=0", + (row["user_id"],) + ) # Status-Mail an Bewerber try: diff --git a/backend/scheduler.py b/backend/scheduler.py index 4f8ce5e..01c17ae 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -638,8 +638,7 @@ async def _job_health_reminders(): FROM health h JOIN dogs d ON d.id = h.dog_id WHERE h.naechstes IN (?, ?, ?, ?) - AND h.typ IN ('impfung', 'entwurmung', 'medikament', - 'parasit', 'krallen', 'fellpflege') + AND h.typ IN ('impfung', 'entwurmung', 'medikament') AND (h.erinnerung IS NULL OR h.erinnerung = 1) """, (str(today), str(in7), str(in3), str(yesterday))).fetchall() diff --git a/backend/static/index.html b/backend/static/index.html index 1e0a5d0..2cbbd06 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index de39835..af67c36 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -204,7 +204,6 @@ const API = (() => { }, create(dogId, data) { return post(`/dogs/${dogId}/health`, data); }, update(dogId, id, d) { return patch(`/dogs/${dogId}/health/${id}`, d); }, - complete(dogId, id) { return post(`/dogs/${dogId}/health/${id}/erledigt`); }, delete(dogId, id) { return del(`/dogs/${dogId}/health/${id}`); }, uploadDokument(dogId, id, formData) { return upload(`/dogs/${dogId}/health/${id}/dokument`, formData); diff --git a/backend/static/js/app.js b/backend/static/js/app.js index a7b5e90..bbc25c6 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1133'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1131'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index 653b117..f2137d8 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -782,7 +782,7 @@ window.Page_diary = (() => { const id = parseInt(btn.dataset.entryId); const label = btn.dataset.label || ''; const location = btn.dataset.location || null; - UI.noteModal('diary', id, label, location || null); + _openNoteModal('diary', id, label, location || null); }); }); } @@ -1190,7 +1190,7 @@ window.Page_diary = (() => { view.querySelector('#diary-dv-note')?.addEventListener('click', e => { e.stopPropagation(); const label = entry.titel || entry.datum || String(entry.id); - UI.noteModal('diary', entry.id, label, entry.location_name || null); + _openNoteModal('diary', entry.id, label, entry.location_name || null); }); // Bearbeiten @@ -1953,6 +1953,83 @@ window.Page_diary = (() => { // ---------------------------------------------------------- // 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'; + + overlay.innerHTML = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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, client_time: API.clientNow() }; + 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 diff --git a/backend/static/js/pages/erste-hilfe.js b/backend/static/js/pages/erste-hilfe.js index 780356b..e8f1582 100644 --- a/backend/static/js/pages/erste-hilfe.js +++ b/backend/static/js/pages/erste-hilfe.js @@ -461,7 +461,7 @@ window.Page_erste_hilfe = (() => { const titel = btn.dataset.titel; const kat = KATEGORIEN.find(k => k.id === katId); const label = kat ? `${kat.label} — ${titel}` : titel; - UI.noteModal('erste_hilfe', katId, label, null); + _openNoteModal('erste_hilfe', katId, label, null); }); }); } @@ -469,6 +469,85 @@ window.Page_erste_hilfe = (() => { // ---------------------------------------------------------------- // 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,'"') : ''; + + overlay.innerHTML = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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 diff --git a/backend/static/js/pages/events.js b/backend/static/js/pages/events.js index 62c2d23..afb7e62 100644 --- a/backend/static/js/pages/events.js +++ b/backend/static/js/pages/events.js @@ -643,7 +643,7 @@ window.Page_events = (() => { const noteBtn = e.target.closest('.ev-note-btn'); if (noteBtn) { e.stopPropagation(); - UI.noteModal( + _openNoteModal( 'event', parseInt(noteBtn.dataset.evNoteId), noteBtn.dataset.evNoteLabel, @@ -660,6 +660,55 @@ window.Page_events = (() => { // ---------------------------------------------------------- // 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 = ` +
+
+ + Notiz — ${UI.escape(parentLabel)} + +
+ +
+ + +
+
+ `; + 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 }; diff --git a/backend/static/js/pages/friends.js b/backend/static/js/pages/friends.js index ea4405c..9c68959 100644 --- a/backend/static/js/pages/friends.js +++ b/backend/static/js/pages/friends.js @@ -442,7 +442,7 @@ window.Page_friends = (() => { e.stopPropagation(); const id = parseInt(btn.dataset.frNoteId); const name = btn.dataset.frNoteName || ''; - UI.noteModal('friends', id, name, null); + _openNoteModal('friends', id, name, null); }); }); @@ -866,6 +866,83 @@ window.Page_friends = (() => { // ---------------------------------------------------------- // NOTIZ-MODAL // ---------------------------------------------------------- + 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'; + + overlay.innerHTML = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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, String(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, String(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, onDogChange, _accept, _decline, _cancel, _removeFriend, _openChat }; diff --git a/backend/static/js/pages/health.js b/backend/static/js/pages/health.js index 0cf9b93..78f2430 100644 --- a/backend/static/js/pages/health.js +++ b/backend/static/js/pages/health.js @@ -19,7 +19,6 @@ window.Page_health = (() => { { key: 'tierarzt', label: 'Besuche', icon: '' }, { key: 'gewicht', label: 'Gewicht', icon: '' }, { key: 'medikament', label: 'Medikamente', icon: '' }, - { key: 'pflege', label: 'Pflege', icon: '' }, { key: 'allergie', label: 'Allergien', icon: '' }, { key: 'dokument', label: 'Dokumente', icon: '' }, { key: 'praxen', label: 'Praxen', icon: '' }, @@ -28,14 +27,6 @@ window.Page_health = (() => { ]; const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '' }; - // Pflege-Routinen — wiederkehrende Pflege-Aufgaben, gebündelt im 'pflege'-Tab - const PFLEGE_TYPEN = ['parasit', 'krallen', 'fellpflege']; - const PFLEGE_META = { - parasit: { label: 'Zecken-/Flohschutz', icon: 'bug-beetle', placeholder: 'z.B. Frontline, Seresto-Halsband' }, - krallen: { label: 'Krallen schneiden', icon: 'scissors', placeholder: 'z.B. Krallen kürzen' }, - fellpflege: { label: 'Fellpflege', icon: 'wind', placeholder: 'z.B. Bürsten, Trimmen, Baden' }, - }; - function _getTabs() { const tabs = [...BASE_TABS]; if (_appState?.activeDog?.geschlecht === 'w') tabs.splice(2, 0, LAEUFIGKEIT_TAB); @@ -299,8 +290,6 @@ window.Page_health = (() => { _data = {}; _getTabs().forEach(t => { _data[t.key] = []; }); _data['laeufigkeit'] = _data['laeufigkeit'] || []; - // Pflege-Routinen: eigene Listen je Typ (Tab 'pflege' bündelt sie beim Rendern) - PFLEGE_TYPEN.forEach(t => { _data[t] = []; }); all.forEach(e => { if (_data[e.typ] !== undefined) _data[e.typ].push(e); }); @@ -344,7 +333,6 @@ window.Page_health = (() => { case 'gewicht': content.innerHTML = _renderGewicht(entries); break; case 'laeufigkeit': content.innerHTML = _renderLaeufigkeit(entries); break; case 'medikament': content.innerHTML = _renderMedikamente(entries); break; - case 'pflege': content.innerHTML = _renderPflege(); break; case 'allergie': content.innerHTML = _renderAllergien(entries); break; case 'dokument': content.innerHTML = _renderDokumente(entries); break; case 'praxen': content.innerHTML = _renderPraxen(); break; @@ -422,65 +410,6 @@ window.Page_health = (() => { return { color: 'green', label: 'Aktuell', icon: '🟢' }; } - // ---------------------------------------------------------- - // PFLEGE-ROUTINEN (Zecken-/Flohschutz, Krallen, Fellpflege) - // ---------------------------------------------------------- - function _intervallLabel(tage) { - if (!tage) return ''; - const m = { 30: 'monatlich', 60: 'alle 2 Monate', 90: 'vierteljährlich', 180: 'halbjährlich', 365: 'jährlich' }; - return m[tage] || `alle ${tage} Tage`; - } - - function _renderPflege() { - const addButtons = ` -
- ${PFLEGE_TYPEN.map(t => ` - `).join('')} -
`; - - const all = PFLEGE_TYPEN.flatMap(t => (_data[t] || []).map(e => ({ ...e, _typ: t }))); - - if (!all.length) return addButtons + _emptyState( - 'paw-print', - 'Noch keine Pflege-Routinen', - 'Lege wiederkehrende Routinen wie Zecken-/Flohschutz, Krallenschneiden oder Fellpflege an — wir erinnern dich rechtzeitig.' - ); - - // Fällige zuerst (nach naechstes), Einträge ohne Folgedatum ans Ende - all.sort((a, b) => { - if (!a.naechstes) return 1; - if (!b.naechstes) return -1; - return a.naechstes.localeCompare(b.naechstes); - }); - - const items = all.map(e => { - const meta = PFLEGE_META[e._typ]; - const ampel = e.naechstes ? _impfAmpel(e.naechstes) : null; - const interv = _intervallLabel(e.intervall_tage); - return ` -
- ${ampel ? `
` : ''} -
-
${UI.icon(meta.icon)} ${UI.escape(e.bezeichnung || meta.label)}
-
- ${meta.label}${e.datum ? ` · zuletzt ${UI.time.format(e.datum + 'T00:00:00')}` : ''}${interv ? ` · ${interv}` : ''} -
- ${e.naechstes ? `
- Nächste: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon} -
` : ''} -
- -
`; - }).join(''); - - return addButtons + `
${items}
`; - } - // ---------------------------------------------------------- // TIERARZTBESUCHE // ---------------------------------------------------------- @@ -954,51 +883,21 @@ window.Page_health = (() => { // ---------------------------------------------------------- // EVENTS BINDEN // ---------------------------------------------------------- - // Sucht einen Eintrag in der/den Liste(n) des aktiven Tabs. - // Im Pflege-Tab sind die Einträge auf mehrere Typ-Listen verteilt. - function _entriesForActiveTab() { - if (_activeTab === 'pflege') return PFLEGE_TYPEN.flatMap(t => _data[t] || []); - return _data[_activeTab] || []; - } - function _bindTabEvents(content) { content.querySelectorAll('[data-action="add-entry"]').forEach(btn => { btn.addEventListener('click', () => _showForm(null, _activeTab)); }); - // Pflege: pro-Typ-Button "+ Routine" → Formular mit festem Typ - content.querySelectorAll('[data-action="add-routine"]').forEach(btn => { - btn.addEventListener('click', () => _showForm(null, btn.dataset.typ)); - }); - // Pflege: Routine als erledigt markieren → Backend schreibt naechstes fort - content.querySelectorAll('[data-action="routine-erledigt"]').forEach(btn => { - btn.addEventListener('click', async e => { - e.stopPropagation(); - const id = parseInt(btn.dataset.id); - await UI.asyncButton(btn, async () => { - const saved = await API.health.complete(_appState.activeDog.id, id); - const list = _data[saved.typ]; - if (list) { - const idx = list.findIndex(x => x.id === id); - if (idx !== -1) list[idx] = saved; - } - _renderTab(); - _renderErinnerungen(); - UI.toast.success('Als erledigt eingetragen.'); - }); - }); - }); content.querySelectorAll('[data-action="open-entry"]').forEach(card => { const id = parseInt(card.dataset.id); - const entry = _entriesForActiveTab().find(e => e.id === id); - if (entry) card.addEventListener('click', () => - _activeTab === 'pflege' ? _showForm(entry, entry.typ) : _openDetail(entry)); + 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 || ''; - UI.noteModal('health', id, label, null); + _openNoteModal('health', id, label, null); }); }); // Praxis öffnen → Detail-Modal mit Bewertungen @@ -1081,19 +980,8 @@ window.Page_health = (() => { // ---------------------------------------------------------- // DETAIL-ANSICHT // ---------------------------------------------------------- - // Tab-Info (Icon + Label) für einen Typ — kennt auch die Pflege-Routine-Typen, - // die keinen eigenen Tab haben (sie liegen im gebündelten 'pflege'-Tab). - function _typInfo(typ) { - const meta = PFLEGE_META[typ]; - if (meta) return { - icon: ``, - label: meta.label, - }; - return _getTabs().find(t => t.key === typ) || BASE_TABS[0]; - } - function _openDetail(entry) { - const tabInfo = _typInfo(entry.typ); + const tabInfo = _getTabs().find(t => t.key === entry.typ) || BASE_TABS[0]; const fields = _detailFields(entry); // Media-Items zusammenstellen (neue + legacy) @@ -1263,7 +1151,7 @@ window.Page_health = (() => { `; - const tabInfo = _typInfo(t); + const tabInfo = _getTabs().find(tab => tab.key === t) || BASE_TABS[0]; UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body, footer }); const form = document.getElementById('health-form'); @@ -1406,9 +1294,6 @@ window.Page_health = (() => { allergie: 'z.B. Hühnchen, Gras, Hausstaub', dokument: 'z.B. Impfpass, Blutbild', laeufigkeit: 'Läufigkeit', - parasit: 'z.B. Frontline, Seresto-Halsband', - krallen: 'z.B. Krallen kürzen', - fellpflege: 'z.B. Bürsten, Trimmen, Baden', }; return ph[typ] || ''; } @@ -1478,17 +1363,6 @@ window.Page_health = (() => { ${_praxisSelectField(entry)} `; - case 'parasit': - case 'krallen': - case 'fellpflege': return ` -
-
- - -
- ${_intervallField(entry)} -
- `; case 'tierarzt': { const aktivePraxen = _praxen.filter(p => p.aktiv); const praxisField = aktivePraxen.length @@ -2975,6 +2849,85 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) { // ---------------------------------------------------------- // 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 = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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); + } + }); + } // ---------------------------------------------------------- // KI-TIERARZTFRAGEN diff --git a/backend/static/js/pages/lost.js b/backend/static/js/pages/lost.js index 50c0a98..ef4aab5 100644 --- a/backend/static/js/pages/lost.js +++ b/backend/static/js/pages/lost.js @@ -353,7 +353,7 @@ window.Page_lost = (() => { e.stopPropagation(); const id = parseInt(btn.dataset.lostNoteId); const name = btn.dataset.lostNoteName || ''; - UI.noteModal('lost', id, name, null); + _openNoteModal('lost', id, name, null); }); }); } @@ -804,6 +804,83 @@ function _emptyState(icon, title, text, cta = '') { // ---------------------------------------------------------- // NOTIZ-MODAL // ---------------------------------------------------------- + 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'; + + overlay.innerHTML = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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, String(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, String(parentId), payload); + } + UI.toast.success('Notiz gespeichert.'); + _close(); + } catch (err) { + UI.toast.error(err.message || 'Fehler beim Speichern.'); + UI.setLoading(saveBtn, false); + } + }); + } // ---------------------------------------------------------- // PUBLIC diff --git a/backend/static/js/pages/poison.js b/backend/static/js/pages/poison.js index 71fafec..f9d1151 100644 --- a/backend/static/js/pages/poison.js +++ b/backend/static/js/pages/poison.js @@ -257,7 +257,7 @@ window.Page_poison = (() => { btn.addEventListener('click', e => { e.stopPropagation(); const id = parseInt(btn.dataset.poisonNoteId); - UI.noteModal('poison', id, 'Giftköder-Meldung ' + id, null); + _openNoteModal('poison', id, 'Giftköder-Meldung ' + id, null); }); }); } @@ -650,6 +650,83 @@ window.Page_poison = (() => { // ---------------------------------------------------------- // NOTIZ-MODAL // ---------------------------------------------------------- + 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'; + + overlay.innerHTML = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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, String(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, String(parentId), payload); + } + UI.toast.success('Notiz gespeichert.'); + _close(); + } catch (err) { + UI.toast.error(err.message || 'Fehler beim Speichern.'); + UI.setLoading(saveBtn, false); + } + }); + } // ---------------------------------------------------------- // PUBLIC diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 0c2295e..acb48c6 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -2397,7 +2397,7 @@ window.Page_routes = (() => { // Notiz-Button document.getElementById('rd-note')?.addEventListener('click', () => { const label = route.name || (route.distanz_km ? route.distanz_km.toFixed(1) + ' km' : 'Route'); - UI.noteModal('route', route.id, label, null); + _openNoteModal('route', route.id, label, null); }); // Mini-Map @@ -3054,6 +3054,55 @@ 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 = ` +
+
+ + Notiz — ${UI.escape(parentLabel)} + +
+ +
+ + +
+
+ `; + 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, client_time: API.clientNow() }; + 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 }; diff --git a/backend/static/js/pages/sitting.js b/backend/static/js/pages/sitting.js index 095a8f8..fd59ec9 100644 --- a/backend/static/js/pages/sitting.js +++ b/backend/static/js/pages/sitting.js @@ -714,7 +714,7 @@ window.Page_sitting = (() => { const noteBtn = e.target.closest('.sit-note-btn'); if (noteBtn) { e.stopPropagation(); - UI.noteModal( + _openNoteModal( 'sitting', parseInt(noteBtn.dataset.sitNoteId), noteBtn.dataset.sitNoteLabel, @@ -763,6 +763,55 @@ window.Page_sitting = (() => { // ---------------------------------------------------------- // 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 = ` +
+
+ + Notiz — ${UI.escape(parentLabel)} + +
+ +
+ + +
+
+ `; + 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 }; diff --git a/backend/static/js/pages/trainingsplaene.js b/backend/static/js/pages/trainingsplaene.js index d7c62d2..7752ea7 100644 --- a/backend/static/js/pages/trainingsplaene.js +++ b/backend/static/js/pages/trainingsplaene.js @@ -538,7 +538,7 @@ function _icon(name) { const planLabel = _activePlan === 'welpe' ? 'Welpe 0–6 Monate' : _activePlan === 'junior' ? 'Junior 6–18 Monate' : `Erwachsener Hund – ${_activeAdultTab}`; - UI.noteModal('trainingsplan', dogId, planLabel, null); + _openNoteModal('trainingsplan', dogId, planLabel, null); }); // Plan selector @@ -768,6 +768,84 @@ function _icon(name) { // ---------------------------------------------------------- // NOTIZ-MODAL // ---------------------------------------------------------- + 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 = ` +
+
+
+
Notiz
+
${UI.escape(parentLabel)}
+
+ +
+
+
+ +
+
+
+ + +
+
+ `; + + 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, String(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, String(parentId), payload); + } + UI.toast.success('Notiz gespeichert.'); + _close(); + } catch (err) { + UI.toast.error(err.message || 'Fehler beim Speichern.'); + UI.setLoading(saveBtn, false); + } + }); + } // ---------------------------------------------------------- // PUBLIC API diff --git a/backend/static/js/pages/walks.js b/backend/static/js/pages/walks.js index 2d2072e..e56f569 100644 --- a/backend/static/js/pages/walks.js +++ b/backend/static/js/pages/walks.js @@ -311,7 +311,7 @@ window.Page_walks = (() => { el.querySelectorAll('.wk-note-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); - UI.noteModal( + _openNoteModal( 'walk', parseInt(btn.dataset.wkNoteId), btn.dataset.wkNoteLabel, @@ -1211,6 +1211,55 @@ 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 = ` +
+
+ + Notiz — ${UI.escape(parentLabel)} + +
+ +
+ + +
+
+ `; + 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.'); } + }); + } // ============================================================== // FEATURE 1: Foto-Challenge der Woche diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index 9f50342..2c5486a 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -1327,91 +1327,9 @@ const UI = (() => { }); } - // ---------------------------------------------------------- - // NOTE-MODAL — Notiz zu einem beliebigen Objekt (parentType/parentId) - // erstellen/bearbeiten. Zentral, damit nicht jede Seite eine eigene Kopie hat. - // ---------------------------------------------------------- - async function noteModal(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'; - - overlay.innerHTML = ` -
-
-
-
${_svgIcon('note-pencil')} Notiz
-
${escape(parentLabel)}
-
- -
-
-
- -
-
-
- - -
-
- `; - - 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(); - setLoading(saveBtn, true); - try { - const payload = { text, parent_label: parentLabel, location_name: locationName || null, client_time: API.clientNow() }; - if (existingNoteId) { - await API.notes.update(existingNoteId, payload); - } else { - await API.notes.create(parentType, parentId, payload); - } - toast.success('Notiz gespeichert.'); - _close(); - } catch (err) { - toast.error(err.message || 'Fehler beim Speichern.'); - setLoading(saveBtn, false); - } - }); - } - // Öffentliche API return { toast, modal, - noteModal, setLoading, asyncButton, formData, setFormError, clearFormErrors, emptyState, errorState, time, text, money, diff --git a/backend/static/landing.html b/backend/static/landing.html index 06ddcf5..a4d6f80 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index b11d249..ac211d5 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1133'; +const VER = '1131'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten