v1.37: KI-Assistent N8N-Workflow fertig eingerichtet; alle Formulare 2-spaltig mit neuem Design

- N8N: Workflow serverseitig in DB eingespielt (Webhook mp-assist, Code-Node mit
  Kimi-Tool-Calling, keine Credentials), E2E live verifiziert
- Formulare: shared FormSection-Komponente (2 gleich breite Spalten, Wide fuer
  Volllbreiten-Felder), 10 Formulare umgebaut, Dialoge verbreitert
- Design: Inter + Sora Fonts, Teal-Akzente, Icons in Sektionskoepfen
This commit is contained in:
2026-10-04 21:30:34 +02:00
parent 9d14d367ee
commit 6eec3ac437
16 changed files with 540 additions and 645 deletions
+62
View File
@@ -0,0 +1,62 @@
import type { ReactNode } from 'react'
import type { LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
/**
* Shared Form-Layout für das Mieterportal.
* Jede FormSection rendert ihre Felder in zwei gleich breiten Spalten
* (mobil: eine Spalte). Elemente, die über die volle Breite laufen sollen
* (Checkbox-Blöcke, Textareas, Uploads, Hinweistexte), in <Wide> packen.
*/
export function FormSection({
titel,
icon: Icon,
children,
className,
}: {
titel: string
icon?: LucideIcon
children: ReactNode
className?: string
}) {
return (
<section
className={cn(
'rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:p-5',
'dark:border-slate-700/60 dark:bg-slate-900/60',
className,
)}
>
<header className="mb-4 flex items-center gap-2.5 border-b border-slate-100 pb-3 dark:border-slate-800">
{Icon && (
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-teal-600/10 text-teal-700 dark:bg-teal-400/10 dark:text-teal-300">
<Icon className="h-4 w-4" />
</span>
)}
<h3 className="font-display text-[13px] font-semibold uppercase tracking-wider text-teal-800 dark:text-teal-300">
{titel}
</h3>
</header>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">{children}</div>
</section>
)
}
/** Volle Breite innerhalb einer 2-spaltigen FormSection. */
export function Wide({ children, className }: { children: ReactNode; className?: string }) {
return <div className={cn('md:col-span-2', className)}>{children}</div>
}
/** Fehlerzeile am Ende eines Formulars. */
export function FormFehler({ fehler }: { fehler: string | null }) {
if (!fehler) return null
return (
<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}
</div>
)
}
+5
View File
@@ -71,6 +71,11 @@
}
body {
@apply bg-background text-foreground;
font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 {
font-family: 'Sora', 'Inter', ui-sans-serif, system-ui, sans-serif;
}
}
+64 -63
View File
@@ -15,6 +15,7 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { useStore } from '../store/store'
import { fmtDatumZeit } from '../lib/format'
import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection'
import type { Benutzer, BenutzerRolle } from '../types'
function Chip({ label, value, tone = 'slate' }: { label: string; value: number | string; tone?: 'slate' | 'amber' | 'teal' | 'blue' }) {
@@ -57,12 +58,12 @@ function NeuerBenutzerDialog() {
<Plus className="h-4 w-4" /> Benutzer anlegen
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Neuen Benutzer anlegen</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormSection titel="Benutzerdaten" icon={UserCheck}>
<div className="space-y-1.5">
<Label htmlFor="nb-name">Name</Label>
<Input id="nb-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Vor- und Nachname" />
@@ -71,8 +72,6 @@ function NeuerBenutzerDialog() {
<Label htmlFor="nb-bn">Benutzername</Label>
<Input id="nb-bn" value={bn} onChange={(e) => setBn(e.target.value)} placeholder="z. B. m.mueller" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="nb-email">E-Mail</Label>
<Input id="nb-email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
@@ -94,32 +93,30 @@ function NeuerBenutzerDialog() {
))}
</div>
</div>
</div>
{rolle === 'mieter' && (
{rolle === 'mieter' && (
<div className="space-y-1.5">
<Label htmlFor="nb-einheit">Wohnung zuweisen</Label>
<select
id="nb-einheit"
value={einheitId}
onChange={(e) => setEinheitId(e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
{units.map((u) => (
<option key={u.id} value={u.id}>
{u.building}, Whg. {u.number} ({u.size} m²)
</option>
))}
</select>
</div>
)}
<div className="space-y-1.5">
<Label htmlFor="nb-einheit">Wohnung zuweisen</Label>
<select
id="nb-einheit"
value={einheitId}
onChange={(e) => setEinheitId(e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
>
{units.map((u) => (
<option key={u.id} value={u.id}>
{u.building}, Whg. {u.number} ({u.size} m²)
</option>
))}
</select>
<Label htmlFor="nb-pw">Passwort (Initial)</Label>
<Input id="nb-pw" value={pw} onChange={(e) => setPw(e.target.value)} />
<p className="text-xs text-slate-500">Der Benutzer sollte das Passwort nach dem ersten Login ändern.</p>
</div>
)}
<div className="space-y-1.5">
<Label htmlFor="nb-pw">Passwort (Initial)</Label>
<Input id="nb-pw" value={pw} onChange={(e) => setPw(e.target.value)} />
<p className="text-xs text-slate-500">Der Benutzer sollte das Passwort nach dem ersten Login ändern.</p>
</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">{fehler}</div>
)}
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
@@ -172,45 +169,49 @@ function EmailDialog({ benutzer: b, einheitLabel }: { benutzer: Benutzer; einhei
<Mail className="h-3.5 w-3.5" /> E-Mail
</button>
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>E-Mail an {b.name}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label>Empfänger</Label>
<div className="rounded-lg border bg-slate-50 px-3 py-2 text-sm text-slate-600 dark:bg-slate-900 dark:text-slate-300">
{b.name} · {hatEmail ? b.email : 'keine E-Mail hinterlegt'}
{einheitLabel && <span className="block text-xs text-slate-400">{einheitLabel}</span>}
</div>
<p className="text-xs text-slate-400">
Die Verwaltung erhält automatisch eine Kopie (CC).
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="m-betreff">Betreff</Label>
<Input
id="m-betreff"
placeholder="z. B. Terminabsprache Hausmeisterservice"
value={betreff}
onChange={(e) => setBetreff(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="m-text">Nachricht</Label>
<Textarea
id="m-text"
rows={6}
placeholder="Ihre Nachricht an den Mieter …"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</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}
</div>
)}
<FormSection titel="Nachricht" icon={Mail}>
<Wide>
<div className="space-y-1.5">
<Label>Empfänger</Label>
<div className="rounded-lg border bg-slate-50 px-3 py-2 text-sm text-slate-600 dark:bg-slate-900 dark:text-slate-300">
{b.name} · {hatEmail ? b.email : 'keine E-Mail hinterlegt'}
{einheitLabel && <span className="block text-xs text-slate-400">{einheitLabel}</span>}
</div>
<p className="text-xs text-slate-400">
Die Verwaltung erhält automatisch eine Kopie (CC).
</p>
</div>
</Wide>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="m-betreff">Betreff</Label>
<Input
id="m-betreff"
placeholder="z. B. Terminabsprache Hausmeisterservice"
value={betreff}
onChange={(e) => setBetreff(e.target.value)}
/>
</div>
</Wide>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="m-text">Nachricht</Label>
<Textarea
id="m-text"
rows={6}
placeholder="Ihre Nachricht an den Mieter …"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
</Wide>
</FormSection>
<FormFehler fehler={fehler} />
{gesendet && (
<div className="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300">
E-Mail wurde versendet (mit CC an die Verwaltung).
+54 -69
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, Flame, Trash2,
Plus, Users, DoorOpen, Search, List, ChevronDown, ChevronRight, Flame, Trash2, FileText,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -25,22 +25,11 @@ import {
import { useStore } from '../store/store'
import type { Building, HausForm, Unit } from '../types'
import { HEIZUNGSARTEN } from '../types'
import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection'
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
const zahl = (s: string) => Number(s.replace(',', '.'))
/** Optischer Abschnitt im einspaltigen Haus-Formular. */
function Abschnitt({ titel, children }: { titel: string; children: React.ReactNode }) {
return (
<div className="space-y-3 rounded-xl border bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/50">
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
{titel}
</h4>
{children}
</div>
)
}
function Feld({ id, label, hint, ...input }: {
id: string
label: string
@@ -119,7 +108,7 @@ function HausDialog({ haus }: { haus?: Building }) {
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{bearbeiten ? `Haus „${haus!.adresse}" bearbeiten` : 'Neues Haus anlegen'}
@@ -129,14 +118,14 @@ function HausDialog({ haus }: { haus?: Building }) {
<p className="text-xs text-slate-500 dark:text-slate-400">
Hausname und Adresszeile werden automatisch aus Straße und Hausnummer gebildet.
</p>
<Abschnitt titel="Adresse">
<FormSection titel="Adresse" icon={MapPin}>
<Feld id="h-strasse" label="Straße" placeholder="z. B. Lindenallee" value={strasse} onChange={(e) => setStrasse(e.target.value)} />
<Feld id="h-nr" label="Hausnummer" placeholder="z. B. 8" value={hausnummer} onChange={(e) => setHausnummer(e.target.value)} />
<Feld id="h-plz" label="PLZ" inputMode="numeric" placeholder="z. B. 47839" value={plz} onChange={(e) => setPlz(e.target.value)} />
<Feld id="h-ort" label="Ort" placeholder="z. B. Krefeld" value={ort} onChange={(e) => setOrt(e.target.value)} />
<Feld id="h-lage" label="Lage" hint={'Optional, z. B. Stadtteil („Krefeld-Hüls")'} placeholder="z. B. Stadtteil Hüls" value={lage} onChange={(e) => setLage(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Gebäude">
</FormSection>
<FormSection titel="Gebäude" icon={Building2}>
<Feld id="h-bau" label="Baujahr" inputMode="numeric" placeholder="z. B. 1998" value={baujahr} onChange={(e) => setBaujahr(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)} />
@@ -154,35 +143,33 @@ function HausDialog({ haus }: { haus?: Building }) {
</Select>
<p className="text-xs text-slate-400 dark:text-slate-500">Optional</p>
</div>
</Abschnitt>
<Abschnitt titel="Flächen">
</FormSection>
<FormSection titel="Flächen" icon={Ruler}>
<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-grundfl" label="Grundstücksfläche (m²)" hint="Optional" inputMode="decimal" placeholder="z. B. 1.250" value={grundstuecksflaeche} onChange={(e) => setGrundstuecksflaeche(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Zuordnung Belege">
<label className="flex cursor-pointer items-start gap-2.5 text-sm">
<input
id="h-eigenobjekt"
type="checkbox"
checked={eigenobjekt}
onChange={(e) => setEigenobjekt(e.target.checked)}
className="mt-0.5 h-4 w-4 rounded border-slate-300 accent-teal-600"
/>
<span>
<span className="font-medium text-slate-800 dark:text-slate-100">Eigenobjekt / Rechnungsempfänger</span>
<span className="block text-xs text-slate-500 dark:text-slate-400">
Diese Adresse steht auf eingehenden Rechnungen als Empfänger (z. B. die eigene Verwaltungsadresse).
Eigenobjekte werden bei der automatischen Beleg-Zuordnung übersprungen – die Zuordnung erfolgt dann
über die Adresse bzw. Stromzählernummer des Mietobjekts.
</FormSection>
<FormSection titel="Zuordnung Belege" icon={FileText}>
<Wide>
<label className="flex cursor-pointer items-start gap-2.5 text-sm">
<input
id="h-eigenobjekt"
type="checkbox"
checked={eigenobjekt}
onChange={(e) => setEigenobjekt(e.target.checked)}
className="mt-0.5 h-4 w-4 rounded border-slate-300 accent-teal-600"
/>
<span>
<span className="font-medium text-slate-800 dark:text-slate-100">Eigenobjekt / Rechnungsempfänger</span>
<span className="block text-xs text-slate-500 dark:text-slate-400">
Diese Adresse steht auf eingehenden Rechnungen als Empfänger (z. B. die eigene Verwaltungsadresse).
Eigenobjekte werden bei der automatischen Beleg-Zuordnung übersprungen – die Zuordnung erfolgt dann
über die Adresse bzw. Stromzählernummer des Mietobjekts.
</span>
</span>
</span>
</label>
</Abschnitt>
{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}
</div>
)}
</label>
</Wide>
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
@@ -248,7 +235,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{bearbeiten
@@ -259,7 +246,7 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Abschnitt titel="Zuordnung">
<FormSection titel="Zuordnung" icon={Home}>
<div className="space-y-1.5">
<Label>Haus</Label>
<Select value={building} onValueChange={waehleHaus} disabled={Boolean(festesHaus)}>
@@ -273,8 +260,8 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
{festesHaus && <p className="text-xs text-slate-400 dark:text-slate-500">Die Wohnung gehört fest zu diesem Haus.</p>}
</div>
<Feld id="w-nr" label="Wohnungsnummer" placeholder="z. B. 12" value={number} onChange={(e) => setNumber(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Ausstattung">
</FormSection>
<FormSection titel="Ausstattung" icon={LayoutGrid}>
<Feld id="w-etage" label="Etage" hint="0 = Erdgeschoss, negative Werte = Keller" inputMode="numeric" placeholder="z. B. 1" value={floor} onChange={(e) => setFloor(e.target.value)} />
<Feld id="w-qm" label="Wohnfläche (m²)" inputMode="decimal" placeholder="z. B. 68,5" value={size} onChange={(e) => setSize(e.target.value)} />
<div className="space-y-1.5">
@@ -292,28 +279,26 @@ function WohnungDialog({ wohnung, gebaeude, festesHaus }: { wohnung?: Unit; geba
Optional{!bearbeiten && ' – wird bei Neuanlage automatisch aus dem Haus übernommen'}.
</p>
</div>
</Abschnitt>
<Abschnitt titel="Status">
<div className="space-y-1.5">
<Label>Status</Label>
<Select value={status} onValueChange={(v) => setStatus(v as Unit['status'])}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<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 && (
<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}
</div>
)}
</FormSection>
<FormSection titel="Status" icon={DoorOpen}>
<Wide>
<div className="space-y-1.5">
<Label>Status</Label>
<Select value={status} onValueChange={(v) => setStatus(v as Unit['status'])}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<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>
</Wide>
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
+50 -47
View File
@@ -31,6 +31,7 @@ import { useStore } from '../store/store'
import { fmtDatum } from '../lib/format'
import PdfLightbox from '../components/PdfLightbox'
import { apiUpload, ApiFehler } from '../lib/api'
import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection'
import type { DocumentItem, Unit } from '../types'
const KATEGORIEN: DocumentItem['category'][] = ['Mietvertrag', 'Nebenkostenabrechnung', 'Hausordnung', 'Protokoll', 'Sonstiges']
@@ -130,21 +131,23 @@ function DokumentDialog({ dokument, einheiten }: { dokument?: DocumentItem; einh
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{bearbeiten ? 'Dokument bearbeiten' : 'Neues Dokument anlegen'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="d-titel">Titel</Label>
<Input
id="d-titel"
placeholder="z. B. Mietvertrag – Blücherstr. 59, Whg. 1"
value={titel}
onChange={(e) => setTitel(e.target.value)}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<FormSection titel="Dokument" icon={FileText}>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="d-titel">Titel</Label>
<Input
id="d-titel"
placeholder="z. B. Mietvertrag – Blücherstr. 59, Whg. 1"
value={titel}
onChange={(e) => setTitel(e.target.value)}
/>
</div>
</Wide>
<div className="space-y-1.5">
<Label>Kategorie</Label>
<Select value={kategorie} onValueChange={(v) => setKategorie(v as DocumentItem['category'])}>
@@ -176,8 +179,6 @@ function DokumentDialog({ dokument, einheiten }: { dokument?: DocumentItem; einh
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="d-datum">Datum</Label>
<Input id="d-datum" placeholder="TT.MM.JJJJ" value={datum} onChange={(e) => setDatum(e.target.value)} />
@@ -186,40 +187,42 @@ function DokumentDialog({ dokument, einheiten }: { dokument?: DocumentItem; einh
<Label htmlFor="d-groesse">Dateigröße (optional)</Label>
<Input id="d-groesse" placeholder="wird beim Upload gesetzt" value={groesse} onChange={(e) => setGroesse(e.target.value)} />
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="d-datei">PDF-Datei (optional, max. 25 MB)</Label>
<div className="flex items-center gap-2">
<Input
id="d-datei"
ref={dateiInput}
type="file"
accept="application/pdf,.pdf"
onChange={(e) => dateiWaehlen(e.target.files?.[0] ?? null)}
/>
{datei && (
<button
type="button"
title="Datei entfernen"
onClick={() => { setDatei(null); if (dateiInput.current) dateiInput.current.value = '' }}
className="shrink-0 rounded-lg bg-slate-100 px-2.5 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-200"
>
Entfernen
</button>
)}
</div>
{bearbeiten && dokument?.hasFile && !datei && (
<p className="text-xs text-slate-400">
Bereits hinterlegt: {dokument.fileName ?? 'PDF'} – ein neuer Upload ersetzt die Datei.
</p>
)}
{datei && (
<p className="flex items-center gap-1 text-xs text-teal-700">
<Upload className="h-3.5 w-3.5" /> {datei.name} wird beim Speichern hochgeladen.
</p>
)}
</div>
{fehler && <p className="text-sm font-medium text-red-600">{fehler}</p>}
<Wide>
<div className="space-y-1.5">
<Label htmlFor="d-datei">PDF-Datei (optional, max. 25 MB)</Label>
<div className="flex items-center gap-2">
<Input
id="d-datei"
ref={dateiInput}
type="file"
accept="application/pdf,.pdf"
onChange={(e) => dateiWaehlen(e.target.files?.[0] ?? null)}
/>
{datei && (
<button
type="button"
title="Datei entfernen"
onClick={() => { setDatei(null); if (dateiInput.current) dateiInput.current.value = '' }}
className="shrink-0 rounded-lg bg-slate-100 px-2.5 py-2 text-xs font-semibold text-slate-600 hover:bg-slate-200"
>
Entfernen
</button>
)}
</div>
{bearbeiten && dokument?.hasFile && !datei && (
<p className="text-xs text-slate-400">
Bereits hinterlegt: {dokument.fileName ?? 'PDF'} – ein neuer Upload ersetzt die Datei.
</p>
)}
{datei && (
<p className="flex items-center gap-1 text-xs text-teal-700">
<Upload className="h-3.5 w-3.5" /> {datei.name} wird beim Speichern hochgeladen.
</p>
)}
</div>
</Wide>
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
+13 -12
View File
@@ -31,6 +31,7 @@ import { useStore, fmtEUR } from '../store/store'
import { fmtDatum } from '../lib/format'
import type { Beleg, DocumentItem, KostenItem } from '../types'
import PdfLightbox from '../components/PdfLightbox'
import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection'
const KOSTEN_ARTEN = [
'Wasser/Abwasser', 'Müllabfuhr', 'Straßenreinigung/Winterdienst', 'Gartenpflege',
@@ -96,12 +97,12 @@ function KostenDialog({ position, vorbefuellung, onGespeichert, triggerLabel }:
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{bearbeiten ? 'Kostenposition bearbeiten' : 'Neue Kostenposition erfassen'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<FormSection titel="Position" icon={Receipt}>
<div className="space-y-1.5">
<Label>Kostenart</Label>
<Select value={kostenart} onValueChange={setKostenart}>
@@ -129,8 +130,6 @@ function KostenDialog({ position, vorbefuellung, onGespeichert, triggerLabel }:
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="k-jahr">Jahr</Label>
<Input id="k-jahr" inputMode="numeric" placeholder="z. B. 2026" value={jahr} onChange={(e) => setJahr(e.target.value)} />
@@ -139,8 +138,8 @@ function KostenDialog({ position, vorbefuellung, onGespeichert, triggerLabel }:
<Label htmlFor="k-betrag">Betrag (€)</Label>
<Input id="k-betrag" inputMode="decimal" placeholder="z. B. 1.250,00" value={betrag} onChange={(e) => setBetrag(e.target.value)} />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
</FormSection>
<FormSection titel="Zuordnung" icon={Landmark}>
<div className="space-y-1.5">
<Label>Haus</Label>
<Select value={haus} onValueChange={setHaus} disabled={einheit !== '__haus'}>
@@ -175,12 +174,14 @@ function KostenDialog({ position, vorbefuellung, onGespeichert, triggerLabel }:
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="k-notiz">Notiz (optional)</Label>
<Input id="k-notiz" placeholder="z. B. Rechnungs-Nr., Abrechnungszeitraum …" value={notiz} onChange={(e) => setNotiz(e.target.value)} />
</div>
{fehler && <p className="text-sm font-medium text-red-600">{fehler}</p>}
<Wide>
<div className="space-y-1.5">
<Label htmlFor="k-notiz">Notiz (optional)</Label>
<Input id="k-notiz" placeholder="z. B. Rechnungs-Nr., Abrechnungszeitraum …" value={notiz} onChange={(e) => setNotiz(e.target.value)} />
</div>
</Wide>
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
+43 -54
View File
@@ -1,6 +1,6 @@
import { useMemo, useRef, useState } from 'react'
import {
BadgeEuro, CalendarRange, FileText, Pencil, Phone, Plus, Search, Trash2, Upload, Users,
BadgeEuro, CalendarRange, FileText, Home, Pencil, Phone, Plus, Search, ShieldCheck, Trash2, Upload, Users,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -15,6 +15,7 @@ import {
import { useStore, fmtEUR } from '../store/store'
import { fmtDatum } from '../lib/format'
import PdfLightbox from '../components/PdfLightbox'
import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection'
import type { DocumentItem, KautionArt, Mieter, Unit, Vertragsstatus } from '../types'
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
@@ -44,18 +45,6 @@ const KAUTION_ART_LABEL: Record<KautionArt, string> = {
sonstiges: 'Andere',
}
/** Optischer Abschnitt im einspaltigen Formular. */
function Abschnitt({ titel, children }: { titel: string; children: React.ReactNode }) {
return (
<div className="space-y-3 rounded-xl border bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/50">
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
{titel}
</h4>
{children}
</div>
)
}
function Feld({ id, label, hint, ...input }: {
id: string
label: string
@@ -226,40 +215,42 @@ function MieterDialog({ mieter }: { mieter?: Mieter }) {
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>
{bearbeiten ? `Mieter „${mieter!.vorname} ${mieter!.name}" bearbeiten` : 'Neuen Mieter anlegen'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Abschnitt titel="Person">
<FormSection titel="Person" icon={Users}>
<Feld id="m-name" label="Name" placeholder="z. B. Berger" value={name} onChange={(e) => setName(e.target.value)} />
<Feld id="m-vorname" label="Vorname" placeholder="z. B. Anna" value={vorname} onChange={(e) => setVorname(e.target.value)} />
<Feld id="m-tel" label="Telefon" placeholder="z. B. 0151 23456789" value={telefon} onChange={(e) => setTelefon(e.target.value)} />
<Feld id="m-mail" label="E-Mail" type="email" placeholder="z. B. a.berger@mail.de" value={email} onChange={(e) => setEmail(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Wohnung">
<div className="space-y-1.5">
<Label>Wohneinheit</Label>
<Select value={unitId} onValueChange={setUnitId}>
<SelectTrigger><SelectValue placeholder="Wohnung wählen …" /></SelectTrigger>
<SelectContent>
{wohnungen.map((u) => (
<SelectItem key={u.id} value={u.id}>{einheitLabel(u)}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-slate-400 dark:text-slate-500">
Pro Wohnung kann genau ein Mieter geführt werden. Beim Speichern wird die Wohnung auf „Vermietet" gesetzt.
</p>
</div>
</Abschnitt>
<Abschnitt titel="Miete">
</FormSection>
<FormSection titel="Wohnung" icon={Home}>
<Wide>
<div className="space-y-1.5">
<Label>Wohneinheit</Label>
<Select value={unitId} onValueChange={setUnitId}>
<SelectTrigger><SelectValue placeholder="Wohnung wählen …" /></SelectTrigger>
<SelectContent>
{wohnungen.map((u) => (
<SelectItem key={u.id} value={u.id}>{einheitLabel(u)}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-slate-400 dark:text-slate-500">
Pro Wohnung kann genau ein Mieter geführt werden. Beim Speichern wird die Wohnung auf „Vermietet" gesetzt.
</p>
</div>
</Wide>
</FormSection>
<FormSection titel="Miete" icon={BadgeEuro}>
<Feld id="m-kalt" label="Kaltmiete (€/Monat)" inputMode="decimal" placeholder="z. B. 520" value={kaltmiete} onChange={(e) => setKaltmiete(e.target.value)} />
<Feld id="m-warm" label="Warmmiete (€/Monat)" inputMode="decimal" placeholder="z. B. 690" value={warmmiete} onChange={(e) => setWarmmiete(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Vertrag">
</FormSection>
<FormSection titel="Vertrag" icon={CalendarRange}>
<div className="space-y-1.5">
<Label>Vertragsstatus</Label>
<Select value={vertragsstatus} onValueChange={(v) => setVertragsstatus(v as Vertragsstatus)}>
@@ -289,8 +280,8 @@ function MieterDialog({ mieter }: { mieter?: Mieter }) {
)}
<Feld id="m-beginn" label="Mietbeginn" type="date" value={mietbeginnAm} onChange={(e) => setMietbeginnAm(e.target.value)} />
<Feld id="m-ende" label="Mietende" hint="Leer lassen bei unbefristetem Vertrag" type="date" value={mietendeAm} onChange={(e) => setMietendeAm(e.target.value)} />
</Abschnitt>
<Abschnitt titel="Kaution">
</FormSection>
<FormSection titel="Kaution" icon={ShieldCheck}>
<Feld id="m-kaut-hoehe" label="Kaution Höhe (€)" inputMode="decimal" placeholder="z. B. 1.500" value={kautionHoehe} onChange={(e) => setKautionHoehe(e.target.value)} />
<div className="space-y-1.5">
<Label>Kaution Art</Label>
@@ -319,25 +310,23 @@ function MieterDialog({ mieter }: { mieter?: Mieter }) {
{kautionszahlung === 'raten' && (
<Feld id="m-kaut-raten" label="Anzahl der Raten" inputMode="numeric" placeholder="z. B. 3" value={kautionRaten} onChange={(e) => setKautionRaten(e.target.value)} />
)}
</Abschnitt>
</FormSection>
{bearbeiten && (
<Abschnitt titel="Dokumente">
<p className="text-xs text-slate-400 dark:text-slate-500">
Personalausweis, SCHUFA-Auskunft, Bankauszug u. a. – Klick auf die Vorschau öffnet Druck- und
E-Mail-Optionen. Pro Kategorie wird genau eine Datei (PDF oder Bild) geführt.
</p>
<div className="grid gap-3 sm:grid-cols-2">
{DOKUMENT_KATEGORIEN.map((k) => (
<DokumentSlot key={k} mieter={mieter!} kategorie={k} />
))}
</div>
</Abschnitt>
)}
{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}
</div>
<FormSection titel="Dokumente" icon={FileText}>
<Wide>
<p className="text-xs text-slate-400 dark:text-slate-500">
Personalausweis, SCHUFA-Auskunft, Bankauszug u. a. – Klick auf die Vorschau öffnet Druck- und
E-Mail-Optionen. Pro Kategorie wird genau eine Datei (PDF oder Bild) geführt.
</p>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
{DOKUMENT_KATEGORIEN.map((k) => (
<DokumentSlot key={k} mieter={mieter!} kategorie={k} />
))}
</div>
</Wide>
</FormSection>
)}
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
+69 -82
View File
@@ -11,7 +11,7 @@
import { useMemo, useRef, useState } from 'react'
import {
FileSignature, Plus, Pencil, Trash2, Mail, FileText, Upload, UserPlus,
Settings, Search, TriangleAlert, Eye,
Settings, Search, TriangleAlert, Eye, BadgeEuro,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -26,6 +26,7 @@ import {
import { useStore, fmtEUR } from '../store/store'
import { fmtDatum } from '../lib/format'
import PdfLightbox from '../components/PdfLightbox'
import { FormSection, Wide, FormFehler } from '@/components/forms/FormSection'
import type { DocumentItem, Mietvertrag, MietvertragForm, Unit, VermieterDaten } from '../types'
/** Komma oder Punkt als Dezimaltrenner akzeptieren. */
@@ -77,20 +78,22 @@ function VermieterDialog() {
<Settings className="h-4 w-4" /> Vermieterdaten
</Button>
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Vermieterdaten</DialogTitle>
</DialogHeader>
<p className="text-xs text-slate-500 dark:text-slate-400">
Diese Angaben erscheinen im Briefkopf und in den Vertragsklauseln der automatisch erzeugten
Mietvertrags-Entwürfe (Name, Anschrift, Kontoverbindung).
</p>
<div className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="v-name">Name des Vermieters</Label>
<Input id="v-name" placeholder="z. B. Hinterstein Immobilien, Inhaber Joseph Hinterstein" value={f.name} onChange={set('name')} />
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-4">
<p className="text-xs text-slate-500 dark:text-slate-400">
Diese Angaben erscheinen im Briefkopf und in den Vertragsklauseln der automatisch erzeugten
Mietvertrags-Entwürfe (Name, Anschrift, Kontoverbindung).
</p>
<FormSection titel="Vermieter" icon={Settings}>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="v-name">Name des Vermieters</Label>
<Input id="v-name" placeholder="z. B. Hinterstein Immobilien, Inhaber Joseph Hinterstein" value={f.name} onChange={set('name')} />
</div>
</Wide>
<div className="space-y-1.5">
<Label htmlFor="v-strasse">Straße</Label>
<Input id="v-strasse" value={f.strasse} onChange={set('strasse')} />
@@ -99,8 +102,6 @@ function VermieterDialog() {
<Label htmlFor="v-nr">Hausnummer</Label>
<Input id="v-nr" value={f.hausnummer} onChange={set('hausnummer')} />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="v-plz">PLZ</Label>
<Input id="v-plz" value={f.plz} onChange={set('plz')} />
@@ -109,8 +110,6 @@ function VermieterDialog() {
<Label htmlFor="v-ort">Ort</Label>
<Input id="v-ort" value={f.ort} onChange={set('ort')} />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="v-tel">Telefon</Label>
<Input id="v-tel" value={f.telefon} onChange={set('telefon')} />
@@ -119,8 +118,6 @@ function VermieterDialog() {
<Label htmlFor="v-mail">E-Mail</Label>
<Input id="v-mail" type="email" value={f.email} onChange={set('email')} />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="v-bank">Bank</Label>
<Input id="v-bank" value={f.bankname} onChange={set('bankname')} />
@@ -129,10 +126,8 @@ function VermieterDialog() {
<Label htmlFor="v-iban">IBAN</Label>
<Input id="v-iban" placeholder="DE00 0000 0000 0000 0000 00" value={f.iban} onChange={set('iban')} />
</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}</div>
)}
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
@@ -207,78 +202,70 @@ function MietvertragDialog({ vertrag, einheiten }: { vertrag?: Mietvertrag; einh
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{bearbeiten ? 'Mietvertrag bearbeiten' : 'Neuen Mietvertrag anlegen'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label>Wohnung</Label>
<Select value={unitId} onValueChange={einheitWechseln}>
<SelectTrigger><SelectValue placeholder="Wohnung wählen …" /></SelectTrigger>
<SelectContent>
{einheiten.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.building}, Whg. {u.number} (Etage {u.floor}, {u.size} m²{u.status === 'leerstand' ? ', Leerstand' : ''})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<FormSection titel="Wohnung" icon={FileSignature}>
<Wide>
<div className="space-y-1.5">
<Label>Wohnung</Label>
<Select value={unitId} onValueChange={einheitWechseln}>
<SelectTrigger><SelectValue placeholder="Wohnung wählen …" /></SelectTrigger>
<SelectContent>
{einheiten.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.building}, Whg. {u.number} (Etage {u.floor}, {u.size} m²{u.status === 'leerstand' ? ', Leerstand' : ''})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</Wide>
</FormSection>
<div className="space-y-3 rounded-xl border bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/50">
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
Zukünftige Mieterin / zukünftiger Mieter
</h4>
<FormSection titel="Zukünftige Mieterin / zukünftiger Mieter" icon={UserPlus}>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="m-name">Name (Vor- und Nachname)</Label>
<Input id="m-name" placeholder="z. B. Anna Berger" value={mieterName} onChange={(e) => setMieterName(e.target.value)} />
</div>
</Wide>
<div className="space-y-1.5">
<Label htmlFor="m-name">Name (Vor- und Nachname)</Label>
<Input id="m-name" placeholder="z. B. Anna Berger" value={mieterName} onChange={(e) => setMieterName(e.target.value)} />
<Label htmlFor="m-mail">E-Mail</Label>
<Input id="m-mail" type="email" placeholder="für Versand des Entwurfs" value={mieterEmail} onChange={(e) => setMieterEmail(e.target.value)} />
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="m-mail">E-Mail</Label>
<Input id="m-mail" type="email" placeholder="für Versand des Entwurfs" value={mieterEmail} onChange={(e) => setMieterEmail(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-tel">Telefon</Label>
<Input id="m-tel" value={mieterTelefon} onChange={(e) => setMieterTelefon(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-tel">Telefon</Label>
<Input id="m-tel" value={mieterTelefon} onChange={(e) => setMieterTelefon(e.target.value)} />
</div>
</div>
</FormSection>
<div className="space-y-3 rounded-xl border bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/50">
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
Mietkonditionen
</h4>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="m-beginn">Mietbeginn</Label>
<Input id="m-beginn" placeholder="TT.MM.JJJJ" value={beginnAm} onChange={(e) => setBeginnAm(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-ende">Mietende</Label>
<Input id="m-ende" placeholder="leer = unbefristet" value={endeAm} onChange={(e) => setEndeAm(e.target.value)} />
</div>
<FormSection titel="Mietkonditionen" icon={BadgeEuro}>
<div className="space-y-1.5">
<Label htmlFor="m-beginn">Mietbeginn</Label>
<Input id="m-beginn" placeholder="TT.MM.JJJJ" value={beginnAm} onChange={(e) => setBeginnAm(e.target.value)} />
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="space-y-1.5">
<Label htmlFor="m-kalt">Kaltmiete (€/Monat)</Label>
<Input id="m-kalt" inputMode="decimal" value={kaltmiete} onChange={(e) => setKaltmiete(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-nk">Nebenkosten (€/Monat)</Label>
<Input id="m-nk" inputMode="decimal" value={nebenkosten} onChange={(e) => setNebenkosten(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-kaution">Kaution (€)</Label>
<Input id="m-kaution" inputMode="decimal" value={kaution} onChange={(e) => setKaution(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-ende">Mietende</Label>
<Input id="m-ende" placeholder="leer = unbefristet" value={endeAm} onChange={(e) => setEndeAm(e.target.value)} />
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="m-kalt">Kaltmiete (€/Monat)</Label>
<Input id="m-kalt" inputMode="decimal" value={kaltmiete} onChange={(e) => setKaltmiete(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-nk">Nebenkosten (€/Monat)</Label>
<Input id="m-nk" inputMode="decimal" value={nebenkosten} onChange={(e) => setNebenkosten(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label htmlFor="m-kaution">Kaution (€)</Label>
<Input id="m-kaution" inputMode="decimal" value={kaution} onChange={(e) => setKaution(e.target.value)} />
</div>
</FormSection>
{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}</div>
)}
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
+18 -9
View File
@@ -63,10 +63,13 @@ export default function Profil() {
<div className="grid gap-4 lg:grid-cols-2">
{/* Feste Daten (nur Ansicht) */}
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="mb-4 flex items-center gap-2 font-semibold">
<IdCard className="h-4 w-4 text-teal-600" /> Konto & Wohnung
<div className="mb-4 flex items-center gap-2.5 border-b border-slate-100 pb-3">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-teal-600/10 text-teal-700 dark:bg-teal-400/10 dark:text-teal-300">
<IdCard className="h-4 w-4" />
</span>
<h2 className="font-display text-[13px] font-semibold uppercase tracking-wider text-teal-800 dark:text-teal-300">Konto & Wohnung</h2>
</div>
<dl className="space-y-3 text-sm">
<dl className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
<div>
<dt className="text-xs uppercase tracking-wide text-slate-400">Name</dt>
<dd className="font-medium">{me.name}</dd>
@@ -96,10 +99,13 @@ export default function Profil() {
{/* Kontaktdaten (editierbar) */}
<div className="rounded-2xl border bg-white p-5 shadow-sm">
<div className="mb-4 flex items-center gap-2 font-semibold">
<Phone className="h-4 w-4 text-teal-600" /> Kontaktdaten
<div className="mb-4 flex items-center gap-2.5 border-b border-slate-100 pb-3">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-teal-600/10 text-teal-700 dark:bg-teal-400/10 dark:text-teal-300">
<Phone className="h-4 w-4" />
</span>
<h2 className="font-display text-[13px] font-semibold uppercase tracking-wider text-teal-800 dark:text-teal-300">Kontaktdaten</h2>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="p-email">E-Mail-Adresse</Label>
<div className="relative">
@@ -135,13 +141,16 @@ export default function Profil() {
{/* Bankverbindung (editierbar; Mieter) */}
{role === 'tenant' && (
<div className="rounded-2xl border bg-white p-5 shadow-sm lg:col-span-2">
<div className="mb-4 flex items-center gap-2 font-semibold">
<Landmark className="h-4 w-4 text-teal-600" /> Bankverbindung
<div className="mb-4 flex items-center gap-2.5 border-b border-slate-100 pb-3">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-teal-600/10 text-teal-700 dark:bg-teal-400/10 dark:text-teal-300">
<Landmark className="h-4 w-4" />
</span>
<h2 className="font-display text-[13px] font-semibold uppercase tracking-wider text-teal-800 dark:text-teal-300">Bankverbindung</h2>
<span className="text-xs font-normal text-slate-400">
(für Rückzahlungen, z. B. Kaution)
</span>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="p-konto">Kontoinhaber</Label>
<Input
+27 -22
View File
@@ -23,6 +23,7 @@ import {
} from '@/components/ui/select'
import { useStore } from '../store/store'
import { fmtDatum, fmtDatumZeit } from '../lib/format'
import { FormSection, Wide } from '@/components/forms/FormSection'
import type { TicketPriority, TicketStatus } from '../types'
const statusStyle: Record<TicketStatus, string> = {
@@ -80,21 +81,23 @@ function NewTicketDialog() {
<Plus className="h-4 w-4" /> Neue Schadensmeldung
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Schaden oder Störung melden</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="t-title">Kurzbeschreibung</Label>
<Input
id="t-title"
placeholder="z. B. Wasserhahn tropft"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormSection titel="Meldung" icon={Wrench}>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="t-title">Kurzbeschreibung</Label>
<Input
id="t-title"
placeholder="z. B. Wasserhahn tropft"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</Wide>
<div className="space-y-1.5">
<Label>Kategorie</Label>
<Select value={category} onValueChange={setCategory}>
@@ -121,17 +124,19 @@ function NewTicketDialog() {
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="t-desc">Beschreibung</Label>
<Textarea
id="t-desc"
rows={4}
placeholder="Was ist passiert? Seit wann? ..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<Wide>
<div className="space-y-1.5">
<Label htmlFor="t-desc">Beschreibung</Label>
<Textarea
id="t-desc"
rows={4}
placeholder="Was ist passiert? Seit wann? ..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
</Wide>
</FormSection>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
+11 -20
View File
@@ -21,6 +21,7 @@ import {
} from '@/components/ui/select'
import { useStore } from '../store/store'
import { fmtDatum } from '../lib/format'
import { FormSection, FormFehler } from '@/components/forms/FormSection'
import type { Meter, MeterReading, ZaehlerArt } from '../types'
const ARTEN: ZaehlerArt[] = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges']
@@ -100,12 +101,12 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{bearbeiten ? `Zähler ${meter!.nummer} bearbeiten` : 'Neuen Zähler anlegen'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<FormSection titel="Zählerdaten" icon={Gauge}>
<div className="space-y-1.5">
<Label htmlFor="z-nr">Zählernummer</Label>
<Input
@@ -126,8 +127,6 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="z-stand">Aktueller Zählerstand</Label>
<Input
@@ -149,8 +148,8 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
/>
</div>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
</FormSection>
<FormSection titel="Zuordnung" icon={Building2}>
<div className="space-y-1.5">
<Label>Wohneinheit</Label>
<Select value={unitId} onValueChange={setUnitId}>
@@ -182,12 +181,8 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
<p className="text-xs text-slate-400">Nur nötig für Zähler ohne Wohnungsbezug.</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}
</div>
)}
</FormSection>
<FormFehler fehler={fehler} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
@@ -336,7 +331,7 @@ function StandEintragenDialog({ meter }: { meter: Meter }) {
<Plus className="h-3.5 w-3.5" /> Stand eintragen
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>
Stand eintragen · Zähler {meter.nummer}
@@ -344,7 +339,7 @@ function StandEintragenDialog({ meter }: { meter: Meter }) {
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<FormSection titel="Neue Ablesung" icon={CalendarDays}>
<div className="space-y-1.5">
<Label htmlFor="zr-stand">Neuer Zählerstand</Label>
<Input
@@ -364,12 +359,8 @@ function StandEintragenDialog({ meter }: { meter: Meter }) {
onChange={(e) => setDatum(e.target.value)}
/>
</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}
</div>
)}
</FormSection>
<FormFehler fehler={fehler} />
{warnung && (
<div className="rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:bg-amber-950 dark:text-amber-200" role="alert">
{warnung}