Notfall-Card: "Zuletzt aktualisiert am"-Zeitstempel #1

Merged
admin merged 1 commits from feature/notfallkarte-zeitstempel into master 2026-08-10 07:23:51 +02:00
11 changed files with 137 additions and 0 deletions
+24
View File
@@ -12,6 +12,18 @@ The `pflegepilot_v3` product was renamed from "Pflegepilot" to **"MeinPflegeFuch
- Historical references to "Pflegepilot" — e.g. the changelog's 1.0.0 entry, or anything below in this file describing what was true *at the time it was written* — are accurate records of the past and were deliberately left as-is rather than rewritten; only forward-looking/current-state documentation and all actual product code got renamed. When adding new documentation to this file, use "MeinPflegeFuchs", not "Pflegepilot".
- `einstellungen.firmenname` (sidebar branding / PDF-footer company name) is a separate, operator-configured field for the *legal entity* name (currently "Daniel Barac Onlinedienste" in production) — unrelated to the product brand name and intentionally not touched by this rename.
## Git-Workflow: Pull Requests über Gitea statt Direkt-Push auf `master` (ab 2026-08-10)
Ab sofort läuft alle Arbeit an diesem Repo über den Remote `origin` (`ssh://git@192.168.1.222:2222/admin/pflegefuchs.git`, self-hosted Gitea auf dem NAS, Web-UI unter `http://192.168.1.222:3666`) — Betreiber-Wunsch, um bei mehreren gleichzeitigen Änderungen die Übersicht zu behalten, statt jede Änderung nur in diesem CLAUDE.md-Dokument und einzelnen `git log`-Commits nachzuvollziehen.
- **Kein Direkt-Push auf `master` mehr durch Claude.** Jede abgeschlossene, in sich sinnvolle Änderung (ein Feature, ein Bugfix, ein Tester-Feedback-Punkt aus `Feedbackfromtester.md`) bekommt einen eigenen Branch, wird dorthin committet/gepusht und als Pull Request gegen `master` geöffnet — **ein PR pro Feature/Feedback-Punkt**, nicht ein Sammel-PR pro Konversation/Session.
- **Branch-Namensschema**: `feature/<kurzbeschreibung>` für neue Funktionen, `fix/<kurzbeschreibung>` für Bugfixes, jeweils kurze englische oder deutsche Kebab-Case-Beschreibung (z. B. `feature/notfallkarte-zeitstempel`, `fix/pdf-upload-passwortgeschuetzt`).
- **PR wird nicht automatisch gemerged** — der Betreiber merged jeden PR manuell nach eigener Durchsicht über die Gitea-Weboberfläche. Das ist die eigentliche Absicht dieses Workflows (Review-Gate + Übersicht), nicht nur eine Formalität — Claude darf einen von sich selbst geöffneten PR nicht selbst mergen, auch nicht auf Nachfrage "mach doch gleich fertig", ohne das explizit noch einmal zu bestätigen.
- **Werkzeug**: `tea` (offizielle Gitea-CLI, bereits unter `admin`-Login auf diesem Rechner eingerichtet, `tea login list` zeigt den Eintrag) — PRs werden per `tea pr create` geöffnet, nicht per Klick im Web-UI. `gh` (GitHub CLI) ist ebenfalls installiert, aber **nicht** für dieses Repo relevant, da hier kein GitHub im Spiel ist.
- **PR-Titel/-Beschreibung**: Titel entspricht dem, was sonst die Commit-Message-Subject-Zeile gewesen wäre (deutsch, gleicher Stil wie die bisherigen Commit-Messages/Changelog-Einträge); Beschreibung fasst die Änderung stichpunktartig zusammen, mit Verweis auf den zugehörigen Tester-Feedback-Punkt (T-Nummer) falls zutreffend und auf den neuen `changelog_seed.py`-Versionseintrag, falls einer dazugehört.
- **Gilt für alle Änderungen an diesem Repo**, nicht nur `pflegepilot_v3` — auch Änderungen an `pflegepilot_alpha_v2` oder an diesem `CLAUDE.md` selbst laufen künftig über einen PR, keine Ausnahme für "nur Dokumentation" oder "nur ein Einzeiler".
- Dieser Workflow-Abschnitt selbst wurde als erster PR unter dem neuen Schema eingeführt (zusammen mit dem bereits fertigen Notfallkarte-Zeitstempel-Feature, siehe unten) — als lebendes Beispiel, nicht nur als Ankündigung.
## Notfall-Card-Bereich im Bearbeiten-Formular hervorgehoben (pflegepilot_v3 only, 2026-08-08)
Betreiber-Feedback: die Notfall-Card-Felder (Blutgruppe, Allergien, Medikamente, Notfallkontakt) im "Patient bearbeiten"-Formular waren nur durch eine kleine graue Zwischenüberschrift von den übrigen Abschnitten (Persönliche Daten, Adresse, Krankenversicherung, Pflegesituation) abgesetzt — optisch identisch zu jedem anderen Abschnittstitel im selben durchlaufenden weißen Formular, dadurch leicht zu übersehen.
@@ -61,6 +73,18 @@ Betreiber-Meldung noch am selben Tag: unter "Dokumente → Datei hochladen" lie
- **Keine verwaisten Dateien gefunden**: ein Abgleich aller Dateien auf der Platte gegen die `Dokument`-Tabelle (NAS) ergab keine Datei ohne zugehörige DB-Zeile — der genaue vom Betreiber getestete Fall bleibt daher nicht zweifelsfrei rekonstruiert (das reproduzierte Passwort-Szenario ist der plausibelste Kandidat für dieselbe Fehlerklasse, aber nicht mit Sicherheit exakt derselbe Auslöser).
- Verifiziert per echtem HTTP-Upload des Passwort-PDFs gegen die lokale Docker-Instanz (201 statt 500, `ocr_text=null`) sowie eines normalen Text-PDFs als Regressionstest (weiterhin korrekt erkannter Text). Changelog-Eintrag `1.18.2`.
## Notfall-Card: "Zuletzt aktualisiert am"-Zeitstempel (pflegepilot_v3 only, 2026-08-08)
Betreiber-Feedback: weder intern (Patientenakte) noch extern (öffentliche Notfall-Card-Seite) war erkennbar, wann die angezeigten Notfalldaten zuletzt eingegeben/geändert wurden.
- **Neue Spalte `Patient.notfallkarte_daten_aktualisiert_am`** (Migration `e2f5c8a1b4d7`) — bewusst ein eigenes Feld statt `Patient.geaendert_am` wiederzuverwenden, das bei **jeder** Patientenänderung (Adresse, Telefonnummer, …) mitläuft und damit fälschlich häufiger "aktualisiert" wirken würde, als es für die Notfalldaten selbst zutrifft.
- **Zwei Stellen setzen den Zeitstempel**, beide nur bei einer echten inhaltlichen Änderung:
1. `PUT /api/patienten/{pid}` (`routers/patienten.py`): vergleicht bei jedem Speichern die 5 Notfall-Felder (`blutgruppe`, `allergien`, `notfallkarte_medikamente`, `notfallkontakt_name`, `notfallkontakt_telefon`) explizit gegen die bisherigen Werte (`NOTFALLKARTE_FELDER`-Set) — bewusst nicht "war das Feld im Request enthalten", da das Bearbeiten-Formular bei jedem Speichern alle Felder mitschickt, auch unveränderte. Nur bei mindestens einer echten Wertänderung wird der Zeitstempel neu gesetzt.
2. `POST /api/patienten/{pid}/patientenverfuegung` (`routers/briefe.py`): eine neu eingereichte Patientenverfügung zählt ebenfalls als "Notfalldaten aktualisiert" (sie wird auf der Notfall-Card mit angezeigt/verlinkt) — nur beim echten Neuanlage-Pfad, nicht wenn `_juengster_duplikat()` einen bereits bestehenden Antrag zurückgibt.
- **Anzeige an beiden Stellen**: intern im "Notfall-Card"-Collapsible der Patientenakte (nur wenn die Karte aktiv ist, direkt unter dem Titel) sowie extern auf der öffentlichen `/notfallkarte/[token]`-Seite (unter Vorname/Nachname/Geburtsdatum) — beide mit identischem `formatZeitpunkt()`-Format ("TT.MM.JJJJ, HH:MM Uhr"), bewusst dieselbe kleine Hilfsfunktion an beiden Stellen dupliziert statt in ein gemeinsames Modul ausgelagert (unterschiedliche Nachbarschaft: Server- vs. Client-Component).
- **Verifiziert** end-to-end gegen die lokale Docker-Instanz (Feldänderung setzt den Zeitstempel, ein reines Ändern eines NICHT-Notfall-Felds bei unveränderten Notfall-Werten setzt ihn nachweislich NICHT neu, öffentlicher Endpunkt liefert denselben Wert, neue Patientenverfügung bumpt ihn erneut) sowie live auf der NAS gegen den echten Test-Account (Allergien-Feld testweise geändert und zurückgesetzt, Zeitstempel beide Male korrekt aktualisiert, intern sichtbar bestätigt per Screenshot). Der öffentliche Seiten-Teil wurde bewusst **nicht** am echten, bereits aktiven Token des Testpatienten verifiziert, um dessen laufenden QR-Code/Link nicht durch eine nötige Neu-Generierung ungültig zu machen — Logik ist identisch zur bereits lokal bestätigten.
- Changelog-Eintrag `1.18.5`.
## Apple-/Google-Wallet-Passes für die Notfall-Card: bewusst weiterhin zurückgestellt (Stand 2026-08-08)
Anschlussfrage des Betreibers nach T37 ("ist die Notfallkarte schon wallet-ready?"), zusammen mit den technischen Rahmenbedingungen besprochen, aber (noch) nicht umgesetzt:
@@ -13,6 +13,14 @@ Message.
"""
CHANGELOG = [
{
"version": "1.18.5",
"datum": "2026-08-08",
"titel": "Notfall-Card zeigt jetzt an, wann die Daten zuletzt aktualisiert wurden",
"punkte": [
"Sowohl in der Patientenakte als auch auf der öffentlichen Notfall-Card-Seite ist jetzt ersichtlich, wann die Notfalldaten (oder eine neue Patientenverfügung) zuletzt geändert wurden.",
],
},
{
"version": "1.18.4",
"datum": "2026-08-08",
+8
View File
@@ -126,6 +126,14 @@ class Patient(Base):
notfallkarte_aktiviert_am: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True)
)
# Wann sich die auf der Notfall-Card angezeigten Daten zuletzt inhaltlich
# geaendert haben (die 5 Notfall-Felder oben, oder eine neu erstellte
# Patientenverfuegung) - bewusst getrennt von geaendert_am, das bei jeder
# beliebigen Patientenaenderung mitlaeuft. Siehe Migration
# e2f5c8a1b4d7 fuer die volle Begruendung.
notfallkarte_daten_aktualisiert_am: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True)
)
pflegeperson: Mapped[str | None] = mapped_column(String(200))
zielgruppe: Mapped[str] = mapped_column(String(20), nullable=False, default="alterspflege")
notizen: Mapped[str | None] = mapped_column(Text)
@@ -496,6 +496,12 @@ def patientenverfuegung_erstellen(
antrag = Antrag(patient_id=pid, typ="patientenverfuegung", status="offen", brief_text=text, pdf_pfad=pdf_pfad)
db.add(antrag)
# T37-Nachtrag: eine neu eingereichte Patientenverfuegung zaehlt fuer die
# Notfall-Card ebenfalls als "Notfalldaten aktualisiert" - sie wird dort
# zusammen mit den Notfall-Basisdaten angezeigt/verlinkt (siehe
# routers/notfallkarte.py). Nur der echte Neuanlage-Pfad hier, nicht der
# Duplikat-Rueckgabepfad oben.
patient.notfallkarte_daten_aktualisiert_am = datetime.datetime.now(datetime.timezone.utc)
db.commit()
db.refresh(antrag)
return antrag
@@ -108,6 +108,7 @@ def oeffentliche_ansicht(token: str, request: Request, db: Session = Depends(get
notfallkontakt_name=patient.notfallkontakt_name,
notfallkontakt_telefon=patient.notfallkontakt_telefon,
patientenverfuegung_vorhanden=bool(verfuegung and verfuegung.pdf_pfad),
notfallkarte_daten_aktualisiert_am=patient.notfallkarte_daten_aktualisiert_am,
)
@@ -21,6 +21,19 @@ router = APIRouter(
prefix="/api/patienten", tags=["patienten"], dependencies=[Depends(require_active_access)]
)
# T37-Nachtrag: genau diese 5 Felder speisen die Notfall-Card - nur wenn sich
# mindestens eines davon tatsaechlich aendert, wird notfallkarte_daten_aktualisiert_am
# neu gesetzt (siehe aktualisieren() unten). Bewusst nicht einfach "irgendein
# Feld war im Request enthalten", da das Bearbeiten-Formular bei jedem
# Speichern alle Felder mitschickt, auch unveraendert gebliebene.
NOTFALLKARTE_FELDER = {
"blutgruppe",
"allergien",
"notfallkarte_medikamente",
"notfallkontakt_name",
"notfallkontakt_telefon",
}
@router.get("", response_model=list[PatientOut])
def liste(
@@ -96,8 +109,14 @@ def aktualisieren(
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
notfall_geaendert = any(
feld in aenderungen and getattr(patient, feld) != aenderungen[feld]
for feld in NOTFALLKARTE_FELDER
)
for feld, wert in aenderungen.items():
setattr(patient, feld, wert)
if notfall_geaendert:
patient.notfallkarte_daten_aktualisiert_am = datetime.now(timezone.utc)
db.commit()
db.refresh(patient)
return patient
+5
View File
@@ -144,6 +144,7 @@ class PatientOut(PatientBase):
# Feld aus dem ORM-Objekt (siehe PatientOut-Validator unten).
notfallkarte_aktiv: bool
notfallkarte_aktiviert_am: datetime.datetime | None
notfallkarte_daten_aktualisiert_am: datetime.datetime | None
class PflegegradPatch(BaseModel):
@@ -849,6 +850,10 @@ class NotfallkarteOeffentlichOut(BaseModel):
notfallkontakt_name: str | None
notfallkontakt_telefon: str | None
patientenverfuegung_vorhanden: bool
# T37-Nachtrag: wann sich diese Daten (die Felder oben oder eine neu
# eingereichte Patientenverfuegung) zuletzt geaendert haben - fuer die
# "Zuletzt aktualisiert am:"-Anzeige auf der oeffentlichen Seite.
notfallkarte_daten_aktualisiert_am: datetime.datetime | None
class NotfallkartenZugriffOut(BaseModel):
@@ -0,0 +1,35 @@
"""notfallkarte: zeitstempel fuer letzte aenderung der notfall-relevanten daten
Revision ID: e2f5c8a1b4d7
Revises: d9f2b6e4a8c1
Create Date: 2026-08-08 00:00:00.000000
Betreiber-Feedback: auf der Notfall-Card (intern wie extern) soll erkennbar
sein, wann die angezeigten Daten zuletzt aktualisiert wurden - bewusst ein
eigenes Feld statt Patient.geaendert_am wiederzuverwenden, da dieses bei
JEDER Patientenaenderung (Adresse, Telefonnummer, ...) mitlaeuft und damit
faelschlich haeufiger "aktualisiert" wirken wuerde, als es fuer die
Notfalldaten selbst zutrifft. Wird gezielt nur bei tatsaechlicher Aenderung
der 5 Notfall-Felder sowie beim Anlegen einer neuen Patientenverfuegung
gesetzt (siehe routers/patienten.py und routers/briefe.py).
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e2f5c8a1b4d7'
down_revision = 'd9f2b6e4a8c1'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
'patienten',
sa.Column('notfallkarte_daten_aktualisiert_am', sa.DateTime(timezone=True), nullable=True),
)
def downgrade() -> None:
op.drop_column('patienten', 'notfallkarte_daten_aktualisiert_am')
@@ -958,6 +958,11 @@ function PatientDetailPageInner({ params }: { params: Promise<{ id: string }> })
</div>
) : (
<div className="flex flex-col gap-3">
{patient.notfallkarte_daten_aktualisiert_am && (
<p className="text-xs text-slate-500">
Zuletzt aktualisiert am: {formatZeitpunkt(patient.notfallkarte_daten_aktualisiert_am)}
</p>
)}
{notfallkarteLink && notfallkarteQr ? (
<div className="flex flex-col items-start gap-2">
{/* eslint-disable-next-line @next/next/no-img-element -- lokal generierte Data-URI, next/image ist dafuer nicht vorgesehen */}
@@ -1179,6 +1184,14 @@ function PatientDetailPageInner({ params }: { params: Promise<{ id: string }> })
);
}
function formatZeitpunkt(iso: string) {
const datum = new Date(iso);
return `${datum.toLocaleDateString("de-DE")}, ${datum.toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
})} Uhr`;
}
function Field({ label, value }: { label: string; value?: string | null }) {
return (
<div>
@@ -18,8 +18,17 @@ type NotfallkarteDaten = {
notfallkontakt_name: string | null;
notfallkontakt_telefon: string | null;
patientenverfuegung_vorhanden: boolean;
notfallkarte_daten_aktualisiert_am: string | null;
};
function formatZeitpunkt(iso: string) {
const datum = new Date(iso);
return `${datum.toLocaleDateString("de-DE")}, ${datum.toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
})} Uhr`;
}
// Gleiches Server-Fetch-Muster wie die Landingpage (siehe (marketing)/page.tsx:
// ladeOeffentlicheEinstellungen()) - API_INTERNAL_URL ist der interne Docker-
// Compose-Servicename, nicht der browserseitig erreichbare NEXT_PUBLIC_API_URL.
@@ -90,6 +99,11 @@ export default async function NotfallkartePage({
geboren am {new Date(daten.geburtsdatum).toLocaleDateString("de-DE")}
</p>
)}
{daten.notfallkarte_daten_aktualisiert_am && (
<p className="mt-2 text-xs text-slate-400">
Zuletzt aktualisiert am: {formatZeitpunkt(daten.notfallkarte_daten_aktualisiert_am)}
</p>
)}
</div>
{daten.blutgruppe && <Feld label="Blutgruppe" wert={daten.blutgruppe} />}
@@ -34,6 +34,10 @@ export type Patient = {
// Der Token-Hash selbst wird nie ausgeliefert, nur dieses abgeleitete bool.
notfallkarte_aktiv: boolean;
notfallkarte_aktiviert_am: string | null;
// Wann sich die Notfall-Basisdaten oder eine neu eingereichte
// Patientenverfuegung zuletzt geaendert haben - fuer "Zuletzt
// aktualisiert am:" in der Notfall-Card-Sektion.
notfallkarte_daten_aktualisiert_am: string | null;
pflegeperson: string | null;
zielgruppe: string;
notizen: string | null;