From 174cefa59595d808dc1a7d161098234003a8ad08 Mon Sep 17 00:00:00 2001
From: patrick
Date: Tue, 8 Sep 2026 01:12:19 +0200
Subject: [PATCH] feat(objekte): Umschalter Kacheln/Gruppierung, useIsDesktop
statt CSS-Doppelrender
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ansicht wird jetzt per Button in der Leiste umgeschaltet (persistiert in
localStorage) statt fest auf Gruppierung. Zusätzlich: Tabelle/Karten-
Umschaltung lief bisher über zwei parallel im DOM liegende, per CSS ein-/
ausgeblendete Blöcke - durch echten useIsDesktop-Hook (matchMedia) ersetzt,
reagiert live auf Resize statt nur beim Neuladen.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
---
frontend/src/hooks/useIsDesktop.ts | 20 +++++
frontend/src/pages/ObjektListPage.tsx | 120 +++++++++++++++++++-------
frontend/src/styles/global.css | 18 +---
3 files changed, 112 insertions(+), 46 deletions(-)
create mode 100644 frontend/src/hooks/useIsDesktop.ts
diff --git a/frontend/src/hooks/useIsDesktop.ts b/frontend/src/hooks/useIsDesktop.ts
new file mode 100644
index 0000000..a58d200
--- /dev/null
+++ b/frontend/src/hooks/useIsDesktop.ts
@@ -0,0 +1,20 @@
+import { useEffect, useState } from "react";
+
+const BREAKPOINT = "(min-width: 900px)";
+
+/** Echte Breiten-Erkennung statt zweier per CSS ein-/ausgeblendeter DOM-Bäume -
+ * reagiert live auf Resize/Fenster-Verschiebung zwischen Monitoren. */
+export function useIsDesktop(): boolean {
+ const [istDesktop, setIstDesktop] = useState(() => window.matchMedia(BREAKPOINT).matches);
+
+ useEffect(() => {
+ const query = window.matchMedia(BREAKPOINT);
+ function aendern(e: MediaQueryListEvent) {
+ setIstDesktop(e.matches);
+ }
+ query.addEventListener("change", aendern);
+ return () => query.removeEventListener("change", aendern);
+ }, []);
+
+ return istDesktop;
+}
diff --git a/frontend/src/pages/ObjektListPage.tsx b/frontend/src/pages/ObjektListPage.tsx
index ece03c4..8edd78d 100644
--- a/frontend/src/pages/ObjektListPage.tsx
+++ b/frontend/src/pages/ObjektListPage.tsx
@@ -4,13 +4,32 @@ import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { apiRequest, ApiError } from "../api/client";
import type { Objekt, Standort } from "../api/types";
import { BarcodeScanner } from "../components/BarcodeScanner";
+import { useIsDesktop } from "../hooks/useIsDesktop";
const OHNE_STANDORT = "Ohne Standort";
+const SPEICHER_KEY_ANSICHT = "mabea-objekte-ansicht";
+
+function ansichtLaden(): boolean {
+ try {
+ return localStorage.getItem(SPEICHER_KEY_ANSICHT) !== "kacheln";
+ } catch {
+ return true;
+ }
+}
+
+function ansichtSpeichern(gruppiert: boolean): void {
+ try {
+ localStorage.setItem(SPEICHER_KEY_ANSICHT, gruppiert ? "gruppiert" : "kacheln");
+ } catch {
+ // Speichern optional
+ }
+}
// 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 istDesktop = useIsDesktop();
const [objekte, setObjekte] = useState([]);
const [standorte, setStandorte] = useState([]);
const [suche, setSuche] = useState("");
@@ -18,6 +37,15 @@ export function ObjektListPage() {
const [laedt, setLaedt] = useState(true);
const [scannerOffen, setScannerOffen] = useState(false);
const [suchparameter, setSuchparameter] = useSearchParams();
+ const [gruppiert, setGruppiert] = useState(ansichtLaden);
+
+ function ansichtUmschalten() {
+ setGruppiert((bisherig) => {
+ const neu = !bisherig;
+ ansichtSpeichern(neu);
+ return neu;
+ });
+ }
// Quick Action "QR scannen" aus der Command Palette springt
// hierher mit ?scan=1 und öffnet den Scanner direkt, statt nur die Liste zu zeigen.
@@ -92,6 +120,9 @@ export function ObjektListPage() {
+
{scannerOffen && (
setScannerOffen(false)} />
@@ -103,13 +134,42 @@ export function ObjektListPage() {
{objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}
)}
- {gruppen.map(([standort, gruppenObjekte]) => (
+ {!gruppiert && gefiltert.length > 0 && (
+
+ )}
+ {gruppiert &&
+ gruppen.map(([standort, gruppenObjekte]) => (
{standort} ({gruppenObjekte.length})
-
+ {istDesktop ? (
@@ -144,34 +204,34 @@ export function ObjektListPage() {
))}
-
-
-
+ ) : (
+
+ )}
))}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 1d2aa7f..879b036 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -697,8 +697,8 @@ select.input:focus {
}
/* 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). */
+ (mehr Objekte auf einen Blick am Desktop) - Umschaltung per useIsDesktop-
+ Hook (echte Breiten-Erkennung, reagiert live auf Resize), nicht per CSS. */
.objekte-gruppe {
margin-bottom: 1.75rem;
}
@@ -708,20 +708,6 @@ select.input:focus {
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) --------- */