Files
muh/frontend/src/pages/AntibiogramPage.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

220 lines
7.7 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { apiGet, apiPut } from "../lib/api";
import type {
ActiveCatalogSummary,
QuarterKey,
SampleDetail,
SensitivityResult,
} from "../lib/types";
type GroupState = Record<string, SensitivityResult | undefined>;
function quarterIdentity(sample: SampleDetail, quarterKey: QuarterKey) {
const quarter = sample.quarters.find((entry) => entry.quarterKey === quarterKey);
if (!quarter) {
return quarterKey;
}
return quarter.pathogenBusinessKey || quarter.customPathogenName || quarter.pathogenName || quarterKey;
}
export default function AntibiogramPage() {
const { sampleId } = useParams();
const navigate = useNavigate();
const [catalogs, setCatalogs] = useState<ActiveCatalogSummary | null>(null);
const [sample, setSample] = useState<SampleDetail | null>(null);
const [groupState, setGroupState] = useState<Record<string, GroupState>>({});
const [message, setMessage] = useState<string | null>(null);
const [saving, setSaving] = 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);
const nextState: Record<string, GroupState> = {};
sampleResponse.antibiogramTargets.forEach((referenceQuarter) => {
const existingGroup =
sampleResponse.antibiograms.find((entry) => entry.quarterKey === referenceQuarter) ??
sampleResponse.antibiograms.find((entry) => entry.inheritedFromQuarter === referenceQuarter);
nextState[referenceQuarter] = {};
existingGroup?.entries.forEach((entry) => {
nextState[referenceQuarter][entry.antibioticBusinessKey] = entry.result;
});
});
setGroupState(nextState);
} catch (loadError) {
setMessage((loadError as Error).message);
}
}
void load();
}, [sampleId]);
const groups = useMemo(() => {
if (!sample) {
return [];
}
return sample.antibiogramTargets.map((referenceQuarter) => {
const identity = quarterIdentity(sample, referenceQuarter);
const reference = sample.quarters.find((quarter) => quarter.quarterKey === referenceQuarter);
const inherited = sample.quarters.filter(
(quarter) =>
quarter.quarterKey !== referenceQuarter &&
quarterIdentity(sample, quarter.quarterKey) === identity &&
quarter.requiresAntibiogram,
);
return { referenceQuarter, reference, inherited };
});
}, [sample]);
function updateResult(
referenceQuarter: QuarterKey,
antibioticBusinessKey: string,
result: SensitivityResult,
) {
setGroupState((current) => ({
...current,
[referenceQuarter]: {
...current[referenceQuarter],
[antibioticBusinessKey]:
current[referenceQuarter]?.[antibioticBusinessKey] === result ? undefined : result,
},
}));
}
async function handleSave() {
if (!sampleId || !sample) {
return;
}
setSaving(true);
setMessage(null);
try {
const response = await apiPut<SampleDetail>(`/samples/${sampleId}/antibiogram`, {
groups: groups.map((group) => ({
referenceQuarter: group.referenceQuarter,
entries: Object.entries(groupState[group.referenceQuarter] ?? {})
.filter((entry): entry is [string, SensitivityResult] => Boolean(entry[1]))
.map(([antibioticBusinessKey, result]) => ({
antibioticBusinessKey,
result,
})),
})),
});
navigate(`/samples/${response.id}/${response.routeSegment}`);
} catch (saveError) {
setMessage((saveError as Error).message);
} finally {
setSaving(false);
}
}
if (!sample || !catalogs) {
return <div className="empty-state">Antibiogramm wird geladen ...</div>;
}
return (
<div className="page-stack">
<section className="section-card section-card--hero">
<div>
<p className="eyebrow">Antibiogramm</p>
<h3>Probe {sample.sampleNumber}</h3>
<p className="muted-text">
Nur Viertel mit bakteriellem Wachstum werden angezeigt. Identische Erreger werden
automatisch zusammengefasst.
</p>
</div>
{sample.antibiogramEditable ? null : (
<div className="alert alert--warning">
Das Antibiogramm ist in diesem Bearbeitungsstand nur noch lesbar.
</div>
)}
{message ? <div className="alert alert--error">{message}</div> : null}
</section>
{!groups.length ? (
<div className="empty-state">Fuer diese Probe ist kein Antibiogramm erforderlich.</div>
) : (
groups.map((group) => (
<section key={group.referenceQuarter} className="section-card">
<div className="section-card__header">
<div>
<p className="eyebrow">{group.reference?.label}</p>
<h3>{group.reference?.customPathogenName || group.reference?.pathogenName || "Erreger"}</h3>
</div>
{group.inherited.length ? (
<div className="info-chip">
Gilt ebenfalls fuer {group.inherited.map((entry) => entry.label).join(", ")}
</div>
) : null}
</div>
<div className="table-shell">
<table className="data-table">
<thead>
<tr>
<th>Antibiotikum</th>
<th className="matrix-col">Sensibel</th>
<th className="matrix-col">Intermediär</th>
<th className="matrix-col">Resistent</th>
</tr>
</thead>
<tbody>
{catalogs.antibiotics.map((antibiotic) => (
<tr key={antibiotic.businessKey}>
<td>
<strong>{antibiotic.name}</strong>
<small className="table-subtext">{antibiotic.code ?? "ANT"}</small>
</td>
{(["SENSITIVE", "INTERMEDIATE", "RESISTANT"] as SensitivityResult[]).map((result) => (
<td key={result} className="matrix-col">
<button
type="button"
className={`matrix-button ${
groupState[group.referenceQuarter]?.[antibiotic.businessKey] === result
? "is-selected"
: ""
}`}
onClick={() => updateResult(group.referenceQuarter, antibiotic.businessKey, result)}
disabled={!sample.antibiogramEditable}
>
{result === "SENSITIVE" ? "Sensibel" : result === "INTERMEDIATE" ? "Intermediär" : "Resistent"}
</button>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</section>
))
)}
<div className="page-actions">
<button
type="button"
className="accent-button"
onClick={() => void handleSave()}
disabled={saving || !sample.antibiogramEditable}
>
{saving ? "Speichern ..." : "Speichern"}
</button>
</div>
</div>
);
}