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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user