From a2a44fb7802f87c176eed0086868f597a72f3a35 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 7 Sep 2026 13:52:43 +0200 Subject: [PATCH] style(dashboard): Kacheln modernisiert - Karten, Einsatzbereitschaft-Balken, Termin-Listen, Skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Card: 14px Radius, weicherer Schatten, Hover-Lift über box-shadow (nicht transform, würde von react-grid-layouts Inline-Positionierung überschrieben). Einsatzbereitschaft-Kachel: große Zahl "X/Y einsatzbereit", proportionaler gestapelter Statusbalken, Vierer-Zeile mit Farbpunkten+Zahl, Detail-Liste bleibt aufklappbar. Prüftermine/Ablaufdaten/Qualifikationsablauf: auf 5 Zeilen begrenzt mit "Alle anzeigen", Status-Badge links (überfällig gefüllt rot, bald fällig Outline amber, FristBadge zeigt nur noch Status), Countdown rechts tabular-nums. Neue --status-ok/warn/unknown/critical CSS-Variablen als Alias auf bestehende Farb-Tokens. Skeleton-Platzhalter statt "Lade…"-Text während des initialen Ladens. Keine API-/Datenänderungen. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- DEVLOG.md | 16 +++++ frontend/src/components/status/FristBadge.tsx | 18 ++--- .../components/status/InspectionStatus.tsx | 4 +- .../src/components/status/InventoryStatus.tsx | 4 +- frontend/src/components/ui/card.tsx | 10 ++- .../src/pages/dashboard/DashboardPage.tsx | 8 ++- .../pages/dashboard/tiles/AblaufdatenTile.tsx | 48 +++++++++---- .../tiles/EinsatzbereitschaftTile.tsx | 70 +++++++++++++------ .../dashboard/tiles/PrueftermineTile.tsx | 48 +++++++++---- .../dashboard/tiles/QualifikationTile.tsx | 45 ++++++++---- frontend/src/styles/global.css | 7 ++ 11 files changed, 204 insertions(+), 74 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 3f385bd..9edb2ad 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -6586,3 +6586,19 @@ Keine Commits in dieser Session. - frontend/src/styles/tailwind.css | 9 ++++++++- --- +## 2026-09-07 13:37 – 13:41 (4m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- 74ed250 feat(dashboard): echtes freies Grid-Layout mit Resize statt Sortable+Größenstufen + +### Geänderte Dateien +- DEVLOG.md | 13 +++++++++++++ +- frontend/package-lock.json | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------- +- frontend/package.json | 4 +--- +- frontend/src/pages/dashboard/DashboardPage.tsx | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------- +- frontend/src/pages/dashboard/TileFrame.tsx | 104 +++++++++++++++++++++++++++++++------------------------------------------------------------------ +- frontend/src/pages/dashboard/layout.ts | 74 +++++++++++++++++++++++++++++++++------------------------------------ + +--- diff --git a/frontend/src/components/status/FristBadge.tsx b/frontend/src/components/status/FristBadge.tsx index c0d7548..514894f 100644 --- a/frontend/src/components/status/FristBadge.tsx +++ b/frontend/src/components/status/FristBadge.tsx @@ -6,26 +6,28 @@ const LABEL: Record = { ok: "In Ordnung", }; +// Überfällig = auffällig gefüllt, bald fällig = zurückhaltender Outline-Look, +// ok = dezente Füllung - Dringlichkeit soll auch ohne Text erkennbar sein. const KLASSE: Record = { - abgelaufen: "badge-danger", - bald_ablaufend: "badge-warning", - ok: "badge-success", + abgelaufen: "bg-[var(--status-critical)] text-white", + bald_ablaufend: "border border-[var(--status-warn)] text-[var(--status-warn)]", + ok: "bg-[var(--status-ok)]/15 text-[var(--status-ok)]", }; interface Props { status: string; - tage?: number; } /** 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) { + * Fristzustände (siehe app/services/dashboard.py `_ablauf_status`). Zeigt nur + * den Status - die verbleibenden Tage rendert die jeweilige Kachel separat + * als eigenes Countdown-Element. */ +export function FristBadge({ status }: 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 index f29b21f..9525b0e 100644 --- a/frontend/src/components/status/InspectionStatus.tsx +++ b/frontend/src/components/status/InspectionStatus.tsx @@ -1,6 +1,6 @@ import { FristBadge } from "./FristBadge"; /** Geräteprüfung-Fristanzeige (Dashboard "Nächste Prüftermine"). */ -export function InspectionStatus({ status, tage }: { status: string; tage?: number }) { - return ; +export function InspectionStatus({ status }: { status: string }) { + return ; } diff --git a/frontend/src/components/status/InventoryStatus.tsx b/frontend/src/components/status/InventoryStatus.tsx index d948a00..d1547cf 100644 --- a/frontend/src/components/status/InventoryStatus.tsx +++ b/frontend/src/components/status/InventoryStatus.tsx @@ -1,6 +1,6 @@ import { FristBadge } from "./FristBadge"; /** Chargen-/Ablaufdatum-Fristanzeige (Dashboard "Bevorstehende Ablaufdaten"). */ -export function InventoryStatus({ status, tage }: { status: string; tage?: number }) { - return ; +export function InventoryStatus({ status }: { status: string }) { + return ; } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index f65a2a9..943055d 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -5,7 +5,15 @@ import { cn } from "../../lib/utils"; export const Card = forwardRef>(({ className, ...props }, ref) => (
)); diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index 388a404..486109c 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -249,7 +249,13 @@ export function DashboardPage() {
)} - {laedt &&

Lade…

} + {laedt && ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ )} {bearbeitenModus && layout.ausgeblendet.length > 0 && (
Ausgeblendet:{" "} diff --git a/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx b/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx index 7c54b64..71eb2c4 100644 --- a/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx +++ b/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx @@ -1,30 +1,52 @@ -import { memo } from "react"; +import { memo, useState } from "react"; import { Link } from "react-router-dom"; import { InventoryStatus } from "../../../components/status/InventoryStatus"; import type { Ablaufdatumeintrag } from "../types"; +const ANZAHL_OHNE_AUFKLAPPEN = 5; + interface Props { ablaufdaten: Ablaufdatumeintrag[]; } function AblaufdatenTileKomponente({ ablaufdaten }: Props) { + const [alleAnzeigen, setAlleAnzeigen] = useState(false); + if (ablaufdaten.length === 0) { - return

Keine bald ablaufenden Chargen.

; + return

Keine bald ablaufenden Chargen.

; } + const sichtbare = alleAnzeigen ? ablaufdaten : ablaufdaten.slice(0, ANZAHL_OHNE_AUFKLAPPEN); + return ( -
    - {ablaufdaten.map((a) => ( -
  • - - {a.objekt_name} – {a.material_name}{" "} - ({a.ablaufdatum}) - - -
  • - ))} -
+ <> +
    + {sichtbare.map((a) => ( +
  • + +
    + + {a.objekt_name} – {a.material_name} + + {a.ablaufdatum} +
    + {a.verbleibende_tage} Tage +
  • + ))} +
+ {ablaufdaten.length > ANZAHL_OHNE_AUFKLAPPEN && ( + + )} + ); } diff --git a/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx b/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx index 4ded672..bd6c34d 100644 --- a/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx +++ b/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx @@ -14,6 +14,12 @@ function readinessVonGruenden(gruende: string[]): Readiness { return "not-ready"; } +interface Segment { + anzahl: number; + farbVar: string; + label: string; +} + interface Props { bereitschaft: Einsatzbereitschaft; } @@ -21,45 +27,69 @@ interface Props { function EinsatzbereitschaftTileKomponente({ bereitschaft }: Props) { const [detailsOffen, setDetailsOffen] = useState(false); + const segmente: Segment[] = [ + { anzahl: bereitschaft.einsatzbereit, farbVar: "var(--status-ok)", label: "Einsatzbereit" }, + { anzahl: bereitschaft.eingeschraenkt_einsatzbereit, farbVar: "var(--status-warn)", label: "Eingeschränkt" }, + { anzahl: bereitschaft.unbekannt, farbVar: "var(--status-unknown)", label: "Unbekannt" }, + { anzahl: bereitschaft.nicht_einsatzbereit, farbVar: "var(--status-critical)", label: "Nicht bereit" }, + ]; + const gesamt = bereitschaft.gesamt || 1; + return ( <> -

- {bereitschaft.einsatzbereit}{" "} - - von {bereitschaft.gesamt} einsatzbereit - +

+ {bereitschaft.einsatzbereit} + /{bereitschaft.gesamt} einsatzbereit

-
- - {bereitschaft.eingeschraenkt_einsatzbereit > 0 && ( - - )} - {bereitschaft.unbekannt > 0 && } - {bereitschaft.nicht_einsatzbereit > 0 && ( - + +
+ {segmente.map( + (s) => + s.anzahl > 0 && ( +
+ ) )}
+ +
+ {segmente.map((s) => ( +
+ + {s.label} + {s.anzahl} +
+ ))} +
+ {bereitschaft.nie_kontrolliert_gesamt > bereitschaft.unbekannt && ( -

+

Davon {bereitschaft.nie_kontrolliert_gesamt} insgesamt noch nie kontrolliert (auch bei Objekten mit weiteren Problemen).

)} + {bereitschaft.details.length > 0 && ( <> - {detailsOffen && ( -
    +
      {bereitschaft.details.map((d) => ( -
    • - +
    • + {d.name} ({d.code}) - + - + {d.gruende.map((g) => GRUND_TEXT[g] ?? g).join(", ")} diff --git a/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx b/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx index fd6df79..4a89102 100644 --- a/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx +++ b/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx @@ -1,30 +1,52 @@ +import { memo, useState } from "react"; import { Link } from "react-router-dom"; -import { memo } from "react"; import { InspectionStatus } from "../../../components/status/InspectionStatus"; import type { Prueftermineintrag } from "../types"; +const ANZAHL_OHNE_AUFKLAPPEN = 5; + interface Props { prueftermine: Prueftermineintrag[]; } function PrueftermineTileKomponente({ prueftermine }: Props) { + const [alleAnzeigen, setAlleAnzeigen] = useState(false); + if (prueftermine.length === 0) { - return

      Keine anstehenden oder überfälligen Geräteprüfungen.

      ; + return

      Keine anstehenden oder überfälligen Geräteprüfungen.

      ; } + const sichtbare = alleAnzeigen ? prueftermine : prueftermine.slice(0, ANZAHL_OHNE_AUFKLAPPEN); + return ( -
        - {prueftermine.map((p) => ( -
      • - - {p.bezeichnung}{" "} - ({p.faelligkeitsdatum}) - - -
      • - ))} -
      + <> +
        + {sichtbare.map((p) => ( +
      • + +
        + + {p.bezeichnung} + + {p.faelligkeitsdatum} +
        + {p.verbleibende_tage} Tage +
      • + ))} +
      + {prueftermine.length > ANZAHL_OHNE_AUFKLAPPEN && ( + + )} + ); } diff --git a/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx b/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx index 6919030..8435316 100644 --- a/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx +++ b/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx @@ -1,29 +1,46 @@ -import { memo } from "react"; +import { memo, useState } from "react"; +import { FristBadge } from "../../../components/status/FristBadge"; import type { Qualifikationsablauf } from "../types"; +const ANZAHL_OHNE_AUFKLAPPEN = 5; + interface Props { qualifikationsablaeufe: Qualifikationsablauf[]; } function QualifikationTileKomponente({ qualifikationsablaeufe }: Props) { + const [alleAnzeigen, setAlleAnzeigen] = useState(false); + if (qualifikationsablaeufe.length === 0) { - return

      Keine bald ablaufenden Qualifikationen.

      ; + return

      Keine bald ablaufenden Qualifikationen.

      ; } + const sichtbare = alleAnzeigen ? qualifikationsablaeufe : qualifikationsablaeufe.slice(0, ANZAHL_OHNE_AUFKLAPPEN); + return ( -
        - {qualifikationsablaeufe.map((q) => ( -
      • - - {q.benutzer_name} ({q.qualifikationstyp_name}) - - - {q.gueltig_bis} ({q.verbleibende_tage} Tage) - -
      • - ))} -
      + <> +
        + {sichtbare.map((q) => ( +
      • + +
        + {q.benutzer_name} + {q.qualifikationstyp_name} +
        + {q.verbleibende_tage} Tage +
      • + ))} +
      + {qualifikationsablaeufe.length > ANZAHL_OHNE_AUFKLAPPEN && ( + + )} + ); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index bfc4a04..ac28cd7 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -29,6 +29,13 @@ --color-not-ready-bg: #fbeaea; --color-unknown: #55606b; --color-unknown-bg: #e9ebee; + /* Dashboard-Redesign: einheitliche Statusfarben-Namen für Kacheln + (ok/warn/unknown/critical), als Alias auf bestehende Tokens - ändern sich + automatisch mit, wenn Light/Dark die --color-ready/... Werte neu setzen. */ + --status-ok: var(--color-ready); + --status-warn: var(--color-limited); + --status-unknown: var(--color-unknown); + --status-critical: var(--color-not-ready); --radius: 8px; --radius-sm: 6px; --shadow-sm: none;