Refactor Org Inbox Context and Enhance Club Creation Management
Some checks failed
Deploy Development / deploy (push) Successful in 47s
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 1m21s
Some checks failed
Deploy Development / deploy (push) Successful in 47s
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 1m21s
- Updated the OrgInboxContext to include handling for club creation requests, allowing for better management of inbox items. - Refactored components to utilize the new `canShowInboxNav` and `canAccessClubCreationInbox` flags for improved access control. - Enhanced the InboxPage to display club creation requests with appropriate actions for approval and rejection. - Updated the DashboardOrgInboxWidget to show both club creation and join requests, improving the user interface for managing inbox items.
This commit is contained in:
parent
8ee8f52e0f
commit
37785135b1
|
|
@ -88,9 +88,9 @@ function AppRouteFallback() {
|
|||
|
||||
// Bottom Navigation (Mobile)
|
||||
function Nav({ showAdminNav, onboardingOnly }) {
|
||||
const { canAccessOrgInbox, inboxCount } = useOrgInbox()
|
||||
const { canShowInboxNav, inboxCount } = useOrgInbox()
|
||||
const items = getMainNavItems(showAdminNav, {
|
||||
showInbox: canAccessOrgInbox,
|
||||
showInbox: canShowInboxNav,
|
||||
onboardingOnly,
|
||||
})
|
||||
const loc = useLocation()
|
||||
|
|
|
|||
|
|
@ -6,11 +6,30 @@ import { useOrgInbox } from '../context/OrgInboxContext'
|
|||
* Desktop-Dashboard: Hinweis auf offene Beitrittsanträge (nur ab 1024px sichtbar via CSS).
|
||||
*/
|
||||
export default function DashboardOrgInboxWidget() {
|
||||
const { canAccessOrgInbox, inboxJoinRequests, inboxCount } = useOrgInbox()
|
||||
const {
|
||||
canShowInboxNav,
|
||||
inboxJoinRequests,
|
||||
inboxClubCreationRequests,
|
||||
clubCreationRequestCount,
|
||||
inboxCount,
|
||||
} = useOrgInbox()
|
||||
|
||||
if (!canAccessOrgInbox) return null
|
||||
if (!canShowInboxNav) return null
|
||||
|
||||
const preview = (inboxJoinRequests || []).slice(0, 5)
|
||||
const preview = [
|
||||
...(inboxClubCreationRequests || []).map((req) => ({
|
||||
key: `creation-${req.id}`,
|
||||
club: req.proposed_name || 'Neuer Verein',
|
||||
applicant: req.applicant_name || req.applicant_email || 'Antragsteller/in',
|
||||
kind: 'creation',
|
||||
})),
|
||||
...(inboxJoinRequests || []).map((req) => ({
|
||||
key: `${req.club_id}-${req.id}`,
|
||||
club: req.club_name || 'Verein',
|
||||
applicant: req.applicant_name || req.applicant_email || 'Bewerber/in',
|
||||
kind: 'join',
|
||||
})),
|
||||
].slice(0, 5)
|
||||
|
||||
return (
|
||||
<section
|
||||
|
|
@ -31,17 +50,27 @@ export default function DashboardOrgInboxWidget() {
|
|||
</div>
|
||||
<p className="muted dashboard-org-inbox-widget__lead">
|
||||
{inboxCount === 0
|
||||
? 'Keine offenen Beitrittsanträge.'
|
||||
: `${inboxCount} offene Beitrittsantrag${inboxCount === 1 ? '' : 'e'}.`}
|
||||
? 'Keine offenen Anträge.'
|
||||
: [
|
||||
clubCreationRequestCount > 0
|
||||
? `${clubCreationRequestCount} Gründungsantrag${clubCreationRequestCount === 1 ? '' : 'e'}`
|
||||
: null,
|
||||
(inboxJoinRequests || []).length > 0
|
||||
? `${(inboxJoinRequests || []).length} Beitrittsantrag${(inboxJoinRequests || []).length === 1 ? '' : 'e'}`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
{preview.length > 0 ? (
|
||||
<ul className="dashboard-org-inbox-widget__list">
|
||||
{preview.map((req) => (
|
||||
<li key={`${req.club_id}-${req.id}`} className="dashboard-org-inbox-widget__item">
|
||||
<span className="dashboard-org-inbox-widget__club">{req.club_name || 'Verein'}</span>
|
||||
<span className="dashboard-org-inbox-widget__applicant">
|
||||
{req.applicant_name || req.applicant_email || 'Bewerber/in'}
|
||||
<li key={req.key} className="dashboard-org-inbox-widget__item">
|
||||
<span className="dashboard-org-inbox-widget__club">
|
||||
{req.kind === 'creation' ? 'Gründung: ' : ''}
|
||||
{req.club}
|
||||
</span>
|
||||
<span className="dashboard-org-inbox-widget__applicant">{req.applicant}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ export default function DesktopSidebar({
|
|||
onLogout
|
||||
}) {
|
||||
const loc = useLocation()
|
||||
const { canAccessOrgInbox, inboxCount } = useOrgInbox()
|
||||
const { canShowInboxNav, inboxCount } = useOrgInbox()
|
||||
const items = getMainNavItems(showAdminNav, {
|
||||
showInbox: canAccessOrgInbox,
|
||||
showInbox: canShowInboxNav,
|
||||
onboardingOnly,
|
||||
})
|
||||
const tier = user?.tier || ''
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ export function canAccessOrgInbox(user) {
|
|||
return activeClubMemberships(user.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
||||
}
|
||||
|
||||
/** Gründungsanträge freigeben — aktuell nur Superadmin (platform.club_creation.approve). */
|
||||
export function canAccessClubCreationInbox(user) {
|
||||
return user?.role === 'superadmin'
|
||||
}
|
||||
|
||||
function canSeeContentReports(user) {
|
||||
if (user?.role === 'admin' || user?.role === 'superadmin') return true
|
||||
return activeClubMemberships(user?.clubs || []).some((c) => (c.roles || []).includes('club_admin'))
|
||||
|
|
@ -28,8 +33,13 @@ export function notifyOrgInboxChanged() {
|
|||
}
|
||||
|
||||
/** Eine konsistente Ladepfad-Logik für Join-Requests + Content-Reports (ein Codepfad für Mount + refresh). */
|
||||
async function fetchOrgInboxSnapshot(canAccess, canAccessReports) {
|
||||
const out = { items: [], contentReports: [], contentReportsError: null }
|
||||
async function fetchOrgInboxSnapshot(canAccess, canAccessReports, canAccessClubCreation) {
|
||||
const out = {
|
||||
items: [],
|
||||
clubCreationRequests: [],
|
||||
contentReports: [],
|
||||
contentReportsError: null,
|
||||
}
|
||||
if (canAccess) {
|
||||
try {
|
||||
const data = await api.getInboxJoinRequests()
|
||||
|
|
@ -38,6 +48,14 @@ async function fetchOrgInboxSnapshot(canAccess, canAccessReports) {
|
|||
out.items = []
|
||||
}
|
||||
}
|
||||
if (canAccessClubCreation) {
|
||||
try {
|
||||
const data = await api.listAdminClubCreationRequests()
|
||||
out.clubCreationRequests = Array.isArray(data) ? data : []
|
||||
} catch {
|
||||
out.clubCreationRequests = []
|
||||
}
|
||||
}
|
||||
if (canAccessReports) {
|
||||
try {
|
||||
const data = await api.getInboxContentReports()
|
||||
|
|
@ -52,27 +70,33 @@ async function fetchOrgInboxSnapshot(canAccess, canAccessReports) {
|
|||
|
||||
export function OrgInboxProvider({ user, children }) {
|
||||
const [items, setItems] = useState([])
|
||||
const [clubCreationRequests, setClubCreationRequests] = useState([])
|
||||
const [contentReports, setContentReports] = useState([])
|
||||
const [contentReportsError, setContentReportsError] = useState(null)
|
||||
const canAccess = useMemo(() => canAccessOrgInbox(user), [user])
|
||||
const canAccessReports = useMemo(() => canSeeContentReports(user), [user])
|
||||
const canAccessClubCreation = useMemo(() => canAccessClubCreationInbox(user), [user])
|
||||
const hasInboxAccess = canAccess || canAccessReports || canAccessClubCreation
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!canAccess && !canAccessReports) {
|
||||
if (!hasInboxAccess) {
|
||||
setItems([])
|
||||
setClubCreationRequests([])
|
||||
setContentReports([])
|
||||
setContentReportsError(null)
|
||||
return
|
||||
}
|
||||
const snap = await fetchOrgInboxSnapshot(canAccess, canAccessReports)
|
||||
const snap = await fetchOrgInboxSnapshot(canAccess, canAccessReports, canAccessClubCreation)
|
||||
setItems(snap.items)
|
||||
setClubCreationRequests(snap.clubCreationRequests)
|
||||
setContentReports(snap.contentReports)
|
||||
setContentReportsError(canAccessReports ? snap.contentReportsError : null)
|
||||
}, [canAccess, canAccessReports])
|
||||
}, [hasInboxAccess, canAccess, canAccessReports, canAccessClubCreation])
|
||||
|
||||
useEffect(() => {
|
||||
if (!canAccess && !canAccessReports) {
|
||||
if (!hasInboxAccess) {
|
||||
setItems([])
|
||||
setClubCreationRequests([])
|
||||
setContentReports([])
|
||||
setContentReportsError(null)
|
||||
return undefined
|
||||
|
|
@ -82,9 +106,10 @@ export function OrgInboxProvider({ user, children }) {
|
|||
let timeoutId = null
|
||||
|
||||
const load = async () => {
|
||||
const snap = await fetchOrgInboxSnapshot(canAccess, canAccessReports)
|
||||
const snap = await fetchOrgInboxSnapshot(canAccess, canAccessReports, canAccessClubCreation)
|
||||
if (cancelled) return
|
||||
setItems(snap.items)
|
||||
setClubCreationRequests(snap.clubCreationRequests)
|
||||
setContentReports(snap.contentReports)
|
||||
setContentReportsError(canAccessReports ? snap.contentReportsError : null)
|
||||
}
|
||||
|
|
@ -116,7 +141,7 @@ export function OrgInboxProvider({ user, children }) {
|
|||
}
|
||||
if (timeoutId != null) window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [canAccess, canAccessReports, user?.id])
|
||||
}, [hasInboxAccess, canAccess, canAccessReports, canAccessClubCreation, user?.id])
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => { refresh() }
|
||||
|
|
@ -124,21 +149,42 @@ export function OrgInboxProvider({ user, children }) {
|
|||
return () => window.removeEventListener('shinkan:inbox-changed', onChange)
|
||||
}, [refresh])
|
||||
|
||||
const clubCreationCount = clubCreationRequests.length
|
||||
const joinCount = items.length
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
inboxJoinRequests: items,
|
||||
inboxCount: items.length,
|
||||
inboxClubCreationRequests: clubCreationRequests,
|
||||
clubCreationRequestCount: clubCreationCount,
|
||||
inboxCount: joinCount + clubCreationCount,
|
||||
contentReports,
|
||||
contentReportCount: contentReports.filter((r) => r.status === 'submitted').length,
|
||||
contentReportsError,
|
||||
refreshOrgInbox: refresh,
|
||||
canAccessOrgInbox: canAccess,
|
||||
canAccessContentReports: canAccessReports,
|
||||
canAccessClubCreationInbox: canAccessClubCreation,
|
||||
canShowInboxNav: hasInboxAccess,
|
||||
isSuperadmin: user?.role === 'superadmin',
|
||||
isPlatformAdmin: user?.role === 'admin' || user?.role === 'superadmin',
|
||||
isClubAdmin: activeClubMemberships(user?.clubs || []).some((c) => (c.roles || []).includes('club_admin')),
|
||||
}),
|
||||
[items, contentReports, contentReportsError, refresh, canAccess, canAccessReports, user?.role, user?.clubs]
|
||||
[
|
||||
items,
|
||||
clubCreationRequests,
|
||||
clubCreationCount,
|
||||
joinCount,
|
||||
contentReports,
|
||||
contentReportsError,
|
||||
refresh,
|
||||
canAccess,
|
||||
canAccessReports,
|
||||
canAccessClubCreation,
|
||||
hasInboxAccess,
|
||||
user?.role,
|
||||
user?.clubs,
|
||||
]
|
||||
)
|
||||
|
||||
return <OrgInboxContext.Provider value={value}>{children}</OrgInboxContext.Provider>
|
||||
|
|
|
|||
|
|
@ -324,11 +324,14 @@ export default function InboxPage() {
|
|||
const {
|
||||
canAccessOrgInbox,
|
||||
canAccessContentReports,
|
||||
canAccessClubCreationInbox,
|
||||
canShowInboxNav,
|
||||
isSuperadmin,
|
||||
isPlatformAdmin,
|
||||
isClubAdmin,
|
||||
refreshOrgInbox,
|
||||
inboxJoinRequests,
|
||||
inboxClubCreationRequests,
|
||||
contentReports,
|
||||
contentReportCount,
|
||||
contentReportsError,
|
||||
|
|
@ -339,7 +342,7 @@ export default function InboxPage() {
|
|||
const [showArchive, setShowArchive] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!canAccessOrgInbox && !canAccessContentReports) {
|
||||
if (!canShowInboxNav) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -349,13 +352,13 @@ export default function InboxPage() {
|
|||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [canAccessOrgInbox, canAccessContentReports, refreshOrgInbox])
|
||||
}, [canShowInboxNav, refreshOrgInbox])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
if (!canAccessOrgInbox && !canAccessContentReports) {
|
||||
if (!canShowInboxNav) {
|
||||
return (
|
||||
<div className="app-page">
|
||||
<h1 className="page-title">Posteingang</h1>
|
||||
|
|
@ -375,7 +378,7 @@ export default function InboxPage() {
|
|||
Posteingang
|
||||
</h1>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
Beitrittsanträge und Inhaltsmeldungen für deine Zuständigkeitsbereiche.
|
||||
Beitrittsanträge, Vereinsgründungen und Inhaltsmeldungen für deine Zuständigkeitsbereiche.
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => load()} disabled={loading}>
|
||||
|
|
@ -389,7 +392,107 @@ export default function InboxPage() {
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Abschnitt 1: Beitrittsanträge */}
|
||||
{/* Abschnitt: Vereinsgründungen (nur Superadmin) */}
|
||||
{canAccessClubCreationInbox && (
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text2)' }}>
|
||||
Vereinsgründungen
|
||||
{inboxClubCreationRequests.length > 0 && (
|
||||
<span
|
||||
style={{
|
||||
background: 'var(--accent)',
|
||||
color: '#fff',
|
||||
borderRadius: '12px',
|
||||
padding: '1px 8px',
|
||||
fontSize: '0.75rem',
|
||||
marginLeft: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{inboxClubCreationRequests.length}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{inboxClubCreationRequests.length === 0 ? (
|
||||
<div className="card" style={{ padding: '1.25rem' }}>
|
||||
<p style={{ margin: 0 }} className="muted">Keine offenen Gründungsanträge.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inbox-page__list">
|
||||
{inboxClubCreationRequests.map((req) => (
|
||||
<div key={`creation-${req.id}`} className="card inbox-request-card">
|
||||
<div className="inbox-request-card__main">
|
||||
<div className="inbox-request-card__club">
|
||||
{req.proposed_name}
|
||||
{req.proposed_abbreviation ? (
|
||||
<span className="muted" style={{ marginLeft: '0.35rem' }}>
|
||||
({req.proposed_abbreviation})
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<strong className="inbox-request-card__applicant">
|
||||
{req.applicant_name || req.applicant_email || 'Antragsteller/in'}
|
||||
</strong>
|
||||
<div className="muted inbox-request-card__meta">
|
||||
{req.applicant_email} · Profil #{req.profile_id} · {formatWhen(req.created_at)}
|
||||
</div>
|
||||
{req.proposed_description ? (
|
||||
<p className="inbox-request-card__message">{req.proposed_description}</p>
|
||||
) : null}
|
||||
{req.message ? (
|
||||
<p className="inbox-request-card__message" style={{ fontStyle: 'italic' }}>
|
||||
Nachricht: {req.message}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="inbox-request-card__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
'Verein anlegen und Antragsteller als Hauptverwalter eintragen?'
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.approveClubCreationRequest(req.id)
|
||||
notifyOrgInboxChanged()
|
||||
await load()
|
||||
} catch (err) {
|
||||
alert(err.message || String(err))
|
||||
}
|
||||
}}
|
||||
>
|
||||
Freigeben
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={async () => {
|
||||
if (!confirm('Gründungsantrag ablehnen?')) return
|
||||
try {
|
||||
await api.rejectClubCreationRequest(req.id)
|
||||
notifyOrgInboxChanged()
|
||||
await load()
|
||||
} catch (err) {
|
||||
alert(err.message || String(err))
|
||||
}
|
||||
}}
|
||||
>
|
||||
Ablehnen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Abschnitt: Beitrittsanträge */}
|
||||
{canAccessOrgInbox && (
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text2)' }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user