feat(objekte): Liste nach Standort gruppiert, Desktop-Tabelle statt Karten
Objekte-Übersicht war eine flache Karten-Liste ohne Struktur. Jetzt nach Standort gruppiert (Pflichtfeld, deckt immer alle Objekte ab) mit Gruppenüberschrift + Anzahl. Ab 900px Breite Tabellen-Ansicht (Code/Name/ Status/letzte Kontrolle/Akte-Link) für mehr Objekte auf einen Blick, darunter weiterhin die Karten-Ansicht für mobil - beide liegen im DOM, CSS-Media-Query schaltet um statt JS-Breitenerkennung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { apiRequest, ApiError } from "../api/client";
|
import { apiRequest, ApiError } from "../api/client";
|
||||||
import type { Objekt } from "../api/types";
|
import type { Objekt, Standort } from "../api/types";
|
||||||
import { BarcodeScanner } from "../components/BarcodeScanner";
|
import { BarcodeScanner } from "../components/BarcodeScanner";
|
||||||
|
|
||||||
|
const OHNE_STANDORT = "Ohne Standort";
|
||||||
|
|
||||||
// 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 [objekte, setObjekte] = useState<Objekt[]>([]);
|
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||||
|
const [standorte, setStandorte] = useState<Standort[]>([]);
|
||||||
const [suche, setSuche] = useState("");
|
const [suche, setSuche] = useState("");
|
||||||
const [ladefehler, setLadefehler] = useState<string | null>(null);
|
const [ladefehler, setLadefehler] = useState<string | null>(null);
|
||||||
const [laedt, setLaedt] = useState(true);
|
const [laedt, setLaedt] = useState(true);
|
||||||
@@ -27,8 +30,11 @@ export function ObjektListPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiRequest<Objekt[]>("/objekte")
|
Promise.all([apiRequest<Objekt[]>("/objekte"), apiRequest<Standort[]>("/standorte")])
|
||||||
.then(setObjekte)
|
.then(([o, s]) => {
|
||||||
|
setObjekte(o);
|
||||||
|
setStandorte(s);
|
||||||
|
})
|
||||||
.catch(() => setLadefehler("Objektliste konnte nicht geladen werden."))
|
.catch(() => setLadefehler("Objektliste konnte nicht geladen werden."))
|
||||||
.finally(() => setLaedt(false));
|
.finally(() => setLaedt(false));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -47,12 +53,32 @@ export function ObjektListPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const standortName = useMemo(() => {
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
for (const s of standorte) map.set(s.id, s.name);
|
||||||
|
return map;
|
||||||
|
}, [standorte]);
|
||||||
|
|
||||||
const gefiltert = objekte.filter(
|
const gefiltert = objekte.filter(
|
||||||
(o) =>
|
(o) =>
|
||||||
o.name.toLowerCase().includes(suche.toLowerCase()) ||
|
o.name.toLowerCase().includes(suche.toLowerCase()) ||
|
||||||
o.code.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<string, Objekt[]>();
|
||||||
|
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 (
|
return (
|
||||||
<main className="page">
|
<main className="page">
|
||||||
<h1>Objekte</h1>
|
<h1>Objekte</h1>
|
||||||
@@ -77,8 +103,51 @@ export function ObjektListPage() {
|
|||||||
{objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}
|
{objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ul className="card-list">
|
{gruppen.map(([standort, gruppenObjekte]) => (
|
||||||
{gefiltert.map((objekt) => (
|
<section key={standort} className="objekte-gruppe">
|
||||||
|
<h2 className="objekte-gruppe-titel">
|
||||||
|
{standort} <span className="text-muted">({gruppenObjekte.length})</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="objekte-tabellen-ansicht">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Code</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Letzte Kontrolle</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{gruppenObjekte.map((objekt) => (
|
||||||
|
<tr key={objekt.id}>
|
||||||
|
<td>{objekt.code}</td>
|
||||||
|
<td>
|
||||||
|
<Link to={`/objekte/${objekt.id}/kontrolle`}>{objekt.name}</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="badge badge-neutral">{objekt.status}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{objekt.letzte_kontrolle_am
|
||||||
|
? new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")
|
||||||
|
: "Noch nie"}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Link to={`/akte/objekt/${objekt.id}`} className="text-muted">
|
||||||
|
Akte
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="card-list objekte-karten-ansicht">
|
||||||
|
{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">
|
||||||
<div className="card-title-row">
|
<div className="card-title-row">
|
||||||
@@ -103,6 +172,8 @@ export function ObjektListPage() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -696,6 +696,32 @@ select.input:focus {
|
|||||||
background: var(--color-surface-sunken);
|
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:
|
/* -- 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