Files
muh/frontend/src/pages/AnamnesisPage.tsx
Sven Carstensen d03dc94ad1 feat: Extend React/Java app to match Lua functionality
Backend:
- Add Pretreatment record for pre-treatment data
- Extend Sample with pretreatment, clinicalExamDate, internalNote
- Extend TherapyRecommendation with detail fields (count, duration, dosage, location)
- Add startvacVaccination and noAntibioticTreatment flags
- Add null-safety defaults for MongoDB compatibility

Frontend:
- Add pretreatment fields to SampleRegistrationPage
- Add special pathogens section to AnamnesisPage
- Add therapy detail pickers to TherapyPage
- Improve AntibiogramPage: full text labels, centered headers
- Fix AdminDashboardPage TypeScript error in chart tooltip

Styling:
- Enlarge matrix buttons for S/I/R text
- Add matrix-col class for centered table columns
2026-03-17 16:50:40 +01:00

287 lines
10 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { apiGet, apiPut } from "../lib/api";
import type { ActiveCatalogSummary, PathogenKind, QuarterKey, QuarterView, SampleDetail } from "../lib/types";
type QuarterFormState = {
pathogenBusinessKey: string;
customPathogenName: string;
cellCount: string;
pathogenKind: PathogenKind | null;
};
function quarterStateFromSample(sample: SampleDetail) {
return sample.quarters.reduce<Record<string, QuarterFormState>>((accumulator, quarter) => {
accumulator[quarter.quarterKey] = {
pathogenBusinessKey: quarter.pathogenBusinessKey ?? "",
customPathogenName: quarter.customPathogenName ?? "",
cellCount: quarter.cellCount ? String(quarter.cellCount) : "",
pathogenKind: quarter.pathogenKind,
};
return accumulator;
}, {});
}
// Special pathogen options like in Lua
const SPECIAL_PATHOGENS = [
{ key: "NO_GROWTH", label: "Kein bakterielles Wachstum", kind: "NO_GROWTH" as PathogenKind },
{ key: "CONTAMINATED", label: "Verunreinigte Probe", kind: "CONTAMINATED" as PathogenKind },
];
export default function AnamnesisPage() {
const { sampleId } = useParams();
const navigate = useNavigate();
const [catalogs, setCatalogs] = useState<ActiveCatalogSummary | null>(null);
const [sample, setSample] = useState<SampleDetail | null>(null);
const [quarterStates, setQuarterStates] = useState<Record<string, QuarterFormState>>({});
const [activeQuarter, setActiveQuarter] = useState<QuarterKey | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [showValidation, setShowValidation] = useState(false);
useEffect(() => {
async function load() {
if (!sampleId) {
return;
}
try {
const [catalogResponse, sampleResponse] = await Promise.all([
apiGet<ActiveCatalogSummary>("/catalogs/summary"),
apiGet<SampleDetail>(`/samples/${sampleId}`),
]);
setCatalogs(catalogResponse);
setSample(sampleResponse);
setQuarterStates(quarterStateFromSample(sampleResponse));
setActiveQuarter(sampleResponse.quarters[0]?.quarterKey ?? null);
} catch (loadError) {
setMessage((loadError as Error).message);
}
}
void load();
}, [sampleId]);
const visibleQuarter = useMemo<QuarterView | null>(() => {
if (!sample) {
return null;
}
return sample.quarters.find((quarter) => quarter.quarterKey === activeQuarter) ?? sample.quarters[0] ?? null;
}, [activeQuarter, sample]);
function updateQuarter(quarterKey: QuarterKey, patch: Partial<QuarterFormState>) {
setQuarterStates((current) => ({
...current,
[quarterKey]: {
...current[quarterKey],
...patch,
},
}));
}
function quarterHasPathogen(quarterKey: QuarterKey) {
const quarterState = quarterStates[quarterKey];
return Boolean(quarterState?.pathogenBusinessKey || quarterState?.customPathogenName?.trim());
}
function selectSpecialPathogen(quarterKey: QuarterKey, kind: PathogenKind) {
updateQuarter(quarterKey, {
pathogenBusinessKey: "",
customPathogenName: "",
pathogenKind: kind,
});
}
function isSpecialPathogenSelected(quarterKey: QuarterKey, kind: PathogenKind) {
return quarterStates[quarterKey]?.pathogenKind === kind &&
!quarterStates[quarterKey]?.pathogenBusinessKey &&
!quarterStates[quarterKey]?.customPathogenName;
}
async function handleSave() {
if (!sampleId || !sample) {
return;
}
setShowValidation(true);
const missingQuarter = sample.quarters.find((quarter) => !quarterHasPathogen(quarter.quarterKey));
if (missingQuarter) {
setActiveQuarter(missingQuarter.quarterKey);
setMessage("Bitte fuer jede Entnahmestelle einen Erreger auswaehlen oder eingeben.");
return;
}
setSaving(true);
setMessage(null);
try {
const response = await apiPut<SampleDetail>(`/samples/${sampleId}/anamnesis`, {
quarters: sample.quarters.map((quarter) => ({
quarterKey: quarter.quarterKey,
pathogenBusinessKey: quarterStates[quarter.quarterKey]?.pathogenBusinessKey || null,
customPathogenName: quarterStates[quarter.quarterKey]?.customPathogenName || null,
cellCount: quarterStates[quarter.quarterKey]?.cellCount
? Number(quarterStates[quarter.quarterKey]?.cellCount)
: null,
})),
});
navigate(`/samples/${response.id}/${response.routeSegment}`);
} catch (saveError) {
setMessage((saveError as Error).message);
} finally {
setSaving(false);
}
}
if (!sample || !catalogs || !visibleQuarter) {
return <div className="empty-state">Anamnese wird geladen ...</div>;
}
const state = quarterStates[visibleQuarter.quarterKey] ?? {
pathogenBusinessKey: "",
customPathogenName: "",
cellCount: "",
pathogenKind: null,
};
return (
<div className="page-stack">
<section className="section-card section-card--hero">
<div>
<p className="eyebrow">Anamnese</p>
<h3>Probe {sample.sampleNumber}</h3>
<p className="muted-text">
Erreger koennen ueber Schnellwahl oder Freitext erfasst werden. Bei 4/4-Proben wird
jedes relevante Viertel separat dokumentiert.
</p>
</div>
{sample.anamnesisEditable ? null : (
<div className="alert alert--warning">
Die Anamnese ist in diesem Bearbeitungsstand nur noch lesbar.
</div>
)}
{message ? <div className="alert alert--error">{message}</div> : null}
</section>
{sample.quarters.length > 1 ? (
<section className="section-card">
<div className="tab-row">
{sample.quarters.map((quarter) => (
<button
key={quarter.quarterKey}
type="button"
className={`tab-chip ${activeQuarter === quarter.quarterKey ? "is-active" : ""} ${
showValidation && !quarterHasPathogen(quarter.quarterKey) ? "is-invalid" : ""
}`}
onClick={() => setActiveQuarter(quarter.quarterKey)}
>
{quarter.label}
{quarter.flagged ? " Auffällig" : ""}
</button>
))}
</div>
</section>
) : null}
<section className="form-grid">
<article className="section-card">
<p className="eyebrow">Entnahmestelle</p>
<h3>{visibleQuarter.label}</h3>
{visibleQuarter.flagged ? (
<div className="info-chip">Auffaelliges Viertel markiert</div>
) : null}
{/* Special pathogen options like in Lua */}
<p className="required-label">Sonderfaelle</p>
<div className="pathogen-grid">
{SPECIAL_PATHOGENS.map((pathogen) => (
<button
key={pathogen.key}
type="button"
className={`pathogen-button ${
isSpecialPathogenSelected(visibleQuarter.quarterKey, pathogen.kind) ? "is-selected" : ""
}`}
onClick={() => selectSpecialPathogen(visibleQuarter.quarterKey, pathogen.kind)}
disabled={!sample.anamnesisEditable}
>
<strong>{pathogen.label}</strong>
</button>
))}
</div>
<p className="required-label section-card__spacer">Erreger (Katalog)</p>
<div className={`pathogen-grid ${showValidation && !quarterHasPathogen(visibleQuarter.quarterKey) ? "is-invalid" : ""}`}>
{catalogs.pathogens.map((pathogen) => (
<button
key={pathogen.businessKey}
type="button"
className={`pathogen-button ${
state.pathogenBusinessKey === pathogen.businessKey ? "is-selected" : ""
}`}
onClick={() =>
updateQuarter(visibleQuarter.quarterKey, {
pathogenBusinessKey: pathogen.businessKey,
customPathogenName: "",
pathogenKind: null,
})
}
disabled={!sample.anamnesisEditable}
>
<strong>{pathogen.name}</strong>
</button>
))}
</div>
<label className="field field--required field--spaced">
<span>Erreger manuell eingeben</span>
<input
className={showValidation && !quarterHasPathogen(visibleQuarter.quarterKey) ? "is-invalid" : ""}
value={state.customPathogenName}
onChange={(event) =>
updateQuarter(visibleQuarter.quarterKey, {
customPathogenName: event.target.value,
pathogenBusinessKey: "",
pathogenKind: null,
})
}
disabled={!sample.anamnesisEditable}
/>
</label>
</article>
<article className="section-card">
<p className="eyebrow">Begleitdaten</p>
<label className="field">
<span>Zellzahl {sample.sampleKind === "DRY_OFF" ? "(optional)" : ""}</span>
<input
value={state.cellCount}
onChange={(event) => updateQuarter(visibleQuarter.quarterKey, { cellCount: event.target.value })}
disabled={!sample.anamnesisEditable}
inputMode="numeric"
/>
</label>
<div className="info-panel info-panel--spaced">
<strong>Hinweis</strong>
<p>
Bei "Kein bakterielles Wachstum" oder "Verunreinigte Probe" wird das Antibiogramm
übersprungen und direkt zur Therapie weitergeleitet.
</p>
</div>
</article>
</section>
<div className="page-actions">
<button
type="button"
className="accent-button"
onClick={() => void handleSave()}
disabled={saving || !sample.anamnesisEditable}
>
{saving ? "Speichern ..." : "Speichern"}
</button>
</div>
</div>
);
}