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 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
|
||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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" : ""}`}
|
||||||
|
|||||||
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