Feat: Tierärzte-Verwaltung (Sprint 4)

Neue Praxen-Tab in Gesundheit: Tierarzt-Stammdaten (Name, Adresse,
Telefon, Notfall-Nr, E-Mail, Website, Notizen), Anruf- und
Notfall-Schnellzugriff via tel:-Links, Soft-Delete (aktiv=0) für
Praxiswechsel ohne Datenverlust. Tierarzt-Dropdown beim Eintragen
von Tierarzt-Besuchen. SW-Cache → by-v7.
This commit is contained in:
rene 2026-04-13 20:06:59 +02:00
parent c06d9e24a7
commit fc0f48c6d0
7 changed files with 371 additions and 42 deletions

View file

@ -134,6 +134,15 @@ const API = (() => {
},
};
// ----------------------------------------------------------
// TIERÄRZTE
// ----------------------------------------------------------
const tieraerzte = {
list() { return get('/tieraerzte'); },
create(data) { return post('/tieraerzte', data); },
update(id, d) { return patch(`/tieraerzte/${id}`, d); },
};
// ----------------------------------------------------------
// GIFTKÖDER-ALARM
// ----------------------------------------------------------
@ -249,7 +258,7 @@ const API = (() => {
// Öffentliche API
return {
get, post, put, patch, del, upload,
auth, dogs, diary, health, poison,
auth, dogs, diary, health, tieraerzte, poison,
places, routes, weather, push,
subscribeToPush, getLocation,
APIError,

View file

@ -9,15 +9,17 @@ window.Page_health = (() => {
let _container = null;
let _appState = null;
let _data = {}; // { impfung:[], tierarzt:[], gewicht:[], medikament:[], allergie:[], dokument:[] }
let _praxen = [];
let _activeTab = 'impfung';
const TABS = [
{ key: 'impfung', label: 'Impfpass', icon: '💉' },
{ key: 'tierarzt', label: 'Tierarzt', icon: '🏥' },
{ key: 'tierarzt', label: 'Tierarzt', 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: '🏥' },
];
// ----------------------------------------------------------
@ -165,6 +167,11 @@ window.Page_health = (() => {
} catch (err) {
UI.toast.error('Gesundheitsdaten konnten nicht geladen werden.');
}
try {
_praxen = await API.tieraerzte.list();
} catch (err) {
// silent fail
}
}
// ----------------------------------------------------------
@ -183,6 +190,7 @@ window.Page_health = (() => {
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;
}
_bindTabEvents(content);
@ -438,6 +446,17 @@ window.Page_health = (() => {
const entry = (_data[_activeTab] || []).find(e => e.id === id);
if (entry) card.addEventListener('click', () => _openDetail(entry));
});
// Praxis öffnen
content.querySelectorAll('[data-action="open-praxis"]').forEach(el => {
el.addEventListener('click', () => {
const id = parseInt(el.dataset.praxisId);
const p = _praxen.find(x => x.id === id);
if (p) _showPraxForm(p);
});
});
// Praxis hinzufügen
content.querySelector('[data-action="add-praxis"]')
?.addEventListener('click', () => _showPraxForm(null));
}
// ----------------------------------------------------------
@ -564,7 +583,20 @@ window.Page_health = (() => {
UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body });
const form = document.getElementById('health-form');
setTimeout(() => form?.querySelector('[name="bezeichnung"]')?.focus(), 150);
setTimeout(() => {
form?.querySelector('[name="bezeichnung"]')?.focus();
// Praxis-Dropdown: Name auto-befüllen
const praxisSelect = document.getElementById('health-praxis-select');
const nameInput = document.getElementById('health-tierarzt-name-input');
if (praxisSelect && nameInput) {
praxisSelect.addEventListener('change', () => {
const selected = praxisSelect.options[praxisSelect.selectedIndex];
if (selected.value) {
nameInput.value = selected.dataset.name || selected.textContent.trim();
}
});
}
}, 150);
document.getElementById('health-form-cancel')?.addEventListener('click', UI.modal.close);
@ -646,25 +678,42 @@ window.Page_health = (() => {
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
</div>
`;
case 'tierarzt': return `
<div class="form-group">
<label class="form-label">Tierarzt / Praxis</label>
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
</div>
<div class="form-group">
<label class="form-label">Diagnose</label>
<textarea class="form-control" name="diagnose" rows="2">${_esc(entry?.diagnose || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Kosten ()</label>
<input class="form-control" type="number" step="0.01" min="0" name="kosten"
value="${entry?.kosten ?? ''}">
</div>
<div class="form-group">
<label class="form-label">Nächster Termin (optional)</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
</div>
`;
case 'tierarzt': {
const aktivePraxen = _praxen.filter(p => p.aktiv);
const praxisDropdown = aktivePraxen.length ? `
<div class="form-group">
<label class="form-label">Praxis auswählen</label>
<select class="form-control" id="health-praxis-select" name="tierarzt_id">
<option value=""> Praxis wählen </option>
${aktivePraxen.map(p => `
<option value="${p.id}" data-name="${_esc(p.name)}"
${entry?.tierarzt_id === p.id ? 'selected' : ''}>
${_esc(p.name)}
</option>`).join('')}
</select>
</div>` : '';
return `
${praxisDropdown}
<div class="form-group">
<label class="form-label">Tierarzt / Praxis (Freitext)</label>
<input class="form-control" type="text" id="health-tierarzt-name-input"
name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
</div>
<div class="form-group">
<label class="form-label">Diagnose</label>
<textarea class="form-control" name="diagnose" rows="2">${_esc(entry?.diagnose || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label">Kosten ()</label>
<input class="form-control" type="number" step="0.01" min="0" name="kosten"
value="${entry?.kosten ?? ''}">
</div>
<div class="form-group">
<label class="form-label">Nächster Termin (optional)</label>
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
</div>
`;
}
case 'gewicht': return `
<div class="form-group">
<label class="form-label">Gewicht (kg) *</label>
@ -728,8 +777,9 @@ window.Page_health = (() => {
schweregrad: fd.schweregrad || null,
reaktion: fd.reaktion || null,
};
if (fd.wert) p.wert = parseFloat(fd.wert);
if (fd.kosten) p.kosten = parseFloat(fd.kosten);
if (fd.wert) p.wert = parseFloat(fd.wert);
if (fd.kosten) p.kosten = parseFloat(fd.kosten);
if (fd.tierarzt_id) p.tierarzt_id = parseInt(fd.tierarzt_id);
if (typ === 'medikament') {
p.aktiv = 'aktiv' in fd ? 1 : 0;
}
@ -738,6 +788,162 @@ window.Page_health = (() => {
return p;
}
// ----------------------------------------------------------
// PRAXEN — Liste
// ----------------------------------------------------------
function _renderPraxen() {
const addBtn = `<button class="btn btn-primary btn-sm" data-action="add-praxis">+ Praxis hinzufügen</button>`;
const aktive = _praxen.filter(p => p.aktiv);
const inaktive = _praxen.filter(p => !p.aktiv);
if (!_praxen.length) return UI.emptyState({
icon: '🏥', title: 'Noch keine Praxis eingetragen',
text: 'Trage deine Tierarztpraxis ein für schnellen Zugriff.',
action: addBtn
});
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 class="health-card-body">
<div class="health-card-title">
${_esc(p.name)}
${!p.aktiv ? '<span style="font-size:var(--text-xs);color:var(--c-text-secondary);font-weight:400"> · Ehemalig</span>' : ''}
</div>
${p.adresse ? `<div class="health-card-meta">${_esc(p.adresse)}</div>` : ''}
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
${p.telefon ? `
<a href="tel:${_esc(p.telefon)}" class="btn btn-secondary btn-sm"
onclick="event.stopPropagation()">
📞 Anrufen
</a>` : ''}
${p.notfall_telefon ? `
<a href="tel:${_esc(p.notfall_telefon)}" class="btn btn-danger btn-sm"
onclick="event.stopPropagation()">
🚨 Notfall
</a>` : ''}
</div>
</div>
</div>
`;
return `
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
${addBtn}
</div>
<div class="health-list">
${aktive.map(renderCard).join('')}
${inaktive.length ? `
<div style="margin-top:var(--space-4);padding-top:var(--space-3);
border-top:1px solid var(--c-border)">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);
margin:0 0 var(--space-3)">Ehemalige Praxen</p>
${inaktive.map(renderCard).join('')}
</div>` : ''}
</div>
`;
}
// ----------------------------------------------------------
// PRAXEN — Formular (Neu / Bearbeiten)
// ----------------------------------------------------------
function _showPraxForm(praxis) {
const isEdit = !!praxis;
UI.modal.open({
title: isEdit ? `${praxis.name} bearbeiten` : 'Praxis hinzufügen',
body: `
<form id="praxis-form" autocomplete="off">
<div class="form-group">
<label class="form-label">Name der Praxis *</label>
<input class="form-control" type="text" name="name"
value="${_esc(praxis?.name || '')}" placeholder="Dr. Muster Tierarztpraxis" required>
</div>
<div class="form-group">
<label class="form-label">Adresse</label>
<input class="form-control" type="text" name="adresse"
value="${_esc(praxis?.adresse || '')}" placeholder="Musterstraße 1, 12345 Stadt">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Telefon</label>
<input class="form-control" type="tel" name="telefon"
value="${_esc(praxis?.telefon || '')}" placeholder="089 123456">
</div>
<div class="form-group">
<label class="form-label">Notfall-Telefon</label>
<input class="form-control" type="tel" name="notfall_telefon"
value="${_esc(praxis?.notfall_telefon || '')}" placeholder="089 999999">
</div>
</div>
<div class="form-group">
<label class="form-label">E-Mail</label>
<input class="form-control" type="email" name="email"
value="${_esc(praxis?.email || '')}" placeholder="praxis@beispiel.de">
</div>
<div class="form-group">
<label class="form-label">Notizen</label>
<textarea class="form-control" name="notizen" rows="2"
placeholder="Öffnungszeiten, Besonderheiten…">${_esc(praxis?.notizen || '')}</textarea>
</div>
<div class="form-group">
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="ist_notfallpraxis" ${praxis?.ist_notfallpraxis ? 'checked' : ''}>
Notfallpraxis (24h / Wochenende)
</label>
</div>
${isEdit ? `
<div class="form-group">
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="inaktiv" ${!praxis?.aktiv ? 'checked' : ''}>
Als ehemalige Praxis markieren (bei Umzug / Arztwechsel)
</label>
</div>` : ''}
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-4)">
<button type="button" class="btn btn-secondary flex-1" id="praxis-cancel">Abbrechen</button>
<button type="submit" class="btn btn-primary flex-1">${isEdit ? 'Speichern' : 'Hinzufügen'}</button>
</div>
</form>
`,
});
document.getElementById('praxis-cancel')?.addEventListener('click', UI.modal.close);
document.getElementById('praxis-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = e.target.querySelector('[type="submit"]');
const fd = UI.formData(e.target);
await UI.asyncButton(btn, async () => {
const payload = {
name: fd.name?.trim(),
adresse: fd.adresse || null,
telefon: fd.telefon || null,
notfall_telefon: fd.notfall_telefon || null,
email: fd.email || null,
notizen: fd.notizen || null,
ist_notfallpraxis: 'ist_notfallpraxis' in fd,
};
if (!payload.name) { UI.toast.warning('Bitte einen Namen eingeben.'); return; }
let saved;
if (isEdit) {
payload.aktiv = !('inaktiv' in fd);
saved = await API.tieraerzte.update(praxis.id, payload);
_praxen = _praxen.map(p => p.id === praxis.id ? saved : p);
UI.toast.success('Praxis gespeichert.');
} else {
saved = await API.tieraerzte.create(payload);
_praxen.push(saved);
UI.toast.success(`${saved.name} hinzugefügt.`);
}
UI.modal.close();
_renderTab();
});
});
}
// ----------------------------------------------------------
// KI-ZUSAMMENFASSUNG
// ----------------------------------------------------------