feat(ui): UI-003 zentrale Statuskomponenten + SyncStatus-Lücke geschlossen
CI / backend-tests (push) Successful in 2m3s
CI / frontend-build (push) Successful in 19s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-06 13:30:45 +02:00
co-authored by Claude Sonnet 5
parent b6ada2b5a1
commit 79ddb37a36
8 changed files with 177 additions and 26 deletions
+36 -25
View File
@@ -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<string, string> = {
fehlbestand_offen: "Offener Fehlbestand",
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
@@ -118,26 +130,22 @@ export function DashboardPage() {
{!laedt && bereitschaft && (
<div className="card-grid">
<div className="card">
<h3 style={{ marginTop: 0 }}>Einsatzbereites Material</h3>
<h3 style={{ marginTop: 0 }}>Einsatzbereitschaft</h3>
<p className="stat-number">
{bereitschaft.einsatzbereit} <span style={{ fontSize: "1rem", fontWeight: 600, color: "var(--color-text-muted)" }}>von {bereitschaft.gesamt} einsatzbereit</span>
</p>
{bereitschaft.eingeschraenkt_einsatzbereit > 0 && (
<p className="text-muted" style={{ color: "var(--color-warning)" }}>
{bereitschaft.eingeschraenkt_einsatzbereit} eingeschränkt einsatzbereit (aktive
Mindermengen-Genehmigung)
</p>
)}
{bereitschaft.unbekannt > 0 && (
<p className="text-muted">
{bereitschaft.unbekannt} noch nie kontrolliert (Status unbekannt)
</p>
)}
{bereitschaft.nicht_einsatzbereit > 0 && (
<p className="text-muted" style={{ color: "var(--color-danger)" }}>
{bereitschaft.nicht_einsatzbereit} nicht einsatzbereit
</p>
)}
<div className="row" style={{ flexWrap: "wrap", marginBottom: "0.5rem" }}>
<ReadinessBadge status="ready" label={`${bereitschaft.einsatzbereit} einsatzbereit`} />
{bereitschaft.eingeschraenkt_einsatzbereit > 0 && (
<ReadinessBadge status="limited" label={`${bereitschaft.eingeschraenkt_einsatzbereit} eingeschränkt`} />
)}
{bereitschaft.unbekannt > 0 && (
<ReadinessBadge status="unknown" label={`${bereitschaft.unbekannt} unbekannt`} />
)}
{bereitschaft.nicht_einsatzbereit > 0 && (
<ReadinessBadge status="not-ready" label={`${bereitschaft.nicht_einsatzbereit} nicht bereit`} />
)}
</div>
{bereitschaft.details.length > 0 && (
<>
<button className="btn btn-secondary" onClick={() => setDetailsOffen(!detailsOffen)}>
@@ -150,8 +158,11 @@ export function DashboardPage() {
<Link to={`/objekte/${d.objekt_id}/kontrolle`}>
{d.name} ({d.code})
</Link>
<span className="text-muted">
{d.gruende.map((g) => GRUND_TEXT[g] ?? g).join(", ")}
<span className="row" style={{ gap: "0.4rem" }}>
<ReadinessBadge status={readinessVonGruenden(d.gruende)} />
<span className="text-muted" style={{ fontSize: "0.85rem" }}>
{d.gruende.map((g) => GRUND_TEXT[g] ?? g).join(", ")}
</span>
</span>
</li>
))}
@@ -169,10 +180,10 @@ export function DashboardPage() {
<ul className="card-list">
{prueftermine.map((p, i) => (
<li key={i} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>{p.bezeichnung}</span>
<span style={{ color: p.status === "abgelaufen" ? "var(--color-danger)" : undefined }}>
{p.faelligkeitsdatum} ({p.verbleibende_tage} Tage)
<span>
{p.bezeichnung} <span className="text-muted">({p.faelligkeitsdatum})</span>
</span>
<InspectionStatus status={p.status} tage={p.verbleibende_tage} />
</li>
))}
</ul>
@@ -187,10 +198,10 @@ export function DashboardPage() {
<ul className="card-list">
{ablaufdaten.map((a) => (
<li key={`${a.objekt_id}-${a.material_id}`} className="row-between" style={{ padding: "0.3rem 0" }}>
<span>Objekt #{a.objekt_id} Material #{a.material_id}</span>
<span className="text-muted" style={{ color: a.status === "abgelaufen" ? "var(--color-danger)" : undefined }}>
{a.ablaufdatum} ({a.verbleibende_tage} Tage)
<span>
Objekt #{a.objekt_id} Material #{a.material_id} <span className="text-muted">({a.ablaufdatum})</span>
</span>
<InventoryStatus status={a.status} tage={a.verbleibende_tage} />
</li>
))}
</ul>