diff --git a/kimi.md b/kimi.md index c263c0a..c356733 100644 --- a/kimi.md +++ b/kimi.md @@ -189,6 +189,25 @@ tar -czf mp-dist.tar.gz -C dist . ## 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 Zählerstände und Dokumente, für beide Rollen sichtbar). Neue Tabelle `costs` (building NOT NULL → buildings.name, unit_id NULL = hausweite Position, jahr, diff --git a/server/src/db.js b/server/src/db.js index cb31565..c68b7fb 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -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_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() { diff --git a/src/pages/Bestand.tsx b/src/pages/Bestand.tsx index f01934f..1106531 100644 --- a/src/pages/Bestand.tsx +++ b/src/pages/Bestand.tsx @@ -1,5 +1,5 @@ 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 { Badge } from '@/components/ui/badge' import { @@ -102,7 +102,7 @@ function HausDialog({ haus }: { haus?: Building }) { setEtagenhoehe(e.target.value)} />
- + setWohneinheiten(e.target.value)} />
@@ -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 bearbeiten = Boolean(wohnung) 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 [floor, setFloor] = useState(String(wohnung?.floor ?? '0')) const [size, setSize] = useState(wohnung ? String(wohnung.size).replace('.', ',') : '') @@ -170,20 +170,26 @@ function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string Bearbeiten ) : ( - )} - {bearbeiten ? `Wohnung ${wohnung!.building}, Whg. ${wohnung!.number} bearbeiten` : 'Neue Wohnung anlegen'} + + {bearbeiten + ? `Wohnung ${wohnung!.building}, Whg. ${wohnung!.number} bearbeiten` + : festesHaus + ? `Neue Wohnung in „${festesHaus}" anlegen` + : 'Neue Wohnung anlegen'} +
- {gebaeude.map((g) => ( @@ -191,6 +197,7 @@ function WohnungDialog({ wohnung, gebaeude }: { wohnung?: Unit; gebaeude: string ))} + {festesHaus &&

Die Wohnung gehört fest zu diesem Haus.

}
@@ -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() { 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 = {} - for (const u of units) z[u.building] = (z[u.building] || 0) + 1 - return z + // 1:n-Struktur: jede Wohnung gehört zu genau einem Haus + const wohnungenJeHaus = useMemo(() => { + const m = new Map() + for (const u of units) { + const l = m.get(u.building) ?? [] + l.push(u) + m.set(u.building, l) + } + for (const [k, l] of m) m.set(k, sortiereWohnungen(l)) + return m }, [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 (

Häuser & Wohnungen

- Bestand pflegen – Häuser anlegen und Wohnungen verwalten + Bestand pflegen – jedes Haus besteht aus mehreren Wohneinheiten, die hier + zugeordnet angelegt und bearbeitet werden

@@ -288,18 +306,21 @@ export default function Bestand() {
- {/* ── Häuser ── */} -
-

Häuser

- {buildings.length === 0 ? ( -
- - Noch keine Häuser angelegt. -
- ) : ( -
- {buildings.map((h) => ( -
+ {buildings.length === 0 ? ( +
+ + Noch keine Häuser angelegt – legen Sie zuerst ein Haus an, dann dessen Wohneinheiten. +
+ ) : ( +
+ {buildings.map((h) => { + const wohnungen = wohnungenJeHaus.get(h.name) ?? [] + const angelegt = wohnungen.length + const voll = angelegt === h.wohneinheiten + const ueberSoll = angelegt > h.wohneinheiten + return ( +
+ {/* Haus-Kopf */}
@@ -327,87 +348,102 @@ export default function Bestand() { Etagenhöhe
{h.etagenhoehe.toLocaleString('de-DE')} m
-
- Wohneinheiten -
-
- {h.wohneinheiten} - (angelegt: {anzahlJeHaus[h.name] ?? 0}) -
Wohnfläche
{h.quadratmeter.toLocaleString('de-DE')} m²
-
- ))} -
- )} -
- {/* ── Wohnungen ── */} -
-
-

Wohnungen

- -
- {sichtbareWohnungen.length === 0 ? ( -
- - Keine Wohnungen für diesen Filter. -
- ) : ( -
- {sichtbareWohnungen.map((u) => ( -
-
-
-
- {u.building}, Whg. {u.number} -
-
- Etage {u.floor} · {u.size.toLocaleString('de-DE')} m² -
-
- - {u.status} - -
-
-
-
Kaltmiete
-
{fmtEUR.format(u.coldRent)}
-
-
-
Warmmiete
-
{fmtEUR.format(u.warmRent)}
-
-
-
- Mieter -
-
{u.tenant ?? '–'}
-
-
+ {/* Belegungsfortschritt: angelegte Wohneinheiten gegen Soll */}
+
+ + Wohneinheiten + + + {angelegt} von {h.wohneinheiten} angelegt + +
+
+
+
+ {ueberSoll && ( +

Achtung: mehr Wohnungen angelegt als im Soll vorgesehen.

+ )} +
+ + {/* Wohneinheiten des Hauses (1:n) */} +
+
+

+ Wohneinheiten ({angelegt}) +

+ +
+ {angelegt === 0 ? ( +
+ + Noch keine Wohnungen – über „+ Wohnung" die erste Einheit anlegen. +
+ ) : ( +
    + {wohnungen.map((u) => ( +
  • +
    +
    + Whg. {u.number} + + Etage {u.floor} · {u.size.toLocaleString('de-DE')} m² + +
    + + {u.status} + +
    +
    + + {u.tenant ?? '–'} + + + {fmtEUR.format(u.warmRent)} + + +
    +
  • + ))} +
+ )} +
+
+ ) + })} +
+ )} + + {verwaisteWohnungen.length > 0 && ( +
+

Wohnungen ohne Hauszuordnung

+

+ Diese Wohnungen verweisen auf ein nicht angelegtes Haus – bitte Zuordnung über „Bearbeiten" korrigieren. +

+
+ {verwaisteWohnungen.map((u) => ( +
+
{u.building}, Whg. {u.number}
+
))}
- )} -
+
+ )}
) }