2026-05-08 09:07:51 +02:00
|
|
|
import React from 'react'
|
2026-04-27 14:27:25 +02:00
|
|
|
import {
|
2026-05-13 17:19:59 +02:00
|
|
|
RouterProvider,
|
|
|
|
|
createBrowserRouter,
|
2026-04-27 14:27:25 +02:00
|
|
|
Navigate,
|
|
|
|
|
NavLink,
|
|
|
|
|
useLocation,
|
|
|
|
|
Outlet,
|
|
|
|
|
} from 'react-router-dom'
|
2026-04-21 14:56:16 +02:00
|
|
|
import { AuthProvider, useAuth } from './context/AuthContext'
|
2026-05-13 16:34:38 +02:00
|
|
|
import { ToastProvider } from './context/ToastContext'
|
2026-05-09 09:13:38 +02:00
|
|
|
import { OrgInboxProvider, useOrgInbox } from './context/OrgInboxContext'
|
2026-04-22 16:25:36 +02:00
|
|
|
import DesktopSidebar from './components/DesktopSidebar'
|
|
|
|
|
import { getMainNavItems } from './config/appNav'
|
2026-04-21 14:56:16 +02:00
|
|
|
import LoginPage from './pages/LoginPage'
|
2026-04-29 11:37:54 +02:00
|
|
|
import VerifyPage from './pages/VerifyPage'
|
2026-04-21 14:56:16 +02:00
|
|
|
import Dashboard from './pages/Dashboard'
|
2026-04-29 11:19:05 +02:00
|
|
|
import AccountSettingsPage from './pages/AccountSettingsPage'
|
2026-05-07 10:29:14 +02:00
|
|
|
import SettingsSystemInfoPage from './pages/SettingsSystemInfoPage'
|
2026-04-27 14:27:25 +02:00
|
|
|
import ExercisesListPage from './pages/ExercisesListPage'
|
|
|
|
|
import ExerciseDetailPage from './pages/ExerciseDetailPage'
|
|
|
|
|
import ExerciseFormPage from './pages/ExerciseFormPage'
|
2026-04-22 06:48:18 +02:00
|
|
|
import ClubsPage from './pages/ClubsPage'
|
2026-05-09 09:13:38 +02:00
|
|
|
import InboxPage from './pages/InboxPage'
|
feat: Skills & Methods catalog complete
Backend:
- Created routers/skills.py with full CRUD
- Skills: list (with category filter), get, create, update, delete
- Methods: list (with category filter), get, create, update, delete
- Default: only show active items
- Read access: all authenticated users
- Write access: admin only
- Registered skills router in main.py
Frontend:
- Complete SkillsPage with 2 tabs (Fähigkeiten, Trainingsmethoden)
- Browse by category with cards layout
- Admin CRUD forms (importance rating for skills, duration/group size for methods)
- Mobile-responsive grid layout
- Updated api.js with all skill/method functions
- Added /skills route to App.jsx
Migration already exists: 003_catalogs.sql (skills, training_methods + seed data)
Next: Training Planning (core feature)
2026-04-22 16:50:31 +02:00
|
|
|
import SkillsPage from './pages/SkillsPage'
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
import TrainingPlanningPage from './pages/TrainingPlanningPage'
|
2026-05-05 09:02:13 +02:00
|
|
|
import TrainingFrameworkProgramsListPage from './pages/TrainingFrameworkProgramsListPage'
|
|
|
|
|
import TrainingFrameworkProgramEditPage from './pages/TrainingFrameworkProgramEditPage'
|
2026-05-12 21:35:07 +02:00
|
|
|
import TrainingModulesListPage from './pages/TrainingModulesListPage'
|
|
|
|
|
import TrainingModuleEditPage from './pages/TrainingModuleEditPage'
|
2026-04-29 06:37:52 +02:00
|
|
|
import TrainingUnitRunPage from './pages/TrainingUnitRunPage'
|
2026-04-29 06:49:16 +02:00
|
|
|
import TrainingCoachPage from './pages/TrainingCoachPage'
|
2026-04-23 07:52:03 +02:00
|
|
|
import AdminCatalogsPage from './pages/AdminCatalogsPage'
|
2026-04-24 07:51:40 +02:00
|
|
|
import AdminHierarchyPage from './pages/AdminHierarchyPage'
|
2026-04-27 11:32:30 +02:00
|
|
|
import AdminMaturityModelsPage from './pages/AdminMaturityModelsPage'
|
2026-04-23 14:26:10 +02:00
|
|
|
import TrainerContextsPage from './pages/TrainerContextsPage'
|
2026-04-24 16:06:49 +02:00
|
|
|
import MediaWikiImportPage from './pages/MediaWikiImportPage'
|
2026-05-05 21:05:52 +02:00
|
|
|
import AdminUsersPage from './pages/AdminUsersPage'
|
2026-05-09 10:02:56 +02:00
|
|
|
import AdminHomeRedirect from './components/AdminHomeRedirect'
|
|
|
|
|
import PlatformAdminRoute from './components/PlatformAdminRoute'
|
2026-05-08 09:07:51 +02:00
|
|
|
import MediaLibraryPage from './pages/MediaLibraryPage'
|
2026-05-10 09:41:45 +02:00
|
|
|
import LegalPage from './pages/LegalPage'
|
2026-05-10 11:53:07 +02:00
|
|
|
import AdminLegalDocumentsPage from './pages/AdminLegalDocumentsPage'
|
2026-05-10 10:51:20 +02:00
|
|
|
import SettingsLegalPage from './pages/SettingsLegalPage'
|
2026-05-05 21:23:16 +02:00
|
|
|
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
|
2026-05-09 10:42:56 +02:00
|
|
|
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
|
2026-04-22 16:25:36 +02:00
|
|
|
import './app.css'
|
|
|
|
|
|
2026-05-09 13:26:22 +02:00
|
|
|
/** Shield „Admin“: nur Super-Admin (global). Vereinsorga: Vereine → Mitglieder. */
|
2026-05-09 10:02:56 +02:00
|
|
|
function computeShowAdminNav(currentUser) {
|
2026-05-09 13:26:22 +02:00
|
|
|
return currentUser?.role === 'superadmin'
|
2026-05-09 10:02:56 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 16:25:36 +02:00
|
|
|
// Bottom Navigation (Mobile)
|
2026-05-09 10:02:56 +02:00
|
|
|
function Nav({ showAdminNav }) {
|
2026-05-09 09:13:38 +02:00
|
|
|
const { canAccessOrgInbox, inboxCount } = useOrgInbox()
|
2026-05-09 10:02:56 +02:00
|
|
|
const items = getMainNavItems(showAdminNav, { showInbox: canAccessOrgInbox })
|
2026-04-22 16:25:36 +02:00
|
|
|
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 }) =>
|
2026-04-27 14:27:25 +02:00
|
|
|
'nav-item' + (navItemActive(loc.pathname, item, isActive) ? ' active' : '')
|
2026-04-22 16:25:36 +02:00
|
|
|
}
|
|
|
|
|
>
|
2026-05-06 12:52:35 +02:00
|
|
|
<item.Icon size={26} strokeWidth={2} />
|
2026-05-09 09:13:38 +02:00
|
|
|
{item.to === '/inbox' && inboxCount > 0 ? (
|
|
|
|
|
<span className="nav-item__badge" aria-label={`${inboxCount} offen`}>
|
|
|
|
|
{inboxCount > 99 ? '99+' : inboxCount}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
2026-04-22 16:25:36 +02:00
|
|
|
<span>{item.shortLabel || item.label}</span>
|
|
|
|
|
</NavLink>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
)
|
|
|
|
|
}
|
feat: Complete MVP setup - Docker, Frontend, Migrations, CI/CD
Docker & Deployment:
- docker-compose.yml (Prod: Port 3003/8003)
- docker-compose.dev-env.yml (Dev: Port 3098/8098)
- Backend Dockerfile (Python 3.12-slim)
- Frontend Dockerfile (Node 20 + Nginx)
- Gitea Actions (deploy-dev.yml, deploy-prod.yml)
Frontend:
- React 18 + Vite setup
- package.json, vite.config.js, index.html
- App.jsx (minimal with version display)
- api.js (complete API client)
- app.css + AuthContext from Mitai
- main.jsx entry point
Backend Migrations:
- 001_auth_membership.sql (Auth + Features + Tier Limits)
- 002_organization.sql (Clubs, Divisions, Training Groups)
- 003_catalogs.sql (Skills + Methods with sample data)
Documentation:
- .claude/rules/ (ARCHITECTURE, CODING_RULES, etc.)
- SHINKAN_PROJECT_SETUP.md (technical setup guide)
Server:
- Directories created on Pi: /home/lars/docker/shinkan[-dev]
- Gitea Runner configured and running
Ready for first deployment to dev.shinkan.jinkendo.de
version: 0.1.0
date: 2026-04-21
2026-04-21 14:36:52 +02:00
|
|
|
|
2026-04-27 14:27:25 +02:00
|
|
|
function ProtectedLayout() {
|
2026-04-22 16:25:36 +02:00
|
|
|
const { isAuthenticated, loading, user, logout } = useAuth()
|
|
|
|
|
|
|
|
|
|
const handleLogout = () => {
|
|
|
|
|
if (confirm('Wirklich abmelden?')) {
|
|
|
|
|
logout()
|
|
|
|
|
window.location.href = '/'
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-21 14:56:16 +02:00
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2026-04-27 14:27:25 +02:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
background: 'var(--bg)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-21 14:56:16 +02:00
|
|
|
<div className="spinner"></div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 16:25:36 +02:00
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
return <Navigate to="/login" replace />
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 10:02:56 +02:00
|
|
|
const showAdminNav = computeShowAdminNav(user)
|
2026-04-22 16:25:36 +02:00
|
|
|
|
|
|
|
|
return (
|
2026-05-09 09:13:38 +02:00
|
|
|
<OrgInboxProvider user={user}>
|
2026-05-09 10:02:56 +02:00
|
|
|
<DesktopSidebar showAdminNav={showAdminNav} user={user} onLogout={handleLogout} />
|
2026-04-22 16:25:36 +02:00
|
|
|
<div className="app-shell">
|
|
|
|
|
<div className="app-shell__column">
|
2026-05-05 21:23:16 +02:00
|
|
|
<div className="app-header app-header--mobile app-header--mobile-stack">
|
|
|
|
|
<div className="app-header-mobile__top">
|
|
|
|
|
<div className="app-logo">🥋 Shinkan</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ActiveClubSwitcher variant="mobile" />
|
2026-04-22 16:25:36 +02:00
|
|
|
</div>
|
2026-04-27 14:27:25 +02:00
|
|
|
<div className="app-main">
|
2026-05-09 10:42:56 +02:00
|
|
|
<InactiveMembershipBanner />
|
2026-04-27 14:27:25 +02:00
|
|
|
<Outlet />
|
|
|
|
|
</div>
|
2026-05-09 10:02:56 +02:00
|
|
|
<Nav showAdminNav={showAdminNav} />
|
2026-04-22 16:25:36 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-09 09:13:38 +02:00
|
|
|
</OrgInboxProvider>
|
2026-04-22 16:25:36 +02:00
|
|
|
)
|
2026-04-21 14:56:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PublicRoute({ children }) {
|
|
|
|
|
const { isAuthenticated, loading } = useAuth()
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
2026-04-27 14:27:25 +02:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
background: 'var(--bg)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-21 14:56:16 +02:00
|
|
|
<div className="spinner"></div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !isAuthenticated ? children : <Navigate to="/" replace />
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 17:19:59 +02:00
|
|
|
/**
|
|
|
|
|
* 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: '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/framework-programs/new', element: <TrainingFrameworkProgramEditPage /> },
|
|
|
|
|
{ path: 'planning/framework-programs/:id', element: <TrainingFrameworkProgramEditPage /> },
|
|
|
|
|
{ path: 'planning/framework-programs', element: <TrainingFrameworkProgramsListPage /> },
|
|
|
|
|
{ path: 'planning/training-modules/new', element: <TrainingModuleEditPage /> },
|
|
|
|
|
{ path: 'planning/training-modules/:id', element: <TrainingModuleEditPage /> },
|
|
|
|
|
{ path: 'planning/training-modules', element: <TrainingModulesListPage /> },
|
|
|
|
|
{ path: 'planning/run/:unitId/coach', element: <TrainingCoachPage /> },
|
|
|
|
|
{ path: 'planning/run/:unitId', element: <TrainingUnitRunPage /> },
|
|
|
|
|
{ path: 'planning', element: <TrainingPlanningPage /> },
|
|
|
|
|
{ path: 'admin', element: <AdminHomeRedirect /> },
|
|
|
|
|
{
|
|
|
|
|
path: 'admin/users',
|
|
|
|
|
element: (
|
|
|
|
|
<PlatformAdminRoute>
|
|
|
|
|
<AdminUsersPage />
|
|
|
|
|
</PlatformAdminRoute>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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: 'trainer-contexts', element: <TrainerContextsPage /> },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{ path: '*', element: <Navigate to="/" replace /> },
|
|
|
|
|
])
|
2026-04-21 14:56:16 +02:00
|
|
|
|
|
|
|
|
function App() {
|
feat: Complete MVP setup - Docker, Frontend, Migrations, CI/CD
Docker & Deployment:
- docker-compose.yml (Prod: Port 3003/8003)
- docker-compose.dev-env.yml (Dev: Port 3098/8098)
- Backend Dockerfile (Python 3.12-slim)
- Frontend Dockerfile (Node 20 + Nginx)
- Gitea Actions (deploy-dev.yml, deploy-prod.yml)
Frontend:
- React 18 + Vite setup
- package.json, vite.config.js, index.html
- App.jsx (minimal with version display)
- api.js (complete API client)
- app.css + AuthContext from Mitai
- main.jsx entry point
Backend Migrations:
- 001_auth_membership.sql (Auth + Features + Tier Limits)
- 002_organization.sql (Clubs, Divisions, Training Groups)
- 003_catalogs.sql (Skills + Methods with sample data)
Documentation:
- .claude/rules/ (ARCHITECTURE, CODING_RULES, etc.)
- SHINKAN_PROJECT_SETUP.md (technical setup guide)
Server:
- Directories created on Pi: /home/lars/docker/shinkan[-dev]
- Gitea Runner configured and running
Ready for first deployment to dev.shinkan.jinkendo.de
version: 0.1.0
date: 2026-04-21
2026-04-21 14:36:52 +02:00
|
|
|
return (
|
|
|
|
|
<AuthProvider>
|
2026-05-13 16:34:38 +02:00
|
|
|
<ToastProvider>
|
2026-05-13 17:19:59 +02:00
|
|
|
<RouterProvider router={appRouter} />
|
2026-05-13 16:34:38 +02:00
|
|
|
</ToastProvider>
|
feat: Complete MVP setup - Docker, Frontend, Migrations, CI/CD
Docker & Deployment:
- docker-compose.yml (Prod: Port 3003/8003)
- docker-compose.dev-env.yml (Dev: Port 3098/8098)
- Backend Dockerfile (Python 3.12-slim)
- Frontend Dockerfile (Node 20 + Nginx)
- Gitea Actions (deploy-dev.yml, deploy-prod.yml)
Frontend:
- React 18 + Vite setup
- package.json, vite.config.js, index.html
- App.jsx (minimal with version display)
- api.js (complete API client)
- app.css + AuthContext from Mitai
- main.jsx entry point
Backend Migrations:
- 001_auth_membership.sql (Auth + Features + Tier Limits)
- 002_organization.sql (Clubs, Divisions, Training Groups)
- 003_catalogs.sql (Skills + Methods with sample data)
Documentation:
- .claude/rules/ (ARCHITECTURE, CODING_RULES, etc.)
- SHINKAN_PROJECT_SETUP.md (technical setup guide)
Server:
- Directories created on Pi: /home/lars/docker/shinkan[-dev]
- Gitea Runner configured and running
Ready for first deployment to dev.shinkan.jinkendo.de
version: 0.1.0
date: 2026-04-21
2026-04-21 14:36:52 +02:00
|
|
|
</AuthProvider>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App
|