2026-05-09 10:02:56 +02:00
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
2026-05-05 21:05:52 +02:00
|
|
|
|
import { Navigate } from 'react-router-dom'
|
|
|
|
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
|
|
import api from '../utils/api'
|
|
|
|
|
|
import AdminPageNav from '../components/AdminPageNav'
|
|
|
|
|
|
|
|
|
|
|
|
const CLUB_ROLE_OPTIONS = [
|
|
|
|
|
|
{ code: 'club_admin', label: 'Vereinsadmin' },
|
|
|
|
|
|
{ code: 'trainer', label: 'Trainer' },
|
|
|
|
|
|
{ code: 'division_lead', label: 'Spartenleitung' },
|
|
|
|
|
|
{ code: 'content_editor', label: 'Inhalte bearbeiten' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const PORTAL_ROLE_LABEL = {
|
2026-05-05 21:05:52 +02:00
|
|
|
|
user: 'Nutzer',
|
2026-05-09 10:02:56 +02:00
|
|
|
|
trainer: 'Portal-Trainer',
|
|
|
|
|
|
admin: 'Portal-Administrator',
|
|
|
|
|
|
superadmin: 'Super-Administrator',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clubAdminClubIds(user) {
|
|
|
|
|
|
return (user?.clubs || [])
|
|
|
|
|
|
.filter((c) => Array.isArray(c.roles) && c.roles.includes('club_admin'))
|
|
|
|
|
|
.map((c) => c.id)
|
2026-05-05 21:05:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
function clubSelectOptions(user, allClubs, isPlatformAdmin) {
|
|
|
|
|
|
if (!isPlatformAdmin) {
|
|
|
|
|
|
const ids = new Set(clubAdminClubIds(user))
|
|
|
|
|
|
return (allClubs || []).filter((c) => ids.has(c.id))
|
|
|
|
|
|
}
|
|
|
|
|
|
return allClubs || []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Plattform-Rollen im UI (Tier/Abo entfällt bis auf Weiteres). */
|
|
|
|
|
|
function portalRoleSelectOptions(viewerIsSuperadmin, currentRole) {
|
|
|
|
|
|
const base = [
|
|
|
|
|
|
{ value: 'user', label: PORTAL_ROLE_LABEL.user },
|
|
|
|
|
|
{ value: 'trainer', label: `${PORTAL_ROLE_LABEL.trainer} (Legacy)` },
|
|
|
|
|
|
{ value: 'admin', label: PORTAL_ROLE_LABEL.admin },
|
|
|
|
|
|
]
|
|
|
|
|
|
const cur = (currentRole || 'user').toLowerCase()
|
|
|
|
|
|
if (viewerIsSuperadmin) base.push({ value: 'superadmin', label: PORTAL_ROLE_LABEL.superadmin })
|
|
|
|
|
|
const values = new Set(base.map((x) => x.value))
|
|
|
|
|
|
if (cur && !values.has(cur)) {
|
|
|
|
|
|
base.unshift({ value: cur, label: cur })
|
|
|
|
|
|
}
|
|
|
|
|
|
return base
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function AdminUsersPage() {
|
2026-05-05 21:05:52 +02:00
|
|
|
|
const { user } = useAuth()
|
|
|
|
|
|
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const isSuperadminViewer = user?.role === 'superadmin'
|
|
|
|
|
|
const managedClubIds = useMemo(() => clubAdminClubIds(user), [user])
|
|
|
|
|
|
const clubOrgMode = !isPlatformAdmin && managedClubIds.length > 0
|
|
|
|
|
|
const canAccess = isPlatformAdmin || clubOrgMode
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const [platformUsers, setPlatformUsers] = useState([])
|
2026-05-05 21:05:52 +02:00
|
|
|
|
const [clubs, setClubs] = useState([])
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const [clubMembers, setClubMembers] = useState([])
|
|
|
|
|
|
const [selectedClubId, setSelectedClubId] = useState(
|
|
|
|
|
|
() => managedClubIds[0] ?? null
|
|
|
|
|
|
)
|
2026-05-05 21:05:52 +02:00
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
|
const [portalDraft, setPortalDraft] = useState({})
|
|
|
|
|
|
const [assignModal, setAssignModal] = useState(null)
|
|
|
|
|
|
const [assignRoles, setAssignRoles] = useState(['trainer'])
|
|
|
|
|
|
const [clubEditModal, setClubEditModal] = useState(null)
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const [addMemberOpen, setAddMemberOpen] = useState(false)
|
|
|
|
|
|
const [newMemberProfileId, setNewMemberProfileId] = useState('')
|
|
|
|
|
|
const [newMemberRoles, setNewMemberRoles] = useState(['trainer'])
|
2026-05-09 10:15:16 +02:00
|
|
|
|
const [pwdModal, setPwdModal] = useState(null)
|
|
|
|
|
|
const [pwdNew, setPwdNew] = useState('')
|
|
|
|
|
|
const [pwdNew2, setPwdNew2] = useState('')
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const selectableClubs = useMemo(
|
|
|
|
|
|
() => clubSelectOptions(user, clubs, isPlatformAdmin),
|
|
|
|
|
|
[user, clubs, isPlatformAdmin]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!clubOrgMode) return
|
|
|
|
|
|
if (selectedClubId == null || !managedClubIds.includes(selectedClubId)) {
|
|
|
|
|
|
setSelectedClubId(managedClubIds[0] ?? null)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [clubOrgMode, managedClubIds, selectedClubId])
|
|
|
|
|
|
|
|
|
|
|
|
const loadPlatform = useCallback(async () => {
|
|
|
|
|
|
const [u, c] = await Promise.all([api.listAdminUsers(), api.listClubs()])
|
|
|
|
|
|
setPlatformUsers(Array.isArray(u) ? u : [])
|
|
|
|
|
|
setClubs(Array.isArray(c) ? c : [])
|
|
|
|
|
|
const d = {}
|
|
|
|
|
|
for (const row of u || []) {
|
|
|
|
|
|
d[row.id] = { role: (row.role || 'user').toLowerCase() }
|
|
|
|
|
|
}
|
|
|
|
|
|
setPortalDraft(d)
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!canAccess) return
|
|
|
|
|
|
if (clubOrgMode) return
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
;(async () => {
|
|
|
|
|
|
setError('')
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
await loadPlatform()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!cancelled) setError(e.message || String(e))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!cancelled) setLoading(false)
|
2026-05-05 21:05:52 +02:00
|
|
|
|
}
|
2026-05-09 10:02:56 +02:00
|
|
|
|
})()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
2026-05-05 21:05:52 +02:00
|
|
|
|
}
|
2026-05-09 10:02:56 +02:00
|
|
|
|
}, [canAccess, clubOrgMode, loadPlatform])
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-09 10:02:56 +02:00
|
|
|
|
if (!canAccess || !clubOrgMode || !selectedClubId) return
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
;(async () => {
|
|
|
|
|
|
setError('')
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [c, m] = await Promise.all([
|
|
|
|
|
|
api.listClubs(),
|
|
|
|
|
|
api.listClubMembers(selectedClubId, { includeInactive: true }),
|
|
|
|
|
|
])
|
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
|
setClubs(Array.isArray(c) ? c : [])
|
|
|
|
|
|
setClubMembers(Array.isArray(m) ? m : [])
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!cancelled) setError(e.message || String(e))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!cancelled) setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
})()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [canAccess, clubOrgMode, selectedClubId])
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
const reloadClubMembers = useCallback(async () => {
|
|
|
|
|
|
if (!selectedClubId) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const m = await api.listClubMembers(selectedClubId, { includeInactive: true })
|
|
|
|
|
|
setClubMembers(Array.isArray(m) ? m : [])
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setClubMembers([])
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [selectedClubId])
|
|
|
|
|
|
|
|
|
|
|
|
if (!canAccess) return <Navigate to="/" replace />
|
|
|
|
|
|
|
|
|
|
|
|
const selectedClubLabel =
|
|
|
|
|
|
selectableClubs.find((c) => c.id === selectedClubId)?.name || 'Verein'
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
|
|
|
|
|
const savePortal = async (profileId) => {
|
|
|
|
|
|
const dr = portalDraft[profileId]
|
|
|
|
|
|
if (!dr) return
|
|
|
|
|
|
try {
|
2026-05-09 10:02:56 +02:00
|
|
|
|
await api.updateProfile(profileId, { role: dr.role })
|
|
|
|
|
|
await loadPlatform()
|
2026-05-05 21:05:52 +02:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const submitAssignClub = async () => {
|
|
|
|
|
|
if (!assignModal) return
|
|
|
|
|
|
const clubId = assignModal.clubId
|
|
|
|
|
|
const profileId = assignModal.profileId
|
|
|
|
|
|
if (!clubId || !assignRoles.length) {
|
|
|
|
|
|
alert('Verein und mindestens eine Rolle wählen.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.addClubMember(clubId, { profile_id: profileId, roles: assignRoles })
|
|
|
|
|
|
setAssignModal(null)
|
|
|
|
|
|
setAssignRoles(['trainer'])
|
2026-05-09 10:02:56 +02:00
|
|
|
|
await loadPlatform()
|
2026-05-05 21:05:52 +02:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const saveClubMembership = async () => {
|
|
|
|
|
|
if (!clubEditModal) return
|
|
|
|
|
|
const { clubId, profileId, roles, status } = clubEditModal
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.updateClubMember(clubId, profileId, { roles, status })
|
|
|
|
|
|
setClubEditModal(null)
|
2026-05-09 10:02:56 +02:00
|
|
|
|
if (clubOrgMode) await reloadClubMembers()
|
|
|
|
|
|
else await loadPlatform()
|
2026-05-05 21:05:52 +02:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const removeClubMembership = async () => {
|
|
|
|
|
|
if (!clubEditModal) return
|
|
|
|
|
|
if (!confirm('Mitgliedschaft in diesem Verein wirklich entfernen?')) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.removeClubMember(clubEditModal.clubId, clubEditModal.profileId)
|
|
|
|
|
|
setClubEditModal(null)
|
2026-05-09 10:02:56 +02:00
|
|
|
|
if (clubOrgMode) await reloadClubMembers()
|
|
|
|
|
|
else await loadPlatform()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const submitAddClubMember = async () => {
|
|
|
|
|
|
const raw = parseInt(String(newMemberProfileId).trim(), 10)
|
|
|
|
|
|
if (!Number.isFinite(raw) || raw < 1) {
|
|
|
|
|
|
alert('Gültige Profil-ID eingeben.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!selectedClubId) return
|
|
|
|
|
|
if (!newMemberRoles.length) {
|
|
|
|
|
|
alert('Mindestens eine Vereinsrolle.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.addClubMember(selectedClubId, { profile_id: raw, roles: newMemberRoles })
|
|
|
|
|
|
setAddMemberOpen(false)
|
|
|
|
|
|
setNewMemberProfileId('')
|
|
|
|
|
|
setNewMemberRoles(['trainer'])
|
|
|
|
|
|
await reloadClubMembers()
|
2026-05-05 21:05:52 +02:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 10:15:16 +02:00
|
|
|
|
const submitPasswordReset = async () => {
|
|
|
|
|
|
if (!pwdModal) return
|
|
|
|
|
|
if (pwdNew.length < 8) {
|
|
|
|
|
|
alert('Mindestens 8 Zeichen.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pwdNew !== pwdNew2) {
|
|
|
|
|
|
alert('Die beiden Passwörter stimmen nicht überein.')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.managementPasswordReset(pwdModal.profileId, pwdNew)
|
|
|
|
|
|
setPwdModal(null)
|
|
|
|
|
|
setPwdNew('')
|
|
|
|
|
|
setPwdNew2('')
|
|
|
|
|
|
alert('Neues Passwort wurde gesetzt.')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert(e.message || String(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 21:05:52 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="app-page">
|
2026-05-09 10:02:56 +02:00
|
|
|
|
<AdminPageNav clubOrgOnly={clubOrgMode} />
|
|
|
|
|
|
|
|
|
|
|
|
<h1 style={{ marginTop: 0 }}>Nutzer & Vereinsrollen</h1>
|
|
|
|
|
|
|
|
|
|
|
|
{clubOrgMode ? (
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55, marginBottom: '1.25rem' }}>
|
|
|
|
|
|
Du verwaltest nur Mitglieder des ausgewählten Vereins und deren <strong>Rollen in diesem Verein</strong>.
|
|
|
|
|
|
Portal-Rollen und andere Vereine sind hier nicht sichtbar.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
2026-05-09 10:15:16 +02:00
|
|
|
|
<>
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55, marginBottom: '0.75rem' }}>
|
|
|
|
|
|
Gesamtübersicht aller Konten und Vereinszuordnungen. <strong>Portal</strong>-Einstellungen steuern nur den
|
|
|
|
|
|
Zugang zur <strong>plattformweiten Administration</strong> (Kataloge, Hierarchie, globale Nutzerliste usw.);
|
|
|
|
|
|
Vereinsarbeit (Trainer, Vereinsadmin …) bleiben <strong>Vereinsrollen</strong>. Abonnement/Tier ist
|
|
|
|
|
|
derzeit nicht freigeschaltet.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="card"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '1.25rem',
|
|
|
|
|
|
padding: '0.85rem 1rem',
|
|
|
|
|
|
maxWidth: '52rem',
|
|
|
|
|
|
fontSize: '0.92rem',
|
|
|
|
|
|
lineHeight: 1.5,
|
|
|
|
|
|
color: 'var(--text2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong style={{ color: 'var(--text1)' }}>Die vier Portal-Zugriffsstufen:</strong>
|
|
|
|
|
|
<ul style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
<strong>Nutzer</strong> — Standardnutzer ohne Plattform-Admin-Bereiche.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
<strong>Portal-Trainer (Legacy)</strong> — ältere Kennzeichnung auf Profil-Ebene; organisatorisch ist
|
|
|
|
|
|
„Trainer“ in der Regel eine <strong>Vereinsrolle</strong>.
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
<strong>Portal-Administrator</strong> — Zugang zu allen geschützten <code>/admin</code>-Bereichen
|
|
|
|
|
|
(Außer: einige Funktionen nur Superadmin, z. B. bestimmte Medien-/Governance-Aktionen).
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li>
|
|
|
|
|
|
<strong>Super-Administrator</strong> — volle Plattform-Governance (u. a. offizielle Medien,
|
|
|
|
|
|
Superadmin-Rolle vergeben, harte Lifecycle-Aktionen an Medien).
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{clubOrgMode && managedClubIds.length > 1 ? (
|
|
|
|
|
|
<div className="form-row" style={{ maxWidth: '24rem', marginBottom: '1rem' }}>
|
|
|
|
|
|
<label className="form-label">Verein für Verwaltung</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
value={selectedClubId ?? ''}
|
|
|
|
|
|
onChange={(e) => setSelectedClubId(parseInt(e.target.value, 10))}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectableClubs.map((c) => (
|
|
|
|
|
|
<option key={c.id} value={c.id}>
|
|
|
|
|
|
{c.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<p style={{ color: 'var(--text2)' }}>Laden…</p>
|
|
|
|
|
|
) : error ? (
|
|
|
|
|
|
<div className="card" style={{ borderColor: 'var(--danger)', color: 'var(--danger)' }}>
|
|
|
|
|
|
{error}
|
|
|
|
|
|
</div>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
) : clubOrgMode ? (
|
2026-05-05 21:05:52 +02:00
|
|
|
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'center' }}>
|
|
|
|
|
|
<button type="button" className="btn btn-primary" onClick={() => setAddMemberOpen(true)}>
|
|
|
|
|
|
Mitglied hinzufügen (Profil-ID)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => reloadClubMembers()}>
|
|
|
|
|
|
Aktualisieren
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{!clubMembers.length ? (
|
|
|
|
|
|
<p className="muted">Keine Mitglieder in diesem Verein.</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
clubMembers.map((m) => (
|
|
|
|
|
|
<div key={m.membership_id} className="card">
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}>
|
2026-05-05 21:05:52 +02:00
|
|
|
|
<div>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
<strong style={{ fontSize: '1.05rem' }}>
|
|
|
|
|
|
{m.name || '—'} <span style={{ color: 'var(--text2)', fontWeight: 400 }}>#{m.profile_id}</span>
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<div style={{ fontSize: '0.875rem', color: 'var(--text2)' }}>{m.email || '—'}</div>
|
|
|
|
|
|
<div style={{ fontSize: '0.78rem', color: 'var(--text3)', marginTop: '0.35rem' }}>
|
|
|
|
|
|
Status: {m.status} · Verifiziert: {m.email_verified ? 'ja' : 'nein'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '0.82rem', marginTop: '0.35rem' }}>
|
|
|
|
|
|
Rollen: {(m.roles || []).join(', ') || '—'}
|
|
|
|
|
|
</div>
|
2026-05-05 21:05:52 +02:00
|
|
|
|
</div>
|
2026-05-09 10:15:16 +02:00
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem', alignItems: 'stretch' }}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setClubEditModal({
|
|
|
|
|
|
clubId: selectedClubId,
|
|
|
|
|
|
clubName: selectedClubLabel,
|
|
|
|
|
|
profileId: m.profile_id,
|
|
|
|
|
|
profileLabel: m.name || m.email,
|
|
|
|
|
|
roles: [...(m.roles || [])],
|
|
|
|
|
|
status: (m.status || 'active').toLowerCase(),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
Bearbeiten
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{m.profile_id !== user?.id ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setPwdModal({
|
|
|
|
|
|
profileId: m.profile_id,
|
|
|
|
|
|
label: m.name || m.email || `Profil #${m.profile_id}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
Passwort setzen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
2026-05-05 21:05:52 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
|
|
|
|
|
{platformUsers.map((row) => {
|
|
|
|
|
|
const portalRoleChoices = portalRoleSelectOptions(isSuperadminViewer, row.role)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={row.id} className="card">
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong style={{ fontSize: '1.05rem' }}>
|
|
|
|
|
|
{row.name || '—'} <span style={{ color: 'var(--text2)', fontWeight: 400 }}>#{row.id}</span>
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<div style={{ fontSize: '0.875rem', color: 'var(--text2)' }}>{row.email || '—'}</div>
|
|
|
|
|
|
<div style={{ fontSize: '0.78rem', color: 'var(--text3)', marginTop: '0.35rem' }}>
|
|
|
|
|
|
Verifiziert: {row.email_verified ? 'ja' : 'nein'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'flex-end' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="form-label" style={{ fontSize: '0.75rem' }}>
|
|
|
|
|
|
Portal-Zugriff
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
style={{ minWidth: '200px' }}
|
|
|
|
|
|
value={(portalDraft[row.id]?.role || row.role || 'user').toLowerCase()}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setPortalDraft((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[row.id]: { role: e.target.value },
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{portalRoleChoices.map((r) => (
|
|
|
|
|
|
<option key={r.value} value={r.value}>
|
|
|
|
|
|
{r.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-05-09 10:15:16 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
disabled={row.id === user?.id}
|
|
|
|
|
|
title={row.id === user?.id ? 'Eigenes Passwort unter Einstellungen' : undefined}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setPwdModal({
|
|
|
|
|
|
profileId: row.id,
|
|
|
|
|
|
label: row.name || row.email || `#${row.id}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
Passwort setzen
|
|
|
|
|
|
</button>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => savePortal(row.id)}>
|
|
|
|
|
|
Portal speichern
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
|
disabled={!clubs.length}
|
|
|
|
|
|
title={!clubs.length ? 'Zuerst einen Verein anlegen' : undefined}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (!clubs.length) return
|
|
|
|
|
|
setAssignRoles(['trainer'])
|
|
|
|
|
|
setAssignModal({
|
|
|
|
|
|
profileId: row.id,
|
|
|
|
|
|
profileLabel: row.name || row.email || `#${row.id}`,
|
|
|
|
|
|
clubId: clubs[0]?.id ?? '',
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Verein zuweisen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-05 21:05:52 +02:00
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
<div style={{ marginTop: '1rem', paddingTop: '0.75rem', borderTop: '1px solid var(--border)' }}>
|
|
|
|
|
|
<strong style={{ fontSize: '0.85rem' }}>Vereinsmitgliedschaften</strong>
|
|
|
|
|
|
{!row.clubs?.length ? (
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', margin: '0.35rem 0 0' }}>
|
|
|
|
|
|
Keine Zuordnung.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ul style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem', fontSize: '0.9rem' }}>
|
|
|
|
|
|
{(row.clubs || []).map((c) => (
|
|
|
|
|
|
<li key={c.id} style={{ marginBottom: '0.35rem' }}>
|
|
|
|
|
|
<strong>{c.name}</strong>
|
|
|
|
|
|
{c.abbreviation ? ` (${c.abbreviation})` : ''} —{' '}
|
|
|
|
|
|
{(c.roles || []).join(', ') || '—'}
|
|
|
|
|
|
{c.membership_status === 'inactive' ? (
|
|
|
|
|
|
<span style={{ color: 'var(--text3)', fontSize: '0.8rem' }}> (inaktiv)</span>
|
|
|
|
|
|
) : null}{' '}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginLeft: '0.35rem',
|
|
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
|
|
padding: '0.12rem 0.45rem',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setClubEditModal({
|
|
|
|
|
|
clubId: c.id,
|
|
|
|
|
|
clubName: c.name,
|
|
|
|
|
|
profileId: row.id,
|
|
|
|
|
|
profileLabel: row.name || row.email,
|
|
|
|
|
|
roles: [...(c.roles || [])],
|
|
|
|
|
|
status: (c.membership_status || 'active').toLowerCase(),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
bearbeiten
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-05-05 21:05:52 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{assignModal && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
background: 'rgba(0,0,0,0.5)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
zIndex: 1200,
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
|
maxWidth: '440px',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 style={{ marginTop: 0 }}>Verein zuweisen</h2>
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>{assignModal.profileLabel}</p>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Verein</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
value={assignModal.clubId === '' ? '' : String(assignModal.clubId)}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setAssignModal((prev) =>
|
|
|
|
|
|
prev ? { ...prev, clubId: e.target.value ? parseInt(e.target.value, 10) : '' } : prev
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{clubs.map((c) => (
|
|
|
|
|
|
<option key={c.id} value={String(c.id)}>
|
|
|
|
|
|
{c.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<span className="form-label">Rollen im Verein</span>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
|
|
|
|
{CLUB_ROLE_OPTIONS.map((opt) => (
|
|
|
|
|
|
<label key={opt.code} style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={assignRoles.includes(opt.code)}
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setAssignRoles((prev) => {
|
|
|
|
|
|
const s = new Set(prev)
|
|
|
|
|
|
if (s.has(opt.code)) s.delete(opt.code)
|
|
|
|
|
|
else s.add(opt.code)
|
|
|
|
|
|
const out = Array.from(s)
|
|
|
|
|
|
return out.length ? out : ['trainer']
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
|
|
|
|
|
<button type="button" className="btn btn-primary" style={{ flex: 1 }} onClick={submitAssignClub}>
|
|
|
|
|
|
Zuweisen
|
|
|
|
|
|
</button>
|
2026-05-09 10:02:56 +02:00
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => setAssignModal(null)}>
|
2026-05-05 21:05:52 +02:00
|
|
|
|
Abbrechen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
|
{addMemberOpen && clubOrgMode ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
background: 'rgba(0,0,0,0.5)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
zIndex: 1200,
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
|
maxWidth: '440px',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 style={{ marginTop: 0 }}>Mitglied hinzufügen</h2>
|
|
|
|
|
|
<p className="muted" style={{ fontSize: '0.9rem' }}>
|
|
|
|
|
|
Verein: <strong>{selectedClubLabel}</strong>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Profil-ID</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
|
value={newMemberProfileId}
|
|
|
|
|
|
onChange={(e) => setNewMemberProfileId(e.target.value)}
|
|
|
|
|
|
placeholder="z. B. 42"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<span className="form-label">Rollen</span>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
|
|
|
|
{CLUB_ROLE_OPTIONS.map((opt) => (
|
|
|
|
|
|
<label key={opt.code} style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={newMemberRoles.includes(opt.code)}
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setNewMemberRoles((prev) => {
|
|
|
|
|
|
const s = new Set(prev)
|
|
|
|
|
|
if (s.has(opt.code)) s.delete(opt.code)
|
|
|
|
|
|
else s.add(opt.code)
|
|
|
|
|
|
const out = Array.from(s)
|
|
|
|
|
|
return out.length ? out : ['trainer']
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
|
|
|
|
|
<button type="button" className="btn btn-primary" style={{ flex: 1 }} onClick={submitAddClubMember}>
|
|
|
|
|
|
Hinzufügen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => setAddMemberOpen(false)}>
|
|
|
|
|
|
Abbrechen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
2026-05-05 21:05:52 +02:00
|
|
|
|
{clubEditModal && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
background: 'rgba(0,0,0,0.5)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
zIndex: 1200,
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
|
maxWidth: '440px',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 style={{ marginTop: 0 }}>Vereinsmitgliedschaft</h2>
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>
|
|
|
|
|
|
{clubEditModal.profileLabel} → {clubEditModal.clubName}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Status</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
value={clubEditModal.status}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setClubEditModal((prev) => (prev ? { ...prev, status: e.target.value } : prev))
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="active">aktiv</option>
|
|
|
|
|
|
<option value="inactive">inaktiv</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<span className="form-label">Rollen</span>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
|
|
|
|
{CLUB_ROLE_OPTIONS.map((opt) => (
|
|
|
|
|
|
<label key={opt.code} style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={clubEditModal.roles.includes(opt.code)}
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setClubEditModal((prev) => {
|
|
|
|
|
|
if (!prev) return prev
|
|
|
|
|
|
const s = new Set(prev.roles)
|
|
|
|
|
|
if (s.has(opt.code)) s.delete(opt.code)
|
|
|
|
|
|
else s.add(opt.code)
|
|
|
|
|
|
let roles = Array.from(s)
|
|
|
|
|
|
if (!roles.length) roles = ['trainer']
|
|
|
|
|
|
return { ...prev, roles }
|
|
|
|
|
|
})
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginTop: '1rem' }}>
|
|
|
|
|
|
<button type="button" className="btn btn-primary" onClick={saveClubMembership}>
|
|
|
|
|
|
Speichern
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn"
|
|
|
|
|
|
style={{ background: 'var(--danger)', color: '#fff', border: 'none' }}
|
|
|
|
|
|
onClick={removeClubMembership}
|
|
|
|
|
|
>
|
|
|
|
|
|
Aus Verein entfernen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => setClubEditModal(null)}>
|
|
|
|
|
|
Schließen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-05-09 10:15:16 +02:00
|
|
|
|
|
|
|
|
|
|
{pwdModal ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
|
inset: 0,
|
|
|
|
|
|
background: 'rgba(0,0,0,0.5)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
zIndex: 1250,
|
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--surface)',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
|
maxWidth: '440px',
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<h2 style={{ marginTop: 0 }}>Login-Passwort setzen</h2>
|
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>{pwdModal.label}</p>
|
|
|
|
|
|
<p className="muted" style={{ fontSize: '0.82rem', marginBottom: '0.75rem' }}>
|
|
|
|
|
|
Mindestens 8 Zeichen. Das Passwort liegt dem Nutzer nicht automatisch vor — gib es sicher weiter.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Neues Passwort</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
autoComplete="new-password"
|
|
|
|
|
|
value={pwdNew}
|
|
|
|
|
|
onChange={(e) => setPwdNew(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Wiederholen</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
autoComplete="new-password"
|
|
|
|
|
|
value={pwdNew2}
|
|
|
|
|
|
onChange={(e) => setPwdNew2(e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
|
|
|
|
|
<button type="button" className="btn btn-primary" style={{ flex: 1 }} onClick={submitPasswordReset}>
|
|
|
|
|
|
Übernehmen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setPwdModal(null)
|
|
|
|
|
|
setPwdNew('')
|
|
|
|
|
|
setPwdNew2('')
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Abbrechen
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-05-05 21:05:52 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|