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 +
|
||||
|
||||
---
|
||||
## 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 })
|
||||
);
|
||||
|
||||
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
|
||||
</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>}
|
||||
{bearbeitenModus && layout.ausgeblendet.length > 0 && (
|
||||
<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 { 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({
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
type="button"
|
||||
className="min-h-11 min-w-11 cursor-grab touch-none text-muted-foreground active:cursor-grabbing"
|
||||
title="Ziehen zum Anordnen"
|
||||
aria-label={`${title} verschieben`}
|
||||
aria-roledescription="Kachel-Griff, mit Pfeiltasten verschiebbar"
|
||||
>
|
||||
⠿
|
||||
</button>
|
||||
@@ -73,10 +76,16 @@ export function TileFrame({
|
||||
</div>
|
||||
{bearbeitenAktiv && (
|
||||
<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]}
|
||||
</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"}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -86,3 +95,5 @@ export function TileFrame({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const TileFrame = memo(TileFrameKomponente);
|
||||
|
||||
@@ -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 <p className="text-muted">Keine bald ablaufenden Chargen.</p>;
|
||||
}
|
||||
@@ -26,3 +27,5 @@ export function AblaufdatenTile({ ablaufdaten }: Props) {
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export const AblaufdatenTile = memo(AblaufdatenTileKomponente);
|
||||
|
||||
@@ -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 <p className="text-muted">Noch keine Ereignisse.</p>;
|
||||
}
|
||||
@@ -32,3 +33,5 @@ export function AktivitaetTile({ aktivitaet }: Props) {
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export const AktivitaetTile = memo(AktivitaetTileKomponente);
|
||||
|
||||
@@ -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 (
|
||||
<ul className="card-list">
|
||||
{aufgaben.map((a, i) => (
|
||||
<li key={i}>{a.ziel ? <Link to={a.ziel}>{a.text}</Link> : <span>{a.text}</span>}</li>
|
||||
{aufgaben.map((a) => (
|
||||
<li key={a.text}>{a.ziel ? <Link to={a.ziel}>{a.text}</Link> : <span>{a.text}</span>}</li>
|
||||
))}
|
||||
</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 { 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);
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
@@ -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 <p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="card-list">
|
||||
{prueftermine.map((p, i) => (
|
||||
<li key={i} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||
{prueftermine.map((p) => (
|
||||
<li key={`${p.typ}-${p.objekt_id}-${p.faelligkeitsdatum}`} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||
<span>
|
||||
<Link to={`/akte/objekt/${p.objekt_id}`}>{p.bezeichnung}</Link>{" "}
|
||||
<span className="text-muted">({p.faelligkeitsdatum})</span>
|
||||
@@ -26,3 +27,5 @@ export function PrueftermineTile({ prueftermine }: Props) {
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export const PrueftermineTile = memo(PrueftermineTileKomponente);
|
||||
|
||||
@@ -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 <p className="text-muted">Keine bald ablaufenden Qualifikationen.</p>;
|
||||
}
|
||||
@@ -24,3 +26,5 @@ export function QualifikationTile({ qualifikationsablaeufe }: Props) {
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export const QualifikationTile = memo(QualifikationTileKomponente);
|
||||
|
||||
Reference in New Issue
Block a user