223 lines
7.6 KiB
React
223 lines
7.6 KiB
React
|
|
import React, { useState, useEffect, useCallback } from 'react'
|
||
|
|
import { Link } from 'react-router-dom'
|
||
|
|
import api from '../utils/api'
|
||
|
|
import { notifyOrgInboxChanged, useOrgInbox } from '../context/OrgInboxContext'
|
||
|
|
|
||
|
|
const CLUB_ROLE_OPTIONS = [
|
||
|
|
{ code: 'club_admin', label: 'Vereinsadmin' },
|
||
|
|
{ code: 'trainer', label: 'Trainer' },
|
||
|
|
{ code: 'division_lead', label: 'Spartenleitung' },
|
||
|
|
{ code: 'content_editor', label: 'Inhalte bearbeiten' },
|
||
|
|
]
|
||
|
|
|
||
|
|
function formatWhen(iso) {
|
||
|
|
if (!iso) return ''
|
||
|
|
const s = String(iso)
|
||
|
|
const d = s.includes('T') ? s.split('T')[0] : s.slice(0, 10)
|
||
|
|
const t = s.includes('T') ? s.split('T')[1] : ''
|
||
|
|
const time = t ? t.slice(0, 5) : ''
|
||
|
|
return time ? `${d} · ${time}` : d
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function InboxPage() {
|
||
|
|
const { canAccessOrgInbox, refreshOrgInbox, inboxJoinRequests } = useOrgInbox()
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
const [acceptModal, setAcceptModal] = useState(null)
|
||
|
|
|
||
|
|
const load = useCallback(async () => {
|
||
|
|
if (!canAccessOrgInbox) {
|
||
|
|
setLoading(false)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
setLoading(true)
|
||
|
|
try {
|
||
|
|
await refreshOrgInbox()
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}, [canAccessOrgInbox, refreshOrgInbox])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
load()
|
||
|
|
}, [load])
|
||
|
|
|
||
|
|
if (!canAccessOrgInbox) {
|
||
|
|
return (
|
||
|
|
<div className="app-page">
|
||
|
|
<h1 className="page-title">Posteingang</h1>
|
||
|
|
<p className="muted">Kein Zugriff. Nur Plattform-Admins und Vereinsadmins sehen den Posteingang.</p>
|
||
|
|
<p>
|
||
|
|
<Link to="/">Zur Übersicht</Link>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="app-page inbox-page">
|
||
|
|
<div className="inbox-page__header">
|
||
|
|
<div>
|
||
|
|
<h1 className="page-title" style={{ marginBottom: '6px' }}>
|
||
|
|
Posteingang
|
||
|
|
</h1>
|
||
|
|
<p className="muted" style={{ marginTop: 0 }}>
|
||
|
|
Offene Beitrittsanträge zu Vereinen, für die du zuständig bist.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<button type="button" className="btn btn-secondary" onClick={() => load()} disabled={loading}>
|
||
|
|
Aktualisieren
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{loading ? (
|
||
|
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||
|
|
<div className="spinner" />
|
||
|
|
</div>
|
||
|
|
) : inboxJoinRequests.length === 0 ? (
|
||
|
|
<div className="card" style={{ padding: '1.25rem' }}>
|
||
|
|
<p style={{ margin: 0 }} className="muted">
|
||
|
|
Keine offenen Beitrittsanträge.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="inbox-page__list">
|
||
|
|
{inboxJoinRequests.map((req) => (
|
||
|
|
<div key={`${req.club_id}-${req.id}`} className="card inbox-request-card">
|
||
|
|
<div className="inbox-request-card__main">
|
||
|
|
<div className="inbox-request-card__club">
|
||
|
|
{req.club_name || 'Verein'}
|
||
|
|
{req.club_abbreviation ? (
|
||
|
|
<span className="muted" style={{ marginLeft: '0.35rem' }}>
|
||
|
|
({req.club_abbreviation})
|
||
|
|
</span>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
<strong className="inbox-request-card__applicant">
|
||
|
|
{req.applicant_name || req.applicant_email || 'Bewerber/in'}
|
||
|
|
</strong>
|
||
|
|
<div className="muted inbox-request-card__meta">
|
||
|
|
{req.applicant_email} · Profil #{req.profile_id} · {formatWhen(req.created_at)}
|
||
|
|
</div>
|
||
|
|
{req.message ? <p className="inbox-request-card__message">{req.message}</p> : null}
|
||
|
|
</div>
|
||
|
|
<div className="inbox-request-card__actions">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="btn btn-primary"
|
||
|
|
onClick={() =>
|
||
|
|
setAcceptModal({
|
||
|
|
id: req.id,
|
||
|
|
club_id: req.club_id,
|
||
|
|
label: req.applicant_name || req.applicant_email,
|
||
|
|
roles: ['trainer'],
|
||
|
|
})
|
||
|
|
}
|
||
|
|
>
|
||
|
|
Annehmen
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="btn btn-secondary"
|
||
|
|
onClick={async () => {
|
||
|
|
if (!confirm('Antrag ablehnen?')) return
|
||
|
|
try {
|
||
|
|
await api.rejectClubJoinRequest(req.club_id, req.id)
|
||
|
|
notifyOrgInboxChanged()
|
||
|
|
await load()
|
||
|
|
} catch (err) {
|
||
|
|
alert(err.message || String(err))
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Ablehnen
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{acceptModal && (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: 'fixed',
|
||
|
|
top: 0,
|
||
|
|
left: 0,
|
||
|
|
right: 0,
|
||
|
|
bottom: 0,
|
||
|
|
background: 'rgba(0,0,0,0.5)',
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
zIndex: 1100,
|
||
|
|
padding: '1rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
background: 'var(--surface)',
|
||
|
|
borderRadius: '12px',
|
||
|
|
padding: '1.5rem',
|
||
|
|
maxWidth: '480px',
|
||
|
|
width: '100%',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Antrag annehmen</h2>
|
||
|
|
<p style={{ color: 'var(--text2)' }}>{acceptModal.label}</p>
|
||
|
|
<div className="form-row">
|
||
|
|
<span className="form-label">Rollen bei Aufnahme</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={acceptModal.roles.includes(opt.code)}
|
||
|
|
onChange={() => {
|
||
|
|
setAcceptModal((prev) => {
|
||
|
|
if (!prev) return prev
|
||
|
|
const set = new Set(prev.roles)
|
||
|
|
if (set.has(opt.code)) set.delete(opt.code)
|
||
|
|
else set.add(opt.code)
|
||
|
|
const roles = Array.from(set)
|
||
|
|
return { ...prev, roles: roles.length ? roles : ['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={async () => {
|
||
|
|
try {
|
||
|
|
await api.acceptClubJoinRequest(
|
||
|
|
acceptModal.club_id,
|
||
|
|
acceptModal.id,
|
||
|
|
acceptModal.roles.length ? acceptModal.roles : ['trainer']
|
||
|
|
)
|
||
|
|
setAcceptModal(null)
|
||
|
|
notifyOrgInboxChanged()
|
||
|
|
await load()
|
||
|
|
} catch (err) {
|
||
|
|
alert(err.message || String(err))
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Aufnehmen
|
||
|
|
</button>
|
||
|
|
<button type="button" className="btn btn-secondary" onClick={() => setAcceptModal(null)}>
|
||
|
|
Abbrechen
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|