feat: extend farmer data model with complete address fields and customer number
Backend: - Add customerNumber, companyName, contactPerson, street, houseNumber, postalCode, city, phoneNumber to Farmer domain model - Update FarmerRow and FarmerMutation records with new fields - Update repositories, services and controllers for new farmer structure - Fix CORS configuration to allow credentials - Remove unused authorizationService and imports - Update data migration for new farmer schema Frontend: - Update FarmerRow and FarmerOption interfaces - Extend AdministrationPage with new farmer form fields - Update SampleRegistrationPage and SearchFarmerPage for new structure
This commit is contained in:
@@ -18,7 +18,8 @@ public class CorsConfig {
|
|||||||
configuration.setAllowedOrigins(allowedOrigins);
|
configuration.setAllowedOrigins(allowedOrigins);
|
||||||
configuration.setAllowedHeaders(List.of("*"));
|
configuration.setAllowedHeaders(List.of("*"));
|
||||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
configuration.setAllowCredentials(false);
|
configuration.setAllowCredentials(true);
|
||||||
|
configuration.setExposedHeaders(List.of("Authorization"));
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
source.registerCorsConfiguration("/api/**", configuration);
|
source.registerCorsConfiguration("/api/**", configuration);
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ public record Farmer(
|
|||||||
@Id String id,
|
@Id String id,
|
||||||
String accountId,
|
String accountId,
|
||||||
String businessKey,
|
String businessKey,
|
||||||
String name,
|
String customerNumber,
|
||||||
|
String companyName,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
String email,
|
String email,
|
||||||
|
String phoneNumber,
|
||||||
boolean active,
|
boolean active,
|
||||||
String supersedesId,
|
String supersedesId,
|
||||||
LocalDateTime createdAt,
|
LocalDateTime createdAt,
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import org.springframework.data.mongodb.repository.MongoRepository;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface FarmerRepository extends MongoRepository<Farmer, String> {
|
public interface FarmerRepository extends MongoRepository<Farmer, String> {
|
||||||
List<Farmer> findByActiveTrueOrderByNameAsc();
|
List<Farmer> findByActiveTrueOrderByCompanyNameAsc();
|
||||||
|
|
||||||
List<Farmer> findByAccountIdOrderByNameAsc(String accountId);
|
List<Farmer> findByAccountIdOrderByCompanyNameAsc(String accountId);
|
||||||
|
|
||||||
List<Farmer> findByAccountIdAndActiveTrueOrderByNameAsc(String accountId);
|
List<Farmer> findByAccountIdAndActiveTrueOrderByCompanyNameAsc(String accountId);
|
||||||
|
|
||||||
List<Farmer> findByNameContainingIgnoreCaseOrderByNameAsc(String name);
|
List<Farmer> findByCompanyNameContainingIgnoreCaseOrderByCompanyNameAsc(String companyName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import de.svencarstensen.muh.repository.FarmerRepository;
|
|||||||
import de.svencarstensen.muh.repository.MedicationCatalogRepository;
|
import de.svencarstensen.muh.repository.MedicationCatalogRepository;
|
||||||
import de.svencarstensen.muh.repository.PathogenCatalogRepository;
|
import de.svencarstensen.muh.repository.PathogenCatalogRepository;
|
||||||
import de.svencarstensen.muh.security.AuthTokenService;
|
import de.svencarstensen.muh.security.AuthTokenService;
|
||||||
import de.svencarstensen.muh.security.AuthorizationService;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
@@ -42,7 +41,7 @@ public class CatalogService {
|
|||||||
|
|
||||||
private static final Comparator<FarmerRow> FARMER_ROW_COMPARATOR = Comparator
|
private static final Comparator<FarmerRow> FARMER_ROW_COMPARATOR = Comparator
|
||||||
.comparing(FarmerRow::active).reversed()
|
.comparing(FarmerRow::active).reversed()
|
||||||
.thenComparing(FarmerRow::name, String.CASE_INSENSITIVE_ORDER)
|
.thenComparing(FarmerRow::companyName, String.CASE_INSENSITIVE_ORDER)
|
||||||
.thenComparing(FarmerRow::updatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
|
.thenComparing(FarmerRow::updatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
|
||||||
|
|
||||||
private static final Comparator<MedicationRow> MEDICATION_ROW_COMPARATOR = Comparator
|
private static final Comparator<MedicationRow> MEDICATION_ROW_COMPARATOR = Comparator
|
||||||
@@ -67,7 +66,6 @@ public class CatalogService {
|
|||||||
private final AppUserRepository appUserRepository;
|
private final AppUserRepository appUserRepository;
|
||||||
private final MongoTemplate mongoTemplate;
|
private final MongoTemplate mongoTemplate;
|
||||||
private final AuthTokenService authTokenService;
|
private final AuthTokenService authTokenService;
|
||||||
private final AuthorizationService authorizationService;
|
|
||||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
public CatalogService(
|
public CatalogService(
|
||||||
@@ -77,8 +75,7 @@ public class CatalogService {
|
|||||||
AntibioticCatalogRepository antibioticRepository,
|
AntibioticCatalogRepository antibioticRepository,
|
||||||
AppUserRepository appUserRepository,
|
AppUserRepository appUserRepository,
|
||||||
MongoTemplate mongoTemplate,
|
MongoTemplate mongoTemplate,
|
||||||
AuthTokenService authTokenService,
|
AuthTokenService authTokenService
|
||||||
AuthorizationService authorizationService
|
|
||||||
) {
|
) {
|
||||||
this.farmerRepository = farmerRepository;
|
this.farmerRepository = farmerRepository;
|
||||||
this.medicationRepository = medicationRepository;
|
this.medicationRepository = medicationRepository;
|
||||||
@@ -87,7 +84,6 @@ public class CatalogService {
|
|||||||
this.appUserRepository = appUserRepository;
|
this.appUserRepository = appUserRepository;
|
||||||
this.mongoTemplate = mongoTemplate;
|
this.mongoTemplate = mongoTemplate;
|
||||||
this.authTokenService = authTokenService;
|
this.authTokenService = authTokenService;
|
||||||
this.authorizationService = authorizationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActiveCatalogSummary activeCatalogSummary(String actorId) {
|
public ActiveCatalogSummary activeCatalogSummary(String actorId) {
|
||||||
@@ -113,7 +109,7 @@ public class CatalogService {
|
|||||||
|
|
||||||
// Hilfsmethoden für Datenzugriff (immer nur eigene Daten des Hauptbenutzers)
|
// Hilfsmethoden für Datenzugriff (immer nur eigene Daten des Hauptbenutzers)
|
||||||
private List<Farmer> listActiveFarmersForActor(AppUser actor) {
|
private List<Farmer> listActiveFarmersForActor(AppUser actor) {
|
||||||
return farmerRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor));
|
return farmerRepository.findByAccountIdAndActiveTrueOrderByCompanyNameAsc(resolveAccountId(actor));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MedicationCatalogItem> listActiveMedicationsForActor(AppUser actor) {
|
private List<MedicationCatalogItem> listActiveMedicationsForActor(AppUser actor) {
|
||||||
@@ -129,7 +125,7 @@ public class CatalogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<FarmerRow> listFarmerRowsForActor(AppUser actor) {
|
private List<FarmerRow> listFarmerRowsForActor(AppUser actor) {
|
||||||
return farmerRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream()
|
return farmerRepository.findByAccountIdOrderByCompanyNameAsc(resolveAccountId(actor)).stream()
|
||||||
.map(this::toFarmerRow)
|
.map(this::toFarmerRow)
|
||||||
.sorted(FARMER_ROW_COMPARATOR)
|
.sorted(FARMER_ROW_COMPARATOR)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -160,7 +156,7 @@ public class CatalogService {
|
|||||||
AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
|
AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
|
||||||
String accountId = resolveAccountId(actor);
|
String accountId = resolveAccountId(actor);
|
||||||
for (FarmerMutation mutation : mutations) {
|
for (FarmerMutation mutation : mutations) {
|
||||||
if (isBlank(mutation.name())) {
|
if (isBlank(mutation.companyName())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -169,8 +165,15 @@ public class CatalogService {
|
|||||||
null,
|
null,
|
||||||
accountId,
|
accountId,
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
mutation.name().trim(),
|
blankToNull(mutation.customerNumber()),
|
||||||
|
mutation.companyName().trim(),
|
||||||
|
blankToNull(mutation.contactPerson()),
|
||||||
|
blankToNull(mutation.street()),
|
||||||
|
blankToNull(mutation.houseNumber()),
|
||||||
|
blankToNull(mutation.postalCode()),
|
||||||
|
blankToNull(mutation.city()),
|
||||||
blankToNull(mutation.email()),
|
blankToNull(mutation.email()),
|
||||||
|
blankToNull(mutation.phoneNumber()),
|
||||||
mutation.active(),
|
mutation.active(),
|
||||||
null,
|
null,
|
||||||
now,
|
now,
|
||||||
@@ -185,15 +188,29 @@ public class CatalogService {
|
|||||||
if (!accountId.equals(existing.accountId())) {
|
if (!accountId.equals(existing.accountId())) {
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt");
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt");
|
||||||
}
|
}
|
||||||
boolean changed = !existing.name().equals(mutation.name().trim())
|
boolean changed = !existing.companyName().equals(mutation.companyName().trim())
|
||||||
|| !safeEquals(existing.email(), blankToNull(mutation.email()));
|
|| !safeEquals(existing.customerNumber(), blankToNull(mutation.customerNumber()))
|
||||||
|
|| !safeEquals(existing.contactPerson(), blankToNull(mutation.contactPerson()))
|
||||||
|
|| !safeEquals(existing.street(), blankToNull(mutation.street()))
|
||||||
|
|| !safeEquals(existing.houseNumber(), blankToNull(mutation.houseNumber()))
|
||||||
|
|| !safeEquals(existing.postalCode(), blankToNull(mutation.postalCode()))
|
||||||
|
|| !safeEquals(existing.city(), blankToNull(mutation.city()))
|
||||||
|
|| !safeEquals(existing.email(), blankToNull(mutation.email()))
|
||||||
|
|| !safeEquals(existing.phoneNumber(), blankToNull(mutation.phoneNumber()));
|
||||||
if (changed) {
|
if (changed) {
|
||||||
farmerRepository.save(new Farmer(
|
farmerRepository.save(new Farmer(
|
||||||
existing.id(),
|
existing.id(),
|
||||||
existing.accountId(),
|
existing.accountId(),
|
||||||
existing.businessKey(),
|
existing.businessKey(),
|
||||||
existing.name(),
|
existing.customerNumber(),
|
||||||
|
existing.companyName(),
|
||||||
|
existing.contactPerson(),
|
||||||
|
existing.street(),
|
||||||
|
existing.houseNumber(),
|
||||||
|
existing.postalCode(),
|
||||||
|
existing.city(),
|
||||||
existing.email(),
|
existing.email(),
|
||||||
|
existing.phoneNumber(),
|
||||||
false,
|
false,
|
||||||
existing.supersedesId(),
|
existing.supersedesId(),
|
||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
@@ -203,8 +220,15 @@ public class CatalogService {
|
|||||||
null,
|
null,
|
||||||
existing.accountId(),
|
existing.accountId(),
|
||||||
existing.businessKey(),
|
existing.businessKey(),
|
||||||
mutation.name().trim(),
|
blankToNull(mutation.customerNumber()),
|
||||||
|
mutation.companyName().trim(),
|
||||||
|
blankToNull(mutation.contactPerson()),
|
||||||
|
blankToNull(mutation.street()),
|
||||||
|
blankToNull(mutation.houseNumber()),
|
||||||
|
blankToNull(mutation.postalCode()),
|
||||||
|
blankToNull(mutation.city()),
|
||||||
blankToNull(mutation.email()),
|
blankToNull(mutation.email()),
|
||||||
|
blankToNull(mutation.phoneNumber()),
|
||||||
mutation.active(),
|
mutation.active(),
|
||||||
existing.id(),
|
existing.id(),
|
||||||
now,
|
now,
|
||||||
@@ -217,8 +241,15 @@ public class CatalogService {
|
|||||||
existing.id(),
|
existing.id(),
|
||||||
existing.accountId(),
|
existing.accountId(),
|
||||||
existing.businessKey(),
|
existing.businessKey(),
|
||||||
existing.name(),
|
existing.customerNumber(),
|
||||||
|
existing.companyName(),
|
||||||
|
existing.contactPerson(),
|
||||||
|
existing.street(),
|
||||||
|
existing.houseNumber(),
|
||||||
|
existing.postalCode(),
|
||||||
|
existing.city(),
|
||||||
existing.email(),
|
existing.email(),
|
||||||
|
existing.phoneNumber(),
|
||||||
mutation.active(),
|
mutation.active(),
|
||||||
existing.supersedesId(),
|
existing.supersedesId(),
|
||||||
existing.createdAt(),
|
existing.createdAt(),
|
||||||
@@ -746,8 +777,15 @@ public class CatalogService {
|
|||||||
return new FarmerRow(
|
return new FarmerRow(
|
||||||
farmer.id(),
|
farmer.id(),
|
||||||
farmer.businessKey(),
|
farmer.businessKey(),
|
||||||
farmer.name(),
|
farmer.customerNumber(),
|
||||||
|
farmer.companyName(),
|
||||||
|
farmer.contactPerson(),
|
||||||
|
farmer.street(),
|
||||||
|
farmer.houseNumber(),
|
||||||
|
farmer.postalCode(),
|
||||||
|
farmer.city(),
|
||||||
farmer.email(),
|
farmer.email(),
|
||||||
|
farmer.phoneNumber(),
|
||||||
farmer.active(),
|
farmer.active(),
|
||||||
farmer.updatedAt()
|
farmer.updatedAt()
|
||||||
);
|
);
|
||||||
@@ -812,7 +850,7 @@ public class CatalogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private FarmerOption toFarmerOption(Farmer farmer) {
|
private FarmerOption toFarmerOption(Farmer farmer) {
|
||||||
return new FarmerOption(farmer.businessKey(), farmer.name(), farmer.email());
|
return new FarmerOption(farmer.businessKey(), farmer.companyName(), farmer.contactPerson(), farmer.email());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MedicationOption toMedicationOption(MedicationCatalogItem item) {
|
private MedicationOption toMedicationOption(MedicationCatalogItem item) {
|
||||||
@@ -1077,20 +1115,31 @@ public class CatalogService {
|
|||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
// Migriere Farmers ohne accountId
|
// Migriere Farmers ohne accountId oder mit altem Schema
|
||||||
farmerRepository.findAll().stream()
|
farmerRepository.findAll().stream()
|
||||||
.filter(farmer -> isBlank(farmer.accountId()))
|
.filter(farmer -> isBlank(farmer.accountId()) || isBlank(farmer.companyName()))
|
||||||
.forEach(farmer -> farmerRepository.save(new Farmer(
|
.forEach(farmer -> {
|
||||||
|
// Wenn companyName fehlt, nutze businessKey als Fallback
|
||||||
|
String companyName = isBlank(farmer.companyName()) ? farmer.businessKey() : farmer.companyName();
|
||||||
|
farmerRepository.save(new Farmer(
|
||||||
farmer.id(),
|
farmer.id(),
|
||||||
defaultAccountId,
|
isBlank(farmer.accountId()) ? defaultAccountId : farmer.accountId(),
|
||||||
farmer.businessKey(),
|
farmer.businessKey(),
|
||||||
farmer.name(),
|
null, // customerNumber
|
||||||
|
companyName,
|
||||||
|
null, // contactPerson
|
||||||
|
null, // street
|
||||||
|
null, // houseNumber
|
||||||
|
null, // postalCode
|
||||||
|
null, // city
|
||||||
farmer.email(),
|
farmer.email(),
|
||||||
|
null, // phoneNumber
|
||||||
farmer.active(),
|
farmer.active(),
|
||||||
farmer.supersedesId(),
|
farmer.supersedesId(),
|
||||||
farmer.createdAt(),
|
farmer.createdAt(),
|
||||||
now
|
now
|
||||||
)));
|
));
|
||||||
|
});
|
||||||
|
|
||||||
// Migriere Medications ohne accountId
|
// Migriere Medications ohne accountId
|
||||||
medicationRepository.findAll().stream()
|
medicationRepository.findAll().stream()
|
||||||
@@ -1213,7 +1262,7 @@ public class CatalogService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record FarmerOption(String businessKey, String name, String email) {
|
public record FarmerOption(String businessKey, String companyName, String contactPerson, String email) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record MedicationOption(String businessKey, String name, MedicationCategory category) {
|
public record MedicationOption(String businessKey, String name, MedicationCategory category) {
|
||||||
@@ -1249,14 +1298,33 @@ public class CatalogService {
|
|||||||
public record FarmerRow(
|
public record FarmerRow(
|
||||||
String id,
|
String id,
|
||||||
String businessKey,
|
String businessKey,
|
||||||
String name,
|
String customerNumber,
|
||||||
|
String companyName,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
String email,
|
String email,
|
||||||
|
String phoneNumber,
|
||||||
boolean active,
|
boolean active,
|
||||||
LocalDateTime updatedAt
|
LocalDateTime updatedAt
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record FarmerMutation(String id, String name, String email, boolean active) {
|
public record FarmerMutation(
|
||||||
|
String id,
|
||||||
|
String customerNumber,
|
||||||
|
String companyName,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
|
String email,
|
||||||
|
String phoneNumber,
|
||||||
|
boolean active
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record MedicationRow(
|
public record MedicationRow(
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class PortalService {
|
|||||||
LocalDate date
|
LocalDate date
|
||||||
) {
|
) {
|
||||||
List<CatalogService.FarmerOption> matchingFarmers = catalogService.activeCatalogSummary(actorId).farmers().stream()
|
List<CatalogService.FarmerOption> matchingFarmers = catalogService.activeCatalogSummary(actorId).farmers().stream()
|
||||||
.filter(farmer -> farmerQuery == null || farmerQuery.isBlank() || farmer.name().toLowerCase(Locale.ROOT).contains(farmerQuery.toLowerCase(Locale.ROOT)))
|
.filter(farmer -> farmerQuery == null || farmerQuery.isBlank() || farmer.companyName().toLowerCase(Locale.ROOT).contains(farmerQuery.toLowerCase(Locale.ROOT)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<PortalSampleRow> sampleRows;
|
List<PortalSampleRow> sampleRows;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ public class SampleService {
|
|||||||
null,
|
null,
|
||||||
sampleNumber,
|
sampleNumber,
|
||||||
farmer.businessKey(),
|
farmer.businessKey(),
|
||||||
farmer.name(),
|
farmer.companyName(),
|
||||||
farmer.email(),
|
farmer.email(),
|
||||||
request.cowNumber().trim(),
|
request.cowNumber().trim(),
|
||||||
blankToNull(request.cowName()),
|
blankToNull(request.cowName()),
|
||||||
@@ -178,7 +178,7 @@ public class SampleService {
|
|||||||
existing.id(),
|
existing.id(),
|
||||||
existing.sampleNumber(),
|
existing.sampleNumber(),
|
||||||
farmer.businessKey(),
|
farmer.businessKey(),
|
||||||
farmer.name(),
|
farmer.companyName(),
|
||||||
farmer.email(),
|
farmer.email(),
|
||||||
request.cowNumber().trim(),
|
request.cowNumber().trim(),
|
||||||
blankToNull(request.cowName()),
|
blankToNull(request.cowName()),
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export type UserRole = "ADMIN" | "CUSTOMER";
|
|||||||
|
|
||||||
export interface FarmerOption {
|
export interface FarmerOption {
|
||||||
businessKey: string;
|
businessKey: string;
|
||||||
name: string;
|
companyName: string;
|
||||||
|
contactPerson: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,8 +205,15 @@ export interface SampleDetail {
|
|||||||
export interface FarmerRow {
|
export interface FarmerRow {
|
||||||
id: string;
|
id: string;
|
||||||
businessKey: string;
|
businessKey: string;
|
||||||
name: string;
|
customerNumber: string | null;
|
||||||
|
companyName: string;
|
||||||
|
contactPerson: string | null;
|
||||||
|
street: string | null;
|
||||||
|
houseNumber: string | null;
|
||||||
|
postalCode: string | null;
|
||||||
|
city: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
|
phoneNumber: string | null;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,23 @@ type DatasetKey = "farmers" | "medications" | "pathogens" | "antibiotics";
|
|||||||
type EditableRow = {
|
type EditableRow = {
|
||||||
id: string;
|
id: string;
|
||||||
businessKey: string;
|
businessKey: string;
|
||||||
name: string;
|
// Farmer fields
|
||||||
active: boolean;
|
customerNumber?: string;
|
||||||
updatedAt: string;
|
companyName?: string;
|
||||||
|
contactPerson?: string;
|
||||||
|
street?: string;
|
||||||
|
houseNumber?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
city?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
// Other fields
|
||||||
|
name?: string;
|
||||||
category?: MedicationCategory;
|
category?: MedicationCategory;
|
||||||
code?: string;
|
code?: string;
|
||||||
kind?: PathogenKind;
|
kind?: PathogenKind;
|
||||||
|
active: boolean;
|
||||||
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DatasetsState = Record<DatasetKey, EditableRow[]>;
|
type DatasetsState = Record<DatasetKey, EditableRow[]>;
|
||||||
@@ -33,13 +43,51 @@ const DATASET_TITLES: Record<DatasetKey, string> = {
|
|||||||
antibiotics: "Die Verwaltung der Antibiogramme",
|
antibiotics: "Die Verwaltung der Antibiogramme",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FARMER_REQUIRED_FIELDS: Array<keyof EditableRow> = [
|
||||||
|
"companyName",
|
||||||
|
"customerNumber",
|
||||||
|
"contactPerson",
|
||||||
|
"street",
|
||||||
|
"houseNumber",
|
||||||
|
"postalCode",
|
||||||
|
"city",
|
||||||
|
"email",
|
||||||
|
"phoneNumber",
|
||||||
|
];
|
||||||
|
|
||||||
|
function isBlankValue(value: string | undefined) {
|
||||||
|
return !value?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFarmerFieldInvalid(row: EditableRow, field: keyof EditableRow, showValidation: boolean) {
|
||||||
|
if (!showValidation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const value = row[field];
|
||||||
|
return typeof value !== "string" || isBlankValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFarmerRowIncomplete(row: EditableRow) {
|
||||||
|
return FARMER_REQUIRED_FIELDS.some((field) => {
|
||||||
|
const value = row[field];
|
||||||
|
return typeof value !== "string" || isBlankValue(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOverview(overview: AdministrationOverview): DatasetsState {
|
function normalizeOverview(overview: AdministrationOverview): DatasetsState {
|
||||||
return {
|
return {
|
||||||
farmers: overview.farmers.map((entry) => ({
|
farmers: overview.farmers.map((entry) => ({
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
businessKey: entry.businessKey,
|
businessKey: entry.businessKey,
|
||||||
name: entry.name,
|
customerNumber: entry.customerNumber ?? "",
|
||||||
|
companyName: entry.companyName,
|
||||||
|
contactPerson: entry.contactPerson ?? "",
|
||||||
|
street: entry.street ?? "",
|
||||||
|
houseNumber: entry.houseNumber ?? "",
|
||||||
|
postalCode: entry.postalCode ?? "",
|
||||||
|
city: entry.city ?? "",
|
||||||
email: entry.email ?? "",
|
email: entry.email ?? "",
|
||||||
|
phoneNumber: entry.phoneNumber ?? "",
|
||||||
active: entry.active,
|
active: entry.active,
|
||||||
updatedAt: entry.updatedAt,
|
updatedAt: entry.updatedAt,
|
||||||
})),
|
})),
|
||||||
@@ -74,7 +122,21 @@ function normalizeOverview(overview: AdministrationOverview): DatasetsState {
|
|||||||
function emptyRow(dataset: DatasetKey): EditableRow {
|
function emptyRow(dataset: DatasetKey): EditableRow {
|
||||||
switch (dataset) {
|
switch (dataset) {
|
||||||
case "farmers":
|
case "farmers":
|
||||||
return { id: "", businessKey: "", name: "", email: "", active: true, updatedAt: new Date().toISOString() };
|
return {
|
||||||
|
id: "",
|
||||||
|
businessKey: "",
|
||||||
|
customerNumber: "",
|
||||||
|
companyName: "",
|
||||||
|
contactPerson: "",
|
||||||
|
street: "",
|
||||||
|
houseNumber: "",
|
||||||
|
postalCode: "",
|
||||||
|
city: "",
|
||||||
|
email: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
active: true,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
case "medications":
|
case "medications":
|
||||||
return {
|
return {
|
||||||
id: "",
|
id: "",
|
||||||
@@ -133,6 +195,7 @@ export default function AdministrationPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const rows = useMemo(() => datasets?.[selectedDataset] ?? [], [datasets, selectedDataset]);
|
const rows = useMemo(() => datasets?.[selectedDataset] ?? [], [datasets, selectedDataset]);
|
||||||
|
const isFarmerDataset = selectedDataset === "farmers";
|
||||||
|
|
||||||
function updateRow(index: number, patch: Partial<EditableRow>) {
|
function updateRow(index: number, patch: Partial<EditableRow>) {
|
||||||
setDatasets((current) => {
|
setDatasets((current) => {
|
||||||
@@ -166,7 +229,11 @@ export default function AdministrationPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setShowValidation(true);
|
setShowValidation(true);
|
||||||
if (rows.some((row) => !row.name.trim())) {
|
if (selectedDataset === "farmers" && rows.some(isFarmerRowIncomplete)) {
|
||||||
|
setMessage("Bitte alle Pflichtfelder fuer den Landwirt ausfuellen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedDataset !== "farmers" && rows.some((row) => !row.name?.trim())) {
|
||||||
setMessage("Bitte alle Pflichtfelder ausfuellen.");
|
setMessage("Bitte alle Pflichtfelder ausfuellen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -178,8 +245,15 @@ export default function AdministrationPage() {
|
|||||||
case "farmers":
|
case "farmers":
|
||||||
response = await apiPost<EditableRow[]>("/catalog/farmers", rows.map((row) => ({
|
response = await apiPost<EditableRow[]>("/catalog/farmers", rows.map((row) => ({
|
||||||
id: row.id || null,
|
id: row.id || null,
|
||||||
name: row.name,
|
customerNumber: row.customerNumber || null,
|
||||||
|
companyName: row.companyName,
|
||||||
|
contactPerson: row.contactPerson || null,
|
||||||
|
street: row.street || null,
|
||||||
|
houseNumber: row.houseNumber || null,
|
||||||
|
postalCode: row.postalCode || null,
|
||||||
|
city: row.city || null,
|
||||||
email: row.email || null,
|
email: row.email || null,
|
||||||
|
phoneNumber: row.phoneNumber || null,
|
||||||
active: row.active,
|
active: row.active,
|
||||||
})));
|
})));
|
||||||
break;
|
break;
|
||||||
@@ -245,12 +319,130 @@ export default function AdministrationPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isFarmerDataset ? (
|
||||||
|
<div className="admin-farmer-list">
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<article key={`${row.id || "new"}-${index}`} className="admin-farmer-card">
|
||||||
|
<div className="admin-farmer-card__row admin-farmer-card__row--primary">
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Firmenname</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "companyName", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.companyName ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { companyName: event.target.value })}
|
||||||
|
placeholder="Firmenname"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Ansprechpartner</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "contactPerson", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.contactPerson ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { contactPerson: event.target.value })}
|
||||||
|
placeholder="Ansprechpartner"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Kunden-Nr.</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "customerNumber", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.customerNumber ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { customerNumber: event.target.value })}
|
||||||
|
placeholder="Kunden-Nr."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-farmer-card__row admin-farmer-card__row--address">
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>PLZ</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "postalCode", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.postalCode ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { postalCode: event.target.value })}
|
||||||
|
placeholder="PLZ"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Ort</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "city", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.city ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { city: event.target.value })}
|
||||||
|
placeholder="Ort"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Straße</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "street", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.street ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { street: event.target.value })}
|
||||||
|
placeholder="Straße"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Hausnummer</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "houseNumber", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.houseNumber ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { houseNumber: event.target.value })}
|
||||||
|
placeholder="Hausnummer"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-farmer-card__row admin-farmer-card__row--contact">
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>E-Mail</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "email", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.email ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { email: event.target.value })}
|
||||||
|
placeholder="E-Mail"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="field field--required">
|
||||||
|
<span>Telefon</span>
|
||||||
|
<input
|
||||||
|
className={isFarmerFieldInvalid(row, "phoneNumber", showValidation) ? "is-invalid" : ""}
|
||||||
|
value={row.phoneNumber ?? ""}
|
||||||
|
onChange={(event) => updateRow(index, { phoneNumber: event.target.value })}
|
||||||
|
placeholder="Telefon"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-farmer-card__row admin-farmer-card__row--toggle">
|
||||||
|
<label className="field">
|
||||||
|
<span>Aktiv</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`eye-button admin-farmer-card__toggle ${row.active ? "is-active" : "is-inactive"}`}
|
||||||
|
onClick={() => updateRow(index, { active: !row.active })}
|
||||||
|
>
|
||||||
|
{row.active ? "sichtbar" : "inaktiv"}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="table-shell">
|
<div className="table-shell">
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="required-label">Name</th>
|
<th className="required-label">Name</th>
|
||||||
{selectedDataset === "farmers" ? <th>E-Mail</th> : null}
|
|
||||||
{selectedDataset === "medications" ? <th>Kategorie</th> : null}
|
{selectedDataset === "medications" ? <th>Kategorie</th> : null}
|
||||||
{selectedDataset === "pathogens" || selectedDataset === "antibiotics" ? <th>Kuerzel</th> : null}
|
{selectedDataset === "pathogens" || selectedDataset === "antibiotics" ? <th>Kuerzel</th> : null}
|
||||||
{selectedDataset === "pathogens" ? <th>Typ</th> : null}
|
{selectedDataset === "pathogens" ? <th>Typ</th> : null}
|
||||||
@@ -262,19 +454,11 @@ export default function AdministrationPage() {
|
|||||||
<tr key={`${row.id || "new"}-${index}`}>
|
<tr key={`${row.id || "new"}-${index}`}>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
className={showValidation && !row.name.trim() ? "is-invalid" : ""}
|
className={showValidation && !row.name?.trim() ? "is-invalid" : ""}
|
||||||
value={row.name}
|
value={row.name ?? ""}
|
||||||
onChange={(event) => updateRow(index, { name: event.target.value })}
|
onChange={(event) => updateRow(index, { name: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{selectedDataset === "farmers" ? (
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
value={row.email ?? ""}
|
|
||||||
onChange={(event) => updateRow(index, { email: event.target.value })}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
) : null}
|
|
||||||
{selectedDataset === "medications" ? (
|
{selectedDataset === "medications" ? (
|
||||||
<td>
|
<td>
|
||||||
<select
|
<select
|
||||||
@@ -326,6 +510,7 @@ export default function AdministrationPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="page-actions page-actions--space-between">
|
<div className="page-actions page-actions--space-between">
|
||||||
<button type="button" className="secondary-button" onClick={addRow}>
|
<button type="button" className="secondary-button" onClick={addRow}>
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export default function SampleRegistrationPage() {
|
|||||||
>
|
>
|
||||||
{catalogs?.farmers.map((farmer) => (
|
{catalogs?.farmers.map((farmer) => (
|
||||||
<option key={farmer.businessKey} value={farmer.businessKey}>
|
<option key={farmer.businessKey} value={farmer.businessKey}>
|
||||||
{farmer.name}
|
{farmer.companyName}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function SearchFarmerPage() {
|
|||||||
`/portal/snapshot?farmerBusinessKey=${encodeURIComponent(farmer.businessKey)}`,
|
`/portal/snapshot?farmerBusinessKey=${encodeURIComponent(farmer.businessKey)}`,
|
||||||
);
|
);
|
||||||
setSamples(response.samples);
|
setSamples(response.samples);
|
||||||
setResultLabel(`Proben von ${farmer.name}`);
|
setResultLabel(`Proben von ${farmer.companyName}`);
|
||||||
setMessage(response.samples.length ? null : "Zu diesem Landwirt wurden noch keine Proben gefunden.");
|
setMessage(response.samples.length ? null : "Zu diesem Landwirt wurden noch keine Proben gefunden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export default function SearchFarmerPage() {
|
|||||||
className="user-card"
|
className="user-card"
|
||||||
onClick={() => void loadFarmerSamples(farmer)}
|
onClick={() => void loadFarmerSamples(farmer)}
|
||||||
>
|
>
|
||||||
<strong>{farmer.name}</strong>
|
<strong>{farmer.companyName}</strong>
|
||||||
<small>{farmer.email ?? "ohne E-Mail"}</small>
|
<small>{farmer.email ?? "ohne E-Mail"}</small>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -570,6 +570,46 @@ a {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-farmer-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(37, 49, 58, 0.08);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row--primary {
|
||||||
|
grid-template-columns: minmax(0, 1.8fr) minmax(0, 1.3fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row--address {
|
||||||
|
grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.05fr) minmax(0, 1.8fr) minmax(0, 0.9fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row--contact {
|
||||||
|
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row--toggle {
|
||||||
|
grid-template-columns: minmax(0, 180px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__toggle {
|
||||||
|
min-width: 140px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -590,6 +630,35 @@ a {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.admin-farmer-card__row--primary,
|
||||||
|
.admin-farmer-card__row--address,
|
||||||
|
.admin-farmer-card__row--contact {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row--toggle {
|
||||||
|
grid-template-columns: minmax(0, 180px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__toggle {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.admin-farmer-card {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-farmer-card__row--primary,
|
||||||
|
.admin-farmer-card__row--address,
|
||||||
|
.admin-farmer-card__row--contact,
|
||||||
|
.admin-farmer-card__row--toggle {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status-pill,
|
.status-pill,
|
||||||
.info-chip {
|
.info-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user