Admin-Portal: Bearbeiten für Bereich/Kategorie/Objekttyp/Standort/Benutzer,
Vorlagen-Versionierung und Objekt-Duplizieren im Frontend nachgerüstet 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -2051,3 +2051,215 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/pages/kontrolle/PositionCard.tsx | 14 +++-
|
- 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 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -9,15 +9,19 @@ from app.models.stammdaten import Bereich, Kategorie, Material, Objekttyp, Stand
|
|||||||
from app.schemas.stammdaten import (
|
from app.schemas.stammdaten import (
|
||||||
BereichCreate,
|
BereichCreate,
|
||||||
BereichRead,
|
BereichRead,
|
||||||
|
BereichUpdate,
|
||||||
KategorieCreate,
|
KategorieCreate,
|
||||||
KategorieRead,
|
KategorieRead,
|
||||||
|
KategorieUpdate,
|
||||||
MaterialCreate,
|
MaterialCreate,
|
||||||
MaterialRead,
|
MaterialRead,
|
||||||
MaterialUpdate,
|
MaterialUpdate,
|
||||||
ObjekttypCreate,
|
ObjekttypCreate,
|
||||||
ObjekttypRead,
|
ObjekttypRead,
|
||||||
|
ObjekttypUpdate,
|
||||||
StandortCreate,
|
StandortCreate,
|
||||||
StandortRead,
|
StandortRead,
|
||||||
|
StandortUpdate,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -45,6 +49,22 @@ async def erstelle_bereich(
|
|||||||
return 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 ------------------------------------------------------------------
|
# -- Kategorie ------------------------------------------------------------------
|
||||||
|
|
||||||
@router.get("/kategorien", response_model=list[KategorieRead])
|
@router.get("/kategorien", response_model=list[KategorieRead])
|
||||||
@@ -67,6 +87,22 @@ async def erstelle_kategorie(
|
|||||||
return 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 ------------------------------------------------------------------
|
# -- Standort ------------------------------------------------------------------
|
||||||
|
|
||||||
@router.get("/standorte", response_model=list[StandortRead])
|
@router.get("/standorte", response_model=list[StandortRead])
|
||||||
@@ -89,6 +125,22 @@ async def erstelle_standort(
|
|||||||
return 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 ------------------------------------------------------------------
|
# -- Objekttyp ------------------------------------------------------------------
|
||||||
|
|
||||||
@router.get("/objekttypen", response_model=list[ObjekttypRead])
|
@router.get("/objekttypen", response_model=list[ObjekttypRead])
|
||||||
@@ -111,6 +163,22 @@ async def erstelle_objekttyp(
|
|||||||
return 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) --------------------------------------------------
|
# -- Material (Prompt 07) --------------------------------------------------
|
||||||
|
|
||||||
@router.get("/materialien", response_model=list[MaterialRead])
|
@router.get("/materialien", response_model=list[MaterialRead])
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ class BereichRead(BereichCreate):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class BereichUpdate(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
beschreibung: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class KategorieCreate(BaseModel):
|
class KategorieCreate(BaseModel):
|
||||||
bereich_id: int
|
bereich_id: int
|
||||||
name: str
|
name: str
|
||||||
@@ -24,6 +29,12 @@ class KategorieRead(KategorieCreate):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class KategorieUpdate(BaseModel):
|
||||||
|
bereich_id: int | None = None
|
||||||
|
name: str | None = None
|
||||||
|
ueberkategorie_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class StandortCreate(BaseModel):
|
class StandortCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
adresse: str | None = None
|
adresse: str | None = None
|
||||||
@@ -34,6 +45,11 @@ class StandortRead(StandortCreate):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class StandortUpdate(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
adresse: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ObjekttypCreate(BaseModel):
|
class ObjekttypCreate(BaseModel):
|
||||||
bereich_id: int
|
bereich_id: int
|
||||||
kategorie_id: int | None = None
|
kategorie_id: int | None = None
|
||||||
@@ -45,6 +61,12 @@ class ObjekttypRead(ObjekttypCreate):
|
|||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class ObjekttypUpdate(BaseModel):
|
||||||
|
bereich_id: int | None = None
|
||||||
|
kategorie_id: int | None = None
|
||||||
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class MaterialCreate(BaseModel):
|
class MaterialCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
artikelnummer: str | None = None
|
artikelnummer: str | None = None
|
||||||
|
|||||||
@@ -60,3 +60,65 @@ async def test_material_not_found(client, admin_user):
|
|||||||
token = await login(client, "admin1")
|
token = await login(client, "admin1")
|
||||||
response = await client.get("/api/v1/materialien/999999", headers=auth_header(token))
|
response = await client.get("/api/v1/materialien/999999", headers=auth_header(token))
|
||||||
assert response.status_code == 404
|
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"
|
||||||
|
|||||||
@@ -61,12 +61,21 @@ export interface Standort {
|
|||||||
adresse: string | null;
|
adresse: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Vorlagenposition {
|
||||||
|
id: string;
|
||||||
|
vorlage_id: number;
|
||||||
|
material_id: number;
|
||||||
|
fach: string | null;
|
||||||
|
sollmenge: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Beladungsvorlage {
|
export interface Beladungsvorlage {
|
||||||
id: number;
|
id: number;
|
||||||
objekttyp_id: number;
|
objekttyp_id: number;
|
||||||
name: string;
|
name: string;
|
||||||
version: number;
|
version: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
positionen: Vorlagenposition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Bereich {
|
export interface Bereich {
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) {
|
|||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [rollen, setRollen] = useState<Rolle[]>(["mitarbeiter"]);
|
const [rollen, setRollen] = useState<Rolle[]>(["mitarbeiter"]);
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||||
|
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||||
|
const [bearbeitungsEmail, setBearbeitungsEmail] = useState("");
|
||||||
|
const [bearbeitungsRollen, setBearbeitungsRollen] = useState<Rolle[]>([]);
|
||||||
|
|
||||||
function rolleUmschalten(rolle: Rolle) {
|
function rolleUmschalten(rolle: Rolle) {
|
||||||
setRollen((bisherige) =>
|
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 (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h2>Benutzer anlegen</h2>
|
<h2>Benutzer anlegen</h2>
|
||||||
@@ -99,14 +129,60 @@ export function BenutzerSection({ benutzer, onGeaendert, onFehler }: Props) {
|
|||||||
{benutzer.length > 0 && (
|
{benutzer.length > 0 && (
|
||||||
<ul className="card-list" style={{ marginTop: "1rem" }}>
|
<ul className="card-list" style={{ marginTop: "1rem" }}>
|
||||||
{benutzer.map((b) => (
|
{benutzer.map((b) => (
|
||||||
<li key={b.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
<li key={b.id} style={{ padding: "0.4rem 0" }}>
|
||||||
|
{bearbeiteteId === b.id ? (
|
||||||
|
<div className="stack">
|
||||||
|
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem" }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bearbeitungsName}
|
||||||
|
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bearbeitungsEmail}
|
||||||
|
onChange={(e) => setBearbeitungsEmail(e.target.value)}
|
||||||
|
placeholder="E-Mail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem" }}>
|
||||||
|
{ALLE_ROLLEN.map((r) => (
|
||||||
|
<label key={r} className="row" style={{ gap: "0.3rem" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={bearbeitungsRollen.includes(r)}
|
||||||
|
onChange={() => bearbeitungsRolleUmschalten(r)}
|
||||||
|
/>
|
||||||
|
{r}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<button className="btn btn-primary" onClick={() => speichern(b.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="row-between">
|
||||||
<span>
|
<span>
|
||||||
<strong>{b.name}</strong> ({b.login}) – {b.rollen.join(", ")}
|
<strong>{b.name}</strong> ({b.login}) – {b.rollen.join(", ")}
|
||||||
{!b.aktiv && " – deaktiviert"}
|
{!b.aktiv && " – deaktiviert"}
|
||||||
</span>
|
</span>
|
||||||
|
<div className="row">
|
||||||
|
<button className="btn btn-secondary" onClick={() => bearbeiten(b)}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
<button className="btn btn-secondary" onClick={() => aktivSchalten(b)}>
|
<button className="btn btn-secondary" onClick={() => aktivSchalten(b)}>
|
||||||
{b.aktiv ? "Deaktivieren" : "Aktivieren"}
|
{b.aktiv ? "Deaktivieren" : "Aktivieren"}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface Props {
|
|||||||
export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||||
|
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||||
|
|
||||||
async function anlegen() {
|
async function anlegen() {
|
||||||
setWirdAngelegt(true);
|
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 (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h2>Bereich anlegen</h2>
|
<h2>Bereich anlegen</h2>
|
||||||
@@ -39,8 +56,29 @@ export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
|||||||
{bereiche.length > 0 && (
|
{bereiche.length > 0 && (
|
||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{bereiche.map((b) => (
|
{bereiche.map((b) => (
|
||||||
<li key={b.id} style={{ padding: "0.4rem 0" }}>
|
<li key={b.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
{b.name}
|
{bearbeiteteId === b.id ? (
|
||||||
|
<div className="row" style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bearbeitungsName}
|
||||||
|
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" onClick={() => speichern(b.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{b.name}</span>
|
||||||
|
<button className="btn btn-secondary" onClick={() => bearbeiten(b)}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [bereichId, setBereichId] = useState<number | "">("");
|
const [bereichId, setBereichId] = useState<number | "">("");
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||||
|
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||||
|
|
||||||
async function anlegen() {
|
async function anlegen() {
|
||||||
if (bereichId === "") {
|
if (bereichId === "") {
|
||||||
@@ -36,6 +38,21 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
|
|||||||
return bereiche.find((b) => b.id === id)?.name ?? `#${id}`;
|
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 (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h2>Kategorie anlegen</h2>
|
<h2>Kategorie anlegen</h2>
|
||||||
@@ -64,8 +81,30 @@ export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }
|
|||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{kategorien.map((k) => (
|
{kategorien.map((k) => (
|
||||||
<li key={k.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
<li key={k.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
<span>{k.name}</span>
|
{bearbeiteteId === k.id ? (
|
||||||
<span className="text-muted">{bereichName(k.bereich_id)}</span>
|
<div className="row" style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bearbeitungsName}
|
||||||
|
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" onClick={() => speichern(k.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{k.name} <span className="text-muted">({bereichName(k.bereich_id)})</span>
|
||||||
|
</span>
|
||||||
|
<button className="btn btn-secondary" onClick={() => bearbeiten(k)}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
const [standortId, setStandortId] = useState<number | "">("");
|
const [standortId, setStandortId] = useState<number | "">("");
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
|
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
|
||||||
|
const [duplizierId, setDuplizierId] = useState<number | null>(null);
|
||||||
|
const [neuerCode, setNeuerCode] = useState("");
|
||||||
|
const [neuerName, setNeuerName] = useState("");
|
||||||
|
const [neuerStandortId, setNeuerStandortId] = useState<number | "">("");
|
||||||
|
|
||||||
async function anlegen() {
|
async function anlegen() {
|
||||||
if (vorlageId === "" || standortId === "") {
|
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 (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h2>Objekt aus Vorlage anlegen</h2>
|
<h2>Objekt aus Vorlage anlegen</h2>
|
||||||
@@ -107,18 +135,57 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
<span>
|
<span>
|
||||||
{o.name} <span className="text-muted">({o.code})</span>
|
{o.name} <span className="text-muted">({o.code})</span>
|
||||||
</span>
|
</span>
|
||||||
|
<div className="row">
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => setAusgeklapptId(ausgeklapptId === o.id ? null : o.id)}
|
onClick={() => setAusgeklapptId(ausgeklapptId === o.id ? null : o.id)}
|
||||||
>
|
>
|
||||||
{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={() => duplizierenStarten(o)}>
|
||||||
|
{duplizierId === o.id ? "Schließen" : "Duplizieren"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ausgeklapptId === o.id && (
|
{ausgeklapptId === o.id && (
|
||||||
<div style={{ marginTop: "0.5rem" }}>
|
<div style={{ marginTop: "0.5rem" }}>
|
||||||
<ObjektPositionenPanel objektId={o.id} materialien={materialien} onFehler={onFehler} />
|
<ObjektPositionenPanel objektId={o.id} materialien={materialien} onFehler={onFehler} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{duplizierId === o.id && (
|
||||||
|
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={neuerCode}
|
||||||
|
onChange={(e) => setNeuerCode(e.target.value)}
|
||||||
|
placeholder="Neuer Code (eindeutig)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={neuerName}
|
||||||
|
onChange={(e) => setNeuerName(e.target.value)}
|
||||||
|
placeholder="Neuer Name"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={neuerStandortId}
|
||||||
|
onChange={(e) => setNeuerStandortId(Number(e.target.value) || "")}
|
||||||
|
>
|
||||||
|
{standorte.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => duplizieren(o.id)}
|
||||||
|
disabled={!neuerCode || !neuerName}
|
||||||
|
>
|
||||||
|
Duplizieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -16,6 +16,23 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
|
|||||||
const [bereichId, setBereichId] = useState<number | "">("");
|
const [bereichId, setBereichId] = useState<number | "">("");
|
||||||
const [kategorieId, setKategorieId] = useState<number | "">("");
|
const [kategorieId, setKategorieId] = useState<number | "">("");
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(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() {
|
async function anlegen() {
|
||||||
if (bereichId === "") {
|
if (bereichId === "") {
|
||||||
@@ -74,8 +91,29 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
|
|||||||
{objekttypen.length > 0 && (
|
{objekttypen.length > 0 && (
|
||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{objekttypen.map((o) => (
|
{objekttypen.map((o) => (
|
||||||
<li key={o.id} style={{ padding: "0.4rem 0" }}>
|
<li key={o.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
{o.name}
|
{bearbeiteteId === o.id ? (
|
||||||
|
<div className="row" style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bearbeitungsName}
|
||||||
|
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" onClick={() => speichern(o.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{o.name}</span>
|
||||||
|
<button className="btn btn-secondary" onClick={() => bearbeiten(o)}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface Props {
|
|||||||
export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
|
export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||||
|
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||||
|
|
||||||
async function anlegen() {
|
async function anlegen() {
|
||||||
setWirdAngelegt(true);
|
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 (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<h2>Standort anlegen</h2>
|
<h2>Standort anlegen</h2>
|
||||||
@@ -45,7 +62,28 @@ export function StandortSection({ standorte, onGeaendert, onFehler }: Props) {
|
|||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{standorte.map((s) => (
|
{standorte.map((s) => (
|
||||||
<li key={s.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
<li key={s.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
|
{bearbeiteteId === s.id ? (
|
||||||
|
<div className="row" style={{ flex: 1 }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bearbeitungsName}
|
||||||
|
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" onClick={() => speichern(s.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<span>{s.name}</span>
|
<span>{s.name}</span>
|
||||||
|
<button className="btn btn-secondary" onClick={() => bearbeiten(s)}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -24,6 +24,40 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert
|
|||||||
const [objekttypId, setObjekttypId] = useState<number | "">("");
|
const [objekttypId, setObjekttypId] = useState<number | "">("");
|
||||||
const [zeilen, setZeilen] = useState<PositionZeile[]>([LEERE_ZEILE]);
|
const [zeilen, setZeilen] = useState<PositionZeile[]>([LEERE_ZEILE]);
|
||||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
|
||||||
|
const [neueVersionZeilen, setNeueVersionZeilen] = useState<PositionZeile[]>([]);
|
||||||
|
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<PositionZeile>) {
|
||||||
|
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<PositionZeile>) {
|
function zeileAendern(index: number, zeile: Partial<PositionZeile>) {
|
||||||
setZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z)));
|
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.length > 0 && (
|
||||||
<ul className="card-list" style={{ marginTop: "1.25rem" }}>
|
<ul className="card-list" style={{ marginTop: "1.25rem" }}>
|
||||||
{vorlagen.map((v) => (
|
{vorlagen.map((v) => (
|
||||||
<li key={v.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
<li key={v.id} style={{ padding: "0.4rem 0" }}>
|
||||||
<span>{v.name}</span>
|
<div className="row-between">
|
||||||
<span className="badge badge-neutral">v{v.version}</span>
|
<span>
|
||||||
|
{v.name} <span className="badge badge-neutral">v{v.version}</span>
|
||||||
|
</span>
|
||||||
|
<button className="btn btn-secondary" onClick={() => versionBearbeiten(v)}>
|
||||||
|
{ausgeklapptId === v.id ? "Schließen" : "Positionen / neue Version"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{ausgeklapptId === v.id && (
|
||||||
|
<div className="stack" style={{ marginTop: "0.6rem" }}>
|
||||||
|
<p className="text-muted" style={{ marginBottom: 0 }}>
|
||||||
|
Änderungen erzeugen eine neue Version – die aktuelle v{v.version} bleibt unverändert
|
||||||
|
erhalten.
|
||||||
|
</p>
|
||||||
|
{neueVersionZeilen.map((zeile, i) => (
|
||||||
|
<div className="row" key={i}>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={zeile.materialId}
|
||||||
|
onChange={(e) =>
|
||||||
|
neueVersionZeileAendern(i, { materialId: Number(e.target.value) || "" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Material…</option>
|
||||||
|
{materialien.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
style={{ width: "8rem" }}
|
||||||
|
value={zeile.fach}
|
||||||
|
onChange={(e) => neueVersionZeileAendern(i, { fach: e.target.value })}
|
||||||
|
placeholder="Fach"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
style={{ width: "5rem" }}
|
||||||
|
type="number"
|
||||||
|
value={zeile.sollmenge}
|
||||||
|
onChange={(e) => neueVersionZeileAendern(i, { sollmenge: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setNeueVersionZeilen((z) => [...z, LEERE_ZEILE])}
|
||||||
|
>
|
||||||
|
+ Position
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => neueVersionSpeichern(v.id)}
|
||||||
|
disabled={wirdVersioniert}
|
||||||
|
>
|
||||||
|
Neue Version speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user