feat: Add Stammdaten page for admin to manage own profile

- Add AdminProfilePage component for admin to edit own data
- Add 'Stammdaten' menu item in admin navigation
- Add route /admin/stammdaten for the new page
- Use existing POST /api/portal/users endpoint to save changes
- Update session context after successful save
This commit is contained in:
2026-03-18 09:09:57 +01:00
parent 49b1a3b363
commit 775b09ebeb
4 changed files with 299 additions and 0 deletions

View File

@@ -105,3 +105,8 @@ Kundenregistrierung:
- `cd backend && mvn test` - `cd backend && mvn test`
- `cd frontend && npm run build` - `cd frontend && npm run build`
## Docker
docker buildx build --platform linux/amd64 -t gitea.appcreation.de/sven/muh:0.8.0 --push .
docker run -d --name muh --network br0 --ip 192.168.180.26 --restart unless-stopped -e MUH_MONGODB_URL=mongodb://192.168.180.25:27017/muh -e MUH_TOKEN_SECRET=local-dev-muh-token-secret-2026-03-13 -e MUH_TOKEN_VALIDITY_HOURS=12 -e MUH_ALLOWED_ORIGINS=https://muh.appcreation.de gitea.appcreation.de/sven/muh:0.8.0

View File

@@ -18,6 +18,7 @@ import ReportTemplatePage from "./pages/ReportTemplatePage";
import InvoiceTemplatePage from "./pages/InvoiceTemplatePage"; import InvoiceTemplatePage from "./pages/InvoiceTemplatePage";
import InvoiceManagementPage from "./pages/InvoiceManagementPage"; import InvoiceManagementPage from "./pages/InvoiceManagementPage";
import PricingPage from "./pages/PricingPage"; import PricingPage from "./pages/PricingPage";
import AdminProfilePage from "./pages/AdminProfilePage";
function ProtectedRoutes() { function ProtectedRoutes() {
const { user, ready } = useSession(); const { user, ready } = useSession();
@@ -48,6 +49,7 @@ function ProtectedRoutes() {
<Route path="/admin/medikamente" element={<AdministrationPage />} /> <Route path="/admin/medikamente" element={<AdministrationPage />} />
<Route path="/admin/erreger" element={<AdministrationPage />} /> <Route path="/admin/erreger" element={<AdministrationPage />} />
<Route path="/admin/antibiogramm" element={<AdministrationPage />} /> <Route path="/admin/antibiogramm" element={<AdministrationPage />} />
<Route path="/admin/stammdaten" element={<AdminProfilePage />} />
<Route path="/admin/preistabelle" element={<PricingPage />} /> <Route path="/admin/preistabelle" element={<PricingPage />} />
<Route path="/admin/rechnung/verwalten" element={<InvoiceManagementPage />} /> <Route path="/admin/rechnung/verwalten" element={<InvoiceManagementPage />} />
<Route path="/admin/rechnung/template" element={<InvoiceTemplatePage />} /> <Route path="/admin/rechnung/template" element={<InvoiceTemplatePage />} />

View File

@@ -8,6 +8,7 @@ const PAGE_TITLES: Record<string, string> = {
"/samples/new": "Neuanlage einer Probe", "/samples/new": "Neuanlage einer Probe",
"/portal": "MUH-Portal", "/portal": "MUH-Portal",
"/report-template": "Bericht", "/report-template": "Bericht",
"/admin/stammdaten": "Meine Stammdaten",
"/admin/preistabelle": "Preistabelle", "/admin/preistabelle": "Preistabelle",
"/admin/rechnung/verwalten": "Rechnungsverwaltung", "/admin/rechnung/verwalten": "Rechnungsverwaltung",
"/admin/rechnung/template": "Rechnungsvorlage", "/admin/rechnung/template": "Rechnungsvorlage",
@@ -65,6 +66,13 @@ export default function AppShell() {
</div> </div>
</div> </div>
<NavLink
to="/admin/stammdaten"
className={({ isActive }) => `nav-link ${isActive ? "is-active" : ""}`}
>
Stammdaten
</NavLink>
<NavLink <NavLink
to="/admin/preistabelle" to="/admin/preistabelle"
className={({ isActive }) => `nav-link ${isActive ? "is-active" : ""}`} className={({ isActive }) => `nav-link ${isActive ? "is-active" : ""}`}

View File

@@ -0,0 +1,284 @@
import { useEffect, useState } from "react";
import { apiGet, apiPost } from "../lib/api";
import { useSession } from "../lib/session";
import type { UserRow } from "../lib/types";
export default function AdminProfilePage() {
const { user, updateUser } = useSession();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [formData, setFormData] = useState({
displayName: "",
companyName: "",
street: "",
houseNumber: "",
postalCode: "",
city: "",
email: "",
phoneNumber: "",
});
// Load current user data
useEffect(() => {
async function loadUserData() {
try {
const users = await apiGet<UserRow[]>("/portal/users");
const currentUser = users.find((u) => u.id === user?.id);
if (currentUser) {
setFormData({
displayName: currentUser.displayName || "",
companyName: currentUser.companyName || "",
street: currentUser.street || "",
houseNumber: currentUser.houseNumber || "",
postalCode: currentUser.postalCode || "",
city: currentUser.city || "",
email: currentUser.email || "",
phoneNumber: currentUser.phoneNumber || "",
});
}
} catch (error) {
setMessage((error as Error).message);
} finally {
setLoading(false);
}
}
if (user?.id) {
void loadUserData();
}
}, [user?.id]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!formData.displayName.trim()) {
setMessage("Name ist erforderlich.");
return;
}
if (!formData.email.trim()) {
setMessage("E-Mail ist erforderlich.");
return;
}
setSaving(true);
setMessage(null);
try {
const response = await apiPost<UserRow>("/portal/users", {
id: user?.id,
displayName: formData.displayName.trim(),
companyName: formData.companyName.trim() || null,
street: formData.street.trim() || null,
houseNumber: formData.houseNumber.trim() || null,
postalCode: formData.postalCode.trim() || null,
city: formData.city.trim() || null,
email: formData.email.trim(),
phoneNumber: formData.phoneNumber.trim() || null,
active: true,
});
// Update session user
if (updateUser && user) {
updateUser({
...user,
displayName: response.displayName,
companyName: response.companyName,
street: response.street,
houseNumber: response.houseNumber,
postalCode: response.postalCode,
city: response.city,
email: response.email,
phoneNumber: response.phoneNumber,
});
}
setMessage("Stammdaten wurden erfolgreich gespeichert.");
setTimeout(() => setMessage(null), 3000);
} catch (error) {
setMessage((error as Error).message);
} finally {
setSaving(false);
}
}
if (loading) {
return (
<div className="page-stack">
<section className="section-card">
<div className="empty-state">Stammdaten werden geladen...</div>
</section>
</div>
);
}
return (
<div className="page-stack">
{/* Header */}
<section className="hero-card admin-hero">
<div>
<p className="eyebrow">Stammdaten</p>
<h3>Meine Stammdaten</h3>
<p className="muted-text">
Verwalten Sie hier Ihre persönlichen und Unternehmensdaten.
</p>
</div>
</section>
{/* Status-Meldung */}
{message && (
<div
className={
message.includes("erfolgreich")
? "alert alert--success"
: "alert alert--error"
}
>
{message}
</div>
)}
{/* Stammdaten-Formular */}
<section className="section-card">
<div className="section-card__header">
<div>
<p className="eyebrow">Profil</p>
<h3>Stammdaten bearbeiten</h3>
</div>
</div>
<form onSubmit={handleSubmit} className="field-grid field-grid--2col">
<label className="field">
<span>Name *</span>
<input
type="text"
value={formData.displayName}
onChange={(e) =>
setFormData({ ...formData, displayName: e.target.value })
}
placeholder="Ihr Name"
required
disabled={saving}
/>
</label>
<label className="field">
<span>Firma</span>
<input
type="text"
value={formData.companyName}
onChange={(e) =>
setFormData({ ...formData, companyName: e.target.value })
}
placeholder="Firmenname"
disabled={saving}
/>
</label>
<label className="field">
<span>Straße</span>
<input
type="text"
value={formData.street}
onChange={(e) =>
setFormData({ ...formData, street: e.target.value })
}
placeholder="Straße"
disabled={saving}
/>
</label>
<label className="field">
<span>Hausnummer</span>
<input
type="text"
value={formData.houseNumber}
onChange={(e) =>
setFormData({ ...formData, houseNumber: e.target.value })
}
placeholder="Hausnummer"
disabled={saving}
/>
</label>
<label className="field">
<span>PLZ</span>
<input
type="text"
value={formData.postalCode}
onChange={(e) =>
setFormData({ ...formData, postalCode: e.target.value })
}
placeholder="Postleitzahl"
disabled={saving}
/>
</label>
<label className="field">
<span>Ort</span>
<input
type="text"
value={formData.city}
onChange={(e) =>
setFormData({ ...formData, city: e.target.value })
}
placeholder="Ort"
disabled={saving}
/>
</label>
<label className="field">
<span>E-Mail *</span>
<input
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
placeholder="email@beispiel.de"
required
disabled={saving}
/>
</label>
<label className="field">
<span>Telefon</span>
<input
type="tel"
value={formData.phoneNumber}
onChange={(e) =>
setFormData({ ...formData, phoneNumber: e.target.value })
}
placeholder="Telefonnummer"
disabled={saving}
/>
</label>
<div className="field" style={{ gridColumn: "1 / -1" }}>
<button
type="submit"
className="accent-button"
disabled={saving}
>
{saving ? "Wird gespeichert..." : "Stammdaten speichern"}
</button>
</div>
</form>
</section>
{/* Info-Box */}
<section className="section-card">
<div className="info-panel">
<strong>Hinweis</strong>
<p>
Ihre Stammdaten werden für die Rechnungsstellung und
Kommunikation verwendet. Stellen Sie sicher, dass die
Daten aktuell und vollständig sind.
</p>
</div>
</section>
</div>
);
}