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:
@@ -0,0 +1,9 @@
|
|||||||
|
package de.svencarstensen.muh.domain;
|
||||||
|
|
||||||
|
public record Pretreatment(
|
||||||
|
String inUdderInjector,
|
||||||
|
String systemicAntibiotics,
|
||||||
|
String painMedication,
|
||||||
|
String dryOffTreatment
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package de.svencarstensen.muh.domain;
|
|||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ public record Sample(
|
|||||||
LocalDateTime completedAt,
|
LocalDateTime completedAt,
|
||||||
String ownerAccountId,
|
String ownerAccountId,
|
||||||
String createdByUserCode,
|
String createdByUserCode,
|
||||||
String createdByDisplayName
|
String createdByDisplayName,
|
||||||
|
// Additional fields from Lua version
|
||||||
|
Pretreatment pretreatment,
|
||||||
|
LocalDate clinicalExamDate,
|
||||||
|
String internalNote
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ public record TherapyRecommendation(
|
|||||||
List<String> dryAntibioticKeys,
|
List<String> dryAntibioticKeys,
|
||||||
List<String> dryAntibioticNames,
|
List<String> dryAntibioticNames,
|
||||||
String farmerNote,
|
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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -537,7 +537,7 @@ public class CatalogService {
|
|||||||
return toSessionResponse(user);
|
return toSessionResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionResponse registerCustomer(RegistrationMutation mutation) {
|
public RegistrationResponse registerCustomer(RegistrationMutation mutation) {
|
||||||
if (isBlank(mutation.companyName())
|
if (isBlank(mutation.companyName())
|
||||||
|| isBlank(mutation.street())
|
|| isBlank(mutation.street())
|
||||||
|| isBlank(mutation.houseNumber())
|
|| isBlank(mutation.houseNumber())
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de.svencarstensen.muh.domain.AntibiogramEntry;
|
|||||||
import de.svencarstensen.muh.domain.AppUser;
|
import de.svencarstensen.muh.domain.AppUser;
|
||||||
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
||||||
import de.svencarstensen.muh.domain.PathogenKind;
|
import de.svencarstensen.muh.domain.PathogenKind;
|
||||||
|
import de.svencarstensen.muh.domain.Pretreatment;
|
||||||
import de.svencarstensen.muh.domain.QuarterAntibiogram;
|
import de.svencarstensen.muh.domain.QuarterAntibiogram;
|
||||||
import de.svencarstensen.muh.domain.QuarterFinding;
|
import de.svencarstensen.muh.domain.QuarterFinding;
|
||||||
import de.svencarstensen.muh.domain.QuarterKey;
|
import de.svencarstensen.muh.domain.QuarterKey;
|
||||||
@@ -22,6 +23,8 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -105,6 +108,18 @@ public class SampleService {
|
|||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
|
||||||
|
|
||||||
long sampleNumber = reserveNextSampleNumber(actorId);
|
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(
|
Sample sample = new Sample(
|
||||||
null,
|
null,
|
||||||
sampleNumber,
|
sampleNumber,
|
||||||
@@ -127,7 +142,10 @@ public class SampleService {
|
|||||||
null,
|
null,
|
||||||
authorizationService.accountId(actor),
|
authorizationService.accountId(actor),
|
||||||
request.userCode(),
|
request.userCode(),
|
||||||
request.userDisplayName()
|
request.userDisplayName(),
|
||||||
|
pretreatment,
|
||||||
|
parseClinicalExamDate(request.clinicalExamDate()),
|
||||||
|
blankToNull(request.internalNote())
|
||||||
);
|
);
|
||||||
|
|
||||||
return toDetail(sampleRepository.save(sample));
|
return toDetail(sampleRepository.save(sample));
|
||||||
@@ -144,6 +162,18 @@ public class SampleService {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
|
.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(
|
Sample saved = sampleRepository.save(new Sample(
|
||||||
existing.id(),
|
existing.id(),
|
||||||
existing.sampleNumber(),
|
existing.sampleNumber(),
|
||||||
@@ -166,7 +196,14 @@ public class SampleService {
|
|||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
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);
|
return toDetail(saved);
|
||||||
@@ -233,7 +270,10 @@ public class SampleService {
|
|||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName(),
|
||||||
|
existing.pretreatment(),
|
||||||
|
existing.clinicalExamDate(),
|
||||||
|
existing.internalNote()
|
||||||
));
|
));
|
||||||
return toDetail(saved);
|
return toDetail(saved);
|
||||||
}
|
}
|
||||||
@@ -324,7 +364,10 @@ public class SampleService {
|
|||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName(),
|
||||||
|
existing.pretreatment(),
|
||||||
|
existing.clinicalExamDate(),
|
||||||
|
existing.internalNote()
|
||||||
));
|
));
|
||||||
return toDetail(saved);
|
return toDetail(saved);
|
||||||
}
|
}
|
||||||
@@ -334,7 +377,7 @@ public class SampleService {
|
|||||||
if (existing.currentStep() == SampleWorkflowStep.COMPLETED) {
|
if (existing.currentStep() == SampleWorkflowStep.COMPLETED) {
|
||||||
TherapyRecommendation previous = existing.therapyRecommendation();
|
TherapyRecommendation previous = existing.therapyRecommendation();
|
||||||
TherapyRecommendation updated = previous == null
|
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(
|
: new TherapyRecommendation(
|
||||||
previous.continueStarted(),
|
previous.continueStarted(),
|
||||||
previous.switchTherapy(),
|
previous.switchTherapy(),
|
||||||
@@ -349,7 +392,15 @@ public class SampleService {
|
|||||||
previous.dryAntibioticKeys(),
|
previous.dryAntibioticKeys(),
|
||||||
previous.dryAntibioticNames(),
|
previous.dryAntibioticNames(),
|
||||||
previous.farmerNote(),
|
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(
|
return toDetail(sampleRepository.save(new Sample(
|
||||||
existing.id(),
|
existing.id(),
|
||||||
@@ -373,7 +424,10 @@ public class SampleService {
|
|||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName(),
|
||||||
|
existing.pretreatment(),
|
||||||
|
existing.clinicalExamDate(),
|
||||||
|
existing.internalNote()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +450,15 @@ public class SampleService {
|
|||||||
request.dryAntibioticKeys(),
|
request.dryAntibioticKeys(),
|
||||||
resolveMedicationNames(request.dryAntibioticKeys(), medications),
|
resolveMedicationNames(request.dryAntibioticKeys(), medications),
|
||||||
blankToNull(request.farmerNote()),
|
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(
|
Sample saved = sampleRepository.save(new Sample(
|
||||||
@@ -421,7 +483,10 @@ public class SampleService {
|
|||||||
LocalDateTime.now(),
|
LocalDateTime.now(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName(),
|
||||||
|
existing.pretreatment(),
|
||||||
|
existing.clinicalExamDate(),
|
||||||
|
existing.internalNote()
|
||||||
));
|
));
|
||||||
return toDetail(saved);
|
return toDetail(saved);
|
||||||
}
|
}
|
||||||
@@ -450,7 +515,10 @@ public class SampleService {
|
|||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName(),
|
||||||
|
existing.pretreatment(),
|
||||||
|
existing.clinicalExamDate(),
|
||||||
|
existing.internalNote()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +546,10 @@ public class SampleService {
|
|||||||
existing.completedAt(),
|
existing.completedAt(),
|
||||||
existing.ownerAccountId(),
|
existing.ownerAccountId(),
|
||||||
existing.createdByUserCode(),
|
existing.createdByUserCode(),
|
||||||
existing.createdByDisplayName()
|
existing.createdByDisplayName(),
|
||||||
|
existing.pretreatment(),
|
||||||
|
existing.clinicalExamDate(),
|
||||||
|
existing.internalNote()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +714,10 @@ public class SampleService {
|
|||||||
sample.completedAt(),
|
sample.completedAt(),
|
||||||
resolvedAccountId,
|
resolvedAccountId,
|
||||||
sample.createdByUserCode(),
|
sample.createdByUserCode(),
|
||||||
sample.createdByDisplayName()
|
sample.createdByDisplayName(),
|
||||||
|
sample.pretreatment(),
|
||||||
|
sample.clinicalExamDate(),
|
||||||
|
sample.internalNote()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,7 +811,10 @@ public class SampleService {
|
|||||||
SampleWorkflowRules.canEditAnamnesis(sample),
|
SampleWorkflowRules.canEditAnamnesis(sample),
|
||||||
SampleWorkflowRules.canEditAntibiogram(sample),
|
SampleWorkflowRules.canEditAntibiogram(sample),
|
||||||
SampleWorkflowRules.canEditTherapy(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.dryAntibioticKeys(),
|
||||||
therapy.dryAntibioticNames(),
|
therapy.dryAntibioticNames(),
|
||||||
therapy.farmerNote(),
|
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> dryAntibioticKeys,
|
||||||
List<String> dryAntibioticNames,
|
List<String> dryAntibioticNames,
|
||||||
String farmerNote,
|
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 anamnesisEditable,
|
||||||
boolean antibiogramEditable,
|
boolean antibiogramEditable,
|
||||||
boolean therapyEditable,
|
boolean therapyEditable,
|
||||||
boolean completed
|
boolean completed,
|
||||||
|
Pretreatment pretreatment,
|
||||||
|
LocalDate clinicalExamDate,
|
||||||
|
String internalNote
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,7 +1017,13 @@ public class SampleService {
|
|||||||
SamplingMode samplingMode,
|
SamplingMode samplingMode,
|
||||||
List<QuarterKey> flaggedQuarters,
|
List<QuarterKey> flaggedQuarters,
|
||||||
String userCode,
|
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> drySealerKeys,
|
||||||
List<String> dryAntibioticKeys,
|
List<String> dryAntibioticKeys,
|
||||||
String farmerNote,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,13 @@ public class SampleController {
|
|||||||
request.samplingMode(),
|
request.samplingMode(),
|
||||||
request.flaggedQuarters(),
|
request.flaggedQuarters(),
|
||||||
deriveUserLabel(user.displayName()),
|
deriveUserLabel(user.displayName()),
|
||||||
user.displayName()
|
user.displayName(),
|
||||||
|
request.pretreatmentInUdderInjector(),
|
||||||
|
request.pretreatmentSystemicAntibiotics(),
|
||||||
|
request.pretreatmentPainMedication(),
|
||||||
|
request.pretreatmentDryOffTreatment(),
|
||||||
|
request.clinicalExamDate(),
|
||||||
|
request.internalNote()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export type QuarterKey =
|
|||||||
| "LEFT_REAR"
|
| "LEFT_REAR"
|
||||||
| "RIGHT_REAR";
|
| "RIGHT_REAR";
|
||||||
export type PathogenKind = "BACTERIAL" | "NO_GROWTH" | "CONTAMINATED" | "OTHER";
|
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 SensitivityResult = "SENSITIVE" | "INTERMEDIATE" | "RESISTANT";
|
||||||
export type MedicationCategory =
|
export type MedicationCategory =
|
||||||
| "IN_UDDER"
|
| "IN_UDDER"
|
||||||
@@ -145,6 +152,14 @@ export interface TherapyView {
|
|||||||
dryAntibioticNames: string[];
|
dryAntibioticNames: string[];
|
||||||
farmerNote: string | null;
|
farmerNote: string | null;
|
||||||
internalNote: 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 {
|
export interface SampleDetail {
|
||||||
@@ -176,6 +191,9 @@ export interface SampleDetail {
|
|||||||
antibiogramEditable: boolean;
|
antibiogramEditable: boolean;
|
||||||
therapyEditable: boolean;
|
therapyEditable: boolean;
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
|
pretreatment: Pretreatment | null;
|
||||||
|
clinicalExamDate: string | null;
|
||||||
|
internalNote: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FarmerRow {
|
export interface FarmerRow {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
|
type TooltipItem,
|
||||||
} from "chart.js";
|
} from "chart.js";
|
||||||
import { Bar } from "react-chartjs-2";
|
import { Bar } from "react-chartjs-2";
|
||||||
import { apiGet } from "../lib/api";
|
import { apiGet } from "../lib/api";
|
||||||
@@ -101,8 +102,9 @@ export default function AdminDashboardPage() {
|
|||||||
size: 13,
|
size: 13,
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context: { parsed: { y: number } }) => {
|
label: (context: TooltipItem<"bar">) => {
|
||||||
return `${context.parsed.y} Proben`;
|
const value = context.parsed.y as number | null;
|
||||||
|
return `${value ?? 0} Proben`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { apiGet, apiPut } from "../lib/api";
|
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 = {
|
type QuarterFormState = {
|
||||||
pathogenBusinessKey: string;
|
pathogenBusinessKey: string;
|
||||||
customPathogenName: string;
|
customPathogenName: string;
|
||||||
cellCount: string;
|
cellCount: string;
|
||||||
|
pathogenKind: PathogenKind | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function quarterStateFromSample(sample: SampleDetail) {
|
function quarterStateFromSample(sample: SampleDetail) {
|
||||||
@@ -15,11 +16,18 @@ function quarterStateFromSample(sample: SampleDetail) {
|
|||||||
pathogenBusinessKey: quarter.pathogenBusinessKey ?? "",
|
pathogenBusinessKey: quarter.pathogenBusinessKey ?? "",
|
||||||
customPathogenName: quarter.customPathogenName ?? "",
|
customPathogenName: quarter.customPathogenName ?? "",
|
||||||
cellCount: quarter.cellCount ? String(quarter.cellCount) : "",
|
cellCount: quarter.cellCount ? String(quarter.cellCount) : "",
|
||||||
|
pathogenKind: quarter.pathogenKind,
|
||||||
};
|
};
|
||||||
return accumulator;
|
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() {
|
export default function AnamnesisPage() {
|
||||||
const { sampleId } = useParams();
|
const { sampleId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -76,6 +84,20 @@ export default function AnamnesisPage() {
|
|||||||
return Boolean(quarterState?.pathogenBusinessKey || quarterState?.customPathogenName?.trim());
|
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() {
|
async function handleSave() {
|
||||||
if (!sampleId || !sample) {
|
if (!sampleId || !sample) {
|
||||||
return;
|
return;
|
||||||
@@ -117,6 +139,7 @@ export default function AnamnesisPage() {
|
|||||||
pathogenBusinessKey: "",
|
pathogenBusinessKey: "",
|
||||||
customPathogenName: "",
|
customPathogenName: "",
|
||||||
cellCount: "",
|
cellCount: "",
|
||||||
|
pathogenKind: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -168,7 +191,25 @@ export default function AnamnesisPage() {
|
|||||||
<div className="info-chip">Auffaelliges Viertel markiert</div>
|
<div className="info-chip">Auffaelliges Viertel markiert</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<p className="required-label">Erreger</p>
|
{/* 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" : ""}`}>
|
<div className={`pathogen-grid ${showValidation && !quarterHasPathogen(visibleQuarter.quarterKey) ? "is-invalid" : ""}`}>
|
||||||
{catalogs.pathogens.map((pathogen) => (
|
{catalogs.pathogens.map((pathogen) => (
|
||||||
<button
|
<button
|
||||||
@@ -181,6 +222,7 @@ export default function AnamnesisPage() {
|
|||||||
updateQuarter(visibleQuarter.quarterKey, {
|
updateQuarter(visibleQuarter.quarterKey, {
|
||||||
pathogenBusinessKey: pathogen.businessKey,
|
pathogenBusinessKey: pathogen.businessKey,
|
||||||
customPathogenName: "",
|
customPathogenName: "",
|
||||||
|
pathogenKind: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!sample.anamnesisEditable}
|
disabled={!sample.anamnesisEditable}
|
||||||
@@ -199,6 +241,7 @@ export default function AnamnesisPage() {
|
|||||||
updateQuarter(visibleQuarter.quarterKey, {
|
updateQuarter(visibleQuarter.quarterKey, {
|
||||||
customPathogenName: event.target.value,
|
customPathogenName: event.target.value,
|
||||||
pathogenBusinessKey: "",
|
pathogenBusinessKey: "",
|
||||||
|
pathogenKind: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!sample.anamnesisEditable}
|
disabled={!sample.anamnesisEditable}
|
||||||
@@ -221,8 +264,8 @@ export default function AnamnesisPage() {
|
|||||||
<div className="info-panel info-panel--spaced">
|
<div className="info-panel info-panel--spaced">
|
||||||
<strong>Hinweis</strong>
|
<strong>Hinweis</strong>
|
||||||
<p>
|
<p>
|
||||||
Kein Wachstum oder verunreinigte Proben werden später automatisch vom
|
Bei "Kein bakterielles Wachstum" oder "Verunreinigte Probe" wird das Antibiogramm
|
||||||
Antibiogramm ausgeschlossen.
|
übersprungen und direkt zur Therapie weitergeleitet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -167,9 +167,9 @@ export default function AntibiogramPage() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Antibiotikum</th>
|
<th>Antibiotikum</th>
|
||||||
<th>S</th>
|
<th className="matrix-col">Sensibel</th>
|
||||||
<th>I</th>
|
<th className="matrix-col">Intermediär</th>
|
||||||
<th>R</th>
|
<th className="matrix-col">Resistent</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -180,7 +180,7 @@ export default function AntibiogramPage() {
|
|||||||
<small className="table-subtext">{antibiotic.code ?? "ANT"}</small>
|
<small className="table-subtext">{antibiotic.code ?? "ANT"}</small>
|
||||||
</td>
|
</td>
|
||||||
{(["SENSITIVE", "INTERMEDIATE", "RESISTANT"] as SensitivityResult[]).map((result) => (
|
{(["SENSITIVE", "INTERMEDIATE", "RESISTANT"] as SensitivityResult[]).map((result) => (
|
||||||
<td key={result}>
|
<td key={result} className="matrix-col">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`matrix-button ${
|
className={`matrix-button ${
|
||||||
@@ -191,7 +191,7 @@ export default function AntibiogramPage() {
|
|||||||
onClick={() => updateResult(group.referenceQuarter, antibiotic.businessKey, result)}
|
onClick={() => updateResult(group.referenceQuarter, antibiotic.businessKey, result)}
|
||||||
disabled={!sample.antibiogramEditable}
|
disabled={!sample.antibiogramEditable}
|
||||||
>
|
>
|
||||||
{result === "SENSITIVE" ? "S" : result === "INTERMEDIATE" ? "I" : "R"}
|
{result === "SENSITIVE" ? "Sensibel" : result === "INTERMEDIATE" ? "Intermediär" : "Resistent"}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ const QUARTERS: { key: QuarterKey; label: string }[] = [
|
|||||||
{ key: "RIGHT_REAR", label: "Hinten rechts" },
|
{ 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() {
|
export default function SampleRegistrationPage() {
|
||||||
const { sampleId } = useParams();
|
const { sampleId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -32,6 +40,15 @@ export default function SampleRegistrationPage() {
|
|||||||
const [sampleKind, setSampleKind] = useState<SampleKind>("LACTATION");
|
const [sampleKind, setSampleKind] = useState<SampleKind>("LACTATION");
|
||||||
const [samplingMode, setSamplingMode] = useState<SamplingMode>("SINGLE_SITE");
|
const [samplingMode, setSamplingMode] = useState<SamplingMode>("SINGLE_SITE");
|
||||||
const [flaggedQuarters, setFlaggedQuarters] = useState<QuarterKey[]>([]);
|
const [flaggedQuarters, setFlaggedQuarters] = useState<QuarterKey[]>([]);
|
||||||
|
// New fields from Lua version
|
||||||
|
const [pretreatment, setPretreatment] = useState<Record<string, string>>({
|
||||||
|
inUdderInjector: "",
|
||||||
|
systemicAntibiotics: "",
|
||||||
|
painMedication: "",
|
||||||
|
dryOffTreatment: "",
|
||||||
|
});
|
||||||
|
const [clinicalExamDate, setClinicalExamDate] = useState("");
|
||||||
|
const [internalNote, setInternalNote] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
@@ -55,6 +72,17 @@ export default function SampleRegistrationPage() {
|
|||||||
setFlaggedQuarters(
|
setFlaggedQuarters(
|
||||||
sample.quarters.filter((quarter) => quarter.flagged).map((quarter) => quarter.quarterKey),
|
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 {
|
} else {
|
||||||
const dashboard = await apiGet<DashboardOverview>("/dashboard");
|
const dashboard = await apiGet<DashboardOverview>("/dashboard");
|
||||||
setSampleNumber(dashboard.nextSampleNumber);
|
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<HTMLFormElement>) {
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setShowValidation(true);
|
setShowValidation(true);
|
||||||
@@ -101,6 +136,13 @@ export default function SampleRegistrationPage() {
|
|||||||
flaggedQuarters,
|
flaggedQuarters,
|
||||||
userCode: user.displayName,
|
userCode: user.displayName,
|
||||||
userDisplayName: 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 {
|
try {
|
||||||
@@ -126,7 +168,7 @@ export default function SampleRegistrationPage() {
|
|||||||
<p className="eyebrow">Neuanlage</p>
|
<p className="eyebrow">Neuanlage</p>
|
||||||
<h3>Probe {sampleNumber ?? "..."}</h3>
|
<h3>Probe {sampleNumber ?? "..."}</h3>
|
||||||
<p className="muted-text">
|
<p className="muted-text">
|
||||||
Die Probenummer wird fortlaufend vergeben. Trockensteller lassen sich ueber den
|
Die Probenummer wird fortlaufend vergeben. Trockensteller lassen sich über den
|
||||||
Schalter Trockenstellerprobe markieren.
|
Schalter Trockenstellerprobe markieren.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,6 +300,55 @@ export default function SampleRegistrationPage() {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* New section: Pretreatment from Lua version */}
|
||||||
|
<section className="section-card">
|
||||||
|
<p className="eyebrow">Vorbehandelt mit</p>
|
||||||
|
<div className="field-grid field-grid--stacked">
|
||||||
|
{PRETREATMENT_OPTIONS.map((option) => (
|
||||||
|
<label key={option.key} className="field">
|
||||||
|
<span>{option.label}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={pretreatment[option.key]}
|
||||||
|
onChange={(event) => updatePretreatment(option.key, event.target.value)}
|
||||||
|
disabled={!editable}
|
||||||
|
placeholder="Ohne Vorbehandlung"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* New section: Clinical Exam Date from Lua version */}
|
||||||
|
<section className="form-grid">
|
||||||
|
<article className="section-card">
|
||||||
|
<p className="eyebrow">Klinische Untersuchung</p>
|
||||||
|
<label className="field">
|
||||||
|
<span>Untersuchungsdatum (TT.MM.JJJJ)</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={clinicalExamDate}
|
||||||
|
onChange={(event) => setClinicalExamDate(event.target.value)}
|
||||||
|
disabled={!editable}
|
||||||
|
placeholder="z.B. 15.03.2024"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="section-card">
|
||||||
|
<p className="eyebrow">Interne Bemerkung</p>
|
||||||
|
<label className="field">
|
||||||
|
<span>Bemerkung zur Probe</span>
|
||||||
|
<textarea
|
||||||
|
value={internalNote}
|
||||||
|
onChange={(event) => setInternalNote(event.target.value)}
|
||||||
|
disabled={!editable}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="page-actions">
|
<div className="page-actions">
|
||||||
<button type="submit" className="accent-button" disabled={saving || !editable}>
|
<button type="submit" className="accent-button" disabled={saving || !editable}>
|
||||||
{saving ? "Speichern ..." : "Speichern"}
|
{saving ? "Speichern ..." : "Speichern"}
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ function medicationOptions(catalogs: ActiveCatalogSummary, category: MedicationC
|
|||||||
return catalogs.medications.filter((medication) => medication.category === category);
|
return catalogs.medications.filter((medication) => medication.category === category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options for dropdowns like in Lua version
|
||||||
|
const COUNT_OPTIONS = ["1", "2", "3", "4", "5"];
|
||||||
|
const DURATION_OPTIONS = ["1 Tag", "2 Tage", "3 Tage", "4 Tage", "5 Tage", "6 Tage", "7 Tage", "8 Tage", "10 Tage", "14 Tage"];
|
||||||
|
const DOSAGE_OPTIONS = ["einmalig", "1 x", "2 x", "3 x"];
|
||||||
|
const LOCATION_OPTIONS = ["i.m.", "i.v.", "s.c."];
|
||||||
|
|
||||||
export default function TherapyPage() {
|
export default function TherapyPage() {
|
||||||
const { sampleId } = useParams();
|
const { sampleId } = useParams();
|
||||||
|
|
||||||
@@ -26,6 +32,15 @@ export default function TherapyPage() {
|
|||||||
const [dryAntibioticKeys, setDryAntibioticKeys] = useState<string[]>([]);
|
const [dryAntibioticKeys, setDryAntibioticKeys] = useState<string[]>([]);
|
||||||
const [farmerNote, setFarmerNote] = useState("");
|
const [farmerNote, setFarmerNote] = useState("");
|
||||||
const [internalNote, setInternalNote] = useState("");
|
const [internalNote, setInternalNote] = useState("");
|
||||||
|
// New fields from Lua version
|
||||||
|
const [inUdderCount, setInUdderCount] = useState("");
|
||||||
|
const [inUdderDuration, setInUdderDuration] = useState("");
|
||||||
|
const [systemicCount, setSystemicCount] = useState("");
|
||||||
|
const [systemicDuration, setSystemicDuration] = useState("");
|
||||||
|
const [systemicDosage, setSystemicDosage] = useState("");
|
||||||
|
const [systemicLocation, setSystemicLocation] = useState("");
|
||||||
|
const [startvacVaccination, setStartvacVaccination] = useState(false);
|
||||||
|
const [noAntibioticTreatment, setNoAntibioticTreatment] = useState(false);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
@@ -51,6 +66,15 @@ export default function TherapyPage() {
|
|||||||
setDryAntibioticKeys(sampleResponse.therapy?.dryAntibioticKeys ?? []);
|
setDryAntibioticKeys(sampleResponse.therapy?.dryAntibioticKeys ?? []);
|
||||||
setFarmerNote(sampleResponse.therapy?.farmerNote ?? "");
|
setFarmerNote(sampleResponse.therapy?.farmerNote ?? "");
|
||||||
setInternalNote(sampleResponse.therapy?.internalNote ?? "");
|
setInternalNote(sampleResponse.therapy?.internalNote ?? "");
|
||||||
|
// Load new fields
|
||||||
|
setInUdderCount(sampleResponse.therapy?.inUdderCount ?? "");
|
||||||
|
setInUdderDuration(sampleResponse.therapy?.inUdderDuration ?? "");
|
||||||
|
setSystemicCount(sampleResponse.therapy?.systemicCount ?? "");
|
||||||
|
setSystemicDuration(sampleResponse.therapy?.systemicDuration ?? "");
|
||||||
|
setSystemicDosage(sampleResponse.therapy?.systemicDosage ?? "");
|
||||||
|
setSystemicLocation(sampleResponse.therapy?.systemicLocation ?? "");
|
||||||
|
setStartvacVaccination(sampleResponse.therapy?.startvacVaccination ?? false);
|
||||||
|
setNoAntibioticTreatment(sampleResponse.therapy?.noAntibioticTreatment ?? false);
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
setMessage((loadError as Error).message);
|
setMessage((loadError as Error).message);
|
||||||
}
|
}
|
||||||
@@ -65,6 +89,14 @@ export default function TherapyPage() {
|
|||||||
setter(list.includes(value) ? list.filter((entry) => entry !== value) : [...list, value]);
|
setter(list.includes(value) ? list.filter((entry) => entry !== value) : [...list, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if "Keine" (None) is selected for in-udder or systemic
|
||||||
|
const noInUdderSelected = inUdderMedicationKeys.some(key =>
|
||||||
|
catalogs?.medications.find(m => m.businessKey === key)?.name === "Keine"
|
||||||
|
);
|
||||||
|
const noSystemicSelected = systemicMedicationKeys.some(key =>
|
||||||
|
catalogs?.medications.find(m => m.businessKey === key)?.name === "Keine"
|
||||||
|
);
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!sampleId) {
|
if (!sampleId) {
|
||||||
return;
|
return;
|
||||||
@@ -83,6 +115,15 @@ export default function TherapyPage() {
|
|||||||
dryAntibioticKeys,
|
dryAntibioticKeys,
|
||||||
farmerNote,
|
farmerNote,
|
||||||
internalNote,
|
internalNote,
|
||||||
|
// New fields
|
||||||
|
inUdderCount: inUdderCount || null,
|
||||||
|
inUdderDuration: inUdderDuration || null,
|
||||||
|
systemicCount: systemicCount || null,
|
||||||
|
systemicDuration: systemicDuration || null,
|
||||||
|
systemicDosage: systemicDosage || null,
|
||||||
|
systemicLocation: systemicLocation || null,
|
||||||
|
startvacVaccination,
|
||||||
|
noAntibioticTreatment,
|
||||||
});
|
});
|
||||||
setSample(response);
|
setSample(response);
|
||||||
setMessage(response.completed ? "Probe gespeichert und abgeschlossen." : "Aenderung gespeichert.");
|
setMessage(response.completed ? "Probe gespeichert und abgeschlossen." : "Aenderung gespeichert.");
|
||||||
@@ -168,7 +209,35 @@ export default function TherapyPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* In-Udder Details from Lua */}
|
||||||
|
{!noInUdderSelected && inUdderMedicationKeys.length > 0 && (
|
||||||
|
<div className="field-grid field-grid--2col section-card__spacer">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
|
<span>Anzahl</span>
|
||||||
|
<select
|
||||||
|
value={inUdderCount}
|
||||||
|
onChange={(e) => setInUdderCount(e.target.value)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{COUNT_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Dauer</span>
|
||||||
|
<select
|
||||||
|
value={inUdderDuration}
|
||||||
|
onChange={(e) => setInUdderDuration(e.target.value)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{DURATION_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="field section-card__spacer">
|
||||||
<span>Sonstiges</span>
|
<span>Sonstiges</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={inUdderOther}
|
value={inUdderOther}
|
||||||
@@ -202,7 +271,57 @@ export default function TherapyPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Systemic Details from Lua */}
|
||||||
|
{!noSystemicSelected && systemicMedicationKeys.length > 0 && (
|
||||||
|
<div className="field-grid field-grid--2col section-card__spacer">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
|
<span>Anzahl</span>
|
||||||
|
<select
|
||||||
|
value={systemicCount}
|
||||||
|
onChange={(e) => setSystemicCount(e.target.value)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{COUNT_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Dauer</span>
|
||||||
|
<select
|
||||||
|
value={systemicDuration}
|
||||||
|
onChange={(e) => setSystemicDuration(e.target.value)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{DURATION_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Dosierung</span>
|
||||||
|
<select
|
||||||
|
value={systemicDosage}
|
||||||
|
onChange={(e) => setSystemicDosage(e.target.value)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{DOSAGE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>Ort</span>
|
||||||
|
<select
|
||||||
|
value={systemicLocation}
|
||||||
|
onChange={(e) => setSystemicLocation(e.target.value)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
>
|
||||||
|
<option value="">-</option>
|
||||||
|
{LOCATION_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="field section-card__spacer">
|
||||||
<span>Sonstiges</span>
|
<span>Sonstiges</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={systemicOther}
|
value={systemicOther}
|
||||||
@@ -254,6 +373,33 @@ export default function TherapyPage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Additional Options from Lua */}
|
||||||
|
{sample.sampleKind === "LACTATION" && (
|
||||||
|
<section className="section-card">
|
||||||
|
<p className="eyebrow">Sonstiges</p>
|
||||||
|
<div className="field-grid">
|
||||||
|
<label className="field field--checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={noAntibioticTreatment}
|
||||||
|
onChange={(e) => setNoAntibioticTreatment(e.target.checked)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
/>
|
||||||
|
<span>Keine Antibiose, gut ausmelken (evtl. Oxytocin), als letzte melken, strikte Hygiene</span>
|
||||||
|
</label>
|
||||||
|
<label className="field field--checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={startvacVaccination}
|
||||||
|
onChange={(e) => setStartvacVaccination(e.target.checked)}
|
||||||
|
disabled={therapyLocked}
|
||||||
|
/>
|
||||||
|
<span>Startvac-Impfung</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="form-grid">
|
<section className="form-grid">
|
||||||
<article className="section-card">
|
<article className="section-card">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
|
|||||||
@@ -624,10 +624,17 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.matrix-button {
|
.matrix-button {
|
||||||
width: 42px;
|
min-width: 90px;
|
||||||
|
width: auto;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
|
padding: 0 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-col {
|
||||||
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eye-button {
|
.eye-button {
|
||||||
@@ -1489,3 +1496,71 @@ a {
|
|||||||
height: 72vh;
|
height: 72vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Additional styles for new Lua features */
|
||||||
|
|
||||||
|
/* Checkbox field styling */
|
||||||
|
.field--checkbox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field--checkbox input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field--checkbox span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special pathogen buttons (NO_GROWTH, CONTAMINATED) */
|
||||||
|
.pathogen-button.special-pathogen {
|
||||||
|
background: rgba(138, 101, 0, 0.1);
|
||||||
|
border: 1px solid rgba(138, 101, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pathogen-button.special-pathogen.is-selected {
|
||||||
|
background: linear-gradient(135deg, rgba(138, 101, 0, 0.25), rgba(138, 101, 0, 0.1));
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(138, 101, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Therapy detail fields */
|
||||||
|
.field-grid--2col {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.field-grid--2col {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled state for therapy when "Keine" is selected */
|
||||||
|
.choice-chip:disabled,
|
||||||
|
.field input:disabled,
|
||||||
|
.field select:disabled,
|
||||||
|
.field textarea:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pretreatment section styling */
|
||||||
|
.pretreatment-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.pretreatment-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user