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.setAllowedHeaders(List.of("*"));
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();
source.registerCorsConfiguration("/api/**", configuration);

View File

@@ -10,8 +10,15 @@ public record Farmer(
@Id String id,
String accountId,
String businessKey,
String name,
String customerNumber,
String companyName,
String contactPerson,
String street,
String houseNumber,
String postalCode,
String city,
String email,
String phoneNumber,
boolean active,
String supersedesId,
LocalDateTime createdAt,

View File

@@ -6,11 +6,11 @@ import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
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.PathogenCatalogRepository;
import de.svencarstensen.muh.security.AuthTokenService;
import de.svencarstensen.muh.security.AuthorizationService;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.data.mongodb.core.MongoTemplate;
@@ -42,7 +41,7 @@ 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::companyName, String.CASE_INSENSITIVE_ORDER)
.thenComparing(FarmerRow::updatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
private static final Comparator<MedicationRow> MEDICATION_ROW_COMPARATOR = Comparator
@@ -67,7 +66,6 @@ public class CatalogService {
private final AppUserRepository appUserRepository;
private final MongoTemplate mongoTemplate;
private final AuthTokenService authTokenService;
private final AuthorizationService authorizationService;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public CatalogService(
@@ -77,8 +75,7 @@ public class CatalogService {
AntibioticCatalogRepository antibioticRepository,
AppUserRepository appUserRepository,
MongoTemplate mongoTemplate,
AuthTokenService authTokenService,
AuthorizationService authorizationService
AuthTokenService authTokenService
) {
this.farmerRepository = farmerRepository;
this.medicationRepository = medicationRepository;
@@ -87,7 +84,6 @@ public class CatalogService {
this.appUserRepository = appUserRepository;
this.mongoTemplate = mongoTemplate;
this.authTokenService = authTokenService;
this.authorizationService = authorizationService;
}
public ActiveCatalogSummary activeCatalogSummary(String actorId) {
@@ -113,7 +109,7 @@ public class CatalogService {
// Hilfsmethoden für Datenzugriff (immer nur eigene Daten des Hauptbenutzers)
private List<Farmer> listActiveFarmersForActor(AppUser actor) {
return farmerRepository.findByAccountIdAndActiveTrueOrderByNameAsc(resolveAccountId(actor));
return farmerRepository.findByAccountIdAndActiveTrueOrderByCompanyNameAsc(resolveAccountId(actor));
}
private List<MedicationCatalogItem> listActiveMedicationsForActor(AppUser actor) {
@@ -129,7 +125,7 @@ public class CatalogService {
}
private List<FarmerRow> listFarmerRowsForActor(AppUser actor) {
return farmerRepository.findByAccountIdOrderByNameAsc(resolveAccountId(actor)).stream()
return farmerRepository.findByAccountIdOrderByCompanyNameAsc(resolveAccountId(actor)).stream()
.map(this::toFarmerRow)
.sorted(FARMER_ROW_COMPARATOR)
.toList();
@@ -160,7 +156,7 @@ public class CatalogService {
AppUser actor = requireActiveActor(actorId, "Nicht berechtigt");
String accountId = resolveAccountId(actor);
for (FarmerMutation mutation : mutations) {
if (isBlank(mutation.name())) {
if (isBlank(mutation.companyName())) {
continue;
}
LocalDateTime now = LocalDateTime.now();
@@ -169,8 +165,15 @@ public class CatalogService {
null,
accountId,
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.phoneNumber()),
mutation.active(),
null,
now,
@@ -185,15 +188,29 @@ public class CatalogService {
if (!accountId.equals(existing.accountId())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Nicht berechtigt");
}
boolean changed = !existing.name().equals(mutation.name().trim())
|| !safeEquals(existing.email(), blankToNull(mutation.email()));
boolean changed = !existing.companyName().equals(mutation.companyName().trim())
|| !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) {
farmerRepository.save(new Farmer(
existing.id(),
existing.accountId(),
existing.businessKey(),
existing.name(),
existing.customerNumber(),
existing.companyName(),
existing.contactPerson(),
existing.street(),
existing.houseNumber(),
existing.postalCode(),
existing.city(),
existing.email(),
existing.phoneNumber(),
false,
existing.supersedesId(),
existing.createdAt(),
@@ -203,8 +220,15 @@ public class CatalogService {
null,
existing.accountId(),
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.phoneNumber()),
mutation.active(),
existing.id(),
now,
@@ -217,8 +241,15 @@ public class CatalogService {
existing.id(),
existing.accountId(),
existing.businessKey(),
existing.name(),
existing.customerNumber(),
existing.companyName(),
existing.contactPerson(),
existing.street(),
existing.houseNumber(),
existing.postalCode(),
existing.city(),
existing.email(),
existing.phoneNumber(),
mutation.active(),
existing.supersedesId(),
existing.createdAt(),
@@ -746,8 +777,15 @@ public class CatalogService {
return new FarmerRow(
farmer.id(),
farmer.businessKey(),
farmer.name(),
farmer.customerNumber(),
farmer.companyName(),
farmer.contactPerson(),
farmer.street(),
farmer.houseNumber(),
farmer.postalCode(),
farmer.city(),
farmer.email(),
farmer.phoneNumber(),
farmer.active(),
farmer.updatedAt()
);
@@ -812,7 +850,7 @@ public class CatalogService {
}
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) {
@@ -1077,20 +1115,31 @@ public class CatalogService {
LocalDateTime now = LocalDateTime.now();
// Migriere Farmers ohne accountId
// Migriere Farmers ohne accountId oder mit altem Schema
farmerRepository.findAll().stream()
.filter(farmer -> isBlank(farmer.accountId()))
.forEach(farmer -> farmerRepository.save(new Farmer(
farmer.id(),
defaultAccountId,
farmer.businessKey(),
farmer.name(),
farmer.email(),
farmer.active(),
farmer.supersedesId(),
farmer.createdAt(),
now
)));
.filter(farmer -> isBlank(farmer.accountId()) || isBlank(farmer.companyName()))
.forEach(farmer -> {
// Wenn companyName fehlt, nutze businessKey als Fallback
String companyName = isBlank(farmer.companyName()) ? farmer.businessKey() : farmer.companyName();
farmerRepository.save(new Farmer(
farmer.id(),
isBlank(farmer.accountId()) ? defaultAccountId : farmer.accountId(),
farmer.businessKey(),
null, // customerNumber
companyName,
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
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) {
@@ -1249,14 +1298,33 @@ public class CatalogService {
public record FarmerRow(
String id,
String businessKey,
String name,
String customerNumber,
String companyName,
String contactPerson,
String street,
String houseNumber,
String postalCode,
String city,
String email,
String phoneNumber,
boolean active,
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(

View File

@@ -14,10 +14,8 @@ import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@Service

View File

@@ -31,7 +31,7 @@ public class PortalService {
LocalDate date
) {
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();
List<PortalSampleRow> sampleRows;

View File

@@ -124,7 +124,7 @@ public class SampleService {
null,
sampleNumber,
farmer.businessKey(),
farmer.name(),
farmer.companyName(),
farmer.email(),
request.cowNumber().trim(),
blankToNull(request.cowName()),
@@ -178,7 +178,7 @@ public class SampleService {
existing.id(),
existing.sampleNumber(),
farmer.businessKey(),
farmer.name(),
farmer.companyName(),
farmer.email(),
request.cowNumber().trim(),
blankToNull(request.cowName()),

View File

@@ -27,7 +27,8 @@ export type UserRole = "ADMIN" | "CUSTOMER";
export interface FarmerOption {
businessKey: string;
name: string;
companyName: string;
contactPerson: string | null;
email: string | null;
}
@@ -204,8 +205,15 @@ export interface SampleDetail {
export interface FarmerRow {
id: 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;
phoneNumber: string | null;
active: boolean;
updatedAt: string;
}

View File

@@ -8,13 +8,23 @@ type DatasetKey = "farmers" | "medications" | "pathogens" | "antibiotics";
type EditableRow = {
id: string;
businessKey: string;
name: string;
active: boolean;
updatedAt: string;
// Farmer fields
customerNumber?: string;
companyName?: string;
contactPerson?: string;
street?: string;
houseNumber?: string;
postalCode?: string;
city?: string;
email?: string;
phoneNumber?: string;
// Other fields
name?: string;
category?: MedicationCategory;
code?: string;
kind?: PathogenKind;
active: boolean;
updatedAt: string;
};
type DatasetsState = Record<DatasetKey, EditableRow[]>;
@@ -33,13 +43,51 @@ const DATASET_TITLES: Record<DatasetKey, string> = {
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 {
return {
farmers: overview.farmers.map((entry) => ({
id: entry.id,
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 ?? "",
phoneNumber: entry.phoneNumber ?? "",
active: entry.active,
updatedAt: entry.updatedAt,
})),
@@ -74,7 +122,21 @@ function normalizeOverview(overview: AdministrationOverview): DatasetsState {
function emptyRow(dataset: DatasetKey): EditableRow {
switch (dataset) {
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":
return {
id: "",
@@ -133,6 +195,7 @@ export default function AdministrationPage() {
}, []);
const rows = useMemo(() => datasets?.[selectedDataset] ?? [], [datasets, selectedDataset]);
const isFarmerDataset = selectedDataset === "farmers";
function updateRow(index: number, patch: Partial<EditableRow>) {
setDatasets((current) => {
@@ -166,7 +229,11 @@ export default function AdministrationPage() {
return;
}
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.");
return;
}
@@ -178,8 +245,15 @@ export default function AdministrationPage() {
case "farmers":
response = await apiPost<EditableRow[]>("/catalog/farmers", rows.map((row) => ({
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,
phoneNumber: row.phoneNumber || null,
active: row.active,
})));
break;
@@ -245,87 +319,198 @@ export default function AdministrationPage() {
</div>
</div>
<div className="table-shell">
<table className="data-table">
<thead>
<tr>
<th className="required-label">Name</th>
{selectedDataset === "farmers" ? <th>E-Mail</th> : null}
{selectedDataset === "medications" ? <th>Kategorie</th> : null}
{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>
{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={showValidation && !row.name.trim() ? "is-invalid" : ""}
value={row.name}
onChange={(event) => updateRow(index, { name: event.target.value })}
className={isFarmerFieldInvalid(row, "companyName", showValidation) ? "is-invalid" : ""}
value={row.companyName ?? ""}
onChange={(event) => updateRow(index, { companyName: event.target.value })}
placeholder="Firmenname"
required
/>
</td>
{selectedDataset === "farmers" ? (
<td>
<input
value={row.email ?? ""}
onChange={(event) => updateRow(index, { email: event.target.value })}
/>
</td>
) : null}
{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>
</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 ${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 })}
>
{row.active ? "sichtbar" : "inaktiv"}
</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>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{rows.map((row, index) => (
<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">
<button type="button" className="secondary-button" onClick={addRow}>

View File

@@ -196,7 +196,7 @@ export default function SampleRegistrationPage() {
>
{catalogs?.farmers.map((farmer) => (
<option key={farmer.businessKey} value={farmer.businessKey}>
{farmer.name}
{farmer.companyName}
</option>
))}
</select>

View File

@@ -22,7 +22,7 @@ export default function SearchFarmerPage() {
`/portal/snapshot?farmerBusinessKey=${encodeURIComponent(farmer.businessKey)}`,
);
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.");
}
@@ -121,7 +121,7 @@ export default function SearchFarmerPage() {
className="user-card"
onClick={() => void loadFarmerSamples(farmer)}
>
<strong>{farmer.name}</strong>
<strong>{farmer.companyName}</strong>
<small>{farmer.email ?? "ohne E-Mail"}</small>
</button>
))}

View File

@@ -570,6 +570,46 @@ a {
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 {
width: 100%;
border-collapse: collapse;
@@ -590,6 +630,35 @@ a {
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,
.info-chip {
display: inline-flex;