diff --git a/frontend/src/pages/ObjektListPage.tsx b/frontend/src/pages/ObjektListPage.tsx index a567cc9..ece03c4 100644 --- a/frontend/src/pages/ObjektListPage.tsx +++ b/frontend/src/pages/ObjektListPage.tsx @@ -1,15 +1,18 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { apiRequest, ApiError } from "../api/client"; -import type { Objekt } from "../api/types"; +import type { Objekt, Standort } from "../api/types"; import { BarcodeScanner } from "../components/BarcodeScanner"; +const OHNE_STANDORT = "Ohne Standort"; + // Objekt-Code-Scan springt direkt zur Akte, sonst füllt er dasselbe Textfeld // wie die manuelle Suche (Kamera fällt bei Etikettschaden auf Tippen zurück). export function ObjektListPage() { const navigate = useNavigate(); const [objekte, setObjekte] = useState([]); + const [standorte, setStandorte] = useState([]); const [suche, setSuche] = useState(""); const [ladefehler, setLadefehler] = useState(null); const [laedt, setLaedt] = useState(true); @@ -27,8 +30,11 @@ export function ObjektListPage() { }, []); useEffect(() => { - apiRequest("/objekte") - .then(setObjekte) + Promise.all([apiRequest("/objekte"), apiRequest("/standorte")]) + .then(([o, s]) => { + setObjekte(o); + setStandorte(s); + }) .catch(() => setLadefehler("Objektliste konnte nicht geladen werden.")) .finally(() => setLaedt(false)); }, []); @@ -47,12 +53,32 @@ export function ObjektListPage() { } } + const standortName = useMemo(() => { + const map = new Map(); + for (const s of standorte) map.set(s.id, s.name); + return map; + }, [standorte]); + const gefiltert = objekte.filter( (o) => o.name.toLowerCase().includes(suche.toLowerCase()) || o.code.toLowerCase().includes(suche.toLowerCase()) ); + // Standort ist bei jedem Objekt gesetzt (Pflichtfeld) - Gruppierung ist + // damit immer vollständig, keine "sonstige" Sammel-Gruppe nötig + // (Fahrzeug/Bereich wären dafür ungeeignet, da oft NULL bzw. nur indirekt + // über Objekttyp verfügbar). + const gruppen = useMemo(() => { + const nachStandort = new Map(); + for (const o of gefiltert) { + const name = standortName.get(o.standort_id) ?? OHNE_STANDORT; + if (!nachStandort.has(name)) nachStandort.set(name, []); + nachStandort.get(name)!.push(o); + } + return [...nachStandort.entries()].sort(([a], [b]) => a.localeCompare(b, "de")); + }, [gefiltert, standortName]); + return (

Objekte

@@ -77,32 +103,77 @@ export function ObjektListPage() { {objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}

)} -
    - {gefiltert.map((objekt) => ( -
  • - -
    - {objekt.name} - {objekt.status} -
    -
    {objekt.code}
    -
    - {objekt.letzte_kontrolle_am - ? `Letzte Kontrolle: ${new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")}` - : "Noch nie kontrolliert"} -
    - - e.stopPropagation()} - > - Akte ansehen - -
  • - ))} -
+ {gruppen.map(([standort, gruppenObjekte]) => ( +
+

+ {standort} ({gruppenObjekte.length}) +

+ +
+ + + + + + + + + + + {gruppenObjekte.map((objekt) => ( + + + + + + + + ))} + +
CodeNameStatusLetzte Kontrolle +
{objekt.code} + {objekt.name} + + {objekt.status} + + {objekt.letzte_kontrolle_am + ? new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE") + : "Noch nie"} + + + Akte + +
+
+ +
    + {gruppenObjekte.map((objekt) => ( +
  • + +
    + {objekt.name} + {objekt.status} +
    +
    {objekt.code}
    +
    + {objekt.letzte_kontrolle_am + ? `Letzte Kontrolle: ${new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")}` + : "Noch nie kontrolliert"} +
    + + e.stopPropagation()} + > + Akte ansehen + +
  • + ))} +
+
+ ))}
); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 757d69d..1d2aa7f 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -696,6 +696,32 @@ select.input:focus { background: var(--color-surface-sunken); } +/* Objekte-Liste: nach Standort gruppiert, ab 900px Tabelle statt Karten + (mehr Objekte auf einen Blick am Desktop) - beide Ansichten liegen im DOM, + CSS schaltet um statt JS-Breite-Erkennung (kein Hydration-/Resize-Zittern). */ +.objekte-gruppe { + margin-bottom: 1.75rem; +} + +.objekte-gruppe-titel { + font-size: 1rem; + margin-bottom: 0.6rem; +} + +.objekte-tabellen-ansicht { + display: none; +} + +@media (min-width: 900px) { + .objekte-tabellen-ansicht { + display: block; + } + + .objekte-karten-ansicht { + display: none; + } +} + /* -- Dashboard-Kacheln (DNA-Referenz Resgrid/OpenFleet/Emergency-Management: KPI-Statistik-Karten sind dort durchgängig das zentrale Muster) --------- */