Initial MUH app implementation
This commit is contained in:
227
frontend/src/pages/AnamnesisPage.tsx
Normal file
227
frontend/src/pages/AnamnesisPage.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { apiGet, apiPut } from "../lib/api";
|
||||
import type { ActiveCatalogSummary, QuarterKey, QuarterView, SampleDetail } from "../lib/types";
|
||||
|
||||
type QuarterFormState = {
|
||||
pathogenBusinessKey: string;
|
||||
customPathogenName: string;
|
||||
cellCount: string;
|
||||
};
|
||||
|
||||
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) : "",
|
||||
};
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!sampleId || !sample) {
|
||||
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: "",
|
||||
};
|
||||
|
||||
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" : ""}`}
|
||||
onClick={() => setActiveQuarter(quarter.quarterKey)}
|
||||
>
|
||||
{quarter.label}
|
||||
{quarter.flagged ? " ⚠" : ""}
|
||||
</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}
|
||||
|
||||
<div className="pathogen-grid">
|
||||
{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: "",
|
||||
})
|
||||
}
|
||||
disabled={!sample.anamnesisEditable}
|
||||
>
|
||||
<strong>{pathogen.name}</strong>
|
||||
<small>{pathogen.code ?? pathogen.kind}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className="field">
|
||||
<span>Erreger manuell eingeben</span>
|
||||
<input
|
||||
value={state.customPathogenName}
|
||||
onChange={(event) =>
|
||||
updateQuarter(visibleQuarter.quarterKey, {
|
||||
customPathogenName: event.target.value,
|
||||
pathogenBusinessKey: "",
|
||||
})
|
||||
}
|
||||
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">
|
||||
<strong>Hinweis</strong>
|
||||
<p>
|
||||
Kein Wachstum oder verunreinigte Proben werden spaeter automatisch vom
|
||||
Antibiogramm ausgeschlossen.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user