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
This commit is contained in:
2026-03-17 16:50:40 +01:00
parent 7c59944646
commit d03dc94ad1
13 changed files with 569 additions and 37 deletions

View File

@@ -0,0 +1,9 @@
package de.svencarstensen.muh.domain;
public record Pretreatment(
String inUdderInjector,
String systemicAntibiotics,
String painMedication,
String dryOffTreatment
) {
}

View File

@@ -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
) {
}

View File

@@ -16,6 +16,15 @@ public record TherapyRecommendation(
List<String> dryAntibioticKeys,
List<String> 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
) {
}

View File

@@ -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())

View File

@@ -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<String> dryAntibioticKeys,
List<String> 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<QuarterKey> 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<String> drySealerKeys,
List<String> 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;
}
}
}
}

View File

@@ -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()
));
}