From a2877f56729dcbff159a997e4f54ac1632711112 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 10:24:15 +0200 Subject: [PATCH] =?UTF-8?q?Feste=20F=C3=A4cher-Liste=20je=20Objekttyp=20(V?= =?UTF-8?q?orlagen-Positionen:=20Dropdown=20+=20Sonstiges)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nutzer-Vorgabe: Rucksack-Fächer sollen aus einer festen, je Objekttyp gepflegten Liste gewählt werden können statt jedes Mal Freitext einzutippen. Neue Tabelle fach (Migration 0004), CRUD-Endpoints /faecher, Admin-Tab Struktur bekommt Fach-Verwaltung, Vorlagen-Editor nutzt Dropdown mit "Sonstiges"-Freitext-Fallback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt --- DEVLOG.md | 132 ++++++++++++++++++++ backend/alembic/versions/0004_add_fach.py | 36 ++++++ backend/app/api/v1/endpoints/stammdaten.py | 56 ++++++++- backend/app/models/stammdaten.py | 13 ++ backend/app/schemas/stammdaten.py | 16 +++ backend/tests/test_stammdaten.py | 56 +++++++++ frontend/src/api/types.ts | 7 ++ frontend/src/pages/AdminPage.tsx | 15 ++- frontend/src/pages/admin/FachAuswahl.tsx | 54 ++++++++ frontend/src/pages/admin/FachSection.tsx | 99 +++++++++++++++ frontend/src/pages/admin/VorlageSection.tsx | 26 ++-- 11 files changed, 493 insertions(+), 17 deletions(-) create mode 100644 backend/alembic/versions/0004_add_fach.py create mode 100644 frontend/src/pages/admin/FachAuswahl.tsx create mode 100644 frontend/src/pages/admin/FachSection.tsx diff --git a/DEVLOG.md b/DEVLOG.md index 01c6c0a..1095d7d 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2263,3 +2263,135 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/BenutzerSection.tsx | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --- +## 2026-09-04 09:51 – 09:56 (4m) +**Beschreibung:** Claude Code Session +**Projekt:** backend + +### Commits +- 2f11011 Admin-Portal: Bearbeiten für Bereich/Kategorie/Objekttyp/Standort/Benutzer, Vorlagen-Versionierung und Objekt-Duplizieren im Frontend nachgerüstet + +### Geänderte Dateien +- 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 ++++++++++++++++++++++++++++++++-- + +--- +## 2026-09-04 09:57 – 09:58 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- 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 ++++++++++++++++++++++++++++++++-- + +--- +## 2026-09-04 10:14 – 10:14 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- 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 ++++++++++++++++++++++++++++++++-- + +--- +## 2026-09-04 10:15 – 10:15 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- 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 ++++++++++++++++++++++++++++++++-- + +--- +## 2026-09-04 10:15 – 10:15 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- 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 ++++++++++++++++++++++++++++++++-- + +--- +## 2026-09-04 10:17 – 10:17 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- 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 ++++++++++++++++++++++++++++++++-- + +--- diff --git a/backend/alembic/versions/0004_add_fach.py b/backend/alembic/versions/0004_add_fach.py new file mode 100644 index 0000000..99d0c33 --- /dev/null +++ b/backend/alembic/versions/0004_add_fach.py @@ -0,0 +1,36 @@ +"""fach-Tabelle: feste Fächer-Liste je Objekttyp + +Revision ID: 0004_add_fach +Revises: 0003_add_benutzer_email +Create Date: 2026-09-04 + +Nutzer-Vorgabe: feste Fächer-Liste pro Objekttyp statt Freitext bei jeder +Vorlagenposition. Additive Schema-Erweiterung, bestehende Vorlagenpositionen +(freies Fach-Textfeld) bleiben unverändert nutzbar (Prompt-Vorgabe "optional +sonstiges für freies Textfeld"). +""" +from typing import Sequence, Union + +from alembic import op + +revision: str = "0004_add_fach" +down_revision: Union[str, None] = "0003_add_benutzer_email" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE fach ( + id SERIAL PRIMARY KEY, + objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id), + name TEXT NOT NULL, + sortierung INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + + +def downgrade() -> None: + op.execute("DROP TABLE fach;") diff --git a/backend/app/api/v1/endpoints/stammdaten.py b/backend/app/api/v1/endpoints/stammdaten.py index b0b5279..cc32794 100644 --- a/backend/app/api/v1/endpoints/stammdaten.py +++ b/backend/app/api/v1/endpoints/stammdaten.py @@ -5,11 +5,14 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, require_roles from app.db.session import get_db from app.models.auth import RolleTyp -from app.models.stammdaten import Bereich, Kategorie, Material, Objekttyp, Standort +from app.models.stammdaten import Bereich, Fach, Kategorie, Material, Objekttyp, Standort from app.schemas.stammdaten import ( BereichCreate, BereichRead, BereichUpdate, + FachCreate, + FachRead, + FachUpdate, KategorieCreate, KategorieRead, KategorieUpdate, @@ -179,6 +182,57 @@ async def aendere_objekttyp( return objekttyp +# -- Fach (feste Fächer-Liste je Objekttyp) ---------------------------------- + +@router.get("/faecher", response_model=list[FachRead]) +async def liste_faecher( + db: AsyncSession = Depends(get_db), _=Depends(get_current_user) +) -> list[Fach]: + result = await db.execute(select(Fach).order_by(Fach.objekttyp_id, Fach.sortierung, Fach.name)) + return list(result.scalars().all()) + + +@router.post("/faecher", response_model=FachRead, status_code=status.HTTP_201_CREATED) +async def erstelle_fach( + payload: FachCreate, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> Fach: + fach = Fach(**payload.model_dump()) + db.add(fach) + await db.flush() + return fach + + +@router.patch("/faecher/{fach_id}", response_model=FachRead) +async def aendere_fach( + fach_id: int, + payload: FachUpdate, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> Fach: + fach = await db.get(Fach, fach_id) + if fach is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fach nicht gefunden") + for feld, wert in payload.model_dump(exclude_unset=True).items(): + setattr(fach, feld, wert) + await db.flush() + return fach + + +@router.delete("/faecher/{fach_id}", status_code=status.HTTP_204_NO_CONTENT) +async def loesche_fach( + fach_id: int, + db: AsyncSession = Depends(get_db), + _=Depends(require_roles(RolleTyp.administration)), +) -> None: + fach = await db.get(Fach, fach_id) + if fach is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fach nicht gefunden") + await db.delete(fach) + await db.flush() + + # -- Material (Prompt 07) -------------------------------------------------- @router.get("/materialien", response_model=list[MaterialRead]) diff --git a/backend/app/models/stammdaten.py b/backend/app/models/stammdaten.py index 6a6e666..415c1d1 100644 --- a/backend/app/models/stammdaten.py +++ b/backend/app/models/stammdaten.py @@ -50,6 +50,19 @@ class Objekttyp(Base): name: Mapped[str] = mapped_column(String, nullable=False) +class Fach(Base): + """Nutzer-Vorgabe: feste Fächer-Liste je Objekttyp, damit beim Anlegen einer + Vorlagenposition nicht jedes Mal Freitext getippt werden muss. `sortierung` + steuert die Anzeigereihenfolge (z. B. von oben nach unten im Rucksack).""" + + __tablename__ = "fach" + + id: Mapped[int] = mapped_column(primary_key=True) + objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + sortierung: Mapped[int] = mapped_column(default=0) + + class Material(Base): __tablename__ = "material" diff --git a/backend/app/schemas/stammdaten.py b/backend/app/schemas/stammdaten.py index 9bfd26a..8a12762 100644 --- a/backend/app/schemas/stammdaten.py +++ b/backend/app/schemas/stammdaten.py @@ -67,6 +67,22 @@ class ObjekttypUpdate(BaseModel): name: str | None = None +class FachCreate(BaseModel): + objekttyp_id: int + name: str + sortierung: int = 0 + + +class FachRead(FachCreate): + model_config = ConfigDict(from_attributes=True) + id: int + + +class FachUpdate(BaseModel): + name: str | None = None + sortierung: int | 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 1d26c84..9e7a951 100644 --- a/backend/tests/test_stammdaten.py +++ b/backend/tests/test_stammdaten.py @@ -122,3 +122,59 @@ async def test_admin_can_update_objekttyp(client, admin_user): ) assert response.status_code == 200 assert response.json()["name"] == "Neu" + + +@pytest.mark.asyncio +async def test_fach_lifecycle(client, admin_user): + """Nutzer-Vorgabe: feste Fächer-Liste je Objekttyp.""" + token = await login(client, "admin1") + bereich = await client.post("/api/v1/bereiche", json={"name": "B"}, headers=auth_header(token)) + objekttyp = await client.post( + "/api/v1/objekttypen", + json={"bereich_id": bereich.json()["id"], "name": "Rucksack"}, + headers=auth_header(token), + ) + objekttyp_id = objekttyp.json()["id"] + + erstellt = await client.post( + "/api/v1/faecher", + json={"objekttyp_id": objekttyp_id, "name": "Fach 1", "sortierung": 1}, + headers=auth_header(token), + ) + assert erstellt.status_code == 201 + fach_id = erstellt.json()["id"] + + liste = await client.get("/api/v1/faecher", headers=auth_header(token)) + assert any(f["id"] == fach_id for f in liste.json()) + + patch_response = await client.patch( + f"/api/v1/faecher/{fach_id}", json={"name": "Fach 1 links"}, headers=auth_header(token) + ) + assert patch_response.status_code == 200 + assert patch_response.json()["name"] == "Fach 1 links" + + delete_response = await client.delete(f"/api/v1/faecher/{fach_id}", headers=auth_header(token)) + assert delete_response.status_code == 204 + + liste_nach_delete = await client.get("/api/v1/faecher", headers=auth_header(token)) + assert not any(f["id"] == fach_id for f in liste_nach_delete.json()) + + +@pytest.mark.asyncio +async def test_mitarbeiter_cannot_create_fach(client, mitarbeiter_user, admin_user, db_session): + from app.models.stammdaten import Bereich, Objekttyp + + bereich = Bereich(name="B2") + db_session.add(bereich) + await db_session.flush() + objekttyp = Objekttyp(bereich_id=bereich.id, name="Rucksack2") + db_session.add(objekttyp) + await db_session.flush() + + token = await login(client, "mitarbeiter1") + response = await client.post( + "/api/v1/faecher", + json={"objekttyp_id": objekttyp.id, "name": "Fach X"}, + headers=auth_header(token), + ) + assert response.status_code == 403 diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 425c168..affe8a5 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -98,6 +98,13 @@ export interface Objekttyp { name: string; } +export interface Fach { + id: number; + objekttyp_id: number; + name: string; + sortierung: number; +} + export interface Me { id: number; name: string; diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index dec2e7f..afe90ac 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -1,9 +1,10 @@ import { useEffect, useState } from "react"; import { apiRequest } from "../api/client"; -import type { Benutzer, Bereich, Beladungsvorlage, 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 { BereichSection } from "./admin/BereichSection"; +import { FachSection } from "./admin/FachSection"; import { KategorieSection } from "./admin/KategorieSection"; import { MaterialSection } from "./admin/MaterialSection"; import { ObjektSection } from "./admin/ObjektSection"; @@ -40,12 +41,13 @@ export function AdminPage() { const [objekte, setObjekte] = useState([]); const [materialien, setMaterialien] = useState([]); const [benutzer, setBenutzer] = useState([]); + const [faecher, setFaecher] = useState([]); const [fehler, setFehler] = useState(null); const [laedt, setLaedt] = useState(true); const [tab, setTab] = useState("objekte"); async function ladeAlles() { - const [be, ka, ot, s, v, o, m, b] = await Promise.all([ + const [be, ka, ot, s, v, o, m, b, f] = await Promise.all([ apiRequest("/bereiche"), apiRequest("/kategorien"), apiRequest("/objekttypen"), @@ -54,6 +56,7 @@ export function AdminPage() { apiRequest("/objekte"), apiRequest("/materialien"), apiRequest("/benutzer"), + apiRequest("/faecher"), ]); setBereiche(be); setKategorien(ka); @@ -63,6 +66,7 @@ export function AdminPage() { setObjekte(o); setMaterialien(m); setBenutzer(b); + setFaecher(f); } useEffect(() => { @@ -111,6 +115,7 @@ export function AdminPage() { vorlagen={vorlagen} objekttypen={objekttypen} materialien={materialien} + faecher={faecher} onGeaendert={ladeAlles} onFehler={setFehler} /> @@ -134,6 +139,12 @@ export function AdminPage() { onGeaendert={ladeAlles} onFehler={setFehler} /> + )} {tab === "standorte" && ( diff --git a/frontend/src/pages/admin/FachAuswahl.tsx b/frontend/src/pages/admin/FachAuswahl.tsx new file mode 100644 index 0000000..2f7022c --- /dev/null +++ b/frontend/src/pages/admin/FachAuswahl.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; + +import type { Fach } from "../../api/types"; + +const SONSTIGES = "__sonstiges__"; + +interface Props { + faecher: Fach[]; + wert: string; + onAendern: (wert: string) => void; +} + +/** Dropdown mit fester Fächer-Liste (je Objekttyp) + "Sonstiges" für Freitext, + * damit Vorlagen-Positionen nicht immer per Hand getippt werden müssen. */ +export function FachAuswahl({ faecher, wert, onAendern }: Props) { + const istBekanntesFach = faecher.some((f) => f.name === wert); + const [sonstigesAktiv, setSonstigesAktiv] = useState(wert !== "" && !istBekanntesFach); + + if (sonstigesAktiv) { + return ( + onAendern(e.target.value)} + placeholder="Fach (frei)" + /> + ); + } + + return ( + + ); +} diff --git a/frontend/src/pages/admin/FachSection.tsx b/frontend/src/pages/admin/FachSection.tsx new file mode 100644 index 0000000..c71227e --- /dev/null +++ b/frontend/src/pages/admin/FachSection.tsx @@ -0,0 +1,99 @@ +import { useState } from "react"; + +import { apiRequest } from "../../api/client"; +import type { Fach, Objekttyp } from "../../api/types"; + +interface Props { + objekttypen: Objekttyp[]; + faecher: Fach[]; + onGeaendert: () => Promise; + onFehler: (text: string) => void; +} + +/** Nutzer-Vorgabe: feste Fächer-Liste je Objekttyp (z. B. Rucksack-Fächer), + * damit bei Vorlagen-Positionen aus einer Liste gewählt werden kann statt + * jedes Mal Freitext einzutippen (Sonstiges bleibt dort als Freitext erhalten). */ +export function FachSection({ objekttypen, faecher, onGeaendert, onFehler }: Props) { + const [objekttypId, setObjekttypId] = useState(""); + const [name, setName] = useState(""); + const [wirdAngelegt, setWirdAngelegt] = useState(false); + + async function anlegen() { + if (objekttypId === "") { + onFehler("Objekttyp auswählen."); + return; + } + setWirdAngelegt(true); + try { + await apiRequest("/faecher", { method: "POST", body: { objekttyp_id: objekttypId, name } }); + setName(""); + await onGeaendert(); + } catch { + onFehler("Fach konnte nicht angelegt werden."); + } finally { + setWirdAngelegt(false); + } + } + + async function loeschen(id: number) { + try { + await apiRequest(`/faecher/${id}`, { method: "DELETE" }); + await onGeaendert(); + } catch { + onFehler("Fach konnte nicht gelöscht werden."); + } + } + + function objekttypName(id: number) { + return objekttypen.find((o) => o.id === id)?.name ?? `#${id}`; + } + + return ( +
+

