feat(zustaendigkeit): ZUST-004 Runde 5 - Lagerort mit echtem Standort verknüpft
CI / backend-tests (push) Successful in 3m42s
CI / frontend-build (push) Successful in 22s

Lagerort.standort_id (Migration 0060, nullable, nur an typ=standort-Knoten
fachlich sinnvoll) löst die bisherige Blockade auf: standort_fuer_lagerort()/
standort_fuer_lagerplatz() laufen die parent_id-Kette hoch bis zum nächsten
Standort-Treffer. Damit jetzt eingeschränkt: GET /lagerplaetze(+bestand),
POST einlagern/auslagern/umlagern (Umlagern prüft Quelle UND Ziel). Admin-UI
zeigt Standort-Auswahl nur bei typ=standort. Lagerbewegung (Objekt-Ebene,
WH-007) bleibt unberührt - anderes Modell, war nie Teil der Blockade.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-14 23:36:48 +02:00
1 parent 6f116edd51
commit 9084e6c9d7
11 files changed
+306 -20

No files matched your search

+87
View File
@@ -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 ++++++++---
---
+24 -10
View File
@@ -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`.
---
@@ -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")
+28 -3
View File
@@ -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,
+10 -1
View File
@@ -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):
+3
View File
@@ -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):
+28 -1
View File
@@ -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)
@@ -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")
+1
View File
@@ -255,6 +255,7 @@ export interface Lagerort {
name: string;
parent_id: number | null;
typ: LagerortTyp;
standort_id: number | null;
}
export interface Lagerplatz {
+3 -1
View File
@@ -349,7 +349,9 @@ export function AdminPage() {
{aktiverTab === "lagerbewegungen" && (
<LagerbewegungSection objekte={objekte} standorte={standorte} onFehler={setFehler} />
)}
{aktiverTab === "lager" && <LagerSection materialien={materialien} onFehler={setFehler} />}
{aktiverTab === "lager" && (
<LagerSection materialien={materialien} standorte={standorte} onFehler={setFehler} />
)}
{aktiverTab === "ausgabe" && (
<AusgabeSection materialien={materialien} benutzer={benutzer} onFehler={setFehler} />
)}
+37 -4
View File
@@ -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<LagerortTyp, string> = {
// 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<Lagerort[]>([]);
const [lagerplaetze, setLagerplaetze] = useState<Lagerplatz[]>([]);
const [neuerOrtName, setNeuerOrtName] = useState("");
const [neuerOrtTyp, setNeuerOrtTyp] = useState<LagerortTyp>("lager");
const [neuerOrtParent, setNeuerOrtParent] = useState<number | "">("");
const [neuerOrtStandortId, setNeuerOrtStandortId] = useState<number | "">("");
function standortName(id: number | null): string {
if (id === null) return "–";
return standorte.find((s) => s.id === id)?.name ?? `#${id}`;
}
const [neuerPlatzOrt, setNeuerPlatzOrt] = useState<number | "">("");
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) {
</option>
))}
</select>
{neuerOrtTyp === "standort" && (
<select
className="input"
value={neuerOrtStandortId}
onChange={(e) => setNeuerOrtStandortId(Number(e.target.value) || "")}
title="Verknüpfung mit echtem Standort (steuert Sichtbarkeit, ZUST-004)"
>
<option value="">Kein Standort verknüpft</option>
{standorte.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
)}
<select
className="input"
value={neuerOrtParent}
@@ -242,7 +270,12 @@ export function LagerSection({ materialien, onFehler }: Props) {
<ul className="card-list" style={{ marginTop: "0.75rem" }}>
{lagerorte.map((o) => (
<li key={o.id}>
{o.name} <span className="text-muted">({TYP_LABEL[o.typ]}{o.parent_id !== null ? `, unter ${ortName(o.parent_id)}` : ""})</span>
{o.name}{" "}
<span className="text-muted">
({TYP_LABEL[o.typ]}
{o.parent_id !== null ? `, unter ${ortName(o.parent_id)}` : ""}
{o.standort_id !== null ? `, Standort: ${standortName(o.standort_id)}` : ""})
</span>
</li>
))}
</ul>