diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a0e100d..b308aed 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() diff --git a/frontend/src/components/DashboardOrgInboxWidget.jsx b/frontend/src/components/DashboardOrgInboxWidget.jsx index eafb532..a00c6ab 100644 --- a/frontend/src/components/DashboardOrgInboxWidget.jsx +++ b/frontend/src/components/DashboardOrgInboxWidget.jsx @@ -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 (

{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(' · ')}

{preview.length > 0 ? ( diff --git a/frontend/src/components/DesktopSidebar.jsx b/frontend/src/components/DesktopSidebar.jsx index b8bcdf9..59af74c 100644 --- a/frontend/src/components/DesktopSidebar.jsx +++ b/frontend/src/components/DesktopSidebar.jsx @@ -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 || '' diff --git a/frontend/src/context/OrgInboxContext.jsx b/frontend/src/context/OrgInboxContext.jsx index 5c6b6db..7b88538 100644 --- a/frontend/src/context/OrgInboxContext.jsx +++ b/frontend/src/context/OrgInboxContext.jsx @@ -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 {children} diff --git a/frontend/src/pages/InboxPage.jsx b/frontend/src/pages/InboxPage.jsx index 11f123e..fd2b829 100644 --- a/frontend/src/pages/InboxPage.jsx +++ b/frontend/src/pages/InboxPage.jsx @@ -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 (

Posteingang

@@ -375,7 +378,7 @@ export default function InboxPage() { Posteingang

- Beitrittsanträge und Inhaltsmeldungen für deine Zuständigkeitsbereiche. + Beitrittsanträge, Vereinsgründungen und Inhaltsmeldungen für deine Zuständigkeitsbereiche.

+ + + + ))} + + )} +
+ )} + + {/* Abschnitt: Beitrittsanträge */} {canAccessOrgInbox && (