diff --git a/frontend/src/pages/InboxPage.jsx b/frontend/src/pages/InboxPage.jsx
new file mode 100644
index 0000000..2aead00
--- /dev/null
+++ b/frontend/src/pages/InboxPage.jsx
@@ -0,0 +1,222 @@
+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 (
+
+
Posteingang
+
Kein Zugriff. Nur Plattform-Admins und Vereinsadmins sehen den Posteingang.
+
+ Zur Übersicht
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ Posteingang
+
+
+ Offene Beitrittsanträge zu Vereinen, für die du zuständig bist.
+
+
+
+
+
+ {loading ? (
+
+ ) : inboxJoinRequests.length === 0 ? (
+
+
+ Keine offenen Beitrittsanträge.
+
+
+ ) : (
+
+ {inboxJoinRequests.map((req) => (
+
+
+
+ {req.club_name || 'Verein'}
+ {req.club_abbreviation ? (
+
+ ({req.club_abbreviation})
+
+ ) : null}
+
+
+ {req.applicant_name || req.applicant_email || 'Bewerber/in'}
+
+
+ {req.applicant_email} · Profil #{req.profile_id} · {formatWhen(req.created_at)}
+
+ {req.message ?
{req.message}
: null}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {acceptModal && (
+
+
+
Antrag annehmen
+
{acceptModal.label}
+
+
Rollen bei Aufnahme
+
+ {CLUB_ROLE_OPTIONS.map((opt) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js
index 8bf5c55..fe5e7b3 100644
--- a/frontend/src/utils/api.js
+++ b/frontend/src/utils/api.js
@@ -264,6 +264,11 @@ export async function rejectClubJoinRequest(clubId, requestId) {
})
}
+/** Aggregierter Posteingang: offene Beitrittsanträge für Vereins-/Plattform-Admins. */
+export async function getInboxJoinRequests() {
+ return request('/api/me/inbox/join-requests')
+}
+
export async function listDivisions(clubId) {
const query = clubId ? `?club_id=${clubId}` : ''
return request(`/api/divisions${query}`)
@@ -1326,6 +1331,7 @@ export const api = {
listClubJoinRequests,
acceptClubJoinRequest,
rejectClubJoinRequest,
+ getInboxJoinRequests,
listDivisions,
createDivision,
updateDivision,