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:
parent
c06d9e24a7
commit
fc0f48c6d0
7 changed files with 371 additions and 42 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v6';
|
||||
const CACHE_VERSION = 'by-v7';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
|
||||
// Diese Dateien werden beim Install gecacht (App Shell)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue