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:
2026-03-18 21:13:29 +01:00
parent f9b83a166d
commit e7a18cd339
12 changed files with 464 additions and 128 deletions

View File

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

View File

@@ -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,

View File

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

View File

@@ -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 -> {
farmer.id(), // Wenn companyName fehlt, nutze businessKey als Fallback
defaultAccountId, String companyName = isBlank(farmer.companyName()) ? farmer.businessKey() : farmer.companyName();
farmer.businessKey(), farmerRepository.save(new Farmer(
farmer.name(), farmer.id(),
farmer.email(), isBlank(farmer.accountId()) ? defaultAccountId : farmer.accountId(),
farmer.active(), farmer.businessKey(),
farmer.supersedesId(), null, // customerNumber
farmer.createdAt(), companyName,
now null, // contactPerson
))); null, // street
null, // houseNumber
null, // postalCode
null, // city
farmer.email(),
null, // phoneNumber
farmer.active(),
farmer.supersedesId(),
farmer.createdAt(),
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(

View File

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

View File

@@ -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;

View File

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

View File

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

View File

@@ -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,87 +319,198 @@ export default function AdministrationPage() {
</div> </div>
</div> </div>
<div className="table-shell"> {isFarmerDataset ? (
<table className="data-table"> <div className="admin-farmer-list">
<thead> {rows.map((row, index) => (
<tr> <article key={`${row.id || "new"}-${index}`} className="admin-farmer-card">
<th className="required-label">Name</th> <div className="admin-farmer-card__row admin-farmer-card__row--primary">
{selectedDataset === "farmers" ? <th>E-Mail</th> : null} <label className="field field--required">
{selectedDataset === "medications" ? <th>Kategorie</th> : null} <span>Firmenname</span>
{selectedDataset === "pathogens" || selectedDataset === "antibiotics" ? <th>Kuerzel</th> : null}
{selectedDataset === "pathogens" ? <th>Typ</th> : null}
<th>Aktiv</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={`${row.id || "new"}-${index}`}>
<td>
<input <input
className={showValidation && !row.name.trim() ? "is-invalid" : ""} className={isFarmerFieldInvalid(row, "companyName", showValidation) ? "is-invalid" : ""}
value={row.name} value={row.companyName ?? ""}
onChange={(event) => updateRow(index, { name: event.target.value })} onChange={(event) => updateRow(index, { companyName: event.target.value })}
placeholder="Firmenname"
required
/> />
</td> </label>
{selectedDataset === "farmers" ? ( <label className="field field--required">
<td> <span>Ansprechpartner</span>
<input <input
value={row.email ?? ""} className={isFarmerFieldInvalid(row, "contactPerson", showValidation) ? "is-invalid" : ""}
onChange={(event) => updateRow(index, { email: event.target.value })} value={row.contactPerson ?? ""}
/> onChange={(event) => updateRow(index, { contactPerson: event.target.value })}
</td> placeholder="Ansprechpartner"
) : null} required
{selectedDataset === "medications" ? ( />
<td> </label>
<select <label className="field field--required">
value={row.category} <span>Kunden-Nr.</span>
onChange={(event) => <input
updateRow(index, { category: event.target.value as MedicationCategory }) className={isFarmerFieldInvalid(row, "customerNumber", showValidation) ? "is-invalid" : ""}
} value={row.customerNumber ?? ""}
> onChange={(event) => updateRow(index, { customerNumber: event.target.value })}
<option value="IN_UDDER">ins Euter</option> placeholder="Kunden-Nr."
<option value="SYSTEMIC_ANTIBIOTIC">systemisch Antibiotika</option> required
<option value="SYSTEMIC_PAIN">systemisch Schmerzmittel</option> />
<option value="DRY_SEALER">Versiegler</option> </label>
<option value="DRY_ANTIBIOTIC">Trockenstellerprobe Antibiotika</option> </div>
</select>
</td> <div className="admin-farmer-card__row admin-farmer-card__row--address">
) : null} <label className="field field--required">
{selectedDataset === "pathogens" || selectedDataset === "antibiotics" ? ( <span>PLZ</span>
<td> <input
<input className={isFarmerFieldInvalid(row, "postalCode", showValidation) ? "is-invalid" : ""}
value={row.code ?? ""} value={row.postalCode ?? ""}
onChange={(event) => updateRow(index, { code: event.target.value })} onChange={(event) => updateRow(index, { postalCode: event.target.value })}
/> placeholder="PLZ"
</td> required
) : null} />
{selectedDataset === "pathogens" ? ( </label>
<td> <label className="field field--required">
<select <span>Ort</span>
value={row.kind} <input
onChange={(event) => updateRow(index, { kind: event.target.value as PathogenKind })} className={isFarmerFieldInvalid(row, "city", showValidation) ? "is-invalid" : ""}
> value={row.city ?? ""}
<option value="BACTERIAL">bakteriell</option> onChange={(event) => updateRow(index, { city: event.target.value })}
<option value="NO_GROWTH">kein Wachstum</option> placeholder="Ort"
<option value="CONTAMINATED">verunreinigt</option> required
<option value="OTHER">sonstiges</option> />
</select> </label>
</td> <label className="field field--required">
) : null} <span>Straße</span>
<td> <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 <button
type="button" type="button"
className={`eye-button ${row.active ? "is-active" : "is-inactive"}`} className={`eye-button admin-farmer-card__toggle ${row.active ? "is-active" : "is-inactive"}`}
onClick={() => updateRow(index, { active: !row.active })} onClick={() => updateRow(index, { active: !row.active })}
> >
{row.active ? "sichtbar" : "inaktiv"} {row.active ? "sichtbar" : "inaktiv"}
</button> </button>
</td> </label>
</div>
</article>
))}
</div>
) : (
<div className="table-shell">
<table className="data-table">
<thead>
<tr>
<th className="required-label">Name</th>
{selectedDataset === "medications" ? <th>Kategorie</th> : null}
{selectedDataset === "pathogens" || selectedDataset === "antibiotics" ? <th>Kuerzel</th> : null}
{selectedDataset === "pathogens" ? <th>Typ</th> : null}
<th>Aktiv</th>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {rows.map((row, index) => (
</div> <tr key={`${row.id || "new"}-${index}`}>
<td>
<input
className={showValidation && !row.name?.trim() ? "is-invalid" : ""}
value={row.name ?? ""}
onChange={(event) => updateRow(index, { name: event.target.value })}
/>
</td>
{selectedDataset === "medications" ? (
<td>
<select
value={row.category}
onChange={(event) =>
updateRow(index, { category: event.target.value as MedicationCategory })
}
>
<option value="IN_UDDER">ins Euter</option>
<option value="SYSTEMIC_ANTIBIOTIC">systemisch Antibiotika</option>
<option value="SYSTEMIC_PAIN">systemisch Schmerzmittel</option>
<option value="DRY_SEALER">Versiegler</option>
<option value="DRY_ANTIBIOTIC">Trockenstellerprobe Antibiotika</option>
</select>
</td>
) : null}
{selectedDataset === "pathogens" || selectedDataset === "antibiotics" ? (
<td>
<input
value={row.code ?? ""}
onChange={(event) => updateRow(index, { code: event.target.value })}
/>
</td>
) : null}
{selectedDataset === "pathogens" ? (
<td>
<select
value={row.kind}
onChange={(event) => updateRow(index, { kind: event.target.value as PathogenKind })}
>
<option value="BACTERIAL">bakteriell</option>
<option value="NO_GROWTH">kein Wachstum</option>
<option value="CONTAMINATED">verunreinigt</option>
<option value="OTHER">sonstiges</option>
</select>
</td>
) : null}
<td>
<button
type="button"
className={`eye-button ${row.active ? "is-active" : "is-inactive"}`}
onClick={() => updateRow(index, { active: !row.active })}
>
{row.active ? "sichtbar" : "inaktiv"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</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}>

View File

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

View File

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

View File

@@ -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;