v1.29: Heizungsart-Dropdown (Haus + Wohnung), Listenansicht als Standard
This commit is contained in:
@@ -189,6 +189,18 @@ tar -czf mp-dist.tar.gz -C dist .
|
|||||||
|
|
||||||
## 8. Verlauf / offene Punkte
|
## 8. Verlauf / offene Punkte
|
||||||
|
|
||||||
|
**Erledigt 04.10. (v1.29):** Heizungsart-Dropdown in Haus- und Wohnungsverwaltung;
|
||||||
|
Listenansicht ist jetzt Standard in allen Modulen. DB: buildings + units je
|
||||||
|
ALTER heizungsart TEXT DEFAULT ''; Server HEIZUNGSARTEN-Liste + heizungsartValid
|
||||||
|
(leer oder Listenwert), POST/PATCH /api/buildings und /api/units persistieren
|
||||||
|
das Feld, mapBuilding/mapUnit liefern es (Fallback ''). Frontend:
|
||||||
|
HEIZUNGSARTEN/Heizungsart in types (9 gängige Formen), Select „Nicht angegeben"
|
||||||
|
Sentinel '__keine' in beiden Dialogen; Wohnungs-Neuanlage übernimmt die
|
||||||
|
Heizungsart automatisch aus dem gewählten Haus (waehleHaus). Anzeige: Haus-Karte
|
||||||
|
(dl-Zeile mit Flame-Icon, nur wenn gesetzt) + Listen-Detailzeile.
|
||||||
|
Listenansicht-Default: Bestand.tsx + Zaehler.tsx ansicht-useState 'liste'.
|
||||||
|
Anleitung aktualisiert.
|
||||||
|
|
||||||
**Erledigt 04.10. (v1.28):** Zählerübersicht mit Suche + Listenansicht
|
**Erledigt 04.10. (v1.28):** Zählerübersicht mit Suche + Listenansicht
|
||||||
(Analogie zur Hausübersicht v1.26): Suchfeld über Nummer/Art/Haus/Wohneinheit
|
(Analogie zur Hausübersicht v1.26): Suchfeld über Nummer/Art/Haus/Wohneinheit
|
||||||
(mit Trefferzähler), Toggle Karten/Liste; Liste = Tabelle (Art-Badge mit Icon,
|
(mit Trefferzähler), Toggle Karten/Liste; Liste = Tabelle (Art-Badge mit Icon,
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ ALTER TABLE buildings ADD COLUMN IF NOT EXISTS ort TEXT NOT NULL DEFAULT '';
|
|||||||
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS lage TEXT NOT NULL DEFAULT '';
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS lage TEXT NOT NULL DEFAULT '';
|
||||||
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS wohnflaeche NUMERIC(9,1);
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS wohnflaeche NUMERIC(9,1);
|
||||||
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS grundstuecksflaeche NUMERIC(9,1);
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS grundstuecksflaeche NUMERIC(9,1);
|
||||||
|
-- v1.29: Heizungsart (Dropdown) in Haus- und Wohnungsverwaltung
|
||||||
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS heizungsart TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE units ADD COLUMN IF NOT EXISTS heizungsart TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS costs (
|
CREATE TABLE IF NOT EXISTS costs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
|||||||
+29
-12
@@ -156,7 +156,7 @@ async function sessionAnlegen(res, userId, req) {
|
|||||||
const mapUnit = (u) => ({
|
const mapUnit = (u) => ({
|
||||||
id: u.id, building: u.building, number: u.number, floor: u.floor,
|
id: u.id, building: u.building, number: u.number, floor: u.floor,
|
||||||
size: Number(u.size), coldRent: Number(u.cold_rent), warmRent: Number(u.warm_rent),
|
size: Number(u.size), coldRent: Number(u.cold_rent), warmRent: Number(u.warm_rent),
|
||||||
tenant: u.tenant, status: u.status,
|
tenant: u.tenant, status: u.status, heizungsart: u.heizungsart ?? '',
|
||||||
})
|
})
|
||||||
const mapTransaction = (t) => ({
|
const mapTransaction = (t) => ({
|
||||||
id: t.id, unitId: t.unit_id, tenant: t.tenant, month: t.month, year: t.year,
|
id: t.id, unitId: t.unit_id, tenant: t.tenant, month: t.month, year: t.year,
|
||||||
@@ -194,6 +194,7 @@ const mapBuilding = (b) => ({
|
|||||||
wohnflaeche: Number(b.wohnflaeche ?? b.quadratmeter),
|
wohnflaeche: Number(b.wohnflaeche ?? b.quadratmeter),
|
||||||
grundstuecksflaeche: b.grundstuecksflaeche === null || b.grundstuecksflaeche === undefined
|
grundstuecksflaeche: b.grundstuecksflaeche === null || b.grundstuecksflaeche === undefined
|
||||||
? null : Number(b.grundstuecksflaeche),
|
? null : Number(b.grundstuecksflaeche),
|
||||||
|
heizungsart: b.heizungsart ?? '',
|
||||||
})
|
})
|
||||||
const mapKosten = (k) => ({
|
const mapKosten = (k) => ({
|
||||||
id: k.id, building: k.building, unitId: k.unit_id, jahr: k.jahr,
|
id: k.id, building: k.building, unitId: k.unit_id, jahr: k.jahr,
|
||||||
@@ -549,6 +550,14 @@ app.patch('/api/tickets/:id', auth, adminOnly, async (req, res, next) => {
|
|||||||
|
|
||||||
// ── Zählerstände (Strom / Wasser / Gas) ─────────────────────
|
// ── Zählerstände (Strom / Wasser / Gas) ─────────────────────
|
||||||
const ZAEHLER_ARTEN = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges']
|
const ZAEHLER_ARTEN = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges']
|
||||||
|
// Gängige Heizungsformen (v1.29) – Dropdown in Haus-/Wohnungsformular
|
||||||
|
const HEIZUNGSARTEN = [
|
||||||
|
'Gas-Zentralheizung', 'Öl-Zentralheizung', 'Fernwärme', 'Wärmepumpe',
|
||||||
|
'Pellets-/Holzheizung', 'Nachtspeicherheizung', 'Elektro-Direktheizung',
|
||||||
|
'Einzelraumheizung (Ofen/Kamin)', 'Sonstiges',
|
||||||
|
]
|
||||||
|
/** Heizungsart validieren: leer (nicht angegeben) oder Listenwert. */
|
||||||
|
const heizungsartValid = (h) => !h || HEIZUNGSARTEN.includes(h)
|
||||||
|
|
||||||
app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
|
app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -1094,14 +1103,16 @@ app.post('/api/buildings', auth, adminOnly, async (req, res, next) => {
|
|||||||
const vorhanden = await pool.query('SELECT 1 FROM buildings WHERE name = $1', [name])
|
const vorhanden = await pool.query('SELECT 1 FROM buildings WHERE name = $1', [name])
|
||||||
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Ein Haus mit dieser Straße und Hausnummer existiert bereits.' })
|
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Ein Haus mit dieser Straße und Hausnummer existiert bereits.' })
|
||||||
const grund = b.grundstuecksflaeche ? Number(b.grundstuecksflaeche) : null
|
const grund = b.grundstuecksflaeche ? Number(b.grundstuecksflaeche) : null
|
||||||
|
if (!heizungsartValid(String(b.heizungsart || '').trim())) return res.status(400).json({ fehler: 'Ungültige Heizungsart.' })
|
||||||
|
const heizung = String(b.heizungsart || '').trim()
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO buildings (name, adresse, strasse, hausnummer, plz, ort, lage,
|
`INSERT INTO buildings (name, adresse, strasse, hausnummer, plz, ort, lage,
|
||||||
baujahr, stockwerke, etagenhoehe, wohneinheiten, wohnflaeche, grundstuecksflaeche)
|
baujahr, stockwerke, etagenhoehe, wohneinheiten, wohnflaeche, grundstuecksflaeche, heizungsart)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`,
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`,
|
||||||
[name, hausAdresseAus(b), String(b.strasse).trim(), String(b.hausnummer).trim(),
|
[name, hausAdresseAus(b), String(b.strasse).trim(), String(b.hausnummer).trim(),
|
||||||
String(b.plz).trim(), String(b.ort).trim(), String(b.lage || '').trim(),
|
String(b.plz).trim(), String(b.ort).trim(), String(b.lage || '').trim(),
|
||||||
Number(b.baujahr), Number(b.stockwerke), Number(b.etagenhoehe),
|
Number(b.baujahr), Number(b.stockwerke), Number(b.etagenhoehe),
|
||||||
Number(b.wohneinheiten), Number(b.wohnflaeche), grund],
|
Number(b.wohneinheiten), Number(b.wohnflaeche), grund, heizung],
|
||||||
)
|
)
|
||||||
res.json({ building: mapBuilding(rows[0]) })
|
res.json({ building: mapBuilding(rows[0]) })
|
||||||
} catch (e) { next(e) }
|
} catch (e) { next(e) }
|
||||||
@@ -1122,18 +1133,20 @@ app.patch('/api/buildings/:name', auth, adminOnly, async (req, res, next) => {
|
|||||||
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Ein Haus mit dieser Straße und Hausnummer existiert bereits.' })
|
if (vorhanden.rows.length) return res.status(400).json({ fehler: 'Ein Haus mit dieser Straße und Hausnummer existiert bereits.' })
|
||||||
}
|
}
|
||||||
const grund = b.grundstuecksflaeche ? Number(b.grundstuecksflaeche) : null
|
const grund = b.grundstuecksflaeche ? Number(b.grundstuecksflaeche) : null
|
||||||
|
if (!heizungsartValid(String(b.heizungsart || '').trim())) return res.status(400).json({ fehler: 'Ungültige Heizungsart.' })
|
||||||
|
const heizung = String(b.heizungsart || '').trim()
|
||||||
const client = await pool.connect()
|
const client = await pool.connect()
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN')
|
await client.query('BEGIN')
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE buildings SET name = $1, adresse = $2, strasse = $3, hausnummer = $4, plz = $5, ort = $6, lage = $7,
|
`UPDATE buildings SET name = $1, adresse = $2, strasse = $3, hausnummer = $4, plz = $5, ort = $6, lage = $7,
|
||||||
baujahr = $8, stockwerke = $9, etagenhoehe = $10, wohneinheiten = $11,
|
baujahr = $8, stockwerke = $9, etagenhoehe = $10, wohneinheiten = $11,
|
||||||
wohnflaeche = $12, grundstuecksflaeche = $13
|
wohnflaeche = $12, grundstuecksflaeche = $13, heizungsart = $14
|
||||||
WHERE name = $14`,
|
WHERE name = $15`,
|
||||||
[neuName, hausAdresseAus(b), String(b.strasse).trim(), String(b.hausnummer).trim(),
|
[neuName, hausAdresseAus(b), String(b.strasse).trim(), String(b.hausnummer).trim(),
|
||||||
String(b.plz).trim(), String(b.ort).trim(), String(b.lage || '').trim(),
|
String(b.plz).trim(), String(b.ort).trim(), String(b.lage || '').trim(),
|
||||||
Number(b.baujahr), Number(b.stockwerke), Number(b.etagenhoehe), Number(b.wohneinheiten),
|
Number(b.baujahr), Number(b.stockwerke), Number(b.etagenhoehe), Number(b.wohneinheiten),
|
||||||
Number(b.wohnflaeche), grund, req.params.name],
|
Number(b.wohnflaeche), grund, heizung, req.params.name],
|
||||||
)
|
)
|
||||||
// Haus umbenannt → Bezüge in Wohnungen und Zählern mitziehen
|
// Haus umbenannt → Bezüge in Wohnungen und Zählern mitziehen
|
||||||
if (neuName !== alt.name) {
|
if (neuName !== alt.name) {
|
||||||
@@ -1168,12 +1181,14 @@ app.post('/api/units', auth, adminOnly, async (req, res, next) => {
|
|||||||
if (!Number.isFinite(coldRent) || coldRent < 0) return res.status(400).json({ fehler: 'Kaltmiete ungültig.' })
|
if (!Number.isFinite(coldRent) || coldRent < 0) return res.status(400).json({ fehler: 'Kaltmiete ungültig.' })
|
||||||
if (!Number.isFinite(warmRent) || warmRent < 0) return res.status(400).json({ fehler: 'Warmmiete ungültig.' })
|
if (!Number.isFinite(warmRent) || warmRent < 0) return res.status(400).json({ fehler: 'Warmmiete ungültig.' })
|
||||||
const status = u.status === 'frei' ? 'frei' : 'vermietet'
|
const status = u.status === 'frei' ? 'frei' : 'vermietet'
|
||||||
|
if (!heizungsartValid(String(u.heizungsart || '').trim())) return res.status(400).json({ fehler: 'Ungültige Heizungsart.' })
|
||||||
|
const heizung = String(u.heizungsart || '').trim()
|
||||||
const id = `u-${Date.now()}`
|
const id = `u-${Date.now()}`
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO units (id, building, number, floor, size, cold_rent, warm_rent, tenant, status)
|
`INSERT INTO units (id, building, number, floor, size, cold_rent, warm_rent, tenant, status, heizungsart)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`,
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`,
|
||||||
[id, building, String(u.number).trim(), floor, size, coldRent, warmRent,
|
[id, building, String(u.number).trim(), floor, size, coldRent, warmRent,
|
||||||
String(u.tenant || '').trim() || null, status],
|
String(u.tenant || '').trim() || null, status, heizung],
|
||||||
)
|
)
|
||||||
res.json({ unit: mapUnit(rows[0]) })
|
res.json({ unit: mapUnit(rows[0]) })
|
||||||
} catch (e) { next(e) }
|
} catch (e) { next(e) }
|
||||||
@@ -1198,11 +1213,13 @@ app.patch('/api/units/:id', auth, adminOnly, async (req, res, next) => {
|
|||||||
if (!Number.isFinite(coldRent) || coldRent < 0) return res.status(400).json({ fehler: 'Kaltmiete ungültig.' })
|
if (!Number.isFinite(coldRent) || coldRent < 0) return res.status(400).json({ fehler: 'Kaltmiete ungültig.' })
|
||||||
if (!Number.isFinite(warmRent) || warmRent < 0) return res.status(400).json({ fehler: 'Warmmiete ungültig.' })
|
if (!Number.isFinite(warmRent) || warmRent < 0) return res.status(400).json({ fehler: 'Warmmiete ungültig.' })
|
||||||
const status = u.status === 'frei' ? 'frei' : 'vermietet'
|
const status = u.status === 'frei' ? 'frei' : 'vermietet'
|
||||||
|
if (!heizungsartValid(String(u.heizungsart || '').trim())) return res.status(400).json({ fehler: 'Ungültige Heizungsart.' })
|
||||||
|
const heizung = String(u.heizungsart || '').trim()
|
||||||
const { rows: neu } = await pool.query(
|
const { rows: neu } = await pool.query(
|
||||||
`UPDATE units SET building = $1, number = $2, floor = $3, size = $4, cold_rent = $5,
|
`UPDATE units SET building = $1, number = $2, floor = $3, size = $4, cold_rent = $5,
|
||||||
warm_rent = $6, tenant = $7, status = $8 WHERE id = $9 RETURNING *`,
|
warm_rent = $6, tenant = $7, status = $8, heizungsart = $9 WHERE id = $10 RETURNING *`,
|
||||||
[building, String(u.number).trim(), floor, size, coldRent, warmRent,
|
[building, String(u.number).trim(), floor, size, coldRent, warmRent,
|
||||||
String(u.tenant || '').trim() || null, status, req.params.id],
|
String(u.tenant || '').trim() || null, status, heizung, req.params.id],
|
||||||
)
|
)
|
||||||
res.json({ unit: mapUnit(neu[0]) })
|
res.json({ unit: mapUnit(neu[0]) })
|
||||||
} catch (e) { next(e) }
|
} catch (e) { next(e) }
|
||||||
|
|||||||
@@ -157,15 +157,15 @@ export const anleitungAbschnitte: AnleitungAbschnitt[] = [
|
|||||||
punkte: [
|
punkte: [
|
||||||
{
|
{
|
||||||
titel: 'Haus anlegen',
|
titel: 'Haus anlegen',
|
||||||
text: 'Pflichtangaben: Straße, Hausnummer, PLZ, Ort, Baujahr, Anzahl Stockwerke, Etagenhöhe, Anzahl Wohneinheiten und Wohnfläche. Optional: Lage (z. B. Stadtteil) und Grundstücksfläche. Hausname und Adresszeile werden automatisch aus Straße und Hausnummer gebildet.',
|
text: 'Pflichtangaben: Straße, Hausnummer, PLZ, Ort, Baujahr, Anzahl Stockwerke, Etagenhöhe, Anzahl Wohneinheiten und Wohnfläche. Optional: Lage (z. B. Stadtteil), Grundstücksfläche und Heizungsart. Hausname und Adresszeile werden automatisch aus Straße und Hausnummer gebildet.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
titel: 'Häuser suchen & Listenansicht',
|
titel: 'Häuser suchen & Ansicht wechseln',
|
||||||
text: 'Über das Suchfeld lassen sich Häuser nach Adresse, Ort, PLZ oder Lage filtern. Der Umschalter Karten/Liste wählt die Ansicht; in der Liste öffnet ein Klick auf eine Zeile die Wohneinheiten des Hauses.',
|
text: 'Die Hausübersicht öffnet standardmäßig in der Listenansicht; der Umschalter Karten/Liste wählt die Ansicht. Über das Suchfeld lassen sich Häuser nach Adresse, Ort, PLZ oder Lage filtern; in der Liste öffnet ein Klick auf eine Zeile die Wohneinheiten des Hauses.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
titel: 'Wohnungen zuordnen',
|
titel: 'Wohnungen zuordnen',
|
||||||
text: 'Jede Wohnung gehört zu einem Haus: Etage, Wohnfläche, Kalt-/Warmmiete, Mieter und Status (vermietet/frei) je Einheit pflegen. Der Bestand erscheint anschließend im Dashboard.',
|
text: 'Jede Wohnung gehört zu einem Haus: Etage, Wohnfläche, Kalt-/Warmmiete, Mieter, Status (vermietet/frei) und optional Heizungsart je Einheit pflegen – bei Neuanlage wird die Heizungsart automatisch aus dem Haus übernommen. Der Bestand erscheint anschließend im Dashboard.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+59
-4
@@ -1,7 +1,7 @@
|
|||||||
import { Fragment, useMemo, useState } from 'react'
|
import { Fragment, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Building2, Home, MapPin, CalendarDays, ArrowUpDown, LayoutGrid, Ruler, Pencil,
|
Building2, Home, MapPin, CalendarDays, ArrowUpDown, LayoutGrid, Ruler, Pencil,
|
||||||
Plus, Users, DoorOpen, Search, List, ChevronDown, ChevronRight,
|
Plus, Users, DoorOpen, Search, List, ChevronDown, ChevronRight, Flame,
|
||||||
} from 'lucide-react'
|
} 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'
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { useStore, fmtEUR } from '../store/store'
|
import { useStore, fmtEUR } from '../store/store'
|
||||||
import type { Building, HausForm, Unit } from '../types'
|
import type { Building, HausForm, Unit } from '../types'
|
||||||
|
import { HEIZUNGSARTEN } from '../types'
|
||||||
|
|
||||||
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
|
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
|
||||||
const zahl = (s: string) => Number(s.replace(',', '.'))
|
const zahl = (s: string) => Number(s.replace(',', '.'))
|
||||||
@@ -71,6 +72,7 @@ function HausDialog({ haus }: { haus?: Building }) {
|
|||||||
const [grundstuecksflaeche, setGrundstuecksflaeche] = useState(
|
const [grundstuecksflaeche, setGrundstuecksflaeche] = useState(
|
||||||
haus?.grundstuecksflaeche != null ? String(haus.grundstuecksflaeche).replace('.', ',') : '',
|
haus?.grundstuecksflaeche != null ? String(haus.grundstuecksflaeche).replace('.', ',') : '',
|
||||||
)
|
)
|
||||||
|
const [heizungsart, setHeizungsart] = useState(haus?.heizungsart ?? '')
|
||||||
const [sende, setSende] = useState(false)
|
const [sende, setSende] = useState(false)
|
||||||
const [fehler, setFehler] = useState<string | null>(null)
|
const [fehler, setFehler] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ function HausDialog({ haus }: { haus?: Building }) {
|
|||||||
wohneinheiten: zahl(wohneinheiten),
|
wohneinheiten: zahl(wohneinheiten),
|
||||||
wohnflaeche: zahl(wohnflaeche),
|
wohnflaeche: zahl(wohnflaeche),
|
||||||
grundstuecksflaeche: grundstuecksflaeche.trim() ? zahl(grundstuecksflaeche) : null,
|
grundstuecksflaeche: grundstuecksflaeche.trim() ? zahl(grundstuecksflaeche) : null,
|
||||||
|
heizungsart,
|
||||||
}
|
}
|
||||||
const err = bearbeiten ? await updateHaus(haus!.name, payload) : await addHaus(payload)
|
const err = bearbeiten ? await updateHaus(haus!.name, payload) : await addHaus(payload)
|
||||||
setSende(false)
|
setSende(false)
|
||||||
@@ -136,6 +139,19 @@ function HausDialog({ haus }: { haus?: Building }) {
|
|||||||
<Feld id="h-stock" label="Anzahl Stockwerke" inputMode="numeric" placeholder="z. B. 3" value={stockwerke} onChange={(e) => setStockwerke(e.target.value)} />
|
<Feld id="h-stock" label="Anzahl Stockwerke" inputMode="numeric" placeholder="z. B. 3" value={stockwerke} onChange={(e) => setStockwerke(e.target.value)} />
|
||||||
<Feld id="h-hoehe" label="Etagenhöhe (m)" inputMode="decimal" placeholder="2,65" value={etagenhoehe} onChange={(e) => setEtagenhoehe(e.target.value)} />
|
<Feld id="h-hoehe" label="Etagenhöhe (m)" inputMode="decimal" placeholder="2,65" value={etagenhoehe} onChange={(e) => setEtagenhoehe(e.target.value)} />
|
||||||
<Feld id="h-whg" label="Anzahl Wohneinheiten" inputMode="numeric" placeholder="z. B. 12" value={wohneinheiten} onChange={(e) => setWohneinheiten(e.target.value)} />
|
<Feld id="h-whg" label="Anzahl Wohneinheiten" inputMode="numeric" placeholder="z. B. 12" value={wohneinheiten} onChange={(e) => setWohneinheiten(e.target.value)} />
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Heizungsart</Label>
|
||||||
|
<Select value={heizungsart || '__keine'} onValueChange={(v) => setHeizungsart(v === '__keine' ? '' : v)}>
|
||||||
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__keine">Nicht angegeben</SelectItem>
|
||||||
|
{HEIZUNGSARTEN.map((hz) => (
|
||||||
|
<SelectItem key={hz} value={hz}>{hz}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-slate-400 dark:text-slate-500">Optional</p>
|
||||||
|
</div>
|
||||||
</Abschnitt>
|
</Abschnitt>
|
||||||
<Abschnitt titel="Flächen">
|
<Abschnitt titel="Flächen">
|
||||||
<Feld id="h-wohnfl" label="Wohnfläche gesamt (m²)" inputMode="decimal" placeholder="z. B. 980" value={wohnflaeche} onChange={(e) => setWohnflaeche(e.target.value)} />
|
<Feld id="h-wohnfl" label="Wohnfläche gesamt (m²)" inputMode="decimal" placeholder="z. B. 980" value={wohnflaeche} onChange={(e) => setWohnflaeche(e.target.value)} />
|
||||||
@@ -159,7 +175,7 @@ function HausDialog({ haus }: { haus?: Building }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; gebaeude: string[]; festesHaus?: string }) {
|
function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; gebaeude: string[]; festesHaus?: string }) {
|
||||||
const { addWohnung, updateWohnung } = useStore()
|
const { addWohnung, updateWohnung, buildings } = 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 ?? festesHaus ?? gebaeude[0] ?? '')
|
const [building, setBuilding] = useState(wohnung?.building ?? festesHaus ?? gebaeude[0] ?? '')
|
||||||
@@ -170,9 +186,19 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
|
|||||||
const [warmRent, setWarmRent] = useState(wohnung ? String(wohnung.warmRent).replace('.', ',') : '')
|
const [warmRent, setWarmRent] = useState(wohnung ? String(wohnung.warmRent).replace('.', ',') : '')
|
||||||
const [tenant, setTenant] = useState(wohnung?.tenant ?? '')
|
const [tenant, setTenant] = useState(wohnung?.tenant ?? '')
|
||||||
const [status, setStatus] = useState<'vermietet' | 'frei'>(wohnung?.status ?? 'frei')
|
const [status, setStatus] = useState<'vermietet' | 'frei'>(wohnung?.status ?? 'frei')
|
||||||
|
const [heizungsart, setHeizungsart] = useState(wohnung?.heizungsart ?? '')
|
||||||
const [sende, setSende] = useState(false)
|
const [sende, setSende] = useState(false)
|
||||||
const [fehler, setFehler] = useState<string | null>(null)
|
const [fehler, setFehler] = useState<string | null>(null)
|
||||||
|
|
||||||
|
/** Bei Neuanlage: Heizungsart der Wohneinheit aus dem gewählten Haus vorbelegen. */
|
||||||
|
const waehleHaus = (b: string) => {
|
||||||
|
setBuilding(b)
|
||||||
|
if (!bearbeiten) {
|
||||||
|
const h = buildings.find((x) => x.name === b)
|
||||||
|
if (h?.heizungsart) setHeizungsart(h.heizungsart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (sende) return
|
if (sende) return
|
||||||
setSende(true)
|
setSende(true)
|
||||||
@@ -186,6 +212,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
|
|||||||
warmRent: zahl(warmRent),
|
warmRent: zahl(warmRent),
|
||||||
tenant: tenant.trim(),
|
tenant: tenant.trim(),
|
||||||
status,
|
status,
|
||||||
|
heizungsart,
|
||||||
}
|
}
|
||||||
const err = bearbeiten ? await updateWohnung(wohnung!.id, payload) : await addWohnung(payload)
|
const err = bearbeiten ? await updateWohnung(wohnung!.id, payload) : await addWohnung(payload)
|
||||||
setSende(false)
|
setSende(false)
|
||||||
@@ -220,7 +247,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
|
|||||||
<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} disabled={Boolean(festesHaus)}>
|
<Select value={building} onValueChange={waehleHaus} disabled={Boolean(festesHaus)}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{gebaeude.map((g) => (
|
{gebaeude.map((g) => (
|
||||||
@@ -271,6 +298,25 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Heizungsart</Label>
|
||||||
|
<Select value={heizungsart || '__keine'} onValueChange={(v) => setHeizungsart(v === '__keine' ? '' : v)}>
|
||||||
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__keine">Nicht angegeben</SelectItem>
|
||||||
|
{HEIZUNGSARTEN.map((hz) => (
|
||||||
|
<SelectItem key={hz} value={hz}>{hz}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{!bearbeiten && (
|
||||||
|
<p className="text-xs text-slate-400 dark:text-slate-500">
|
||||||
|
Wird bei Neuanlage automatisch aus dem Haus übernommen.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{fehler && (
|
{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">
|
<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}
|
{fehler}
|
||||||
@@ -356,7 +402,7 @@ function BelegungsFortschritt({ angelegt, soll }: { angelegt: number; soll: numb
|
|||||||
export default function Bestand() {
|
export default function Bestand() {
|
||||||
const { buildings, units } = useStore()
|
const { buildings, units } = useStore()
|
||||||
const [suche, setSuche] = useState('')
|
const [suche, setSuche] = useState('')
|
||||||
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('karten')
|
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('liste')
|
||||||
const [offen, setOffen] = useState<string | null>(null)
|
const [offen, setOffen] = useState<string | null>(null)
|
||||||
|
|
||||||
const gebaeudeNamen = useMemo(
|
const gebaeudeNamen = useMemo(
|
||||||
@@ -504,6 +550,14 @@ export default function Bestand() {
|
|||||||
<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 dark:text-slate-100">{h.wohnflaeche.toLocaleString('de-DE')} m²</dd>
|
<dd className="text-right font-medium text-slate-800 dark:text-slate-100">{h.wohnflaeche.toLocaleString('de-DE')} m²</dd>
|
||||||
|
{h.heizungsart && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-1.5 text-slate-500 dark:text-slate-400">
|
||||||
|
<Flame className="h-3.5 w-3.5 shrink-0" /> Heizung
|
||||||
|
</div>
|
||||||
|
<dd className="text-right font-medium text-slate-800 dark:text-slate-100">{h.heizungsart}</dd>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@@ -599,6 +653,7 @@ export default function Bestand() {
|
|||||||
{h.wohnflaeche.toLocaleString('de-DE')} m²
|
{h.wohnflaeche.toLocaleString('de-DE')} m²
|
||||||
{h.grundstuecksflaeche != null &&
|
{h.grundstuecksflaeche != null &&
|
||||||
` · Grundstück ${h.grundstuecksflaeche.toLocaleString('de-DE')} m²`}
|
` · Grundstück ${h.grundstuecksflaeche.toLocaleString('de-DE')} m²`}
|
||||||
|
{h.heizungsart && ` · Heizung: ${h.heizungsart}`}
|
||||||
</span>
|
</span>
|
||||||
<WohnungDialog gebaeude={gebaeudeNamen} festesHaus={h.name} />
|
<WohnungDialog gebaeude={gebaeudeNamen} festesHaus={h.name} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ export default function Zaehler() {
|
|||||||
const [filterHaus, setFilterHaus] = useState('alle')
|
const [filterHaus, setFilterHaus] = useState('alle')
|
||||||
const [filterArt, setFilterArt] = useState<'alle' | ZaehlerArt>('alle')
|
const [filterArt, setFilterArt] = useState<'alle' | ZaehlerArt>('alle')
|
||||||
const [suche, setSuche] = useState('')
|
const [suche, setSuche] = useState('')
|
||||||
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('karten')
|
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('liste')
|
||||||
|
|
||||||
const gebaeude = useMemo(
|
const gebaeude = useMemo(
|
||||||
() => Array.from(new Set(meters.map((m) => m.building))).sort(),
|
() => Array.from(new Set(meters.map((m) => m.building))).sort(),
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ interface Store extends Bootstrap {
|
|||||||
warmRent: number
|
warmRent: number
|
||||||
tenant: string
|
tenant: string
|
||||||
status: 'vermietet' | 'frei'
|
status: 'vermietet' | 'frei'
|
||||||
|
heizungsart: string
|
||||||
}) => Promise<string | null>
|
}) => Promise<string | null>
|
||||||
updateWohnung: (id: string, u: Partial<Unit>) => Promise<string | null>
|
updateWohnung: (id: string, u: Partial<Unit>) => Promise<string | null>
|
||||||
// ── Dokumente (Verwaltung pflegt, Mieter lesen) ─────────
|
// ── Dokumente (Verwaltung pflegt, Mieter lesen) ─────────
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export interface Unit {
|
|||||||
warmRent: number // €
|
warmRent: number // €
|
||||||
tenant: string | null
|
tenant: string | null
|
||||||
status: 'vermietet' | 'frei'
|
status: 'vermietet' | 'frei'
|
||||||
|
/** Heizungsart der Wohnung (Dropdown, '' = nicht angegeben) */
|
||||||
|
heizungsart: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
@@ -107,6 +109,20 @@ export interface Announcement {
|
|||||||
|
|
||||||
export type ZaehlerArt = 'Strom' | 'Wasser' | 'Gas' | 'Wärme' | 'Sonstiges'
|
export type ZaehlerArt = 'Strom' | 'Wasser' | 'Gas' | 'Wärme' | 'Sonstiges'
|
||||||
|
|
||||||
|
/** Gängige Heizungsformen (Dropdown in Haus- und Wohnungsformular, v1.29). */
|
||||||
|
export const HEIZUNGSARTEN = [
|
||||||
|
'Gas-Zentralheizung',
|
||||||
|
'Öl-Zentralheizung',
|
||||||
|
'Fernwärme',
|
||||||
|
'Wärmepumpe',
|
||||||
|
'Pellets-/Holzheizung',
|
||||||
|
'Nachtspeicherheizung',
|
||||||
|
'Elektro-Direktheizung',
|
||||||
|
'Einzelraumheizung (Ofen/Kamin)',
|
||||||
|
'Sonstiges',
|
||||||
|
] as const
|
||||||
|
export type Heizungsart = (typeof HEIZUNGSARTEN)[number]
|
||||||
|
|
||||||
export interface Building {
|
export interface Building {
|
||||||
/** Interner Schlüssel – wird serverseitig aus Straße + Hausnummer abgeleitet. */
|
/** Interner Schlüssel – wird serverseitig aus Straße + Hausnummer abgeleitet. */
|
||||||
name: string
|
name: string
|
||||||
@@ -124,6 +140,8 @@ export interface Building {
|
|||||||
wohneinheiten: number // Anzahl Wohneinheiten (Soll)
|
wohneinheiten: number // Anzahl Wohneinheiten (Soll)
|
||||||
wohnflaeche: number // Gesamtwohnfläche in m²
|
wohnflaeche: number // Gesamtwohnfläche in m²
|
||||||
grundstuecksflaeche: number | null // in m², optional
|
grundstuecksflaeche: number | null // in m², optional
|
||||||
|
/** Heizungsart des Hauses (Dropdown, '' = nicht angegeben) */
|
||||||
|
heizungsart: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Formular-Daten für Neuanlage/Bearbeitung (Name/Adresse entfallen). */
|
/** Formular-Daten für Neuanlage/Bearbeitung (Name/Adresse entfallen). */
|
||||||
|
|||||||
Reference in New Issue
Block a user