v1.29: Heizungsart-Dropdown (Haus + Wohnung), Listenansicht als Standard

This commit is contained in:
2026-10-04 18:04:02 +02:00
parent e115908cd9
commit 662676a992
8 changed files with 127 additions and 21 deletions
+4 -4
View File
@@ -157,15 +157,15 @@ export const anleitungAbschnitte: AnleitungAbschnitt[] = [
punkte: [
{
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',
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.',
titel: 'Häuser suchen & Ansicht wechseln',
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',
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
View File
@@ -1,7 +1,7 @@
import { Fragment, useMemo, useState } from 'react'
import {
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'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -24,6 +24,7 @@ import {
} from '@/components/ui/select'
import { useStore, fmtEUR } from '../store/store'
import type { Building, HausForm, Unit } from '../types'
import { HEIZUNGSARTEN } from '../types'
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
const zahl = (s: string) => Number(s.replace(',', '.'))
@@ -71,6 +72,7 @@ function HausDialog({ haus }: { haus?: Building }) {
const [grundstuecksflaeche, setGrundstuecksflaeche] = useState(
haus?.grundstuecksflaeche != null ? String(haus.grundstuecksflaeche).replace('.', ',') : '',
)
const [heizungsart, setHeizungsart] = useState(haus?.heizungsart ?? '')
const [sende, setSende] = useState(false)
const [fehler, setFehler] = useState<string | null>(null)
@@ -94,6 +96,7 @@ function HausDialog({ haus }: { haus?: Building }) {
wohneinheiten: zahl(wohneinheiten),
wohnflaeche: zahl(wohnflaeche),
grundstuecksflaeche: grundstuecksflaeche.trim() ? zahl(grundstuecksflaeche) : null,
heizungsart,
}
const err = bearbeiten ? await updateHaus(haus!.name, payload) : await addHaus(payload)
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-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)} />
<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 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)} />
@@ -159,7 +175,7 @@ function HausDialog({ haus }: { haus?: Building }) {
}
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 [open, setOpen] = useState(false)
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 [tenant, setTenant] = useState(wohnung?.tenant ?? '')
const [status, setStatus] = useState<'vermietet' | 'frei'>(wohnung?.status ?? 'frei')
const [heizungsart, setHeizungsart] = useState(wohnung?.heizungsart ?? '')
const [sende, setSende] = useState(false)
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 () => {
if (sende) return
setSende(true)
@@ -186,6 +212,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
warmRent: zahl(warmRent),
tenant: tenant.trim(),
status,
heizungsart,
}
const err = bearbeiten ? await updateWohnung(wohnung!.id, payload) : await addWohnung(payload)
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="space-y-1.5">
<Label>Haus</Label>
<Select value={building} onValueChange={setBuilding} disabled={Boolean(festesHaus)}>
<Select value={building} onValueChange={waehleHaus} disabled={Boolean(festesHaus)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{gebaeude.map((g) => (
@@ -271,6 +298,25 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
</Select>
</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 && (
<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}
@@ -356,7 +402,7 @@ function BelegungsFortschritt({ angelegt, soll }: { angelegt: number; soll: numb
export default function Bestand() {
const { buildings, units } = useStore()
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 gebaeudeNamen = useMemo(
@@ -504,6 +550,14 @@ export default function Bestand() {
<Home className="h-3.5 w-3.5 shrink-0" /> Wohnfläche
</div>
<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>
<div className="mt-4">
@@ -599,6 +653,7 @@ export default function Bestand() {
{h.wohnflaeche.toLocaleString('de-DE')} m²
{h.grundstuecksflaeche != null &&
` · Grundstück ${h.grundstuecksflaeche.toLocaleString('de-DE')} m²`}
{h.heizungsart && ` · Heizung: ${h.heizungsart}`}
</span>
<WohnungDialog gebaeude={gebaeudeNamen} festesHaus={h.name} />
</div>
+1 -1
View File
@@ -391,7 +391,7 @@ export default function Zaehler() {
const [filterHaus, setFilterHaus] = useState('alle')
const [filterArt, setFilterArt] = useState<'alle' | ZaehlerArt>('alle')
const [suche, setSuche] = useState('')
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('karten')
const [ansicht, setAnsicht] = useState<'karten' | 'liste'>('liste')
const gebaeude = useMemo(
() => Array.from(new Set(meters.map((m) => m.building))).sort(),
+1
View File
@@ -150,6 +150,7 @@ interface Store extends Bootstrap {
warmRent: number
tenant: string
status: 'vermietet' | 'frei'
heizungsart: string
}) => Promise<string | null>
updateWohnung: (id: string, u: Partial<Unit>) => Promise<string | null>
// ── Dokumente (Verwaltung pflegt, Mieter lesen) ─────────
+18
View File
@@ -37,6 +37,8 @@ export interface Unit {
warmRent: number // €
tenant: string | null
status: 'vermietet' | 'frei'
/** Heizungsart der Wohnung (Dropdown, '' = nicht angegeben) */
heizungsart: string
}
export interface Transaction {
@@ -107,6 +109,20 @@ export interface Announcement {
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 {
/** Interner Schlüssel – wird serverseitig aus Straße + Hausnummer abgeleitet. */
name: string
@@ -124,6 +140,8 @@ export interface Building {
wohneinheiten: number // Anzahl Wohneinheiten (Soll)
wohnflaeche: number // Gesamtwohnfläche in m²
grundstuecksflaeche: number | null // in m², optional
/** Heizungsart des Hauses (Dropdown, '' = nicht angegeben) */
heizungsart: string
}
/** Formular-Daten für Neuanlage/Bearbeitung (Name/Adresse entfallen). */