feat: Allow primary users to access user management and create sub-users

Frontend:
- Extend UserManagementPage for both ADMIN and primary users
- Admins see all primary users (excluding other admins)
- Primary users see only their sub-users
- Add create sub-user form for primary users
- Adjust UI text based on user role
- Fix table columns (hide company column for primary users)
This commit is contained in:
2026-03-17 16:58:47 +01:00
parent d03dc94ad1
commit 91f67f7dfc

View File

@@ -3,10 +3,9 @@ import { apiGet, apiPost } from "../lib/api";
import { useSession } from "../lib/session"; import { useSession } from "../lib/session";
import type { UserRow } from "../lib/types"; import type { UserRow } from "../lib/types";
interface PrimaryUserRow { interface SubUserRow {
id: string; id: string;
displayName: string; displayName: string;
companyName: string | null;
email: string | null; email: string | null;
active: boolean; active: boolean;
updatedAt: string; updatedAt: string;
@@ -14,27 +13,49 @@ interface PrimaryUserRow {
export default function UserManagementPage() { export default function UserManagementPage() {
const { user } = useSession(); const { user } = useSession();
const [users, setUsers] = useState<PrimaryUserRow[]>([]); const [users, setUsers] = useState<SubUserRow[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const isAdmin = user?.role === "ADMIN"; const isAdmin = user?.role === "ADMIN";
const isPrimaryUser = user?.primaryUser === true;
const canManageUsers = isAdmin || isPrimaryUser;
// Form state for creating new sub-user
const [newUserName, setNewUserName] = useState("");
const [newUserEmail, setNewUserEmail] = useState("");
const [newUserPassword, setNewUserPassword] = useState("");
const [creating, setCreating] = useState(false);
useEffect(() => { useEffect(() => {
async function loadUsers() { async function loadUsers() {
try { try {
const response = await apiGet<UserRow[]>("/portal/users"); const response = await apiGet<UserRow[]>("/portal/users");
// Nur Hauptnutzer (primaryUser=true) anzeigen, aber Admin ausblenden if (isAdmin) {
// Admin sieht alle Hauptnutzer (außer andere Admins)
const primaryUsers = response const primaryUsers = response
.filter((u) => u.primaryUser && u.role !== "ADMIN") .filter((u) => u.primaryUser && u.role !== "ADMIN")
.map((u) => ({ .map((u) => ({
id: u.id, id: u.id,
displayName: u.displayName, displayName: u.displayName,
companyName: u.companyName,
email: u.email, email: u.email,
active: u.active, active: u.active,
updatedAt: u.updatedAt, updatedAt: u.updatedAt,
})); }));
setUsers(primaryUsers); setUsers(primaryUsers);
} else {
// Hauptnutzer sieht alle seine Unterbenutzer (nicht-primary)
const subUsers = response
.filter((u) => !u.primaryUser)
.map((u) => ({
id: u.id,
displayName: u.displayName,
email: u.email,
active: u.active,
updatedAt: u.updatedAt,
}));
setUsers(subUsers);
}
} catch (error) { } catch (error) {
setMessage((error as Error).message); setMessage((error as Error).message);
} finally { } finally {
@@ -43,7 +64,7 @@ export default function UserManagementPage() {
} }
void loadUsers(); void loadUsers();
}, []); }, [isAdmin]);
async function toggleUserStatus(userId: string, newStatus: boolean) { async function toggleUserStatus(userId: string, newStatus: boolean) {
try { try {
@@ -65,14 +86,54 @@ export default function UserManagementPage() {
}.` }.`
); );
// Nachricht nach 3 Sekunden ausblenden
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), 3000);
} catch (error) { } catch (error) {
setMessage((error as Error).message); setMessage((error as Error).message);
} }
} }
// Formatierungsfunktion für das Datum async function handleCreateUser(e: React.FormEvent) {
e.preventDefault();
if (!newUserName.trim() || !newUserEmail.trim() || !newUserPassword.trim()) {
setMessage("Bitte alle Felder ausfüllen.");
return;
}
setCreating(true);
try {
await apiPost("/portal/users", {
displayName: newUserName.trim(),
email: newUserEmail.trim(),
password: newUserPassword,
});
// Reload users
const response = await apiGet<UserRow[]>("/portal/users");
const subUsers = response
.filter((u) => !u.primaryUser)
.map((u) => ({
id: u.id,
displayName: u.displayName,
email: u.email,
active: u.active,
updatedAt: u.updatedAt,
}));
setUsers(subUsers);
// Reset form
setNewUserName("");
setNewUserEmail("");
setNewUserPassword("");
setShowCreateForm(false);
setMessage(`Benutzer "${newUserName}" wurde erstellt.`);
setTimeout(() => setMessage(null), 3000);
} catch (error) {
setMessage((error as Error).message);
} finally {
setCreating(false);
}
}
function formatDate(value: string) { function formatDate(value: string) {
return new Intl.DateTimeFormat("de-DE", { return new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium", dateStyle: "medium",
@@ -80,13 +141,12 @@ export default function UserManagementPage() {
}).format(new Date(value)); }).format(new Date(value));
} }
// Nicht-Admin Ansicht (sollte nicht passieren, da Route geschützt ist) if (!canManageUsers) {
if (!isAdmin) {
return ( return (
<div className="page-stack"> <div className="page-stack">
<section className="section-card"> <section className="section-card">
<div className="alert alert--error"> <div className="alert alert--error">
Zugriff verweigert. Diese Seite ist nur für Administratoren. Zugriff verweigert. Diese Seite ist nur für Administratoren und Hauptbenutzer.
</div> </div>
</section> </section>
</div> </div>
@@ -99,10 +159,11 @@ export default function UserManagementPage() {
<section className="hero-card admin-hero"> <section className="hero-card admin-hero">
<div> <div>
<p className="eyebrow">Benutzerverwaltung</p> <p className="eyebrow">Benutzerverwaltung</p>
<h3>Hauptnutzer freigeben oder sperren</h3> <h3>{isAdmin ? "Hauptnutzer freigeben oder sperren" : "Unterbenutzer verwalten"}</h3>
<p className="muted-text"> <p className="muted-text">
Verwalten Sie den Zugriff von Hauptnutzern auf das System. {isAdmin
Gesperrte Benutzer können sich nicht mehr anmelden. ? "Verwalten Sie den Zugriff von Hauptnutzern auf das System. Gesperrte Benutzer können sich nicht mehr anmelden."
: "Erstellen und verwalten Sie Unterbenutzer für Ihr Konto. Unterbenutzer können Proben registrieren und bearbeiten."}
</p> </p>
</div> </div>
</section> </section>
@@ -111,7 +172,9 @@ export default function UserManagementPage() {
{message ? ( {message ? (
<div <div
className={ className={
message.includes("freigegeben") || message.includes("gesperrt") message.includes("freigegeben") ||
message.includes("gesperrt") ||
message.includes("erstellt")
? "alert alert--success" ? "alert alert--success"
: "alert alert--error" : "alert alert--error"
} }
@@ -120,26 +183,107 @@ export default function UserManagementPage() {
</div> </div>
) : null} ) : null}
{/* Tabelle mit Hauptnutzern */} {/* 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>
<button
type="button"
className="accent-button"
onClick={() => setShowCreateForm(true)}
>
+ Benutzer anlegen
</button>
</div>
) : (
<>
<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"
required
/>
</label>
<label className="field">
<span>E-Mail</span>
<input
type="email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
placeholder="email@beispiel.de"
required
/>
</label>
<label className="field">
<span>Passwort</span>
<input
type="password"
value={newUserPassword}
onChange={(e) => setNewUserPassword(e.target.value)}
placeholder="Passwort"
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"> <section className="section-card">
<div className="section-card__header"> <div className="section-card__header">
<div> <div>
<p className="eyebrow">Hauptnutzer</p> <p className="eyebrow">{isAdmin ? "Hauptnutzer" : "Unterbenutzer"}</p>
<h3>Registrierte Hauptnutzer</h3> <h3>{isAdmin ? "Registrierte Hauptnutzer" : "Ihre Unterbenutzer"}</h3>
</div> </div>
</div> </div>
{loading ? ( {loading ? (
<div className="empty-state">Benutzer werden geladen...</div> <div className="empty-state">Benutzer werden geladen...</div>
) : users.length === 0 ? ( ) : users.length === 0 ? (
<div className="empty-state">Keine Hauptnutzer vorhanden.</div> <div className="empty-state">
{isAdmin ? "Keine Hauptnutzer vorhanden." : "Keine Unterbenutzer vorhanden."}
</div>
) : ( ) : (
<div className="table-shell"> <div className="table-shell">
<table className="data-table"> <table className="data-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Firma</th> {isAdmin && <th>Firma</th>}
<th>E-Mail</th> <th>E-Mail</th>
<th>Status</th> <th>Status</th>
<th>Letzte Änderung</th> <th>Letzte Änderung</th>
@@ -152,7 +296,7 @@ export default function UserManagementPage() {
<td> <td>
<strong>{entry.displayName}</strong> <strong>{entry.displayName}</strong>
</td> </td>
<td>{entry.companyName ?? "-"}</td> {isAdmin && <td>{(entry as SubUserRow & { companyName?: string }).companyName ?? "-"}</td>}
<td>{entry.email ?? "-"}</td> <td>{entry.email ?? "-"}</td>
<td> <td>
<span <span
@@ -188,9 +332,9 @@ export default function UserManagementPage() {
<div className="info-panel"> <div className="info-panel">
<strong>Hinweis</strong> <strong>Hinweis</strong>
<p> <p>
Hauptnutzer sind die primären Kontoinhaber. Wenn Sie einen Hauptnutzer sperren, {isAdmin
können sich dieser und alle zugehörigen Nebennutzer nicht mehr anmelden. ? "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."
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> </p>
</div> </div>
</section> </section>