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
287 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|