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."}
)}
-
+ {gruppen.map(([standort, gruppenObjekte]) => (
+
+
+ {standort} ({gruppenObjekte.length})
+
+
+
+
+
+
+ | Code |
+ Name |
+ Status |
+ Letzte Kontrolle |
+ |
+
+
+
+ {gruppenObjekte.map((objekt) => (
+
+ | {objekt.code} |
+
+ {objekt.name}
+ |
+
+ {objekt.status}
+ |
+
+ {objekt.letzte_kontrolle_am
+ ? new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")
+ : "Noch nie"}
+ |
+
+
+ Akte
+
+ |
+
+ ))}
+
+
+
+
+
+
+ ))}
);
}
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) --------- */