From 79ddb37a36d46fac588383f7213c67bb93b60473 Mon Sep 17 00:00:00 2001 From: patrick Date: Sun, 6 Sep 2026 13:30:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20UI-003=20zentrale=20Statuskomponent?= =?UTF-8?q?en=20+=20SyncStatus-L=C3=BCcke=20geschlossen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReadinessBadge (ready/limited/not-ready/unknown, "unbekannt" nie wie "einsatzbereit"), FristBadge als Basis für InspectionStatus/ InventoryStatus, SyncStatus neu in AppShell verdrahtet - Online/ Offline/Sync-Queue-Status war zuvor nirgends sichtbar, obwohl die Offline-Infrastruktur seit Prompt 17 existiert. Dashboard nutzt die neuen Komponenten statt Ad-hoc-Inline-Farben. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- arbeitskacheln/20_ui_redesign.md | 13 +++- frontend/src/components/AppShell.tsx | 2 + frontend/src/components/status/FristBadge.tsx | 31 ++++++++++ .../components/status/InspectionStatus.tsx | 6 ++ .../src/components/status/InventoryStatus.tsx | 6 ++ .../src/components/status/ReadinessBadge.tsx | 40 ++++++++++++ frontend/src/components/status/SyncStatus.tsx | 44 +++++++++++++ frontend/src/pages/DashboardPage.tsx | 61 +++++++++++-------- 8 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/status/FristBadge.tsx create mode 100644 frontend/src/components/status/InspectionStatus.tsx create mode 100644 frontend/src/components/status/InventoryStatus.tsx create mode 100644 frontend/src/components/status/ReadinessBadge.tsx create mode 100644 frontend/src/components/status/SyncStatus.tsx diff --git a/arbeitskacheln/20_ui_redesign.md b/arbeitskacheln/20_ui_redesign.md index 233ae2c..485c5e0 100644 --- a/arbeitskacheln/20_ui_redesign.md +++ b/arbeitskacheln/20_ui_redesign.md @@ -75,7 +75,18 @@ Aktueller Stand vor Redesign: 39 Seiten-/Komponenten-Dateien, ~7.000 Zeilen, ein - **Akzeptanzkriterien:** kein Status mehr als Inline-`` dupliziert. - **Tests:** ggf. kleiner Komponententest je Status-Mapping. -- **DoD:** offen. +- **DoD:** umgesetzt (2026-09-06). `components/status/ReadinessBadge.tsx` + (ready/limited/not-ready/unknown, eigene Farben/Icons, "unbekannt" sieht nie + wie "einsatzbereit" aus), `FristBadge.tsx` als gemeinsame Basis für + `InspectionStatus.tsx`/`InventoryStatus.tsx` (beide Backend-Endpunkte liefern + dieselben zwei Fristzustände), `SyncStatus.tsx` neu in `AppShell` verdrahtet + - Online/Offline/Queue-Länge war zuvor NIRGENDS sichtbar, obwohl + `useOnlineStatus`/Offline-Queue seit Prompt 17 existieren. In `DashboardPage` + eingesetzt (Einsatzbereitschaft-Chips, Prüftermine, Ablaufdaten). + `objekt.status` (aktiv/ausser_dienst/in_wartung, ObjektListPage/AktePage) + bewusst NICHT auf ReadinessBadge umgestellt - anderes Konzept + (Objekt-Lebenszyklus statt Einsatzbereitschaft), Vermischung wäre fachlich + falsch. Echte Einsatzbereitschaft pro Objekt (mit Begründung) kommt in UI-005. ## UI-004 — Dashboard neu diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 69b9288..31b4739 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -3,6 +3,7 @@ import { NavLink, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/AuthContext"; import { CommandPalette } from "./CommandPalette"; +import { SyncStatus } from "./status/SyncStatus"; import { ThemeToggle } from "./ThemeToggle"; function paletteOeffnen() { @@ -47,6 +48,7 @@ export function AppShell({ children }: { children: ReactNode }) { {istAdmin ? "Administration" : "Verwaltung"} )} + diff --git a/frontend/src/components/status/FristBadge.tsx b/frontend/src/components/status/FristBadge.tsx new file mode 100644 index 0000000..2080742 --- /dev/null +++ b/frontend/src/components/status/FristBadge.tsx @@ -0,0 +1,31 @@ +export type FristStatus = "abgelaufen" | "bald_ablaufend" | "ok"; + +const LABEL: Record = { + abgelaufen: "Überfällig", + bald_ablaufend: "Bald fällig", + ok: "In Ordnung", +}; + +const KLASSE: Record = { + abgelaufen: "badge-danger", + bald_ablaufend: "badge-warning", + ok: "badge-success", +}; + +interface Props { + status: string; + tage?: number; +} + +/** UI-003 (Epic 20): gemeinsame Basis für InspectionStatus/InventoryStatus - + * beide Backend-Endpunkte (Prüftermine/Ablaufdaten) liefern dieselben zwei + * Fristzustände (siehe app/services/dashboard.py `_ablauf_status`). */ +export function FristBadge({ status, tage }: Props) { + const bekannt = status === "abgelaufen" || status === "bald_ablaufend" ? status : "ok"; + return ( + + {LABEL[bekannt]} + {tage !== undefined && ` (${tage} Tage)`} + + ); +} diff --git a/frontend/src/components/status/InspectionStatus.tsx b/frontend/src/components/status/InspectionStatus.tsx new file mode 100644 index 0000000..ead2bbd --- /dev/null +++ b/frontend/src/components/status/InspectionStatus.tsx @@ -0,0 +1,6 @@ +import { FristBadge } from "./FristBadge"; + +/** UI-003 (Epic 20): Geräteprüfung-Fristanzeige (Dashboard "Nächste Prüftermine"). */ +export function InspectionStatus({ status, tage }: { status: string; tage?: number }) { + return ; +} diff --git a/frontend/src/components/status/InventoryStatus.tsx b/frontend/src/components/status/InventoryStatus.tsx new file mode 100644 index 0000000..365c64f --- /dev/null +++ b/frontend/src/components/status/InventoryStatus.tsx @@ -0,0 +1,6 @@ +import { FristBadge } from "./FristBadge"; + +/** UI-003 (Epic 20): Chargen-/Ablaufdatum-Fristanzeige (Dashboard "Bevorstehende Ablaufdaten"). */ +export function InventoryStatus({ status, tage }: { status: string; tage?: number }) { + return ; +} diff --git a/frontend/src/components/status/ReadinessBadge.tsx b/frontend/src/components/status/ReadinessBadge.tsx new file mode 100644 index 0000000..309e7cb --- /dev/null +++ b/frontend/src/components/status/ReadinessBadge.tsx @@ -0,0 +1,40 @@ +export type Readiness = "ready" | "limited" | "not-ready" | "unknown"; + +const LABEL: Record = { + ready: "Einsatzbereit", + limited: "Eingeschränkt einsatzbereit", + "not-ready": "Nicht einsatzbereit", + unknown: "Unbekannt", +}; + +const ICON: Record = { + ready: "🟢", + limited: "🟡", + "not-ready": "🔴", + unknown: "⚪", +}; + +const KLASSE: Record = { + ready: "badge-ready", + limited: "badge-limited", + "not-ready": "badge-not-ready", + unknown: "badge-unknown", +}; + +interface Props { + status: Readiness; + label?: string; +} + +/** + * UI-003 (Epic 20): einzige Stelle, die Einsatzbereitschaft farblich/textlich + * darstellt - "unbekannt" (noch nie kontrolliert) sieht bewusst NIE wie + * "einsatzbereit" aus (eigene Farbe/Icon, siehe global.css badge-unknown). + */ +export function ReadinessBadge({ status, label }: Props) { + return ( + + {ICON[status]} {label ?? LABEL[status]} + + ); +} diff --git a/frontend/src/components/status/SyncStatus.tsx b/frontend/src/components/status/SyncStatus.tsx new file mode 100644 index 0000000..d015555 --- /dev/null +++ b/frontend/src/components/status/SyncStatus.tsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from "react"; + +import { alleEintraege } from "../../offline/queue"; +import { useOnlineStatus } from "../../offline/useOnlineStatus"; + +/** + * UI-003 (Epic 20): Online-/Offline-/Sync-Status war bisher nirgends sichtbar + * (useOnlineStatus/Queue existierten nur intern für den Kontroll-Workflow) - + * echte Lücke laut Redesign-Vorgabe, kein reiner Umbau. + */ +export function SyncStatus() { + const online = useOnlineStatus(); + const [wartend, setWartend] = useState(0); + + useEffect(() => { + let aktiv = true; + async function pruefen() { + const eintraege = await alleEintraege(); + if (aktiv) setWartend(eintraege.filter((e) => e.status !== "fehler").length); + } + void pruefen(); + const interval = window.setInterval(pruefen, 5000); + return () => { + aktiv = false; + window.clearInterval(interval); + }; + }, []); + + if (online && wartend === 0) { + return 🟢 Online; + } + if (online && wartend > 0) { + return ( + + 🟡 Synchronisiere ({wartend}) + + ); + } + return ( + 0 ? `${wartend} Eintrag/Einträge warten auf Verbindung` : "Keine Verbindung"}> + 🔴 Offline{wartend > 0 ? ` (${wartend} wartend)` : ""} + + ); +} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 666095d..f24e23a 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -3,6 +3,10 @@ import { Link } from "react-router-dom"; import { apiRequest } from "../api/client"; import type { Mangel } from "../api/types"; +import { InspectionStatus } from "../components/status/InspectionStatus"; +import { InventoryStatus } from "../components/status/InventoryStatus"; +import type { Readiness } from "../components/status/ReadinessBadge"; +import { ReadinessBadge } from "../components/status/ReadinessBadge"; interface Kennzahlen { anzahl_offener_fehlbestaende: number; @@ -57,6 +61,14 @@ interface Ablaufdatumeintrag { status: string; } +// Spiegelt die Fallunterscheidung in app/services/dashboard.py::einsatzbereitschaft - +// welche Kombination von Gründen zu welcher Kategorie führt. +function readinessVonGruenden(gruende: string[]): Readiness { + if (gruende.length === 1 && gruende[0] === "mindermenge_genehmigt") return "limited"; + if (gruende.length === 1 && gruende[0] === "noch_nie_kontrolliert") return "unknown"; + return "not-ready"; +} + const GRUND_TEXT: Record = { fehlbestand_offen: "Offener Fehlbestand", ablaufdatum_abgelaufen: "Ablaufdatum überschritten", @@ -118,26 +130,22 @@ export function DashboardPage() { {!laedt && bereitschaft && (
-

Einsatzbereites Material

+

Einsatzbereitschaft

{bereitschaft.einsatzbereit} von {bereitschaft.gesamt} einsatzbereit

- {bereitschaft.eingeschraenkt_einsatzbereit > 0 && ( -

- {bereitschaft.eingeschraenkt_einsatzbereit} eingeschränkt einsatzbereit (aktive - Mindermengen-Genehmigung) -

- )} - {bereitschaft.unbekannt > 0 && ( -

- {bereitschaft.unbekannt} noch nie kontrolliert (Status unbekannt) -

- )} - {bereitschaft.nicht_einsatzbereit > 0 && ( -

- {bereitschaft.nicht_einsatzbereit} nicht einsatzbereit -

- )} +
+ + {bereitschaft.eingeschraenkt_einsatzbereit > 0 && ( + + )} + {bereitschaft.unbekannt > 0 && ( + + )} + {bereitschaft.nicht_einsatzbereit > 0 && ( + + )} +
{bereitschaft.details.length > 0 && ( <>