fix(dashboard): nie kontrollierte Objekte gelten nicht mehr als einsatzbereit
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -4358,3 +4358,33 @@ Keine Commits in dieser Session.
|
|||||||
- arbeitskacheln/11_defects.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
- 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 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class ObjektMangelRead(BaseModel):
|
|||||||
class EinsatzbereitschaftRead(BaseModel):
|
class EinsatzbereitschaftRead(BaseModel):
|
||||||
einsatzbereit: int
|
einsatzbereit: int
|
||||||
eingeschraenkt_einsatzbereit: int
|
eingeschraenkt_einsatzbereit: int
|
||||||
|
unbekannt: int
|
||||||
nicht_einsatzbereit: int
|
nicht_einsatzbereit: int
|
||||||
gesamt: int
|
gesamt: int
|
||||||
details: list[ObjektMangelRead]
|
details: list[ObjektMangelRead]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from app.models.objektposition import Objektposition
|
|||||||
from app.models.personal import BenutzerQualifikation, Qualifikationstyp
|
from app.models.personal import BenutzerQualifikation, Qualifikationstyp
|
||||||
from app.models.stammdaten import Material
|
from app.models.stammdaten import Material
|
||||||
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
|
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"
|
# Prompt 12.2: offen/in_bearbeitung/nachgefuellt_teilweise zählen gemeinsam als "offen"
|
||||||
# (technischer Substatus ist keine Erledigung, Prompt 03/20).
|
# (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)
|
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(
|
objekt_ids_hu_uvv_ueberfaellig = set(
|
||||||
(
|
(
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -190,7 +196,7 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
bereit = eingeschraenkt = 0
|
bereit = eingeschraenkt = unbekannt = 0
|
||||||
details: list[dict] = []
|
details: list[dict] = []
|
||||||
for objekt in objekte:
|
for objekt in objekte:
|
||||||
gruende: list[str] = []
|
gruende: list[str] = []
|
||||||
@@ -216,6 +222,11 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
details.append(
|
details.append(
|
||||||
{"objekt_id": objekt.id, "code": objekt.code, "name": objekt.name, "gruende": ["mindermenge_genehmigt"]}
|
{"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:
|
elif not gruende:
|
||||||
bereit += 1
|
bereit += 1
|
||||||
else:
|
else:
|
||||||
@@ -224,7 +235,8 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
return {
|
return {
|
||||||
"einsatzbereit": bereit,
|
"einsatzbereit": bereit,
|
||||||
"eingeschraenkt_einsatzbereit": eingeschraenkt,
|
"eingeschraenkt_einsatzbereit": eingeschraenkt,
|
||||||
"nicht_einsatzbereit": len(objekte) - bereit - eingeschraenkt,
|
"unbekannt": unbekannt,
|
||||||
|
"nicht_einsatzbereit": len(objekte) - bereit - eingeschraenkt - unbekannt,
|
||||||
"gesamt": len(objekte),
|
"gesamt": len(objekte),
|
||||||
"details": details,
|
"details": details,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,19 +153,41 @@ async def test_abgelaufenes_material_wird_als_abgelaufen_markiert(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_objekt_ohne_maengel_ist_einsatzbereit(
|
async def test_objekt_ohne_maengel_und_kontrolliert_ist_einsatzbereit(
|
||||||
client, objekt_mit_position, materialverantwortlicher_user
|
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")
|
token = await login(client, "materialverantwortlicher1")
|
||||||
response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
|
response = await client.get("/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(token))
|
||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["einsatzbereit"] == 1
|
assert body["einsatzbereit"] == 1
|
||||||
assert body["eingeschraenkt_einsatzbereit"] == 0
|
assert body["eingeschraenkt_einsatzbereit"] == 0
|
||||||
|
assert body["unbekannt"] == 0
|
||||||
assert body["nicht_einsatzbereit"] == 0
|
assert body["nicht_einsatzbereit"] == 0
|
||||||
assert body["details"] == []
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_offener_fehlbestand_macht_objekt_nicht_einsatzbereit(
|
async def test_offener_fehlbestand_macht_objekt_nicht_einsatzbereit(
|
||||||
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
|
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
|
||||||
|
|||||||
@@ -100,8 +100,22 @@ async def test_kritischer_offener_mangel_macht_objekt_nicht_einsatzbereit(
|
|||||||
async def test_niedrige_prioritaet_blockiert_einsatzbereitschaft_nicht(
|
async def test_niedrige_prioritaet_blockiert_einsatzbereitschaft_nicht(
|
||||||
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
|
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
|
||||||
):
|
):
|
||||||
objekt, _material = objekt_mit_position
|
objekt, material = objekt_mit_position
|
||||||
mitarbeiter_token = await login(client, "mitarbeiter1")
|
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(
|
await client.post(
|
||||||
"/api/v1/maengel",
|
"/api/v1/maengel",
|
||||||
json={"objekt_id": objekt.id, "beschreibung": "Kratzer im Lack", "prioritaet": "niedrig"},
|
json={"objekt_id": objekt.id, "beschreibung": "Kratzer im Lack", "prioritaet": "niedrig"},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface ObjektMangel {
|
|||||||
interface Einsatzbereitschaft {
|
interface Einsatzbereitschaft {
|
||||||
einsatzbereit: number;
|
einsatzbereit: number;
|
||||||
eingeschraenkt_einsatzbereit: number;
|
eingeschraenkt_einsatzbereit: number;
|
||||||
|
unbekannt: number;
|
||||||
nicht_einsatzbereit: number;
|
nicht_einsatzbereit: number;
|
||||||
gesamt: number;
|
gesamt: number;
|
||||||
details: ObjektMangel[];
|
details: ObjektMangel[];
|
||||||
@@ -64,6 +65,7 @@ const GRUND_TEXT: Record<string, string> = {
|
|||||||
mangel_kritisch_offen: "Kritischer Mangel offen",
|
mangel_kritisch_offen: "Kritischer Mangel offen",
|
||||||
in_wartung: "In Wartung",
|
in_wartung: "In Wartung",
|
||||||
hu_uvv_ueberfaellig: "HU/UVV überfällig",
|
hu_uvv_ueberfaellig: "HU/UVV überfällig",
|
||||||
|
noch_nie_kontrolliert: "Noch nie kontrolliert",
|
||||||
mindermenge_genehmigt: "Mindermenge genehmigt",
|
mindermenge_genehmigt: "Mindermenge genehmigt",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,6 +128,11 @@ export function DashboardPage() {
|
|||||||
Mindermengen-Genehmigung)
|
Mindermengen-Genehmigung)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{bereitschaft.unbekannt > 0 && (
|
||||||
|
<p className="text-muted">
|
||||||
|
{bereitschaft.unbekannt} noch nie kontrolliert (Status unbekannt)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{bereitschaft.nicht_einsatzbereit > 0 && (
|
{bereitschaft.nicht_einsatzbereit > 0 && (
|
||||||
<p className="text-muted" style={{ color: "var(--color-danger)" }}>
|
<p className="text-muted" style={{ color: "var(--color-danger)" }}>
|
||||||
{bereitschaft.nicht_einsatzbereit} nicht einsatzbereit
|
{bereitschaft.nicht_einsatzbereit} nicht einsatzbereit
|
||||||
|
|||||||
Reference in New Issue
Block a user