perf(dashboard): React-Review - Memoization, stabile Callbacks, Key-Fixes
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -6417,3 +6417,117 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/vite.config.ts | 2 +
|
- 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 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -94,40 +94,55 @@ export function DashboardPage() {
|
|||||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||||
);
|
);
|
||||||
|
|
||||||
function dragEnde(event: DragEndEvent) {
|
const aktualisiereLayout = useCallback((aendern: (bisherig: typeof layout) => typeof layout) => {
|
||||||
|
setLayout((bisherig) => {
|
||||||
|
const neuesLayout = aendern(bisherig);
|
||||||
|
layoutSpeichern(neuesLayout);
|
||||||
|
return neuesLayout;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dragEnde = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over || active.id === over.id) return;
|
if (!over || active.id === over.id) return;
|
||||||
const von = layout.reihenfolge.indexOf(active.id as DashboardTileId);
|
aktualisiereLayout((bisherig) => {
|
||||||
const nach = layout.reihenfolge.indexOf(over.id as DashboardTileId);
|
const von = bisherig.reihenfolge.indexOf(active.id as DashboardTileId);
|
||||||
if (von === -1 || nach === -1) return;
|
const nach = bisherig.reihenfolge.indexOf(over.id as DashboardTileId);
|
||||||
const neuesLayout = { ...layout, reihenfolge: arrayMove(layout.reihenfolge, von, nach) };
|
if (von === -1 || nach === -1) return bisherig;
|
||||||
setLayout(neuesLayout);
|
return { ...bisherig, reihenfolge: arrayMove(bisherig.reihenfolge, von, nach) };
|
||||||
layoutSpeichern(neuesLayout);
|
});
|
||||||
}
|
},
|
||||||
|
[aktualisiereLayout]
|
||||||
|
);
|
||||||
|
|
||||||
function ausblendenUmschalten(id: DashboardTileId) {
|
const ausblendenUmschalten = useCallback(
|
||||||
const istAusgeblendet = layout.ausgeblendet.includes(id);
|
(id: DashboardTileId) => {
|
||||||
const neuesLayout = {
|
aktualisiereLayout((bisherig) => {
|
||||||
...layout,
|
const istAusgeblendet = bisherig.ausgeblendet.includes(id);
|
||||||
ausgeblendet: istAusgeblendet ? layout.ausgeblendet.filter((x) => x !== id) : [...layout.ausgeblendet, id],
|
return {
|
||||||
|
...bisherig,
|
||||||
|
ausgeblendet: istAusgeblendet ? bisherig.ausgeblendet.filter((x) => x !== id) : [...bisherig.ausgeblendet, id],
|
||||||
};
|
};
|
||||||
setLayout(neuesLayout);
|
});
|
||||||
layoutSpeichern(neuesLayout);
|
},
|
||||||
}
|
[aktualisiereLayout]
|
||||||
|
);
|
||||||
|
|
||||||
function groesseAendern(id: DashboardTileId) {
|
const groesseAendern = useCallback(
|
||||||
const aktuelle = layout.groessen[id] ?? "normal";
|
(id: DashboardTileId) => {
|
||||||
|
aktualisiereLayout((bisherig) => {
|
||||||
|
const aktuelle = bisherig.groessen[id] ?? "normal";
|
||||||
const naechsteIndex = (GROESSEN_REIHENFOLGE.indexOf(aktuelle) + 1) % GROESSEN_REIHENFOLGE.length;
|
const naechsteIndex = (GROESSEN_REIHENFOLGE.indexOf(aktuelle) + 1) % GROESSEN_REIHENFOLGE.length;
|
||||||
const neuesLayout = { ...layout, groessen: { ...layout.groessen, [id]: GROESSEN_REIHENFOLGE[naechsteIndex] } };
|
return { ...bisherig, groessen: { ...bisherig.groessen, [id]: GROESSEN_REIHENFOLGE[naechsteIndex] } };
|
||||||
setLayout(neuesLayout);
|
});
|
||||||
layoutSpeichern(neuesLayout);
|
},
|
||||||
}
|
[aktualisiereLayout]
|
||||||
|
);
|
||||||
|
|
||||||
function layoutZuruecksetzen() {
|
const layoutZuruecksetzen = useCallback(() => {
|
||||||
const neuesLayout: typeof layout = { reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} };
|
aktualisiereLayout(() => ({ reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} }));
|
||||||
setLayout(neuesLayout);
|
}, [aktualisiereLayout]);
|
||||||
layoutSpeichern(neuesLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function monitorUmschalten() {
|
async function monitorUmschalten() {
|
||||||
if (monitorModus) {
|
if (monitorModus) {
|
||||||
@@ -234,7 +249,14 @@ export function DashboardPage() {
|
|||||||
Monitor beenden
|
Monitor beenden
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
{fehler && (
|
||||||
|
<div className="alert alert-danger flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<span>{fehler}</span>
|
||||||
|
<Button variant="secondary" size="sm" onClick={ladeDaten}>
|
||||||
|
Erneut versuchen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{laedt && <p className="text-muted">Lade…</p>}
|
{laedt && <p className="text-muted">Lade…</p>}
|
||||||
{bearbeitenModus && layout.ausgeblendet.length > 0 && (
|
{bearbeitenModus && layout.ausgeblendet.length > 0 && (
|
||||||
<div className="mt-3 rounded-md border border-border bg-surface-sunken px-4 py-3 text-sm">
|
<div className="mt-3 rounded-md border border-border bg-surface-sunken px-4 py-3 text-sm">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { memo } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
@@ -23,12 +24,12 @@ interface Props {
|
|||||||
akzent?: TileAkzent;
|
akzent?: TileAkzent;
|
||||||
bearbeitenAktiv: boolean;
|
bearbeitenAktiv: boolean;
|
||||||
ausgeblendet: boolean;
|
ausgeblendet: boolean;
|
||||||
onAusblendenUmschalten: () => void;
|
onAusblendenUmschalten: (id: DashboardTileId) => void;
|
||||||
onGroesseAendern: () => void;
|
onGroesseAendern: (id: DashboardTileId) => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TileFrame({
|
function TileFrameKomponente({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
flexBasis,
|
flexBasis,
|
||||||
@@ -62,9 +63,11 @@ export function TileFrame({
|
|||||||
<button
|
<button
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
|
type="button"
|
||||||
className="min-h-11 min-w-11 cursor-grab touch-none text-muted-foreground active:cursor-grabbing"
|
className="min-h-11 min-w-11 cursor-grab touch-none text-muted-foreground active:cursor-grabbing"
|
||||||
title="Ziehen zum Anordnen"
|
title="Ziehen zum Anordnen"
|
||||||
aria-label={`${title} verschieben`}
|
aria-label={`${title} verschieben`}
|
||||||
|
aria-roledescription="Kachel-Griff, mit Pfeiltasten verschiebbar"
|
||||||
>
|
>
|
||||||
⠿
|
⠿
|
||||||
</button>
|
</button>
|
||||||
@@ -73,10 +76,16 @@ export function TileFrame({
|
|||||||
</div>
|
</div>
|
||||||
{bearbeitenAktiv && (
|
{bearbeitenAktiv && (
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<Button variant="secondary" size="sm" className="min-h-11" onClick={onGroesseAendern} title="Größe ändern">
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="min-h-11"
|
||||||
|
onClick={() => onGroesseAendern(id)}
|
||||||
|
title="Größe ändern"
|
||||||
|
>
|
||||||
{GROESSE_LABEL[groesse]}
|
{GROESSE_LABEL[groesse]}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" className="min-h-11" onClick={onAusblendenUmschalten}>
|
<Button variant="secondary" size="sm" className="min-h-11" onClick={() => onAusblendenUmschalten(id)}>
|
||||||
{ausgeblendet ? "Einblenden" : "Ausblenden"}
|
{ausgeblendet ? "Einblenden" : "Ausblenden"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,3 +95,5 @@ export function TileFrame({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TileFrame = memo(TileFrameKomponente);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { InventoryStatus } from "../../../components/status/InventoryStatus";
|
import { InventoryStatus } from "../../../components/status/InventoryStatus";
|
||||||
@@ -7,7 +8,7 @@ interface Props {
|
|||||||
ablaufdaten: Ablaufdatumeintrag[];
|
ablaufdaten: Ablaufdatumeintrag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AblaufdatenTile({ ablaufdaten }: Props) {
|
function AblaufdatenTileKomponente({ ablaufdaten }: Props) {
|
||||||
if (ablaufdaten.length === 0) {
|
if (ablaufdaten.length === 0) {
|
||||||
return <p className="text-muted">Keine bald ablaufenden Chargen.</p>;
|
return <p className="text-muted">Keine bald ablaufenden Chargen.</p>;
|
||||||
}
|
}
|
||||||
@@ -26,3 +27,5 @@ export function AblaufdatenTile({ ablaufdaten }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AblaufdatenTile = memo(AblaufdatenTileKomponente);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import type { HistorieEintrag } from "../../../api/types";
|
import type { HistorieEintrag } from "../../../api/types";
|
||||||
@@ -7,7 +8,7 @@ interface Props {
|
|||||||
aktivitaet: HistorieEintrag[];
|
aktivitaet: HistorieEintrag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AktivitaetTile({ aktivitaet }: Props) {
|
function AktivitaetTileKomponente({ aktivitaet }: Props) {
|
||||||
if (aktivitaet.length === 0) {
|
if (aktivitaet.length === 0) {
|
||||||
return <p className="text-muted">Noch keine Ereignisse.</p>;
|
return <p className="text-muted">Noch keine Ereignisse.</p>;
|
||||||
}
|
}
|
||||||
@@ -32,3 +33,5 @@ export function AktivitaetTile({ aktivitaet }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AktivitaetTile = memo(AktivitaetTileKomponente);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import type { Einsatzbereitschaft, Kennzahlen } from "../types";
|
import type { Einsatzbereitschaft, Kennzahlen } from "../types";
|
||||||
@@ -11,7 +12,7 @@ interface Props {
|
|||||||
bereitschaft: Einsatzbereitschaft;
|
bereitschaft: Einsatzbereitschaft;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AufgabenTile({
|
function AufgabenTileKomponente({
|
||||||
kennzahlen,
|
kennzahlen,
|
||||||
maengelNeuAnzahl,
|
maengelNeuAnzahl,
|
||||||
maengelKritischAnzahl,
|
maengelKritischAnzahl,
|
||||||
@@ -50,9 +51,11 @@ export function AufgabenTile({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{aufgaben.map((a, i) => (
|
{aufgaben.map((a) => (
|
||||||
<li key={i}>{a.ziel ? <Link to={a.ziel}>{a.text}</Link> : <span>{a.text}</span>}</li>
|
<li key={a.text}>{a.ziel ? <Link to={a.ziel}>{a.text}</Link> : <span>{a.text}</span>}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AufgabenTile = memo(AufgabenTileKomponente);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { memo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { GRUND_TEXT } from "../../../components/status/gruendeText";
|
import { GRUND_TEXT } from "../../../components/status/gruendeText";
|
||||||
@@ -18,7 +18,7 @@ interface Props {
|
|||||||
bereitschaft: Einsatzbereitschaft;
|
bereitschaft: Einsatzbereitschaft;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EinsatzbereitschaftTile({ bereitschaft }: Props) {
|
function EinsatzbereitschaftTileKomponente({ bereitschaft }: Props) {
|
||||||
const [detailsOffen, setDetailsOffen] = useState(false);
|
const [detailsOffen, setDetailsOffen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,3 +72,5 @@ export function EinsatzbereitschaftTile({ bereitschaft }: Props) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EinsatzbereitschaftTile = memo(EinsatzbereitschaftTileKomponente);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
import type { Kennzahlen } from "../types";
|
import type { Kennzahlen } from "../types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
kennzahlen: Kennzahlen;
|
kennzahlen: Kennzahlen;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KennzahlenTile({ kennzahlen }: Props) {
|
function KennzahlenTileKomponente({ kennzahlen }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-4xl font-bold tabular-nums leading-none">{kennzahlen.anzahl_offener_fehlbestaende}</p>
|
<p className="text-4xl font-bold tabular-nums leading-none">{kennzahlen.anzahl_offener_fehlbestaende}</p>
|
||||||
@@ -20,3 +22,5 @@ export function KennzahlenTile({ kennzahlen }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const KennzahlenTile = memo(KennzahlenTileKomponente);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { memo, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import type { Mangel } from "../../../api/types";
|
import type { Mangel } from "../../../api/types";
|
||||||
@@ -9,9 +9,9 @@ interface Props {
|
|||||||
maengelKritischAnzahl: number;
|
maengelKritischAnzahl: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MaengelTile({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }: Props) {
|
function MaengelTileKomponente({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }: Props) {
|
||||||
const [maengelOffen, setMaengelOffen] = useState(false);
|
const [maengelOffen, setMaengelOffen] = useState(false);
|
||||||
const maengelOffenListe = maengel.filter((m) => m.status !== "erledigt");
|
const maengelOffenListe = useMemo(() => maengel.filter((m) => m.status !== "erledigt"), [maengel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -53,3 +53,5 @@ export function MaengelTile({ maengel, maengelNeuAnzahl, maengelKritischAnzahl }
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MaengelTile = memo(MaengelTileKomponente);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
import { InspectionStatus } from "../../../components/status/InspectionStatus";
|
import { InspectionStatus } from "../../../components/status/InspectionStatus";
|
||||||
import type { Prueftermineintrag } from "../types";
|
import type { Prueftermineintrag } from "../types";
|
||||||
@@ -7,15 +8,15 @@ interface Props {
|
|||||||
prueftermine: Prueftermineintrag[];
|
prueftermine: Prueftermineintrag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrueftermineTile({ prueftermine }: Props) {
|
function PrueftermineTileKomponente({ prueftermine }: Props) {
|
||||||
if (prueftermine.length === 0) {
|
if (prueftermine.length === 0) {
|
||||||
return <p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>;
|
return <p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{prueftermine.map((p, i) => (
|
{prueftermine.map((p) => (
|
||||||
<li key={i} className="row-between" style={{ padding: "0.3rem 0" }}>
|
<li key={`${p.typ}-${p.objekt_id}-${p.faelligkeitsdatum}`} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||||
<span>
|
<span>
|
||||||
<Link to={`/akte/objekt/${p.objekt_id}`}>{p.bezeichnung}</Link>{" "}
|
<Link to={`/akte/objekt/${p.objekt_id}`}>{p.bezeichnung}</Link>{" "}
|
||||||
<span className="text-muted">({p.faelligkeitsdatum})</span>
|
<span className="text-muted">({p.faelligkeitsdatum})</span>
|
||||||
@@ -26,3 +27,5 @@ export function PrueftermineTile({ prueftermine }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PrueftermineTile = memo(PrueftermineTileKomponente);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
import type { Qualifikationsablauf } from "../types";
|
import type { Qualifikationsablauf } from "../types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
qualifikationsablaeufe: Qualifikationsablauf[];
|
qualifikationsablaeufe: Qualifikationsablauf[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QualifikationTile({ qualifikationsablaeufe }: Props) {
|
function QualifikationTileKomponente({ qualifikationsablaeufe }: Props) {
|
||||||
if (qualifikationsablaeufe.length === 0) {
|
if (qualifikationsablaeufe.length === 0) {
|
||||||
return <p className="text-muted">Keine bald ablaufenden Qualifikationen.</p>;
|
return <p className="text-muted">Keine bald ablaufenden Qualifikationen.</p>;
|
||||||
}
|
}
|
||||||
@@ -24,3 +26,5 @@ export function QualifikationTile({ qualifikationsablaeufe }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const QualifikationTile = memo(QualifikationTileKomponente);
|
||||||
|
|||||||
Reference in New Issue
Block a user