v1.20: Mieter pflegen eigene Stammdaten (Telefon, E-Mail, Bankverbindung)
This commit is contained in:
@@ -12,6 +12,7 @@ import Zaehler from './pages/Zaehler'
|
||||
import Kosten from './pages/Kosten'
|
||||
import Bestand from './pages/Bestand'
|
||||
import BenutzerVerwaltung from './pages/BenutzerVerwaltung'
|
||||
import Profil from './pages/Profil'
|
||||
|
||||
/** Fängt Render-Fehler ab – statt schwarzem Bildschirm: Hinweis + Reload. */
|
||||
class FehlerGrenze extends Component<{ children: ReactNode }, { fehler: boolean }> {
|
||||
@@ -88,6 +89,7 @@ export default function App() {
|
||||
<Route path="/bestand" element={<Bestand />} />
|
||||
<Route path="/dokumente" element={<Documents />} />
|
||||
<Route path="/nachrichten" element={<Messages />} />
|
||||
<Route path="/profil" element={<Profil />} />
|
||||
<Route path="/benutzer" element={<BenutzerVerwaltung />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Building2,
|
||||
Gauge,
|
||||
Calculator,
|
||||
UserRound,
|
||||
} from 'lucide-react'
|
||||
import { useStore } from '../store/store'
|
||||
import ThemeToggle from './ThemeToggle'
|
||||
@@ -23,6 +24,7 @@ const navItems = [
|
||||
{ to: '/kosten', label: 'Steuer & Kosten', icon: Calculator },
|
||||
{ to: '/dokumente', label: 'Dokumente', icon: FileText },
|
||||
{ to: '/nachrichten', label: 'Nachrichten', icon: MessagesSquare },
|
||||
{ to: '/profil', label: 'Mein Profil', icon: UserRound },
|
||||
]
|
||||
|
||||
export default function AppLayout() {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Check, IdCard, Phone, Landmark, Mail } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useStore } from '../store/store'
|
||||
|
||||
export default function Profil() {
|
||||
const { me, role, meineEinheit, units, aktualisiereProfil } = useStore()
|
||||
const [email, setEmail] = useState('')
|
||||
const [telefon, setTelefon] = useState('')
|
||||
const [kontoinhaber, setKontoinhaber] = useState('')
|
||||
const [iban, setIban] = useState('')
|
||||
const [bankname, setBankname] = useState('')
|
||||
const [sende, setSende] = useState(false)
|
||||
const [fehler, setFehler] = useState<string | null>(null)
|
||||
const [gespeichert, setGespeichert] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!me) return
|
||||
setEmail(me.email ?? '')
|
||||
setTelefon(me.telefon ?? '')
|
||||
setKontoinhaber(me.kontoinhaber ?? '')
|
||||
setIban(me.iban ?? '')
|
||||
setBankname(me.bankname ?? '')
|
||||
}, [me])
|
||||
|
||||
if (!me) return null
|
||||
|
||||
const einheit = units.find((u) => u.id === me.einheitId) ?? meineEinheit
|
||||
|
||||
const speichern = async () => {
|
||||
if (sende) return
|
||||
setSende(true)
|
||||
setFehler(null)
|
||||
setGespeichert(false)
|
||||
const err = await aktualisiereProfil({
|
||||
email: email.trim(),
|
||||
telefon: telefon.trim(),
|
||||
kontoinhaber: kontoinhaber.trim(),
|
||||
iban: iban.trim(),
|
||||
bankname: bankname.trim(),
|
||||
})
|
||||
setSende(false)
|
||||
if (err) {
|
||||
setFehler(err)
|
||||
return
|
||||
}
|
||||
setGespeichert(true)
|
||||
window.setTimeout(() => setGespeichert(false), 4000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Mein Profil</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
Ihre Stammdaten – hier gepflegt, überall im Portal aktuell.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<dl className="space-y-3 text-sm">
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-wide text-slate-400">Name</dt>
|
||||
<dd className="font-medium">{me.name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-wide text-slate-400">Benutzername</dt>
|
||||
<dd className="font-medium">{me.benutzername}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-wide text-slate-400">
|
||||
{role === 'admin' ? 'Gruppe' : 'Wohneinheit'}
|
||||
</dt>
|
||||
<dd className="font-medium">
|
||||
{role === 'admin'
|
||||
? 'Verwaltung'
|
||||
: einheit
|
||||
? `${einheit.building}, Whg. ${einheit.number}`
|
||||
: '–'}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-wide text-slate-400">Mitglied seit</dt>
|
||||
<dd className="font-medium">{me.erstelltAm}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="p-email">E-Mail-Adresse</Label>
|
||||
<div className="relative">
|
||||
<Mail className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
id="p-email"
|
||||
type="email"
|
||||
className="pl-9"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">
|
||||
Wird für Benachrichtigungen des Portals verwendet.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="p-tel">Telefon</Label>
|
||||
<div className="relative">
|
||||
<Phone className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
id="p-tel"
|
||||
className="pl-9"
|
||||
placeholder="z. B. 0171 2345678"
|
||||
value={telefon}
|
||||
onChange={(e) => setTelefon(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
<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="space-y-1.5">
|
||||
<Label htmlFor="p-konto">Kontoinhaber</Label>
|
||||
<Input
|
||||
id="p-konto"
|
||||
placeholder="Vor- und Nachname"
|
||||
value={kontoinhaber}
|
||||
onChange={(e) => setKontoinhaber(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="p-iban">IBAN</Label>
|
||||
<Input
|
||||
id="p-iban"
|
||||
placeholder="DE00 0000 0000 0000 0000 00"
|
||||
value={iban}
|
||||
onChange={(e) => setIban(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="p-bank">Bank</Label>
|
||||
<Input
|
||||
id="p-bank"
|
||||
placeholder="z. B. Sparkasse"
|
||||
value={bankname}
|
||||
onChange={(e) => setBankname(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button onClick={speichern} disabled={sende} className="gap-2">
|
||||
{sende ? 'Wird gespeichert …' : 'Änderungen speichern'}
|
||||
</Button>
|
||||
{gespeichert && (
|
||||
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-emerald-600">
|
||||
<Check className="h-4 w-4" /> Gespeichert.
|
||||
</span>
|
||||
)}
|
||||
{fehler && <span className="text-sm font-medium text-red-600">{fehler}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -80,6 +80,14 @@ interface Store extends Bootstrap {
|
||||
meineEinheit: Unit
|
||||
login: (benutzername: string, passwort: string) => Promise<string | null>
|
||||
logout: () => Promise<void>
|
||||
/** Stammdaten-Selbstpflege (v1.20): Telefon, E-Mail, Bankverbindung */
|
||||
aktualisiereProfil: (daten: {
|
||||
email: string
|
||||
telefon: string
|
||||
kontoinhaber: string
|
||||
iban: string
|
||||
bankname: string
|
||||
}) => Promise<string | null>
|
||||
registrieren: (
|
||||
code: string,
|
||||
benutzername: string,
|
||||
@@ -237,6 +245,15 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
||||
setMe(null)
|
||||
setDaten(LEER)
|
||||
},
|
||||
async aktualisiereProfil(daten) {
|
||||
try {
|
||||
const { benutzer } = await apiPatch<{ benutzer: Benutzer }>('/api/auth/profil', daten)
|
||||
setMe(benutzer)
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async registrieren(code, benutzername, name, email, passwort) {
|
||||
try {
|
||||
const { benutzer } = await apiPost<{ benutzer: Benutzer }>('/api/auth/register', {
|
||||
|
||||
@@ -13,6 +13,11 @@ export interface Benutzer {
|
||||
aktiv: boolean
|
||||
erstelltAm: string
|
||||
letzterLogin?: string
|
||||
/** Selbst gepflegte Stammdaten (v1.20) */
|
||||
telefon?: string | null
|
||||
kontoinhaber?: string | null
|
||||
iban?: string | null
|
||||
bankname?: string | null
|
||||
}
|
||||
|
||||
export interface RegistrierungsCode {
|
||||
|
||||
Reference in New Issue
Block a user