Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Failing after 1m14s
- Introduced endpoints for managing club creation requests, including fetching, creating, and withdrawing requests. - Updated the onboarding page to allow users to submit new club creation requests and view their existing requests. - Enhanced the admin interface with navigation and routing for club creation requests management. - Incremented version to 0.8.191 to reflect these new features and updates in the application.
170 lines
5.3 KiB
JavaScript
170 lines
5.3 KiB
JavaScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { Navigate } from 'react-router-dom'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import api from '../utils/api'
|
|
import AdminPageNav from '../components/AdminPageNav'
|
|
|
|
function formatDate(value) {
|
|
if (!value) return '—'
|
|
try {
|
|
return new Date(value).toLocaleString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
} catch {
|
|
return String(value)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Superadmin: offene Anträge auf Vereinsgründung freigeben oder ablehnen.
|
|
*/
|
|
export default function AdminClubCreationRequestsPage() {
|
|
const { user } = useAuth()
|
|
const isSuperadmin = user?.role === 'superadmin'
|
|
|
|
const [requests, setRequests] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [busyId, setBusyId] = useState(null)
|
|
|
|
const load = useCallback(async () => {
|
|
const rows = await api.listAdminClubCreationRequests()
|
|
setRequests(Array.isArray(rows) ? rows : [])
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!isSuperadmin) return
|
|
let cancelled = false
|
|
;(async () => {
|
|
setError('')
|
|
setLoading(true)
|
|
try {
|
|
await load()
|
|
} catch (e) {
|
|
if (!cancelled) setError(e.message || String(e))
|
|
} finally {
|
|
if (!cancelled) setLoading(false)
|
|
}
|
|
})()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [isSuperadmin, load])
|
|
|
|
if (!isSuperadmin) return <Navigate to="/" replace />
|
|
|
|
const handleApprove = async (id) => {
|
|
if (!confirm('Verein anlegen und Antragsteller als Hauptverwalter eintragen?')) return
|
|
setBusyId(id)
|
|
setError('')
|
|
try {
|
|
await api.approveClubCreationRequest(id)
|
|
await load()
|
|
} catch (e) {
|
|
setError(e.message || String(e))
|
|
} finally {
|
|
setBusyId(null)
|
|
}
|
|
}
|
|
|
|
const handleReject = async (id) => {
|
|
if (!confirm('Gründungsantrag wirklich ablehnen?')) return
|
|
setBusyId(id)
|
|
setError('')
|
|
try {
|
|
await api.rejectClubCreationRequest(id)
|
|
await load()
|
|
} catch (e) {
|
|
setError(e.message || String(e))
|
|
} finally {
|
|
setBusyId(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="page-padding app-page">
|
|
<AdminPageNav />
|
|
<h1 style={{ marginTop: '1rem', fontSize: '1.35rem' }}>Vereinsgründungen</h1>
|
|
<p style={{ color: 'var(--text2)', maxWidth: '42rem', lineHeight: 1.5 }}>
|
|
Offene Anträge von verifizierten Nutzern ohne Vereinsmitgliedschaft. Bei Freigabe wird ein
|
|
neuer Verein mit Free-Abo angelegt; der Antragsteller wird Vereinsadmin und Trainer.
|
|
</p>
|
|
|
|
{error ? (
|
|
<p role="alert" style={{ color: 'var(--danger)' }}>
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
|
|
{loading ? (
|
|
<p className="spinner" style={{ marginTop: '1rem' }}>
|
|
Laden…
|
|
</p>
|
|
) : requests.length === 0 ? (
|
|
<div className="card" style={{ marginTop: '1rem' }}>
|
|
<p style={{ margin: 0, color: 'var(--text2)' }}>Keine offenen Gründungsanträge.</p>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginTop: '1rem' }}>
|
|
{requests.map((r) => (
|
|
<div key={r.id} className="card">
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem 1rem', marginBottom: '0.5rem' }}>
|
|
<strong>{r.proposed_name}</strong>
|
|
{r.proposed_abbreviation ? (
|
|
<span style={{ color: 'var(--text2)' }}>({r.proposed_abbreviation})</span>
|
|
) : null}
|
|
</div>
|
|
<p style={{ margin: '0 0 0.35rem', fontSize: '0.9rem', color: 'var(--text2)' }}>
|
|
Antragsteller: {r.applicant_name || '—'}{' '}
|
|
{r.applicant_email ? `· ${r.applicant_email}` : ''}
|
|
</p>
|
|
<p style={{ margin: '0 0 0.35rem', fontSize: '0.85rem', color: 'var(--text3)' }}>
|
|
Eingereicht: {formatDate(r.created_at)}
|
|
</p>
|
|
{r.proposed_description ? (
|
|
<p style={{ margin: '0.5rem 0', fontSize: '0.9rem', whiteSpace: 'pre-wrap' }}>
|
|
{r.proposed_description}
|
|
</p>
|
|
) : null}
|
|
{r.message ? (
|
|
<p
|
|
style={{
|
|
margin: '0.5rem 0 0',
|
|
fontSize: '0.875rem',
|
|
color: 'var(--text2)',
|
|
fontStyle: 'italic',
|
|
}}
|
|
>
|
|
Nachricht: {r.message}
|
|
</p>
|
|
) : null}
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.85rem', flexWrap: 'wrap' }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
disabled={busyId === r.id}
|
|
onClick={() => handleApprove(r.id)}
|
|
>
|
|
{busyId === r.id ? '…' : 'Freigeben'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary"
|
|
disabled={busyId === r.id}
|
|
onClick={() => handleReject(r.id)}
|
|
>
|
|
Ablehnen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|