From 2f11011ed1f945766c58ccc6d7040adac88853ae Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 09:55:52 +0200 Subject: [PATCH] =?UTF-8?q?Admin-Portal:=20Bearbeiten=20f=C3=BCr=20Bereich?= =?UTF-8?q?/Kategorie/Objekttyp/Standort/Benutzer,=20Vorlagen-Versionierun?= =?UTF-8?q?g=20und=20Objekt-Duplizieren=20im=20Frontend=20nachger=C3=BCste?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: PATCH-Endpoints für Bereich/Kategorie/Standort/Objekttyp ergänzt (analog zu Material), die bislang fehlten und Bearbeiten grundsätzlich unmöglich machten. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt --- DEVLOG.md | 212 ++++++++++++++++++ backend/app/api/v1/endpoints/stammdaten.py | 68 ++++++ backend/app/schemas/stammdaten.py | 22 ++ backend/tests/test_stammdaten.py | 62 +++++ frontend/src/api/types.ts | 9 + frontend/src/pages/admin/BenutzerSection.tsx | 92 +++++++- frontend/src/pages/admin/BereichSection.tsx | 42 +++- frontend/src/pages/admin/KategorieSection.tsx | 43 +++- frontend/src/pages/admin/ObjektSection.tsx | 79 ++++++- frontend/src/pages/admin/ObjekttypSection.tsx | 42 +++- frontend/src/pages/admin/StandortSection.tsx | 40 +++- frontend/src/pages/admin/VorlageSection.tsx | 99 +++++++- 12 files changed, 786 insertions(+), 24 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 1730f61..01c6c0a 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2051,3 +2051,215 @@ Keine Commits in dieser Session. - frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++- --- +## 2026-09-04 09:01 – 09:04 (3m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- d70558f Inline-Nachfüll-Dialog nach Abweichung in der Kontrolle (Karte 07) + +### Geänderte Dateien +- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/offline/queue.ts | 18 +++++++++++++++++- +- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++------- +- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++-- +- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++- + +--- +## 2026-09-04 09:05 – 09:05 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/offline/queue.ts | 18 +++++++++++++++++- +- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++------- +- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++-- +- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++- + +--- +## 2026-09-04 09:11 – 09:11 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/offline/queue.ts | 18 +++++++++++++++++- +- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++------- +- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++-- +- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++- + +--- +## 2026-09-04 09:11 – 09:11 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/offline/queue.ts | 18 +++++++++++++++++- +- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++------- +- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++-- +- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++- + +--- +## 2026-09-04 09:11 – 09:12 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/src/offline/queue.ts | 18 +++++++++++++++++- +- frontend/src/pages/KontrollPage.tsx | 29 ++++++++++++++++++++++------- +- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/kontrolle/PositionCard.tsx | 4 ++-- +- frontend/src/pages/kontrolle/useKontrolle.ts | 32 +++++++++++++++++++++++++++++++- + +--- +## 2026-09-04 09:12 – 09:14 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- 032126c fix(kontrolle): Abbruch löscht bereits nachgefüllten Fehlbestand nicht mehr + +### Geänderte Dateien +- backend/app/services/kontrolle/lifecycle.py | 8 +++++++- +- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:21 – 09:21 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/services/kontrolle/lifecycle.py | 8 +++++++- +- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:21 – 09:22 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/services/kontrolle/lifecycle.py | 8 +++++++- +- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:23 – 09:23 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/services/kontrolle/lifecycle.py | 8 +++++++- +- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:26 – 09:26 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- backend/app/services/kontrolle/lifecycle.py | 8 +++++++- +- backend/tests/test_kontrolle.py | 40 ++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:29 – 09:30 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- 2c9ca3e Admin-Portal: Tab-Menü statt einer langen Scroll-Seite + +### Geänderte Dateien +- frontend/src/pages/AdminPage.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- + +--- +## 2026-09-04 09:38 – 09:38 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- frontend/src/pages/AdminPage.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- + +--- +## 2026-09-04 09:43 – 09:43 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- frontend/src/pages/AdminPage.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- + +--- +## 2026-09-04 09:43 – 09:44 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- 1987818 Admin-Portal: Benutzerverwaltung (anlegen, Rollen, aktivieren/deaktivieren) + +### Geänderte Dateien +- frontend/src/api/types.ts | 11 +++++++ +- frontend/src/pages/AdminPage.tsx | 14 +++++++-- +- frontend/src/pages/admin/BenutzerSection.tsx | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:44 – 09:45 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- frontend/src/api/types.ts | 11 +++++++ +- frontend/src/pages/AdminPage.tsx | 14 +++++++-- +- frontend/src/pages/admin/BenutzerSection.tsx | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-04 09:47 – 09:47 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- frontend/src/api/types.ts | 11 +++++++ +- frontend/src/pages/AdminPage.tsx | 14 +++++++-- +- frontend/src/pages/admin/BenutzerSection.tsx | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- diff --git a/backend/app/api/v1/endpoints/stammdaten.py b/backend/app/api/v1/endpoints/stammdaten.py index 006caa4..b0b5279 100644 --- a/backend/app/api/v1/endpoints/stammdaten.py +++ b/backend/app/api/v1/endpoints/stammdaten.py @@ -9,15 +9,19 @@ from app.models.stammdaten import Bereich, Kategorie, Material, Objekttyp, Stand from app.schemas.stammdaten import ( BereichCreate, BereichRead, + BereichUpdate, KategorieCreate, KategorieRead, + KategorieUpdate, MaterialCreate, MaterialRead, MaterialUpdate, ObjekttypCreate, ObjekttypRead, + ObjekttypUpdate, StandortCreate, StandortRead, + StandortUpdate, ) router = APIRouter() @@ -45,6 +49,22 @@ async def erstelle_bereich( return bereich +@router.patch("/bereiche/{bereich_id}", response_model=BereichRead) +async def aendere_bereich( + bereich_id: int, + payload: BereichUpdate, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> Bereich: + bereich = await db.get(Bereich, bereich_id) + if bereich is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bereich nicht gefunden") + for feld, wert in payload.model_dump(exclude_unset=True).items(): + setattr(bereich, feld, wert) + await db.flush() + return bereich + + # -- Kategorie ------------------------------------------------------------------ @router.get("/kategorien", response_model=list[KategorieRead]) @@ -67,6 +87,22 @@ async def erstelle_kategorie( return kategorie +@router.patch("/kategorien/{kategorie_id}", response_model=KategorieRead) +async def aendere_kategorie( + kategorie_id: int, + payload: KategorieUpdate, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> Kategorie: + kategorie = await db.get(Kategorie, kategorie_id) + if kategorie is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kategorie nicht gefunden") + for feld, wert in payload.model_dump(exclude_unset=True).items(): + setattr(kategorie, feld, wert) + await db.flush() + return kategorie + + # -- Standort ------------------------------------------------------------------ @router.get("/standorte", response_model=list[StandortRead]) @@ -89,6 +125,22 @@ async def erstelle_standort( return standort +@router.patch("/standorte/{standort_id}", response_model=StandortRead) +async def aendere_standort( + standort_id: int, + payload: StandortUpdate, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> Standort: + standort = await db.get(Standort, standort_id) + if standort is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Standort nicht gefunden") + for feld, wert in payload.model_dump(exclude_unset=True).items(): + setattr(standort, feld, wert) + await db.flush() + return standort + + # -- Objekttyp ------------------------------------------------------------------ @router.get("/objekttypen", response_model=list[ObjekttypRead]) @@ -111,6 +163,22 @@ async def erstelle_objekttyp( return objekttyp +@router.patch("/objekttypen/{objekttyp_id}", response_model=ObjekttypRead) +async def aendere_objekttyp( + objekttyp_id: int, + payload: ObjekttypUpdate, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> Objekttyp: + objekttyp = await db.get(Objekttyp, objekttyp_id) + if objekttyp is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekttyp nicht gefunden") + for feld, wert in payload.model_dump(exclude_unset=True).items(): + setattr(objekttyp, feld, wert) + await db.flush() + return objekttyp + + # -- Material (Prompt 07) -------------------------------------------------- @router.get("/materialien", response_model=list[MaterialRead]) diff --git a/backend/app/schemas/stammdaten.py b/backend/app/schemas/stammdaten.py index 3a87a5b..9bfd26a 100644 --- a/backend/app/schemas/stammdaten.py +++ b/backend/app/schemas/stammdaten.py @@ -13,6 +13,11 @@ class BereichRead(BereichCreate): id: int +class BereichUpdate(BaseModel): + name: str | None = None + beschreibung: str | None = None + + class KategorieCreate(BaseModel): bereich_id: int name: str @@ -24,6 +29,12 @@ class KategorieRead(KategorieCreate): id: int +class KategorieUpdate(BaseModel): + bereich_id: int | None = None + name: str | None = None + ueberkategorie_id: int | None = None + + class StandortCreate(BaseModel): name: str adresse: str | None = None @@ -34,6 +45,11 @@ class StandortRead(StandortCreate): id: int +class StandortUpdate(BaseModel): + name: str | None = None + adresse: str | None = None + + class ObjekttypCreate(BaseModel): bereich_id: int kategorie_id: int | None = None @@ -45,6 +61,12 @@ class ObjekttypRead(ObjekttypCreate): id: int +class ObjekttypUpdate(BaseModel): + bereich_id: int | None = None + kategorie_id: int | None = None + name: str | None = None + + class MaterialCreate(BaseModel): name: str artikelnummer: str | None = None diff --git a/backend/tests/test_stammdaten.py b/backend/tests/test_stammdaten.py index 507e44e..1d26c84 100644 --- a/backend/tests/test_stammdaten.py +++ b/backend/tests/test_stammdaten.py @@ -60,3 +60,65 @@ async def test_material_not_found(client, admin_user): token = await login(client, "admin1") response = await client.get("/api/v1/materialien/999999", headers=auth_header(token)) assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_admin_can_update_bereich(client, admin_user): + token = await login(client, "admin1") + erstellt = await client.post("/api/v1/bereiche", json={"name": "Alt"}, headers=auth_header(token)) + bereich_id = erstellt.json()["id"] + + response = await client.patch( + f"/api/v1/bereiche/{bereich_id}", json={"name": "Neu"}, headers=auth_header(token) + ) + assert response.status_code == 200 + assert response.json()["name"] == "Neu" + + +@pytest.mark.asyncio +async def test_admin_can_update_kategorie(client, admin_user): + token = await login(client, "admin1") + bereich = await client.post("/api/v1/bereiche", json={"name": "B"}, headers=auth_header(token)) + erstellt = await client.post( + "/api/v1/kategorien", + json={"bereich_id": bereich.json()["id"], "name": "Alt"}, + headers=auth_header(token), + ) + kategorie_id = erstellt.json()["id"] + + response = await client.patch( + f"/api/v1/kategorien/{kategorie_id}", json={"name": "Neu"}, headers=auth_header(token) + ) + assert response.status_code == 200 + assert response.json()["name"] == "Neu" + + +@pytest.mark.asyncio +async def test_admin_can_update_standort(client, admin_user): + token = await login(client, "admin1") + erstellt = await client.post("/api/v1/standorte", json={"name": "Alt"}, headers=auth_header(token)) + standort_id = erstellt.json()["id"] + + response = await client.patch( + f"/api/v1/standorte/{standort_id}", json={"name": "Neu"}, headers=auth_header(token) + ) + assert response.status_code == 200 + assert response.json()["name"] == "Neu" + + +@pytest.mark.asyncio +async def test_admin_can_update_objekttyp(client, admin_user): + token = await login(client, "admin1") + bereich = await client.post("/api/v1/bereiche", json={"name": "B"}, headers=auth_header(token)) + erstellt = await client.post( + "/api/v1/objekttypen", + json={"bereich_id": bereich.json()["id"], "name": "Alt"}, + headers=auth_header(token), + ) + objekttyp_id = erstellt.json()["id"] + + response = await client.patch( + f"/api/v1/objekttypen/{objekttyp_id}", json={"name": "Neu"}, headers=auth_header(token) + ) + assert response.status_code == 200 + assert response.json()["name"] == "Neu" diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 216975a..425c168 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -61,12 +61,21 @@ export interface Standort { adresse: string | null; } +export interface Vorlagenposition { + id: string; + vorlage_id: number; + material_id: number; + fach: string | null; + sollmenge: string; +} + export interface Beladungsvorlage { id: number; objekttyp_id: number; name: string; version: number; status: string; + positionen: Vorlagenposition[]; } export interface Bereich { diff --git a/frontend/src/pages/admin/BenutzerSection.tsx b/frontend/src/pages/admin/BenutzerSection.tsx index 46a12ba..2b0d05d 100644 --- a/frontend/src/pages/admin/BenutzerSection.tsx +++ b/frontend/src/pages/admin/BenutzerSection.tsx @@ -23,6 +23,10 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) { const [email, setEmail] = useState(""); const [rollen, setRollen] = useState(["mitarbeiter"]); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [bearbeiteteId, setBearbeiteteId] = useState(null); + const [bearbeitungsName, setBearbeitungsName] = useState(""); + const [bearbeitungsEmail, setBearbeitungsEmail] = useState(""); + const [bearbeitungsRollen, setBearbeitungsRollen] = useState([]); function rolleUmschalten(rolle: Rolle) { setRollen((bisherige) => @@ -59,6 +63,32 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) { } } + function bearbeiten(b: Benutzer) { + setBearbeiteteId(b.id); + setBearbeitungsName(b.name); + setBearbeitungsEmail(b.email ?? ""); + setBearbeitungsRollen(b.rollen as Rolle[]); + } + + function bearbeitungsRolleUmschalten(rolle: Rolle) { + setBearbeitungsRollen((bisherige) => + bisherige.includes(rolle) ? bisherige.filter((r) => r !== rolle) : [...bisherige, rolle] + ); + } + + async function speichern(id: number) { + try { + await apiRequest(`/benutzer/${id}`, { + method: "PATCH", + body: { name: bearbeitungsName, email: bearbeitungsEmail || null, rollen: bearbeitungsRollen }, + }); + setBearbeiteteId(null); + await onGeaendert(); + } catch { + onFehler("Benutzer konnte nicht geändert werden."); + } + } + return (

