Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration

- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push
- Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push
- Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge,
  forum, wiki, walks) vollständig auf Phosphor-Icons migriert
- Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos)
- TheDogAPI lokal gespiegelt (169 Rassen + Fotos)
- Quiz-Result-Cards horizontal (korrekte Bildproportionen)
- SW by-v89
This commit is contained in:
rene 2026-04-15 21:33:53 +02:00
parent 96bd57f0ad
commit 097295c628
44 changed files with 9980 additions and 300 deletions

View file

@ -13,13 +13,14 @@ window.Page_health = (() => {
let _activeTab = 'impfung';
const TABS = [
{ key: 'impfung', label: 'Impfpass', icon: '💉' },
{ key: 'tierarzt', label: 'Besuche', icon: '🩺' },
{ key: 'gewicht', label: 'Gewicht', icon: '⚖️' },
{ key: 'medikament', label: 'Medikamente',icon: '💊' },
{ key: 'allergie', label: 'Allergien', icon: '🌿' },
{ key: 'dokument', label: 'Dokumente', icon: '📄' },
{ key: 'praxen', label: 'Praxen', icon: '🏥' },
{ key: 'impfung', label: 'Impfpass', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></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: 'medikament', label: 'Medikamente', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></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: 'praxen', label: 'Praxen', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
{ key: 'symptomcheck', label: 'Symptom-Check', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>' },
];
// ----------------------------------------------------------
@ -56,7 +57,7 @@ window.Page_health = (() => {
async function _render() {
if (!_appState.activeDog) {
_container.innerHTML = UI.emptyState({
icon: '💉',
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>',
title: 'Noch kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.',
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Profil erstellen</button>`,
@ -81,7 +82,7 @@ window.Page_health = (() => {
const isActive = dog.id === activeDogId;
const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}">`
: `<span style="font-size:2.5rem">🐕</span>`;
: `<span>${UI.icon('dog')}</span>`;
return `
<div class="diary-picker-card${isActive ? ' diary-picker-card--active' : ''}"
data-dog-id="${dog.id}">
@ -119,7 +120,7 @@ window.Page_health = (() => {
_container.innerHTML = `
<div class="health-header">
<button class="btn btn-secondary btn-sm" id="health-ki-btn">
KI-Zusammenfassung
${UI.icon('star')} KI-Zusammenfassung
</button>
</div>
<div id="health-reminders"></div>
@ -165,13 +166,17 @@ window.Page_health = (() => {
if (!items.length) { el.innerHTML = ''; return; }
const ICONS = { impfung: '💉', entwurmung: '🪱', medikament: '💊' };
const ICONS = {
impfung: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>',
entwurmung: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>',
medikament: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>',
};
el.innerHTML = `
<div style="padding:var(--space-3) var(--space-4) 0">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2)">
📅 Anstehende Erinnerungen
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-dots"></use></svg> Anstehende Erinnerungen
</div>
${items.map(e => {
const ampel = _impfAmpel(e.naechstes);
@ -185,7 +190,7 @@ window.Page_health = (() => {
padding:var(--space-2) var(--space-3);margin-bottom:var(--space-1);
background:var(--c-surface);border-radius:var(--radius-md);
border-left:3px solid ${ampel.color === 'red' ? '#ef4444' : ampel.color === 'yellow' ? '#f59e0b' : '#22c55e'}">
<span style="font-size:1.2rem">${ICONS[e._typ] || '📋'}</span>
<span style="font-size:1.2rem">${ICONS[e._typ] || '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg>'}</span>
<div style="flex:1;min-width:0">
<div style="font-size:var(--text-sm);font-weight:var(--weight-medium);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
@ -199,7 +204,7 @@ window.Page_health = (() => {
data-action="reminder-erledigt"
data-entry-id="${e.id}" data-entry-typ="${e._typ}"
style="flex-shrink:0;white-space:nowrap">
Erledigt
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> Erledigt
</button>
</div>`;
}).join('')}
@ -302,13 +307,14 @@ window.Page_health = (() => {
const entries = _data[_activeTab] || [];
switch (_activeTab) {
case 'impfung': content.innerHTML = _renderImpfungen(entries); break;
case 'tierarzt': content.innerHTML = _renderTierarzt(entries); break;
case 'gewicht': content.innerHTML = _renderGewicht(entries); break;
case 'medikament': content.innerHTML = _renderMedikamente(entries); break;
case 'allergie': content.innerHTML = _renderAllergien(entries); break;
case 'dokument': content.innerHTML = _renderDokumente(entries); break;
case 'praxen': content.innerHTML = _renderPraxen(); break;
case 'impfung': content.innerHTML = _renderImpfungen(entries); break;
case 'tierarzt': content.innerHTML = _renderTierarzt(entries); break;
case 'gewicht': content.innerHTML = _renderGewicht(entries); break;
case 'medikament': content.innerHTML = _renderMedikamente(entries); break;
case 'allergie': content.innerHTML = _renderAllergien(entries); break;
case 'dokument': content.innerHTML = _renderDokumente(entries); break;
case 'praxen': content.innerHTML = _renderPraxen(); break;
case 'symptomcheck': _renderSymptomCheck(content); break;
}
_bindTabEvents(content);
@ -321,7 +327,7 @@ window.Page_health = (() => {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Impfung eintragen</button>`;
if (!entries.length) return UI.emptyState({
icon: '💉', title: 'Noch keine Impfungen', text: 'Trage alle Impfungen ein, um nichts zu verpassen.', action: addBtn
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>', title: 'Noch keine Impfungen', text: 'Trage alle Impfungen ein, um nichts zu verpassen.', action: addBtn
});
const items = entries.map(e => {
@ -337,7 +343,7 @@ window.Page_health = (() => {
${UI.time.format(e.datum + 'T00:00:00')}
${e.charge_nr ? ` · Ch.-Nr: ${_esc(e.charge_nr)}` : ''}
</div>
${vetName ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)">🏥 ${_esc(vetName)}</div>` : ''}
${vetName ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(vetName)}</div>` : ''}
${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}">
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
</div>` : ''}
@ -365,7 +371,7 @@ window.Page_health = (() => {
function _renderTierarzt(entries) {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Besuch eintragen</button>`;
if (!entries.length) return UI.emptyState({
icon: '🩺', title: 'Noch keine Tierarztbesuche', text: 'Halte alle Tierarztbesuche fest.', action: addBtn
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>', title: 'Noch keine Tierarztbesuche', text: 'Halte alle Tierarztbesuche fest.', action: addBtn
});
const items = entries.map(e => {
@ -383,7 +389,7 @@ window.Page_health = (() => {
${praxisName ? `
<div style="display:flex;align-items:center;gap:var(--space-1);
margin-top:var(--space-1);font-size:var(--text-sm);color:var(--c-text-secondary)">
🏥 ${_esc(praxisName)}${praxisOrt ? ` · ${_esc(praxisOrt)}` : ''}
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(praxisName)}${praxisOrt ? ` · ${_esc(praxisOrt)}` : ''}
</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>` : ''}
@ -403,7 +409,7 @@ window.Page_health = (() => {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Gewicht eintragen</button>`;
if (!entries.length) return UI.emptyState({
icon: '⚖️', title: 'Noch keine Gewichtseinträge', action: addBtn
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>', title: 'Noch keine Gewichtseinträge', action: addBtn
});
const sorted = [...entries].sort((a, b) => a.datum.localeCompare(b.datum));
@ -529,7 +535,7 @@ window.Page_health = (() => {
function _renderMedikamente(entries) {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Medikament eintragen</button>`;
if (!entries.length) return UI.emptyState({
icon: '💊', title: 'Noch keine Medikamente', action: addBtn
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>', title: 'Noch keine Medikamente', action: addBtn
});
const aktive = entries.filter(e => e.aktiv);
@ -554,7 +560,7 @@ window.Page_health = (() => {
return `
<div class="health-list">
${renderGroup(aktive, '💊 Aktuelle Medikamente')}
${renderGroup(aktive, '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> Aktuelle Medikamente')}
${renderGroup(inaktive, 'Vergangene Medikamente')}
</div>
<div style="text-align:center;padding:var(--space-4)">${addBtn}</div>
@ -567,7 +573,7 @@ window.Page_health = (() => {
function _renderAllergien(entries) {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Allergie eintragen</button>`;
if (!entries.length) return UI.emptyState({
icon: '🌿', title: 'Noch keine Allergien eingetragen', action: addBtn
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#leaf"></use></svg>', title: 'Noch keine Allergien eingetragen', action: addBtn
});
const SCHWEREGRAD = { leicht: '🟡', mittel: '🟠', schwer: '🔴' };
@ -598,7 +604,7 @@ window.Page_health = (() => {
function _renderDokumente(entries) {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-entry">+ Dokument hinzufügen</button>`;
if (!entries.length) return UI.emptyState({
icon: '📄', title: 'Noch keine Dokumente', text: 'Lade Impfpässe, Befunde und mehr hoch.', action: addBtn
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg>', title: 'Noch keine Dokumente', text: 'Lade Impfpässe, Befunde und mehr hoch.', action: addBtn
});
const items = entries.map(e => {
@ -610,7 +616,7 @@ window.Page_health = (() => {
? `<img src="${e.datei_url}" class="health-doc-thumb" alt="Vorschau"
style="width:64px;height:64px;object-fit:cover;border-radius:var(--radius-md);flex-shrink:0">`
: `<div style="width:48px;height:48px;display:flex;align-items:center;justify-content:center;
font-size:2rem;flex-shrink:0">${isPdf ? '📄' : '📎'}</div>`}
font-size:2rem;flex-shrink:0"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg></div>`}
<div class="health-card-body">
<div class="health-card-title">${_esc(e.bezeichnung)}</div>
<div class="health-card-meta">${UI.time.format(e.datum + 'T00:00:00')}</div>
@ -619,7 +625,7 @@ window.Page_health = (() => {
? `<a href="${e.datei_url}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm" style="margin-top:var(--space-2);display:inline-flex"
onclick="event.stopPropagation()">
${isPdf ? '📄 PDF öffnen' : '🖼️ Bild öffnen'}
${isPdf ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF öffnen' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg> Bild öffnen'}
</a>`
: `<span style="font-size:var(--text-xs);color:var(--c-text-muted)">Noch keine Datei hochgeladen</span>`}
</div>
@ -668,7 +674,7 @@ window.Page_health = (() => {
${fields}
${entry.datei_url
? (entry.datei_typ === 'pdf'
? `<a href="${entry.datei_url}" target="_blank" class="btn btn-secondary btn-sm" style="margin-top:var(--space-3)">📄 PDF öffnen</a>`
? `<a href="${entry.datei_url}" target="_blank" class="btn btn-secondary btn-sm" style="margin-top:var(--space-3)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF öffnen</a>`
: `<img src="${entry.datei_url}" style="width:100%;border-radius:var(--radius-md);margin-top:var(--space-3)" alt="Dokument">`)
: ''}
</div>
@ -717,7 +723,7 @@ window.Page_health = (() => {
if (praxis) {
const adresse = [praxis.strasse, [praxis.plz, praxis.ort].filter(Boolean).join(' ')].filter(Boolean).join(', ');
const tel = praxis.telefon ? ` · <a href="tel:${_esc(praxis.telefon)}">${_esc(praxis.telefon)}</a>` : '';
rows.push(['Praxis', `🏥 ${_esc(praxis.name)}${adresse ? `<br><small style="color:var(--c-text-secondary)">${_esc(adresse)}${tel}</small>` : tel}`]);
rows.push(['Praxis', `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(praxis.name)}${adresse ? `<br><small style="color:var(--c-text-secondary)">${_esc(adresse)}${tel}</small>` : tel}`]);
}
} else if (e.tierarzt_name) {
rows.push(['Tierarzt', _esc(e.tierarzt_name)]);
@ -940,7 +946,7 @@ window.Page_health = (() => {
: `<div class="form-group">
<div style="padding:var(--space-3);background:var(--c-bg);border-radius:var(--radius-md);
font-size:var(--text-sm);color:var(--c-text-secondary)">
🏥 Noch keine Praxis angelegt
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> Noch keine Praxis angelegt
<button type="button" class="btn btn-ghost btn-sm" style="padding:0;font-size:inherit"
data-action="goto-praxen">Praxis im Tab Praxen anlegen</button>
</div>
@ -1062,7 +1068,7 @@ window.Page_health = (() => {
const inaktive = _praxen.filter(p => !p.aktiv);
if (!_praxen.length) return UI.emptyState({
icon: '🏥', title: 'Noch keine Praxis eingetragen',
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>', title: 'Noch keine Praxis eingetragen',
text: 'Trage deine Tierarztpraxis ein für schnellen Zugriff.',
action: addBtn
});
@ -1070,7 +1076,7 @@ window.Page_health = (() => {
const renderCard = p => `
<div class="health-card praxis-card${!p.aktiv ? ' health-card--inactive' : ''}"
data-praxis-id="${p.id}" data-action="open-praxis">
<div style="font-size:1.5rem">${p.ist_notfallpraxis ? '🚨' : '🏥'}</div>
<div style="font-size:1.5rem">${p.ist_notfallpraxis ? '🚨' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>'}</div>
<div class="health-card-body">
<div class="health-card-title">
${_esc(p.name)}
@ -1225,6 +1231,111 @@ window.Page_health = (() => {
});
}
// ----------------------------------------------------------
// SYMPTOM-CHECK
// ----------------------------------------------------------
function _renderSymptomCheck(content) {
content.innerHTML = `
<div style="padding:var(--space-4)">
<div class="card" style="padding:var(--space-4)">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-4)">
Beschreibe die Symptome deines Hundes. Die KI gibt eine erste Einschätzung kein Ersatz für den Tierarzt.
</p>
<div class="form-group">
<label class="form-label" for="symptom-input">Symptome</label>
<textarea id="symptom-input" class="form-control" rows="4"
placeholder="z.B. frisst nicht, trinkt viel, schläft mehr als sonst..."></textarea>
</div>
<button id="symptom-submit-btn" class="btn btn-primary" style="width:100%">
Symptome analysieren <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
</button>
<div id="symptom-result" style="display:none;margin-top:var(--space-5)"></div>
</div>
</div>
`;
content.querySelector('#symptom-submit-btn').addEventListener('click', async function () {
const btn = this;
const textarea = content.querySelector('#symptom-input');
const resultEl = content.querySelector('#symptom-result');
const symptoms = textarea.value.trim();
if (!symptoms) {
UI.toast.warning('Bitte Symptome eingeben.');
return;
}
await UI.asyncButton(btn, async () => {
resultEl.style.display = 'none';
resultEl.innerHTML = '';
let result;
try {
result = await API.post(
`/dogs/${_appState.activeDog.id}/health/symptom-check`,
{ symptoms }
);
} catch (err) {
if (err.status === 402) {
resultEl.innerHTML = `
<div class="card" style="padding:var(--space-4);border:1px solid var(--c-warning,#f59e0b)">
<p style="margin:0;font-size:var(--text-sm)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg> Dieses Feature benötigt Ban Yaro Plus oder einen laufenden KI-Server.
</p>
</div>`;
} else if (err.status === 503) {
resultEl.innerHTML = `
<div class="card" style="padding:var(--space-4);border:1px solid var(--c-danger,#ef4444)">
<p style="margin:0;font-size:var(--text-sm)">
KI-Server nicht erreichbar. Bitte später versuchen.
</p>
</div>`;
} else {
UI.toast.error(err.message || 'Fehler bei der Symptomanalyse.');
return;
}
resultEl.style.display = '';
return;
}
const DRINGLICHKEIT = {
beobachten: { badgeClass: 'badge-success', label: '🟢 Beobachten' },
tierarzt: { badgeClass: 'badge-warning', label: '🟡 Zum Tierarzt' },
notfall: { badgeClass: 'badge-danger', label: '🔴 Notfall — sofort zum Tierarzt!' },
};
const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', label: _esc(result.dringlichkeit) };
const hinweiseHtml = (result.hinweise || []).length
? `<ul style="margin:var(--space-2) 0 0;padding-left:var(--space-5);font-size:var(--text-sm)">
${result.hinweise.map(h => `<li style="margin-bottom:var(--space-1)">${_esc(h)}</li>`).join('')}
</ul>`
: '';
const zumTierarztHtml = result.zum_tierarzt_wenn
? `<div style="margin-top:var(--space-3);padding:var(--space-3);
background:var(--c-surface-alt,var(--c-surface));
border-radius:var(--radius-md);font-size:var(--text-sm)">
<strong>Zum Tierarzt wenn:</strong> ${_esc(result.zum_tierarzt_wenn)}
</div>`
: '';
resultEl.innerHTML = `
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
<span class="badge ${d.badgeClass}" style="font-size:var(--text-sm);padding:var(--space-1) var(--space-3)">
${d.label}
</span>
</div>
${result.einschaetzung
? `<p style="font-size:var(--text-sm);line-height:1.6;margin:0">${_esc(result.einschaetzung)}</p>`
: ''}
${hinweiseHtml}
${zumTierarztHtml}
`;
resultEl.style.display = '';
});
});
}
// ----------------------------------------------------------
// KI-ZUSAMMENFASSUNG
// ----------------------------------------------------------
@ -1235,7 +1346,7 @@ window.Page_health = (() => {
try {
const { zusammenfassung } = await API.health.kiZusammenfassung(_appState.activeDog.id);
UI.modal.open({
title: '✨ KI-Gesundheitsbericht',
title: `${UI.icon('star')} KI-Gesundheitsbericht`,
body: `<div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${_esc(zusammenfassung)}</div>`,
});
} catch (err) {