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
|
|
@ -249,6 +249,23 @@ def init_db():
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- TIERÄRZTE (user-level, nie löschen — Historien-Erhalt bei Umzug)
|
||||||
|
CREATE TABLE IF NOT EXISTS tieraerzte (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
adresse TEXT,
|
||||||
|
telefon TEXT,
|
||||||
|
notfall_telefon TEXT,
|
||||||
|
email TEXT,
|
||||||
|
website TEXT,
|
||||||
|
notizen TEXT,
|
||||||
|
ist_notfallpraxis INTEGER NOT NULL DEFAULT 0,
|
||||||
|
aktiv INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tieraerzte_user ON tieraerzte(user_id, aktiv);
|
||||||
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Migrations: neue Spalten zu bestehenden Tabellen hinzufügen (idempotent)
|
# Migrations: neue Spalten zu bestehenden Tabellen hinzufügen (idempotent)
|
||||||
|
|
@ -277,6 +294,7 @@ def _migrate(conn_factory):
|
||||||
("health", "reaktion", "TEXT"),
|
("health", "reaktion", "TEXT"),
|
||||||
("health", "datei_url", "TEXT"),
|
("health", "datei_url", "TEXT"),
|
||||||
("health", "datei_typ", "TEXT"),
|
("health", "datei_typ", "TEXT"),
|
||||||
|
("health", "tierarzt_id", "INTEGER"),
|
||||||
]
|
]
|
||||||
with conn_factory() as conn:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|
|
||||||
|
|
@ -46,21 +46,23 @@ app = FastAPI(
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# API-Router registrieren (werden nach und nach hinzugefügt)
|
# API-Router registrieren (werden nach und nach hinzugefügt)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
from routes.auth import router as auth_router
|
from routes.auth import router as auth_router
|
||||||
from routes.dogs import router as dogs_router
|
from routes.dogs import router as dogs_router
|
||||||
from routes.diary import router as diary_router
|
from routes.diary import router as diary_router
|
||||||
from routes.health import router as health_router
|
from routes.health import router as health_router
|
||||||
from routes.poison import router as poison_router
|
from routes.poison import router as poison_router
|
||||||
from routes.push import router as push_router
|
from routes.push import router as push_router
|
||||||
from routes.ki import router as ki_router
|
from routes.ki import router as ki_router
|
||||||
|
from routes.tieraerzte import router as tieraerzte_router
|
||||||
|
|
||||||
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
|
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
|
||||||
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
|
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
|
||||||
app.include_router(diary_router, prefix="/api/dogs", tags=["Tagebuch"])
|
app.include_router(diary_router, prefix="/api/dogs", tags=["Tagebuch"])
|
||||||
app.include_router(health_router, prefix="/api/dogs", tags=["Gesundheit"])
|
app.include_router(health_router, prefix="/api/dogs", tags=["Gesundheit"])
|
||||||
app.include_router(poison_router, prefix="/api/poison", tags=["Giftköder"])
|
app.include_router(poison_router, prefix="/api/poison", tags=["Giftköder"])
|
||||||
app.include_router(push_router, prefix="/api/push", tags=["Push"])
|
app.include_router(push_router, prefix="/api/push", tags=["Push"])
|
||||||
app.include_router(ki_router, prefix="/api/ki", tags=["KI"])
|
app.include_router(ki_router, prefix="/api/ki", tags=["KI"])
|
||||||
|
app.include_router(tieraerzte_router, prefix="/api/tieraerzte", tags=["Tierärzte"])
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ class HealthCreate(BaseModel):
|
||||||
schweregrad: Optional[str] = None # leicht | mittel | schwer
|
schweregrad: Optional[str] = None # leicht | mittel | schwer
|
||||||
reaktion: Optional[str] = None
|
reaktion: Optional[str] = None
|
||||||
erinnerung: Optional[int] = 1
|
erinnerung: Optional[int] = 1
|
||||||
|
# Tierarzt-Verknüpfung
|
||||||
|
tierarzt_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class HealthUpdate(BaseModel):
|
class HealthUpdate(BaseModel):
|
||||||
|
|
@ -62,6 +64,7 @@ class HealthUpdate(BaseModel):
|
||||||
schweregrad: Optional[str] = None
|
schweregrad: Optional[str] = None
|
||||||
reaktion: Optional[str] = None
|
reaktion: Optional[str] = None
|
||||||
erinnerung: Optional[int] = None
|
erinnerung: Optional[int] = None
|
||||||
|
tierarzt_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -113,13 +116,13 @@ 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)
|
schweregrad, reaktion, erinnerung, tierarzt_id)
|
||||||
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.schweregrad, data.reaktion, data.erinnerung, data.tierarzt_id)
|
||||||
)
|
)
|
||||||
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",
|
||||||
|
|
|
||||||
91
backend/routes/tieraerzte.py
Normal file
91
backend/routes/tieraerzte.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""BAN YARO — Tierärzte Routes (user-level, nie löschen)"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from database import db
|
||||||
|
from auth import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class TierarztCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
adresse: Optional[str] = None
|
||||||
|
telefon: Optional[str] = None
|
||||||
|
notfall_telefon: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
website: Optional[str] = None
|
||||||
|
notizen: Optional[str] = None
|
||||||
|
ist_notfallpraxis: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TierarztUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
adresse: Optional[str] = None
|
||||||
|
telefon: Optional[str] = None
|
||||||
|
notfall_telefon: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
website: Optional[str] = None
|
||||||
|
notizen: Optional[str] = None
|
||||||
|
ist_notfallpraxis: Optional[bool] = None
|
||||||
|
aktiv: Optional[bool] = None # False = inaktiv (Umzug etc.)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_tieraerzte(user=Depends(get_current_user)):
|
||||||
|
"""Alle Tierärzte des Users — aktive zuerst, dann inaktive (für Historienansicht)."""
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM tieraerzte WHERE user_id=? ORDER BY aktiv DESC, name",
|
||||||
|
(user["id"],)
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", status_code=201)
|
||||||
|
async def create_tierarzt(data: TierarztCreate, user=Depends(get_current_user)):
|
||||||
|
with db() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO tieraerzte
|
||||||
|
(user_id, name, adresse, telefon, notfall_telefon,
|
||||||
|
email, website, notizen, ist_notfallpraxis)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(user["id"], data.name, data.adresse, data.telefon,
|
||||||
|
data.notfall_telefon, data.email, data.website,
|
||||||
|
data.notizen, int(data.ist_notfallpraxis))
|
||||||
|
)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM tieraerzte WHERE user_id=? ORDER BY id DESC LIMIT 1",
|
||||||
|
(user["id"],)
|
||||||
|
).fetchone()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{tierarzt_id}")
|
||||||
|
async def update_tierarzt(tierarzt_id: int, data: TierarztUpdate,
|
||||||
|
user=Depends(get_current_user)):
|
||||||
|
with db() as conn:
|
||||||
|
entry = conn.execute(
|
||||||
|
"SELECT id FROM tieraerzte WHERE id=? AND user_id=?",
|
||||||
|
(tierarzt_id, user["id"])
|
||||||
|
).fetchone()
|
||||||
|
if not entry:
|
||||||
|
raise HTTPException(404, "Tierarzt nicht gefunden.")
|
||||||
|
|
||||||
|
updates = {k: v for k, v in data.model_dump().items() if v is not None}
|
||||||
|
if "ist_notfallpraxis" in updates:
|
||||||
|
updates["ist_notfallpraxis"] = int(updates["ist_notfallpraxis"])
|
||||||
|
if "aktiv" in updates:
|
||||||
|
updates["aktiv"] = int(updates["aktiv"])
|
||||||
|
if not updates:
|
||||||
|
row = conn.execute("SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
set_clause = ", ".join(f"{k}=?" for k in updates)
|
||||||
|
conn.execute(
|
||||||
|
f"UPDATE tieraerzte SET {set_clause} WHERE id=?",
|
||||||
|
list(updates.values()) + [tierarzt_id]
|
||||||
|
)
|
||||||
|
row = conn.execute("SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone()
|
||||||
|
return dict(row)
|
||||||
|
|
@ -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
|
// GIFTKÖDER-ALARM
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -249,7 +258,7 @@ const API = (() => {
|
||||||
// Öffentliche API
|
// Öffentliche API
|
||||||
return {
|
return {
|
||||||
get, post, put, patch, del, upload,
|
get, post, put, patch, del, upload,
|
||||||
auth, dogs, diary, health, poison,
|
auth, dogs, diary, health, tieraerzte, poison,
|
||||||
places, routes, weather, push,
|
places, routes, weather, push,
|
||||||
subscribeToPush, getLocation,
|
subscribeToPush, getLocation,
|
||||||
APIError,
|
APIError,
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,17 @@ window.Page_health = (() => {
|
||||||
let _container = null;
|
let _container = null;
|
||||||
let _appState = null;
|
let _appState = null;
|
||||||
let _data = {}; // { impfung:[], tierarzt:[], gewicht:[], medikament:[], allergie:[], dokument:[] }
|
let _data = {}; // { impfung:[], tierarzt:[], gewicht:[], medikament:[], allergie:[], dokument:[] }
|
||||||
|
let _praxen = [];
|
||||||
let _activeTab = 'impfung';
|
let _activeTab = 'impfung';
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: 'impfung', label: 'Impfpass', icon: '💉' },
|
{ key: 'impfung', label: 'Impfpass', icon: '💉' },
|
||||||
{ key: 'tierarzt', label: 'Tierarzt', icon: '🏥' },
|
{ key: 'tierarzt', label: 'Tierarzt', icon: '🩺' },
|
||||||
{ key: 'gewicht', label: 'Gewicht', icon: '⚖️' },
|
{ key: 'gewicht', label: 'Gewicht', icon: '⚖️' },
|
||||||
{ key: 'medikament', label: 'Medikamente',icon: '💊' },
|
{ key: 'medikament', label: 'Medikamente',icon: '💊' },
|
||||||
{ key: 'allergie', label: 'Allergien', icon: '🌿' },
|
{ key: 'allergie', label: 'Allergien', icon: '🌿' },
|
||||||
{ key: 'dokument', label: 'Dokumente', icon: '📄' },
|
{ key: 'dokument', label: 'Dokumente', icon: '📄' },
|
||||||
|
{ key: 'praxen', label: 'Praxen', icon: '🏥' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -165,6 +167,11 @@ window.Page_health = (() => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
UI.toast.error('Gesundheitsdaten konnten nicht geladen werden.');
|
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 'medikament': content.innerHTML = _renderMedikamente(entries); 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
_bindTabEvents(content);
|
_bindTabEvents(content);
|
||||||
|
|
@ -438,6 +446,17 @@ window.Page_health = (() => {
|
||||||
const entry = (_data[_activeTab] || []).find(e => e.id === id);
|
const entry = (_data[_activeTab] || []).find(e => e.id === id);
|
||||||
if (entry) card.addEventListener('click', () => _openDetail(entry));
|
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 });
|
UI.modal.open({ title: `${tabInfo.icon} ${isEdit ? 'Bearbeiten' : tabInfo.label}`, body });
|
||||||
|
|
||||||
const form = document.getElementById('health-form');
|
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);
|
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 || '')}">
|
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
case 'tierarzt': return `
|
case 'tierarzt': {
|
||||||
<div class="form-group">
|
const aktivePraxen = _praxen.filter(p => p.aktiv);
|
||||||
<label class="form-label">Tierarzt / Praxis</label>
|
const praxisDropdown = aktivePraxen.length ? `
|
||||||
<input class="form-control" type="text" name="tierarzt_name" value="${_esc(entry?.tierarzt_name || '')}">
|
<div class="form-group">
|
||||||
</div>
|
<label class="form-label">Praxis auswählen</label>
|
||||||
<div class="form-group">
|
<select class="form-control" id="health-praxis-select" name="tierarzt_id">
|
||||||
<label class="form-label">Diagnose</label>
|
<option value="">– Praxis wählen –</option>
|
||||||
<textarea class="form-control" name="diagnose" rows="2">${_esc(entry?.diagnose || '')}</textarea>
|
${aktivePraxen.map(p => `
|
||||||
</div>
|
<option value="${p.id}" data-name="${_esc(p.name)}"
|
||||||
<div class="form-group">
|
${entry?.tierarzt_id === p.id ? 'selected' : ''}>
|
||||||
<label class="form-label">Kosten (€)</label>
|
${_esc(p.name)}
|
||||||
<input class="form-control" type="number" step="0.01" min="0" name="kosten"
|
</option>`).join('')}
|
||||||
value="${entry?.kosten ?? ''}">
|
</select>
|
||||||
</div>
|
</div>` : '';
|
||||||
<div class="form-group">
|
return `
|
||||||
<label class="form-label">Nächster Termin (optional)</label>
|
${praxisDropdown}
|
||||||
<input class="form-control" type="date" name="naechstes" value="${entry?.naechstes || ''}">
|
<div class="form-group">
|
||||||
</div>
|
<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 `
|
case 'gewicht': return `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Gewicht (kg) *</label>
|
<label class="form-label">Gewicht (kg) *</label>
|
||||||
|
|
@ -728,8 +777,9 @@ window.Page_health = (() => {
|
||||||
schweregrad: fd.schweregrad || null,
|
schweregrad: fd.schweregrad || null,
|
||||||
reaktion: fd.reaktion || null,
|
reaktion: fd.reaktion || null,
|
||||||
};
|
};
|
||||||
if (fd.wert) p.wert = parseFloat(fd.wert);
|
if (fd.wert) p.wert = parseFloat(fd.wert);
|
||||||
if (fd.kosten) p.kosten = parseFloat(fd.kosten);
|
if (fd.kosten) p.kosten = parseFloat(fd.kosten);
|
||||||
|
if (fd.tierarzt_id) p.tierarzt_id = parseInt(fd.tierarzt_id);
|
||||||
if (typ === 'medikament') {
|
if (typ === 'medikament') {
|
||||||
p.aktiv = 'aktiv' in fd ? 1 : 0;
|
p.aktiv = 'aktiv' in fd ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
@ -738,6 +788,162 @@ window.Page_health = (() => {
|
||||||
return p;
|
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
|
// KI-ZUSAMMENFASSUNG
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications
|
Offline-Cache + Push Notifications
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v6';
|
const CACHE_VERSION = 'by-v7';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
|
|
||||||
// Diese Dateien werden beim Install gecacht (App Shell)
|
// Diese Dateien werden beim Install gecacht (App Shell)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue