From a97886defdfbdc67139cf133ec05e8ea4c5e47d9 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 00:33:28 +0200 Subject: [PATCH] Sprint 8: minimale Admin-Seite (Standort/Objekt-Anlage) im Frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend deckte bisher nur den Mitarbeiter-Kontrollflow ab, Stammdaten mussten per API/Skript angelegt werden (Test-Deployment-Lücke). /admin zeigt Standort- und Objekt-Anlage (aus aktiver Vorlage), nur für Rolle administration sichtbar (/auth/me liefert Rollen an AuthContext). Bewusst kein Bereich/Kategorie/Material-CRUD - kommt aus Excel-Import. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt --- frontend/src/App.tsx | 20 ++++ frontend/src/api/types.ts | 21 ++++ frontend/src/auth/AuthContext.tsx | 20 +++- frontend/src/pages/AdminPage.tsx | 135 ++++++++++++++++++++++++++ frontend/src/pages/ObjektListPage.tsx | 7 ++ 5 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/AdminPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 758d98b..3be3029 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { AuthProvider, useAuth } from "./auth/AuthContext"; +import { AdminPage } from "./pages/AdminPage"; import { KontrollPage } from "./pages/KontrollPage"; import { LoginPage } from "./pages/LoginPage"; import { ObjektListPage } from "./pages/ObjektListPage"; @@ -14,6 +15,17 @@ function GeschuetzteRoute({ children }: { children: ReactNode }) { return <>{children}; } +function AdminRoute({ children }: { children: ReactNode }) { + const { eingeloggt, istAdmin } = useAuth(); + if (!eingeloggt) { + return ; + } + if (!istAdmin) { + return ; + } + return <>{children}; +} + export default function App() { return ( @@ -35,6 +47,14 @@ export default function App() { } /> + + + + } + /> } /> diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 1608880..42c889b 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -47,3 +47,24 @@ export interface Material { einheit: string; materialtyp: string; } + +export interface Standort { + id: number; + name: string; + adresse: string | null; +} + +export interface Beladungsvorlage { + id: number; + objekttyp_id: number; + name: string; + version: number; + status: string; +} + +export interface Me { + id: number; + name: string; + login: string; + rollen: string[]; +} diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index 2d0239f..f84400b 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -1,9 +1,11 @@ -import { createContext, useContext, useMemo, useState, type ReactNode } from "react"; +import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; -import { getAuthToken, login as apiLogin, setAuthToken } from "../api/client"; +import { apiRequest, getAuthToken, login as apiLogin, setAuthToken } from "../api/client"; +import type { Me } from "../api/types"; interface AuthContextValue { eingeloggt: boolean; + istAdmin: boolean; login: (username: string, password: string) => Promise; logout: () => void; } @@ -12,10 +14,22 @@ const AuthContext = createContext(null); export function AuthProvider({ children }: { children: ReactNode }) { const [eingeloggt, setEingeloggt] = useState(getAuthToken() !== null); + const [rollen, setRollen] = useState([]); + + useEffect(() => { + if (!eingeloggt) { + setRollen([]); + return; + } + apiRequest("/auth/me") + .then((me) => setRollen(me.rollen)) + .catch(() => setRollen([])); + }, [eingeloggt]); const value = useMemo( () => ({ eingeloggt, + istAdmin: rollen.includes("administration"), login: async (username: string, password: string) => { const token = await apiLogin(username, password); setAuthToken(token); @@ -26,7 +40,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setEingeloggt(false); }, }), - [eingeloggt] + [eingeloggt, rollen] ); return {children}; diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx new file mode 100644 index 0000000..a6f0922 --- /dev/null +++ b/frontend/src/pages/AdminPage.tsx @@ -0,0 +1,135 @@ +import { useEffect, useState } from "react"; + +import { apiRequest } from "../api/client"; +import type { Beladungsvorlage, Objekt, Standort } from "../api/types"; + +/** + * Minimale Admin-Seite (Standort/Objekt-Anlage), nachgezogen weil das + * Frontend bisher nur den Mitarbeiter-Kontrollflow (Sprint 7) abdeckte und + * Stammdaten/Objekte nur per API/Skript angelegt werden konnten. Bewusst + * schlank: keine Bereich-/Kategorie-/Material-/Vorlagen-CRUD, nur was für + * die Pilotphase sofort nötig ist (Vorlagen kommen aus dem Excel-Import). + */ +export function AdminPage() { + const [standorte, setStandorte] = useState([]); + const [vorlagen, setVorlagen] = useState([]); + const [objekte, setObjekte] = useState([]); + const [fehler, setFehler] = useState(null); + + const [standortName, setStandortName] = useState(""); + const [objektCode, setObjektCode] = useState(""); + const [objektName, setObjektName] = useState(""); + const [vorlageId, setVorlageId] = useState(""); + const [standortId, setStandortId] = useState(""); + + async function ladeAlles() { + const [s, v, o] = await Promise.all([ + apiRequest("/standorte"), + apiRequest("/vorlagen"), + apiRequest("/objekte"), + ]); + setStandorte(s); + setVorlagen(v.filter((vorlage) => vorlage.status === "aktiv")); + setObjekte(o); + } + + useEffect(() => { + ladeAlles().catch(() => setFehler("Stammdaten konnten nicht geladen werden.")); + }, []); + + async function standortAnlegen() { + setFehler(null); + try { + await apiRequest("/standorte", { method: "POST", body: { name: standortName } }); + setStandortName(""); + await ladeAlles(); + } catch { + setFehler("Standort konnte nicht angelegt werden."); + } + } + + async function objektAnlegen() { + if (vorlageId === "" || standortId === "") { + setFehler("Vorlage und Standort auswählen."); + return; + } + setFehler(null); + const vorlage = vorlagen.find((v) => v.id === vorlageId); + try { + await apiRequest("/objekte", { + method: "POST", + body: { + code: objektCode, + name: objektName, + objekttyp_id: vorlage?.objekttyp_id, + vorlage_id: vorlageId, + standort_id: standortId, + }, + }); + setObjektCode(""); + setObjektName(""); + await ladeAlles(); + } catch { + setFehler("Objekt konnte nicht angelegt werden (Code evtl. schon vergeben)."); + } + } + + return ( +
+

Administration

+ {fehler &&

{fehler}

} + +
+

Standort anlegen

+ setStandortName(e.target.value)} + placeholder="Name" + /> + +
    + {standorte.map((s) => ( +
  • {s.name}
  • + ))} +
+
+ +
+

Objekt aus Vorlage anlegen

+
+ + + setObjektCode(e.target.value)} placeholder="Code (eindeutig)" /> + setObjektName(e.target.value)} placeholder="Name" /> + +
+ +
    + {objekte.map((o) => ( +
  • + {o.name} ({o.code}) +
  • + ))} +
+
+
+ ); +} diff --git a/frontend/src/pages/ObjektListPage.tsx b/frontend/src/pages/ObjektListPage.tsx index d110483..eb24125 100644 --- a/frontend/src/pages/ObjektListPage.tsx +++ b/frontend/src/pages/ObjektListPage.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import { apiRequest } from "../api/client"; import type { Objekt } from "../api/types"; +import { useAuth } from "../auth/AuthContext"; // Prompt 11 Screen 1: Standort/Objekt wählen. QR-/Barcode-Scan (Karte 10) ist // Roadmap - dieser Screen bietet bewusst schon die Code-Suche als Textfeld an, @@ -11,6 +12,7 @@ export function ObjektListPage() { const [objekte, setObjekte] = useState([]); const [suche, setSuche] = useState(""); const [ladefehler, setLadefehler] = useState(null); + const { istAdmin } = useAuth(); useEffect(() => { apiRequest("/objekte") @@ -27,6 +29,11 @@ export function ObjektListPage() { return (

Objekte

+ {istAdmin && ( + + Administration → + + )}