v1.15: Haus -> Wohneinheiten Struktur in der Verwaltung + Fix Haus-Umbenennung (costs FK ON UPDATE CASCADE)
This commit is contained in:
@@ -189,6 +189,25 @@ tar -czf mp-dist.tar.gz -C dist .
|
|||||||
|
|
||||||
## 8. Verlauf / offene Punkte
|
## 8. Verlauf / offene Punkte
|
||||||
|
|
||||||
|
**Erledigt 04.10. (v1.15):** Struktur „1 Haus → mehrere Wohneinheiten" in der
|
||||||
|
Verwaltung verankert + Bugfix Haus-Umbenennung. BUGFIX (wichtig): der FK
|
||||||
|
`costs_building_fkey` aus v1.14 hatte kein ON UPDATE CASCADE → jede
|
||||||
|
Haus-Umbenennung (PATCH /api/buildings/:name) schlug mit FK-Verletzung fehl
|
||||||
|
(„Interner Serverfehler"), sobald Kostenpositionen existieren. Fix in db.js:
|
||||||
|
Constraint per DO-Block idempotent auf ON UPDATE CASCADE neu gesetzt (DROP +
|
||||||
|
ADD; SCHEMA läuft als eine Simple-Query ohne Parameter, DO-Blöcke sind darin
|
||||||
|
erlaubt). E2E-Rundreise verifiziert: Umbenennen zieht units + meters +
|
||||||
|
costs mit, Rückbenennung sauber. UI (Bestand.tsx komplett überarbeitet):
|
||||||
|
Haus-Karten enthalten jetzt die 1:n-Struktur sichtbar – pro Haus
|
||||||
|
Belegungsfortschritt (Balken „X von Y Wohneinheiten angelegt", amber bei
|
||||||
|
Über-Soll, Hinweistext), eingebettete sortierte Wohnungsliste (Etage, dann
|
||||||
|
Nummer numerisch) mit Status/Mieter/Warmmiete/Bearbeiten, „+ Wohnung"
|
||||||
|
direkt pro Haus (festesHaus-Prop → Haus-Select vorbelegt und disabled,
|
||||||
|
Dialogtitel passt sich an). Globaler „Neue Wohnung"-Button oben bleibt mit
|
||||||
|
freier Hauswahl. Verwaiste Wohnungen (Haus existiert nicht) werden separat
|
||||||
|
mit Korrekturhinweis gelistet. Server: unverändert – POST/PATCH /api/units
|
||||||
|
prüft bereits, dass das Ziel-Haus in buildings existiert.
|
||||||
|
|
||||||
**Erledigt 04.10. (v1.14):** Steuermodul „Steuer & Kosten" (/kosten, Nav zwischen
|
**Erledigt 04.10. (v1.14):** Steuermodul „Steuer & Kosten" (/kosten, Nav zwischen
|
||||||
Zählerstände und Dokumente, für beide Rollen sichtbar). Neue Tabelle `costs`
|
Zählerstände und Dokumente, für beide Rollen sichtbar). Neue Tabelle `costs`
|
||||||
(building NOT NULL → buildings.name, unit_id NULL = hausweite Position, jahr,
|
(building NOT NULL → buildings.name, unit_id NULL = hausweite Position, jahr,
|
||||||
|
|||||||
@@ -153,6 +153,17 @@ CREATE TABLE IF NOT EXISTS costs (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_costs_building ON costs(building);
|
CREATE INDEX IF NOT EXISTS idx_costs_building ON costs(building);
|
||||||
CREATE INDEX IF NOT EXISTS idx_costs_unit ON costs(unit_id);
|
CREATE INDEX IF NOT EXISTS idx_costs_unit ON costs(unit_id);
|
||||||
|
-- FK hausweit: Umbenennung eines Hauses muss costs.building automatisch
|
||||||
|
-- mitziehen (sonst bricht jede Haus-Umbenennung mit FK-Verletzung ab,
|
||||||
|
-- sobald Kostenpositionen existieren). Constraint idempotent neu setzen.
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'costs_building_fkey') THEN
|
||||||
|
ALTER TABLE costs DROP CONSTRAINT costs_building_fkey;
|
||||||
|
END IF;
|
||||||
|
ALTER TABLE costs ADD CONSTRAINT costs_building_fkey
|
||||||
|
FOREIGN KEY (building) REFERENCES buildings(name) ON UPDATE CASCADE;
|
||||||
|
END $$;
|
||||||
`
|
`
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
+139
-103
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Building2, Home, MapPin, CalendarDays, ArrowUpDown, LayoutGrid, Ruler, Pencil, Plus, Users } from 'lucide-react'
|
import { Building2, Home, MapPin, CalendarDays, ArrowUpDown, LayoutGrid, Ruler, Pencil, Plus, Users, DoorOpen } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
@@ -102,7 +102,7 @@ function HausDialog({ haus }: { haus?: Building }) {
|
|||||||
<Input id="h-hoehe" inputMode="decimal" placeholder="2,65" value={etagenhoehe} onChange={(e) => setEtagenhoehe(e.target.value)} />
|
<Input id="h-hoehe" inputMode="decimal" placeholder="2,65" value={etagenhoehe} onChange={(e) => setEtagenhoehe(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="h-whg">Wohneinheiten</Label>
|
<Label htmlFor="h-whg">Wohneinheiten (Soll)</Label>
|
||||||
<Input id="h-whg" inputMode="numeric" placeholder="z. B. 12" value={wohneinheiten} onChange={(e) => setWohneinheiten(e.target.value)} />
|
<Input id="h-whg" inputMode="numeric" placeholder="z. B. 12" value={wohneinheiten} onChange={(e) => setWohneinheiten(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -127,11 +127,11 @@ function HausDialog({ haus }: { haus?: Building }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string[] }) {
|
function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; gebaeude: string[]; festesHaus?: string }) {
|
||||||
const { addWohnung, updateWohnung } = useStore()
|
const { addWohnung, updateWohnung } = useStore()
|
||||||
const bearbeiten = Boolean(wohnung)
|
const bearbeiten = Boolean(wohnung)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [building, setBuilding] = useState(wohnung?.building ?? gebaeude[0] ?? '')
|
const [building, setBuilding] = useState(wohnung?.building ?? festesHaus ?? gebaeude[0] ?? '')
|
||||||
const [number, setNumber] = useState(wohnung?.number ?? '')
|
const [number, setNumber] = useState(wohnung?.number ?? '')
|
||||||
const [floor, setFloor] = useState(String(wohnung?.floor ?? '0'))
|
const [floor, setFloor] = useState(String(wohnung?.floor ?? '0'))
|
||||||
const [size, setSize] = useState(wohnung ? String(wohnung.size).replace('.', ',') : '')
|
const [size, setSize] = useState(wohnung ? String(wohnung.size).replace('.', ',') : '')
|
||||||
@@ -170,20 +170,26 @@ function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string
|
|||||||
<Pencil className="h-3.5 w-3.5" /> Bearbeiten
|
<Pencil className="h-3.5 w-3.5" /> Bearbeiten
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button className="gap-2">
|
<Button size="sm" className="gap-1.5">
|
||||||
<Plus className="h-4 w-4" /> Neue Wohnung
|
<Plus className="h-3.5 w-3.5" /> Wohnung
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{bearbeiten ? `Wohnung ${wohnung!.building}, Whg. ${wohnung!.number} bearbeiten` : 'Neue Wohnung anlegen'}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{bearbeiten
|
||||||
|
? `Wohnung ${wohnung!.building}, Whg. ${wohnung!.number} bearbeiten`
|
||||||
|
: festesHaus
|
||||||
|
? `Neue Wohnung in „${festesHaus}" anlegen`
|
||||||
|
: 'Neue Wohnung anlegen'}
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label>Haus</Label>
|
<Label>Haus</Label>
|
||||||
<Select value={building} onValueChange={setBuilding}>
|
<Select value={building} onValueChange={setBuilding} disabled={Boolean(festesHaus)}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{gebaeude.map((g) => (
|
{gebaeude.map((g) => (
|
||||||
@@ -191,6 +197,7 @@ function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{festesHaus && <p className="text-xs text-slate-400">Die Wohnung gehört fest zu diesem Haus.</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="w-nr">Wohnungsnummer</Label>
|
<Label htmlFor="w-nr">Wohnungsnummer</Label>
|
||||||
@@ -250,36 +257,47 @@ function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wohnungen eines Hauses: Etage aufsteigend, dann Nummer numerisch. */
|
||||||
|
function sortiereWohnungen(liste: Unit[]): Unit[] {
|
||||||
|
return [...liste].sort(
|
||||||
|
(a, b) => a.floor - b.floor || a.number.localeCompare(b.number, 'de-DE', { numeric: true }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Bestand() {
|
export default function Bestand() {
|
||||||
const { buildings, units } = useStore()
|
const { buildings, units } = useStore()
|
||||||
const [filterHaus, setFilterHaus] = useState('alle')
|
|
||||||
|
|
||||||
const gebaeudeNamen = useMemo(
|
const gebaeudeNamen = useMemo(
|
||||||
() => buildings.map((b) => b.name).sort(),
|
() => buildings.map((b) => b.name).sort(),
|
||||||
[buildings],
|
[buildings],
|
||||||
)
|
)
|
||||||
|
|
||||||
const sichtbareWohnungen = useMemo(
|
// 1:n-Struktur: jede Wohnung gehört zu genau einem Haus
|
||||||
() =>
|
const wohnungenJeHaus = useMemo(() => {
|
||||||
units
|
const m = new Map<string, Unit[]>()
|
||||||
.filter((u) => filterHaus === 'alle' || u.building === filterHaus)
|
for (const u of units) {
|
||||||
.sort((a, b) => a.building.localeCompare(b.building) || a.number.localeCompare(b.number, 'de-DE', { numeric: true })),
|
const l = m.get(u.building) ?? []
|
||||||
[units, filterHaus],
|
l.push(u)
|
||||||
)
|
m.set(u.building, l)
|
||||||
|
}
|
||||||
const anzahlJeHaus = useMemo(() => {
|
for (const [k, l] of m) m.set(k, sortiereWohnungen(l))
|
||||||
const z: Record<string, number> = {}
|
return m
|
||||||
for (const u of units) z[u.building] = (z[u.building] || 0) + 1
|
|
||||||
return z
|
|
||||||
}, [units])
|
}, [units])
|
||||||
|
|
||||||
|
// Wohnungen ohne Haus (kann bei Altbeständen vorkommen) separat anzeigen
|
||||||
|
const verwaisteWohnungen = useMemo(
|
||||||
|
() => units.filter((u) => !buildings.some((b) => b.name === u.building)),
|
||||||
|
[units, buildings],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Häuser & Wohnungen</h1>
|
<h1 className="text-2xl font-bold tracking-tight">Häuser & Wohnungen</h1>
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-slate-500">
|
||||||
Bestand pflegen – Häuser anlegen und Wohnungen verwalten
|
Bestand pflegen – jedes Haus besteht aus mehreren Wohneinheiten, die hier
|
||||||
|
zugeordnet angelegt und bearbeitet werden
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -288,18 +306,21 @@ export default function Bestand() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Häuser ── */}
|
{buildings.length === 0 ? (
|
||||||
<section className="space-y-3">
|
<div className="rounded-2xl border border-dashed bg-white p-10 text-center text-sm text-slate-500">
|
||||||
<h2 className="text-lg font-semibold">Häuser</h2>
|
<Building2 className="mx-auto mb-3 h-8 w-8 text-slate-300" />
|
||||||
{buildings.length === 0 ? (
|
Noch keine Häuser angelegt – legen Sie zuerst ein Haus an, dann dessen Wohneinheiten.
|
||||||
<div className="rounded-2xl border border-dashed bg-white p-10 text-center text-sm text-slate-500">
|
</div>
|
||||||
<Building2 className="mx-auto mb-3 h-8 w-8 text-slate-300" />
|
) : (
|
||||||
Noch keine Häuser angelegt.
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
</div>
|
{buildings.map((h) => {
|
||||||
) : (
|
const wohnungen = wohnungenJeHaus.get(h.name) ?? []
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
const angelegt = wohnungen.length
|
||||||
{buildings.map((h) => (
|
const voll = angelegt === h.wohneinheiten
|
||||||
<div key={h.name} className="rounded-2xl border bg-white p-5 shadow-sm">
|
const ueberSoll = angelegt > h.wohneinheiten
|
||||||
|
return (
|
||||||
|
<div key={h.name} className="flex flex-col rounded-2xl border bg-white p-5 shadow-sm">
|
||||||
|
{/* Haus-Kopf */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="rounded-xl bg-slate-100 p-2">
|
<span className="rounded-xl bg-slate-100 p-2">
|
||||||
@@ -327,87 +348,102 @@ export default function Bestand() {
|
|||||||
<Ruler className="h-3.5 w-3.5 shrink-0" /> Etagenhöhe
|
<Ruler className="h-3.5 w-3.5 shrink-0" /> Etagenhöhe
|
||||||
</div>
|
</div>
|
||||||
<dd className="text-right font-medium text-slate-800">{h.etagenhoehe.toLocaleString('de-DE')} m</dd>
|
<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">
|
<div className="flex items-center gap-1.5 text-slate-500">
|
||||||
<Home className="h-3.5 w-3.5 shrink-0" /> Wohnfläche
|
<Home className="h-3.5 w-3.5 shrink-0" /> Wohnfläche
|
||||||
</div>
|
</div>
|
||||||
<dd className="text-right font-medium text-slate-800">{h.quadratmeter.toLocaleString('de-DE')} m²</dd>
|
<dd className="text-right font-medium text-slate-800">{h.quadratmeter.toLocaleString('de-DE')} m²</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ── Wohnungen ── */}
|
{/* Belegungsfortschritt: angelegte Wohneinheiten gegen Soll */}
|
||||||
<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">
|
<div className="mt-4">
|
||||||
|
<div className="mb-1 flex items-center justify-between text-xs">
|
||||||
|
<span className="flex items-center gap-1.5 text-slate-500">
|
||||||
|
<LayoutGrid className="h-3.5 w-3.5" /> Wohneinheiten
|
||||||
|
</span>
|
||||||
|
<span className={`font-semibold ${voll ? 'text-teal-700' : ueberSoll ? 'text-amber-600' : 'text-slate-700'}`}>
|
||||||
|
{angelegt} von {h.wohneinheiten} angelegt
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 overflow-hidden rounded-full bg-slate-100">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all ${voll ? 'bg-teal-500' : ueberSoll ? 'bg-amber-500' : 'bg-slate-400'}`}
|
||||||
|
style={{ width: `${Math.min(100, h.wohneinheiten ? (angelegt / h.wohneinheiten) * 100 : 0)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{ueberSoll && (
|
||||||
|
<p className="mt-1 text-xs text-amber-600">Achtung: mehr Wohnungen angelegt als im Soll vorgesehen.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Wohneinheiten des Hauses (1:n) */}
|
||||||
|
<div className="mt-4 border-t pt-3">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||||
|
Wohneinheiten ({angelegt})
|
||||||
|
</h3>
|
||||||
|
<WohnungDialog gebaeude={gebaeudeNamen} festesHaus={h.name} />
|
||||||
|
</div>
|
||||||
|
{angelegt === 0 ? (
|
||||||
|
<div className="rounded-xl border border-dashed py-6 text-center text-xs text-slate-400">
|
||||||
|
<DoorOpen className="mx-auto mb-2 h-6 w-6 text-slate-300" />
|
||||||
|
Noch keine Wohnungen – über „+ Wohnung" die erste Einheit anlegen.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{wohnungen.map((u) => (
|
||||||
|
<li key={u.id} className="rounded-xl border bg-slate-50/60 px-3 py-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="text-sm font-semibold text-slate-800">
|
||||||
|
Whg. {u.number}
|
||||||
|
<span className="ml-1.5 text-xs font-normal text-slate-400">
|
||||||
|
Etage {u.floor} · {u.size.toLocaleString('de-DE')} m²
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
className={u.status === 'vermietet' ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-200 text-slate-600'}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{u.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center justify-between gap-2 text-xs text-slate-500">
|
||||||
|
<span className="flex items-center gap-1 truncate">
|
||||||
|
<Users className="h-3.5 w-3.5 shrink-0" /> {u.tenant ?? '–'}
|
||||||
|
</span>
|
||||||
|
<span className="flex shrink-0 items-center gap-2">
|
||||||
|
<span className="font-medium text-slate-700">{fmtEUR.format(u.warmRent)}</span>
|
||||||
|
<WohnungDialog wohnung={u} gebaeude={gebaeudeNamen} festesHaus={h.name} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{verwaisteWohnungen.length > 0 && (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-lg font-semibold">Wohnungen ohne Hauszuordnung</h2>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
Diese Wohnungen verweisen auf ein nicht angelegtes Haus – bitte Zuordnung über „Bearbeiten" korrigieren.
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{verwaisteWohnungen.map((u) => (
|
||||||
|
<div key={u.id} className="rounded-2xl border border-amber-200 bg-amber-50/50 p-4">
|
||||||
|
<div className="font-semibold">{u.building}, Whg. {u.number}</div>
|
||||||
|
<div className="mt-2">
|
||||||
<WohnungDialog wohnung={u} gebaeude={gebaeudeNamen} />
|
<WohnungDialog wohnung={u} gebaeude={gebaeudeNamen} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</section>
|
||||||
</section>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user