feat(objekte): Umschalter Kacheln/Gruppierung, useIsDesktop statt CSS-Doppelrender
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -4,13 +4,32 @@ import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
|||||||
import { apiRequest, ApiError } from "../api/client";
|
import { apiRequest, ApiError } from "../api/client";
|
||||||
import type { Objekt, Standort } from "../api/types";
|
import type { Objekt, Standort } from "../api/types";
|
||||||
import { BarcodeScanner } from "../components/BarcodeScanner";
|
import { BarcodeScanner } from "../components/BarcodeScanner";
|
||||||
|
import { useIsDesktop } from "../hooks/useIsDesktop";
|
||||||
|
|
||||||
const OHNE_STANDORT = "Ohne Standort";
|
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
|
// 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).
|
// wie die manuelle Suche (Kamera fällt bei Etikettschaden auf Tippen zurück).
|
||||||
export function ObjektListPage() {
|
export function ObjektListPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const istDesktop = useIsDesktop();
|
||||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||||
const [standorte, setStandorte] = useState<Standort[]>([]);
|
const [standorte, setStandorte] = useState<Standort[]>([]);
|
||||||
const [suche, setSuche] = useState("");
|
const [suche, setSuche] = useState("");
|
||||||
@@ -18,6 +37,15 @@ export function ObjektListPage() {
|
|||||||
const [laedt, setLaedt] = useState(true);
|
const [laedt, setLaedt] = useState(true);
|
||||||
const [scannerOffen, setScannerOffen] = useState(false);
|
const [scannerOffen, setScannerOffen] = useState(false);
|
||||||
const [suchparameter, setSuchparameter] = useSearchParams();
|
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
|
// Quick Action "QR scannen" aus der Command Palette springt
|
||||||
// hierher mit ?scan=1 und öffnet den Scanner direkt, statt nur die Liste zu zeigen.
|
// hierher mit ?scan=1 und öffnet den Scanner direkt, statt nur die Liste zu zeigen.
|
||||||
@@ -92,6 +120,9 @@ export function ObjektListPage() {
|
|||||||
<button className="btn btn-secondary" onClick={() => setScannerOffen(true)}>
|
<button className="btn btn-secondary" onClick={() => setScannerOffen(true)}>
|
||||||
Scannen
|
Scannen
|
||||||
</button>
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={ansichtUmschalten}>
|
||||||
|
{gruppiert ? "Kacheln" : "Nach Standort gruppieren"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{scannerOffen && (
|
{scannerOffen && (
|
||||||
<BarcodeScanner onErkannt={scanVerarbeiten} onSchliessen={() => setScannerOffen(false)} />
|
<BarcodeScanner onErkannt={scanVerarbeiten} onSchliessen={() => setScannerOffen(false)} />
|
||||||
@@ -103,13 +134,42 @@ export function ObjektListPage() {
|
|||||||
{objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}
|
{objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{gruppen.map(([standort, gruppenObjekte]) => (
|
{!gruppiert && gefiltert.length > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{gefiltert.map((objekt) => (
|
||||||
|
<li key={objekt.id}>
|
||||||
|
<Link to={`/objekte/${objekt.id}/kontrolle`} className="card card-link">
|
||||||
|
<div className="card-title-row">
|
||||||
|
<strong>{objekt.name}</strong>
|
||||||
|
<span className="badge badge-neutral">{objekt.status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-subtitle">{objekt.code}</div>
|
||||||
|
<div className="text-muted" style={{ fontSize: "0.85rem" }}>
|
||||||
|
{objekt.letzte_kontrolle_am
|
||||||
|
? `Letzte Kontrolle: ${new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")}`
|
||||||
|
: "Noch nie kontrolliert"}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={`/akte/objekt/${objekt.id}`}
|
||||||
|
className="text-muted"
|
||||||
|
style={{ fontSize: "0.85rem" }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
Akte ansehen
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{gruppiert &&
|
||||||
|
gruppen.map(([standort, gruppenObjekte]) => (
|
||||||
<section key={standort} className="objekte-gruppe">
|
<section key={standort} className="objekte-gruppe">
|
||||||
<h2 className="objekte-gruppe-titel">
|
<h2 className="objekte-gruppe-titel">
|
||||||
{standort} <span className="text-muted">({gruppenObjekte.length})</span>
|
{standort} <span className="text-muted">({gruppenObjekte.length})</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="objekte-tabellen-ansicht">
|
{istDesktop ? (
|
||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -144,9 +204,8 @@ export function ObjektListPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
) : (
|
||||||
|
<ul className="card-list">
|
||||||
<ul className="card-list objekte-karten-ansicht">
|
|
||||||
{gruppenObjekte.map((objekt) => (
|
{gruppenObjekte.map((objekt) => (
|
||||||
<li key={objekt.id}>
|
<li key={objekt.id}>
|
||||||
<Link to={`/objekte/${objekt.id}/kontrolle`} className="card card-link">
|
<Link to={`/objekte/${objekt.id}/kontrolle`} className="card card-link">
|
||||||
@@ -172,6 +231,7 @@ export function ObjektListPage() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -697,8 +697,8 @@ select.input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Objekte-Liste: nach Standort gruppiert, ab 900px Tabelle statt Karten
|
/* Objekte-Liste: nach Standort gruppiert, ab 900px Tabelle statt Karten
|
||||||
(mehr Objekte auf einen Blick am Desktop) - beide Ansichten liegen im DOM,
|
(mehr Objekte auf einen Blick am Desktop) - Umschaltung per useIsDesktop-
|
||||||
CSS schaltet um statt JS-Breite-Erkennung (kein Hydration-/Resize-Zittern). */
|
Hook (echte Breiten-Erkennung, reagiert live auf Resize), nicht per CSS. */
|
||||||
.objekte-gruppe {
|
.objekte-gruppe {
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.75rem;
|
||||||
}
|
}
|
||||||
@@ -708,20 +708,6 @@ select.input:focus {
|
|||||||
margin-bottom: 0.6rem;
|
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:
|
/* -- Dashboard-Kacheln (DNA-Referenz Resgrid/OpenFleet/Emergency-Management:
|
||||||
KPI-Statistik-Karten sind dort durchgängig das zentrale Muster) --------- */
|
KPI-Statistik-Karten sind dort durchgängig das zentrale Muster) --------- */
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user