v1.10+v1.11: Bestandsverwaltung (Häuser & Wohnungen) + KI-Antworten via n8n/Ollama
This commit is contained in:
@@ -9,6 +9,7 @@ import Tickets from './pages/Tickets'
|
||||
import Documents from './pages/Documents'
|
||||
import Messages from './pages/Messages'
|
||||
import Zaehler from './pages/Zaehler'
|
||||
import Bestand from './pages/Bestand'
|
||||
import BenutzerVerwaltung from './pages/BenutzerVerwaltung'
|
||||
|
||||
/** Fängt Render-Fehler ab – statt schwarzem Bildschirm: Hinweis + Reload. */
|
||||
@@ -82,6 +83,7 @@ export default function App() {
|
||||
<Route path="/mietkonto" element={<Payments />} />
|
||||
<Route path="/schadensmeldungen" element={<Tickets />} />
|
||||
<Route path="/zaehler" element={<Zaehler />} />
|
||||
<Route path="/bestand" element={<Bestand />} />
|
||||
<Route path="/dokumente" element={<Documents />} />
|
||||
<Route path="/nachrichten" element={<Messages />} />
|
||||
<Route path="/benutzer" element={<BenutzerVerwaltung />} />
|
||||
|
||||
@@ -37,7 +37,9 @@ export default function AppLayout() {
|
||||
|
||||
const alleNavItems =
|
||||
role === 'admin'
|
||||
? [...navItems, { to: '/benutzer', label: 'Benutzerverwaltung', icon: UserCog }]
|
||||
? [...navItems,
|
||||
{ to: '/bestand', label: 'Häuser & Wohnungen', icon: Building2 },
|
||||
{ to: '/benutzer', label: 'Benutzerverwaltung', icon: UserCog }]
|
||||
: navItems
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Building2, Home, MapPin, CalendarDays, ArrowUpDown, LayoutGrid, Ruler, Pencil, Plus, Users } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useStore, fmtEUR } from '../store/store'
|
||||
import type { Building, Unit } from '../types'
|
||||
|
||||
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
|
||||
const zahl = (s: string) => Number(s.replace(',', '.'))
|
||||
|
||||
function HausDialog({ haus }: { haus?: Building }) {
|
||||
const { addHaus, updateHaus } = useStore()
|
||||
const bearbeiten = Boolean(haus)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [name, setName] = useState(haus?.name ?? '')
|
||||
const [adresse, setAdresse] = useState(haus?.adresse ?? '')
|
||||
const [baujahr, setBaujahr] = useState(String(haus?.baujahr ?? ''))
|
||||
const [stockwerke, setStockwerke] = useState(String(haus?.stockwerke ?? ''))
|
||||
const [etagenhoehe, setEtagenhoehe] = useState(String(haus?.etagenhoehe ?? '').replace('.', ','))
|
||||
const [wohneinheiten, setWohneinheiten] = useState(String(haus?.wohneinheiten ?? ''))
|
||||
const [quadratmeter, setQuadratmeter] = useState(String(haus?.quadratmeter ?? '').replace('.', ','))
|
||||
const [sende, setSende] = useState(false)
|
||||
const [fehler, setFehler] = useState<string | null>(null)
|
||||
|
||||
const submit = async () => {
|
||||
if (sende) return
|
||||
setSende(true)
|
||||
setFehler(null)
|
||||
const payload = {
|
||||
name: name.trim(),
|
||||
adresse: adresse.trim(),
|
||||
baujahr: zahl(baujahr),
|
||||
stockwerke: zahl(stockwerke),
|
||||
etagenhoehe: zahl(etagenhoehe),
|
||||
wohneinheiten: zahl(wohneinheiten),
|
||||
quadratmeter: zahl(quadratmeter),
|
||||
}
|
||||
const err = bearbeiten ? await updateHaus(haus!.name, payload) : await addHaus(payload)
|
||||
setSende(false)
|
||||
if (err) return setFehler(err)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{bearbeiten ? (
|
||||
<Button variant="outline" size="sm" className="gap-1.5">
|
||||
<Pencil className="h-3.5 w-3.5" /> Bearbeiten
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Neues Haus
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{bearbeiten ? `Haus „${haus!.name}" bearbeiten` : 'Neues Haus anlegen'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-name">Hausname</Label>
|
||||
<Input id="h-name" placeholder="z. B. Lindenallee 8" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-adr">Adresse</Label>
|
||||
<Input id="h-adr" placeholder="Straße Nr., PLZ Ort" value={adresse} onChange={(e) => setAdresse(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-bau">Baujahr</Label>
|
||||
<Input id="h-bau" inputMode="numeric" placeholder="z. B. 1998" value={baujahr} onChange={(e) => setBaujahr(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-stock">Stockwerke</Label>
|
||||
<Input id="h-stock" inputMode="numeric" placeholder="z. B. 3" value={stockwerke} onChange={(e) => setStockwerke(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-hoehe">Etagenhöhe (m)</Label>
|
||||
<Input id="h-hoehe" inputMode="decimal" placeholder="2,65" value={etagenhoehe} onChange={(e) => setEtagenhoehe(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-whg">Wohneinheiten</Label>
|
||||
<Input id="h-whg" inputMode="numeric" placeholder="z. B. 12" value={wohneinheiten} onChange={(e) => setWohneinheiten(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="h-qm">Quadratmeter (m²)</Label>
|
||||
<Input id="h-qm" inputMode="decimal" placeholder="z. B. 980" value={quadratmeter} onChange={(e) => setQuadratmeter(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
{fehler && (
|
||||
<div className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-300" role="alert">
|
||||
{fehler}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
|
||||
<Button onClick={submit} disabled={sende || !name.trim() || !adresse.trim() || !baujahr || !stockwerke || !etagenhoehe || !wohneinheiten || !quadratmeter}>
|
||||
{sende ? 'Wird gespeichert …' : bearbeiten ? 'Speichern' : 'Haus anlegen'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string[] }) {
|
||||
const { addWohnung, updateWohnung } = useStore()
|
||||
const bearbeiten = Boolean(wohnung)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [building, setBuilding] = useState(wohnung?.building ?? gebaeude[0] ?? '')
|
||||
const [number, setNumber] = useState(wohnung?.number ?? '')
|
||||
const [floor, setFloor] = useState(String(wohnung?.floor ?? '0'))
|
||||
const [size, setSize] = useState(wohnung ? String(wohnung.size).replace('.', ',') : '')
|
||||
const [coldRent, setColdRent] = useState(wohnung ? String(wohnung.coldRent).replace('.', ',') : '')
|
||||
const [warmRent, setWarmRent] = useState(wohnung ? String(wohnung.warmRent).replace('.', ',') : '')
|
||||
const [tenant, setTenant] = useState(wohnung?.tenant ?? '')
|
||||
const [status, setStatus] = useState<'vermietet' | 'frei'>(wohnung?.status ?? 'frei')
|
||||
const [sende, setSende] = useState(false)
|
||||
const [fehler, setFehler] = useState<string | null>(null)
|
||||
|
||||
const submit = async () => {
|
||||
if (sende) return
|
||||
setSende(true)
|
||||
setFehler(null)
|
||||
const payload = {
|
||||
building,
|
||||
number: number.trim(),
|
||||
floor: zahl(floor),
|
||||
size: zahl(size),
|
||||
coldRent: zahl(coldRent),
|
||||
warmRent: zahl(warmRent),
|
||||
tenant: tenant.trim(),
|
||||
status,
|
||||
}
|
||||
const err = bearbeiten ? await updateWohnung(wohnung!.id, payload) : await addWohnung(payload)
|
||||
setSende(false)
|
||||
if (err) return setFehler(err)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{bearbeiten ? (
|
||||
<Button variant="outline" size="sm" className="gap-1.5">
|
||||
<Pencil className="h-3.5 w-3.5" /> Bearbeiten
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Neue Wohnung
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{bearbeiten ? `Wohnung ${wohnung!.building}, Whg. ${wohnung!.number} bearbeiten` : 'Neue Wohnung anlegen'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Haus</Label>
|
||||
<Select value={building} onValueChange={setBuilding}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{gebaeude.map((g) => (
|
||||
<SelectItem key={g} value={g}>{g}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="w-nr">Wohnungsnummer</Label>
|
||||
<Input id="w-nr" placeholder="z. B. 12" value={number} onChange={(e) => setNumber(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="w-etage">Etage</Label>
|
||||
<Input id="w-etage" inputMode="numeric" placeholder="0 = Erdgeschoss" value={floor} onChange={(e) => setFloor(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="w-qm">Wohnfläche (m²)</Label>
|
||||
<Input id="w-qm" inputMode="decimal" placeholder="z. B. 68,5" value={size} onChange={(e) => setSize(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="w-kalt">Kaltmiete (€)</Label>
|
||||
<Input id="w-kalt" inputMode="decimal" placeholder="z. B. 520" value={coldRent} onChange={(e) => setColdRent(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="w-warm">Warmmiete (€)</Label>
|
||||
<Input id="w-warm" inputMode="decimal" placeholder="z. B. 690" value={warmRent} onChange={(e) => setWarmRent(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="w-mieter">Mieter (Name)</Label>
|
||||
<Input id="w-mieter" placeholder="leer = frei" value={tenant} onChange={(e) => setTenant(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>Status</Label>
|
||||
<Select value={status} onValueChange={(v) => setStatus(v as 'vermietet' | 'frei')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="vermietet">vermietet</SelectItem>
|
||||
<SelectItem value="frei">frei</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{fehler && (
|
||||
<div className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-300" role="alert">
|
||||
{fehler}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
|
||||
<Button onClick={submit} disabled={sende || !building || !number.trim() || !size || !coldRent || !warmRent}>
|
||||
{sende ? 'Wird gespeichert …' : bearbeiten ? 'Speichern' : 'Wohnung anlegen'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Bestand() {
|
||||
const { buildings, units } = useStore()
|
||||
const [filterHaus, setFilterHaus] = useState('alle')
|
||||
|
||||
const gebaeudeNamen = useMemo(
|
||||
() => buildings.map((b) => b.name).sort(),
|
||||
[buildings],
|
||||
)
|
||||
|
||||
const sichtbareWohnungen = useMemo(
|
||||
() =>
|
||||
units
|
||||
.filter((u) => filterHaus === 'alle' || u.building === filterHaus)
|
||||
.sort((a, b) => a.building.localeCompare(b.building) || a.number.localeCompare(b.number, 'de-DE', { numeric: true })),
|
||||
[units, filterHaus],
|
||||
)
|
||||
|
||||
const anzahlJeHaus = useMemo(() => {
|
||||
const z: Record<string, number> = {}
|
||||
for (const u of units) z[u.building] = (z[u.building] || 0) + 1
|
||||
return z
|
||||
}, [units])
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Häuser & Wohnungen</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
Bestand pflegen – Häuser anlegen und Wohnungen verwalten
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<WohnungDialog gebaeude={gebaeudeNamen} />
|
||||
<HausDialog />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Häuser ── */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold">Häuser</h2>
|
||||
{buildings.length === 0 ? (
|
||||
<div className="rounded-2xl border border-dashed bg-white p-10 text-center text-sm text-slate-500">
|
||||
<Building2 className="mx-auto mb-3 h-8 w-8 text-slate-300" />
|
||||
Noch keine Häuser angelegt.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{buildings.map((h) => (
|
||||
<div key={h.name} className="rounded-2xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-xl bg-slate-100 p-2">
|
||||
<Building2 className="h-5 w-5 text-slate-600" />
|
||||
</span>
|
||||
<div>
|
||||
<div className="font-semibold">{h.name}</div>
|
||||
<div className="flex items-center gap-1 text-xs text-slate-500">
|
||||
<MapPin className="h-3 w-3" /> {h.adresse}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HausDialog haus={h} />
|
||||
</div>
|
||||
<dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-slate-500">
|
||||
<CalendarDays className="h-3.5 w-3.5 shrink-0" /> Baujahr
|
||||
</div>
|
||||
<dd className="text-right font-medium text-slate-800">{h.baujahr}</dd>
|
||||
<div className="flex items-center gap-1.5 text-slate-500">
|
||||
<ArrowUpDown className="h-3.5 w-3.5 shrink-0" /> Stockwerke
|
||||
</div>
|
||||
<dd className="text-right font-medium text-slate-800">{h.stockwerke}</dd>
|
||||
<div className="flex items-center gap-1.5 text-slate-500">
|
||||
<Ruler className="h-3.5 w-3.5 shrink-0" /> Etagenhöhe
|
||||
</div>
|
||||
<dd className="text-right font-medium text-slate-800">{h.etagenhoehe.toLocaleString('de-DE')} m</dd>
|
||||
<div className="flex items-center gap-1.5 text-slate-500">
|
||||
<LayoutGrid className="h-3.5 w-3.5 shrink-0" /> Wohneinheiten
|
||||
</div>
|
||||
<dd className="text-right font-medium text-slate-800">
|
||||
{h.wohneinheiten}
|
||||
<span className="text-slate-400"> (angelegt: {anzahlJeHaus[h.name] ?? 0})</span>
|
||||
</dd>
|
||||
<div className="flex items-center gap-1.5 text-slate-500">
|
||||
<Home className="h-3.5 w-3.5 shrink-0" /> Wohnfläche
|
||||
</div>
|
||||
<dd className="text-right font-medium text-slate-800">{h.quadratmeter.toLocaleString('de-DE')} m²</dd>
|
||||
</dl>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ── Wohnungen ── */}
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 className="text-lg font-semibold">Wohnungen</h2>
|
||||
<Select value={filterHaus} onValueChange={setFilterHaus}>
|
||||
<SelectTrigger className="h-9 w-auto min-w-40"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alle">Alle Häuser</SelectItem>
|
||||
{gebaeudeNamen.map((g) => (
|
||||
<SelectItem key={g} value={g}>{g}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{sichtbareWohnungen.length === 0 ? (
|
||||
<div className="rounded-2xl border border-dashed bg-white p-10 text-center text-sm text-slate-500">
|
||||
<Home className="mx-auto mb-3 h-8 w-8 text-slate-300" />
|
||||
Keine Wohnungen für diesen Filter.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{sichtbareWohnungen.map((u) => (
|
||||
<div key={u.id} className="rounded-2xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
{u.building}, Whg. {u.number}
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1 text-xs text-slate-500">
|
||||
<Building2 className="h-3 w-3" /> Etage {u.floor} · {u.size.toLocaleString('de-DE')} m²
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={u.status === 'vermietet' ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-600'}
|
||||
variant="secondary"
|
||||
>
|
||||
{u.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<dl className="mt-3 space-y-1.5 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-slate-500">Kaltmiete</dt>
|
||||
<dd className="font-medium text-slate-800">{fmtEUR.format(u.coldRent)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-slate-500">Warmmiete</dt>
|
||||
<dd className="font-medium text-slate-800">{fmtEUR.format(u.warmRent)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="flex items-center gap-1 text-slate-500">
|
||||
<Users className="h-3.5 w-3.5" /> Mieter
|
||||
</dt>
|
||||
<dd className="font-medium text-slate-800">{u.tenant ?? '–'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="mt-4">
|
||||
<WohnungDialog wohnung={u} gebaeude={gebaeudeNamen} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
Announcement,
|
||||
Meter,
|
||||
ZaehlerArt,
|
||||
Building,
|
||||
} from '../types'
|
||||
import { apiGet, apiPost, apiPatch, ApiFehler } from '../lib/api'
|
||||
import { sha256, DEMO_PASSWORT } from '../lib/auth'
|
||||
@@ -49,6 +50,7 @@ interface Bootstrap {
|
||||
benutzer: Benutzer[]
|
||||
codes: RegistrierungsCode[]
|
||||
meters: Meter[]
|
||||
buildings: Building[]
|
||||
}
|
||||
|
||||
const LEER: Bootstrap = {
|
||||
@@ -61,6 +63,7 @@ const LEER: Bootstrap = {
|
||||
benutzer: [],
|
||||
codes: [],
|
||||
meters: [],
|
||||
buildings: [],
|
||||
}
|
||||
|
||||
interface Store extends Bootstrap {
|
||||
@@ -107,6 +110,20 @@ interface Store extends Bootstrap {
|
||||
id: string,
|
||||
z: Partial<{ nummer: string; art: ZaehlerArt; stand: number; unitId: string | null; building: string }>,
|
||||
) => Promise<string | null>
|
||||
// ── Bestand: Häuser & Wohnungen (Verwaltung) ────────────
|
||||
addHaus: (h: Omit<Building, 'name'> & { name: string }) => Promise<string | null>
|
||||
updateHaus: (name: string, h: Partial<Building>) => Promise<string | null>
|
||||
addWohnung: (u: {
|
||||
building: string
|
||||
number: string
|
||||
floor: number
|
||||
size: number
|
||||
coldRent: number
|
||||
warmRent: number
|
||||
tenant: string
|
||||
status: 'vermietet' | 'frei'
|
||||
}) => Promise<string | null>
|
||||
updateWohnung: (id: string, u: Partial<Unit>) => Promise<string | null>
|
||||
}
|
||||
|
||||
const StoreContext = createContext<Store | null>(null)
|
||||
@@ -294,6 +311,43 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
// ── Bestand: Häuser & Wohnungen ─────────────────────────
|
||||
async addHaus(h) {
|
||||
try {
|
||||
const { building } = await apiPost<{ building: Building }>('/api/buildings', h)
|
||||
setDaten((d) => ({ ...d, buildings: [...d.buildings, building].sort((a, b) => a.name.localeCompare(b.name)) }))
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async updateHaus(name, h) {
|
||||
try {
|
||||
await apiPatch<{ building: Building }>(`/api/buildings/${encodeURIComponent(name)}`, h)
|
||||
await ladeAlles() // Umbenennung zieht Wohnungen/Zähler mit – komplette Neuladung
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async addWohnung(u) {
|
||||
try {
|
||||
const { unit } = await apiPost<{ unit: Unit }>('/api/units', u)
|
||||
setDaten((d) => ({ ...d, units: [...d.units, unit] }))
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async updateWohnung(id, u) {
|
||||
try {
|
||||
const { unit } = await apiPatch<{ unit: Unit }>(`/api/units/${id}`, u)
|
||||
setDaten((d) => ({ ...d, units: d.units.map((x) => (x.id === id ? unit : x)) }))
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
}), [daten, bereit, me, role, meineEinheit, ladeAlles])
|
||||
|
||||
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>
|
||||
|
||||
@@ -98,6 +98,16 @@ export interface Announcement {
|
||||
|
||||
export type ZaehlerArt = 'Strom' | 'Wasser' | 'Gas' | 'Wärme' | 'Sonstiges'
|
||||
|
||||
export interface Building {
|
||||
name: string
|
||||
adresse: string
|
||||
baujahr: number
|
||||
stockwerke: number
|
||||
etagenhoehe: number // Deckenhöhe in m
|
||||
wohneinheiten: number
|
||||
quadratmeter: number
|
||||
}
|
||||
|
||||
export interface Meter {
|
||||
id: string
|
||||
unitId: string | null // null = Haus-/Allgemeinzähler
|
||||
|
||||
Reference in New Issue
Block a user