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 && ( +
    + {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 + +
  • + ))} +
+ )} + {gruppiert && + gruppen.map(([standort, gruppenObjekte]) => (

{standort} ({gruppenObjekte.length})

-
+ {istDesktop ? ( @@ -144,34 +204,34 @@ export function ObjektListPage() { ))}
-
- -
    - {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 - -
  • - ))} -
+ ) : ( +
    + {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 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) --------- */