Initial MUH app implementation
This commit is contained in:
59
backend/pom.xml
Normal file
59
backend/pom.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>de.svencarstensen</groupId>
|
||||
<artifactId>muh-backend</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>muh-backend</name>
|
||||
<description>MUH application backend</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.svencarstensen.muh;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MuhApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MuhApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.svencarstensen.muh.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
CorsFilter corsFilter(@Value("${muh.cors.allowed-origins}") List<String> allowedOrigins) {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(allowedOrigins);
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||
configuration.setAllowCredentials(false);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/**", configuration);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public record AntibiogramEntry(
|
||||
String antibioticBusinessKey,
|
||||
String antibioticCode,
|
||||
String antibioticName,
|
||||
SensitivityResult result
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Document("antibiotics")
|
||||
public record AntibioticCatalogItem(
|
||||
@Id String id,
|
||||
String businessKey,
|
||||
String code,
|
||||
String name,
|
||||
boolean active,
|
||||
String supersedesId,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Document("users")
|
||||
public record AppUser(
|
||||
@Id String id,
|
||||
String code,
|
||||
String displayName,
|
||||
String companyName,
|
||||
String address,
|
||||
String email,
|
||||
String portalLogin,
|
||||
String passwordHash,
|
||||
boolean active,
|
||||
UserRole role,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Document("farmers")
|
||||
public record Farmer(
|
||||
@Id String id,
|
||||
String businessKey,
|
||||
String name,
|
||||
String email,
|
||||
boolean active,
|
||||
String supersedesId,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Document("medications")
|
||||
public record MedicationCatalogItem(
|
||||
@Id String id,
|
||||
String businessKey,
|
||||
String name,
|
||||
MedicationCategory category,
|
||||
boolean active,
|
||||
String supersedesId,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum MedicationCategory {
|
||||
IN_UDDER,
|
||||
SYSTEMIC_ANTIBIOTIC,
|
||||
SYSTEMIC_PAIN,
|
||||
DRY_SEALER,
|
||||
DRY_ANTIBIOTIC
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Document("pathogens")
|
||||
public record PathogenCatalogItem(
|
||||
@Id String id,
|
||||
String businessKey,
|
||||
String code,
|
||||
String name,
|
||||
PathogenKind kind,
|
||||
boolean active,
|
||||
String supersedesId,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum PathogenKind {
|
||||
BACTERIAL,
|
||||
NO_GROWTH,
|
||||
CONTAMINATED,
|
||||
OTHER
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record QuarterAntibiogram(
|
||||
QuarterKey quarterKey,
|
||||
String pathogenBusinessKey,
|
||||
String pathogenName,
|
||||
QuarterKey inheritedFromQuarter,
|
||||
List<AntibiogramEntry> entries
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public record QuarterFinding(
|
||||
QuarterKey quarterKey,
|
||||
boolean flagged,
|
||||
String pathogenBusinessKey,
|
||||
String pathogenCode,
|
||||
String pathogenName,
|
||||
PathogenKind pathogenKind,
|
||||
String customPathogenName,
|
||||
Integer cellCount
|
||||
) {
|
||||
|
||||
public boolean requiresAntibiogram() {
|
||||
return pathogenKind == PathogenKind.BACTERIAL;
|
||||
}
|
||||
|
||||
public String effectivePathogenLabel() {
|
||||
if (customPathogenName != null && !customPathogenName.isBlank()) {
|
||||
return customPathogenName.trim();
|
||||
}
|
||||
return pathogenName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum QuarterKey {
|
||||
SINGLE("Einzelprobe"),
|
||||
UNKNOWN("Unbekannt"),
|
||||
LEFT_FRONT("Vorne links"),
|
||||
RIGHT_FRONT("Vorne rechts"),
|
||||
LEFT_REAR("Hinten links"),
|
||||
RIGHT_REAR("Hinten rechts");
|
||||
|
||||
private final String label;
|
||||
|
||||
QuarterKey(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Document("samples")
|
||||
public record Sample(
|
||||
@Id String id,
|
||||
long sampleNumber,
|
||||
String farmerBusinessKey,
|
||||
String farmerName,
|
||||
String farmerEmail,
|
||||
String cowNumber,
|
||||
String cowName,
|
||||
SampleKind sampleKind,
|
||||
SamplingMode samplingMode,
|
||||
SampleWorkflowStep currentStep,
|
||||
List<QuarterFinding> quarters,
|
||||
List<QuarterAntibiogram> antibiograms,
|
||||
TherapyRecommendation therapyRecommendation,
|
||||
boolean reportSent,
|
||||
boolean reportBlocked,
|
||||
LocalDateTime reportSentAt,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt,
|
||||
LocalDateTime completedAt,
|
||||
String createdByUserCode,
|
||||
String createdByDisplayName
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum SampleKind {
|
||||
LACTATION,
|
||||
DRY_OFF
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum SampleWorkflowStep {
|
||||
ANAMNESIS,
|
||||
ANTIBIOGRAM,
|
||||
THERAPY,
|
||||
COMPLETED
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum SamplingMode {
|
||||
SINGLE_SITE,
|
||||
FOUR_QUARTER,
|
||||
UNKNOWN_SITE
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum SensitivityResult {
|
||||
SENSITIVE,
|
||||
INTERMEDIATE,
|
||||
RESISTANT
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TherapyRecommendation(
|
||||
boolean continueStarted,
|
||||
boolean switchTherapy,
|
||||
List<String> inUdderMedicationKeys,
|
||||
List<String> inUdderMedicationNames,
|
||||
String inUdderOther,
|
||||
List<String> systemicMedicationKeys,
|
||||
List<String> systemicMedicationNames,
|
||||
String systemicOther,
|
||||
List<String> drySealerKeys,
|
||||
List<String> drySealerNames,
|
||||
List<String> dryAntibioticKeys,
|
||||
List<String> dryAntibioticNames,
|
||||
String farmerNote,
|
||||
String internalNote
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.svencarstensen.muh.domain;
|
||||
|
||||
public enum UserRole {
|
||||
APP,
|
||||
ADMIN,
|
||||
CUSTOMER
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.svencarstensen.muh.repository;
|
||||
|
||||
import de.svencarstensen.muh.domain.AntibioticCatalogItem;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AntibioticCatalogRepository extends MongoRepository<AntibioticCatalogItem, String> {
|
||||
List<AntibioticCatalogItem> findByActiveTrueOrderByNameAsc();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.svencarstensen.muh.repository;
|
||||
|
||||
import de.svencarstensen.muh.domain.AppUser;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AppUserRepository extends MongoRepository<AppUser, String> {
|
||||
List<AppUser> findByActiveTrueOrderByDisplayNameAsc();
|
||||
|
||||
Optional<AppUser> findByCodeIgnoreCase(String code);
|
||||
|
||||
Optional<AppUser> findByEmailIgnoreCase(String email);
|
||||
|
||||
Optional<AppUser> findByPortalLoginIgnoreCase(String portalLogin);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.svencarstensen.muh.repository;
|
||||
|
||||
import de.svencarstensen.muh.domain.Farmer;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface FarmerRepository extends MongoRepository<Farmer, String> {
|
||||
List<Farmer> findByActiveTrueOrderByNameAsc();
|
||||
|
||||
List<Farmer> findByNameContainingIgnoreCaseOrderByNameAsc(String name);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.svencarstensen.muh.repository;
|
||||
|
||||
import de.svencarstensen.muh.domain.MedicationCatalogItem;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MedicationCatalogRepository extends MongoRepository<MedicationCatalogItem, String> {
|
||||
List<MedicationCatalogItem> findByActiveTrueOrderByNameAsc();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.svencarstensen.muh.repository;
|
||||
|
||||
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PathogenCatalogRepository extends MongoRepository<PathogenCatalogItem, String> {
|
||||
List<PathogenCatalogItem> findByActiveTrueOrderByNameAsc();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.svencarstensen.muh.repository;
|
||||
|
||||
import de.svencarstensen.muh.domain.Sample;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SampleRepository extends MongoRepository<Sample, String> {
|
||||
Optional<Sample> findBySampleNumber(long sampleNumber);
|
||||
|
||||
Optional<Sample> findTopByOrderBySampleNumberDesc();
|
||||
|
||||
List<Sample> findTop12ByOrderByUpdatedAtDesc();
|
||||
|
||||
List<Sample> findByFarmerBusinessKeyOrderByCreatedAtDesc(String farmerBusinessKey);
|
||||
|
||||
List<Sample> findByCompletedAtBetweenOrderByCompletedAtDesc(LocalDateTime start, LocalDateTime end);
|
||||
|
||||
List<Sample> findByCompletedAtNotNullOrderByCompletedAtDesc();
|
||||
}
|
||||
@@ -0,0 +1,906 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.AntibioticCatalogItem;
|
||||
import de.svencarstensen.muh.domain.AppUser;
|
||||
import de.svencarstensen.muh.domain.Farmer;
|
||||
import de.svencarstensen.muh.domain.MedicationCatalogItem;
|
||||
import de.svencarstensen.muh.domain.MedicationCategory;
|
||||
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
||||
import de.svencarstensen.muh.domain.PathogenKind;
|
||||
import de.svencarstensen.muh.domain.UserRole;
|
||||
import de.svencarstensen.muh.repository.AntibioticCatalogRepository;
|
||||
import de.svencarstensen.muh.repository.AppUserRepository;
|
||||
import de.svencarstensen.muh.repository.FarmerRepository;
|
||||
import de.svencarstensen.muh.repository.MedicationCatalogRepository;
|
||||
import de.svencarstensen.muh.repository.PathogenCatalogRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class CatalogService {
|
||||
|
||||
private static final Comparator<FarmerRow> FARMER_ROW_COMPARATOR = Comparator
|
||||
.comparing(FarmerRow::active).reversed()
|
||||
.thenComparing(FarmerRow::name, String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparing(FarmerRow::updatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
|
||||
|
||||
private static final Comparator<MedicationRow> MEDICATION_ROW_COMPARATOR = Comparator
|
||||
.comparing(MedicationRow::active).reversed()
|
||||
.thenComparing(MedicationRow::category)
|
||||
.thenComparing(MedicationRow::name, String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private static final Comparator<PathogenRow> PATHOGEN_ROW_COMPARATOR = Comparator
|
||||
.comparing(PathogenRow::active).reversed()
|
||||
.thenComparing(PathogenRow::name, String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparing(PathogenRow::code, String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private static final Comparator<AntibioticRow> ANTIBIOTIC_ROW_COMPARATOR = Comparator
|
||||
.comparing(AntibioticRow::active).reversed()
|
||||
.thenComparing(AntibioticRow::name, String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparing(AntibioticRow::code, String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private final FarmerRepository farmerRepository;
|
||||
private final MedicationCatalogRepository medicationRepository;
|
||||
private final PathogenCatalogRepository pathogenRepository;
|
||||
private final AntibioticCatalogRepository antibioticRepository;
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
|
||||
public CatalogService(
|
||||
FarmerRepository farmerRepository,
|
||||
MedicationCatalogRepository medicationRepository,
|
||||
PathogenCatalogRepository pathogenRepository,
|
||||
AntibioticCatalogRepository antibioticRepository,
|
||||
AppUserRepository appUserRepository
|
||||
) {
|
||||
this.farmerRepository = farmerRepository;
|
||||
this.medicationRepository = medicationRepository;
|
||||
this.pathogenRepository = pathogenRepository;
|
||||
this.antibioticRepository = antibioticRepository;
|
||||
this.appUserRepository = appUserRepository;
|
||||
}
|
||||
|
||||
public ActiveCatalogSummary activeCatalogSummary() {
|
||||
return new ActiveCatalogSummary(
|
||||
farmerRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toFarmerOption).toList(),
|
||||
medicationRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toMedicationOption).toList(),
|
||||
pathogenRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toPathogenOption).toList(),
|
||||
antibioticRepository.findByActiveTrueOrderByNameAsc().stream().map(this::toAntibioticOption).toList(),
|
||||
activeQuickLoginUsers().stream().map(this::toUserOption).toList()
|
||||
);
|
||||
}
|
||||
|
||||
public AdministrationOverview administrationOverview() {
|
||||
return new AdministrationOverview(listFarmerRows(), listMedicationRows(), listPathogenRows(), listAntibioticRows());
|
||||
}
|
||||
|
||||
public List<FarmerRow> listFarmerRows() {
|
||||
return farmerRepository.findAll().stream()
|
||||
.map(this::toFarmerRow)
|
||||
.sorted(FARMER_ROW_COMPARATOR)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<MedicationRow> listMedicationRows() {
|
||||
return medicationRepository.findAll().stream()
|
||||
.map(this::toMedicationRow)
|
||||
.sorted(MEDICATION_ROW_COMPARATOR)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<PathogenRow> listPathogenRows() {
|
||||
return pathogenRepository.findAll().stream()
|
||||
.map(this::toPathogenRow)
|
||||
.sorted(PATHOGEN_ROW_COMPARATOR)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<AntibioticRow> listAntibioticRows() {
|
||||
return antibioticRepository.findAll().stream()
|
||||
.map(this::toAntibioticRow)
|
||||
.sorted(ANTIBIOTIC_ROW_COMPARATOR)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<FarmerRow> saveFarmers(List<FarmerMutation> mutations) {
|
||||
for (FarmerMutation mutation : mutations) {
|
||||
if (isBlank(mutation.name())) {
|
||||
continue;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (isBlank(mutation.id())) {
|
||||
farmerRepository.save(new Farmer(
|
||||
null,
|
||||
UUID.randomUUID().toString(),
|
||||
mutation.name().trim(),
|
||||
blankToNull(mutation.email()),
|
||||
mutation.active(),
|
||||
null,
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
String mutationId = requireText(mutation.id(), "Landwirt-ID fehlt");
|
||||
Farmer existing = farmerRepository.findById(mutationId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Landwirt nicht gefunden"));
|
||||
boolean changed = !existing.name().equals(mutation.name().trim())
|
||||
|| !safeEquals(existing.email(), blankToNull(mutation.email()));
|
||||
if (changed) {
|
||||
farmerRepository.save(new Farmer(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.name(),
|
||||
existing.email(),
|
||||
false,
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
farmerRepository.save(new Farmer(
|
||||
null,
|
||||
existing.businessKey(),
|
||||
mutation.name().trim(),
|
||||
blankToNull(mutation.email()),
|
||||
mutation.active(),
|
||||
existing.id(),
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (existing.active() != mutation.active()) {
|
||||
farmerRepository.save(new Farmer(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.name(),
|
||||
existing.email(),
|
||||
mutation.active(),
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
}
|
||||
}
|
||||
return listFarmerRows();
|
||||
}
|
||||
|
||||
public List<MedicationRow> saveMedications(List<MedicationMutation> mutations) {
|
||||
for (MedicationMutation mutation : mutations) {
|
||||
if (isBlank(mutation.name()) || mutation.category() == null) {
|
||||
continue;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (isBlank(mutation.id())) {
|
||||
medicationRepository.save(new MedicationCatalogItem(
|
||||
null,
|
||||
UUID.randomUUID().toString(),
|
||||
mutation.name().trim(),
|
||||
mutation.category(),
|
||||
mutation.active(),
|
||||
null,
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
String mutationId = requireText(mutation.id(), "Medikament-ID fehlt");
|
||||
MedicationCatalogItem existing = medicationRepository.findById(mutationId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Medikament nicht gefunden"));
|
||||
boolean changed = !existing.name().equals(mutation.name().trim())
|
||||
|| existing.category() != mutation.category();
|
||||
if (changed) {
|
||||
medicationRepository.save(new MedicationCatalogItem(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.name(),
|
||||
existing.category(),
|
||||
false,
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
medicationRepository.save(new MedicationCatalogItem(
|
||||
null,
|
||||
existing.businessKey(),
|
||||
mutation.name().trim(),
|
||||
mutation.category(),
|
||||
mutation.active(),
|
||||
existing.id(),
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (existing.active() != mutation.active()) {
|
||||
medicationRepository.save(new MedicationCatalogItem(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.name(),
|
||||
existing.category(),
|
||||
mutation.active(),
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
}
|
||||
}
|
||||
return listMedicationRows();
|
||||
}
|
||||
|
||||
public List<PathogenRow> savePathogens(List<PathogenMutation> mutations) {
|
||||
for (PathogenMutation mutation : mutations) {
|
||||
if (isBlank(mutation.name()) || mutation.kind() == null) {
|
||||
continue;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (isBlank(mutation.id())) {
|
||||
pathogenRepository.save(new PathogenCatalogItem(
|
||||
null,
|
||||
UUID.randomUUID().toString(),
|
||||
blankToNull(mutation.code()),
|
||||
mutation.name().trim(),
|
||||
mutation.kind(),
|
||||
mutation.active(),
|
||||
null,
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
String mutationId = requireText(mutation.id(), "Erreger-ID fehlt");
|
||||
PathogenCatalogItem existing = pathogenRepository.findById(mutationId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Erreger nicht gefunden"));
|
||||
boolean changed = !existing.name().equals(mutation.name().trim())
|
||||
|| !safeEquals(existing.code(), blankToNull(mutation.code()))
|
||||
|| existing.kind() != mutation.kind();
|
||||
if (changed) {
|
||||
pathogenRepository.save(new PathogenCatalogItem(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.code(),
|
||||
existing.name(),
|
||||
existing.kind(),
|
||||
false,
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
pathogenRepository.save(new PathogenCatalogItem(
|
||||
null,
|
||||
existing.businessKey(),
|
||||
blankToNull(mutation.code()),
|
||||
mutation.name().trim(),
|
||||
mutation.kind(),
|
||||
mutation.active(),
|
||||
existing.id(),
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (existing.active() != mutation.active()) {
|
||||
pathogenRepository.save(new PathogenCatalogItem(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.code(),
|
||||
existing.name(),
|
||||
existing.kind(),
|
||||
mutation.active(),
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
}
|
||||
}
|
||||
return listPathogenRows();
|
||||
}
|
||||
|
||||
public List<AntibioticRow> saveAntibiotics(List<AntibioticMutation> mutations) {
|
||||
for (AntibioticMutation mutation : mutations) {
|
||||
if (isBlank(mutation.name())) {
|
||||
continue;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (isBlank(mutation.id())) {
|
||||
antibioticRepository.save(new AntibioticCatalogItem(
|
||||
null,
|
||||
UUID.randomUUID().toString(),
|
||||
blankToNull(mutation.code()),
|
||||
mutation.name().trim(),
|
||||
mutation.active(),
|
||||
null,
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
String mutationId = requireText(mutation.id(), "Antibiotika-ID fehlt");
|
||||
AntibioticCatalogItem existing = antibioticRepository.findById(mutationId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Antibiotikum nicht gefunden"));
|
||||
boolean changed = !existing.name().equals(mutation.name().trim())
|
||||
|| !safeEquals(existing.code(), blankToNull(mutation.code()));
|
||||
if (changed) {
|
||||
antibioticRepository.save(new AntibioticCatalogItem(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.code(),
|
||||
existing.name(),
|
||||
false,
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
antibioticRepository.save(new AntibioticCatalogItem(
|
||||
null,
|
||||
existing.businessKey(),
|
||||
blankToNull(mutation.code()),
|
||||
mutation.name().trim(),
|
||||
mutation.active(),
|
||||
existing.id(),
|
||||
now,
|
||||
now
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (existing.active() != mutation.active()) {
|
||||
antibioticRepository.save(new AntibioticCatalogItem(
|
||||
existing.id(),
|
||||
existing.businessKey(),
|
||||
existing.code(),
|
||||
existing.name(),
|
||||
mutation.active(),
|
||||
existing.supersedesId(),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
}
|
||||
}
|
||||
return listAntibioticRows();
|
||||
}
|
||||
|
||||
public List<UserRow> listUsers() {
|
||||
ensureDefaultUsers();
|
||||
return appUserRepository.findAll().stream()
|
||||
.map(this::toUserRow)
|
||||
.sorted(Comparator.comparing(UserRow::active).reversed().thenComparing(UserRow::displayName, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public UserRow createOrUpdateUser(UserMutation mutation) {
|
||||
if (isBlank(mutation.displayName()) || isBlank(mutation.code())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Benutzername und Kürzel sind erforderlich");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
validateUserMutation(mutation);
|
||||
if (isBlank(mutation.id())) {
|
||||
AppUser created = appUserRepository.save(new AppUser(
|
||||
null,
|
||||
mutation.code().trim().toUpperCase(),
|
||||
mutation.displayName().trim(),
|
||||
blankToNull(mutation.companyName()),
|
||||
blankToNull(mutation.address()),
|
||||
normalizeEmail(mutation.email()),
|
||||
blankToNull(mutation.portalLogin()),
|
||||
encodeIfPresent(mutation.password()),
|
||||
mutation.active(),
|
||||
Optional.ofNullable(mutation.role()).orElse(UserRole.APP),
|
||||
now,
|
||||
now
|
||||
));
|
||||
return toUserRow(created);
|
||||
}
|
||||
String mutationId = requireText(mutation.id(), "Benutzer-ID fehlt");
|
||||
AppUser existing = appUserRepository.findById(mutationId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Benutzer nicht gefunden"));
|
||||
AppUser saved = appUserRepository.save(new AppUser(
|
||||
existing.id(),
|
||||
mutation.code().trim().toUpperCase(),
|
||||
mutation.displayName().trim(),
|
||||
blankToNull(mutation.companyName()),
|
||||
blankToNull(mutation.address()),
|
||||
normalizeEmail(mutation.email()),
|
||||
blankToNull(mutation.portalLogin()),
|
||||
isBlank(mutation.password()) ? existing.passwordHash() : passwordEncoder.encode(mutation.password()),
|
||||
mutation.active(),
|
||||
Optional.ofNullable(mutation.role()).orElse(existing.role()),
|
||||
existing.createdAt(),
|
||||
now
|
||||
));
|
||||
return toUserRow(saved);
|
||||
}
|
||||
|
||||
public void deleteUser(String id) {
|
||||
appUserRepository.deleteById(requireText(id, "Benutzer-ID fehlt"));
|
||||
}
|
||||
|
||||
public void changePassword(String id, String newPassword) {
|
||||
if (isBlank(newPassword)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Passwort darf nicht leer sein");
|
||||
}
|
||||
AppUser existing = appUserRepository.findById(requireText(id, "Benutzer-ID fehlt"))
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Benutzer nicht gefunden"));
|
||||
appUserRepository.save(new AppUser(
|
||||
existing.id(),
|
||||
existing.code(),
|
||||
existing.displayName(),
|
||||
existing.companyName(),
|
||||
existing.address(),
|
||||
existing.email(),
|
||||
existing.portalLogin(),
|
||||
passwordEncoder.encode(newPassword),
|
||||
existing.active(),
|
||||
existing.role(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now()
|
||||
));
|
||||
}
|
||||
|
||||
public UserOption loginByCode(String code) {
|
||||
AppUser user = activeQuickLoginUsers().stream()
|
||||
.filter(candidate -> candidate.code().equalsIgnoreCase(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Benutzerkürzel unbekannt"));
|
||||
return toUserOption(user);
|
||||
}
|
||||
|
||||
public UserOption loginWithPassword(String identifier, String password) {
|
||||
if (isBlank(identifier) || isBlank(password)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Benutzername/E-Mail und Passwort sind erforderlich");
|
||||
}
|
||||
|
||||
AppUser user = resolvePasswordUser(identifier.trim())
|
||||
.filter(AppUser::active)
|
||||
.filter(candidate -> candidate.passwordHash() != null && passwordEncoder.matches(password, candidate.passwordHash()))
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Anmeldung fehlgeschlagen"));
|
||||
return toUserOption(user);
|
||||
}
|
||||
|
||||
public UserOption registerCustomer(RegistrationMutation mutation) {
|
||||
if (isBlank(mutation.companyName()) || isBlank(mutation.address()) || isBlank(mutation.email()) || isBlank(mutation.password())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Firmenname, Adresse, E-Mail und Passwort sind erforderlich");
|
||||
}
|
||||
|
||||
String normalizedEmail = normalizeEmail(mutation.email());
|
||||
if (appUserRepository.findByEmailIgnoreCase(normalizedEmail).isPresent()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Diese E-Mail-Adresse ist bereits registriert");
|
||||
}
|
||||
|
||||
String companyName = mutation.companyName().trim();
|
||||
String address = mutation.address().trim();
|
||||
String displayName = companyName;
|
||||
String portalLogin = generateUniquePortalLogin(localPart(normalizedEmail));
|
||||
String code = generateUniqueCode("K" + companyName);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
AppUser created = appUserRepository.save(new AppUser(
|
||||
null,
|
||||
code,
|
||||
displayName,
|
||||
companyName,
|
||||
address,
|
||||
normalizedEmail,
|
||||
portalLogin,
|
||||
passwordEncoder.encode(mutation.password()),
|
||||
true,
|
||||
UserRole.CUSTOMER,
|
||||
now,
|
||||
now
|
||||
));
|
||||
return toUserOption(created);
|
||||
}
|
||||
|
||||
private List<AppUser> activeUsers() {
|
||||
ensureDefaultUsers();
|
||||
return appUserRepository.findByActiveTrueOrderByDisplayNameAsc();
|
||||
}
|
||||
|
||||
private List<AppUser> activeQuickLoginUsers() {
|
||||
return activeUsers().stream()
|
||||
.filter(user -> user.role() != UserRole.CUSTOMER)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void ensureDefaultUsers() {
|
||||
ensureDefaultUser("ADM", "Administrator", "admin@muh.local", "admin", "Admin123!", UserRole.ADMIN);
|
||||
ensureDefaultUser("SV", "Sven", "sven@muh.local", "sven", "muh123", UserRole.APP);
|
||||
ensureDefaultUser("AK", "Anna", "anna@muh.local", "anna", "muh123", UserRole.APP);
|
||||
ensureDefaultUser("LH", "Lena", "lena@muh.local", "lena", "muh123", UserRole.APP);
|
||||
}
|
||||
|
||||
public Farmer requireActiveFarmer(String businessKey) {
|
||||
return farmerRepository.findByActiveTrueOrderByNameAsc().stream()
|
||||
.filter(farmer -> farmer.businessKey().equals(businessKey))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
|
||||
}
|
||||
|
||||
public Map<String, PathogenCatalogItem> activePathogensByBusinessKey() {
|
||||
return pathogenRepository.findByActiveTrueOrderByNameAsc().stream()
|
||||
.collect(Collectors.toMap(PathogenCatalogItem::businessKey, Function.identity()));
|
||||
}
|
||||
|
||||
public Map<String, AntibioticCatalogItem> activeAntibioticsByBusinessKey() {
|
||||
return antibioticRepository.findByActiveTrueOrderByNameAsc().stream()
|
||||
.collect(Collectors.toMap(AntibioticCatalogItem::businessKey, Function.identity()));
|
||||
}
|
||||
|
||||
public Map<String, MedicationCatalogItem> activeMedicationsByBusinessKey() {
|
||||
return medicationRepository.findByActiveTrueOrderByNameAsc().stream()
|
||||
.collect(Collectors.toMap(MedicationCatalogItem::businessKey, Function.identity()));
|
||||
}
|
||||
|
||||
private FarmerRow toFarmerRow(Farmer farmer) {
|
||||
return new FarmerRow(
|
||||
farmer.id(),
|
||||
farmer.businessKey(),
|
||||
farmer.name(),
|
||||
farmer.email(),
|
||||
farmer.active(),
|
||||
farmer.updatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private MedicationRow toMedicationRow(MedicationCatalogItem item) {
|
||||
return new MedicationRow(
|
||||
item.id(),
|
||||
item.businessKey(),
|
||||
item.name(),
|
||||
item.category(),
|
||||
item.active(),
|
||||
item.updatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private PathogenRow toPathogenRow(PathogenCatalogItem item) {
|
||||
return new PathogenRow(
|
||||
item.id(),
|
||||
item.businessKey(),
|
||||
item.code(),
|
||||
item.name(),
|
||||
item.kind(),
|
||||
item.active(),
|
||||
item.updatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private AntibioticRow toAntibioticRow(AntibioticCatalogItem item) {
|
||||
return new AntibioticRow(
|
||||
item.id(),
|
||||
item.businessKey(),
|
||||
item.code(),
|
||||
item.name(),
|
||||
item.active(),
|
||||
item.updatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private UserRow toUserRow(AppUser user) {
|
||||
return new UserRow(
|
||||
user.id(),
|
||||
user.code(),
|
||||
user.displayName(),
|
||||
user.companyName(),
|
||||
user.address(),
|
||||
user.email(),
|
||||
user.portalLogin(),
|
||||
user.active(),
|
||||
user.role(),
|
||||
user.updatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private FarmerOption toFarmerOption(Farmer farmer) {
|
||||
return new FarmerOption(farmer.businessKey(), farmer.name(), farmer.email());
|
||||
}
|
||||
|
||||
private MedicationOption toMedicationOption(MedicationCatalogItem item) {
|
||||
return new MedicationOption(item.businessKey(), item.name(), item.category());
|
||||
}
|
||||
|
||||
private PathogenOption toPathogenOption(PathogenCatalogItem item) {
|
||||
return new PathogenOption(item.businessKey(), item.code(), item.name(), item.kind());
|
||||
}
|
||||
|
||||
private AntibioticOption toAntibioticOption(AntibioticCatalogItem item) {
|
||||
return new AntibioticOption(item.businessKey(), item.code(), item.name());
|
||||
}
|
||||
|
||||
private UserOption toUserOption(AppUser user) {
|
||||
return new UserOption(
|
||||
user.id(),
|
||||
user.code(),
|
||||
user.displayName(),
|
||||
user.companyName(),
|
||||
user.address(),
|
||||
user.email(),
|
||||
user.portalLogin(),
|
||||
user.role()
|
||||
);
|
||||
}
|
||||
|
||||
private String encodeIfPresent(String password) {
|
||||
return isBlank(password) ? null : passwordEncoder.encode(password);
|
||||
}
|
||||
|
||||
private @NonNull String requireText(String value, String message) {
|
||||
if (isBlank(value)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
String sanitized = Objects.requireNonNull(value).trim();
|
||||
return Objects.requireNonNull(sanitized);
|
||||
}
|
||||
|
||||
private void validateUserMutation(UserMutation mutation) {
|
||||
String normalizedEmail = normalizeEmail(mutation.email());
|
||||
String normalizedLogin = blankToNull(mutation.portalLogin());
|
||||
String normalizedCode = mutation.code().trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
appUserRepository.findAll().forEach(existing -> {
|
||||
if (!safeEquals(existing.id(), blankToNull(mutation.id()))
|
||||
&& existing.code() != null
|
||||
&& existing.code().equalsIgnoreCase(normalizedCode)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Dieses Kürzel ist bereits vergeben");
|
||||
}
|
||||
if (normalizedEmail != null
|
||||
&& existing.email() != null
|
||||
&& !safeEquals(existing.id(), blankToNull(mutation.id()))
|
||||
&& existing.email().equalsIgnoreCase(normalizedEmail)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Diese E-Mail-Adresse ist bereits vergeben");
|
||||
}
|
||||
if (normalizedLogin != null
|
||||
&& existing.portalLogin() != null
|
||||
&& !safeEquals(existing.id(), blankToNull(mutation.id()))
|
||||
&& existing.portalLogin().equalsIgnoreCase(normalizedLogin)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Dieser Benutzername ist bereits vergeben");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
return isBlank(value) ? null : value.trim();
|
||||
}
|
||||
|
||||
private boolean safeEquals(String left, String right) {
|
||||
return left == null ? right == null : left.equals(right);
|
||||
}
|
||||
|
||||
private Optional<AppUser> resolvePasswordUser(String identifier) {
|
||||
return appUserRepository.findByEmailIgnoreCase(identifier)
|
||||
.or(() -> appUserRepository.findByPortalLoginIgnoreCase(identifier));
|
||||
}
|
||||
|
||||
private void ensureDefaultUser(
|
||||
String code,
|
||||
String displayName,
|
||||
String email,
|
||||
String portalLogin,
|
||||
String rawPassword,
|
||||
UserRole role
|
||||
) {
|
||||
boolean exists = appUserRepository.findByCodeIgnoreCase(code).isPresent()
|
||||
|| appUserRepository.findByEmailIgnoreCase(email).isPresent()
|
||||
|| appUserRepository.findByPortalLoginIgnoreCase(portalLogin).isPresent();
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
appUserRepository.save(new AppUser(
|
||||
null,
|
||||
code,
|
||||
displayName,
|
||||
null,
|
||||
null,
|
||||
email,
|
||||
portalLogin,
|
||||
passwordEncoder.encode(rawPassword),
|
||||
true,
|
||||
role,
|
||||
now,
|
||||
now
|
||||
));
|
||||
}
|
||||
|
||||
private String normalizeEmail(String email) {
|
||||
return isBlank(email) ? null : email.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String localPart(String email) {
|
||||
int separator = email.indexOf('@');
|
||||
return separator >= 0 ? email.substring(0, separator) : email;
|
||||
}
|
||||
|
||||
private String generateUniquePortalLogin(String seed) {
|
||||
String base = seed.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]", "");
|
||||
if (base.isBlank()) {
|
||||
base = "user";
|
||||
}
|
||||
|
||||
String candidate = base;
|
||||
int index = 2;
|
||||
while (appUserRepository.findByPortalLoginIgnoreCase(candidate).isPresent()) {
|
||||
candidate = base + index++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String generateUniqueCode(String seed) {
|
||||
String compact = seed.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
|
||||
if (compact.isBlank()) {
|
||||
compact = "USR";
|
||||
}
|
||||
|
||||
String base = compact.length() >= 3 ? compact.substring(0, Math.min(4, compact.length())) : (compact + "XXX").substring(0, 3);
|
||||
Set<String> usedCodes = appUserRepository.findAll().stream()
|
||||
.map(AppUser::code)
|
||||
.filter(code -> code != null && !code.isBlank())
|
||||
.map(code -> code.toUpperCase(Locale.ROOT))
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
|
||||
String candidate = base.length() > 3 ? base.substring(0, 3) : base;
|
||||
if (!usedCodes.contains(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
String prefix = candidate.substring(0, Math.min(2, candidate.length()));
|
||||
int index = 1;
|
||||
while (true) {
|
||||
String numbered = (prefix + index).toUpperCase(Locale.ROOT);
|
||||
if (!usedCodes.contains(numbered)) {
|
||||
return numbered;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
public record ActiveCatalogSummary(
|
||||
List<FarmerOption> farmers,
|
||||
List<MedicationOption> medications,
|
||||
List<PathogenOption> pathogens,
|
||||
List<AntibioticOption> antibiotics,
|
||||
List<UserOption> users
|
||||
) {
|
||||
}
|
||||
|
||||
public record AdministrationOverview(
|
||||
List<FarmerRow> farmers,
|
||||
List<MedicationRow> medications,
|
||||
List<PathogenRow> pathogens,
|
||||
List<AntibioticRow> antibiotics
|
||||
) {
|
||||
}
|
||||
|
||||
public record FarmerOption(String businessKey, String name, String email) {
|
||||
}
|
||||
|
||||
public record MedicationOption(String businessKey, String name, MedicationCategory category) {
|
||||
}
|
||||
|
||||
public record PathogenOption(String businessKey, String code, String name, PathogenKind kind) {
|
||||
}
|
||||
|
||||
public record AntibioticOption(String businessKey, String code, String name) {
|
||||
}
|
||||
|
||||
public record UserOption(
|
||||
String id,
|
||||
String code,
|
||||
String displayName,
|
||||
String companyName,
|
||||
String address,
|
||||
String email,
|
||||
String portalLogin,
|
||||
UserRole role
|
||||
) {
|
||||
}
|
||||
|
||||
public record FarmerRow(
|
||||
String id,
|
||||
String businessKey,
|
||||
String name,
|
||||
String email,
|
||||
boolean active,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record FarmerMutation(String id, String name, String email, boolean active) {
|
||||
}
|
||||
|
||||
public record MedicationRow(
|
||||
String id,
|
||||
String businessKey,
|
||||
String name,
|
||||
MedicationCategory category,
|
||||
boolean active,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record MedicationMutation(String id, String name, MedicationCategory category, boolean active) {
|
||||
}
|
||||
|
||||
public record PathogenRow(
|
||||
String id,
|
||||
String businessKey,
|
||||
String code,
|
||||
String name,
|
||||
PathogenKind kind,
|
||||
boolean active,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record PathogenMutation(String id, String code, String name, PathogenKind kind, boolean active) {
|
||||
}
|
||||
|
||||
public record AntibioticRow(
|
||||
String id,
|
||||
String businessKey,
|
||||
String code,
|
||||
String name,
|
||||
boolean active,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record AntibioticMutation(String id, String code, String name, boolean active) {
|
||||
}
|
||||
|
||||
public record UserRow(
|
||||
String id,
|
||||
String code,
|
||||
String displayName,
|
||||
String companyName,
|
||||
String address,
|
||||
String email,
|
||||
String portalLogin,
|
||||
boolean active,
|
||||
UserRole role,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record UserMutation(
|
||||
String id,
|
||||
String code,
|
||||
String displayName,
|
||||
String companyName,
|
||||
String address,
|
||||
String email,
|
||||
String portalLogin,
|
||||
String password,
|
||||
boolean active,
|
||||
UserRole role
|
||||
) {
|
||||
}
|
||||
|
||||
public record RegistrationMutation(
|
||||
String companyName,
|
||||
String address,
|
||||
String email,
|
||||
String password
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.AntibioticCatalogItem;
|
||||
import de.svencarstensen.muh.domain.Farmer;
|
||||
import de.svencarstensen.muh.domain.MedicationCatalogItem;
|
||||
import de.svencarstensen.muh.domain.MedicationCategory;
|
||||
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
||||
import de.svencarstensen.muh.domain.PathogenKind;
|
||||
import de.svencarstensen.muh.repository.AntibioticCatalogRepository;
|
||||
import de.svencarstensen.muh.repository.FarmerRepository;
|
||||
import de.svencarstensen.muh.repository.MedicationCatalogRepository;
|
||||
import de.svencarstensen.muh.repository.PathogenCatalogRepository;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class DemoDataInitializer implements ApplicationRunner {
|
||||
|
||||
private final FarmerRepository farmerRepository;
|
||||
private final MedicationCatalogRepository medicationRepository;
|
||||
private final PathogenCatalogRepository pathogenRepository;
|
||||
private final AntibioticCatalogRepository antibioticRepository;
|
||||
private final CatalogService catalogService;
|
||||
|
||||
public DemoDataInitializer(
|
||||
FarmerRepository farmerRepository,
|
||||
MedicationCatalogRepository medicationRepository,
|
||||
PathogenCatalogRepository pathogenRepository,
|
||||
AntibioticCatalogRepository antibioticRepository,
|
||||
CatalogService catalogService
|
||||
) {
|
||||
this.farmerRepository = farmerRepository;
|
||||
this.medicationRepository = medicationRepository;
|
||||
this.pathogenRepository = pathogenRepository;
|
||||
this.antibioticRepository = antibioticRepository;
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (farmerRepository.count() == 0) {
|
||||
farmerRepository.save(new Farmer(null, UUID.randomUUID().toString(), "Hof Hansen", "hansen@example.com", true, null, now, now));
|
||||
farmerRepository.save(new Farmer(null, UUID.randomUUID().toString(), "Agrar Lindenblick", "lindenblick@example.com", true, null, now, now));
|
||||
farmerRepository.save(new Farmer(null, UUID.randomUUID().toString(), "Gut Westerkamp", "westerkamp@example.com", true, null, now, now));
|
||||
}
|
||||
|
||||
if (medicationRepository.count() == 0) {
|
||||
medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Mastijet", MedicationCategory.IN_UDDER, true, null, now, now));
|
||||
medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Metacam", MedicationCategory.SYSTEMIC_PAIN, true, null, now, now));
|
||||
medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Cobactan", MedicationCategory.SYSTEMIC_ANTIBIOTIC, true, null, now, now));
|
||||
medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Orbeseal", MedicationCategory.DRY_SEALER, true, null, now, now));
|
||||
medicationRepository.save(new MedicationCatalogItem(null, UUID.randomUUID().toString(), "Nafpenzal", MedicationCategory.DRY_ANTIBIOTIC, true, null, now, now));
|
||||
}
|
||||
|
||||
if (pathogenRepository.count() == 0) {
|
||||
pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "SAU", "Staph. aureus", PathogenKind.BACTERIAL, true, null, now, now));
|
||||
pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "ECO", "E. coli", PathogenKind.BACTERIAL, true, null, now, now));
|
||||
pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "NG", "Kein Wachstum", PathogenKind.NO_GROWTH, true, null, now, now));
|
||||
pathogenRepository.save(new PathogenCatalogItem(null, UUID.randomUUID().toString(), "VER", "Verunreinigt", PathogenKind.CONTAMINATED, true, null, now, now));
|
||||
}
|
||||
|
||||
if (antibioticRepository.count() == 0) {
|
||||
antibioticRepository.save(new AntibioticCatalogItem(null, UUID.randomUUID().toString(), "PEN", "Penicillin", true, null, now, now));
|
||||
antibioticRepository.save(new AntibioticCatalogItem(null, UUID.randomUUID().toString(), "CEF", "Cefalexin", true, null, now, now));
|
||||
antibioticRepository.save(new AntibioticCatalogItem(null, UUID.randomUUID().toString(), "ENR", "Enrofloxacin", true, null, now, now));
|
||||
}
|
||||
|
||||
catalogService.ensureDefaultUsers();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.Sample;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Service
|
||||
public class PortalService {
|
||||
|
||||
private final SampleService sampleService;
|
||||
private final ReportService reportService;
|
||||
private final CatalogService catalogService;
|
||||
|
||||
public PortalService(SampleService sampleService, ReportService reportService, CatalogService catalogService) {
|
||||
this.sampleService = sampleService;
|
||||
this.reportService = reportService;
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
public PortalSnapshot snapshot(String farmerBusinessKey, String farmerQuery, String cowQuery, Long sampleNumber, LocalDate date) {
|
||||
List<CatalogService.FarmerOption> matchingFarmers = catalogService.activeCatalogSummary().farmers().stream()
|
||||
.filter(farmer -> farmerQuery == null || farmerQuery.isBlank() || farmer.name().toLowerCase(Locale.ROOT).contains(farmerQuery.toLowerCase(Locale.ROOT)))
|
||||
.toList();
|
||||
|
||||
List<PortalSampleRow> sampleRows;
|
||||
if (sampleNumber != null) {
|
||||
sampleRows = List.of(toPortalRow(sampleService.getSampleByNumber(sampleNumber)));
|
||||
} else if (farmerBusinessKey != null && !farmerBusinessKey.isBlank()) {
|
||||
sampleRows = sampleService.samplesByFarmerBusinessKey(farmerBusinessKey).stream()
|
||||
.filter(sample -> cowQuery == null || cowQuery.isBlank() || cowMatches(sample, cowQuery))
|
||||
.map(this::toPortalRow)
|
||||
.sorted(Comparator.comparing(PortalSampleRow::createdAt).reversed())
|
||||
.toList();
|
||||
} else if (date != null) {
|
||||
sampleRows = sampleService.samplesByDate(date).stream()
|
||||
.map(this::toPortalRow)
|
||||
.sorted(Comparator.comparing(PortalSampleRow::completedAt, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||
.toList();
|
||||
} else {
|
||||
sampleRows = sampleService.completedSamples().stream()
|
||||
.limit(25)
|
||||
.map(this::toPortalRow)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return new PortalSnapshot(
|
||||
matchingFarmers,
|
||||
sampleRows,
|
||||
reportService.reportCandidates(),
|
||||
catalogService.listUsers()
|
||||
);
|
||||
}
|
||||
|
||||
private boolean cowMatches(Sample sample, String cowQuery) {
|
||||
String query = cowQuery.toLowerCase(Locale.ROOT);
|
||||
return (sample.cowNumber() != null && sample.cowNumber().toLowerCase(Locale.ROOT).contains(query))
|
||||
|| (sample.cowName() != null && sample.cowName().toLowerCase(Locale.ROOT).contains(query));
|
||||
}
|
||||
|
||||
private PortalSampleRow toPortalRow(Sample sample) {
|
||||
return new PortalSampleRow(
|
||||
sample.id(),
|
||||
sample.sampleNumber(),
|
||||
sample.createdAt(),
|
||||
sample.completedAt(),
|
||||
sample.farmerBusinessKey(),
|
||||
sample.farmerName(),
|
||||
sample.farmerEmail(),
|
||||
sample.cowNumber(),
|
||||
sample.cowName(),
|
||||
sample.sampleKind().name(),
|
||||
sample.therapyRecommendation() == null ? null : sample.therapyRecommendation().internalNote(),
|
||||
sample.currentStep() == de.svencarstensen.muh.domain.SampleWorkflowStep.COMPLETED,
|
||||
sample.reportSent(),
|
||||
sample.reportBlocked()
|
||||
);
|
||||
}
|
||||
|
||||
private PortalSampleRow toPortalRow(SampleService.SampleDetail sample) {
|
||||
return new PortalSampleRow(
|
||||
sample.id(),
|
||||
sample.sampleNumber(),
|
||||
sample.createdAt(),
|
||||
sample.completedAt(),
|
||||
sample.farmerBusinessKey(),
|
||||
sample.farmerName(),
|
||||
sample.farmerEmail(),
|
||||
sample.cowNumber(),
|
||||
sample.cowName(),
|
||||
sample.sampleKind().name(),
|
||||
sample.therapy() == null ? null : sample.therapy().internalNote(),
|
||||
sample.completed(),
|
||||
sample.reportSent(),
|
||||
sample.reportBlocked()
|
||||
);
|
||||
}
|
||||
|
||||
public record PortalSnapshot(
|
||||
List<CatalogService.FarmerOption> farmers,
|
||||
List<PortalSampleRow> samples,
|
||||
List<ReportService.ReportCandidate> reportCandidates,
|
||||
List<CatalogService.UserRow> users
|
||||
) {
|
||||
}
|
||||
|
||||
public record PortalSampleRow(
|
||||
String sampleId,
|
||||
long sampleNumber,
|
||||
java.time.LocalDateTime createdAt,
|
||||
java.time.LocalDateTime completedAt,
|
||||
String farmerBusinessKey,
|
||||
String farmerName,
|
||||
String farmerEmail,
|
||||
String cowNumber,
|
||||
String cowName,
|
||||
String sampleKindLabel,
|
||||
String internalNote,
|
||||
boolean completed,
|
||||
boolean reportSent,
|
||||
boolean reportBlocked
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.Sample;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class ReportService {
|
||||
|
||||
private final SampleService sampleService;
|
||||
private final ObjectProvider<JavaMailSender> mailSenderProvider;
|
||||
private final boolean mailEnabled;
|
||||
private final String mailFrom;
|
||||
|
||||
public ReportService(
|
||||
SampleService sampleService,
|
||||
ObjectProvider<JavaMailSender> mailSenderProvider,
|
||||
@Value("${muh.mail.enabled:false}") boolean mailEnabled,
|
||||
@Value("${muh.mail.from:no-reply@muh.local}") String mailFrom
|
||||
) {
|
||||
this.sampleService = sampleService;
|
||||
this.mailSenderProvider = mailSenderProvider;
|
||||
this.mailEnabled = mailEnabled;
|
||||
this.mailFrom = mailFrom;
|
||||
}
|
||||
|
||||
public List<ReportCandidate> reportCandidates() {
|
||||
return sampleService.completedSamples().stream()
|
||||
.filter(sample -> sample.farmerEmail() != null && !sample.farmerEmail().isBlank())
|
||||
.map(this::toCandidate)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public DispatchResult sendReports(List<String> sampleIds) {
|
||||
List<ReportCandidate> sent = new ArrayList<>();
|
||||
List<ReportCandidate> skipped = new ArrayList<>();
|
||||
|
||||
for (String sampleId : sampleIds) {
|
||||
Sample sample = sampleService.loadSampleEntity(sampleId);
|
||||
if (sample.farmerEmail() == null || sample.farmerEmail().isBlank() || sample.reportBlocked()) {
|
||||
skipped.add(toCandidate(sample));
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] pdf = buildPdf(sample);
|
||||
if (mailEnabled && mailSenderProvider.getIfAvailable() != null) {
|
||||
sendMail(sample, pdf);
|
||||
}
|
||||
Sample updated = sampleService.markReportSent(sample.id(), LocalDateTime.now());
|
||||
sent.add(toCandidate(updated));
|
||||
}
|
||||
return new DispatchResult(sent, skipped, mailEnabled && mailSenderProvider.getIfAvailable() != null);
|
||||
}
|
||||
|
||||
public byte[] reportPdf(String sampleId) {
|
||||
return buildPdf(sampleService.loadSampleEntity(sampleId));
|
||||
}
|
||||
|
||||
public SampleService.SampleDetail toggleReportBlocked(String sampleId, boolean blocked) {
|
||||
return sampleService.getSample(sampleService.toggleReportBlocked(sampleId, blocked).id());
|
||||
}
|
||||
|
||||
private void sendMail(Sample sample, byte[] pdf) {
|
||||
try {
|
||||
JavaMailSender sender = mailSenderProvider.getIfAvailable();
|
||||
if (sender == null) {
|
||||
return;
|
||||
}
|
||||
MimeMessage message = sender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
|
||||
helper.setFrom(requireText(mailFrom, "Absender fehlt"));
|
||||
helper.setTo(requireText(sample.farmerEmail(), "Empfänger fehlt"));
|
||||
helper.setSubject("MUH-Bericht Probe " + sample.sampleNumber());
|
||||
helper.setText("Im Anhang befindet sich der Bericht zur Probe " + sample.sampleNumber() + ".", false);
|
||||
helper.addAttachment("MUH-Bericht-" + sample.sampleNumber() + ".pdf", new ByteArrayResource(Objects.requireNonNull(pdf)));
|
||||
sender.send(message);
|
||||
} catch (Exception exception) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Mailversand fehlgeschlagen", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private ReportCandidate toCandidate(Sample sample) {
|
||||
return new ReportCandidate(
|
||||
sample.id(),
|
||||
sample.sampleNumber(),
|
||||
sample.farmerName(),
|
||||
sample.farmerEmail(),
|
||||
sample.cowName() == null ? sample.cowNumber() : sample.cowNumber() + " / " + sample.cowName(),
|
||||
sample.completedAt(),
|
||||
sample.reportSent(),
|
||||
sample.reportBlocked()
|
||||
);
|
||||
}
|
||||
|
||||
private byte[] buildPdf(Sample sample) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("MUH-Bericht");
|
||||
lines.add("Probe: " + sample.sampleNumber());
|
||||
lines.add("Landwirt: " + sample.farmerName());
|
||||
lines.add("Kuh: " + sample.cowNumber() + (sample.cowName() == null ? "" : " / " + sample.cowName()));
|
||||
lines.add("Typ: " + sample.sampleKind());
|
||||
lines.add("Abgeschlossen: " + (sample.completedAt() == null ? "-" : sample.completedAt().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))));
|
||||
lines.add("");
|
||||
lines.add("Anamnese");
|
||||
sample.quarters().forEach(quarter -> lines.add(
|
||||
quarter.quarterKey().label() + ": "
|
||||
+ (quarter.effectivePathogenLabel() == null ? "-" : quarter.effectivePathogenLabel())
|
||||
+ (quarter.flagged() ? " [auffaellig]" : "")
|
||||
));
|
||||
lines.add("");
|
||||
lines.add("Therapie");
|
||||
if (sample.therapyRecommendation() != null) {
|
||||
lines.add("Hinweis Landwirt: " + defaultText(sample.therapyRecommendation().farmerNote()));
|
||||
lines.add("Interne Bemerkung: " + defaultText(sample.therapyRecommendation().internalNote()));
|
||||
}
|
||||
|
||||
return renderSimplePdf(lines);
|
||||
}
|
||||
|
||||
private String defaultText(String value) {
|
||||
return value == null || value.isBlank() ? "-" : value;
|
||||
}
|
||||
|
||||
private @NonNull String requireText(String value, String message) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
return Objects.requireNonNull(value);
|
||||
}
|
||||
|
||||
private byte[] renderSimplePdf(List<String> lines) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
content.append("BT\n/F1 12 Tf\n50 790 Td\n");
|
||||
boolean first = true;
|
||||
for (String line : lines) {
|
||||
if (!first) {
|
||||
content.append("0 -18 Td\n");
|
||||
}
|
||||
content.append("(").append(escapePdf(line)).append(") Tj\n");
|
||||
first = false;
|
||||
}
|
||||
content.append("ET");
|
||||
|
||||
String stream = content.toString();
|
||||
List<String> objects = List.of(
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||||
"<< /Length " + stream.getBytes(StandardCharsets.UTF_8).length + " >>\nstream\n" + stream + "\nendstream"
|
||||
);
|
||||
|
||||
StringBuilder pdf = new StringBuilder("%PDF-1.4\n");
|
||||
List<Integer> offsets = new ArrayList<>();
|
||||
for (int index = 0; index < objects.size(); index++) {
|
||||
offsets.add(pdf.toString().getBytes(StandardCharsets.UTF_8).length);
|
||||
pdf.append(index + 1).append(" 0 obj\n").append(objects.get(index)).append("\nendobj\n");
|
||||
}
|
||||
int xrefOffset = pdf.toString().getBytes(StandardCharsets.UTF_8).length;
|
||||
pdf.append("xref\n0 ").append(objects.size() + 1).append("\n");
|
||||
pdf.append("0000000000 65535 f \n");
|
||||
for (Integer offset : offsets) {
|
||||
pdf.append(String.format("%010d 00000 n %n", offset));
|
||||
}
|
||||
pdf.append("trailer\n<< /Size ").append(objects.size() + 1).append(" /Root 1 0 R >>\n");
|
||||
pdf.append("startxref\n").append(xrefOffset).append("\n%%EOF");
|
||||
return pdf.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String escapePdf(String value) {
|
||||
return value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("(", "\\(")
|
||||
.replace(")", "\\)");
|
||||
}
|
||||
|
||||
public record ReportCandidate(
|
||||
String sampleId,
|
||||
long sampleNumber,
|
||||
String farmerName,
|
||||
String farmerEmail,
|
||||
String cowLabel,
|
||||
LocalDateTime completedAt,
|
||||
boolean reportSent,
|
||||
boolean reportBlocked
|
||||
) {
|
||||
}
|
||||
|
||||
public record DispatchResult(
|
||||
List<ReportCandidate> sent,
|
||||
List<ReportCandidate> skipped,
|
||||
boolean mailDeliveryActive
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,769 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.AntibiogramEntry;
|
||||
import de.svencarstensen.muh.domain.PathogenCatalogItem;
|
||||
import de.svencarstensen.muh.domain.PathogenKind;
|
||||
import de.svencarstensen.muh.domain.QuarterAntibiogram;
|
||||
import de.svencarstensen.muh.domain.QuarterFinding;
|
||||
import de.svencarstensen.muh.domain.QuarterKey;
|
||||
import de.svencarstensen.muh.domain.Sample;
|
||||
import de.svencarstensen.muh.domain.SampleKind;
|
||||
import de.svencarstensen.muh.domain.SampleWorkflowStep;
|
||||
import de.svencarstensen.muh.domain.SamplingMode;
|
||||
import de.svencarstensen.muh.domain.SensitivityResult;
|
||||
import de.svencarstensen.muh.domain.TherapyRecommendation;
|
||||
import de.svencarstensen.muh.repository.SampleRepository;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class SampleService {
|
||||
|
||||
private final SampleRepository sampleRepository;
|
||||
private final CatalogService catalogService;
|
||||
|
||||
public SampleService(SampleRepository sampleRepository, CatalogService catalogService) {
|
||||
this.sampleRepository = sampleRepository;
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
public DashboardOverview dashboardOverview() {
|
||||
List<SampleSummary> recent = sampleRepository.findTop12ByOrderByUpdatedAtDesc().stream()
|
||||
.map(this::toSummary)
|
||||
.toList();
|
||||
long openCount = sampleRepository.findAll().stream().filter(sample -> sample.currentStep() != SampleWorkflowStep.COMPLETED).count();
|
||||
LocalDate today = LocalDate.now();
|
||||
long completedToday = sampleRepository.findByCompletedAtBetweenOrderByCompletedAtDesc(
|
||||
today.atStartOfDay(),
|
||||
today.plusDays(1).atStartOfDay()
|
||||
).size();
|
||||
return new DashboardOverview(nextSampleNumber(), openCount, completedToday, recent);
|
||||
}
|
||||
|
||||
public LookupResult lookup(long sampleNumber) {
|
||||
return sampleRepository.findBySampleNumber(sampleNumber)
|
||||
.map(sample -> new LookupResult(
|
||||
true,
|
||||
"Probe gefunden",
|
||||
sample.id(),
|
||||
sample.currentStep(),
|
||||
SampleWorkflowRules.routeSegment(sample.currentStep())
|
||||
))
|
||||
.orElseGet(() -> new LookupResult(false, "Proben-Nummer unbekannt", null, null, null));
|
||||
}
|
||||
|
||||
public SampleDetail getSample(String id) {
|
||||
return toDetail(loadSample(id));
|
||||
}
|
||||
|
||||
public SampleDetail getSampleByNumber(long sampleNumber) {
|
||||
return sampleRepository.findBySampleNumber(sampleNumber)
|
||||
.map(this::toDetail)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden"));
|
||||
}
|
||||
|
||||
public SampleDetail createSample(RegistrationRequest request) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream()
|
||||
.filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
|
||||
|
||||
Sample sample = new Sample(
|
||||
null,
|
||||
nextSampleNumber(),
|
||||
farmer.businessKey(),
|
||||
farmer.name(),
|
||||
farmer.email(),
|
||||
request.cowNumber().trim(),
|
||||
blankToNull(request.cowName()),
|
||||
request.sampleKind(),
|
||||
request.samplingMode(),
|
||||
SampleWorkflowStep.ANAMNESIS,
|
||||
buildQuarters(request),
|
||||
List.of(),
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
now,
|
||||
now,
|
||||
null,
|
||||
request.userCode(),
|
||||
request.userDisplayName()
|
||||
);
|
||||
|
||||
return toDetail(sampleRepository.save(sample));
|
||||
}
|
||||
|
||||
public SampleDetail saveRegistration(String id, RegistrationRequest request) {
|
||||
Sample existing = loadSample(id);
|
||||
if (!SampleWorkflowRules.canEditRegistration(existing)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Stammdaten können nicht mehr geändert werden");
|
||||
}
|
||||
|
||||
CatalogService.FarmerOption farmer = catalogService.activeCatalogSummary().farmers().stream()
|
||||
.filter(candidate -> candidate.businessKey().equals(request.farmerBusinessKey()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Landwirt nicht gefunden"));
|
||||
|
||||
Sample saved = sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
farmer.businessKey(),
|
||||
farmer.name(),
|
||||
farmer.email(),
|
||||
request.cowNumber().trim(),
|
||||
blankToNull(request.cowName()),
|
||||
request.sampleKind(),
|
||||
request.samplingMode(),
|
||||
SampleWorkflowStep.ANAMNESIS,
|
||||
buildQuarters(request),
|
||||
List.of(),
|
||||
existing.therapyRecommendation(),
|
||||
existing.reportSent(),
|
||||
existing.reportBlocked(),
|
||||
existing.reportSentAt(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
existing.completedAt(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
));
|
||||
|
||||
return toDetail(saved);
|
||||
}
|
||||
|
||||
public SampleDetail saveAnamnesis(String id, AnamnesisRequest request) {
|
||||
Sample existing = loadSample(id);
|
||||
if (!SampleWorkflowRules.canEditAnamnesis(existing)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Anamnese kann an dieser Stelle nicht geändert werden");
|
||||
}
|
||||
|
||||
Map<QuarterKey, QuarterFinding> current = new HashMap<>();
|
||||
for (QuarterFinding quarter : existing.quarters()) {
|
||||
current.put(quarter.quarterKey(), quarter);
|
||||
}
|
||||
|
||||
Map<String, PathogenCatalogItem> pathogens = catalogService.activePathogensByBusinessKey();
|
||||
List<QuarterFinding> updatedQuarters = new ArrayList<>();
|
||||
for (AnamnesisQuarterRequest quarterRequest : request.quarters()) {
|
||||
QuarterFinding base = current.get(quarterRequest.quarterKey());
|
||||
if (base == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Entnahmestelle unbekannt");
|
||||
}
|
||||
|
||||
PathogenCatalogItem catalogItem = quarterRequest.pathogenBusinessKey() == null
|
||||
? null
|
||||
: pathogens.get(quarterRequest.pathogenBusinessKey());
|
||||
|
||||
String customPathogen = blankToNull(quarterRequest.customPathogenName());
|
||||
PathogenKind pathogenKind = catalogItem != null ? catalogItem.kind() : (customPathogen == null ? null : PathogenKind.OTHER);
|
||||
updatedQuarters.add(new QuarterFinding(
|
||||
base.quarterKey(),
|
||||
base.flagged(),
|
||||
catalogItem != null ? catalogItem.businessKey() : null,
|
||||
catalogItem != null ? catalogItem.code() : null,
|
||||
catalogItem != null ? catalogItem.name() : null,
|
||||
pathogenKind,
|
||||
customPathogen,
|
||||
quarterRequest.cellCount()
|
||||
));
|
||||
}
|
||||
|
||||
updatedQuarters.sort(Comparator.comparingInt(this::quarterSort));
|
||||
SampleWorkflowStep nextStep = SampleWorkflowRules.nextStepAfterAnamnesis(updatedQuarters);
|
||||
Sample saved = sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
existing.farmerBusinessKey(),
|
||||
existing.farmerName(),
|
||||
existing.farmerEmail(),
|
||||
existing.cowNumber(),
|
||||
existing.cowName(),
|
||||
existing.sampleKind(),
|
||||
existing.samplingMode(),
|
||||
nextStep,
|
||||
updatedQuarters,
|
||||
List.of(),
|
||||
existing.therapyRecommendation(),
|
||||
existing.reportSent(),
|
||||
existing.reportBlocked(),
|
||||
existing.reportSentAt(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
existing.completedAt(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
));
|
||||
return toDetail(saved);
|
||||
}
|
||||
|
||||
public SampleDetail saveAntibiogram(String id, AntibiogramRequest request) {
|
||||
Sample existing = loadSample(id);
|
||||
if (!SampleWorkflowRules.canEditAntibiogram(existing)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Antibiogramm kann nicht mehr geändert werden");
|
||||
}
|
||||
|
||||
Map<String, de.svencarstensen.muh.domain.AntibioticCatalogItem> antibiotics = catalogService.activeAntibioticsByBusinessKey();
|
||||
Map<QuarterKey, QuarterAntibiogram> groups = new HashMap<>();
|
||||
Map<QuarterKey, QuarterFinding> quartersByKey = existing.quarters().stream()
|
||||
.collect(java.util.stream.Collectors.toMap(QuarterFinding::quarterKey, quarter -> quarter));
|
||||
|
||||
for (AntibiogramGroupRequest groupRequest : request.groups()) {
|
||||
QuarterFinding referenceQuarter = quartersByKey.get(groupRequest.referenceQuarter());
|
||||
if (referenceQuarter == null || !referenceQuarter.requiresAntibiogram()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Ungültige Referenz für Antibiogramm");
|
||||
}
|
||||
|
||||
List<AntibiogramEntry> entries = groupRequest.entries().stream()
|
||||
.map(entry -> {
|
||||
de.svencarstensen.muh.domain.AntibioticCatalogItem catalogItem = antibiotics.get(entry.antibioticBusinessKey());
|
||||
if (catalogItem == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Antibiotikum unbekannt");
|
||||
}
|
||||
return new AntibiogramEntry(
|
||||
catalogItem.businessKey(),
|
||||
catalogItem.code(),
|
||||
catalogItem.name(),
|
||||
entry.result()
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
for (QuarterFinding quarter : existing.quarters()) {
|
||||
if (!quarter.requiresAntibiogram()) {
|
||||
continue;
|
||||
}
|
||||
if (Objects.equals(
|
||||
SampleWorkflowRules.pathogenIdentity(referenceQuarter),
|
||||
SampleWorkflowRules.pathogenIdentity(quarter)
|
||||
)) {
|
||||
groups.put(quarter.quarterKey(), new QuarterAntibiogram(
|
||||
quarter.quarterKey(),
|
||||
quarter.pathogenBusinessKey(),
|
||||
quarter.effectivePathogenLabel(),
|
||||
referenceQuarter.quarterKey().equals(quarter.quarterKey()) ? null : referenceQuarter.quarterKey(),
|
||||
entries
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<QuarterAntibiogram> savedGroups = existing.quarters().stream()
|
||||
.filter(QuarterFinding::requiresAntibiogram)
|
||||
.map(quarter -> groups.getOrDefault(quarter.quarterKey(), new QuarterAntibiogram(
|
||||
quarter.quarterKey(),
|
||||
quarter.pathogenBusinessKey(),
|
||||
quarter.effectivePathogenLabel(),
|
||||
SampleWorkflowRules.referenceQuarterForPathogen(existing.quarters(), quarter).equals(quarter.quarterKey())
|
||||
? null
|
||||
: SampleWorkflowRules.referenceQuarterForPathogen(existing.quarters(), quarter),
|
||||
List.of()
|
||||
)))
|
||||
.toList();
|
||||
|
||||
Sample saved = sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
existing.farmerBusinessKey(),
|
||||
existing.farmerName(),
|
||||
existing.farmerEmail(),
|
||||
existing.cowNumber(),
|
||||
existing.cowName(),
|
||||
existing.sampleKind(),
|
||||
existing.samplingMode(),
|
||||
SampleWorkflowStep.THERAPY,
|
||||
existing.quarters(),
|
||||
savedGroups,
|
||||
existing.therapyRecommendation(),
|
||||
existing.reportSent(),
|
||||
existing.reportBlocked(),
|
||||
existing.reportSentAt(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
existing.completedAt(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
));
|
||||
return toDetail(saved);
|
||||
}
|
||||
|
||||
public SampleDetail saveTherapy(String id, TherapyRequest request) {
|
||||
Sample existing = loadSample(id);
|
||||
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(
|
||||
previous.continueStarted(),
|
||||
previous.switchTherapy(),
|
||||
previous.inUdderMedicationKeys(),
|
||||
previous.inUdderMedicationNames(),
|
||||
previous.inUdderOther(),
|
||||
previous.systemicMedicationKeys(),
|
||||
previous.systemicMedicationNames(),
|
||||
previous.systemicOther(),
|
||||
previous.drySealerKeys(),
|
||||
previous.drySealerNames(),
|
||||
previous.dryAntibioticKeys(),
|
||||
previous.dryAntibioticNames(),
|
||||
previous.farmerNote(),
|
||||
blankToNull(request.internalNote())
|
||||
);
|
||||
return toDetail(sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
existing.farmerBusinessKey(),
|
||||
existing.farmerName(),
|
||||
existing.farmerEmail(),
|
||||
existing.cowNumber(),
|
||||
existing.cowName(),
|
||||
existing.sampleKind(),
|
||||
existing.samplingMode(),
|
||||
SampleWorkflowStep.COMPLETED,
|
||||
existing.quarters(),
|
||||
existing.antibiograms(),
|
||||
updated,
|
||||
existing.reportSent(),
|
||||
existing.reportBlocked(),
|
||||
existing.reportSentAt(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
existing.completedAt(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
)));
|
||||
}
|
||||
|
||||
if (!SampleWorkflowRules.canEditTherapy(existing)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Therapie kann nicht bearbeitet werden");
|
||||
}
|
||||
|
||||
Map<String, de.svencarstensen.muh.domain.MedicationCatalogItem> medications = catalogService.activeMedicationsByBusinessKey();
|
||||
TherapyRecommendation therapy = new TherapyRecommendation(
|
||||
request.continueStarted(),
|
||||
request.switchTherapy(),
|
||||
request.inUdderMedicationKeys(),
|
||||
resolveMedicationNames(request.inUdderMedicationKeys(), medications),
|
||||
blankToNull(request.inUdderOther()),
|
||||
request.systemicMedicationKeys(),
|
||||
resolveMedicationNames(request.systemicMedicationKeys(), medications),
|
||||
blankToNull(request.systemicOther()),
|
||||
request.drySealerKeys(),
|
||||
resolveMedicationNames(request.drySealerKeys(), medications),
|
||||
request.dryAntibioticKeys(),
|
||||
resolveMedicationNames(request.dryAntibioticKeys(), medications),
|
||||
blankToNull(request.farmerNote()),
|
||||
blankToNull(request.internalNote())
|
||||
);
|
||||
|
||||
Sample saved = sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
existing.farmerBusinessKey(),
|
||||
existing.farmerName(),
|
||||
existing.farmerEmail(),
|
||||
existing.cowNumber(),
|
||||
existing.cowName(),
|
||||
existing.sampleKind(),
|
||||
existing.samplingMode(),
|
||||
SampleWorkflowStep.COMPLETED,
|
||||
existing.quarters(),
|
||||
existing.antibiograms(),
|
||||
therapy,
|
||||
existing.reportSent(),
|
||||
existing.reportBlocked(),
|
||||
existing.reportSentAt(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
LocalDateTime.now(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
));
|
||||
return toDetail(saved);
|
||||
}
|
||||
|
||||
public Sample markReportSent(String id, LocalDateTime sentAt) {
|
||||
Sample existing = loadSample(id);
|
||||
return sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
existing.farmerBusinessKey(),
|
||||
existing.farmerName(),
|
||||
existing.farmerEmail(),
|
||||
existing.cowNumber(),
|
||||
existing.cowName(),
|
||||
existing.sampleKind(),
|
||||
existing.samplingMode(),
|
||||
existing.currentStep(),
|
||||
existing.quarters(),
|
||||
existing.antibiograms(),
|
||||
existing.therapyRecommendation(),
|
||||
true,
|
||||
existing.reportBlocked(),
|
||||
sentAt,
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
existing.completedAt(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
));
|
||||
}
|
||||
|
||||
public Sample toggleReportBlocked(String id, boolean blocked) {
|
||||
Sample existing = loadSample(id);
|
||||
return sampleRepository.save(new Sample(
|
||||
existing.id(),
|
||||
existing.sampleNumber(),
|
||||
existing.farmerBusinessKey(),
|
||||
existing.farmerName(),
|
||||
existing.farmerEmail(),
|
||||
existing.cowNumber(),
|
||||
existing.cowName(),
|
||||
existing.sampleKind(),
|
||||
existing.samplingMode(),
|
||||
existing.currentStep(),
|
||||
existing.quarters(),
|
||||
existing.antibiograms(),
|
||||
existing.therapyRecommendation(),
|
||||
existing.reportSent(),
|
||||
blocked,
|
||||
existing.reportSentAt(),
|
||||
existing.createdAt(),
|
||||
LocalDateTime.now(),
|
||||
existing.completedAt(),
|
||||
existing.createdByUserCode(),
|
||||
existing.createdByDisplayName()
|
||||
));
|
||||
}
|
||||
|
||||
public List<Sample> completedSamples() {
|
||||
return sampleRepository.findByCompletedAtNotNullOrderByCompletedAtDesc();
|
||||
}
|
||||
|
||||
public List<Sample> samplesByFarmerBusinessKey(String businessKey) {
|
||||
return sampleRepository.findByFarmerBusinessKeyOrderByCreatedAtDesc(businessKey);
|
||||
}
|
||||
|
||||
public List<Sample> samplesByDate(LocalDate date) {
|
||||
return sampleRepository.findByCompletedAtBetweenOrderByCompletedAtDesc(date.atStartOfDay(), date.plusDays(1).atStartOfDay());
|
||||
}
|
||||
|
||||
public long nextSampleNumber() {
|
||||
return sampleRepository.findTopByOrderBySampleNumberDesc()
|
||||
.map(sample -> sample.sampleNumber() + 1)
|
||||
.orElse(100001L);
|
||||
}
|
||||
|
||||
public Sample loadSampleEntity(String id) {
|
||||
return loadSample(id);
|
||||
}
|
||||
|
||||
private Sample loadSample(String id) {
|
||||
return sampleRepository.findById(Objects.requireNonNull(id))
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Probe nicht gefunden"));
|
||||
}
|
||||
|
||||
private SampleSummary toSummary(Sample sample) {
|
||||
return new SampleSummary(
|
||||
sample.id(),
|
||||
sample.sampleNumber(),
|
||||
sample.farmerName(),
|
||||
sample.cowName() == null ? sample.cowNumber() : sample.cowNumber() + " / " + sample.cowName(),
|
||||
sample.sampleKind(),
|
||||
sample.currentStep(),
|
||||
sample.updatedAt(),
|
||||
sample.reportSent(),
|
||||
sample.reportBlocked()
|
||||
);
|
||||
}
|
||||
|
||||
private SampleDetail toDetail(Sample sample) {
|
||||
List<QuarterView> quarters = sample.quarters().stream()
|
||||
.map(quarter -> new QuarterView(
|
||||
quarter.quarterKey(),
|
||||
quarter.quarterKey().label(),
|
||||
quarter.flagged(),
|
||||
quarter.pathogenBusinessKey(),
|
||||
quarter.pathogenCode(),
|
||||
quarter.pathogenName(),
|
||||
quarter.pathogenKind(),
|
||||
quarter.customPathogenName(),
|
||||
quarter.cellCount(),
|
||||
quarter.requiresAntibiogram()
|
||||
))
|
||||
.sorted(Comparator.comparingInt(quarter -> quarterSort(quarter.quarterKey())))
|
||||
.toList();
|
||||
|
||||
List<AntibiogramView> antibiograms = sample.antibiograms().stream()
|
||||
.map(group -> new AntibiogramView(
|
||||
group.quarterKey(),
|
||||
group.pathogenName(),
|
||||
group.inheritedFromQuarter(),
|
||||
group.entries().stream()
|
||||
.map(entry -> new AntibiogramEntryView(entry.antibioticBusinessKey(), entry.antibioticCode(), entry.antibioticName(), entry.result()))
|
||||
.toList()
|
||||
))
|
||||
.toList();
|
||||
|
||||
return new SampleDetail(
|
||||
sample.id(),
|
||||
sample.sampleNumber(),
|
||||
sample.farmerBusinessKey(),
|
||||
sample.farmerName(),
|
||||
sample.farmerEmail(),
|
||||
sample.cowNumber(),
|
||||
sample.cowName(),
|
||||
sample.sampleKind(),
|
||||
sample.samplingMode(),
|
||||
sample.currentStep(),
|
||||
sample.createdAt(),
|
||||
sample.updatedAt(),
|
||||
sample.completedAt(),
|
||||
sample.createdByUserCode(),
|
||||
sample.createdByDisplayName(),
|
||||
sample.reportSent(),
|
||||
sample.reportBlocked(),
|
||||
sample.reportSentAt(),
|
||||
SampleWorkflowRules.routeSegment(sample.currentStep()),
|
||||
quarters,
|
||||
antibiograms,
|
||||
toTherapyView(sample.therapyRecommendation()),
|
||||
SampleWorkflowRules.antibiogramTargets(sample.quarters()),
|
||||
SampleWorkflowRules.canEditRegistration(sample),
|
||||
SampleWorkflowRules.canEditAnamnesis(sample),
|
||||
SampleWorkflowRules.canEditAntibiogram(sample),
|
||||
SampleWorkflowRules.canEditTherapy(sample),
|
||||
sample.currentStep() == SampleWorkflowStep.COMPLETED
|
||||
);
|
||||
}
|
||||
|
||||
private TherapyView toTherapyView(TherapyRecommendation therapy) {
|
||||
if (therapy == null) {
|
||||
return null;
|
||||
}
|
||||
return new TherapyView(
|
||||
therapy.continueStarted(),
|
||||
therapy.switchTherapy(),
|
||||
therapy.inUdderMedicationKeys(),
|
||||
therapy.inUdderMedicationNames(),
|
||||
therapy.inUdderOther(),
|
||||
therapy.systemicMedicationKeys(),
|
||||
therapy.systemicMedicationNames(),
|
||||
therapy.systemicOther(),
|
||||
therapy.drySealerKeys(),
|
||||
therapy.drySealerNames(),
|
||||
therapy.dryAntibioticKeys(),
|
||||
therapy.dryAntibioticNames(),
|
||||
therapy.farmerNote(),
|
||||
therapy.internalNote()
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> resolveMedicationNames(List<String> keys, Map<String, de.svencarstensen.muh.domain.MedicationCatalogItem> medications) {
|
||||
return keys == null ? List.of() : keys.stream()
|
||||
.map(medications::get)
|
||||
.filter(Objects::nonNull)
|
||||
.map(de.svencarstensen.muh.domain.MedicationCatalogItem::name)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<QuarterFinding> buildQuarters(RegistrationRequest request) {
|
||||
return switch (request.samplingMode()) {
|
||||
case SINGLE_SITE -> List.of(new QuarterFinding(QuarterKey.SINGLE, false, null, null, null, null, null, null));
|
||||
case UNKNOWN_SITE -> List.of(new QuarterFinding(QuarterKey.UNKNOWN, false, null, null, null, null, null, null));
|
||||
case FOUR_QUARTER -> List.of(
|
||||
new QuarterFinding(QuarterKey.LEFT_FRONT, request.flaggedQuarters().contains(QuarterKey.LEFT_FRONT), null, null, null, null, null, null),
|
||||
new QuarterFinding(QuarterKey.RIGHT_FRONT, request.flaggedQuarters().contains(QuarterKey.RIGHT_FRONT), null, null, null, null, null, null),
|
||||
new QuarterFinding(QuarterKey.LEFT_REAR, request.flaggedQuarters().contains(QuarterKey.LEFT_REAR), null, null, null, null, null, null),
|
||||
new QuarterFinding(QuarterKey.RIGHT_REAR, request.flaggedQuarters().contains(QuarterKey.RIGHT_REAR), null, null, null, null, null, null)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private int quarterSort(QuarterFinding finding) {
|
||||
return quarterSort(finding.quarterKey());
|
||||
}
|
||||
|
||||
private int quarterSort(QuarterKey key) {
|
||||
return switch (key) {
|
||||
case SINGLE -> 0;
|
||||
case UNKNOWN -> 1;
|
||||
case LEFT_FRONT -> 2;
|
||||
case RIGHT_FRONT -> 3;
|
||||
case LEFT_REAR -> 4;
|
||||
case RIGHT_REAR -> 5;
|
||||
};
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
public record DashboardOverview(
|
||||
long nextSampleNumber,
|
||||
long openSamples,
|
||||
long completedToday,
|
||||
List<SampleSummary> recentSamples
|
||||
) {
|
||||
}
|
||||
|
||||
public record SampleSummary(
|
||||
String id,
|
||||
long sampleNumber,
|
||||
String farmerName,
|
||||
String cowLabel,
|
||||
SampleKind sampleKind,
|
||||
SampleWorkflowStep currentStep,
|
||||
LocalDateTime updatedAt,
|
||||
boolean reportSent,
|
||||
boolean reportBlocked
|
||||
) {
|
||||
}
|
||||
|
||||
public record LookupResult(
|
||||
boolean found,
|
||||
String message,
|
||||
String sampleId,
|
||||
SampleWorkflowStep step,
|
||||
String routeSegment
|
||||
) {
|
||||
}
|
||||
|
||||
public record QuarterView(
|
||||
QuarterKey quarterKey,
|
||||
String label,
|
||||
boolean flagged,
|
||||
String pathogenBusinessKey,
|
||||
String pathogenCode,
|
||||
String pathogenName,
|
||||
PathogenKind pathogenKind,
|
||||
String customPathogenName,
|
||||
Integer cellCount,
|
||||
boolean requiresAntibiogram
|
||||
) {
|
||||
}
|
||||
|
||||
public record AntibiogramEntryView(
|
||||
String antibioticBusinessKey,
|
||||
String antibioticCode,
|
||||
String antibioticName,
|
||||
SensitivityResult result
|
||||
) {
|
||||
}
|
||||
|
||||
public record AntibiogramView(
|
||||
QuarterKey quarterKey,
|
||||
String pathogenName,
|
||||
QuarterKey inheritedFromQuarter,
|
||||
List<AntibiogramEntryView> entries
|
||||
) {
|
||||
}
|
||||
|
||||
public record TherapyView(
|
||||
boolean continueStarted,
|
||||
boolean switchTherapy,
|
||||
List<String> inUdderMedicationKeys,
|
||||
List<String> inUdderMedicationNames,
|
||||
String inUdderOther,
|
||||
List<String> systemicMedicationKeys,
|
||||
List<String> systemicMedicationNames,
|
||||
String systemicOther,
|
||||
List<String> drySealerKeys,
|
||||
List<String> drySealerNames,
|
||||
List<String> dryAntibioticKeys,
|
||||
List<String> dryAntibioticNames,
|
||||
String farmerNote,
|
||||
String internalNote
|
||||
) {
|
||||
}
|
||||
|
||||
public record SampleDetail(
|
||||
String id,
|
||||
long sampleNumber,
|
||||
String farmerBusinessKey,
|
||||
String farmerName,
|
||||
String farmerEmail,
|
||||
String cowNumber,
|
||||
String cowName,
|
||||
SampleKind sampleKind,
|
||||
SamplingMode samplingMode,
|
||||
SampleWorkflowStep currentStep,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt,
|
||||
LocalDateTime completedAt,
|
||||
String createdByUserCode,
|
||||
String createdByDisplayName,
|
||||
boolean reportSent,
|
||||
boolean reportBlocked,
|
||||
LocalDateTime reportSentAt,
|
||||
String routeSegment,
|
||||
List<QuarterView> quarters,
|
||||
List<AntibiogramView> antibiograms,
|
||||
TherapyView therapy,
|
||||
List<QuarterKey> antibiogramTargets,
|
||||
boolean registrationEditable,
|
||||
boolean anamnesisEditable,
|
||||
boolean antibiogramEditable,
|
||||
boolean therapyEditable,
|
||||
boolean completed
|
||||
) {
|
||||
}
|
||||
|
||||
public record RegistrationRequest(
|
||||
String farmerBusinessKey,
|
||||
String cowNumber,
|
||||
String cowName,
|
||||
SampleKind sampleKind,
|
||||
SamplingMode samplingMode,
|
||||
List<QuarterKey> flaggedQuarters,
|
||||
String userCode,
|
||||
String userDisplayName
|
||||
) {
|
||||
}
|
||||
|
||||
public record AnamnesisQuarterRequest(
|
||||
QuarterKey quarterKey,
|
||||
String pathogenBusinessKey,
|
||||
String customPathogenName,
|
||||
Integer cellCount
|
||||
) {
|
||||
}
|
||||
|
||||
public record AnamnesisRequest(List<AnamnesisQuarterRequest> quarters) {
|
||||
}
|
||||
|
||||
public record AntibiogramLineRequest(String antibioticBusinessKey, SensitivityResult result) {
|
||||
}
|
||||
|
||||
public record AntibiogramGroupRequest(QuarterKey referenceQuarter, List<AntibiogramLineRequest> entries) {
|
||||
}
|
||||
|
||||
public record AntibiogramRequest(List<AntibiogramGroupRequest> groups) {
|
||||
}
|
||||
|
||||
public record TherapyRequest(
|
||||
boolean continueStarted,
|
||||
boolean switchTherapy,
|
||||
List<String> inUdderMedicationKeys,
|
||||
String inUdderOther,
|
||||
List<String> systemicMedicationKeys,
|
||||
String systemicOther,
|
||||
List<String> drySealerKeys,
|
||||
List<String> dryAntibioticKeys,
|
||||
String farmerNote,
|
||||
String internalNote
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.PathogenKind;
|
||||
import de.svencarstensen.muh.domain.QuarterFinding;
|
||||
import de.svencarstensen.muh.domain.QuarterKey;
|
||||
import de.svencarstensen.muh.domain.Sample;
|
||||
import de.svencarstensen.muh.domain.SampleWorkflowStep;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SampleWorkflowRules {
|
||||
|
||||
private SampleWorkflowRules() {
|
||||
}
|
||||
|
||||
public static String routeSegment(SampleWorkflowStep step) {
|
||||
return switch (step) {
|
||||
case ANAMNESIS -> "anamnesis";
|
||||
case ANTIBIOGRAM -> "antibiogram";
|
||||
case THERAPY, COMPLETED -> "therapy";
|
||||
};
|
||||
}
|
||||
|
||||
public static SampleWorkflowStep nextStepAfterAnamnesis(List<QuarterFinding> quarters) {
|
||||
return antibiogramTargets(quarters).isEmpty()
|
||||
? SampleWorkflowStep.THERAPY
|
||||
: SampleWorkflowStep.ANTIBIOGRAM;
|
||||
}
|
||||
|
||||
public static boolean canEditRegistration(Sample sample) {
|
||||
return sample.currentStep() == SampleWorkflowStep.ANAMNESIS;
|
||||
}
|
||||
|
||||
public static boolean canEditAnamnesis(Sample sample) {
|
||||
return sample.currentStep() == SampleWorkflowStep.ANAMNESIS
|
||||
|| sample.currentStep() == SampleWorkflowStep.ANTIBIOGRAM;
|
||||
}
|
||||
|
||||
public static boolean canEditAntibiogram(Sample sample) {
|
||||
return sample.currentStep() == SampleWorkflowStep.ANTIBIOGRAM
|
||||
|| sample.currentStep() == SampleWorkflowStep.THERAPY;
|
||||
}
|
||||
|
||||
public static boolean canEditTherapy(Sample sample) {
|
||||
return sample.currentStep() == SampleWorkflowStep.THERAPY;
|
||||
}
|
||||
|
||||
public static List<QuarterKey> antibiogramTargets(List<QuarterFinding> quarters) {
|
||||
Map<String, QuarterKey> firstByPathogen = new HashMap<>();
|
||||
List<QuarterKey> targets = new ArrayList<>();
|
||||
|
||||
for (QuarterFinding quarter : quarters) {
|
||||
if (quarter.pathogenKind() != PathogenKind.BACTERIAL) {
|
||||
continue;
|
||||
}
|
||||
String identity = pathogenIdentity(quarter);
|
||||
if (firstByPathogen.putIfAbsent(identity, quarter.quarterKey()) == null) {
|
||||
targets.add(quarter.quarterKey());
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
public static QuarterKey referenceQuarterForPathogen(List<QuarterFinding> quarters, QuarterFinding candidate) {
|
||||
String identity = pathogenIdentity(candidate);
|
||||
for (QuarterFinding quarter : quarters) {
|
||||
if (quarter.pathogenKind() == PathogenKind.BACTERIAL
|
||||
&& Objects.equals(identity, pathogenIdentity(quarter))) {
|
||||
return quarter.quarterKey();
|
||||
}
|
||||
}
|
||||
return candidate.quarterKey();
|
||||
}
|
||||
|
||||
public static String pathogenIdentity(QuarterFinding quarter) {
|
||||
if (quarter.pathogenBusinessKey() != null && !quarter.pathogenBusinessKey().isBlank()) {
|
||||
return quarter.pathogenBusinessKey();
|
||||
}
|
||||
return quarter.effectivePathogenLabel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.svencarstensen.muh.web;
|
||||
|
||||
import de.svencarstensen.muh.service.CatalogService;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class CatalogController {
|
||||
|
||||
private final CatalogService catalogService;
|
||||
|
||||
public CatalogController(CatalogService catalogService) {
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
@GetMapping("/catalogs/summary")
|
||||
public CatalogService.ActiveCatalogSummary catalogSummary() {
|
||||
return catalogService.activeCatalogSummary();
|
||||
}
|
||||
|
||||
@GetMapping("/admin")
|
||||
public CatalogService.AdministrationOverview administrationOverview() {
|
||||
return catalogService.administrationOverview();
|
||||
}
|
||||
|
||||
@PostMapping("/admin/farmers")
|
||||
public List<CatalogService.FarmerRow> saveFarmers(@RequestBody List<CatalogService.FarmerMutation> mutations) {
|
||||
return catalogService.saveFarmers(mutations);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/medications")
|
||||
public List<CatalogService.MedicationRow> saveMedications(@RequestBody List<CatalogService.MedicationMutation> mutations) {
|
||||
return catalogService.saveMedications(mutations);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/pathogens")
|
||||
public List<CatalogService.PathogenRow> savePathogens(@RequestBody List<CatalogService.PathogenMutation> mutations) {
|
||||
return catalogService.savePathogens(mutations);
|
||||
}
|
||||
|
||||
@PostMapping("/admin/antibiotics")
|
||||
public List<CatalogService.AntibioticRow> saveAntibiotics(@RequestBody List<CatalogService.AntibioticMutation> mutations) {
|
||||
return catalogService.saveAntibiotics(mutations);
|
||||
}
|
||||
|
||||
@GetMapping("/portal/users")
|
||||
public List<CatalogService.UserRow> users() {
|
||||
return catalogService.listUsers();
|
||||
}
|
||||
|
||||
@PostMapping("/portal/users")
|
||||
public CatalogService.UserRow saveUser(@RequestBody CatalogService.UserMutation mutation) {
|
||||
return catalogService.createOrUpdateUser(mutation);
|
||||
}
|
||||
|
||||
@DeleteMapping("/portal/users/{id}")
|
||||
public void deleteUser(@PathVariable String id) {
|
||||
catalogService.deleteUser(id);
|
||||
}
|
||||
|
||||
@PostMapping("/portal/users/{id}/password")
|
||||
public void changePassword(@PathVariable String id, @RequestBody PasswordChangeRequest request) {
|
||||
catalogService.changePassword(id, request.password());
|
||||
}
|
||||
|
||||
public record PasswordChangeRequest(String password) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.svencarstensen.muh.web;
|
||||
|
||||
import de.svencarstensen.muh.service.PortalService;
|
||||
import de.svencarstensen.muh.service.ReportService;
|
||||
import de.svencarstensen.muh.service.SampleService;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/portal")
|
||||
public class PortalController {
|
||||
|
||||
private final PortalService portalService;
|
||||
private final ReportService reportService;
|
||||
|
||||
public PortalController(PortalService portalService, ReportService reportService) {
|
||||
this.portalService = portalService;
|
||||
this.reportService = reportService;
|
||||
}
|
||||
|
||||
@GetMapping("/snapshot")
|
||||
public PortalService.PortalSnapshot snapshot(
|
||||
@RequestParam(required = false) String farmerBusinessKey,
|
||||
@RequestParam(required = false) String farmerQuery,
|
||||
@RequestParam(required = false) String cowQuery,
|
||||
@RequestParam(required = false) Long sampleNumber,
|
||||
@RequestParam(required = false) LocalDate date
|
||||
) {
|
||||
return portalService.snapshot(farmerBusinessKey, farmerQuery, cowQuery, sampleNumber, date);
|
||||
}
|
||||
|
||||
@GetMapping("/reports")
|
||||
public List<ReportService.ReportCandidate> reports() {
|
||||
return reportService.reportCandidates();
|
||||
}
|
||||
|
||||
@PostMapping("/reports/send")
|
||||
public ReportService.DispatchResult send(@RequestBody ReportDispatchRequest request) {
|
||||
return reportService.sendReports(request.sampleIds());
|
||||
}
|
||||
|
||||
@PatchMapping("/reports/{sampleId}/block")
|
||||
public SampleService.SampleDetail block(@PathVariable String sampleId, @RequestBody BlockRequest request) {
|
||||
return reportService.toggleReportBlocked(sampleId, request.blocked());
|
||||
}
|
||||
|
||||
@GetMapping("/reports/{sampleId}/pdf")
|
||||
public ResponseEntity<byte[]> pdf(@PathVariable String sampleId) {
|
||||
byte[] pdf = reportService.reportPdf(sampleId);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(Objects.requireNonNull(MediaType.APPLICATION_PDF))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline()
|
||||
.filename("MUH-Bericht-" + sampleId + ".pdf")
|
||||
.build()
|
||||
.toString())
|
||||
.body(pdf);
|
||||
}
|
||||
|
||||
public record ReportDispatchRequest(List<String> sampleIds) {
|
||||
}
|
||||
|
||||
public record BlockRequest(boolean blocked) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package de.svencarstensen.muh.web;
|
||||
|
||||
import de.svencarstensen.muh.service.SampleService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class SampleController {
|
||||
|
||||
private final SampleService sampleService;
|
||||
|
||||
public SampleController(SampleService sampleService) {
|
||||
this.sampleService = sampleService;
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public SampleService.DashboardOverview dashboardOverview() {
|
||||
return sampleService.dashboardOverview();
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard/lookup/{sampleNumber}")
|
||||
public SampleService.LookupResult lookup(@PathVariable long sampleNumber) {
|
||||
return sampleService.lookup(sampleNumber);
|
||||
}
|
||||
|
||||
@GetMapping("/samples/{id}")
|
||||
public SampleService.SampleDetail sample(@PathVariable String id) {
|
||||
return sampleService.getSample(id);
|
||||
}
|
||||
|
||||
@GetMapping("/samples/by-number/{sampleNumber}")
|
||||
public SampleService.SampleDetail sampleByNumber(@PathVariable long sampleNumber) {
|
||||
return sampleService.getSampleByNumber(sampleNumber);
|
||||
}
|
||||
|
||||
@PostMapping("/samples")
|
||||
public SampleService.SampleDetail create(@RequestBody SampleService.RegistrationRequest request) {
|
||||
return sampleService.createSample(request);
|
||||
}
|
||||
|
||||
@PutMapping("/samples/{id}/registration")
|
||||
public SampleService.SampleDetail saveRegistration(@PathVariable String id, @RequestBody SampleService.RegistrationRequest request) {
|
||||
return sampleService.saveRegistration(id, request);
|
||||
}
|
||||
|
||||
@PutMapping("/samples/{id}/anamnesis")
|
||||
public SampleService.SampleDetail saveAnamnesis(@PathVariable String id, @RequestBody SampleService.AnamnesisRequest request) {
|
||||
return sampleService.saveAnamnesis(id, request);
|
||||
}
|
||||
|
||||
@PutMapping("/samples/{id}/antibiogram")
|
||||
public SampleService.SampleDetail saveAntibiogram(@PathVariable String id, @RequestBody SampleService.AntibiogramRequest request) {
|
||||
return sampleService.saveAntibiogram(id, request);
|
||||
}
|
||||
|
||||
@PutMapping("/samples/{id}/therapy")
|
||||
public SampleService.SampleDetail saveTherapy(@PathVariable String id, @RequestBody SampleService.TherapyRequest request) {
|
||||
return sampleService.saveTherapy(id, request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.svencarstensen.muh.web;
|
||||
|
||||
import de.svencarstensen.muh.service.CatalogService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/session")
|
||||
public class SessionController {
|
||||
|
||||
private final CatalogService catalogService;
|
||||
|
||||
public SessionController(CatalogService catalogService) {
|
||||
this.catalogService = catalogService;
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public List<CatalogService.UserOption> activeUsers() {
|
||||
return catalogService.activeCatalogSummary().users();
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public CatalogService.UserOption login(@RequestBody LoginRequest request) {
|
||||
return catalogService.loginByCode(request.code());
|
||||
}
|
||||
|
||||
@PostMapping("/password-login")
|
||||
public CatalogService.UserOption passwordLogin(@RequestBody PasswordLoginRequest request) {
|
||||
return catalogService.loginWithPassword(request.identifier(), request.password());
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public CatalogService.UserOption register(@RequestBody RegistrationRequest request) {
|
||||
return catalogService.registerCustomer(new CatalogService.RegistrationMutation(
|
||||
request.companyName(),
|
||||
request.address(),
|
||||
request.email(),
|
||||
request.password()
|
||||
));
|
||||
}
|
||||
|
||||
public record LoginRequest(@NotBlank String code) {
|
||||
}
|
||||
|
||||
public record PasswordLoginRequest(@NotBlank String identifier, @NotBlank String password) {
|
||||
}
|
||||
|
||||
public record RegistrationRequest(
|
||||
@NotBlank String companyName,
|
||||
@NotBlank String address,
|
||||
@NotBlank String email,
|
||||
@NotBlank String password
|
||||
) {
|
||||
}
|
||||
}
|
||||
29
backend/src/main/resources/application.yml
Normal file
29
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
server:
|
||||
port: 8090
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: muh-backend
|
||||
data:
|
||||
mongodb:
|
||||
uri: mongodb://192.168.180.25:27017/muh
|
||||
jackson:
|
||||
time-zone: Europe/Berlin
|
||||
mail:
|
||||
host: ${MUH_MAIL_HOST:}
|
||||
port: ${MUH_MAIL_PORT:587}
|
||||
username: ${MUH_MAIL_USERNAME:}
|
||||
password: ${MUH_MAIL_PASSWORD:}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: ${MUH_MAIL_AUTH:false}
|
||||
starttls:
|
||||
enable: ${MUH_MAIL_STARTTLS:false}
|
||||
|
||||
muh:
|
||||
cors:
|
||||
allowed-origins: ${MUH_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000}
|
||||
mail:
|
||||
enabled: ${MUH_MAIL_ENABLED:false}
|
||||
from: ${MUH_MAIL_FROM:no-reply@muh.local}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.svencarstensen.muh.service;
|
||||
|
||||
import de.svencarstensen.muh.domain.PathogenKind;
|
||||
import de.svencarstensen.muh.domain.QuarterFinding;
|
||||
import de.svencarstensen.muh.domain.QuarterKey;
|
||||
import de.svencarstensen.muh.domain.SampleWorkflowStep;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class SampleWorkflowRulesTest {
|
||||
|
||||
@Test
|
||||
void shouldSkipAntibiogramIfNoBacterialGrowthExists() {
|
||||
List<QuarterFinding> quarters = List.of(
|
||||
new QuarterFinding(QuarterKey.SINGLE, false, "ng", "NG", "Kein Wachstum", PathogenKind.NO_GROWTH, null, null)
|
||||
);
|
||||
|
||||
assertEquals(SampleWorkflowStep.THERAPY, SampleWorkflowRules.nextStepAfterAnamnesis(quarters));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnOnlyOneAntibiogramTargetForSamePathogen() {
|
||||
List<QuarterFinding> quarters = List.of(
|
||||
new QuarterFinding(QuarterKey.LEFT_FRONT, false, "sau", "SAU", "Staph. aureus", PathogenKind.BACTERIAL, null, null),
|
||||
new QuarterFinding(QuarterKey.RIGHT_FRONT, false, "sau", "SAU", "Staph. aureus", PathogenKind.BACTERIAL, null, null),
|
||||
new QuarterFinding(QuarterKey.LEFT_REAR, false, "eco", "ECO", "E. coli", PathogenKind.BACTERIAL, null, null)
|
||||
);
|
||||
|
||||
assertEquals(List.of(QuarterKey.LEFT_FRONT, QuarterKey.LEFT_REAR), SampleWorkflowRules.antibiogramTargets(quarters));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user