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:
@@ -105,3 +105,8 @@ Kundenregistrierung:
|
||||
|
||||
- `cd backend && mvn test`
|
||||
- `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
|
||||
@@ -18,6 +18,7 @@ import ReportTemplatePage from "./pages/ReportTemplatePage";
|
||||
import InvoiceTemplatePage from "./pages/InvoiceTemplatePage";
|
||||
import InvoiceManagementPage from "./pages/InvoiceManagementPage";
|
||||
import PricingPage from "./pages/PricingPage";
|
||||
import AdminProfilePage from "./pages/AdminProfilePage";
|
||||
|
||||
function ProtectedRoutes() {
|
||||
const { user, ready } = useSession();
|
||||
@@ -48,6 +49,7 @@ function ProtectedRoutes() {
|
||||
<Route path="/admin/medikamente" element={<AdministrationPage />} />
|
||||
<Route path="/admin/erreger" element={<AdministrationPage />} />
|
||||
<Route path="/admin/antibiogramm" element={<AdministrationPage />} />
|
||||
<Route path="/admin/stammdaten" element={<AdminProfilePage />} />
|
||||
<Route path="/admin/preistabelle" element={<PricingPage />} />
|
||||
<Route path="/admin/rechnung/verwalten" element={<InvoiceManagementPage />} />
|
||||
<Route path="/admin/rechnung/template" element={<InvoiceTemplatePage />} />
|
||||
|
||||
@@ -8,6 +8,7 @@ const PAGE_TITLES: Record<string, string> = {
|
||||
"/samples/new": "Neuanlage einer Probe",
|
||||
"/portal": "MUH-Portal",
|
||||
"/report-template": "Bericht",
|
||||
"/admin/stammdaten": "Meine Stammdaten",
|
||||
"/admin/preistabelle": "Preistabelle",
|
||||
"/admin/rechnung/verwalten": "Rechnungsverwaltung",
|
||||
"/admin/rechnung/template": "Rechnungsvorlage",
|
||||
@@ -65,6 +66,13 @@ export default function AppShell() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NavLink
|
||||
to="/admin/stammdaten"
|
||||
className={({ isActive }) => `nav-link ${isActive ? "is-active" : ""}`}
|
||||
>
|
||||
Stammdaten
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
to="/admin/preistabelle"
|
||||
className={({ isActive }) => `nav-link ${isActive ? "is-active" : ""}`}
|
||||
|
||||
284
frontend/src/pages/AdminProfilePage.tsx
Normal file
284
frontend/src/pages/AdminProfilePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user