Benutzer anlegen

@@ -99,14 +129,60 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) { {benutzer.length > 0 && (
    {benutzer.map((b) => ( -
  • - - {b.name} ({b.login}) – {b.rollen.join(", ")} - {!b.aktiv && " – deaktiviert"} - - +
  • + {bearbeiteteId === b.id ? ( +
    +
    + setBearbeitungsName(e.target.value)} + placeholder="Name" + /> + setBearbeitungsEmail(e.target.value)} + placeholder="E-Mail" + /> +
    +
    + {ALLE_ROLLEN.map((r) => ( + + ))} +
    +
    + + +
    +
    + ) : ( +
    + + {b.name} ({b.login}) – {b.rollen.join(", ")} + {!b.aktiv && " – deaktiviert"} + +
    + + +
    +
    + )}
  • ))}
diff --git a/frontend/src/pages/admin/BereichSection.tsx b/frontend/src/pages/admin/BereichSection.tsx index 03adc7c..80f99b8 100644 --- a/frontend/src/pages/admin/BereichSection.tsx +++ b/frontend/src/pages/admin/BereichSection.tsx @@ -12,6 +12,8 @@ interface Props { export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) { const [name, setName] = useState(""); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [bearbeiteteId, setBearbeiteteId] = useState(null); + const [bearbeitungsName, setBearbeitungsName] = useState(""); async function anlegen() { setWirdAngelegt(true); @@ -26,6 +28,21 @@ export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) { } } + function bearbeiten(b: Bereich) { + setBearbeiteteId(b.id); + setBearbeitungsName(b.name); + } + + async function speichern(id: number) { + try { + await apiRequest(`/bereiche/${id}`, { method: "PATCH", body: { name: bearbeitungsName } }); + setBearbeiteteId(null); + await onGeaendert(); + } catch { + onFehler("Bereich konnte nicht geändert werden."); + } + } + return (

Bereich anlegen

@@ -39,8 +56,29 @@ export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) { {bereiche.length > 0 && (
    {bereiche.map((b) => ( -
  • - {b.name} +
  • + {bearbeiteteId === b.id ? ( +
    + setBearbeitungsName(e.target.value)} + /> + + +
    + ) : ( + <> + {b.name} + + + )}
  • ))}
diff --git a/frontend/src/pages/admin/KategorieSection.tsx b/frontend/src/pages/admin/KategorieSection.tsx index 076ed04..9072557 100644 --- a/frontend/src/pages/admin/KategorieSection.tsx +++ b/frontend/src/pages/admin/KategorieSection.tsx @@ -14,6 +14,8 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler } const [name, setName] = useState(""); const [bereichId, setBereichId] = useState(""); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [bearbeiteteId, setBearbeiteteId] = useState(null); + const [bearbeitungsName, setBearbeitungsName] = useState(""); async function anlegen() { if (bereichId === "") { @@ -36,6 +38,21 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler } return bereiche.find((b) => b.id === id)?.name ?? `#${id}`; } + function bearbeiten(k: Kategorie) { + setBearbeiteteId(k.id); + setBearbeitungsName(k.name); + } + + async function speichern(id: number) { + try { + await apiRequest(`/kategorien/${id}`, { method: "PATCH", body: { name: bearbeitungsName } }); + setBearbeiteteId(null); + await onGeaendert(); + } catch { + onFehler("Kategorie konnte nicht geändert werden."); + } + } + return (

Kategorie anlegen

@@ -64,8 +81,30 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
    {kategorien.map((k) => (
  • - {k.name} - {bereichName(k.bereich_id)} + {bearbeiteteId === k.id ? ( +
    + setBearbeitungsName(e.target.value)} + /> + + +
    + ) : ( + <> + + {k.name} ({bereichName(k.bereich_id)}) + + + + )}
  • ))}
diff --git a/frontend/src/pages/admin/ObjektSection.tsx b/frontend/src/pages/admin/ObjektSection.tsx index b60373a..3166ca5 100644 --- a/frontend/src/pages/admin/ObjektSection.tsx +++ b/frontend/src/pages/admin/ObjektSection.tsx @@ -20,6 +20,10 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea const [standortId, setStandortId] = useState(""); const [wirdAngelegt, setWirdAngelegt] = useState(false); const [ausgeklapptId, setAusgeklapptId] = useState(null); + const [duplizierId, setDuplizierId] = useState(null); + const [neuerCode, setNeuerCode] = useState(""); + const [neuerName, setNeuerName] = useState(""); + const [neuerStandortId, setNeuerStandortId] = useState(""); async function anlegen() { if (vorlageId === "" || standortId === "") { @@ -49,6 +53,30 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea } } + function duplizierenStarten(o: Objekt) { + setDuplizierId(duplizierId === o.id ? null : o.id); + setNeuerCode(""); + setNeuerName(`${o.name} (Kopie)`); + setNeuerStandortId(o.standort_id); + } + + async function duplizieren(quelleId: number) { + if (neuerStandortId === "") { + onFehler("Standort auswählen."); + return; + } + try { + await apiRequest(`/objekte/${quelleId}/duplizieren`, { + method: "POST", + body: { neuer_code: neuerCode, neuer_name: neuerName, neuer_standort_id: neuerStandortId }, + }); + setDuplizierId(null); + await onGeaendert(); + } catch { + onFehler("Objekt konnte nicht dupliziert werden (Code evtl. schon vergeben)."); + } + } + return (

Objekt aus Vorlage anlegen

@@ -107,18 +135,57 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea {o.name} ({o.code}) - +
+ + +
{ausgeklapptId === o.id && (
)} + {duplizierId === o.id && ( +
+ setNeuerCode(e.target.value)} + placeholder="Neuer Code (eindeutig)" + /> + setNeuerName(e.target.value)} + placeholder="Neuer Name" + /> + + +
+ )} ))} diff --git a/frontend/src/pages/admin/ObjekttypSection.tsx b/frontend/src/pages/admin/ObjekttypSection.tsx index e09a536..c73af45 100644 --- a/frontend/src/pages/admin/ObjekttypSection.tsx +++ b/frontend/src/pages/admin/ObjekttypSection.tsx @@ -16,6 +16,23 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender const [bereichId, setBereichId] = useState(""); const [kategorieId, setKategorieId] = useState(""); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [bearbeiteteId, setBearbeiteteId] = useState(null); + const [bearbeitungsName, setBearbeitungsName] = useState(""); + + function bearbeiten(o: Objekttyp) { + setBearbeiteteId(o.id); + setBearbeitungsName(o.name); + } + + async function speichern(id: number) { + try { + await apiRequest(`/objekttypen/${id}`, { method: "PATCH", body: { name: bearbeitungsName } }); + setBearbeiteteId(null); + await onGeaendert(); + } catch { + onFehler("Objekttyp konnte nicht geändert werden."); + } + } async function anlegen() { if (bereichId === "") { @@ -74,8 +91,29 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender {objekttypen.length > 0 && (
    {objekttypen.map((o) => ( -
  • - {o.name} +
  • + {bearbeiteteId === o.id ? ( +
    + setBearbeitungsName(e.target.value)} + /> + + +
    + ) : ( + <> + {o.name} + + + )}
  • ))}
diff --git a/frontend/src/pages/admin/StandortSection.tsx b/frontend/src/pages/admin/StandortSection.tsx index 422571d..2f91de7 100644 --- a/frontend/src/pages/admin/StandortSection.tsx +++ b/frontend/src/pages/admin/StandortSection.tsx @@ -12,6 +12,8 @@ interface Props { export function StandortSection({ standorte, onGeaendert, onFehler }: Props) { const [name, setName] = useState(""); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [bearbeiteteId, setBearbeiteteId] = useState(null); + const [bearbeitungsName, setBearbeitungsName] = useState(""); async function anlegen() { setWirdAngelegt(true); @@ -26,6 +28,21 @@ export function StandortSection({ standorte, onGeaendert, onFehler }: Props) { } } + function bearbeiten(s: Standort) { + setBearbeiteteId(s.id); + setBearbeitungsName(s.name); + } + + async function speichern(id: number) { + try { + await apiRequest(`/standorte/${id}`, { method: "PATCH", body: { name: bearbeitungsName } }); + setBearbeiteteId(null); + await onGeaendert(); + } catch { + onFehler("Standort konnte nicht geändert werden."); + } + } + return (

Standort anlegen

@@ -45,7 +62,28 @@ export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
    {standorte.map((s) => (
  • - {s.name} + {bearbeiteteId === s.id ? ( +
    + setBearbeitungsName(e.target.value)} + /> + + +
    + ) : ( + <> + {s.name} + + + )}
  • ))}
diff --git a/frontend/src/pages/admin/VorlageSection.tsx b/frontend/src/pages/admin/VorlageSection.tsx index bf2c06f..f5c2fee 100644 --- a/frontend/src/pages/admin/VorlageSection.tsx +++ b/frontend/src/pages/admin/VorlageSection.tsx @@ -24,6 +24,40 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert const [objekttypId, setObjekttypId] = useState(""); const [zeilen, setZeilen] = useState([LEERE_ZEILE]); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [ausgeklapptId, setAusgeklapptId] = useState(null); + const [neueVersionZeilen, setNeueVersionZeilen] = useState([]); + const [wirdVersioniert, setWirdVersioniert] = useState(false); + + function versionBearbeiten(v: Beladungsvorlage) { + setAusgeklapptId(ausgeklapptId === v.id ? null : v.id); + setNeueVersionZeilen( + v.positionen.map((p) => ({ + materialId: p.material_id, + fach: p.fach ?? "", + sollmenge: p.sollmenge, + })) + ); + } + + function neueVersionZeileAendern(index: number, zeile: Partial) { + setNeueVersionZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z))); + } + + async function neueVersionSpeichern(vorlageId: number) { + const positionen = neueVersionZeilen + .filter((z) => z.materialId !== "") + .map((z) => ({ material_id: z.materialId, fach: z.fach || null, sollmenge: z.sollmenge })); + setWirdVersioniert(true); + try { + await apiRequest(`/vorlagen/${vorlageId}/neue-version`, { method: "POST", body: { positionen } }); + setAusgeklapptId(null); + await onGeaendert(); + } catch { + onFehler("Neue Version konnte nicht angelegt werden."); + } finally { + setWirdVersioniert(false); + } + } function zeileAendern(index: number, zeile: Partial) { setZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z))); @@ -124,9 +158,68 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert {vorlagen.length > 0 && (
    {vorlagen.map((v) => ( -
  • - {v.name} - v{v.version} +
  • +
    + + {v.name} v{v.version} + + +
    + {ausgeklapptId === v.id && ( +
    +

    + Änderungen erzeugen eine neue Version – die aktuelle v{v.version} bleibt unverändert + erhalten. +

    + {neueVersionZeilen.map((zeile, i) => ( +
    + + neueVersionZeileAendern(i, { fach: e.target.value })} + placeholder="Fach" + /> + neueVersionZeileAendern(i, { sollmenge: e.target.value })} + /> +
    + ))} + + +
    + )}
  • ))}