feat(ui): UI-005 begründete Einsatzbereitschaft + Dokumente in der Akte
Backend: objekt_readiness() in dashboard.py liefert Status+Gründe für
ein einzelnes Objekt, Fallunterscheidung aus einsatzbereitschaft()
extrahiert (_kategorie_und_gruende, keine Duplizierung). Neue Felder
an /akte/objekt/{id}. Frontend: ReadinessBadge + Gründeliste ersetzt
den rohen objekt.status-Badge (Objekt-Lebenszyklus-Status bleibt
separat sichtbar). Dokumente waren in der Akte bisher nur Text ohne
Interaktion - jetzt DokumentePanel eingebunden plus neuer "Ansehen"-
Klick (PDF/Bild im neuen Tab statt Zwangs-Download).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -136,7 +136,20 @@ Aktueller Stand vor Redesign: 39 Seiten-/Komponenten-Dateien, ~7.000 Zeilen, ein
|
|||||||
- **Akzeptanzkriterien:** „unbekannt/nie kontrolliert" wird nie als einsatzbereit
|
- **Akzeptanzkriterien:** „unbekannt/nie kontrolliert" wird nie als einsatzbereit
|
||||||
dargestellt (harte Regel, Akzeptanzkriterium testbar).
|
dargestellt (harte Regel, Akzeptanzkriterium testbar).
|
||||||
- **Tests:** Begründungslogik testen (welche Bedingungen führen zu welchem Status).
|
- **Tests:** Begründungslogik testen (welche Bedingungen führen zu welchem Status).
|
||||||
- **DoD:** offen.
|
- **DoD:** umgesetzt (2026-09-06). Backend: `objekt_readiness()` in
|
||||||
|
`app/services/dashboard.py` - Fallunterscheidung aus `einsatzbereitschaft()`
|
||||||
|
in `_kategorie_und_gruende()` extrahiert, keine Duplizierung, für ein
|
||||||
|
einzelnes Objekt statt aller. Neue Felder `einsatzbereitschaft_status`/
|
||||||
|
`einsatzbereitschaft_gruende` an `/akte/objekt/{id}`. Frontend:
|
||||||
|
`ReadinessBadge` + Gründeliste (gemeinsames `GRUND_TEXT`-Mapping mit
|
||||||
|
Dashboard) statt des rohen `objekt.status`-Badges - Objekt-Lebenszyklus-
|
||||||
|
Status (aktiv/ausser_dienst/in_wartung) bleibt separat als Text sichtbar,
|
||||||
|
keine Vermischung. Getestet: kritischer Mangel -> "not-ready", nie
|
||||||
|
kontrolliert ohne sonstige Auffälligkeit -> "unknown" (nie "ready").
|
||||||
|
Nebenbei (Nutzer-Nachfrage): Dokumente in der Akte waren bisher nur Text
|
||||||
|
ohne jede Interaktion - jetzt `DokumentePanel` eingebunden (Upload/Liste/
|
||||||
|
Löschen wie in Mängel/Admin), plus neuer "Ansehen"-Klick (`ladeDokumentAnsehen`
|
||||||
|
öffnet PDF/Bild im neuen Tab) zusätzlich zum bisherigen Zwangs-Download.
|
||||||
|
|
||||||
## UI-006 — Kontrolle & Beladung modernisiert
|
## UI-006 — Kontrolle & Beladung modernisiert
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class AkteRead(BaseModel):
|
|||||||
03_digital_file.md)."""
|
03_digital_file.md)."""
|
||||||
|
|
||||||
objekt: ObjektRead
|
objekt: ObjektRead
|
||||||
|
einsatzbereitschaft_status: str
|
||||||
|
einsatzbereitschaft_gruende: list[str]
|
||||||
objekttyp_name: str
|
objekttyp_name: str
|
||||||
standort_name: str
|
standort_name: str
|
||||||
fahrzeugdetails: FahrzeugdetailsRead | None
|
fahrzeugdetails: FahrzeugdetailsRead | None
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from app.schemas.historie import HistorieRead
|
|||||||
from app.schemas.mangel import MangelRead
|
from app.schemas.mangel import MangelRead
|
||||||
from app.schemas.objekt import FahrzeugdetailsRead, ObjektRead
|
from app.schemas.objekt import FahrzeugdetailsRead, ObjektRead
|
||||||
from app.schemas.zustaendigkeit import KontrollverantwortungRead
|
from app.schemas.zustaendigkeit import KontrollverantwortungRead
|
||||||
|
from app.services.dashboard import objekt_readiness
|
||||||
from app.services.objekte import letzte_kontrollen_map
|
from app.services.objekte import letzte_kontrollen_map
|
||||||
|
|
||||||
|
|
||||||
@@ -89,12 +90,16 @@ async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None:
|
|||||||
|
|
||||||
maengel_result = await db.execute(select(Mangel).where(Mangel.objekt_id == objekt_id))
|
maengel_result = await db.execute(select(Mangel).where(Mangel.objekt_id == objekt_id))
|
||||||
|
|
||||||
|
readiness = await objekt_readiness(db, objekt_id)
|
||||||
|
|
||||||
objekt_read = ObjektRead.model_validate(objekt).model_copy(
|
objekt_read = ObjektRead.model_validate(objekt).model_copy(
|
||||||
update={"letzte_kontrolle_am": letzte_map.get(objekt_id)}
|
update={"letzte_kontrolle_am": letzte_map.get(objekt_id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
return AkteRead(
|
return AkteRead(
|
||||||
objekt=objekt_read,
|
objekt=objekt_read,
|
||||||
|
einsatzbereitschaft_status=readiness["status"],
|
||||||
|
einsatzbereitschaft_gruende=readiness["gruende"],
|
||||||
objekttyp_name=objekttyp.name if objekttyp else "",
|
objekttyp_name=objekttyp.name if objekttyp else "",
|
||||||
standort_name=standort.name if standort else "",
|
standort_name=standort.name if standort else "",
|
||||||
fahrzeugdetails=FahrzeugdetailsRead.model_validate(fahrzeugdetails) if fahrzeugdetails else None,
|
fahrzeugdetails=FahrzeugdetailsRead.model_validate(fahrzeugdetails) if fahrzeugdetails else None,
|
||||||
|
|||||||
@@ -199,37 +199,24 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
bereit = eingeschraenkt = unbekannt = 0
|
bereit = eingeschraenkt = unbekannt = 0
|
||||||
details: list[dict] = []
|
details: list[dict] = []
|
||||||
for objekt in objekte:
|
for objekt in objekte:
|
||||||
gruende: list[str] = []
|
kategorie, gruende = _kategorie_und_gruende(
|
||||||
fehlbestand_ids = objekt_ids_mit_fehlbestand.get(objekt.id, set())
|
objekt,
|
||||||
ungenehmigt = fehlbestand_ids - fehlbestand_ids_genehmigt
|
letzte_kontrolle_map.get(objekt.id),
|
||||||
if ungenehmigt:
|
objekt_ids_mit_fehlbestand.get(objekt.id, set()),
|
||||||
gruende.append("fehlbestand_offen")
|
fehlbestand_ids_genehmigt,
|
||||||
if objekt.id in objekt_ids_abgelaufen:
|
objekt_ids_abgelaufen,
|
||||||
gruende.append("ablaufdatum_abgelaufen")
|
objekt_ids_pruefung_ueberfaellig,
|
||||||
if objekt.id in objekt_ids_pruefung_ueberfaellig:
|
objekt_ids_gesperrt,
|
||||||
gruende.append("pruefung_ueberfaellig")
|
objekt_ids_kritischer_mangel,
|
||||||
if objekt.id in objekt_ids_gesperrt:
|
objekt_ids_hu_uvv_ueberfaellig,
|
||||||
gruende.append("kontrolle_laeuft")
|
)
|
||||||
if objekt.id in objekt_ids_kritischer_mangel:
|
if kategorie == "ready":
|
||||||
gruende.append("mangel_kritisch_offen")
|
|
||||||
if objekt.status == ObjektStatus.in_wartung:
|
|
||||||
gruende.append("in_wartung")
|
|
||||||
if objekt.id in objekt_ids_hu_uvv_ueberfaellig:
|
|
||||||
gruende.append("hu_uvv_ueberfaellig")
|
|
||||||
|
|
||||||
if not gruende and fehlbestand_ids and fehlbestand_ids <= fehlbestand_ids_genehmigt:
|
|
||||||
eingeschraenkt += 1
|
|
||||||
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
|
bereit += 1
|
||||||
else:
|
else:
|
||||||
|
if kategorie == "limited":
|
||||||
|
eingeschraenkt += 1
|
||||||
|
elif kategorie == "unknown":
|
||||||
|
unbekannt += 1
|
||||||
details.append({"objekt_id": objekt.id, "code": objekt.code, "name": objekt.name, "gruende": gruende})
|
details.append({"objekt_id": objekt.id, "code": objekt.code, "name": objekt.name, "gruende": gruende})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -242,6 +229,136 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _kategorie_und_gruende(
|
||||||
|
objekt: Objekt,
|
||||||
|
letzte_kontrolle,
|
||||||
|
fehlbestand_ids: set,
|
||||||
|
fehlbestand_ids_genehmigt: set,
|
||||||
|
objekt_ids_abgelaufen: set,
|
||||||
|
objekt_ids_pruefung_ueberfaellig: set,
|
||||||
|
objekt_ids_gesperrt: set,
|
||||||
|
objekt_ids_kritischer_mangel: set,
|
||||||
|
objekt_ids_hu_uvv_ueberfaellig: set,
|
||||||
|
) -> tuple[str, list[str]]:
|
||||||
|
"""UI-005 (Epic 20): aus der `einsatzbereitschaft()`-Schleife extrahiert, damit
|
||||||
|
`objekt_readiness()` (Begründung auf der Objektakte) dieselbe Logik nutzt statt
|
||||||
|
sie zu duplizieren."""
|
||||||
|
gruende: list[str] = []
|
||||||
|
ungenehmigt = fehlbestand_ids - fehlbestand_ids_genehmigt
|
||||||
|
if ungenehmigt:
|
||||||
|
gruende.append("fehlbestand_offen")
|
||||||
|
if objekt.id in objekt_ids_abgelaufen:
|
||||||
|
gruende.append("ablaufdatum_abgelaufen")
|
||||||
|
if objekt.id in objekt_ids_pruefung_ueberfaellig:
|
||||||
|
gruende.append("pruefung_ueberfaellig")
|
||||||
|
if objekt.id in objekt_ids_gesperrt:
|
||||||
|
gruende.append("kontrolle_laeuft")
|
||||||
|
if objekt.id in objekt_ids_kritischer_mangel:
|
||||||
|
gruende.append("mangel_kritisch_offen")
|
||||||
|
if objekt.status == ObjektStatus.in_wartung:
|
||||||
|
gruende.append("in_wartung")
|
||||||
|
if objekt.id in objekt_ids_hu_uvv_ueberfaellig:
|
||||||
|
gruende.append("hu_uvv_ueberfaellig")
|
||||||
|
|
||||||
|
if not gruende and fehlbestand_ids and fehlbestand_ids <= fehlbestand_ids_genehmigt:
|
||||||
|
return "limited", ["mindermenge_genehmigt"]
|
||||||
|
if not gruende and letzte_kontrolle is None:
|
||||||
|
return "unknown", ["noch_nie_kontrolliert"]
|
||||||
|
if not gruende:
|
||||||
|
return "ready", []
|
||||||
|
return "not-ready", gruende
|
||||||
|
|
||||||
|
|
||||||
|
async def objekt_readiness(db: AsyncSession, objekt_id: int) -> dict:
|
||||||
|
"""UI-005 (Epic 20): begründete Einsatzbereitschaft für EIN Objekt (Objektakte),
|
||||||
|
dieselbe Fachlogik wie `einsatzbereitschaft()` (Dashboard-Aggregat), nur für
|
||||||
|
einen einzelnen Datensatz statt aller Objekte. `ausser_dienst` zählt hier
|
||||||
|
bewusst nicht als eigener Fall - Objektakte zeigt das Objekt so oder so an,
|
||||||
|
der Objekt-Lebenszyklus-Status wird separat dargestellt."""
|
||||||
|
heute = datetime.now(timezone.utc).date()
|
||||||
|
objekt = await db.get(Objekt, objekt_id)
|
||||||
|
if objekt is None:
|
||||||
|
return {"status": "unknown", "gruende": []}
|
||||||
|
|
||||||
|
fehlbestand_ids = set(
|
||||||
|
(
|
||||||
|
await db.execute(
|
||||||
|
select(Fehlbestand.id).where(
|
||||||
|
Fehlbestand.objekt_id == objekt_id, Fehlbestand.status.in_(OFFEN_STATI)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
fehlbestand_ids_genehmigt = set(
|
||||||
|
(
|
||||||
|
await db.execute(
|
||||||
|
select(func.distinct(MindermengenGenehmigung.fehlbestand_id)).where(
|
||||||
|
MindermengenGenehmigung.fehlbestand_id.in_(fehlbestand_ids),
|
||||||
|
MindermengenGenehmigung.status == MindermengeStatus.aktiv,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
hat_ablaufdatum = (
|
||||||
|
await db.execute(
|
||||||
|
select(Objektposition.id).where(
|
||||||
|
Objektposition.objekt_id == objekt_id,
|
||||||
|
Objektposition.ablaufdatum.is_not(None),
|
||||||
|
Objektposition.ablaufdatum < heute,
|
||||||
|
).limit(1)
|
||||||
|
)
|
||||||
|
).first() is not None
|
||||||
|
hat_ueberfaellige_pruefung = (
|
||||||
|
await db.execute(
|
||||||
|
select(GeraetInstanz.id)
|
||||||
|
.join(Objektposition, GeraetInstanz.objektposition_id == Objektposition.id)
|
||||||
|
.where(
|
||||||
|
Objektposition.objekt_id == objekt_id,
|
||||||
|
GeraetInstanz.naechste_pruefung.is_not(None),
|
||||||
|
GeraetInstanz.naechste_pruefung < heute,
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
).first() is not None
|
||||||
|
ist_gesperrt = (
|
||||||
|
await db.execute(
|
||||||
|
select(Kontrolle.id)
|
||||||
|
.where(Kontrolle.objekt_id == objekt_id, Kontrolle.status == KontrollStatus.in_bearbeitung)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
).first() is not None
|
||||||
|
hat_kritischen_mangel = objekt_id in await objekt_ids_mit_kritischem_offenem_mangel(db)
|
||||||
|
hat_ueberfaellige_hu_uvv = (
|
||||||
|
await db.execute(
|
||||||
|
select(Fahrzeugdetails.objekt_id).where(
|
||||||
|
Fahrzeugdetails.objekt_id == objekt_id,
|
||||||
|
(
|
||||||
|
(Fahrzeugdetails.naechste_hu.is_not(None)) & (Fahrzeugdetails.naechste_hu < heute)
|
||||||
|
)
|
||||||
|
| ((Fahrzeugdetails.naechste_uvv.is_not(None)) & (Fahrzeugdetails.naechste_uvv < heute)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first() is not None
|
||||||
|
letzte_kontrolle_map = await letzte_kontrollen_map(db, [objekt_id])
|
||||||
|
|
||||||
|
kategorie, gruende = _kategorie_und_gruende(
|
||||||
|
objekt,
|
||||||
|
letzte_kontrolle_map.get(objekt_id),
|
||||||
|
fehlbestand_ids,
|
||||||
|
fehlbestand_ids_genehmigt,
|
||||||
|
{objekt_id} if hat_ablaufdatum else set(),
|
||||||
|
{objekt_id} if hat_ueberfaellige_pruefung else set(),
|
||||||
|
{objekt_id} if ist_gesperrt else set(),
|
||||||
|
{objekt_id} if hat_kritischen_mangel else set(),
|
||||||
|
{objekt_id} if hat_ueberfaellige_hu_uvv else set(),
|
||||||
|
)
|
||||||
|
return {"status": kategorie, "gruende": gruende}
|
||||||
|
|
||||||
|
|
||||||
async def bevorstehende_prueftermine(db: AsyncSession, warnzeitraum_tage: int = 30) -> list[dict]:
|
async def bevorstehende_prueftermine(db: AsyncSession, warnzeitraum_tage: int = 30) -> list[dict]:
|
||||||
"""Karte-Nachtrag (Leitungs-Dashboard Kachel 2): Geräteprüfungen (Karte 14),
|
"""Karte-Nachtrag (Leitungs-Dashboard Kachel 2): Geräteprüfungen (Karte 14),
|
||||||
nicht Ablaufdaten von Verbrauchsmaterial (bleibt eigene, bestehende Kachel/
|
nicht Ablaufdaten von Verbrauchsmaterial (bleibt eigene, bestehende Kachel/
|
||||||
|
|||||||
@@ -48,6 +48,21 @@ async def test_akte_buendelt_teilbereiche(client, db_session, objekt_mit_positio
|
|||||||
assert body["maengel"][0]["beschreibung"] == "Riss im Gurt"
|
assert body["maengel"][0]["beschreibung"] == "Riss im Gurt"
|
||||||
assert len(body["historie"]) == 1
|
assert len(body["historie"]) == 1
|
||||||
assert body["historie"][0]["entitaet_typ"] == "objekt"
|
assert body["historie"][0]["entitaet_typ"] == "objekt"
|
||||||
|
assert body["einsatzbereitschaft_status"] == "not-ready"
|
||||||
|
assert "mangel_kritisch_offen" in body["einsatzbereitschaft_gruende"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_akte_readiness_unbekannt_bei_nie_kontrolliert(client, objekt_mit_position, admin_user):
|
||||||
|
"""UI-005: ein Objekt ohne jede Auffälligkeit, aber noch nie kontrolliert,
|
||||||
|
gilt als "unbekannt" - NIE als "einsatzbereit" (Akzeptanzkriterium)."""
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
token = await login(client, "admin1")
|
||||||
|
antwort = await client.get(f"/api/v1/akte/objekt/{objekt.id}", headers=auth_header(token))
|
||||||
|
assert antwort.status_code == 200
|
||||||
|
body = antwort.json()
|
||||||
|
assert body["einsatzbereitschaft_status"] == "unknown"
|
||||||
|
assert body["einsatzbereitschaft_status"] != "ready"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -106,6 +106,24 @@ export async function ladeDokumentHerunter(dokumentId: string, dateiname: string
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UI-005 (Epic 20): Dokumente waren bisher nur per Zwangs-Download einsehbar,
|
||||||
|
// nicht direkt im Browser anschaubar (PDF/Bild in neuem Tab statt Datei
|
||||||
|
// speichern). Auch hier: Auth-Header nötig, daher Blob statt einfachem
|
||||||
|
// <a href> auf den Download-Endpunkt. Object-URL wird bewusst NICHT sofort
|
||||||
|
// revoked (Tab braucht sie noch); der Browser räumt sie beim Tab-Schließen auf.
|
||||||
|
export async function ladeDokumentAnsehen(dokumentId: string): Promise<void> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
||||||
|
|
||||||
|
const response = await fetch(`${BASE_URL}/dokumente/${dokumentId}/download`, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(response.status, null);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
window.open(url, "_blank", "noopener");
|
||||||
|
}
|
||||||
|
|
||||||
export async function login(username: string, password: string): Promise<string> {
|
export async function login(username: string, password: string): Promise<string> {
|
||||||
const body = new URLSearchParams({ username, password });
|
const body = new URLSearchParams({ username, password });
|
||||||
const response = await fetch(`${BASE_URL}/auth/login`, {
|
const response = await fetch(`${BASE_URL}/auth/login`, {
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ export interface ZustaendigerBenutzer {
|
|||||||
|
|
||||||
export interface Akte {
|
export interface Akte {
|
||||||
objekt: Objekt;
|
objekt: Objekt;
|
||||||
|
einsatzbereitschaft_status: "ready" | "limited" | "not-ready" | "unknown";
|
||||||
|
einsatzbereitschaft_gruende: string[];
|
||||||
objekttyp_name: string;
|
objekttyp_name: string;
|
||||||
standort_name: string;
|
standort_name: string;
|
||||||
fahrzeugdetails: Fahrzeugdetails | null;
|
fahrzeugdetails: Fahrzeugdetails | null;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { apiRequest, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
import { apiRequest, ladeDokumentAnsehen, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import type { Dokument } from "../api/types";
|
import type { Dokument } from "../api/types";
|
||||||
|
|
||||||
@@ -63,6 +63,14 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ansehen(d: Dokument) {
|
||||||
|
try {
|
||||||
|
await ladeDokumentAnsehen(d.id);
|
||||||
|
} catch {
|
||||||
|
onFehler("Datei konnte nicht geöffnet werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function herunterladen(d: Dokument) {
|
async function herunterladen(d: Dokument) {
|
||||||
try {
|
try {
|
||||||
await ladeDokumentHerunter(d.id, d.dateiname);
|
await ladeDokumentHerunter(d.id, d.dateiname);
|
||||||
@@ -100,7 +108,8 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
|||||||
<li key={d.id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
<li key={d.id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||||
<span>
|
<span>
|
||||||
<button
|
<button
|
||||||
onClick={() => herunterladen(d)}
|
onClick={() => ansehen(d)}
|
||||||
|
title="In neuem Tab ansehen"
|
||||||
style={{
|
style={{
|
||||||
background: "none",
|
background: "none",
|
||||||
border: "none",
|
border: "none",
|
||||||
@@ -115,11 +124,16 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
|||||||
{d.beschreibung && <span className="text-muted"> – {d.beschreibung}</span>}
|
{d.beschreibung && <span className="text-muted"> – {d.beschreibung}</span>}
|
||||||
<span className="text-muted"> ({formatGroesse(d.groesse_bytes)})</span>
|
<span className="text-muted"> ({formatGroesse(d.groesse_bytes)})</span>
|
||||||
</span>
|
</span>
|
||||||
{darfLoeschen && (
|
<span className="row" style={{ gap: "0.4rem" }}>
|
||||||
<button className="btn btn-secondary" onClick={() => loeschen(d)}>
|
<button className="btn btn-secondary" onClick={() => herunterladen(d)}>
|
||||||
Löschen
|
Herunterladen
|
||||||
</button>
|
</button>
|
||||||
)}
|
{darfLoeschen && (
|
||||||
|
<button className="btn btn-secondary" onClick={() => loeschen(d)}>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/** Spiegelt die Grund-Codes aus app/services/dashboard.py (einsatzbereitschaft/
|
||||||
|
* objekt_readiness) - gemeinsam genutzt von DashboardPage und AktePage. */
|
||||||
|
export const GRUND_TEXT: Record<string, string> = {
|
||||||
|
fehlbestand_offen: "Offener Fehlbestand",
|
||||||
|
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
|
||||||
|
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
||||||
|
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
||||||
|
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",
|
||||||
|
};
|
||||||
@@ -3,6 +3,9 @@ import { Link, useParams } from "react-router-dom";
|
|||||||
|
|
||||||
import { apiRequest } from "../api/client";
|
import { apiRequest } from "../api/client";
|
||||||
import type { Akte } from "../api/types";
|
import type { Akte } from "../api/types";
|
||||||
|
import { DokumentePanel } from "../components/DokumentePanel";
|
||||||
|
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||||
|
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||||
|
|
||||||
const MANGEL_PRIORITAET_LABEL: Record<string, string> = {
|
const MANGEL_PRIORITAET_LABEL: Record<string, string> = {
|
||||||
niedrig: "Niedrig",
|
niedrig: "Niedrig",
|
||||||
@@ -39,11 +42,20 @@ export function AktePage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 style={{ marginBottom: "0.15rem" }}>{objekt.name}</h1>
|
<h1 style={{ marginBottom: "0.15rem" }}>{objekt.name}</h1>
|
||||||
<div className="text-muted">
|
<div className="text-muted">
|
||||||
{objekt.code} · {akte.objekttyp_name}
|
{objekt.code} · {akte.objekttyp_name} · Objekt-Status: {objekt.status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="badge badge-neutral">{objekt.status}</span>
|
<ReadinessBadge status={akte.einsatzbereitschaft_status} />
|
||||||
</div>
|
</div>
|
||||||
|
{akte.einsatzbereitschaft_gruende.length > 0 && (
|
||||||
|
<ul className="card-list" style={{ marginTop: "0.5rem" }}>
|
||||||
|
{akte.einsatzbereitschaft_gruende.map((g) => (
|
||||||
|
<li key={g} className="text-muted" style={{ fontSize: "0.9rem" }}>
|
||||||
|
• {GRUND_TEXT[g] ?? g}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="card" style={{ marginTop: "1rem" }}>
|
<div className="card" style={{ marginTop: "1rem" }}>
|
||||||
<h3 style={{ marginTop: 0 }}>Stammdaten</h3>
|
<h3 style={{ marginTop: 0 }}>Stammdaten</h3>
|
||||||
@@ -118,16 +130,7 @@ export function AktePage() {
|
|||||||
|
|
||||||
<div className="card" style={{ marginTop: "1rem" }}>
|
<div className="card" style={{ marginTop: "1rem" }}>
|
||||||
<h3 style={{ marginTop: 0 }}>Dokumente</h3>
|
<h3 style={{ marginTop: 0 }}>Dokumente</h3>
|
||||||
{akte.dokumente.length === 0 && <p className="text-muted">Keine Dokumente hinterlegt.</p>}
|
<DokumentePanel entitaetTyp="objekt" entitaetId={String(objekt.id)} onFehler={setFehler} />
|
||||||
{akte.dokumente.length > 0 && (
|
|
||||||
<ul>
|
|
||||||
{akte.dokumente.map((d) => (
|
|
||||||
<li key={d.id}>
|
|
||||||
{d.dateiname} <span className="text-muted">({Math.round(d.groesse_bytes / 1024)} KB)</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card" style={{ marginTop: "1rem" }}>
|
<div className="card" style={{ marginTop: "1rem" }}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { InspectionStatus } from "../components/status/InspectionStatus";
|
|||||||
import { InventoryStatus } from "../components/status/InventoryStatus";
|
import { InventoryStatus } from "../components/status/InventoryStatus";
|
||||||
import type { Readiness } from "../components/status/ReadinessBadge";
|
import type { Readiness } from "../components/status/ReadinessBadge";
|
||||||
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||||
|
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||||
|
|
||||||
interface Kennzahlen {
|
interface Kennzahlen {
|
||||||
anzahl_offener_fehlbestaende: number;
|
anzahl_offener_fehlbestaende: number;
|
||||||
@@ -69,18 +70,6 @@ function readinessVonGruenden(gruende: string[]): Readiness {
|
|||||||
return "not-ready";
|
return "not-ready";
|
||||||
}
|
}
|
||||||
|
|
||||||
const GRUND_TEXT: Record<string, string> = {
|
|
||||||
fehlbestand_offen: "Offener Fehlbestand",
|
|
||||||
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
|
|
||||||
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
|
||||||
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Leitungs-Dashboard (Karte-Nachtrag): zwei neue Kacheln (Einsatzbereitschaft,
|
// Leitungs-Dashboard (Karte-Nachtrag): zwei neue Kacheln (Einsatzbereitschaft,
|
||||||
// Prüftermine) plus die bestehende Ablaufdaten-Kachel, alle drei eigenständig -
|
// Prüftermine) plus die bestehende Ablaufdaten-Kachel, alle drei eigenständig -
|
||||||
// widersprechen sich nicht, da überfällige Prüfung/Ablauf beide zusätzlich als
|
// widersprechen sich nicht, da überfällige Prüfung/Ablauf beide zusätzlich als
|
||||||
|
|||||||
Reference in New Issue
Block a user