From fab3a06073d99961366eeca8ccdf33b9e40b0313 Mon Sep 17 00:00:00 2001
From: patrick
Date: Sat, 5 Sep 2026 15:47:39 +0200
Subject: [PATCH] fix(dashboard): nie kontrollierte Objekte gelten nicht mehr
als einsatzbereit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Nutzer-Fund (Readiness-Backlog-Review): einsatzbereitschaft() zählte ein
Objekt ohne jede Kontrolle bisher als "einsatzbereit", weil keine Kriterien
verletzt waren - fachlich falsch, "nie geprüft" ist nicht dasselbe wie
"geprüft und in Ordnung".
Neuer vierter Zustand "unbekannt": Objekte ohne abgeschlossene Kontrolle UND
ohne sonstige Blocker fallen jetzt hierunter statt unter "einsatzbereit".
Objekte mit anderen Gründen (Fehlbestand/Mangel/Prüfung/...) bleiben davon
unberührt, die zählten schon vorher korrekt.
Tests angepasst: zwei bestehende Tests nutzten ungeprüfte Objekte und
erwarteten fälschlich "einsatzbereit" - jetzt entweder explizit kontrolliert
(Ist=Soll) oder auf "unbekannt" korrigiert, plus neuer Test für den Kernfall.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
---
DEVLOG.md | 30 ++++++++++++++++++++++++++++
backend/app/schemas/dashboard.py | 1 +
backend/app/services/dashboard.py | 16 +++++++++++++--
backend/tests/test_dashboard.py | 28 +++++++++++++++++++++++---
backend/tests/test_mangel.py | 16 ++++++++++++++-
frontend/src/pages/DashboardPage.tsx | 7 +++++++
6 files changed, 92 insertions(+), 6 deletions(-)
diff --git a/DEVLOG.md b/DEVLOG.md
index 58d62c0..6e1b075 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -4358,3 +4358,33 @@ Keine Commits in dieser Session.
- arbeitskacheln/11_defects.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
---
+## 2026-09-05 15:35 – 15:37 (2m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+- 27c2499 feat(fahrzeug): Funkkenner-Feld ergänzt (BOS-Digitalfunk-Kennung)
+
+### Geänderte Dateien
+- DEVLOG.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+- arbeitskacheln/05_fleet.md | 15 +++++---
+- backend/alembic/versions/0018_funkkenner.py | 25 ++++++++++++++
+- backend/app/models/fahrzeugdetails.py | 3 ++
+- backend/app/schemas/objekt.py | 2 ++
+- backend/tests/test_fahrzeugdetails.py | 8 ++++-
+- frontend/src/api/types.ts | 1 +
+- frontend/src/pages/admin/ObjektSection.tsx | 7 ++++
+
+---
+## 2026-09-05 15:39 – 15:41 (1m)
+**Beschreibung:** Claude Code Session
+**Projekt:** asb-material
+
+### Commits
+- 286227b docs: Personnel-Epic komplettiert (PERS-001..007)
+
+### Geänderte Dateien
+- arbeitskacheln/00_index.md | 5 +--
+- arbeitskacheln/12_personnel.md | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+---
diff --git a/backend/app/schemas/dashboard.py b/backend/app/schemas/dashboard.py
index f31d197..b13b528 100644
--- a/backend/app/schemas/dashboard.py
+++ b/backend/app/schemas/dashboard.py
@@ -31,6 +31,7 @@ class ObjektMangelRead(BaseModel):
class EinsatzbereitschaftRead(BaseModel):
einsatzbereit: int
eingeschraenkt_einsatzbereit: int
+ unbekannt: int
nicht_einsatzbereit: int
gesamt: int
details: list[ObjektMangelRead]
diff --git a/backend/app/services/dashboard.py b/backend/app/services/dashboard.py
index 0980491..dab17a9 100644
--- a/backend/app/services/dashboard.py
+++ b/backend/app/services/dashboard.py
@@ -14,6 +14,7 @@ from app.models.objektposition import Objektposition
from app.models.personal import BenutzerQualifikation, Qualifikationstyp
from app.models.stammdaten import Material
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
+from app.services.objekte import letzte_kontrollen_map
# Prompt 12.2: offen/in_bearbeitung/nachgefuellt_teilweise zählen gemeinsam als "offen"
# (technischer Substatus ist keine Erledigung, Prompt 03/20).
@@ -171,6 +172,11 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
objekt_ids_kritischer_mangel = await objekt_ids_mit_kritischem_offenem_mangel(db)
+ # Nutzer-Fund (Readiness-Review): ein nie kontrolliertes Objekt hatte bisher als
+ # "einsatzbereit" gegolten, weil keine Kriterien verletzt waren - fachlich falsch,
+ # "nie geprüft" ist nicht dasselbe wie "geprüft und in Ordnung".
+ letzte_kontrolle_map = await letzte_kontrollen_map(db, [o.id for o in objekte])
+
objekt_ids_hu_uvv_ueberfaellig = set(
(
await db.execute(
@@ -190,7 +196,7 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
.all()
)
- bereit = eingeschraenkt = 0
+ bereit = eingeschraenkt = unbekannt = 0
details: list[dict] = []
for objekt in objekte:
gruende: list[str] = []
@@ -216,6 +222,11 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
details.append(
{"objekt_id": objekt.id, "code": objekt.code, "name": objekt.name, "gruende": ["mindermenge_genehmigt"]}
)
+ elif not gruende and letzte_kontrolle_map.get(objekt.id) is None:
+ unbekannt += 1
+ details.append(
+ {"objekt_id": objekt.id, "code": objekt.code, "name": objekt.name, "gruende": ["noch_nie_kontrolliert"]}
+ )
elif not gruende:
bereit += 1
else:
@@ -224,7 +235,8 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
return {
"einsatzbereit": bereit,
"eingeschraenkt_einsatzbereit": eingeschraenkt,
- "nicht_einsatzbereit": len(objekte) - bereit - eingeschraenkt,
+ "unbekannt": unbekannt,
+ "nicht_einsatzbereit": len(objekte) - bereit - eingeschraenkt - unbekannt,
"gesamt": len(objekte),
"details": details,
}
diff --git a/backend/tests/test_dashboard.py b/backend/tests/test_dashboard.py
index 9b05a15..d62ca80 100644
--- a/backend/tests/test_dashboard.py
+++ b/backend/tests/test_dashboard.py
@@ -153,19 +153,41 @@ async def test_abgelaufenes_material_wird_als_abgelaufen_markiert(
@pytest.mark.asyncio
-async def test_objekt_ohne_maengel_ist_einsatzbereit(
- client, objekt_mit_position, materialverantwortlicher_user
+async def test_objekt_ohne_maengel_und_kontrolliert_ist_einsatzbereit(
+ client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
- objekt, _material = objekt_mit_position
+ """Kontrolliert (Ist=Soll, kein Fehlbestand) und ohne sonstige Mängel -> bereit.
+ Unterscheidet sich bewusst von test_nie_kontrolliertes_objekt_ist_unbekannt."""
+ objekt, material = objekt_mit_position
+ mitarbeiter_token = await login(client, "mitarbeiter1")
+ await _fehlbestand(client, mitarbeiter_token, objekt, material, "10") # = Sollmenge, kein Fehlbestand
+
token = await login(client, "materialverantwortlicher1")
response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
body = response.json()
assert body["einsatzbereit"] == 1
assert body["eingeschraenkt_einsatzbereit"] == 0
+ assert body["unbekannt"] == 0
assert body["nicht_einsatzbereit"] == 0
assert body["details"] == []
+@pytest.mark.asyncio
+async def test_nie_kontrolliertes_objekt_ist_unbekannt(
+ client, objekt_mit_position, materialverantwortlicher_user
+):
+ """Nutzer-Fund: ein nie kontrolliertes Objekt darf NICHT als einsatzbereit zählen
+ (unterscheidet "nie geprüft" von "geprüft und in Ordnung")."""
+ objekt, _material = objekt_mit_position
+ token = await login(client, "materialverantwortlicher1")
+ response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
+ body = response.json()
+ assert body["einsatzbereit"] == 0
+ assert body["unbekannt"] == 1
+ assert body["nicht_einsatzbereit"] == 0
+ assert body["details"][0]["gruende"] == ["noch_nie_kontrolliert"]
+
+
@pytest.mark.asyncio
async def test_offener_fehlbestand_macht_objekt_nicht_einsatzbereit(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
diff --git a/backend/tests/test_mangel.py b/backend/tests/test_mangel.py
index 910d25c..92e5ddf 100644
--- a/backend/tests/test_mangel.py
+++ b/backend/tests/test_mangel.py
@@ -100,8 +100,22 @@ async def test_kritischer_offener_mangel_macht_objekt_nicht_einsatzbereit(
async def test_niedrige_prioritaet_blockiert_einsatzbereitschaft_nicht(
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
):
- objekt, _material = objekt_mit_position
+ objekt, material = objekt_mit_position
mitarbeiter_token = await login(client, "mitarbeiter1")
+ # kontrolliert (Ist=Soll), damit dieser Test unabhängig vom "nie kontrolliert"-Fall
+ # (siehe test_dashboard.py: test_nie_kontrolliertes_objekt_ist_unbekannt) nur die
+ # Priorität isoliert prüft.
+ start = await client.post(
+ f"/api/v1/objekte/{objekt.id}/kontrollen", json={"uebernehmen": False}, headers=auth_header(mitarbeiter_token)
+ )
+ kontrolle_id = start.json()["id"]
+ await client.put(
+ f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}",
+ json={"istmenge": "10"},
+ headers=auth_header(mitarbeiter_token),
+ )
+ await client.post(f"/api/v1/kontrollen/{kontrolle_id}/abschliessen", headers=auth_header(mitarbeiter_token))
+
await client.post(
"/api/v1/maengel",
json={"objekt_id": objekt.id, "beschreibung": "Kratzer im Lack", "prioritaet": "niedrig"},
diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx
index 2aaa786..666095d 100644
--- a/frontend/src/pages/DashboardPage.tsx
+++ b/frontend/src/pages/DashboardPage.tsx
@@ -34,6 +34,7 @@ interface ObjektMangel {
interface Einsatzbereitschaft {
einsatzbereit: number;
eingeschraenkt_einsatzbereit: number;
+ unbekannt: number;
nicht_einsatzbereit: number;
gesamt: number;
details: ObjektMangel[];
@@ -64,6 +65,7 @@ const GRUND_TEXT: Record = {
mangel_kritisch_offen: "Kritischer Mangel offen",
in_wartung: "In Wartung",
hu_uvv_ueberfaellig: "HU/UVV überfällig",
+ noch_nie_kontrolliert: "Noch nie kontrolliert",
mindermenge_genehmigt: "Mindermenge genehmigt",
};
@@ -126,6 +128,11 @@ export function DashboardPage() {
Mindermengen-Genehmigung)
)}
+ {bereitschaft.unbekannt > 0 && (
+
+ {bereitschaft.unbekannt} noch nie kontrolliert (Status unbekannt)
+
+ )}
{bereitschaft.nicht_einsatzbereit > 0 && (
{bereitschaft.nicht_einsatzbereit} nicht einsatzbereit