feat: enhance role permissions and UI for clubs and training planning
- Updated role permissions to allow trainers and users to create clubs and training groups. - Modified database insertion logic to reflect the correct role for trainers during registration. - Enhanced frontend components to display appropriate messages and buttons based on user roles. - Improved user guidance in the Clubs and Training Planning pages, emphasizing the need for clubs and groups before planning training sessions.
This commit is contained in:
parent
1f2a4595cd
commit
69b26fc928
|
|
@ -245,7 +245,7 @@ async def register(req: RegisterRequest, request: Request):
|
|||
id, name, email, pin_hash, auth_type, role, tier,
|
||||
email_verified, verification_token, verification_expires,
|
||||
trial_ends_at, created_at
|
||||
) VALUES (%s, %s, %s, %s, 'email', 'user', 'free', FALSE, %s, %s, %s, CURRENT_TIMESTAMP)
|
||||
) VALUES (%s, %s, %s, %s, 'email', 'trainer', 'free', FALSE, %s, %s, %s, CURRENT_TIMESTAMP)
|
||||
""", (profile_id, name, email, pin_hash, verification_token, verification_expires, trial_ends))
|
||||
|
||||
# Send verification email
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@ def get_club(club_id: int, session=Depends(require_auth)):
|
|||
# ── Create Club ───────────────────────────────────────────────────────
|
||||
@router.post("/clubs")
|
||||
def create_club(data: dict, session=Depends(require_auth)):
|
||||
"""Create new club (admin only)."""
|
||||
"""Create new club (Admin oder Trainer — MVP ohne separates Vereins-Onboarding)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Vereine erstellen")
|
||||
if role not in ['admin', 'superadmin', 'trainer', 'user']:
|
||||
raise HTTPException(403, "Keine Berechtigung, Vereine anzulegen")
|
||||
|
||||
name = data.get('name')
|
||||
if not name:
|
||||
|
|
@ -395,10 +395,11 @@ def get_training_group(group_id: int, session=Depends(require_auth)):
|
|||
# ── Create Training Group ─────────────────────────────────────────────
|
||||
@router.post("/groups")
|
||||
def create_training_group(data: dict, session=Depends(require_auth)):
|
||||
"""Create new training group (admin or trainer)."""
|
||||
"""Create new training group (admin, trainer oder normaler Nutzer mit Vereinsbezug)."""
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin', 'trainer']:
|
||||
raise HTTPException(403, "Nur Admins und Trainer dürfen Gruppen erstellen")
|
||||
if role not in ['admin', 'superadmin', 'trainer', 'user']:
|
||||
raise HTTPException(403, "Keine Berechtigung, Trainingsgruppen anzulegen")
|
||||
|
||||
club_id = data.get('club_id')
|
||||
name = data.get('name')
|
||||
|
|
@ -406,6 +407,10 @@ def create_training_group(data: dict, session=Depends(require_auth)):
|
|||
if not club_id or not name:
|
||||
raise HTTPException(400, "club_id und name sind Pflichtfelder")
|
||||
|
||||
trainer_id = data.get('trainer_id')
|
||||
if trainer_id in (None, "", 0) and role in ("trainer", "user"):
|
||||
trainer_id = profile_id
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
|
|
@ -436,7 +441,7 @@ def create_training_group(data: dict, session=Depends(require_auth)):
|
|||
data.get('time_start'),
|
||||
data.get('time_end'),
|
||||
data.get('location'),
|
||||
data.get('trainer_id'),
|
||||
trainer_id,
|
||||
data.get('co_trainer_ids'),
|
||||
data.get('status', 'active')
|
||||
))
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ from auth import require_auth
|
|||
router = APIRouter(prefix="/api", tags=["training_planning"])
|
||||
|
||||
|
||||
def _has_planning_role(role: Optional[str]) -> bool:
|
||||
"""Kann Trainingseinheiten/Vorlagen anlegen (bis Governance: auch einfacher Account)."""
|
||||
return role in ("admin", "superadmin", "trainer", "user")
|
||||
|
||||
|
||||
def _optional_positive_int(val, field_name: str) -> Optional[int]:
|
||||
if val is None or val == "":
|
||||
return None
|
||||
|
|
@ -50,7 +55,7 @@ def _can_access_group_for_create(cur, group_id: int, profile_id: int, role: str)
|
|||
if not group:
|
||||
raise HTTPException(status_code=404, detail="Trainingsgruppe nicht gefunden")
|
||||
co_trainers = group["co_trainer_ids"] or []
|
||||
if role not in ["admin", "superadmin", "trainer"]:
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Trainingseinheiten erstellen")
|
||||
if role not in ["admin", "superadmin"]:
|
||||
if group["trainer_id"] != profile_id and profile_id not in co_trainers:
|
||||
|
|
@ -381,7 +386,7 @@ def get_training_plan_template(template_id: int, session=Depends(require_auth)):
|
|||
def create_training_plan_template(data: dict, session=Depends(require_auth)):
|
||||
profile_id = session["profile_id"]
|
||||
role = session.get("role")
|
||||
if role not in ["admin", "superadmin", "trainer"]:
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Vorlagen anlegen")
|
||||
name = (data.get("name") or "").strip()
|
||||
if not name:
|
||||
|
|
@ -787,7 +792,7 @@ def quick_create_training_unit(data: dict, session=Depends(require_auth)):
|
|||
role = session.get("role")
|
||||
co_trainers = group["co_trainer_ids"] or []
|
||||
|
||||
if role not in ["admin", "superadmin", "trainer"]:
|
||||
if not _has_planning_role(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Trainingseinheiten erstellen")
|
||||
|
||||
if role not in ["admin", "superadmin"]:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function ClubsPage() {
|
|||
|
||||
const isAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
||||
const isTrainer = user?.role === 'trainer' || isAdmin
|
||||
const canCreateClub = ['admin', 'superadmin', 'trainer', 'user'].includes(user?.role)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
|
|
@ -61,7 +62,7 @@ function ClubsPage() {
|
|||
time_start: '',
|
||||
time_end: '',
|
||||
location: '',
|
||||
trainer_id: '',
|
||||
trainer_id: user?.id ?? '',
|
||||
co_trainer_ids: [],
|
||||
status: 'active'
|
||||
})
|
||||
|
|
@ -144,7 +145,11 @@ function ClubsPage() {
|
|||
return (
|
||||
<div style={{ padding: '2rem' }}>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: '1.5rem' }}>Vereinsverwaltung</h1>
|
||||
<h1 style={{ marginBottom: '0.75rem' }}>Vereinsverwaltung</h1>
|
||||
<p style={{ color: 'var(--text2)', marginBottom: '1.35rem', maxWidth: '46rem', lineHeight: 1.55 }}>
|
||||
Für die Trainingsplanung wird mindestens ein <strong>Verein</strong> und eine <strong>Trainingsgruppe</strong> gebraucht.
|
||||
Sparten sind optional — typische Eckdaten einer Gruppe (Wochentag, Zeit, Ort) kannst du schrittweise eintragen.
|
||||
</p>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{
|
||||
|
|
@ -179,7 +184,7 @@ function ClubsPage() {
|
|||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||
<h2>Vereine</h2>
|
||||
{isAdmin && (
|
||||
{canCreateClub && (
|
||||
<button className="btn btn-primary" onClick={() => handleCreate('club')}>
|
||||
+ Neuer Verein
|
||||
</button>
|
||||
|
|
@ -188,9 +193,17 @@ function ClubsPage() {
|
|||
|
||||
{clubs.length === 0 ? (
|
||||
<div className="card">
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||
Keine Vereine gefunden
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center', marginBottom: canCreateClub ? '1rem' : 0 }}>
|
||||
Noch kein Verein angelegt.
|
||||
{canCreateClub
|
||||
? ' Nutze „+ Neuer Verein“ — ein Name reicht zum Start.'
|
||||
: ' Bitte einen Administrator oder Support um Anlage.'}
|
||||
</p>
|
||||
{canCreateClub && (
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center', fontSize: '0.9rem' }}>
|
||||
Danach im Tab Trainingsgruppen eine Gruppe diesem Verein zuordnen; Details sind optional.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||
|
|
@ -328,7 +341,7 @@ function ClubsPage() {
|
|||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||
<h2>Trainingsgruppen</h2>
|
||||
{isTrainer && (
|
||||
{canCreateClub && (
|
||||
<button className="btn btn-primary" onClick={() => handleCreate('group')}>
|
||||
+ Neue Gruppe
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import api from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
|
|
@ -481,7 +482,32 @@ function TrainingPlanningPage() {
|
|||
return (
|
||||
<div style={{ padding: '2rem' }}>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: '1.5rem' }}>Trainingsplanung</h1>
|
||||
<h1 style={{ marginBottom: '0.35rem' }}>Trainingsplanung</h1>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', marginBottom: '1.25rem' }}>
|
||||
Wähle eine Trainingsgruppe, lege dann Termine mit Inhalt (Abschnitte und Übungen) an — ein Plan entsteht aus einer oder mehreren{' '}
|
||||
<strong>Trainingseinheiten</strong> im gewählten Zeitraum.
|
||||
</p>
|
||||
|
||||
{!loading && groups.length === 0 && (
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
marginBottom: '1.25rem',
|
||||
borderLeft: '4px solid var(--accent)',
|
||||
padding: '1rem 1.25rem'
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: '1.1rem', marginBottom: '0.5rem' }}>Erst Verein & Gruppe anlegen</h2>
|
||||
<p style={{ color: 'var(--text2)', marginBottom: '0.85rem', lineHeight: 1.5 }}>
|
||||
Ohne Trainingsgruppe kann hier nichts gebucht werden. Unter <strong>Vereine</strong> legst du einen Verein an
|
||||
(kurzer Name genügt), optional eine Sparte, dann eine <strong>Trainingsgruppe</strong>. Wochentage, feste Zeiten oder
|
||||
Eigenschaften sind optional und kannst du später ergänzen.
|
||||
</p>
|
||||
<Link to="/clubs" className="btn btn-primary" style={{ textDecoration: 'none' }}>
|
||||
Zu Vereinen & Trainingsgruppen
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||||
<div
|
||||
|
|
@ -545,56 +571,93 @@ function TrainingPlanningPage() {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedGroupId && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '1.5rem',
|
||||
alignItems: 'center'
|
||||
marginTop: '1.25rem',
|
||||
paddingTop: '1rem',
|
||||
borderTop: '1px solid var(--border, rgba(0,0,0,0.08))'
|
||||
}}
|
||||
>
|
||||
<button className="btn btn-primary" onClick={handleCreate}>
|
||||
+ Neue Trainingseinheit
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<label className="form-label" style={{ marginBottom: 0 }}>
|
||||
Schnell (+ optional Vorlage):
|
||||
</label>
|
||||
<select
|
||||
className="form-input"
|
||||
style={{ minWidth: '200px', marginBottom: 0 }}
|
||||
value={quickTemplateId}
|
||||
onChange={(e) => setQuickTemplateId(e.target.value)}
|
||||
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', marginBottom: '0.75rem' }}>
|
||||
<strong>Plan anlegen:</strong> neue Trainingseinheit mit Datum, Zeit und Ablauf — oder schnell nur mit Datum (Zeiten aus der Gruppe).
|
||||
{!selectedGroupId && (
|
||||
<span style={{ display: 'block', marginTop: '0.35rem' }}>
|
||||
Wähle oben eine Trainingsgruppe, um die Schaltflächen zu aktivieren.
|
||||
</span>
|
||||
)}
|
||||
{groups.length === 0 && (
|
||||
<span style={{ display: 'block', marginTop: '0.35rem' }}>
|
||||
Es gibt noch keine aktive Trainingsgruppe — unter{' '}
|
||||
<Link to="/clubs">
|
||||
Vereine
|
||||
</Link>{' '}
|
||||
anlegen oder aktivieren.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!selectedGroupId}
|
||||
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
<option value="">Standard (leer)</option>
|
||||
{planTemplates.map((t) => (
|
||||
<option key={t.id} value={String(t.id)}>
|
||||
{t.name}
|
||||
{typeof t.sections_count === 'number' ? ` (${t.sections_count} Abschn.)` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="btn btn-secondary" onClick={handleQuickCreate}>
|
||||
Schnell erstellen
|
||||
+ Neue Trainingseinheit planen
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<label className="form-label" style={{ marginBottom: 0 }}>
|
||||
Schnell (+ optional Vorlage):
|
||||
</label>
|
||||
<select
|
||||
className="form-input"
|
||||
style={{ minWidth: '180px', marginBottom: 0 }}
|
||||
value={quickTemplateId}
|
||||
onChange={(e) => setQuickTemplateId(e.target.value)}
|
||||
disabled={!selectedGroupId}
|
||||
title={!selectedGroupId ? 'Zuerst Trainingsgruppe wählen' : undefined}
|
||||
>
|
||||
<option value="">Standard (leer)</option>
|
||||
{planTemplates.map((t) => (
|
||||
<option key={t.id} value={String(t.id)}>
|
||||
{t.name}
|
||||
{typeof t.sections_count === 'number' ? ` (${t.sections_count} Abschn.)` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={!selectedGroupId}
|
||||
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
|
||||
onClick={handleQuickCreate}
|
||||
>
|
||||
Schnell erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!selectedGroupId ? (
|
||||
<div className="card">
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||
Bitte wähle eine Trainingsgruppe aus
|
||||
Wähle oben eine Trainingsgruppe — danach kannst du mit <strong>„Neue Trainingseinheit planen“</strong> starten.
|
||||
</p>
|
||||
</div>
|
||||
) : units.length === 0 ? (
|
||||
<div className="card">
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||
Keine Trainingseinheiten im gewählten Zeitraum
|
||||
Keine Trainingseinheiten in diesem Zeitraum. Nutze oben <strong>„Neue Trainingseinheit planen“</strong> oder{' '}
|
||||
<strong>„Schnell erstellen“</strong>, um den ersten Termin anzulegen.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user