refactor: update farmer table layout and styling

This commit is contained in:
2026-03-18 22:04:13 +01:00
parent e7a18cd339
commit 3755f4c414
3 changed files with 1118 additions and 470 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,29 @@ export default function UserManagementPage() {
} }
}, [user]); }, [user]);
function resetProfileData() {
setProfileData({
displayName: user?.displayName || "",
companyName: user?.companyName || "",
street: user?.street || "",
houseNumber: user?.houseNumber || "",
postalCode: user?.postalCode || "",
city: user?.city || "",
email: user?.email || "",
phoneNumber: user?.phoneNumber || "",
});
}
function openProfileDialog() {
resetProfileData();
setShowProfileForm(true);
}
function closeProfileDialog() {
resetProfileData();
setShowProfileForm(false);
}
useEffect(() => { useEffect(() => {
async function loadUsers() { async function loadUsers() {
try { try {
@@ -170,6 +193,14 @@ export default function UserManagementPage() {
} }
} }
function closeCreateUserDialog() {
setShowCreateForm(false);
setNewUserName("");
setNewUserEmail("");
setNewUserPassword("");
setNewUserPasswordConfirm("");
}
async function handleSaveProfile(e: React.FormEvent) { async function handleSaveProfile(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
if (!profileData.displayName.trim()) { if (!profileData.displayName.trim()) {
@@ -269,36 +300,191 @@ export default function UserManagementPage() {
{/* Own Profile Form (nur für Hauptbenutzer) */} {/* Own Profile Form (nur für Hauptbenutzer) */}
{isPrimaryUser && !isAdmin && ( {isPrimaryUser && !isAdmin && (
<section className="section-card"> <section className="section-card">
{!showProfileForm ? ( <div className="section-card__header user-management-page__profile-header">
<div className="section-card__header"> <div>
<p className="eyebrow">Meine Stammdaten</p>
<h3>{user?.displayName}</h3>
</div>
<button
type="button"
className="accent-button"
onClick={openProfileDialog}
>
Bearbeiten
</button>
</div>
</section>
)}
{/* Tabelle mit Benutzern */}
<section className="section-card">
<div className="section-card__header">
<div>
<p className="eyebrow">{isAdmin ? "Hauptnutzer" : "Unterbenutzer"}</p>
<h3>{isAdmin ? "Registrierte Hauptnutzer" : "Ihre Unterbenutzer"}</h3>
</div>
</div>
{loading ? (
<div className="empty-state">Benutzer werden geladen...</div>
) : users.length === 0 ? (
<div className="empty-state">
{isAdmin ? "Keine Hauptnutzer vorhanden." : "Keine Unterbenutzer vorhanden."}
</div>
) : (
<div className="table-shell">
<table className="data-table">
<thead>
<tr>
<th>Name</th>
{isAdmin && <th>Firma</th>}
<th>E-Mail</th>
<th>Status</th>
<th>Letzte Änderung</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{users.map((entry) => (
<tr key={entry.id} className={!entry.active ? "table-row--inactive" : ""}>
<td>
<strong>{entry.displayName}</strong>
</td>
{isAdmin && <td>{(entry as SubUserRow & { companyName?: string }).companyName ?? "-"}</td>}
<td>{entry.email ?? "-"}</td>
<td>
<span
className={`status-pill ${
entry.active ? "status-pill--active" : "status-pill--inactive"
}`}
>
{entry.active ? "Freigegeben" : "Gesperrt"}
</span>
</td>
<td className="text-muted">{formatDate(entry.updatedAt)}</td>
<td>
<button
type="button"
className={`action-button ${
entry.active ? "action-button--danger" : "action-button--success"
}`}
onClick={() => toggleUserStatus(entry.id, !entry.active)}
>
{entry.active ? "Sperren" : "Freigeben"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{isPrimaryUser && !isAdmin ? (
<div className="page-actions page-actions--space-between user-management-page__create-action">
<button
type="button"
className="accent-button"
onClick={() => setShowCreateForm(true)}
>
+ Benutzer anlegen
</button>
</div>
) : null}
</section>
{isPrimaryUser && !isAdmin && showCreateForm ? (
<div className="dialog-backdrop" onClick={closeCreateUserDialog}>
<form className="dialog" onClick={(event) => event.stopPropagation()} onSubmit={handleCreateUser}>
<div className="dialog__header">
<div> <div>
<p className="eyebrow">Meine Stammdaten</p> <p className="eyebrow">Neuer Unterbenutzer</p>
<h3>{user?.displayName}</h3> <h4>Benutzer anlegen</h4>
</div> </div>
</div>
<div className="dialog__body dialog__body--form">
<div className="field-grid field-grid--2col">
<label className="field">
<span>Name *</span>
<input
type="text"
value={newUserName}
onChange={(e) => setNewUserName(e.target.value)}
placeholder="Name des Benutzers"
autoComplete="off"
required
/>
</label>
<label className="field">
<span>E-Mail *</span>
<input
type="email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="email@beispiel.de"
autoComplete="off"
required
/>
</label>
<label className="field">
<span>Passwort *</span>
<input
type="password"
value={newUserPassword}
onChange={(e) => setNewUserPassword(e.target.value)}
placeholder="Passwort"
autoComplete="new-password"
required
/>
</label>
<label className="field">
<span>Passwort wiederholen *</span>
<input
type="password"
value={newUserPasswordConfirm}
onChange={(e) => setNewUserPasswordConfirm(e.target.value)}
placeholder="Passwort wiederholen"
autoComplete="new-password"
required
/>
</label>
</div>
</div>
<div className="dialog__actions dialog__actions--start">
<button <button
type="button" type="button"
className="accent-button" className="secondary-button"
onClick={() => setShowProfileForm(true)} onClick={closeCreateUserDialog}
disabled={creating}
> >
Bearbeiten Abbrechen
</button>
<button
type="submit"
className="accent-button"
disabled={creating}
>
{creating ? "Wird erstellt..." : "Benutzer erstellen"}
</button> </button>
</div> </div>
) : ( </form>
<> </div>
<div className="section-card__header"> ) : null}
<div>
<p className="eyebrow">Meine Stammdaten</p> {isPrimaryUser && !isAdmin && showProfileForm ? (
<h3>Stammdaten bearbeiten</h3> <div className="dialog-backdrop" onClick={closeProfileDialog}>
</div> <form className="dialog" onClick={(event) => event.stopPropagation()} onSubmit={handleSaveProfile}>
<button <div className="dialog__header">
type="button" <div>
className="ghost-button" <p className="eyebrow">Meine Stammdaten</p>
onClick={() => setShowProfileForm(false)} <h4>Stammdaten bearbeiten</h4>
>
Abbrechen
</button>
</div> </div>
<form onSubmit={handleSaveProfile} className="field-grid field-grid--2col"> </div>
<div className="dialog__body dialog__body--form">
<div className="field-grid field-grid--2col">
<label className="field"> <label className="field">
<span>Name *</span> <span>Name *</span>
<input <input
@@ -372,190 +558,29 @@ export default function UserManagementPage() {
placeholder="Telefonnummer" placeholder="Telefonnummer"
/> />
</label> </label>
<div className="field" style={{ gridColumn: "1 / -1" }}>
<button
type="submit"
className="accent-button"
disabled={savingProfile}
>
{savingProfile ? "Wird gespeichert..." : "Stammdaten speichern"}
</button>
</div>
</form>
</>
)}
</section>
)}
{/* Create Sub-user Form (nur für Hauptnutzer) */}
{isPrimaryUser && !isAdmin && (
<section className="section-card">
{!showCreateForm ? (
<div className="section-card__header">
<div>
<p className="eyebrow">Neuer Unterbenutzer</p>
<h3>Benutzer anlegen</h3>
</div> </div>
</div>
<div className="dialog__actions dialog__actions--start">
<button <button
type="button" type="button"
className="accent-button" className="secondary-button"
onClick={() => setShowCreateForm(true)} onClick={closeProfileDialog}
disabled={savingProfile}
> >
+ Benutzer anlegen Abbrechen
</button>
<button
type="submit"
className="accent-button"
disabled={savingProfile}
>
{savingProfile ? "Wird gespeichert..." : "Stammdaten speichern"}
</button> </button>
</div> </div>
) : ( </form>
<>
<div className="section-card__header">
<div>
<p className="eyebrow">Neuer Unterbenutzer</p>
<h3>Benutzer anlegen</h3>
</div>
<button
type="button"
className="ghost-button"
onClick={() => setShowCreateForm(false)}
>
Abbrechen
</button>
</div>
<form onSubmit={handleCreateUser} className="field-grid field-grid--2col">
<label className="field">
<span>Name *</span>
<input
type="text"
value={newUserName}
onChange={(e) => setNewUserName(e.target.value)}
placeholder="Name des Benutzers"
autoComplete="off"
required
/>
</label>
<label className="field">
<span>E-Mail *</span>
<input
type="email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="email@beispiel.de"
autoComplete="off"
required
/>
</label>
<label className="field">
<span>Passwort *</span>
<input
type="password"
value={newUserPassword}
onChange={(e) => setNewUserPassword(e.target.value)}
placeholder="Passwort"
autoComplete="new-password"
required
/>
</label>
<label className="field">
<span>Passwort wiederholen *</span>
<input
type="password"
value={newUserPasswordConfirm}
onChange={(e) => setNewUserPasswordConfirm(e.target.value)}
placeholder="Passwort wiederholen"
autoComplete="new-password"
required
/>
</label>
<div className="field" style={{ display: "flex", alignItems: "flex-end" }}>
<button
type="submit"
className="accent-button"
disabled={creating}
style={{ width: "100%" }}
>
{creating ? "Wird erstellt..." : "Benutzer erstellen"}
</button>
</div>
</form>
</>
)}
</section>
)}
{/* Tabelle mit Benutzern */}
<section className="section-card">
<div className="section-card__header">
<div>
<p className="eyebrow">{isAdmin ? "Hauptnutzer" : "Unterbenutzer"}</p>
<h3>{isAdmin ? "Registrierte Hauptnutzer" : "Ihre Unterbenutzer"}</h3>
</div>
</div> </div>
) : null}
{loading ? (
<div className="empty-state">Benutzer werden geladen...</div>
) : users.length === 0 ? (
<div className="empty-state">
{isAdmin ? "Keine Hauptnutzer vorhanden." : "Keine Unterbenutzer vorhanden."}
</div>
) : (
<div className="table-shell">
<table className="data-table">
<thead>
<tr>
<th>Name</th>
{isAdmin && <th>Firma</th>}
<th>E-Mail</th>
<th>Status</th>
<th>Letzte Änderung</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{users.map((entry) => (
<tr key={entry.id} className={!entry.active ? "table-row--inactive" : ""}>
<td>
<strong>{entry.displayName}</strong>
</td>
{isAdmin && <td>{(entry as SubUserRow & { companyName?: string }).companyName ?? "-"}</td>}
<td>{entry.email ?? "-"}</td>
<td>
<span
className={`status-pill ${
entry.active ? "status-pill--active" : "status-pill--inactive"
}`}
>
{entry.active ? "Freigegeben" : "Gesperrt"}
</span>
</td>
<td className="text-muted">{formatDate(entry.updatedAt)}</td>
<td>
<button
type="button"
className={`action-button ${
entry.active ? "action-button--danger" : "action-button--success"
}`}
onClick={() => toggleUserStatus(entry.id, !entry.active)}
>
{entry.active ? "Sperren" : "Freigeben"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
{/* Info-Box */}
<section className="section-card">
<div className="info-panel">
<strong>Hinweis</strong>
<p>
{isAdmin
? "Hauptnutzer sind die primären Kontoinhaber. Wenn Sie einen Hauptnutzer sperren, können sich dieser und alle zugehörigen Nebennutzer nicht mehr anmelden. Die Daten bleiben erhalten und können durch Freigabe wieder aktiviert werden."
: "Unterbenutzer können Proben registrieren und bearbeiten, aber keine neuen Benutzer anlegen. Wenn Sie einen Unterbenutzer sperren, kann sich dieser nicht mehr anmelden."}
</p>
</div>
</section>
</div> </div>
); );
} }

View File

@@ -570,11 +570,18 @@ a {
overflow: auto; overflow: auto;
} }
.admin-catalog-list,
.admin-farmer-list { .admin-farmer-list {
display: grid; display: grid;
gap: 20px; gap: 20px;
} }
.admin-catalog-section,
.admin-farmer-section {
display: grid;
gap: 16px;
}
.admin-farmer-card { .admin-farmer-card {
display: grid; display: grid;
gap: 16px; gap: 16px;
@@ -584,6 +591,15 @@ a {
background: rgba(255, 255, 255, 0.42); background: rgba(255, 255, 255, 0.42);
} }
.admin-catalog-form {
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 { .admin-farmer-card__row {
display: grid; display: grid;
gap: 16px; gap: 16px;
@@ -610,6 +626,11 @@ a {
justify-content: center; justify-content: center;
} }
.admin-catalog-form__toggle {
min-width: 140px;
justify-content: center;
}
.data-table { .data-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -630,6 +651,32 @@ a {
text-transform: uppercase; text-transform: uppercase;
} }
.admin-farmer-table__row {
cursor: pointer;
}
.admin-catalog-table__row {
cursor: pointer;
}
.admin-farmer-table__row:hover,
.admin-farmer-table__row:focus-visible,
.admin-catalog-table__row:hover,
.admin-catalog-table__row:focus-visible {
background: rgba(17, 109, 99, 0.08);
outline: none;
}
.admin-farmer-table__empty {
color: var(--muted);
text-align: center !important;
}
.admin-catalog-table__empty {
color: var(--muted);
text-align: center !important;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
.admin-farmer-card__row--primary, .admin-farmer-card__row--primary,
.admin-farmer-card__row--address, .admin-farmer-card__row--address,
@@ -647,6 +694,7 @@ a {
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.admin-catalog-form,
.admin-farmer-card { .admin-farmer-card {
padding: 18px; padding: 18px;
} }
@@ -850,6 +898,14 @@ a {
justify-content: flex-end; justify-content: flex-end;
} }
.user-management-page__create-action {
margin-top: 18px;
}
.user-management-page__profile-header {
margin-bottom: 0;
}
.invoice-template-page { .invoice-template-page {
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
@@ -1520,6 +1576,10 @@ a {
justify-content: flex-end; justify-content: flex-end;
} }
.dialog__actions--start {
justify-content: flex-start;
}
.dialog__actions a { .dialog__actions a {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -1531,6 +1591,11 @@ a {
min-height: 0; min-height: 0;
} }
.dialog__body--form,
.dialog__body--farmer {
overflow: auto;
}
.dialog__body--pdf { .dialog__body--pdf {
height: min(80vh, 900px); height: min(80vh, 900px);
} }