diff --git a/kimi.md b/kimi.md index 7c77f06..45098dc 100644 --- a/kimi.md +++ b/kimi.md @@ -189,6 +189,22 @@ tar -czf mp-dist.tar.gz -C dist . ## 8. Verlauf / offene Punkte +**Erledigt 04.10. (v1.20):** Stammdaten-Selbstpflege. users-Tabelle um +telefon/kontoinhaber/iban/bankname erweitert (ALTER ... ADD COLUMN IF NOT +EXISTS). PATCH /api/auth/profil (auth, eigene Daten): E-Mail (Pflicht + +Eindeutigkeitsprüfung case-insensitiv), Telefon, Kontoinhaber, IBAN +(Leerzeichen werden entfernt/großgeschrieben, Formatprüfung +^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$), Bankname. oeffentlicherBenutzer liefert die +neuen Felder mit (gelten damit auch für /api/auth/me, Bootstrap benutzer, +Benutzerverwaltung). Frontend: neue Seite /profil (Nav „Mein Profil", beide +Rollen): Ansichtskarte Konto & Wohnung (Name/Benutzername/Wohneit/ Mitglied +seit, readonly), editierbare Kontaktdaten (E-Mail, Telefon) und +Bankverbindung (nur Mieter, Hinweis auf Kaution). Store: aktualisiereProfil +→ PATCH, setMe mit Server-Antwort; Erfolgs-/Fehlermeldung inline. E2E +verifiziert: Speichern als a.berger (IBAN normalisiert), Auslesen via +/api/auth/me, Negativtests ungültige IBAN + fremde E-Mail werden sauber +abgelehnt. Demo-Daten der a.berger bleiben als Schaufenster-Datensatz. + **Erledigt 04.10. (v1.19):** Schadensmeldungen mit Kommunikation verknüpft. threads-Tabelle um `ticket_id TEXT REFERENCES tickets(id)` erweitert (ALTER + Index idx_threads_ticket). POST /api/tickets legt den Auto-Thread bereits mit diff --git a/server/src/db.js b/server/src/db.js index b04f433..ac56c07 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -106,6 +106,11 @@ CREATE TABLE IF NOT EXISTS users ( erstellt_am TEXT NOT NULL, letzter_login TEXT ); +-- Stammdaten zur Selbstpflege durch den Nutzer (v1.20) +ALTER TABLE users ADD COLUMN IF NOT EXISTS telefon TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS kontoinhaber TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS iban TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS bankname TEXT; CREATE TABLE IF NOT EXISTS registration_codes ( code TEXT PRIMARY KEY, diff --git a/server/src/index.js b/server/src/index.js index 5ffaee8..6f7094e 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -135,6 +135,10 @@ const oeffentlicherBenutzer = (b) => ({ aktiv: b.aktiv, erstelltAm: b.erstellt_am, letzterLogin: b.letzter_login ?? undefined, + telefon: b.telefon ?? null, + kontoinhaber: b.kontoinhaber ?? null, + iban: b.iban ?? null, + bankname: b.bankname ?? null, }) async function sessionAnlegen(res, userId, req) { @@ -238,6 +242,31 @@ app.get('/api/auth/me', auth, (req, res) => { res.json({ benutzer: oeffentlicherBenutzer(req.user) }) }) +// Stammdaten-Selbstpflege (v1.20): Telefon, E-Mail, Bankverbindung +app.patch('/api/auth/profil', auth, async (req, res, next) => { + try { + const { email, telefon, kontoinhaber, iban, bankname } = req.body || {} + const emailNeu = String(email || '').trim() + if (!/^\S+@\S+\.\S+$/.test(emailNeu)) + return res.status(400).json({ fehler: 'Gültige E-Mail-Adresse erforderlich.' }) + const { rows: fremde } = await pool.query( + 'SELECT id FROM users WHERE lower(email) = lower($1) AND id <> $2', [emailNeu, req.user.id], + ) + if (fremde.length) + return res.status(400).json({ fehler: 'Diese E-Mail-Adresse wird bereits verwendet.' }) + const ibanNeu = String(iban || '').trim().replace(/\s+/g, '').toUpperCase() + if (ibanNeu && !/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(ibanNeu)) + return res.status(400).json({ fehler: 'Ungültige IBAN.' }) + const { rows } = await pool.query( + `UPDATE users SET email = $1, telefon = $2, kontoinhaber = $3, iban = $4, bankname = $5 + WHERE id = $6 RETURNING *`, + [emailNeu, String(telefon || '').trim() || null, String(kontoinhaber || '').trim() || null, + ibanNeu || null, String(bankname || '').trim() || null, req.user.id], + ) + res.json({ benutzer: oeffentlicherBenutzer(rows[0]) }) + } catch (e) { next(e) } +}) + app.post('/api/auth/register', async (req, res, next) => { try { const { code, benutzername, name, email, passwortHash } = req.body || {} diff --git a/src/App.tsx b/src/App.tsx index 9938dce..e6cf31f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 74f3a71..ff596e4 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -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() { diff --git a/src/pages/Profil.tsx b/src/pages/Profil.tsx new file mode 100644 index 0000000..4266220 --- /dev/null +++ b/src/pages/Profil.tsx @@ -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(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 ( +
+
+

Mein Profil

+

+ Ihre Stammdaten – hier gepflegt, überall im Portal aktuell. +

+
+ +
+ {/* Feste Daten (nur Ansicht) */} +
+
+ Konto & Wohnung +
+
+
+
Name
+
{me.name}
+
+
+
Benutzername
+
{me.benutzername}
+
+
+
+ {role === 'admin' ? 'Gruppe' : 'Wohneinheit'} +
+
+ {role === 'admin' + ? 'Verwaltung' + : einheit + ? `${einheit.building}, Whg. ${einheit.number}` + : '–'} +
+
+
+
Mitglied seit
+
{me.erstelltAm}
+
+
+
+ + {/* Kontaktdaten (editierbar) */} +
+
+ Kontaktdaten +
+
+
+ +
+ + setEmail(e.target.value)} + /> +
+

+ Wird für Benachrichtigungen des Portals verwendet. +

+
+
+ +
+ + setTelefon(e.target.value)} + /> +
+
+
+
+ + {/* Bankverbindung (editierbar; Mieter) */} + {role === 'tenant' && ( +
+
+ Bankverbindung + + (für Rückzahlungen, z. B. Kaution) + +
+
+
+ + setKontoinhaber(e.target.value)} + /> +
+
+ + setIban(e.target.value)} + /> +
+
+ + setBankname(e.target.value)} + /> +
+
+
+ )} +
+ +
+ + {gespeichert && ( + + Gespeichert. + + )} + {fehler && {fehler}} +
+
+ ) +} diff --git a/src/store/store.tsx b/src/store/store.tsx index ecd1b79..4c96b83 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -80,6 +80,14 @@ interface Store extends Bootstrap { meineEinheit: Unit login: (benutzername: string, passwort: string) => Promise logout: () => Promise + /** Stammdaten-Selbstpflege (v1.20): Telefon, E-Mail, Bankverbindung */ + aktualisiereProfil: (daten: { + email: string + telefon: string + kontoinhaber: string + iban: string + bankname: string + }) => Promise 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', { diff --git a/src/types/index.ts b/src/types/index.ts index a535daa..06de0a8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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 {