v1.28: Zaehleruebersicht mit Suche und Listenansicht
This commit is contained in:
+108
-15
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Gauge, Zap, Droplets, Flame, Building2, Hash, CalendarDays, Pencil, Plus, History, TriangleAlert, Trash2 } from 'lucide-react'
|
||||
import { Gauge, Zap, Droplets, Flame, Building2, Hash, CalendarDays, Pencil, Plus, History, TriangleAlert, Trash2, Search, List, LayoutGrid } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
@@ -390,28 +390,32 @@ export default function Zaehler() {
|
||||
const { meters, units, role } = useStore()
|
||||
const [filterHaus, setFilterHaus] = useState('alle')
|
||||
const [filterArt, setFilterArt] = useState<'alle' | ZaehlerArt>('alle')
|
||||
const [suche, setSuche] = useState('')
|
||||
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('karten')
|
||||
|
||||
const gebaeude = useMemo(
|
||||
() => Array.from(new Set(meters.map((m) => m.building))).sort(),
|
||||
[meters],
|
||||
)
|
||||
|
||||
const visible = useMemo(
|
||||
() =>
|
||||
meters.filter(
|
||||
(m) =>
|
||||
(filterHaus === 'alle' || m.building === filterHaus) &&
|
||||
(filterArt === 'alle' || m.art === filterArt),
|
||||
),
|
||||
[meters, filterHaus, filterArt],
|
||||
)
|
||||
|
||||
const einheitLabel = (m: Meter) => {
|
||||
if (!m.unitId) return 'Allgemeinzähler'
|
||||
const u = units.find((x) => x.id === m.unitId)
|
||||
return u ? `Whg. ${u.number}` : m.unitId
|
||||
}
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const q = suche.trim().toLowerCase()
|
||||
return meters.filter(
|
||||
(m) =>
|
||||
(filterHaus === 'alle' || m.building === filterHaus) &&
|
||||
(filterArt === 'alle' || m.art === filterArt) &&
|
||||
(!q ||
|
||||
[m.nummer, m.art, m.building, einheitLabel(m)]
|
||||
.some((f) => f.toLowerCase().includes(q))),
|
||||
)
|
||||
}, [meters, filterHaus, filterArt, suche, units])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
@@ -426,7 +430,17 @@ export default function Zaehler() {
|
||||
{role === 'admin' && <ZaehlerDialog />}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-56 flex-1 sm:max-w-xs">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
className="pl-9"
|
||||
placeholder="Zähler suchen …"
|
||||
value={suche}
|
||||
onChange={(e) => setSuche(e.target.value)}
|
||||
aria-label="Zähler suchen"
|
||||
/>
|
||||
</div>
|
||||
<Select value={filterHaus} onValueChange={setFilterHaus}>
|
||||
<SelectTrigger className="h-9 w-auto min-w-36"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -445,14 +459,39 @@ export default function Zaehler() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex rounded-lg border p-0.5 dark:border-slate-700">
|
||||
<Button
|
||||
variant={ansicht === 'karten' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => setAnsicht('karten')}
|
||||
aria-pressed={ansicht === 'karten'}
|
||||
>
|
||||
<LayoutGrid className="h-3.5 w-3.5" /> Karten
|
||||
</Button>
|
||||
<Button
|
||||
variant={ansicht === 'liste' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => setAnsicht('liste')}
|
||||
aria-pressed={ansicht === 'liste'}
|
||||
>
|
||||
<List className="h-3.5 w-3.5" /> Liste
|
||||
</Button>
|
||||
</div>
|
||||
{suche && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{visible.length} von {meters.length} Zählern
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{visible.length === 0 ? (
|
||||
<div className="rounded-2xl border border-dashed bg-white p-10 text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
<Gauge className="mx-auto mb-3 h-8 w-8 text-slate-300 dark:text-slate-600" />
|
||||
Keine Zähler für diesen Filter.
|
||||
<Search className="mx-auto mb-3 h-8 w-8 text-slate-300 dark:text-slate-600" />
|
||||
{suche ? `Kein Zähler passt zur Suche „${suche}".` : 'Keine Zähler für diesen Filter.'}
|
||||
</div>
|
||||
) : (
|
||||
) : ansicht === 'karten' ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{visible.map((m) => {
|
||||
const Icon = artIcon(m.art)
|
||||
@@ -501,6 +540,60 @@ export default function Zaehler() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
/* ── Listenansicht ─────────────────────────────── */
|
||||
<div className="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400">
|
||||
<th className="px-3 py-2.5">Art</th>
|
||||
<th className="px-3 py-2.5">Zählernummer</th>
|
||||
<th className="hidden px-3 py-2.5 sm:table-cell">Haus</th>
|
||||
<th className="hidden px-3 py-2.5 md:table-cell">Wohneinheit</th>
|
||||
<th className="px-3 py-2.5 text-right">Stand</th>
|
||||
<th className="hidden px-3 py-2.5 lg:table-cell">Stand am</th>
|
||||
<th className="px-3 py-2.5" aria-label="Auffälligkeit" />
|
||||
<th className="px-3 py-2.5 text-right">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map((m) => {
|
||||
const Icon = artIcon(m.art)
|
||||
return (
|
||||
<tr key={m.id} className="border-b last:border-0 hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800/40">
|
||||
<td className="px-3 py-2.5">
|
||||
<Badge className={`${artStyle[m.art]} gap-1`} variant="secondary">
|
||||
<Icon className="h-3 w-3" /> {m.art}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 font-medium text-slate-800 dark:text-slate-100">{m.nummer}</td>
|
||||
<td className="hidden px-3 py-2.5 text-slate-500 dark:text-slate-400 sm:table-cell">{m.building}</td>
|
||||
<td className="hidden px-3 py-2.5 text-slate-500 dark:text-slate-400 md:table-cell">{einheitLabel(m)}</td>
|
||||
<td className="px-3 py-2.5 text-right font-semibold tabular-nums text-slate-800 dark:text-slate-100">
|
||||
{fmtStand(m.stand)}
|
||||
</td>
|
||||
<td className="hidden px-3 py-2.5 text-slate-500 dark:text-slate-400 lg:table-cell">{m.standAm}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
{m.letzteAnomalie && (
|
||||
<TriangleAlert
|
||||
className="h-4 w-4 text-amber-500"
|
||||
aria-label="Auffälligkeit im Verlauf"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<div className="flex flex-wrap justify-end gap-1.5">
|
||||
<VerlaufDialog meter={m} />
|
||||
<StandEintragenDialog meter={m} />
|
||||
{role === 'admin' && <ZaehlerDialog meter={m} />}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user