Rollen-differenzierte Verwaltungs-UI (Material-/Leitungsverantwortliche)
Bug: Frontend prüfte nur istAdmin, Materialverantwortliche und Leitungs- verantwortliche sahen exakt dieselbe UI wie Mitarbeiter - obwohl das Backend längst granular unterscheidet (require_roles je Endpunkt). - AuthContext gibt volle Rollenliste + istVerantwortlich/istMaterial- verantwortlich/istLeitungsverantwortlich weiter. - /admin-Route für alle drei Rollen offen, AdminPage filtert Tabs nach Rolle (Material/Leitung: Objekte-Pflege, Kontrollverantwortung, Änderungslog; nur Administration: Stammdaten/Benutzer/Eskalation/ Zuständigkeit). - ObjektSection blendet Anlegen/Duplizieren/Kopplung für Nicht-Admins aus (waren admin-only im Backend, hätten 403 geworfen). - Promise.allSettled statt Promise.all beim Laden, damit ein admin-only Endpunkt (z.B. /benutzer) nicht die ganze Seite für niedrigere Rollen blockiert. - Backend: GET /benutzer jetzt auch für Material-/Leitungsverantwortliche lesbar (für Kontrollverantwortung-Auswahl im Frontend nötig), Schreiben bleibt admin-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -3433,3 +3433,27 @@ Keine Commits in dieser Session.
|
|||||||
- ergebnisse/material_bereinigung_log.md | 17 ++++++++++-------
|
- ergebnisse/material_bereinigung_log.md | 17 ++++++++++-------
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-04 22:07 – 22:08 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- cf97b2e Materialstamm: Nachkontrolle bestätigt sauber, 6 Namen getrimmt
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 13 +++++++++++++
|
||||||
|
- ergebnisse/material_bereinigung_log.md | 8 ++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 22:20 – 22:25 (4m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 13 +++++++++++++
|
||||||
|
- ergebnisse/material_bereinigung_log.md | 8 ++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ from app.schemas.benutzer import BenutzerCreate, BenutzerRead, BenutzerUpdate
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
_admin_only = require_roles(RolleTyp.administration)
|
_admin_only = require_roles(RolleTyp.administration)
|
||||||
|
# Lesend auch für Material-/Leitungsverantwortliche (Rollen-Vorgabe): brauchen
|
||||||
|
# die Liste u.a. für Kontrollverantwortung/Zuständigkeit-Auswahl im Frontend -
|
||||||
|
# Anlegen/Ändern bleibt admin-only.
|
||||||
|
_lesend = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/benutzer", response_model=list[BenutzerRead])
|
@router.get("/benutzer", response_model=list[BenutzerRead])
|
||||||
async def liste_benutzer(db: AsyncSession = Depends(get_db), _=Depends(_admin_only)) -> list[BenutzerRead]:
|
async def liste_benutzer(db: AsyncSession = Depends(get_db), _=Depends(_lesend)) -> list[BenutzerRead]:
|
||||||
result = await db.execute(select(Benutzer))
|
result = await db.execute(select(Benutzer))
|
||||||
return [BenutzerRead.from_orm_benutzer(b) for b in result.scalars().all()]
|
return [BenutzerRead.from_orm_benutzer(b) for b in result.scalars().all()]
|
||||||
|
|
||||||
|
|||||||
@@ -54,3 +54,40 @@ async def test_admin_can_update_rollen(client, admin_user, mitarbeiter_user):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["rollen"] == ["leitungsverantwortlicher"]
|
assert response.json()["rollen"] == ["leitungsverantwortlicher"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_materialverantwortlicher_darf_benutzer_lesen_nicht_anlegen(
|
||||||
|
client, materialverantwortlicher_user
|
||||||
|
):
|
||||||
|
"""Rollen-Vorgabe: Material-/Leitungsverantwortliche brauchen die Liste
|
||||||
|
lesend (z.B. Kontrollverantwortung-Auswahl im Frontend), aber kein
|
||||||
|
Schreibrecht - anders als Administration."""
|
||||||
|
token = await login(client, "materialverantwortlicher1")
|
||||||
|
gelesen = await client.get("/api/v1/benutzer", headers=auth_header(token))
|
||||||
|
assert gelesen.status_code == 200
|
||||||
|
|
||||||
|
angelegt = await client.post(
|
||||||
|
"/api/v1/benutzer",
|
||||||
|
json={"name": "X", "login": "x1", "passwort": "sicheres-passwort", "rollen": ["mitarbeiter"]},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert angelegt.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_leitungsverantwortlicher_darf_benutzer_lesen(client, db_session):
|
||||||
|
from app.core.security import hash_password
|
||||||
|
from app.models.auth import Benutzer, BenutzerRolle, RolleTyp
|
||||||
|
|
||||||
|
benutzer = Benutzer(
|
||||||
|
name="Test Leitung", login="leitung1", passwort_hash=hash_password("test-passwort-123"), aktiv=True
|
||||||
|
)
|
||||||
|
db_session.add(benutzer)
|
||||||
|
await db_session.flush()
|
||||||
|
db_session.add(BenutzerRolle(benutzer_id=benutzer.id, rolle=RolleTyp.leitungsverantwortlicher))
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
token = await login(client, "leitung1")
|
||||||
|
response = await client.get("/api/v1/benutzer", headers=auth_header(token))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ function GeschuetzteRoute({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AdminRoute({ children }: { children: ReactNode }) {
|
function AdminRoute({ children }: { children: ReactNode }) {
|
||||||
const { eingeloggt, istAdmin } = useAuth();
|
const { eingeloggt, istVerantwortlich } = useAuth();
|
||||||
if (!eingeloggt) {
|
if (!eingeloggt) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
if (!istAdmin) {
|
// Nicht mehr admin-exklusiv: Materialverantwortliche/Leitungsverantwortliche
|
||||||
|
// dürfen rein, sehen dort aber nur die für sie freigeschalteten Tabs
|
||||||
|
// (AdminPage filtert selbst nach Rolle, Backend erzwingt es ohnehin je Endpunkt).
|
||||||
|
if (!istVerantwortlich) {
|
||||||
return <Navigate to="/objekte" replace />;
|
return <Navigate to="/objekte" replace />;
|
||||||
}
|
}
|
||||||
return <AppShell>{children}</AppShell>;
|
return <AppShell>{children}</AppShell>;
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ import type { Me } from "../api/types";
|
|||||||
|
|
||||||
interface AuthContextValue {
|
interface AuthContextValue {
|
||||||
eingeloggt: boolean;
|
eingeloggt: boolean;
|
||||||
|
rollen: string[];
|
||||||
istAdmin: boolean;
|
istAdmin: boolean;
|
||||||
|
// Rollen-Vorgabe: Materialverantwortliche/Leitungsverantwortliche sahen bisher
|
||||||
|
// exakt dieselbe UI wie Mitarbeiter (nur istAdmin wurde geprüft), obwohl das
|
||||||
|
// Backend längst granular unterscheidet (require_roles je Endpunkt). Diese
|
||||||
|
// beiden Flags spiegeln die Backend-Rollenprüfung ins Frontend.
|
||||||
|
istMaterialverantwortlich: boolean;
|
||||||
|
istLeitungsverantwortlich: boolean;
|
||||||
|
istVerantwortlich: boolean;
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (username: string, password: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
@@ -29,7 +37,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const value = useMemo<AuthContextValue>(
|
const value = useMemo<AuthContextValue>(
|
||||||
() => ({
|
() => ({
|
||||||
eingeloggt,
|
eingeloggt,
|
||||||
|
rollen,
|
||||||
istAdmin: rollen.includes("administration"),
|
istAdmin: rollen.includes("administration"),
|
||||||
|
istMaterialverantwortlich: rollen.includes("materialverantwortlicher"),
|
||||||
|
istLeitungsverantwortlich: rollen.includes("leitungsverantwortlicher"),
|
||||||
|
istVerantwortlich:
|
||||||
|
rollen.includes("administration") ||
|
||||||
|
rollen.includes("materialverantwortlicher") ||
|
||||||
|
rollen.includes("leitungsverantwortlicher"),
|
||||||
login: async (username: string, password: string) => {
|
login: async (username: string, password: string) => {
|
||||||
const token = await apiLogin(username, password);
|
const token = await apiLogin(username, password);
|
||||||
setAuthToken(token);
|
setAuthToken(token);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAuth } from "../auth/AuthContext";
|
|||||||
|
|
||||||
/** Gemeinsamer Rahmen (Kopfzeile + Navigation) für alle eingeloggten Screens. */
|
/** Gemeinsamer Rahmen (Kopfzeile + Navigation) für alle eingeloggten Screens. */
|
||||||
export function AppShell({ children }: { children: ReactNode }) {
|
export function AppShell({ children }: { children: ReactNode }) {
|
||||||
const { istAdmin, logout } = useAuth();
|
const { istAdmin, istVerantwortlich, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
@@ -23,9 +23,9 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|||||||
<NavLink to="/objekte" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/objekte" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
Objekte
|
Objekte
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{istAdmin && (
|
{istVerantwortlich && (
|
||||||
<NavLink to="/admin" className={({ isActive }) => (isActive ? "active" : "")}>
|
<NavLink to="/admin" className={({ isActive }) => (isActive ? "active" : "")}>
|
||||||
Administration
|
{istAdmin ? "Administration" : "Verwaltung"}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
<button className="btn btn-secondary" onClick={handleLogout}>
|
<button className="btn btn-secondary" onClick={handleLogout}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { apiRequest } from "../api/client";
|
import { apiRequest } from "../api/client";
|
||||||
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import type { Benutzer, Bereich, Beladungsvorlage, Fach, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
import type { Benutzer, Bereich, Beladungsvorlage, Fach, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
||||||
import { BenutzerSection } from "./admin/BenutzerSection";
|
import { BenutzerSection } from "./admin/BenutzerSection";
|
||||||
import { BereichSection } from "./admin/BereichSection";
|
import { BereichSection } from "./admin/BereichSection";
|
||||||
@@ -28,17 +29,29 @@ type Tab =
|
|||||||
| "kontrollverantwortung"
|
| "kontrollverantwortung"
|
||||||
| "zustaendigkeit";
|
| "zustaendigkeit";
|
||||||
|
|
||||||
const TABS: { key: Tab; label: string }[] = [
|
// Deckt sich mit den require_roles()-Gates der jeweiligen Backend-Endpunkte
|
||||||
{ key: "objekte", label: "Objekte" },
|
// (Rollen-Vorgabe: Material-/Leitungsverantwortliche sahen bisher exakt
|
||||||
{ key: "vorlagen", label: "Vorlagen" },
|
// dasselbe wie Mitarbeiter - hier je Tab, wer laut Backend tatsächlich
|
||||||
{ key: "material", label: "Material" },
|
// etwas damit anfangen kann).
|
||||||
{ key: "struktur", label: "Struktur" },
|
const TABS: { key: Tab; label: string; rollen: ("administration" | "materialverantwortlicher" | "leitungsverantwortlicher")[] }[] = [
|
||||||
{ key: "standorte", label: "Standorte" },
|
{ key: "objekte", label: "Objekte", rollen: ["administration", "materialverantwortlicher"] },
|
||||||
{ key: "benutzer", label: "Benutzer" },
|
{ key: "vorlagen", label: "Vorlagen", rollen: ["administration", "materialverantwortlicher"] },
|
||||||
{ key: "zustaendigkeit", label: "Zuständigkeit" },
|
{ key: "material", label: "Material", rollen: ["administration"] },
|
||||||
{ key: "kontrollverantwortung", label: "Kontrollverantwortung" },
|
{ key: "struktur", label: "Struktur", rollen: ["administration"] },
|
||||||
{ key: "historie", label: "Änderungslog" },
|
{ key: "standorte", label: "Standorte", rollen: ["administration"] },
|
||||||
{ key: "eskalation", label: "Eskalation" },
|
{ key: "benutzer", label: "Benutzer", rollen: ["administration"] },
|
||||||
|
{ key: "zustaendigkeit", label: "Zuständigkeit", rollen: ["administration"] },
|
||||||
|
{
|
||||||
|
key: "kontrollverantwortung",
|
||||||
|
label: "Kontrollverantwortung",
|
||||||
|
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "historie",
|
||||||
|
label: "Änderungslog",
|
||||||
|
rollen: ["administration", "materialverantwortlicher", "leitungsverantwortlicher"],
|
||||||
|
},
|
||||||
|
{ key: "eskalation", label: "Eskalation", rollen: ["administration"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +64,8 @@ const TABS: { key: Tab; label: string }[] = [
|
|||||||
* Monolith-Dateien).
|
* Monolith-Dateien).
|
||||||
*/
|
*/
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
|
const { rollen, istAdmin } = useAuth();
|
||||||
|
const sichtbareTabs = TABS.filter((t) => t.rollen.some((r) => rollen.includes(r)));
|
||||||
const [bereiche, setBereiche] = useState<Bereich[]>([]);
|
const [bereiche, setBereiche] = useState<Bereich[]>([]);
|
||||||
const [kategorien, setKategorien] = useState<Kategorie[]>([]);
|
const [kategorien, setKategorien] = useState<Kategorie[]>([]);
|
||||||
const [objekttypen, setObjekttypen] = useState<Objekttyp[]>([]);
|
const [objekttypen, setObjekttypen] = useState<Objekttyp[]>([]);
|
||||||
@@ -62,10 +77,16 @@ export function AdminPage() {
|
|||||||
const [faecher, setFaecher] = useState<Fach[]>([]);
|
const [faecher, setFaecher] = useState<Fach[]>([]);
|
||||||
const [fehler, setFehler] = useState<string | null>(null);
|
const [fehler, setFehler] = useState<string | null>(null);
|
||||||
const [laedt, setLaedt] = useState(true);
|
const [laedt, setLaedt] = useState(true);
|
||||||
const [tab, setTab] = useState<Tab>("objekte");
|
const [tab, setTab] = useState<Tab | null>(null);
|
||||||
|
const aktiverTab = tab && sichtbareTabs.some((t) => t.key === tab) ? tab : sichtbareTabs[0]?.key ?? null;
|
||||||
|
|
||||||
async function ladeAlles() {
|
async function ladeAlles() {
|
||||||
const [be, ka, ot, s, v, o, m, b, f] = await Promise.all([
|
// /benutzer ist admin-only (Backend require_roles) - Promise.all würde bei
|
||||||
|
// Material-/Leitungsverantwortlichen mit 403 durchfallen und die komplette
|
||||||
|
// Seite als "nicht geladen" zeigen, obwohl deren Tabs (z.B. Kontroll-
|
||||||
|
// verantwortung) trotzdem funktionieren sollen. allSettled + leerer
|
||||||
|
// Fallback statt hartem Fehler.
|
||||||
|
const [be, ka, ot, s, v, o, m, b, f] = await Promise.allSettled([
|
||||||
apiRequest<Bereich[]>("/bereiche"),
|
apiRequest<Bereich[]>("/bereiche"),
|
||||||
apiRequest<Kategorie[]>("/kategorien"),
|
apiRequest<Kategorie[]>("/kategorien"),
|
||||||
apiRequest<Objekttyp[]>("/objekttypen"),
|
apiRequest<Objekttyp[]>("/objekttypen"),
|
||||||
@@ -76,15 +97,16 @@ export function AdminPage() {
|
|||||||
apiRequest<Benutzer[]>("/benutzer"),
|
apiRequest<Benutzer[]>("/benutzer"),
|
||||||
apiRequest<Fach[]>("/faecher"),
|
apiRequest<Fach[]>("/faecher"),
|
||||||
]);
|
]);
|
||||||
setBereiche(be);
|
const wert = <T,>(r: PromiseSettledResult<T[]>): T[] => (r.status === "fulfilled" ? r.value : []);
|
||||||
setKategorien(ka);
|
setBereiche(wert(be));
|
||||||
setObjekttypen(ot);
|
setKategorien(wert(ka));
|
||||||
setStandorte(s);
|
setObjekttypen(wert(ot));
|
||||||
setVorlagen(v);
|
setStandorte(wert(s));
|
||||||
setObjekte(o);
|
setVorlagen(wert(v));
|
||||||
setMaterialien(m);
|
setObjekte(wert(o));
|
||||||
setBenutzer(b);
|
setMaterialien(wert(m));
|
||||||
setFaecher(f);
|
setBenutzer(wert(b));
|
||||||
|
setFaecher(wert(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,14 +125,14 @@ export function AdminPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="page">
|
<main className="page">
|
||||||
<h1>Administration</h1>
|
<h1>{istAdmin ? "Administration" : "Verwaltung"}</h1>
|
||||||
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
||||||
|
|
||||||
<nav className="row" style={{ marginBottom: "1.25rem", flexWrap: "wrap", gap: "0.5rem" }}>
|
<nav className="row" style={{ marginBottom: "1.25rem", flexWrap: "wrap", gap: "0.5rem" }}>
|
||||||
{TABS.map((t) => (
|
{sichtbareTabs.map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.key}
|
key={t.key}
|
||||||
className={tab === t.key ? "btn btn-primary" : "btn btn-secondary"}
|
className={aktiverTab === t.key ? "btn btn-primary" : "btn btn-secondary"}
|
||||||
onClick={() => setTab(t.key)}
|
onClick={() => setTab(t.key)}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
@@ -118,17 +140,18 @@ export function AdminPage() {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{tab === "objekte" && (
|
{aktiverTab === "objekte" && (
|
||||||
<ObjektSection
|
<ObjektSection
|
||||||
objekte={objekte}
|
objekte={objekte}
|
||||||
vorlagen={vorlagen.filter((v) => v.status === "aktiv")}
|
vorlagen={vorlagen.filter((v) => v.status === "aktiv")}
|
||||||
standorte={standorte}
|
standorte={standorte}
|
||||||
materialien={materialien}
|
materialien={materialien}
|
||||||
|
nurPflegen={!istAdmin}
|
||||||
onGeaendert={ladeAlles}
|
onGeaendert={ladeAlles}
|
||||||
onFehler={setFehler}
|
onFehler={setFehler}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "vorlagen" && (
|
{aktiverTab === "vorlagen" && (
|
||||||
<VorlageSection
|
<VorlageSection
|
||||||
vorlagen={vorlagen}
|
vorlagen={vorlagen}
|
||||||
objekttypen={objekttypen}
|
objekttypen={objekttypen}
|
||||||
@@ -138,10 +161,10 @@ export function AdminPage() {
|
|||||||
onFehler={setFehler}
|
onFehler={setFehler}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "material" && (
|
{aktiverTab === "material" && (
|
||||||
<MaterialSection materialien={materialien} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<MaterialSection materialien={materialien} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
{tab === "struktur" && (
|
{aktiverTab === "struktur" && (
|
||||||
<>
|
<>
|
||||||
<BereichSection bereiche={bereiche} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<BereichSection bereiche={bereiche} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
<KategorieSection
|
<KategorieSection
|
||||||
@@ -165,20 +188,21 @@ export function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tab === "standorte" && (
|
{aktiverTab === "standorte" && (
|
||||||
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
{tab === "benutzer" && (
|
{aktiverTab === "benutzer" && (
|
||||||
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<BenutzerSection benutzer={benutzer} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
{tab === "zustaendigkeit" && (
|
{aktiverTab === "zustaendigkeit" && (
|
||||||
<ZustaendigkeitSection standorte={standorte} objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
|
<ZustaendigkeitSection standorte={standorte} objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
{tab === "kontrollverantwortung" && (
|
{aktiverTab === "kontrollverantwortung" && (
|
||||||
<KontrollverantwortungSection objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
|
<KontrollverantwortungSection objekte={objekte} benutzer={benutzer} onFehler={setFehler} />
|
||||||
)}
|
)}
|
||||||
{tab === "historie" && <HistorieSection onFehler={setFehler} />}
|
{aktiverTab === "historie" && <HistorieSection onFehler={setFehler} />}
|
||||||
{tab === "eskalation" && <EskalationSection onFehler={setFehler} />}
|
{aktiverTab === "eskalation" && <EskalationSection onFehler={setFehler} />}
|
||||||
|
{aktiverTab === null && <p className="text-muted">Keine Bereiche für deine Rolle verfügbar.</p>}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,25 @@ interface Props {
|
|||||||
vorlagen: Beladungsvorlage[];
|
vorlagen: Beladungsvorlage[];
|
||||||
standorte: Standort[];
|
standorte: Standort[];
|
||||||
materialien: Material[];
|
materialien: Material[];
|
||||||
|
// Materialverantwortliche dürfen laut Backend nur Objektpositionen pflegen
|
||||||
|
// (Ablauf/Charge/SN/Prüfintervall/Geräte-Instanzen), nicht Objekte anlegen/
|
||||||
|
// duplizieren/koppeln (require_roles(administration) dort) - Anlegen-Formular
|
||||||
|
// und Duplizieren/Kopplung blenden wir entsprechend aus statt sie ins Leere
|
||||||
|
// laufen zu lassen (403).
|
||||||
|
nurPflegen?: boolean;
|
||||||
onGeaendert: () => Promise<void>;
|
onGeaendert: () => Promise<void>;
|
||||||
onFehler: (text: string) => void;
|
onFehler: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGeaendert, onFehler }: Props) {
|
export function ObjektSection({
|
||||||
|
objekte,
|
||||||
|
vorlagen,
|
||||||
|
standorte,
|
||||||
|
materialien,
|
||||||
|
nurPflegen = false,
|
||||||
|
onGeaendert,
|
||||||
|
onFehler,
|
||||||
|
}: Props) {
|
||||||
const [praefix, setPraefix] = useState("");
|
const [praefix, setPraefix] = useState("");
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -142,8 +156,9 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h2>Objekt aus Vorlage anlegen</h2>
|
<h2>{nurPflegen ? "Objekte" : "Objekt aus Vorlage anlegen"}</h2>
|
||||||
<div className="card">
|
{!nurPflegen && (
|
||||||
|
<div className="card" style={{ marginBottom: "1.25rem" }}>
|
||||||
{vorlagen.length === 0 ? (
|
{vorlagen.length === 0 ? (
|
||||||
<p className="text-muted">
|
<p className="text-muted">
|
||||||
Keine aktive Vorlage vorhanden – erst Excel-Import oder Vorlage per API anlegen.
|
Keine aktive Vorlage vorhanden – erst Excel-Import oder Vorlage per API anlegen.
|
||||||
@@ -216,9 +231,12 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
{objekte.length > 0 && (
|
{objekte.length > 0 && (
|
||||||
<ul className="card-list" style={{ marginTop: "1.25rem" }}>
|
<ul className="card-list">
|
||||||
{objekte.map((o) => (
|
{objekte.map((o) => (
|
||||||
<li key={o.id}>
|
<li key={o.id}>
|
||||||
<div className="row-between" style={{ padding: "0.4rem 0" }}>
|
<div className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
@@ -235,12 +253,16 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
>
|
>
|
||||||
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
|
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-secondary" onClick={() => kopplungBearbeiten(o)}>
|
{!nurPflegen && (
|
||||||
{kopplungBearbeitenId === o.id ? "Schließen" : "Kopplung ändern"}
|
<button className="btn btn-secondary" onClick={() => kopplungBearbeiten(o)}>
|
||||||
</button>
|
{kopplungBearbeitenId === o.id ? "Schließen" : "Kopplung ändern"}
|
||||||
<button className="btn btn-secondary" onClick={() => duplizierenStarten(o)}>
|
</button>
|
||||||
{duplizierId === o.id ? "Schließen" : "Duplizieren"}
|
)}
|
||||||
</button>
|
{!nurPflegen && (
|
||||||
|
<button className="btn btn-secondary" onClick={() => duplizierenStarten(o)}>
|
||||||
|
{duplizierId === o.id ? "Schließen" : "Duplizieren"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{kopplungBearbeitenId === o.id && (
|
{kopplungBearbeitenId === o.id && (
|
||||||
|
|||||||
Reference in New Issue
Block a user