From 0c448c7ff766017cd0b87fd87b0d78fabbbed684 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 7 Sep 2026 13:27:36 +0200 Subject: [PATCH] perf(dashboard): React-Review - Memoization, stabile Callbacks, Key-Fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React.memo auf allen 8 Kachel-Komponenten + TileFrame, damit Interaktionen (Anpassen/Monitor/Größe/Ausblenden) nicht mehr alle Kacheln unnötig neu rendern. Voraussetzung dafür: onAusblendenUmschalten/onGroesseAendern als stabile useCallback-Referenzen mit id-Parameter statt pro Kachel neu erzeugter Closures. Die vier Layout-Mutationsfunktionen (dragEnde/ausblendenUmschalten/groesseAendern/layoutZuruecksetzen) über eine gemeinsame aktualisiereLayout-Helper-Funktion mit funktionalem setState dedupliziert (vermeidet stale-closure-Risiko bei schnellen Interaktionen). Instabile Index-Keys in PrueftermineTile/AufgabenTile durch stabile Keys ersetzt. maengelOffenListe in MaengelTile mit useMemo. Fehler-Anzeige bekommt "Erneut versuchen"-Button. Drag-Griff in TileFrame: type="button" + aria-roledescription für Screenreader. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- DEVLOG.md | 114 ++++++++++++++++++ .../src/pages/dashboard/DashboardPage.tsx | 86 ++++++++----- frontend/src/pages/dashboard/TileFrame.tsx | 21 +++- .../pages/dashboard/tiles/AblaufdatenTile.tsx | 5 +- .../pages/dashboard/tiles/AktivitaetTile.tsx | 5 +- .../pages/dashboard/tiles/AufgabenTile.tsx | 9 +- .../tiles/EinsatzbereitschaftTile.tsx | 6 +- .../pages/dashboard/tiles/KennzahlenTile.tsx | 6 +- .../src/pages/dashboard/tiles/MaengelTile.tsx | 8 +- .../dashboard/tiles/PrueftermineTile.tsx | 9 +- .../dashboard/tiles/QualifikationTile.tsx | 6 +- 11 files changed, 223 insertions(+), 52 deletions(-) diff --git a/DEVLOG.md b/DEVLOG.md index 9072110..fd9ca74 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -6417,3 +6417,117 @@ Keine Commits in dieser Session. - frontend/vite.config.ts | 2 + --- +## 2026-09-07 12:40 – 12:42 (2m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- c18638a feat(dashboard): echtes Drag & Drop statt Pfeil-Buttons, moderneres Kachel-Design + +### Geänderte Dateien +- DEVLOG.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/package-lock.json | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/package.json | 3 +++ +- frontend/src/components/ui/card.tsx | 2 +- +- frontend/src/pages/dashboard/DashboardPage.tsx | 79 ++++++++++++++++++++++++++++++++++++----------------------------- +- frontend/src/pages/dashboard/TileFrame.tsx | 64 ++++++++++++++++++++++++++-------------------------- +- frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx | 21 ++++++++++-------- +- frontend/src/pages/dashboard/tiles/MaengelTile.tsx | 11 ++++----- + +--- +## 2026-09-07 13:16 – 13:17 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/package-lock.json | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/package.json | 3 +++ +- frontend/src/components/ui/card.tsx | 2 +- +- frontend/src/pages/dashboard/DashboardPage.tsx | 79 ++++++++++++++++++++++++++++++++++++----------------------------- +- frontend/src/pages/dashboard/TileFrame.tsx | 64 ++++++++++++++++++++++++++-------------------------- +- frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx | 21 ++++++++++-------- +- frontend/src/pages/dashboard/tiles/MaengelTile.tsx | 11 ++++----- + +--- +## 2026-09-07 13:19 – 13:20 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/package-lock.json | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ +- frontend/package.json | 3 +++ +- frontend/src/components/ui/card.tsx | 2 +- +- frontend/src/pages/dashboard/DashboardPage.tsx | 79 ++++++++++++++++++++++++++++++++++++----------------------------- +- frontend/src/pages/dashboard/TileFrame.tsx | 64 ++++++++++++++++++++++++++-------------------------- +- frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx | 21 ++++++++++-------- +- frontend/src/pages/dashboard/tiles/MaengelTile.tsx | 11 ++++----- + +--- +## 2026-09-07 13:21 – 13:22 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** agents + +### Commits +- 726d6e2 chore(agents): react/tailwind/css/html-expert Subagenten aus 0xfurai/claude-code-subagents geladen + +### Geänderte Dateien +- .claude/agents/DEVLOG.md | 20 ++++++++++++++++++++ +- .claude/agents/css-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/html-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/react-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/tailwind-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-07 13:23 – 13:24 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .claude/agents/DEVLOG.md | 20 ++++++++++++++++++++ +- .claude/agents/css-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/html-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/react-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/tailwind-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-07 13:24 – 13:24 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .claude/agents/DEVLOG.md | 20 ++++++++++++++++++++ +- .claude/agents/css-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/html-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/react-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/tailwind-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-07 13:25 – 13:25 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- .claude/agents/DEVLOG.md | 20 ++++++++++++++++++++ +- .claude/agents/css-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/html-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/react-expert.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- .claude/agents/tailwind-expert.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + +--- diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx index 6db3a1c..bc475da 100644 --- a/frontend/src/pages/dashboard/DashboardPage.tsx +++ b/frontend/src/pages/dashboard/DashboardPage.tsx @@ -94,40 +94,55 @@ export function DashboardPage() { useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); - function dragEnde(event: DragEndEvent) { - const { active, over } = event; - if (!over || active.id === over.id) return; - const von = layout.reihenfolge.indexOf(active.id as DashboardTileId); - const nach = layout.reihenfolge.indexOf(over.id as DashboardTileId); - if (von === -1 || nach === -1) return; - const neuesLayout = { ...layout, reihenfolge: arrayMove(layout.reihenfolge, von, nach) }; - setLayout(neuesLayout); - layoutSpeichern(neuesLayout); - } + const aktualisiereLayout = useCallback((aendern: (bisherig: typeof layout) => typeof layout) => { + setLayout((bisherig) => { + const neuesLayout = aendern(bisherig); + layoutSpeichern(neuesLayout); + return neuesLayout; + }); + }, []); - function ausblendenUmschalten(id: DashboardTileId) { - const istAusgeblendet = layout.ausgeblendet.includes(id); - const neuesLayout = { - ...layout, - ausgeblendet: istAusgeblendet ? layout.ausgeblendet.filter((x) => x !== id) : [...layout.ausgeblendet, id], - }; - setLayout(neuesLayout); - layoutSpeichern(neuesLayout); - } + const dragEnde = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + aktualisiereLayout((bisherig) => { + const von = bisherig.reihenfolge.indexOf(active.id as DashboardTileId); + const nach = bisherig.reihenfolge.indexOf(over.id as DashboardTileId); + if (von === -1 || nach === -1) return bisherig; + return { ...bisherig, reihenfolge: arrayMove(bisherig.reihenfolge, von, nach) }; + }); + }, + [aktualisiereLayout] + ); - function groesseAendern(id: DashboardTileId) { - const aktuelle = layout.groessen[id] ?? "normal"; - const naechsteIndex = (GROESSEN_REIHENFOLGE.indexOf(aktuelle) + 1) % GROESSEN_REIHENFOLGE.length; - const neuesLayout = { ...layout, groessen: { ...layout.groessen, [id]: GROESSEN_REIHENFOLGE[naechsteIndex] } }; - setLayout(neuesLayout); - layoutSpeichern(neuesLayout); - } + const ausblendenUmschalten = useCallback( + (id: DashboardTileId) => { + aktualisiereLayout((bisherig) => { + const istAusgeblendet = bisherig.ausgeblendet.includes(id); + return { + ...bisherig, + ausgeblendet: istAusgeblendet ? bisherig.ausgeblendet.filter((x) => x !== id) : [...bisherig.ausgeblendet, id], + }; + }); + }, + [aktualisiereLayout] + ); - function layoutZuruecksetzen() { - const neuesLayout: typeof layout = { reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} }; - setLayout(neuesLayout); - layoutSpeichern(neuesLayout); - } + const groesseAendern = useCallback( + (id: DashboardTileId) => { + aktualisiereLayout((bisherig) => { + const aktuelle = bisherig.groessen[id] ?? "normal"; + const naechsteIndex = (GROESSEN_REIHENFOLGE.indexOf(aktuelle) + 1) % GROESSEN_REIHENFOLGE.length; + return { ...bisherig, groessen: { ...bisherig.groessen, [id]: GROESSEN_REIHENFOLGE[naechsteIndex] } }; + }); + }, + [aktualisiereLayout] + ); + + const layoutZuruecksetzen = useCallback(() => { + aktualisiereLayout(() => ({ reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} })); + }, [aktualisiereLayout]); async function monitorUmschalten() { if (monitorModus) { @@ -234,7 +249,14 @@ export function DashboardPage() { Monitor beenden )} - {fehler &&
{fehler}
} + {fehler && ( +
+ {fehler} + +
+ )} {laedt &&

Lade…

} {bearbeitenModus && layout.ausgeblendet.length > 0 && (
diff --git a/frontend/src/pages/dashboard/TileFrame.tsx b/frontend/src/pages/dashboard/TileFrame.tsx index 37e31ac..858c65c 100644 --- a/frontend/src/pages/dashboard/TileFrame.tsx +++ b/frontend/src/pages/dashboard/TileFrame.tsx @@ -1,5 +1,6 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { memo } from "react"; import type { ReactNode } from "react"; import { Button } from "../../components/ui/button"; @@ -23,12 +24,12 @@ interface Props { akzent?: TileAkzent; bearbeitenAktiv: boolean; ausgeblendet: boolean; - onAusblendenUmschalten: () => void; - onGroesseAendern: () => void; + onAusblendenUmschalten: (id: DashboardTileId) => void; + onGroesseAendern: (id: DashboardTileId) => void; children: ReactNode; } -export function TileFrame({ +function TileFrameKomponente({ id, title, flexBasis, @@ -62,9 +63,11 @@ export function TileFrame({ @@ -73,10 +76,16 @@ export function TileFrame({
{bearbeitenAktiv && (
- -
@@ -86,3 +95,5 @@ export function TileFrame({ ); } + +export const TileFrame = memo(TileFrameKomponente); diff --git a/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx b/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx index a4048d2..7c54b64 100644 --- a/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx +++ b/frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { Link } from "react-router-dom"; import { InventoryStatus } from "../../../components/status/InventoryStatus"; @@ -7,7 +8,7 @@ interface Props { ablaufdaten: Ablaufdatumeintrag[]; } -export function AblaufdatenTile({ ablaufdaten }: Props) { +function AblaufdatenTileKomponente({ ablaufdaten }: Props) { if (ablaufdaten.length === 0) { return

Keine bald ablaufenden Chargen.

; } @@ -26,3 +27,5 @@ export function AblaufdatenTile({ ablaufdaten }: Props) { ); } + +export const AblaufdatenTile = memo(AblaufdatenTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/AktivitaetTile.tsx b/frontend/src/pages/dashboard/tiles/AktivitaetTile.tsx index 5d6f9c6..11c9714 100644 --- a/frontend/src/pages/dashboard/tiles/AktivitaetTile.tsx +++ b/frontend/src/pages/dashboard/tiles/AktivitaetTile.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { Link } from "react-router-dom"; import type { HistorieEintrag } from "../../../api/types"; @@ -7,7 +8,7 @@ interface Props { aktivitaet: HistorieEintrag[]; } -export function AktivitaetTile({ aktivitaet }: Props) { +function AktivitaetTileKomponente({ aktivitaet }: Props) { if (aktivitaet.length === 0) { return

Noch keine Ereignisse.

; } @@ -32,3 +33,5 @@ export function AktivitaetTile({ aktivitaet }: Props) { ); } + +export const AktivitaetTile = memo(AktivitaetTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/AufgabenTile.tsx b/frontend/src/pages/dashboard/tiles/AufgabenTile.tsx index c495abd..745270e 100644 --- a/frontend/src/pages/dashboard/tiles/AufgabenTile.tsx +++ b/frontend/src/pages/dashboard/tiles/AufgabenTile.tsx @@ -1,3 +1,4 @@ +import { memo } from "react"; import { Link } from "react-router-dom"; import type { Einsatzbereitschaft, Kennzahlen } from "../types"; @@ -11,7 +12,7 @@ interface Props { bereitschaft: Einsatzbereitschaft; } -export function AufgabenTile({ +function AufgabenTileKomponente({ kennzahlen, maengelNeuAnzahl, maengelKritischAnzahl, @@ -50,9 +51,11 @@ export function AufgabenTile({ return ( ); } + +export const AufgabenTile = memo(AufgabenTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx b/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx index 2935cb6..4ded672 100644 --- a/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx +++ b/frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { memo, useState } from "react"; import { Link } from "react-router-dom"; import { GRUND_TEXT } from "../../../components/status/gruendeText"; @@ -18,7 +18,7 @@ interface Props { bereitschaft: Einsatzbereitschaft; } -export function EinsatzbereitschaftTile({ bereitschaft }: Props) { +function EinsatzbereitschaftTileKomponente({ bereitschaft }: Props) { const [detailsOffen, setDetailsOffen] = useState(false); return ( @@ -72,3 +72,5 @@ export function EinsatzbereitschaftTile({ bereitschaft }: Props) { ); } + +export const EinsatzbereitschaftTile = memo(EinsatzbereitschaftTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx b/frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx index 12ab5e0..207ecd9 100644 --- a/frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx +++ b/frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx @@ -1,10 +1,12 @@ +import { memo } from "react"; + import type { Kennzahlen } from "../types"; interface Props { kennzahlen: Kennzahlen; } -export function KennzahlenTile({ kennzahlen }: Props) { +function KennzahlenTileKomponente({ kennzahlen }: Props) { return (

{kennzahlen.anzahl_offener_fehlbestaende}

@@ -20,3 +22,5 @@ export function KennzahlenTile({ kennzahlen }: Props) {
); } + +export const KennzahlenTile = memo(KennzahlenTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/MaengelTile.tsx b/frontend/src/pages/dashboard/tiles/MaengelTile.tsx index 6133076..fbf2972 100644 --- a/frontend/src/pages/dashboard/tiles/MaengelTile.tsx +++ b/frontend/src/pages/dashboard/tiles/MaengelTile.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { memo, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import type { Mangel } from "../../../api/types"; @@ -9,9 +9,9 @@ interface Props { maengelKritischAnzahl: number; } -export function MaengelTile({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }: Props) { +function MaengelTileKomponente({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }: Props) { const [maengelOffen, setMaengelOffen] = useState(false); - const maengelOffenListe = maengel.filter((m) => m.status !== "erledigt"); + const maengelOffenListe = useMemo(() => maengel.filter((m) => m.status !== "erledigt"), [maengel]); return ( <> @@ -53,3 +53,5 @@ export function MaengelTile({ maengel, maengelNeuAnzahl, maengelKritischAnzahl } ); } + +export const MaengelTile = memo(MaengelTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx b/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx index 5cdcb3c..fd6df79 100644 --- a/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx +++ b/frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx @@ -1,4 +1,5 @@ import { Link } from "react-router-dom"; +import { memo } from "react"; import { InspectionStatus } from "../../../components/status/InspectionStatus"; import type { Prueftermineintrag } from "../types"; @@ -7,15 +8,15 @@ interface Props { prueftermine: Prueftermineintrag[]; } -export function PrueftermineTile({ prueftermine }: Props) { +function PrueftermineTileKomponente({ prueftermine }: Props) { if (prueftermine.length === 0) { return

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

; } return ( ); } + +export const PrueftermineTile = memo(PrueftermineTileKomponente); diff --git a/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx b/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx index 4a31bfb..6919030 100644 --- a/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx +++ b/frontend/src/pages/dashboard/tiles/QualifikationTile.tsx @@ -1,10 +1,12 @@ +import { memo } from "react"; + import type { Qualifikationsablauf } from "../types"; interface Props { qualifikationsablaeufe: Qualifikationsablauf[]; } -export function QualifikationTile({ qualifikationsablaeufe }: Props) { +function QualifikationTileKomponente({ qualifikationsablaeufe }: Props) { if (qualifikationsablaeufe.length === 0) { return

Keine bald ablaufenden Qualifikationen.

; } @@ -24,3 +26,5 @@ export function QualifikationTile({ qualifikationsablaeufe }: Props) { ); } + +export const QualifikationTile = memo(QualifikationTileKomponente);