v1.36: Mieterverwaltung (Mieter 1:1 pro Wohnung, Kaution/Vertrag/Dokumente) + v1.37: KI-Assistent (N8N + Kimi, 6 Portal-Tools)

This commit is contained in:
2026-10-04 20:51:00 +02:00
parent 9e74813e22
commit 9d14d367ee
22 changed files with 1840 additions and 107 deletions
+35 -27
View File
@@ -22,7 +22,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useStore, fmtEUR } from '../store/store'
import { useStore } from '../store/store'
import type { Building, HausForm, Unit } from '../types'
import { HEIZUNGSARTEN } from '../types'
@@ -203,10 +203,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
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 [status, setStatus] = useState<Unit['status']>(wohnung?.status ?? 'leerstand')
const [heizungsart, setHeizungsart] = useState(wohnung?.heizungsart ?? '')
const [sende, setSende] = useState(false)
const [fehler, setFehler] = useState<string | null>(null)
@@ -229,9 +226,6 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
number: number.trim(),
floor: zahl(floor),
size: zahl(size),
coldRent: zahl(coldRent),
warmRent: zahl(warmRent),
tenant: tenant.trim(),
status,
heizungsart,
}
@@ -299,21 +293,20 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
</p>
</div>
</Abschnitt>
<Abschnitt titel="Miete">
<Feld id="w-kalt" label="Kaltmiete (€)" inputMode="decimal" placeholder="z. B. 520" value={coldRent} onChange={(e) => setColdRent(e.target.value)} />
<Feld id="w-warm" label="Warmmiete (€)" inputMode="decimal" placeholder="z. B. 690" value={warmRent} onChange={(e) => setWarmRent(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Mieter">
<Feld id="w-mieter" label="Mieter (Name)" hint="Leer lassen, solange die Wohnung frei ist" placeholder="z. B. Anna Berger" value={tenant} onChange={(e) => setTenant(e.target.value)} />
<Abschnitt titel="Status">
<div className="space-y-1.5">
<Label>Status</Label>
<Select value={status} onValueChange={(v) => setStatus(v as 'vermietet' | 'frei')}>
<Select value={status} onValueChange={(v) => setStatus(v as Unit['status'])}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="vermietet">vermietet</SelectItem>
<SelectItem value="frei">frei</SelectItem>
<SelectItem value="vermietet">Vermietet</SelectItem>
<SelectItem value="leerstand">Leerstand</SelectItem>
<SelectItem value="unklar">Unklar</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-slate-400 dark:text-slate-500">
Vermietet/Leerstand wird bei Anlage oder Löschung eines Mieters in der Mieterverwaltung automatisch gesetzt.
</p>
</div>
</Abschnitt>
{fehler && (
@@ -324,7 +317,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
<Button onClick={submit} disabled={sende || !building || !number.trim() || !size || !coldRent || !warmRent}>
<Button onClick={submit} disabled={sende || !building || !number.trim() || !size}>
{sende ? 'Wird gespeichert …' : bearbeiten ? 'Speichern' : 'Wohnung anlegen'}
</Button>
</DialogFooter>
@@ -340,6 +333,21 @@ function sortiereWohnungen(liste: Unit[]): Unit[] {
)
}
/** Farben des Wohnungsstatus-Badges. */
function statusBadgeKlasse(status: Unit['status']) {
if (status === 'vermietet')
return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300'
if (status === 'unklar')
return 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300'
return 'bg-slate-200 text-slate-600 dark:bg-slate-800 dark:text-slate-300'
}
const STATUS_LABEL: Record<Unit['status'], string> = {
vermietet: 'Vermietet',
leerstand: 'Leerstand',
unklar: 'Unklar',
}
/** Wohnungszeile, in Karten- und Listenansicht gleich. */
function WohnungsZeile({ u, gebaeude, festesHaus }: { u: Unit; gebaeude: string[]; festesHaus: string }) {
const { deleteWohnung, role } = useStore()
@@ -357,19 +365,19 @@ function WohnungsZeile({ u, gebaeude, festesHaus }: { u: Unit; gebaeude: string[
Etage {u.floor} · {u.size.toLocaleString('de-DE')} m²
</span>
</div>
<Badge
className={u.status === 'vermietet' ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300' : 'bg-slate-200 text-slate-600 dark:bg-slate-800 dark:text-slate-300'}
variant="secondary"
>
{u.status}
<Badge className={statusBadgeKlasse(u.status)} variant="secondary">
{STATUS_LABEL[u.status]}
</Badge>
</div>
<div className="mt-1 flex items-center justify-between gap-2 text-xs text-slate-500 dark:text-slate-400">
<span className="flex items-center gap-1 truncate">
<Users className="h-3.5 w-3.5 shrink-0" /> {u.tenant ?? '–'}
</span>
<a
href="/mieter"
className="flex items-center gap-1 truncate text-teal-700 hover:underline dark:text-teal-400"
title="Mieterverwaltung öffnen"
>
<Users className="h-3.5 w-3.5 shrink-0" /> Mieter verwalten
</a>
<span className="flex shrink-0 items-center gap-2">
<span className="font-medium text-slate-700 dark:text-slate-200">{fmtEUR.format(u.warmRent)}</span>
<WohnungDialog wohnung={u} gebaeude={gebaeude} festesHaus={festesHaus} />
{role === 'admin' && (
<button