diff --git a/VERSION b/VERSION
index 2d0ce9f..9c6266b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1131
\ No newline at end of file
+1132
\ No newline at end of file
diff --git a/backend/routes/health.py b/backend/routes/health.py
index 7b9ed35..f16645e 100644
--- a/backend/routes/health.py
+++ b/backend/routes/health.py
@@ -15,7 +15,9 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"}
# Erlaubte Typen
-TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument", "laeufigkeit"}
+# Routine-/Pflege-Typen (wiederkehrend mit intervall_tage): parasit, krallen, fellpflege
+TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument",
+ "laeufigkeit", "parasit", "krallen", "fellpflege"}
# ------------------------------------------------------------------
@@ -164,15 +166,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, tierarzt_id,
+ schweregrad, reaktion, erinnerung, intervall_tage, 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.tierarzt_id,
- data.deckdatum, data.wurftermin)
+ data.schweregrad, data.reaktion, data.erinnerung, data.intervall_tage,
+ data.tierarzt_id, data.deckdatum, data.wurftermin)
)
row = conn.execute(
"SELECT * FROM health WHERE dog_id=? ORDER BY id DESC LIMIT 1",
@@ -212,6 +214,34 @@ 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}
# ------------------------------------------------------------------
@@ -500,6 +530,9 @@ _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/scheduler.py b/backend/scheduler.py
index 01c17ae..4f8ce5e 100644
--- a/backend/scheduler.py
+++ b/backend/scheduler.py
@@ -638,7 +638,8 @@ 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')
+ AND h.typ IN ('impfung', 'entwurmung', 'medikament',
+ 'parasit', 'krallen', 'fellpflege')
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 2cbbd06..45ab38a 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 af67c36..de39835 100644
--- a/backend/static/js/api.js
+++ b/backend/static/js/api.js
@@ -204,6 +204,7 @@ 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 bbc25c6..f957ae0 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 = '1131'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1132'; // ← 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/health.js b/backend/static/js/pages/health.js
index 78f2430..ff59b94 100644
--- a/backend/static/js/pages/health.js
+++ b/backend/static/js/pages/health.js
@@ -19,6 +19,7 @@ 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: '' },
@@ -27,6 +28,14 @@ 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);
@@ -290,6 +299,8 @@ 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);
});
@@ -333,6 +344,7 @@ 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;
@@ -410,6 +422,65 @@ 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
// ----------------------------------------------------------
@@ -883,14 +954,44 @@ 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 = (_data[_activeTab] || []).find(e => e.id === id);
- if (entry) card.addEventListener('click', () => _openDetail(entry));
+ const entry = _entriesForActiveTab().find(e => e.id === id);
+ if (entry) card.addEventListener('click', () =>
+ _activeTab === 'pflege' ? _showForm(entry, entry.typ) : _openDetail(entry));
});
content.querySelectorAll('[data-action="open-note"]').forEach(btn => {
btn.addEventListener('click', e => {
@@ -980,8 +1081,19 @@ 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 = _getTabs().find(t => t.key === entry.typ) || BASE_TABS[0];
+ const tabInfo = _typInfo(entry.typ);
const fields = _detailFields(entry);
// Media-Items zusammenstellen (neue + legacy)
@@ -1151,7 +1263,7 @@ window.Page_health = (() => {
`;
- const tabInfo = _getTabs().find(tab => tab.key === t) || BASE_TABS[0];
+ const tabInfo = _typInfo(t);
UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body, footer });
const form = document.getElementById('health-form');
@@ -1294,6 +1406,9 @@ 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] || '';
}
@@ -1363,6 +1478,17 @@ 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
diff --git a/backend/static/landing.html b/backend/static/landing.html
index a4d6f80..0bc6044 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 ac211d5..ded7238 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 = '1131';
+const VER = '1132';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten