Feature: Pflege-Routinen (Zecken-/Flohschutz, Krallen, Fellpflege) — neuer Pflege-Tab mit Erledigt+Auto-Wiedervorlage, Push-Erinnerungen, intervall_tage-Fix im INSERT, SW v1132

This commit is contained in:
rene 2026-05-29 10:32:05 +02:00
parent 8c2bc0c445
commit a356626d39
9 changed files with 187 additions and 26 deletions

View file

@ -1 +1 @@
1131 1132

View file

@ -15,7 +15,9 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"} ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"}
# Erlaubte Typen # 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, (dog_id, typ, bezeichnung, datum, naechstes, notiz,
wert, einheit, charge_nr, tierarzt_name, kosten, diagnose, wert, einheit, charge_nr, tierarzt_name, kosten, diagnose,
dosierung, haeufigkeit, aktiv, bis_datum, dosierung, haeufigkeit, aktiv, bis_datum,
schweregrad, reaktion, erinnerung, tierarzt_id, schweregrad, reaktion, erinnerung, intervall_tage, tierarzt_id,
deckdatum, wurftermin) deckdatum, wurftermin)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(dog_id, data.typ, data.bezeichnung, data.datum, data.naechstes, (dog_id, data.typ, data.bezeichnung, data.datum, data.naechstes,
data.notiz, data.wert, data.einheit, data.charge_nr, data.notiz, data.wert, data.einheit, data.charge_nr,
data.tierarzt_name, data.kosten, data.diagnose, data.dosierung, data.tierarzt_name, data.kosten, data.diagnose, data.dosierung,
data.haeufigkeit, data.aktiv, data.bis_datum, data.haeufigkeit, data.aktiv, data.bis_datum,
data.schweregrad, data.reaktion, data.erinnerung, data.tierarzt_id, data.schweregrad, data.reaktion, data.erinnerung, data.intervall_tage,
data.deckdatum, data.wurftermin) data.tierarzt_id, data.deckdatum, data.wurftermin)
) )
row = conn.execute( row = conn.execute(
"SELECT * FROM health WHERE dog_id=? ORDER BY id DESC LIMIT 1", "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) 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} # DELETE /api/dogs/{dog_id}/health/{id}
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -500,6 +530,9 @@ _TERMIN_TYPEN = {
'tierarzt': {'label': 'Tierarztbesuch','beim_tierarzt': True, 'icon': 'first-aid'}, 'tierarzt': {'label': 'Tierarztbesuch','beim_tierarzt': True, 'icon': 'first-aid'},
'medikament': {'label': 'Medikament', 'beim_tierarzt': False, 'icon': 'pill'}, 'medikament': {'label': 'Medikament', 'beim_tierarzt': False, 'icon': 'pill'},
'laeufigkeit': {'label': 'Läufigkeit', 'beim_tierarzt': False, 'icon': 'calendar'}, '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") @router.get("/{dog_id}/health/terminvorschlaege")

View file

@ -638,7 +638,8 @@ async def _job_health_reminders():
FROM health h FROM health h
JOIN dogs d ON d.id = h.dog_id JOIN dogs d ON d.id = h.dog_id
WHERE h.naechstes IN (?, ?, ?, ?) 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) AND (h.erinnerung IS NULL OR h.erinnerung = 1)
""", (str(today), str(in7), str(in3), str(yesterday))).fetchall() """, (str(today), str(in7), str(in3), str(yesterday))).fetchall()

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title> <title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen --> <!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1131"></script> <script src="/js/boot-early.js?v=1132"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung --> <!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1131"> <link rel="stylesheet" href="/css/design-system.css?v=1132">
<link rel="stylesheet" href="/css/layout.css?v=1131"> <link rel="stylesheet" href="/css/layout.css?v=1132">
<link rel="stylesheet" href="/css/components.css?v=1131"> <link rel="stylesheet" href="/css/components.css?v=1132">
<link rel="stylesheet" href="/css/utilities.css?v=1131"> <link rel="stylesheet" href="/css/utilities.css?v=1132">
<link rel="stylesheet" href="/css/lists.css?v=1131"> <link rel="stylesheet" href="/css/lists.css?v=1132">
</head> </head>
<body> <body>
@ -617,11 +617,11 @@
<div id="modal-container"></div> <div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features --> <!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1131"></script> <script src="/js/api.js?v=1132"></script>
<script src="/js/ui.js?v=1131"></script> <script src="/js/ui.js?v=1132"></script>
<script src="/js/app.js?v=1131"></script> <script src="/js/app.js?v=1132"></script>
<script src="/js/worlds.js?v=1131"></script> <script src="/js/worlds.js?v=1132"></script>
<script src="/js/offline-indicator.js?v=1131"></script> <script src="/js/offline-indicator.js?v=1132"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) --> <!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1131"></script> <script src="/js/boot.js?v=1132"></script>
</body> </body>

View file

@ -204,6 +204,7 @@ const API = (() => {
}, },
create(dogId, data) { return post(`/dogs/${dogId}/health`, data); }, create(dogId, data) { return post(`/dogs/${dogId}/health`, data); },
update(dogId, id, d) { return patch(`/dogs/${dogId}/health/${id}`, d); }, 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}`); }, delete(dogId, id) { return del(`/dogs/${dogId}/health/${id}`); },
uploadDokument(dogId, id, formData) { uploadDokument(dogId, id, formData) {
return upload(`/dogs/${dogId}/health/${id}/dokument`, formData); return upload(`/dogs/${dogId}/health/${id}/dokument`, formData);

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. 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 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_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION; window.APP_VERSION = APP_VERSION;

View file

@ -19,6 +19,7 @@ window.Page_health = (() => {
{ key: 'tierarzt', label: 'Besuche', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' }, { key: 'tierarzt', label: 'Besuche', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
{ key: 'gewicht', label: 'Gewicht', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>' }, { key: 'gewicht', label: 'Gewicht', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>' },
{ key: 'medikament', label: 'Medikamente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' }, { key: 'medikament', label: 'Medikamente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
{ key: 'pflege', label: 'Pflege', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>' },
{ key: 'allergie', label: 'Allergien', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>' }, { key: 'allergie', label: 'Allergien', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>' },
{ key: 'dokument', label: 'Dokumente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg>' }, { key: 'dokument', label: 'Dokumente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg>' },
{ key: 'praxen', label: 'Praxen', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' }, { key: 'praxen', label: 'Praxen', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
@ -27,6 +28,14 @@ window.Page_health = (() => {
]; ];
const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>' }; const LAEUFIGKEIT_TAB = { key: 'laeufigkeit', label: 'Läufigkeit', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>' };
// 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() { function _getTabs() {
const tabs = [...BASE_TABS]; const tabs = [...BASE_TABS];
if (_appState?.activeDog?.geschlecht === 'w') tabs.splice(2, 0, LAEUFIGKEIT_TAB); if (_appState?.activeDog?.geschlecht === 'w') tabs.splice(2, 0, LAEUFIGKEIT_TAB);
@ -290,6 +299,8 @@ window.Page_health = (() => {
_data = {}; _data = {};
_getTabs().forEach(t => { _data[t.key] = []; }); _getTabs().forEach(t => { _data[t.key] = []; });
_data['laeufigkeit'] = _data['laeufigkeit'] || []; _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 => { all.forEach(e => {
if (_data[e.typ] !== undefined) _data[e.typ].push(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 'gewicht': content.innerHTML = _renderGewicht(entries); break;
case 'laeufigkeit': content.innerHTML = _renderLaeufigkeit(entries); break; case 'laeufigkeit': content.innerHTML = _renderLaeufigkeit(entries); break;
case 'medikament': content.innerHTML = _renderMedikamente(entries); break; case 'medikament': content.innerHTML = _renderMedikamente(entries); break;
case 'pflege': content.innerHTML = _renderPflege(); break;
case 'allergie': content.innerHTML = _renderAllergien(entries); break; case 'allergie': content.innerHTML = _renderAllergien(entries); break;
case 'dokument': content.innerHTML = _renderDokumente(entries); break; case 'dokument': content.innerHTML = _renderDokumente(entries); break;
case 'praxen': content.innerHTML = _renderPraxen(); break; case 'praxen': content.innerHTML = _renderPraxen(); break;
@ -410,6 +422,65 @@ window.Page_health = (() => {
return { color: 'green', label: 'Aktuell', icon: '🟢' }; 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 = `
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-3)">
${PFLEGE_TYPEN.map(t => `
<button class="btn btn-secondary btn-sm" data-action="add-routine" data-typ="${t}">
${UI.icon(PFLEGE_META[t].icon)} ${PFLEGE_META[t].label}
</button>`).join('')}
</div>`;
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 `
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
${ampel ? `<div class="health-card-ampel ampel-${ampel.color}" title="${ampel.label}"></div>` : ''}
<div class="list-item-body">
<div class="list-item-title">${UI.icon(meta.icon)} ${UI.escape(e.bezeichnung || meta.label)}</div>
<div class="list-item-meta-row">
${meta.label}${e.datum ? ` · zuletzt ${UI.time.format(e.datum + 'T00:00:00')}` : ''}${interv ? ` · ${interv}` : ''}
</div>
${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}">
Nächste: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
</div>` : ''}
</div>
<button class="btn btn-sm btn-primary" data-action="routine-erledigt" data-id="${e.id}"
style="flex-shrink:0;white-space:nowrap">
${UI.icon('check')} Erledigt
</button>
</div>`;
}).join('');
return addButtons + `<div class="health-list">${items}</div>`;
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// TIERARZTBESUCHE // TIERARZTBESUCHE
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -883,14 +954,44 @@ window.Page_health = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// EVENTS BINDEN // 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) { function _bindTabEvents(content) {
content.querySelectorAll('[data-action="add-entry"]').forEach(btn => { content.querySelectorAll('[data-action="add-entry"]').forEach(btn => {
btn.addEventListener('click', () => _showForm(null, _activeTab)); 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 => { content.querySelectorAll('[data-action="open-entry"]').forEach(card => {
const id = parseInt(card.dataset.id); const id = parseInt(card.dataset.id);
const entry = (_data[_activeTab] || []).find(e => e.id === id); const entry = _entriesForActiveTab().find(e => e.id === id);
if (entry) card.addEventListener('click', () => _openDetail(entry)); if (entry) card.addEventListener('click', () =>
_activeTab === 'pflege' ? _showForm(entry, entry.typ) : _openDetail(entry));
}); });
content.querySelectorAll('[data-action="open-note"]').forEach(btn => { content.querySelectorAll('[data-action="open-note"]').forEach(btn => {
btn.addEventListener('click', e => { btn.addEventListener('click', e => {
@ -980,8 +1081,19 @@ window.Page_health = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// DETAIL-ANSICHT // 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: `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${meta.icon}"></use></svg>`,
label: meta.label,
};
return _getTabs().find(t => t.key === typ) || BASE_TABS[0];
}
function _openDetail(entry) { function _openDetail(entry) {
const tabInfo = _getTabs().find(t => t.key === entry.typ) || BASE_TABS[0]; const tabInfo = _typInfo(entry.typ);
const fields = _detailFields(entry); const fields = _detailFields(entry);
// Media-Items zusammenstellen (neue + legacy) // Media-Items zusammenstellen (neue + legacy)
@ -1151,7 +1263,7 @@ window.Page_health = (() => {
</div> </div>
`; `;
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 }); UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body, footer });
const form = document.getElementById('health-form'); const form = document.getElementById('health-form');
@ -1294,6 +1406,9 @@ window.Page_health = (() => {
allergie: 'z.B. Hühnchen, Gras, Hausstaub', allergie: 'z.B. Hühnchen, Gras, Hausstaub',
dokument: 'z.B. Impfpass, Blutbild', dokument: 'z.B. Impfpass, Blutbild',
laeufigkeit: 'Läufigkeit', 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] || ''; return ph[typ] || '';
} }
@ -1363,6 +1478,17 @@ window.Page_health = (() => {
</div> </div>
${_praxisSelectField(entry)} ${_praxisSelectField(entry)}
`; `;
case 'parasit':
case 'krallen':
case 'fellpflege': return `
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Nächste Fälligkeit</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
</div>
${_intervallField(entry)}
</div>
`;
case 'tierarzt': { case 'tierarzt': {
const aktivePraxen = _praxen.filter(p => p.aktiv); const aktivePraxen = _praxen.filter(p => p.aktiv);
const praxisField = aktivePraxen.length const praxisField = aktivePraxen.length

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1131"></script> <script src="/js/landing-init.js?v=1132"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title> <title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store."> <meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz"> <meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */ ============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab // ← 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_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten