diff --git a/DEVLOG.md b/DEVLOG.md
index 714ae9d..cd648b9 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -15991,3 +15991,90 @@ Keine Commits in dieser Session.
- frontend/src/pages/admin/ObjekttypSection.tsx | 163 ++++++++++++++++++++++++++++++++++++++++--------------------------------
---
+## 2026-09-14 23:04 – 23:07 (3m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+- 6c05d45 feat(zustaendigkeit): ZUST-004 Runde 4 - Historie und Dashboard-Aggregate
+
+### Geänderte Dateien
+- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++
+- arbeitskacheln/17_zustaendigkeit.md | 24 +++++++++++++++++++-----
+- backend/app/api/v1/endpoints/dashboard.py | 21 +++++++++++++++------
+- backend/app/api/v1/endpoints/historie.py | 11 +++++++++--
+- backend/app/services/dashboard.py | 43 +++++++++++++++++++++++++------------------
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+---
+## 2026-09-14 23:11 – 23:11 (0m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+Keine Commits in dieser Session.
+
+### Geänderte Dateien
+- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++
+- arbeitskacheln/17_zustaendigkeit.md | 24 +++++++++++++++++++-----
+- backend/app/api/v1/endpoints/dashboard.py | 21 +++++++++++++++------
+- backend/app/api/v1/endpoints/historie.py | 11 +++++++++--
+- backend/app/services/dashboard.py | 43 +++++++++++++++++++++++++------------------
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+---
+## 2026-09-14 23:14 – 23:15 (1m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+- 6f116ed fix(tests): test_historie_standort_eingeschraenkt falsche Rolle
+
+### Geänderte Dateien
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 11 ++++++++---
+
+---
+## 2026-09-14 23:20 – 23:20 (0m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+Keine Commits in dieser Session.
+
+### Geänderte Dateien
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 11 ++++++++---
+
+---
+## 2026-09-14 23:29 – 23:29 (0m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+Keine Commits in dieser Session.
+
+### Geänderte Dateien
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 11 ++++++++---
+
+---
+## 2026-09-14 23:30 – 23:30 (0m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+Keine Commits in dieser Session.
+
+### Geänderte Dateien
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 11 ++++++++---
+
+---
+## 2026-09-14 23:30 – 23:31 (0m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+Keine Commits in dieser Session.
+
+### Geänderte Dateien
+- backend/tests/test_zust004_standort_sichtbarkeit.py | 11 ++++++++---
+
+---
diff --git a/arbeitskacheln/17_zustaendigkeit.md b/arbeitskacheln/17_zustaendigkeit.md
index 2867615..b44d543 100644
--- a/arbeitskacheln/17_zustaendigkeit.md
+++ b/arbeitskacheln/17_zustaendigkeit.md
@@ -191,13 +191,9 @@ war die Vorgängerlücke, diese hier ist die zweite gefundene Lücke).
Umgesetzt (Runde 3): `GET /wartungsauftraege`, `GET /verleihe`,
`GET /verleihe/{id}/uebergabeliste`, `PATCH /objekte/{id}/zaehlerstand`,
`GET /objekte/{id}/uebergabeliste` (Aussonderung).
- **Blockiert, braucht erst eine eigene Entscheidung:** Lagerbewegungen/
- Lagerplätze - `Lagerort` hat KEINE FK auf die echte `Standort`-Tabelle
- (eigene, unabhängige Hierarchie mit `standort` nur als Typ-Label, siehe
- Docstring `app/models/lager.py:Lagerort` - "Objekt/Zustaendigkeit bleiben
- unverändert an `standort` hängen, Lagerort ist ein zusätzliches, feineres
- Konzept"). ZUST-004 lässt sich hier nicht 1:1 anwenden, ohne vorher zu
- klären, wie ein `Lagerort`-Knoten einem echten `Standort` zugeordnet wird.
+ **War blockiert bis Runde 5 (siehe unten):** Lagerbewegungen/Lagerplätze -
+ `Lagerort` hatte KEINE FK auf die echte `Standort`-Tabelle (eigene,
+ unabhängige Hierarchie mit `standort` nur als Typ-Label).
Umgesetzt (Runde 4, 2026-09-14): `GET /historie` (nur wenn
`entitaet_typ`+`entitaet_id` gesetzt sind - ohne diese Filter ist es eine
globale Liste ohne Einzelobjekt-Bezug, bleibt unrestricted). **Nutzer-
@@ -212,15 +208,33 @@ war die Vorgängerlücke, diese hier ist die zweite gefundene Lücke).
`bevorstehende_ablaufdaten()`, `alle_faelligen_wartungen()` (dashboard-
weit), `bevorstehende_qualifikationsablaeufe()` (Personal-bezogen, kein
Objekt-/Standort-Konzept) - eigene Follow-up-Runde bei Bedarf, gleiche
- "kein Big-Bang"-Linie. Lagerbewegungen/Lagerplätze weiterhin blockiert
- (siehe oben). Tests:
+ "kein Big-Bang"-Linie.
+ **Runde 5 (2026-09-14): Lagerbewegungen/Lagerplätze-Blockade aufgehoben.**
+ `Lagerort` bekommt ein neues, nullable `standort_id`-Feld (Migration
+ `0060_lagerort_standort_id.py`), das NUR fachlich sinnvoll an einem
+ Knoten mit `typ=standort` gesetzt wird (Anwendungslogik, kein DB-
+ Constraint). `app/services/lager.py:standort_fuer_lagerort()`/
+ `standort_fuer_lagerplatz()` lösen für einen beliebigen Lagerort/
+ Lagerplatz den zuständigen Standort über die `parent_id`-Kette auf (erster
+ Vorfahre mit gesetzter `standort_id`, `None` = weiterhin unrestricted -
+ bestehende Lagerorte ohne Zuordnung ändern ihr Verhalten nicht). Umgesetzt:
+ `GET /lagerplaetze` (Filterung je Eintrag statt SQL-Join, da Kettenlänge
+ variabel), `GET /lagerplaetze/{id}/bestand`, `POST .../einlagern`,
+ `POST .../auslagern`, `POST .../umlagern` (BEIDE Lagerplätze geprüft -
+ Standort wechseln braucht Sichtbarkeit an Quelle UND Ziel). Admin-UI:
+ `LagerSection.tsx` zeigt eine Standort-Auswahl nur, wenn beim Anlegen
+ `typ=standort` gewählt ist. `Lagerbewegung` (Objekt-Ebene, WH-007) bleibt
+ unberührt - das ist ein anderes Modell als `Materialbewegung`
+ (Lagerplatz-Ebene), war nie Teil dieser Blockade.
+ Tests:
`test_mangel_liste_standort_eingeschraenkt`,
`test_kontroll_fortschritt_standort_eingeschraenkt`,
`test_wartungsauftraege_liste_standort_eingeschraenkt`,
`test_verleih_liste_und_uebergabeliste_standort_eingeschraenkt`,
`test_objekt_zaehlerstand_und_aussonderung_uebergabeliste_standort_eingeschraenkt`,
`test_historie_standort_eingeschraenkt`,
- `test_dashboard_einsatzbereitschaft_und_prueftermine_standort_eingeschraenkt`
+ `test_dashboard_einsatzbereitschaft_und_prueftermine_standort_eingeschraenkt`,
+ `test_lagerplaetze_und_bestand_standort_eingeschraenkt`
in `backend/tests/test_zust004_standort_sichtbarkeit.py`.
---
diff --git a/backend/alembic/versions/0060_lagerort_standort_id.py b/backend/alembic/versions/0060_lagerort_standort_id.py
new file mode 100644
index 0000000..e855149
--- /dev/null
+++ b/backend/alembic/versions/0060_lagerort_standort_id.py
@@ -0,0 +1,28 @@
+"""ZUST-004-Nachtrag: Lagerort mit echtem Standort verknüpfen. Nullable FK
+nur fachlich sinnvoll an typ=standort-Knoten, Anwendungslogik erzwingt das
+(kein DB-CHECK, da SQLAlchemy-Enum-Vergleich in einem CHECK umständlich wäre
+und der Wert ohnehin nur über den Service-Layer gesetzt wird).
+
+Revision ID: 0060_lagerort_standort_id
+Revises: 0059_objekttyp_wartung_aktiv
+Create Date: 2026-09-14
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+
+revision: str = "0060_lagerort_standort_id"
+down_revision: Union[str, None] = "0059_objekttyp_wartung_aktiv"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ op.execute(
+ "ALTER TABLE lagerort ADD COLUMN standort_id INTEGER REFERENCES standort(id) ON DELETE SET NULL"
+ )
+
+
+def downgrade() -> None:
+ op.execute("ALTER TABLE lagerort DROP COLUMN standort_id")
diff --git a/backend/app/api/v1/endpoints/lager.py b/backend/app/api/v1/endpoints/lager.py
index 2d37b8a..aef8a47 100644
--- a/backend/app/api/v1/endpoints/lager.py
+++ b/backend/app/api/v1/endpoints/lager.py
@@ -24,8 +24,10 @@ from app.services.lager import (
NichtGenugBestandError,
auslagern,
einlagern,
+ standort_fuer_lagerplatz,
umlagern,
)
+from app.services.standort_sichtbarkeit import pruefe_standort_sichtbar, sichtbare_standort_ids
router = APIRouter()
@@ -80,13 +82,26 @@ async def aendere_lagerort(
@router.get("/lagerplaetze", response_model=list[LagerplatzRead])
async def liste_lagerplaetze(
- lagerort_id: int | None = None, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
+ lagerort_id: int | None = None, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user)
) -> list[Lagerplatz]:
+ """ZUST-004-Erweiterung: Standort-Auflösung je Lagerplatz (Eltern-Kette,
+ kein direktes Feld) - Filterung nach Laden statt per SQL-Join, da die
+ Kettenlänge variabel ist und Lagerplatz-Listen typischerweise klein sind."""
stmt = select(Lagerplatz)
if lagerort_id is not None:
stmt = stmt.where(Lagerplatz.lagerort_id == lagerort_id)
result = await db.execute(stmt)
- return list(result.scalars().all())
+ lagerplaetze = list(result.scalars().all())
+
+ sichtbare = await sichtbare_standort_ids(db, current_user.id)
+ if sichtbare is None:
+ return lagerplaetze
+ gefiltert = []
+ for lagerplatz in lagerplaetze:
+ standort_id = await standort_fuer_lagerplatz(db, lagerplatz.id)
+ if standort_id is None or standort_id in sichtbare:
+ gefiltert.append(lagerplatz)
+ return gefiltert
@router.post("/lagerplaetze", response_model=LagerplatzRead, status_code=status.HTTP_201_CREATED)
@@ -107,9 +122,11 @@ async def erstelle_lagerplatz(
@router.get("/lagerplaetze/{lagerplatz_id}/bestand", response_model=list[BestandRead])
async def hole_bestand(
- lagerplatz_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
+ lagerplatz_id: int, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user)
) -> list[Bestand]:
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Lagerplatz nicht gefunden")
+ standort_id = await standort_fuer_lagerplatz(db, lagerplatz_id)
+ await pruefe_standort_sichtbar(db, benutzer_id=current_user.id, standort_id=standort_id)
result = await db.execute(select(Bestand).where(Bestand.lagerplatz_id == lagerplatz_id))
return list(result.scalars().all())
@@ -126,6 +143,8 @@ async def route_einlagern(
current_user=Depends(get_current_user),
) -> Materialbewegung:
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Lagerplatz nicht gefunden")
+ standort_id = await standort_fuer_lagerplatz(db, lagerplatz_id)
+ await pruefe_standort_sichtbar(db, benutzer_id=current_user.id, standort_id=standort_id)
return await einlagern(
db,
lagerplatz_id=lagerplatz_id,
@@ -148,6 +167,8 @@ async def route_auslagern(
current_user=Depends(get_current_user),
) -> Materialbewegung:
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Lagerplatz nicht gefunden")
+ standort_id = await standort_fuer_lagerplatz(db, lagerplatz_id)
+ await pruefe_standort_sichtbar(db, benutzer_id=current_user.id, standort_id=standort_id)
try:
return await auslagern(
db,
@@ -175,6 +196,10 @@ async def route_umlagern(
"""WH-005: Verschieben in einem Schritt statt Aus+Ein manuell."""
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Quell-Lagerplatz nicht gefunden")
await _get_or_404(db, Lagerplatz, payload.nach_lagerplatz_id, "Ziel-Lagerplatz nicht gefunden")
+ von_standort_id = await standort_fuer_lagerplatz(db, lagerplatz_id)
+ await pruefe_standort_sichtbar(db, benutzer_id=current_user.id, standort_id=von_standort_id)
+ nach_standort_id = await standort_fuer_lagerplatz(db, payload.nach_lagerplatz_id)
+ await pruefe_standort_sichtbar(db, benutzer_id=current_user.id, standort_id=nach_standort_id)
try:
return await umlagern(
db,
diff --git a/backend/app/models/lager.py b/backend/app/models/lager.py
index 0674c95..4f0b688 100644
--- a/backend/app/models/lager.py
+++ b/backend/app/models/lager.py
@@ -24,7 +24,15 @@ lagerort_typ_pg = PgEnum(LagerortTyp, name="lagerort_typ", create_type=False)
class Lagerort(Base):
"""WH-001: mehrstufige Hierarchie statt flachem `standort`-Feld (Objekt/
Zustaendigkeit bleiben unverändert an `standort` hängen - Lagerort ist ein
- zusätzliches, feineres Konzept für das Warehouse-Epic, kein Ersatz)."""
+ zusätzliches, feineres Konzept für das Warehouse-Epic, kein Ersatz).
+
+ ZUST-004-Nachtrag (2026-09-14): `standort_id` verknüpft NUR Knoten mit
+ `typ=standort` mit der echten `Standort`-Tabelle (fachlich sinnlos an
+ einem `raum`/`lager`-Knoten - dessen Standort ergibt sich aus dem
+ nächsten `standort`-Vorfahren in der Kette, siehe
+ app/services/lager.py:standort_fuer_lagerort()). Nullable, weil ein
+ `standort`-Knoten ohne Zuordnung weiterhin funktionieren muss
+ (Rückwärtskompatibilität - bestehende Lagerorte haben keine Zuordnung)."""
__tablename__ = "lagerort"
@@ -32,6 +40,7 @@ class Lagerort(Base):
name: Mapped[str] = mapped_column(String, nullable=False)
parent_id: Mapped[int | None] = mapped_column(ForeignKey("lagerort.id", ondelete="SET NULL"))
typ: Mapped[LagerortTyp] = mapped_column(lagerort_typ_pg, nullable=False)
+ standort_id: Mapped[int | None] = mapped_column(ForeignKey("standort.id", ondelete="SET NULL"))
class Lagerplatz(Base):
diff --git a/backend/app/schemas/lager.py b/backend/app/schemas/lager.py
index 983d58f..10bd8f3 100644
--- a/backend/app/schemas/lager.py
+++ b/backend/app/schemas/lager.py
@@ -11,12 +11,14 @@ class LagerortCreate(BaseModel):
name: str
parent_id: int | None = None
typ: LagerortTyp
+ standort_id: int | None = None
class LagerortUpdate(BaseModel):
name: str | None = None
parent_id: int | None = None
typ: LagerortTyp | None = None
+ standort_id: int | None = None
class LagerortRead(BaseModel):
@@ -25,6 +27,7 @@ class LagerortRead(BaseModel):
name: str
parent_id: int | None
typ: LagerortTyp
+ standort_id: int | None
class LagerplatzCreate(BaseModel):
diff --git a/backend/app/services/lager.py b/backend/app/services/lager.py
index 847fc5a..6e8ae9f 100644
--- a/backend/app/services/lager.py
+++ b/backend/app/services/lager.py
@@ -4,13 +4,40 @@ from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from app.models.lager import Bestand, Materialbewegung
+from app.models.lager import Bestand, Lagerort, Lagerplatz, Materialbewegung
class NichtGenugBestandError(Exception):
pass
+async def standort_fuer_lagerort(db: AsyncSession, lagerort_id: int | None) -> int | None:
+ """ZUST-004-Nachtrag: läuft die `parent_id`-Kette nach oben bis zum
+ nächsten Knoten mit gesetztem `standort_id` (nicht zwingend `typ=
+ standort` - ein Admin könnte die Zuordnung theoretisch an jeder Ebene
+ setzen, das schadet nicht, es zählt einfach der nächste Treffer).
+ `None`, wenn kein Vorfahre eine Zuordnung hat (unrestricted, wie bisher).
+ Feste Obergrenze gegen einen versehentlichen Parent-Zyklus."""
+ aktuelle_id = lagerort_id
+ for _ in range(50):
+ if aktuelle_id is None:
+ return None
+ lagerort = await db.get(Lagerort, aktuelle_id)
+ if lagerort is None:
+ return None
+ if lagerort.standort_id is not None:
+ return lagerort.standort_id
+ aktuelle_id = lagerort.parent_id
+ return None
+
+
+async def standort_fuer_lagerplatz(db: AsyncSession, lagerplatz_id: int) -> int | None:
+ lagerplatz = await db.get(Lagerplatz, lagerplatz_id)
+ if lagerplatz is None:
+ return None
+ return await standort_fuer_lagerort(db, lagerplatz.lagerort_id)
+
+
async def _bestand_zeile(db: AsyncSession, lagerplatz_id: int, material_id: int) -> Bestand | None:
result = await db.execute(
select(Bestand).where(Bestand.lagerplatz_id == lagerplatz_id, Bestand.material_id == material_id)
diff --git a/backend/tests/test_zust004_standort_sichtbarkeit.py b/backend/tests/test_zust004_standort_sichtbarkeit.py
index d9e84da..7fda8f0 100644
--- a/backend/tests/test_zust004_standort_sichtbarkeit.py
+++ b/backend/tests/test_zust004_standort_sichtbarkeit.py
@@ -351,6 +351,63 @@ async def test_dashboard_einsatzbereitschaft_und_prueftermine_standort_eingeschr
assert prueftermine.status_code == 200
+@pytest.mark.asyncio
+async def test_lagerplaetze_und_bestand_standort_eingeschraenkt(
+ client, admin_user, materialverantwortlicher_user, standort_factory, material_factory
+):
+ """ZUST-004 Runde 5: Lagerort mit echtem Standort verknüpft (neues Feld
+ Lagerort.standort_id), Sichtbarkeit löst über die parent_id-Kette auf."""
+ standort_a = await standort_factory("Standort A12")
+ standort_b = await standort_factory("Standort B12")
+ material = await material_factory("Verbandpäckchen ZUST4")
+ admin_token = await login(client, "admin1")
+
+ ort_a = await client.post(
+ "/api/v1/lagerorte",
+ json={"name": "Wache A12", "typ": "standort", "standort_id": standort_a.id},
+ headers=auth_header(admin_token),
+ )
+ ort_b = await client.post(
+ "/api/v1/lagerorte",
+ json={"name": "Wache B12", "typ": "standort", "standort_id": standort_b.id},
+ headers=auth_header(admin_token),
+ )
+ # Unterknoten OHNE eigene standort_id - muss die Kette hoch zum Standort-
+ # Knoten auflösen.
+ unterordner_b = await client.post(
+ "/api/v1/lagerorte",
+ json={"name": "Regal B12", "typ": "lager", "parent_id": ort_b.json()["id"]},
+ headers=auth_header(admin_token),
+ )
+ platz_b = await client.post(
+ "/api/v1/lagerplaetze",
+ json={"lagerort_id": unterordner_b.json()["id"], "code": "ZUST4-PLATZ-B12"},
+ headers=auth_header(admin_token),
+ )
+ platz_b_id = platz_b.json()["id"]
+
+ await client.put(
+ f"/api/v1/benutzer/{materialverantwortlicher_user.id}/standorte",
+ json={"standort_ids": [standort_a.id]},
+ headers=auth_header(admin_token),
+ )
+ token = await login(client, "materialverantwortlicher1")
+
+ liste = await client.get("/api/v1/lagerplaetze", headers=auth_header(token))
+ ids = {p["id"] for p in liste.json()}
+ assert platz_b_id not in ids
+
+ bestand_verboten = await client.get(f"/api/v1/lagerplaetze/{platz_b_id}/bestand", headers=auth_header(token))
+ assert bestand_verboten.status_code == 403
+
+ einlagern_verboten = await client.post(
+ f"/api/v1/lagerplaetze/{platz_b_id}/einlagern",
+ json={"material_id": material.id, "menge": "5"},
+ headers=auth_header(token),
+ )
+ assert einlagern_verboten.status_code == 403
+
+
@pytest.mark.asyncio
async def test_standort_zugriff_setzen_nur_admin(client, mitarbeiter_user, mitarbeiter2_user):
token = await login(client, "mitarbeiter1")
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index e54614e..865824c 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -255,6 +255,7 @@ export interface Lagerort {
name: string;
parent_id: number | null;
typ: LagerortTyp;
+ standort_id: number | null;
}
export interface Lagerplatz {
diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx
index 60d12fa..ce8c6ed 100644
--- a/frontend/src/pages/AdminPage.tsx
+++ b/frontend/src/pages/AdminPage.tsx
@@ -349,7 +349,9 @@ export function AdminPage() {
{aktiverTab === "lagerbewegungen" && (
)}
- {aktiverTab === "lager" && }
+ {aktiverTab === "lager" && (
+
+ )}
{aktiverTab === "ausgabe" && (
)}
diff --git a/frontend/src/pages/admin/LagerSection.tsx b/frontend/src/pages/admin/LagerSection.tsx
index b0488c5..f632f2b 100644
--- a/frontend/src/pages/admin/LagerSection.tsx
+++ b/frontend/src/pages/admin/LagerSection.tsx
@@ -1,11 +1,12 @@
import { useEffect, useState } from "react";
import { apiRequest, ApiError } from "../../api/client";
-import type { Bestand, Lagerort, LagerortTyp, Lagerplatz, Material, Materialbewegung } from "../../api/types";
+import type { Bestand, Lagerort, LagerortTyp, Lagerplatz, Material, Materialbewegung, Standort } from "../../api/types";
import * as lagerbewegungQueue from "../../offline/lagerbewegungQueue";
interface Props {
materialien: Material[];
+ standorte: Standort[];
onFehler: (text: string) => void;
}
@@ -19,13 +20,19 @@ const TYP_LABEL: Record = {
// WH-001..005+007: Lagerort-Hierarchie, Lagerplätze, Bestand je Lagerplatz,
// Ein-/Auslagerung, Umlagerung. Inventur (WH-006) noch offen (arbeitskacheln/07_warehouse.md).
-export function LagerSection({ materialien, onFehler }: Props) {
+export function LagerSection({ materialien, standorte, onFehler }: Props) {
const [lagerorte, setLagerorte] = useState([]);
const [lagerplaetze, setLagerplaetze] = useState([]);
const [neuerOrtName, setNeuerOrtName] = useState("");
const [neuerOrtTyp, setNeuerOrtTyp] = useState("lager");
const [neuerOrtParent, setNeuerOrtParent] = useState("");
+ const [neuerOrtStandortId, setNeuerOrtStandortId] = useState("");
+
+ function standortName(id: number | null): string {
+ if (id === null) return "–";
+ return standorte.find((s) => s.id === id)?.name ?? `#${id}`;
+ }
const [neuerPlatzOrt, setNeuerPlatzOrt] = useState("");
const [neuerPlatzCode, setNeuerPlatzCode] = useState("");
@@ -99,10 +106,16 @@ export function LagerSection({ materialien, onFehler }: Props) {
try {
await apiRequest("/lagerorte", {
method: "POST",
- body: { name: neuerOrtName, typ: neuerOrtTyp, parent_id: neuerOrtParent === "" ? null : neuerOrtParent },
+ body: {
+ name: neuerOrtName,
+ typ: neuerOrtTyp,
+ parent_id: neuerOrtParent === "" ? null : neuerOrtParent,
+ standort_id: neuerOrtTyp === "standort" && neuerOrtStandortId !== "" ? neuerOrtStandortId : null,
+ },
});
setNeuerOrtName("");
setNeuerOrtParent("");
+ setNeuerOrtStandortId("");
await laden();
} catch {
onFehler("Lagerort konnte nicht angelegt werden.");
@@ -223,6 +236,21 @@ export function LagerSection({ materialien, onFehler }: Props) {
))}
+ {neuerOrtTyp === "standort" && (
+
+ )}