shinkan-jinkendo/frontend/src/pages/AdminClubCreationRequestsPage.jsx
Lars 8ee8f52e0f
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
Add Club Creation Request Management Features
- 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.
2026-06-07 07:09:39 +02:00

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>
)
}