diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/Pretreatment.java b/backend/src/main/java/de/svencarstensen/muh/domain/Pretreatment.java new file mode 100644 index 0000000..5e5ea76 --- /dev/null +++ b/backend/src/main/java/de/svencarstensen/muh/domain/Pretreatment.java @@ -0,0 +1,9 @@ +package de.svencarstensen.muh.domain; + +public record Pretreatment( + String inUdderInjector, + String systemicAntibiotics, + String painMedication, + String dryOffTreatment +) { +} diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java b/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java index 205340b..660248b 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/Sample.java @@ -3,6 +3,7 @@ package de.svencarstensen.muh.domain; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -29,6 +30,10 @@ public record Sample( LocalDateTime completedAt, String ownerAccountId, String createdByUserCode, - String createdByDisplayName + String createdByDisplayName, + // Additional fields from Lua version + Pretreatment pretreatment, + LocalDate clinicalExamDate, + String internalNote ) { } diff --git a/backend/src/main/java/de/svencarstensen/muh/domain/TherapyRecommendation.java b/backend/src/main/java/de/svencarstensen/muh/domain/TherapyRecommendation.java index 83ef8ad..f9e49ce 100644 --- a/backend/src/main/java/de/svencarstensen/muh/domain/TherapyRecommendation.java +++ b/backend/src/main/java/de/svencarstensen/muh/domain/TherapyRecommendation.java @@ -16,6 +16,15 @@ public record TherapyRecommendation( List dryAntibioticKeys, List dryAntibioticNames, String farmerNote, - String internalNote + String internalNote, + // Additional fields from Lua version + String inUdderCount, + String inUdderDuration, + String systemicCount, + String systemicDuration, + String systemicDosage, + String systemicLocation, + Boolean startvacVaccination, + Boolean noAntibioticTreatment ) { } diff --git a/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java b/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java index 769fd3b..aa2f117 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/CatalogService.java @@ -537,7 +537,7 @@ public class CatalogService { return toSessionResponse(user); } - public SessionResponse registerCustomer(RegistrationMutation mutation) { + public RegistrationResponse registerCustomer(RegistrationMutation mutation) { if (isBlank(mutation.companyName()) || isBlank(mutation.street()) || isBlank(mutation.houseNumber()) diff --git a/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java b/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java index 3febef3..92b0e5b 100644 --- a/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java +++ b/backend/src/main/java/de/svencarstensen/muh/service/SampleService.java @@ -4,6 +4,7 @@ import de.svencarstensen.muh.domain.AntibiogramEntry; import de.svencarstensen.muh.domain.AppUser; import de.svencarstensen.muh.domain.PathogenCatalogItem; import de.svencarstensen.muh.domain.PathogenKind; +import de.svencarstensen.muh.domain.Pretreatment; import de.svencarstensen.muh.domain.QuarterAntibiogram; import de.svencarstensen.muh.domain.QuarterFinding; import de.svencarstensen.muh.domain.QuarterKey; @@ -22,6 +23,8 @@ import org.springframework.web.server.ResponseStatusException; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -105,6 +108,18 @@ public class SampleService { .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); long sampleNumber = reserveNextSampleNumber(actorId); + Pretreatment pretreatment = request.pretreatmentInUdderInjector() == null && + request.pretreatmentSystemicAntibiotics() == null && + request.pretreatmentPainMedication() == null && + request.pretreatmentDryOffTreatment() == null + ? null + : new Pretreatment( + blankToNull(request.pretreatmentInUdderInjector()), + blankToNull(request.pretreatmentSystemicAntibiotics()), + blankToNull(request.pretreatmentPainMedication()), + blankToNull(request.pretreatmentDryOffTreatment()) + ); + Sample sample = new Sample( null, sampleNumber, @@ -127,7 +142,10 @@ public class SampleService { null, authorizationService.accountId(actor), request.userCode(), - request.userDisplayName() + request.userDisplayName(), + pretreatment, + parseClinicalExamDate(request.clinicalExamDate()), + blankToNull(request.internalNote()) ); return toDetail(sampleRepository.save(sample)); @@ -144,6 +162,18 @@ public class SampleService { .findFirst() .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden")); + Pretreatment pretreatment = request.pretreatmentInUdderInjector() == null && + request.pretreatmentSystemicAntibiotics() == null && + request.pretreatmentPainMedication() == null && + request.pretreatmentDryOffTreatment() == null + ? existing.pretreatment() + : new Pretreatment( + blankToNull(request.pretreatmentInUdderInjector()), + blankToNull(request.pretreatmentSystemicAntibiotics()), + blankToNull(request.pretreatmentPainMedication()), + blankToNull(request.pretreatmentDryOffTreatment()) + ); + Sample saved = sampleRepository.save(new Sample( existing.id(), existing.sampleNumber(), @@ -166,7 +196,14 @@ public class SampleService { existing.completedAt(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + pretreatment, + parseClinicalExamDate(request.clinicalExamDate()) != null + ? parseClinicalExamDate(request.clinicalExamDate()) + : existing.clinicalExamDate(), + request.internalNote() != null + ? blankToNull(request.internalNote()) + : existing.internalNote() )); return toDetail(saved); @@ -233,7 +270,10 @@ public class SampleService { existing.completedAt(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + existing.pretreatment(), + existing.clinicalExamDate(), + existing.internalNote() )); return toDetail(saved); } @@ -324,7 +364,10 @@ public class SampleService { existing.completedAt(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + existing.pretreatment(), + existing.clinicalExamDate(), + existing.internalNote() )); return toDetail(saved); } @@ -334,7 +377,7 @@ public class SampleService { if (existing.currentStep() == SampleWorkflowStep.COMPLETED) { TherapyRecommendation previous = existing.therapyRecommendation(); TherapyRecommendation updated = previous == null - ? new TherapyRecommendation(false, false, List.of(), List.of(), null, List.of(), List.of(), null, List.of(), List.of(), List.of(), List.of(), null, blankToNull(request.internalNote())) + ? new TherapyRecommendation(false, false, List.of(), List.of(), null, List.of(), List.of(), null, List.of(), List.of(), List.of(), List.of(), null, blankToNull(request.internalNote()), null, null, null, null, null, null, Boolean.FALSE, Boolean.FALSE) : new TherapyRecommendation( previous.continueStarted(), previous.switchTherapy(), @@ -349,7 +392,15 @@ public class SampleService { previous.dryAntibioticKeys(), previous.dryAntibioticNames(), previous.farmerNote(), - blankToNull(request.internalNote()) + blankToNull(request.internalNote()), + previous.inUdderCount(), + previous.inUdderDuration(), + previous.systemicCount(), + previous.systemicDuration(), + previous.systemicDosage(), + previous.systemicLocation(), + previous.startvacVaccination() != null ? previous.startvacVaccination() : Boolean.FALSE, + previous.noAntibioticTreatment() != null ? previous.noAntibioticTreatment() : Boolean.FALSE ); return toDetail(sampleRepository.save(new Sample( existing.id(), @@ -373,7 +424,10 @@ public class SampleService { existing.completedAt(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + existing.pretreatment(), + existing.clinicalExamDate(), + existing.internalNote() ))); } @@ -396,7 +450,15 @@ public class SampleService { request.dryAntibioticKeys(), resolveMedicationNames(request.dryAntibioticKeys(), medications), blankToNull(request.farmerNote()), - blankToNull(request.internalNote()) + blankToNull(request.internalNote()), + blankToNull(request.inUdderCount()), + blankToNull(request.inUdderDuration()), + blankToNull(request.systemicCount()), + blankToNull(request.systemicDuration()), + blankToNull(request.systemicDosage()), + blankToNull(request.systemicLocation()), + request.startvacVaccination(), + request.noAntibioticTreatment() ); Sample saved = sampleRepository.save(new Sample( @@ -421,7 +483,10 @@ public class SampleService { LocalDateTime.now(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + existing.pretreatment(), + existing.clinicalExamDate(), + existing.internalNote() )); return toDetail(saved); } @@ -450,7 +515,10 @@ public class SampleService { existing.completedAt(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + existing.pretreatment(), + existing.clinicalExamDate(), + existing.internalNote() )); } @@ -478,7 +546,10 @@ public class SampleService { existing.completedAt(), existing.ownerAccountId(), existing.createdByUserCode(), - existing.createdByDisplayName() + existing.createdByDisplayName(), + existing.pretreatment(), + existing.clinicalExamDate(), + existing.internalNote() )); } @@ -643,7 +714,10 @@ public class SampleService { sample.completedAt(), resolvedAccountId, sample.createdByUserCode(), - sample.createdByDisplayName() + sample.createdByDisplayName(), + sample.pretreatment(), + sample.clinicalExamDate(), + sample.internalNote() )); } } @@ -737,7 +811,10 @@ public class SampleService { SampleWorkflowRules.canEditAnamnesis(sample), SampleWorkflowRules.canEditAntibiogram(sample), SampleWorkflowRules.canEditTherapy(sample), - sample.currentStep() == SampleWorkflowStep.COMPLETED + sample.currentStep() == SampleWorkflowStep.COMPLETED, + sample.pretreatment(), + sample.clinicalExamDate(), + sample.internalNote() ); } @@ -759,7 +836,15 @@ public class SampleService { therapy.dryAntibioticKeys(), therapy.dryAntibioticNames(), therapy.farmerNote(), - therapy.internalNote() + therapy.internalNote(), + therapy.inUdderCount(), + therapy.inUdderDuration(), + therapy.systemicCount(), + therapy.systemicDuration(), + therapy.systemicDosage(), + therapy.systemicLocation(), + therapy.startvacVaccination() != null ? therapy.startvacVaccination() : Boolean.FALSE, + therapy.noAntibioticTreatment() != null ? therapy.noAntibioticTreatment() : Boolean.FALSE ); } @@ -877,7 +962,15 @@ public class SampleService { List dryAntibioticKeys, List dryAntibioticNames, String farmerNote, - String internalNote + String internalNote, + String inUdderCount, + String inUdderDuration, + String systemicCount, + String systemicDuration, + String systemicDosage, + String systemicLocation, + Boolean startvacVaccination, + Boolean noAntibioticTreatment ) { } @@ -909,7 +1002,10 @@ public class SampleService { boolean anamnesisEditable, boolean antibiogramEditable, boolean therapyEditable, - boolean completed + boolean completed, + Pretreatment pretreatment, + LocalDate clinicalExamDate, + String internalNote ) { } @@ -921,7 +1017,13 @@ public class SampleService { SamplingMode samplingMode, List flaggedQuarters, String userCode, - String userDisplayName + String userDisplayName, + String pretreatmentInUdderInjector, + String pretreatmentSystemicAntibiotics, + String pretreatmentPainMedication, + String pretreatmentDryOffTreatment, + String clinicalExamDate, + String internalNote ) { } @@ -955,7 +1057,33 @@ public class SampleService { List drySealerKeys, List dryAntibioticKeys, String farmerNote, - String internalNote + String internalNote, + String inUdderCount, + String inUdderDuration, + String systemicCount, + String systemicDuration, + String systemicDosage, + String systemicLocation, + Boolean startvacVaccination, + Boolean noAntibioticTreatment ) { } + + private LocalDate parseClinicalExamDate(String dateStr) { + if (dateStr == null || dateStr.isBlank()) { + return null; + } + try { + // Try ISO format first (YYYY-MM-DD) + return LocalDate.parse(dateStr); + } catch (DateTimeParseException e) { + try { + // Try German format (DD.MM.YYYY) + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); + return LocalDate.parse(dateStr, formatter); + } catch (DateTimeParseException e2) { + return null; + } + } + } } diff --git a/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java b/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java index 7e211d6..b292b8e 100644 --- a/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java +++ b/backend/src/main/java/de/svencarstensen/muh/web/SampleController.java @@ -54,7 +54,13 @@ public class SampleController { request.samplingMode(), request.flaggedQuarters(), deriveUserLabel(user.displayName()), - user.displayName() + user.displayName(), + request.pretreatmentInUdderInjector(), + request.pretreatmentSystemicAntibiotics(), + request.pretreatmentPainMedication(), + request.pretreatmentDryOffTreatment(), + request.clinicalExamDate(), + request.internalNote() )); } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 3cacaef..0827db6 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -9,6 +9,13 @@ export type QuarterKey = | "LEFT_REAR" | "RIGHT_REAR"; export type PathogenKind = "BACTERIAL" | "NO_GROWTH" | "CONTAMINATED" | "OTHER"; + +export interface Pretreatment { + inUdderInjector: string | null; + systemicAntibiotics: string | null; + painMedication: string | null; + dryOffTreatment: string | null; +} export type SensitivityResult = "SENSITIVE" | "INTERMEDIATE" | "RESISTANT"; export type MedicationCategory = | "IN_UDDER" @@ -145,6 +152,14 @@ export interface TherapyView { dryAntibioticNames: string[]; farmerNote: string | null; internalNote: string | null; + inUdderCount: string | null; + inUdderDuration: string | null; + systemicCount: string | null; + systemicDuration: string | null; + systemicDosage: string | null; + systemicLocation: string | null; + startvacVaccination: boolean | null; + noAntibioticTreatment: boolean | null; } export interface SampleDetail { @@ -176,6 +191,9 @@ export interface SampleDetail { antibiogramEditable: boolean; therapyEditable: boolean; completed: boolean; + pretreatment: Pretreatment | null; + clinicalExamDate: string | null; + internalNote: string | null; } export interface FarmerRow { diff --git a/frontend/src/pages/AdminDashboardPage.tsx b/frontend/src/pages/AdminDashboardPage.tsx index 12983f4..0ffa54e 100644 --- a/frontend/src/pages/AdminDashboardPage.tsx +++ b/frontend/src/pages/AdminDashboardPage.tsx @@ -7,6 +7,7 @@ import { Title, Tooltip, Legend, + type TooltipItem, } from "chart.js"; import { Bar } from "react-chartjs-2"; import { apiGet } from "../lib/api"; @@ -101,8 +102,9 @@ export default function AdminDashboardPage() { size: 13, }, callbacks: { - label: (context: { parsed: { y: number } }) => { - return `${context.parsed.y} Proben`; + label: (context: TooltipItem<"bar">) => { + const value = context.parsed.y as number | null; + return `${value ?? 0} Proben`; }, }, }, diff --git a/frontend/src/pages/AnamnesisPage.tsx b/frontend/src/pages/AnamnesisPage.tsx index 1c1c60d..bc39eb1 100644 --- a/frontend/src/pages/AnamnesisPage.tsx +++ b/frontend/src/pages/AnamnesisPage.tsx @@ -1,12 +1,13 @@ 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"; +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) { @@ -15,11 +16,18 @@ function quarterStateFromSample(sample: SampleDetail) { 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(); @@ -76,6 +84,20 @@ export default function AnamnesisPage() { 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; @@ -117,6 +139,7 @@ export default function AnamnesisPage() { pathogenBusinessKey: "", customPathogenName: "", cellCount: "", + pathogenKind: null, }; return ( @@ -168,7 +191,25 @@ export default function AnamnesisPage() {
Auffaelliges Viertel markiert
) : null} -