Fächer je Objekttyp

+
+ {objekttypen.length === 0 ? ( +

Erst einen Objekttyp anlegen.

+ ) : ( +
+ + setName(e.target.value)} + placeholder="Fach-Name (z. B. Fach 1)" + /> + +
+ )} + {faecher.length > 0 && ( +
    + {faecher.map((f) => ( +
  • + + {f.name} ({objekttypName(f.objekttyp_id)}) + + +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/admin/VorlageSection.tsx b/frontend/src/pages/admin/VorlageSection.tsx index f5c2fee..bcd4dd6 100644 --- a/frontend/src/pages/admin/VorlageSection.tsx +++ b/frontend/src/pages/admin/VorlageSection.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import { apiRequest } from "../../api/client"; -import type { Beladungsvorlage, Material, Objekttyp } from "../../api/types"; +import type { Beladungsvorlage, Fach, Material, Objekttyp } from "../../api/types"; +import { FachAuswahl } from "./FachAuswahl"; interface PositionZeile { materialId: number | ""; @@ -15,11 +16,12 @@ interface Props { vorlagen: Beladungsvorlage[]; objekttypen: Objekttyp[]; materialien: Material[]; + faecher: Fach[]; onGeaendert: () => Promise; onFehler: (text: string) => void; } -export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert, onFehler }: Props) { +export function VorlageSection({ vorlagen, objekttypen, materialien, faecher, onGeaendert, onFehler }: Props) { const [name, setName] = useState(""); const [objekttypId, setObjekttypId] = useState(""); const [zeilen, setZeilen] = useState([LEERE_ZEILE]); @@ -129,12 +131,10 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert ))} - zeileAendern(i, { fach: e.target.value })} - placeholder="Fach" + f.objekttyp_id === objekttypId)} + wert={zeile.fach} + onAendern={(wert) => zeileAendern(i, { fach: wert })} /> ))} - neueVersionZeileAendern(i, { fach: e.target.value })} - placeholder="Fach" + f.objekttyp_id === v.objekttyp_id)} + wert={zeile.fach} + onAendern={(wert) => neueVersionZeileAendern(i, { fach: wert })} />