feat(zustaendigkeit): ZUST-004 Runde 6 - kennzahlen/ablaufdaten/wartungen
CI / backend-tests (push) Successful in 3m44s
CI / frontend-build (push) Successful in 22s

Letzte dashboard-nahen Objekt-bezogenen Endpunkte ohne Standort-Filter
geschlossen: kennzahlen() (vier Fehlbestand-Zählungen + Objekte-Gesamtzahl),
bevorstehende_ablaufdaten(), alle_faelligen_wartungen() bekommen optionales
sichtbare_standort_ids. Hintergrund-Job wartung_benachrichtigung.py ruft
weiterhin ohne Filter auf. bevorstehende_qualifikationsablaeufe() bleibt
bewusst unangetastet - rein Personal-bezogen, kein Objekt-/Standort-Konzept.
Damit ist ZUST-004 für alle Objekt-bezogenen Endpunkte durchgezogen.

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:56:56 +02:00
1 parent 9084e6c9d7
commit b68314a06d
6 files changed
+229 -30

No files matched your search

+105
View File
@@ -16078,3 +16078,108 @@ Keine Commits in dieser Session.
- backend/tests/test_zust004_standort_sichtbarkeit.py | 11 ++++++++---
---
## 2026-09-14 23:31 – 23:37 (5m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
- 9084e6c feat(zustaendigkeit): ZUST-004 Runde 5 - Lagerort mit echtem Standort verknüpft
### Geänderte Dateien
- DEVLOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 34 ++++++++++++++++++--------
- backend/alembic/versions/0060_lagerort_standort_id.py | 28 +++++++++++++++++++++
- backend/app/api/v1/endpoints/lager.py | 31 ++++++++++++++++++++---
- backend/app/models/lager.py | 11 ++++++++-
- backend/app/schemas/lager.py | 3 +++
- backend/app/services/lager.py | 29 +++++++++++++++++++++-
- backend/tests/test_zust004_standort_sichtbarkeit.py | 57 ++++++++++++++++++++++++++++++++++++++++++
- frontend/src/api/types.ts | 1 +
- frontend/src/pages/AdminPage.tsx | 4 ++-
- frontend/src/pages/admin/LagerSection.tsx | 41 ++++++++++++++++++++++++++++---
---
## 2026-09-14 23:40 – 23:41 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** nexarch
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 34 ++++++++++++++++++--------
- backend/alembic/versions/0060_lagerort_standort_id.py | 28 +++++++++++++++++++++
- backend/app/api/v1/endpoints/lager.py | 31 ++++++++++++++++++++---
- backend/app/models/lager.py | 11 ++++++++-
- backend/app/schemas/lager.py | 3 +++
- backend/app/services/lager.py | 29 +++++++++++++++++++++-
- backend/tests/test_zust004_standort_sichtbarkeit.py | 57 ++++++++++++++++++++++++++++++++++++++++++
- frontend/src/api/types.ts | 1 +
- frontend/src/pages/AdminPage.tsx | 4 ++-
- frontend/src/pages/admin/LagerSection.tsx | 41 ++++++++++++++++++++++++++++---
---
## 2026-09-14 23:49 – 23:50 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 34 ++++++++++++++++++--------
- backend/alembic/versions/0060_lagerort_standort_id.py | 28 +++++++++++++++++++++
- backend/app/api/v1/endpoints/lager.py | 31 ++++++++++++++++++++---
- backend/app/models/lager.py | 11 ++++++++-
- backend/app/schemas/lager.py | 3 +++
- backend/app/services/lager.py | 29 +++++++++++++++++++++-
- backend/tests/test_zust004_standort_sichtbarkeit.py | 57 ++++++++++++++++++++++++++++++++++++++++++
- frontend/src/api/types.ts | 1 +
- frontend/src/pages/AdminPage.tsx | 4 ++-
- frontend/src/pages/admin/LagerSection.tsx | 41 ++++++++++++++++++++++++++++---
---
## 2026-09-14 23:50 – 23:52 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** vsb-talk
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 34 ++++++++++++++++++--------
- backend/alembic/versions/0060_lagerort_standort_id.py | 28 +++++++++++++++++++++
- backend/app/api/v1/endpoints/lager.py | 31 ++++++++++++++++++++---
- backend/app/models/lager.py | 11 ++++++++-
- backend/app/schemas/lager.py | 3 +++
- backend/app/services/lager.py | 29 +++++++++++++++++++++-
- backend/tests/test_zust004_standort_sichtbarkeit.py | 57 ++++++++++++++++++++++++++++++++++++++++++
- frontend/src/api/types.ts | 1 +
- frontend/src/pages/AdminPage.tsx | 4 ++-
- frontend/src/pages/admin/LagerSection.tsx | 41 ++++++++++++++++++++++++++++---
---
## 2026-09-14 23:52 – 23:52 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 34 ++++++++++++++++++--------
- backend/alembic/versions/0060_lagerort_standort_id.py | 28 +++++++++++++++++++++
- backend/app/api/v1/endpoints/lager.py | 31 ++++++++++++++++++++---
- backend/app/models/lager.py | 11 ++++++++-
- backend/app/schemas/lager.py | 3 +++
- backend/app/services/lager.py | 29 +++++++++++++++++++++-
- backend/tests/test_zust004_standort_sichtbarkeit.py | 57 ++++++++++++++++++++++++++++++++++++++++++
- frontend/src/api/types.ts | 1 +
- frontend/src/pages/AdminPage.tsx | 4 ++-
- frontend/src/pages/admin/LagerSection.tsx | 41 ++++++++++++++++++++++++++++---
---
+13 -5
View File
@@ -204,11 +204,7 @@ war die Vorgängerlücke, diese hier ist die zweite gefundene Lücke).
`bevorstehende_prueftermine()` bekommen optionales
`sichtbare_standort_ids`, `None` = unrestricted - `readiness_snapshot.py`
ruft weiterhin ohne Filter auf, der Snapshot-Job kennt kein Nutzer-
Konzept). **Bewusst NICHT eingeschränkt:** `kennzahlen()`,
`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.
Konzept).
**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
@@ -236,6 +232,18 @@ war die Vorgängerlücke, diese hier ist die zweite gefundene Lücke).
`test_dashboard_einsatzbereitschaft_und_prueftermine_standort_eingeschraenkt`,
`test_lagerplaetze_und_bestand_standort_eingeschraenkt`
in `backend/tests/test_zust004_standort_sichtbarkeit.py`.
**Runde 6 (2026-09-14): letzte dashboard-nahen Lücken geschlossen.**
`kennzahlen()` (alle vier Fehlbestand-Zählungen + Objekte-Gesamtzahl über
Join auf `Objekt`), `bevorstehende_ablaufdaten()`,
`alle_faelligen_wartungen()` bekommen ebenfalls optionales
`sichtbare_standort_ids` - `wartung_benachrichtigung.py` (Hintergrund-Job)
ruft weiterhin ohne Filter auf. **Bewusst weiterhin NICHT eingeschränkt:**
`bevorstehende_qualifikationsablaeufe()` - rein Personal-/Benutzer-
bezogen (Qualifikationsablauf einer Person), kein Objekt-/Standort-Konzept,
für das ZUST-004 überhaupt greifen könnte. Damit ist ZUST-004 für alle
Objekt-bezogenen Endpunkte durchgezogen. Test:
`test_dashboard_kennzahlen_ablaufdaten_wartungen_standort_eingeschraenkt`
in `backend/tests/test_zust004_standort_sichtbarkeit.py`.
---
+21 -9
View File
@@ -30,16 +30,25 @@ _verantwortliche = require_roles(
@router.get("/dashboard/kennzahlen", response_model=KennzahlenRead)
async def hole_kennzahlen(db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)) -> KennzahlenRead:
async def hole_kennzahlen(
db: AsyncSession = Depends(get_db), current_user=Depends(_verantwortliche)
) -> KennzahlenRead:
"""Prompt 12.1: Zielgruppe Materialverantwortlicher/Leitungsverantwortlicher/
Administration. Filterung nach Zuständigkeitsbereich (Karte 04) folgt später,
V1 zeigt globale Zahlen für diese Rollen."""
return KennzahlenRead(**await kennzahlen(db))
Administration. ZUST-004-Erweiterung (2026-09-14, Nutzer-Entscheidung):
berücksichtigt jetzt die Standort-Sichtbarkeit - ursprüngliche "V1 zeigt
globale Zahlen"-Festlegung bewusst aufgehoben, gleiche Linie wie
einsatzbereitschaft()/prueftermine()."""
sichtbare = await sichtbare_standort_ids(db, current_user.id)
return KennzahlenRead(**await kennzahlen(db, sichtbare))
@router.get("/dashboard/ablaufdaten", response_model=list[AblaufdatumRead])
async def hole_ablaufdaten(db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)) -> list[AblaufdatumRead]:
return [AblaufdatumRead(**eintrag) for eintrag in await bevorstehende_ablaufdaten(db)]
async def hole_ablaufdaten(
db: AsyncSession = Depends(get_db), current_user=Depends(_verantwortliche)
) -> list[AblaufdatumRead]:
sichtbare = await sichtbare_standort_ids(db, current_user.id)
eintraege = await bevorstehende_ablaufdaten(db, sichtbare)
return [AblaufdatumRead(**eintrag) for eintrag in eintraege]
@router.get("/dashboard/einsatzbereitschaft", response_model=EinsatzbereitschaftRead)
@@ -71,12 +80,15 @@ async def hole_prueftermine(
@router.get("/dashboard/faellige-wartungen", response_model=list[FaelligeWartungRead])
async def hole_faellige_wartungen_global(
db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)
db: AsyncSession = Depends(get_db), current_user=Depends(_verantwortliche)
) -> list[FaelligeWartungRead]:
"""NOTIF-003: globale Dashboard-Kachel "Fällige Wartungen" - ergänzt die
bereits bestehende Objekt-bezogene Sicht (MAINT-002,
/objekte/{id}/faellige-wartungen) um eine Übersicht über alle Objekte."""
return [FaelligeWartungRead(**eintrag) for eintrag in await alle_faelligen_wartungen(db)]
/objekte/{id}/faellige-wartungen) um eine Übersicht über alle Objekte.
ZUST-004-Erweiterung: Standort-Sichtbarkeit wie bei kennzahlen()."""
sichtbare = await sichtbare_standort_ids(db, current_user.id)
eintraege = await alle_faelligen_wartungen(db, sichtbare)
return [FaelligeWartungRead(**eintrag) for eintrag in eintraege]
@router.get("/dashboard/qualifikationsablaeufe", response_model=list[QualifikationsablaufRead])
+46 -13
View File
@@ -24,33 +24,62 @@ OFFEN_STATI = (FehlbestandStatus.offen, FehlbestandStatus.in_bearbeitung, Fehlbe
STANDARD_WARNZEITRAUM_TAGE = 30
async def kennzahlen(db: AsyncSession) -> dict:
async def kennzahlen(db: AsyncSession, sichtbare_standort_ids: set[int] | None = None) -> dict:
"""ZUST-004-Erweiterung (2026-09-14, gleiche Nutzer-Entscheidung wie bei
einsatzbereitschaft()): `sichtbare_standort_ids=None` = unrestricted
(bewusste Abkehr von der ursprünglichen "V1 zeigt globale Zahlen"-
Festlegung)."""
jetzt = datetime.now(timezone.utc)
result = await db.execute(select(func.count()).select_from(Fehlbestand).where(Fehlbestand.status.in_(OFFEN_STATI)))
fehlbestand_stmt = select(func.count()).select_from(Fehlbestand).where(Fehlbestand.status.in_(OFFEN_STATI))
if sichtbare_standort_ids is not None:
fehlbestand_stmt = fehlbestand_stmt.join(Objekt, Objekt.id == Fehlbestand.objekt_id).where(
Objekt.standort_id.in_(sichtbare_standort_ids)
)
result = await db.execute(fehlbestand_stmt)
anzahl_offen = result.scalar_one()
result = await db.execute(
select(func.count(func.distinct(MindermengenGenehmigung.fehlbestand_id))).where(
MindermengenGenehmigung.status == MindermengeStatus.aktiv
)
genehmigt_stmt = select(func.count(func.distinct(MindermengenGenehmigung.fehlbestand_id))).where(
MindermengenGenehmigung.status == MindermengeStatus.aktiv
)
if sichtbare_standort_ids is not None:
genehmigt_stmt = (
genehmigt_stmt.join(Fehlbestand, Fehlbestand.id == MindermengenGenehmigung.fehlbestand_id)
.join(Objekt, Objekt.id == Fehlbestand.objekt_id)
.where(Objekt.standort_id.in_(sichtbare_standort_ids))
)
result = await db.execute(genehmigt_stmt)
anzahl_genehmigt = result.scalar_one()
result = await db.execute(
erledigt_stmt = (
select(func.count())
.select_from(Fehlbestand)
.where(Fehlbestand.status == FehlbestandStatus.erledigt, Fehlbestand.erledigt_am >= jetzt - timedelta(days=7))
)
if sichtbare_standort_ids is not None:
erledigt_stmt = erledigt_stmt.join(Objekt, Objekt.id == Fehlbestand.objekt_id).where(
Objekt.standort_id.in_(sichtbare_standort_ids)
)
result = await db.execute(erledigt_stmt)
anzahl_kuerzlich_erledigt = result.scalar_one()
result = await db.execute(select(func.count(func.distinct(Fehlbestand.objekt_id))).where(Fehlbestand.status.in_(OFFEN_STATI)))
problematisch_stmt = select(func.count(func.distinct(Fehlbestand.objekt_id))).where(
Fehlbestand.status.in_(OFFEN_STATI)
)
if sichtbare_standort_ids is not None:
problematisch_stmt = problematisch_stmt.join(Objekt, Objekt.id == Fehlbestand.objekt_id).where(
Objekt.standort_id.in_(sichtbare_standort_ids)
)
result = await db.execute(problematisch_stmt)
anzahl_problematische_objekte = result.scalar_one()
result = await db.execute(select(func.count()).select_from(Objekt))
objekte_stmt = select(func.count()).select_from(Objekt)
if sichtbare_standort_ids is not None:
objekte_stmt = objekte_stmt.where(Objekt.standort_id.in_(sichtbare_standort_ids))
result = await db.execute(objekte_stmt)
anzahl_objekte_gesamt = result.scalar_one()
ablaufdaten_liste = await bevorstehende_ablaufdaten(db)
ablaufdaten_liste = await bevorstehende_ablaufdaten(db, sichtbare_standort_ids)
return {
"anzahl_offener_fehlbestaende": anzahl_offen,
@@ -71,16 +100,20 @@ def _ablauf_status(ablaufdatum: date, warnzeitraum_tage: int, heute: date) -> tu
return "gueltig", verbleibende_tage
async def bevorstehende_ablaufdaten(db: AsyncSession) -> list[dict]:
async def bevorstehende_ablaufdaten(db: AsyncSession, sichtbare_standort_ids: set[int] | None = None) -> list[dict]:
"""Prompt 14.5: nur bald_ablaufend/abgelaufen, sortiert nach verbleibenden Tagen
aufsteigend (dringendste zuerst). "gueltig" gehört nicht in diese Liste."""
aufsteigend (dringendste zuerst). "gueltig" gehört nicht in diese Liste.
ZUST-004-Erweiterung: `sichtbare_standort_ids=None` = unrestricted."""
heute = datetime.now(timezone.utc).date()
result = await db.execute(
stmt = (
select(Objektposition, Material, Objekt)
.join(Material, Objektposition.material_id == Material.id)
.join(Objekt, Objektposition.objekt_id == Objekt.id)
.where(Objektposition.ablaufdatum.is_not(None))
)
if sichtbare_standort_ids is not None:
stmt = stmt.where(Objekt.standort_id.in_(sichtbare_standort_ids))
result = await db.execute(stmt)
eintraege = []
for objektposition, material, objekt in result.all():
warnzeitraum = material.warnzeitraum_tage or STANDARD_WARNZEITRAUM_TAGE
+9 -3
View File
@@ -255,11 +255,17 @@ async def faellige_wartungen_fuer_objekt(db: AsyncSession, objekt: Objekt) -> li
return ergebnisse
async def alle_faelligen_wartungen(db: AsyncSession) -> list[dict]:
async def alle_faelligen_wartungen(db: AsyncSession, sichtbare_standort_ids: set[int] | None = None) -> list[dict]:
"""NOTIF-003: globale Sicht über alle Objekte für Dashboard-Kachel und
Benachrichtigungs-Prüflauf - iteriert `faellige_wartungen_fuer_objekt` je
Objekt (keine bestehende Aggregation vorhanden), filtert auf faellig=True."""
objekte = (await db.execute(select(Objekt))).scalars().all()
Objekt (keine bestehende Aggregation vorhanden), filtert auf faellig=True.
ZUST-004-Erweiterung: `sichtbare_standort_ids=None` = unrestricted - der
Benachrichtigungs-Prüflauf (wartung_benachrichtigung.py) ruft weiterhin
ohne Filter auf, kein Nutzer-Konzept im Hintergrund-Job."""
stmt = select(Objekt)
if sichtbare_standort_ids is not None:
stmt = stmt.where(Objekt.standort_id.in_(sichtbare_standort_ids))
objekte = (await db.execute(stmt)).scalars().all()
ergebnisse: list[dict] = []
for objekt in objekte:
for eintrag in await faellige_wartungen_fuer_objekt(db, objekt):
@@ -408,6 +408,41 @@ async def test_lagerplaetze_und_bestand_standort_eingeschraenkt(
assert einlagern_verboten.status_code == 403
@pytest.mark.asyncio
async def test_dashboard_kennzahlen_ablaufdaten_wartungen_standort_eingeschraenkt(
client, admin_user, materialverantwortlicher_user, standort_factory, objekt_factory
):
"""ZUST-004 Runde 6: kennzahlen()/bevorstehende_ablaufdaten()/
alle_faelligen_wartungen() waren die letzten dashboard-nahen Funktionen
ohne Standort-Filter - Nutzer-Entscheidung, ursprüngliche "V1 zeigt
globale Zahlen"-Festlegung endgültig aufgehoben."""
standort_a = await standort_factory("Standort A13")
standort_b = await standort_factory("Standort B13")
await objekt_factory(name="Objekt A13", code="ZUST4-A13", standort=standort_a)
await objekt_factory(name="Objekt B13", code="ZUST4-B13", standort=standort_b)
admin_token = await login(client, "admin1")
unbeschraenkt = await client.get("/api/v1/dashboard/kennzahlen", headers=auth_header(admin_token))
gesamt_vorher = unbeschraenkt.json()["anzahl_objekte_gesamt"]
assert gesamt_vorher >= 2
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")
eingeschraenkt = await client.get("/api/v1/dashboard/kennzahlen", headers=auth_header(token))
assert eingeschraenkt.json()["anzahl_objekte_gesamt"] < gesamt_vorher
ablaufdaten = await client.get("/api/v1/dashboard/ablaufdaten", headers=auth_header(token))
assert ablaufdaten.status_code == 200
wartungen = await client.get("/api/v1/dashboard/faellige-wartungen", headers=auth_header(token))
assert wartungen.status_code == 200
@pytest.mark.asyncio
async def test_standort_zugriff_setzen_nur_admin(client, mitarbeiter_user, mitarbeiter2_user):
token = await login(client, "mitarbeiter1")