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
220 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|