Erreger

+ {/* Special pathogen options like in Lua */} +

Sonderfaelle

+
+ {SPECIAL_PATHOGENS.map((pathogen) => ( + + ))} +
+ +

Erreger (Katalog)

{catalogs.pathogens.map((pathogen) => (
diff --git a/frontend/src/pages/AntibiogramPage.tsx b/frontend/src/pages/AntibiogramPage.tsx index 13dbd43..b43d662 100644 --- a/frontend/src/pages/AntibiogramPage.tsx +++ b/frontend/src/pages/AntibiogramPage.tsx @@ -167,9 +167,9 @@ export default function AntibiogramPage() { Antibiotikum - S - I - R + Sensibel + Intermediär + Resistent @@ -180,7 +180,7 @@ export default function AntibiogramPage() { {antibiotic.code ?? "ANT"} {(["SENSITIVE", "INTERMEDIATE", "RESISTANT"] as SensitivityResult[]).map((result) => ( - + ))} diff --git a/frontend/src/pages/SampleRegistrationPage.tsx b/frontend/src/pages/SampleRegistrationPage.tsx index 0e085b1..03a83d5 100644 --- a/frontend/src/pages/SampleRegistrationPage.tsx +++ b/frontend/src/pages/SampleRegistrationPage.tsx @@ -18,6 +18,14 @@ const QUARTERS: { key: QuarterKey; label: string }[] = [ { key: "RIGHT_REAR", label: "Hinten rechts" }, ]; +// Pretreatment options from Lua version +const PRETREATMENT_OPTIONS = [ + { key: "inUdderInjector", label: "Euterinjektor" }, + { key: "systemicAntibiotics", label: "Systemische Antibiotika" }, + { key: "painMedication", label: "Schmerzmittel" }, + { key: "dryOffTreatment", label: "Trockensteller" }, +]; + export default function SampleRegistrationPage() { const { sampleId } = useParams(); const navigate = useNavigate(); @@ -32,6 +40,15 @@ export default function SampleRegistrationPage() { const [sampleKind, setSampleKind] = useState("LACTATION"); const [samplingMode, setSamplingMode] = useState("SINGLE_SITE"); const [flaggedQuarters, setFlaggedQuarters] = useState([]); + // New fields from Lua version + const [pretreatment, setPretreatment] = useState>({ + inUdderInjector: "", + systemicAntibiotics: "", + painMedication: "", + dryOffTreatment: "", + }); + const [clinicalExamDate, setClinicalExamDate] = useState(""); + const [internalNote, setInternalNote] = useState(""); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(null); @@ -55,6 +72,17 @@ export default function SampleRegistrationPage() { setFlaggedQuarters( sample.quarters.filter((quarter) => quarter.flagged).map((quarter) => quarter.quarterKey), ); + // Load new fields + if (sample.pretreatment) { + setPretreatment({ + inUdderInjector: sample.pretreatment.inUdderInjector ?? "", + systemicAntibiotics: sample.pretreatment.systemicAntibiotics ?? "", + painMedication: sample.pretreatment.painMedication ?? "", + dryOffTreatment: sample.pretreatment.dryOffTreatment ?? "", + }); + } + setClinicalExamDate(sample.clinicalExamDate ?? ""); + setInternalNote(sample.internalNote ?? ""); } else { const dashboard = await apiGet("/dashboard"); setSampleNumber(dashboard.nextSampleNumber); @@ -78,6 +106,13 @@ export default function SampleRegistrationPage() { ); } + function updatePretreatment(key: string, value: string) { + setPretreatment((current) => ({ + ...current, + [key]: value, + })); + } + async function handleSubmit(event: FormEvent) { event.preventDefault(); setShowValidation(true); @@ -101,6 +136,13 @@ export default function SampleRegistrationPage() { flaggedQuarters, userCode: user.displayName, userDisplayName: user.displayName, + // New fields + pretreatmentInUdderInjector: pretreatment.inUdderInjector || null, + pretreatmentSystemicAntibiotics: pretreatment.systemicAntibiotics || null, + pretreatmentPainMedication: pretreatment.painMedication || null, + pretreatmentDryOffTreatment: pretreatment.dryOffTreatment || null, + clinicalExamDate: clinicalExamDate || null, + internalNote: internalNote || null, }; try { @@ -126,7 +168,7 @@ export default function SampleRegistrationPage() {

Neuanlage

Probe {sampleNumber ?? "..."}

- Die Probenummer wird fortlaufend vergeben. Trockensteller lassen sich ueber den + Die Probenummer wird fortlaufend vergeben. Trockensteller lassen sich über den Schalter Trockenstellerprobe markieren.

@@ -258,6 +300,55 @@ export default function SampleRegistrationPage() { ) : null} + {/* New section: Pretreatment from Lua version */} +
+

Vorbehandelt mit

+
+ {PRETREATMENT_OPTIONS.map((option) => ( + + ))} +
+
+ + {/* New section: Clinical Exam Date from Lua version */} +
+
+

Klinische Untersuchung

+ +
+ +
+

Interne Bemerkung

+