Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Has been cancelled
- Replaced the admin club feature exemptions router with a new admin rights router to streamline capability management. - Added new API endpoints for managing admin rights, including capability grants and quota bypass for portal roles and profiles. - Updated the frontend to include navigation and lazy loading for the new Admin Rights page. - Incremented application version to 0.8.197 to reflect these changes and enhancements.
397 lines
14 KiB
JavaScript
397 lines
14 KiB
JavaScript
import React, { Suspense, lazy } from 'react'
|
|
import LoginPage from './pages/LoginPage'
|
|
import { lazyWithRetry } from './utils/lazyWithRetry'
|
|
import {
|
|
RouterProvider,
|
|
createBrowserRouter,
|
|
Navigate,
|
|
NavLink,
|
|
useLocation,
|
|
Outlet,
|
|
} from 'react-router-dom'
|
|
import { AuthProvider, useAuth } from './context/AuthContext'
|
|
import { EntitlementsProvider } from './context/EntitlementsContext'
|
|
import { FormEditorActionsProvider, FormEditorBottomSlot } from './context/FormEditorActionsContext'
|
|
import { ToastProvider } from './context/ToastContext'
|
|
import { OrgInboxProvider, useOrgInbox } from './context/OrgInboxContext'
|
|
import DesktopSidebar from './components/DesktopSidebar'
|
|
import { getMainNavItems } from './config/appNav'
|
|
import { isOnboardingAllowedPath, isOnboardingRestricted } from './utils/accountState'
|
|
import AdminHomeRedirect from './components/AdminHomeRedirect'
|
|
import PlatformAdminRoute from './components/PlatformAdminRoute'
|
|
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
|
|
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
|
|
import './app.css'
|
|
|
|
const OnboardingPage = lazyWithRetry(() => import('./pages/OnboardingPage'))
|
|
const VerifyPage = lazy(() => import('./pages/VerifyPage'))
|
|
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
|
const AccountSettingsPage = lazy(() => import('./pages/AccountSettingsPage'))
|
|
const SettingsSystemInfoPage = lazy(() => import('./pages/SettingsSystemInfoPage'))
|
|
const ExercisesListPage = lazy(() => import('./pages/ExercisesListPage'))
|
|
const ExerciseDetailPage = lazy(() => import('./pages/ExerciseDetailPage'))
|
|
const ExerciseFormPage = lazy(() => import('./pages/ExerciseFormPage'))
|
|
const ClubsPage = lazy(() => import('./pages/ClubsPage'))
|
|
const InboxPage = lazy(() => import('./pages/InboxPage'))
|
|
const SkillsPage = lazy(() => import('./pages/SkillsPage'))
|
|
const TrainingPlanningPage = lazy(() => import('./pages/TrainingPlanningPage'))
|
|
const TrainingPlanTemplatesListPage = lazy(() => import('./pages/TrainingPlanTemplatesListPage'))
|
|
const TrainingPlanTemplateEditPage = lazy(() => import('./pages/TrainingPlanTemplateEditPage'))
|
|
const PlanningLayout = lazy(() => import('./layouts/PlanningLayout'))
|
|
const TrainingFrameworkProgramsListPage = lazy(() =>
|
|
import('./pages/TrainingFrameworkProgramsListPage'),
|
|
)
|
|
const TrainingFrameworkProgramEditPage = lazy(() =>
|
|
import('./pages/TrainingFrameworkProgramEditPage'),
|
|
)
|
|
const TrainingModulesListPage = lazy(() => import('./pages/TrainingModulesListPage'))
|
|
const TrainingModuleEditPage = lazy(() => import('./pages/TrainingModuleEditPage'))
|
|
const TrainingUnitRunPage = lazy(() => import('./pages/TrainingUnitRunPage'))
|
|
const TrainingUnitEditPage = lazy(() => import('./pages/TrainingUnitEditPage'))
|
|
const TrainingCoachPage = lazy(() => import('./pages/TrainingCoachPage'))
|
|
const AdminCatalogsPage = lazy(() => import('./pages/AdminCatalogsPage'))
|
|
const AdminHierarchyPage = lazy(() => import('./pages/AdminHierarchyPage'))
|
|
const AdminMaturityModelsPage = lazy(() => import('./pages/AdminMaturityModelsPage'))
|
|
const TrainerContextsPage = lazy(() => import('./pages/TrainerContextsPage'))
|
|
const MediaWikiImportPage = lazy(() => import('./pages/MediaWikiImportPage'))
|
|
const AdminUsersPage = lazy(() => import('./pages/AdminUsersPage'))
|
|
const AdminClubCreationRequestsPage = lazy(() => import('./pages/AdminClubCreationRequestsPage'))
|
|
const AdminRightsPage = lazy(() => import('./pages/AdminRightsPage'))
|
|
const MediaLibraryPage = lazy(() => import('./pages/MediaLibraryPage'))
|
|
const LegalPage = lazy(() => import('./pages/LegalPage'))
|
|
const AdminLegalDocumentsPage = lazy(() => import('./pages/AdminLegalDocumentsPage'))
|
|
const AdminAiSkillRetrievalPage = lazy(() => import('./pages/AdminAiSkillRetrievalPage'))
|
|
const AdminAiPromptsPage = lazy(() => import('./pages/AdminAiPromptsPage'))
|
|
const AdminExerciseEnrichmentPage = lazy(() => import('./pages/AdminExerciseEnrichmentPage'))
|
|
const AdminUserContentPage = lazy(() => import('./pages/AdminUserContentPage'))
|
|
const SettingsLegalPage = lazy(() => import('./pages/SettingsLegalPage'))
|
|
|
|
/** Shield „Admin“: nur Super-Admin (global). Vereinsorga: Vereine → Mitglieder. */
|
|
function computeShowAdminNav(currentUser) {
|
|
return currentUser?.role === 'superadmin'
|
|
}
|
|
|
|
function AppRouteFallback() {
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'var(--bg)',
|
|
}}
|
|
>
|
|
<div className="spinner"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Bottom Navigation (Mobile)
|
|
function Nav({ showAdminNav, onboardingOnly }) {
|
|
const { canShowInboxNav, inboxCount } = useOrgInbox()
|
|
const items = getMainNavItems(showAdminNav, {
|
|
showInbox: canShowInboxNav,
|
|
onboardingOnly,
|
|
})
|
|
const loc = useLocation()
|
|
|
|
const navItemActive = (pathname, item, routerIsActive) => {
|
|
if (item.to.startsWith('/admin')) return pathname.startsWith('/admin')
|
|
return routerIsActive
|
|
}
|
|
|
|
return (
|
|
<nav className="bottom-nav">
|
|
{items.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
end={!!item.end}
|
|
className={({ isActive }) =>
|
|
'nav-item' + (navItemActive(loc.pathname, item, isActive) ? ' active' : '')
|
|
}
|
|
>
|
|
<item.Icon size={26} strokeWidth={2} />
|
|
{item.to === '/inbox' && inboxCount > 0 ? (
|
|
<span className="nav-item__badge" aria-label={`${inboxCount} offen`}>
|
|
{inboxCount > 99 ? '99+' : inboxCount}
|
|
</span>
|
|
) : null}
|
|
<span>{item.shortLabel || item.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
function ProtectedLayout() {
|
|
const { isAuthenticated, loading, user, logout } = useAuth()
|
|
|
|
const handleLogout = async () => {
|
|
if (!confirm('Wirklich abmelden?')) return
|
|
await logout()
|
|
window.location.href = '/login'
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'var(--bg)',
|
|
}}
|
|
>
|
|
<div className="spinner"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return <Navigate to="/login" replace />
|
|
}
|
|
|
|
const location = useLocation()
|
|
const onboardingOnly = isOnboardingRestricted(user)
|
|
if (onboardingOnly && !isOnboardingAllowedPath(location.pathname)) {
|
|
return <Navigate to="/onboarding" replace />
|
|
}
|
|
|
|
const showAdminNav = computeShowAdminNav(user) && !onboardingOnly
|
|
|
|
return (
|
|
<OrgInboxProvider user={user}>
|
|
<FormEditorActionsProvider>
|
|
<DesktopSidebar
|
|
showAdminNav={showAdminNav}
|
|
onboardingOnly={onboardingOnly}
|
|
user={user}
|
|
onLogout={handleLogout}
|
|
/>
|
|
<div className="app-shell">
|
|
<div className="app-shell__column">
|
|
<div className="app-header app-header--mobile app-header--mobile-stack">
|
|
<div className="app-header-mobile__top">
|
|
<div className="app-logo">🥋 Shinkan</div>
|
|
</div>
|
|
{!onboardingOnly ? <ActiveClubSwitcher variant="mobile" /> : null}
|
|
</div>
|
|
<div className="app-main">
|
|
<InactiveMembershipBanner />
|
|
<Outlet />
|
|
</div>
|
|
<FormEditorBottomSlot>
|
|
<Nav showAdminNav={showAdminNav} onboardingOnly={onboardingOnly} />
|
|
</FormEditorBottomSlot>
|
|
</div>
|
|
</div>
|
|
</FormEditorActionsProvider>
|
|
</OrgInboxProvider>
|
|
)
|
|
}
|
|
|
|
function PublicRoute({ children }) {
|
|
const { isAuthenticated, loading } = useAuth()
|
|
|
|
if (loading) {
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: 'var(--bg)',
|
|
}}
|
|
>
|
|
<div className="spinner"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return !isAuthenticated ? children : <Navigate to="/" replace />
|
|
}
|
|
|
|
/**
|
|
* Data Router — erforderlich für `useBlocker` (ungespeicherte Änderungen).
|
|
* Klassisches `BrowserRouter` stellt keinen DataRouterContext bereit; ohne Migration
|
|
* werfen Seiten mit `useUnsavedChangesBlocker` beim Rendern eine Invariante.
|
|
*/
|
|
const appRouter = createBrowserRouter([
|
|
{ path: '/verify', element: <VerifyPage /> },
|
|
{
|
|
path: '/login',
|
|
element: (
|
|
<PublicRoute>
|
|
<LoginPage />
|
|
</PublicRoute>
|
|
),
|
|
},
|
|
{ path: '/impressum', element: <LegalPage type="impressum" /> },
|
|
{ path: '/datenschutz', element: <LegalPage type="datenschutz" /> },
|
|
{ path: '/nutzungsbedingungen', element: <LegalPage type="nutzungsbedingungen" /> },
|
|
{ path: '/medienrichtlinie', element: <LegalPage type="medienrichtlinie" /> },
|
|
{
|
|
element: <ProtectedLayout />,
|
|
children: [
|
|
{ index: true, element: <Dashboard /> },
|
|
{ path: 'onboarding', element: <OnboardingPage /> },
|
|
{ path: 'profile', element: <Navigate to="/settings" replace /> },
|
|
{ path: 'settings', element: <AccountSettingsPage /> },
|
|
{ path: 'settings/system', element: <SettingsSystemInfoPage /> },
|
|
{ path: 'settings/legal', element: <SettingsLegalPage /> },
|
|
{ path: 'media', element: <MediaLibraryPage /> },
|
|
{
|
|
path: 'exercises',
|
|
children: [
|
|
{ index: true, element: <ExercisesListPage /> },
|
|
{ path: 'new', element: <ExerciseFormPage /> },
|
|
{ path: ':id/edit', element: <ExerciseFormPage /> },
|
|
{ path: ':id', element: <ExerciseDetailPage /> },
|
|
],
|
|
},
|
|
{ path: 'clubs', element: <ClubsPage /> },
|
|
{ path: 'inbox', element: <InboxPage /> },
|
|
{ path: 'skills', element: <SkillsPage /> },
|
|
{
|
|
path: 'planning',
|
|
element: <PlanningLayout />,
|
|
children: [
|
|
{ index: true, element: <TrainingPlanningPage /> },
|
|
{ path: 'framework-programs', element: <TrainingFrameworkProgramsListPage /> },
|
|
{ path: 'training-modules', element: <TrainingModulesListPage /> },
|
|
{ path: 'plan-templates', element: <TrainingPlanTemplatesListPage /> },
|
|
{ path: 'units/new', element: <TrainingUnitEditPage /> },
|
|
{ path: 'units/:id/edit', element: <TrainingUnitEditPage /> },
|
|
],
|
|
},
|
|
{ path: 'planning/framework-programs/new', element: <TrainingFrameworkProgramEditPage /> },
|
|
{ path: 'planning/framework-programs/:id', element: <TrainingFrameworkProgramEditPage /> },
|
|
{ path: 'planning/training-modules/new', element: <TrainingModuleEditPage /> },
|
|
{ path: 'planning/training-modules/:id', element: <TrainingModuleEditPage /> },
|
|
{ path: 'planning/plan-templates/:id', element: <TrainingPlanTemplateEditPage /> },
|
|
{ path: 'planning/run/:unitId/coach', element: <TrainingCoachPage /> },
|
|
{ path: 'planning/run/:unitId', element: <TrainingUnitRunPage /> },
|
|
{ path: 'admin', element: <AdminHomeRedirect /> },
|
|
{
|
|
path: 'admin/users',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminUsersPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/club-creation-requests',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminClubCreationRequestsPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/rights',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminRightsPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{ path: 'admin/membership', element: <Navigate to="/admin/rights" replace /> },
|
|
{
|
|
path: 'admin/hierarchy',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminHierarchyPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/maturity-models',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminMaturityModelsPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/catalogs',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminCatalogsPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/mediawiki-import',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<MediaWikiImportPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/legal-documents',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminLegalDocumentsPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/ai-skill-retrieval',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminAiSkillRetrievalPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/ai-prompts',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminAiPromptsPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/exercise-enrichment',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminExerciseEnrichmentPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{
|
|
path: 'admin/user-content',
|
|
element: (
|
|
<PlatformAdminRoute>
|
|
<AdminUserContentPage />
|
|
</PlatformAdminRoute>
|
|
),
|
|
},
|
|
{ path: 'trainer-contexts', element: <TrainerContextsPage /> },
|
|
],
|
|
},
|
|
{ path: '*', element: <Navigate to="/" replace /> },
|
|
])
|
|
|
|
function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<EntitlementsProvider>
|
|
<ToastProvider>
|
|
<Suspense fallback={<AppRouteFallback />}>
|
|
<RouterProvider router={appRouter} />
|
|
</Suspense>
|
|
</ToastProvider>
|
|
</EntitlementsProvider>
|
|
</AuthProvider>
|
|
)
|
|
}
|
|
|
|
export default App
|