Ajout de fonctionnalités:

Ajout de notes personnels et de notes d'équipe avec historique.
Ajout de prise de rendez-vous avec un coach selon ses disponibilités
This commit is contained in:
cedrick2711
2026-07-16 20:34:56 -04:00
parent b02fe29e30
commit 24fcc9a048
24 changed files with 2625 additions and 264 deletions
+693 -3
View File
@@ -8,9 +8,10 @@ import os
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
from flask_login import login_required, current_user
from extensions import db, hash_password
from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam
from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam, CoachAvailability, TeamNote, PersonalNote, OneOnOneRequest, Match, Team, TeamMember, MatchParticipant, Tryout
from werkzeug.utils import secure_filename
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date as date_type
import requests
users_bp = Blueprint('users', __name__, url_prefix='/users')
@@ -716,4 +717,693 @@ def download_signed_contract(contract_id):
flash('No signed contract available.', 'danger')
return redirect(url_for('users.list_contracts'))
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
# One on One Functions
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '')
def send_discord_notification(player_name, points, date_str, start_time_str, end_time_str, team_name, coach_name, coach_discord):
"""Send a Discord webhook notification for a One on One request.
Args:
player_name: Name of the player making the request.
points: Discussion points from the player.
date_str: Date of the requested session.
start_time_str: Start time of the requested session.
end_time_str: End time of the requested session.
team_name: Name of the player's team.
coach_name: Name of the coach.
coach_discord: Coach's Discord username.
"""
if not DISCORD_WEBHOOK_URL:
return # No webhook configured, skip notification
embed = {
"embeds": [{
"title": "One on One Request",
"color": 3447003, # Blue color
"fields": [
{"name": "Player", "value": player_name, "inline": True},
{"name": "Team", "value": team_name or "Unknown Team", "inline": True},
{"name": "Date", "value": date_str, "inline": True},
{"name": "Time", "value": f"{start_time_str} - {end_time_str}", "inline": True},
{"name": "Discussion Points", "value": points or "No specific points provided", "inline": False}
],
"footer": {
"text": f"Coach: {coach_name}" + (f" (Discord: {coach_discord})" if coach_discord else "")
}
}]
}
try:
requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5)
except Exception:
pass # Silently fail if webhook doesn't work
@users_bp.route('/one-on-one', methods=['GET', 'POST'])
@login_required
def one_on_one():
"""One on One request page for players.
Players can view team notes, personal notes, and request One on One sessions.
Returns:
Response: Rendered One on One page.
"""
if current_user.role != 'player':
flash('Only players can request One on One sessions.', 'danger')
return redirect(url_for('main.dashboard'))
# Get player's team and coach
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None
coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None
if not coach:
flash('You do not have a coach assigned to your team.', 'info')
# Get team notes for this player's team
team_notes = []
if org_team:
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
# Get personal notes for this player
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
# Get coach's availability for the next 7 days
coach_availability = []
if coach:
coach_availability = CoachAvailability.query.filter_by(coach_id=coach.id).all()
if request.method == 'POST':
# Handle One on One request submission
date_str = request.form.get('date')
start_time_str = request.form.get('start_time')
end_time_str = request.form.get('end_time')
points = request.form.get('points', '').strip()
if not coach:
flash('Cannot request One on One - no coach assigned.', 'danger')
return redirect(url_for('users.one_on_one'))
# Validate date and time
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
start_time = datetime.strptime(start_time_str, '%H:%M').time()
end_time = datetime.strptime(end_time_str, '%H:%M').time()
except (ValueError, TypeError):
flash('Invalid date or time format.', 'danger')
return redirect(url_for('users.one_on_one'))
# Check if the requested slot is within coach's availability
date_parts = date_str.split('-')
check_date = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2]))
# Python weekday: Monday=0, Sunday=6
day_of_week = check_date.weekday()
is_available = any(
av.day_of_week == day_of_week and
av.start_time <= start_time and
av.end_time >= end_time
for av in coach_availability
)
if not is_available:
flash('The requested time is not within the coach\'s availability.', 'danger')
return redirect(url_for('users.one_on_one'))
# Create the request
request_obj = OneOnOneRequest(
player_id=current_user.id,
coach_id=coach.id,
org_team_id=org_team.id if org_team else None,
date=date_obj,
start_time=start_time,
end_time=end_time,
points=points if points else None
)
db.session.add(request_obj)
db.session.commit()
# Send Discord notification
send_discord_notification(
player_name=current_user.full_name,
points=points,
date_str=date_str,
start_time_str=start_time_str,
end_time_str=end_time_str,
team_name=org_team.name if org_team else None,
coach_name=coach.full_name,
coach_discord=coach.discord_username
)
flash('One on One request sent to your coach!', 'success')
return redirect(url_for('users.one_on_one'))
# Calculate dates for the next week (starting from today)
dates = []
for i in range(7):
d = date_type.today() + timedelta(days=i)
dates.append({
'value': d.strftime('%Y-%m-%d'),
'display': d.strftime('%A, %b %d'),
'day_of_week': d.weekday()
})
# Serialize coach availability for JavaScript
coach_availability_serialized = [
{
'id': av.id,
'day_of_week': av.day_of_week,
'start_time': av.start_time.strftime('%H:%M'),
'end_time': av.end_time.strftime('%H:%M')
}
for av in coach_availability
]
return render_template('pages/one_on_one.html',
org_team=org_team,
coach=coach,
team_notes=team_notes,
personal_notes=personal_notes,
coach_availability=coach_availability_serialized,
dates=dates)
@users_bp.route('/coach-availability', methods=['GET', 'POST'])
@login_required
def manage_coach_availability():
"""Manage coach availability (for coaches).
GET: Render the availability management page.
POST: Add availability slots via bulk.
Returns:
Response: Rendered management page or redirect.
"""
if current_user.role != 'coach':
flash('Only coaches can manage availability.', 'danger')
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
data = request.get_json()
slots = data.get('slots', [])
created = []
for slot in slots:
day_of_week = slot.get('day_of_week')
start_time_str = slot.get('start_time')
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
continue
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
except (ValueError, TypeError):
continue
end_time = add_30_minutes(start_time)
# Check if this slot already exists for this coach
existing = CoachAvailability.query.filter_by(
coach_id=current_user.id,
day_of_week=day_of_week,
start_time=start_time
).first()
if not existing:
availability = CoachAvailability(
coach_id=current_user.id,
day_of_week=day_of_week,
start_time=start_time,
end_time=end_time
)
db.session.add(availability)
db.session.flush()
created.append({
'id': availability.id,
'day_of_week': availability.day_of_week,
'day_name': DAY_NAMES[availability.day_of_week],
'start_time': availability.start_time.strftime('%H:%M'),
'end_time': availability.end_time.strftime('%H:%M')
})
db.session.commit()
return jsonify({'success': True, 'created': created})
# Get existing availability
existing_availability = CoachAvailability.query.filter_by(coach_id=current_user.id).all()
return render_template('pages/coach_availability.html',
existing_availability=existing_availability)
@users_bp.route('/coach-availability/clear', methods=['POST'])
@login_required
def clear_coach_availability():
"""Clear all coach availability slots.
Returns:
Response: JSON with success status.
"""
if current_user.role != 'coach':
return jsonify({'error': 'Unauthorized'}), 403
CoachAvailability.query.filter_by(coach_id=current_user.id).delete()
db.session.commit()
return jsonify({'success': True})
@users_bp.route('/coach-availability/<int:availability_id>/delete', methods=['POST'])
@login_required
def delete_coach_availability(availability_id):
"""Delete a coach availability slot.
Args:
availability_id: The ID of the availability slot to delete.
Returns:
Response: JSON with success status or error.
"""
if current_user.role != 'coach':
return jsonify({'error': 'Unauthorized'}), 403
availability = CoachAvailability.query.get_or_404(availability_id)
if availability.coach_id != current_user.id:
return jsonify({'error': 'Unauthorized'}), 403
db.session.delete(availability)
db.session.commit()
return jsonify({'success': True})
@users_bp.route('/api/coach-availability/<int:coach_id>')
@login_required
def api_get_coach_availability(coach_id):
"""API endpoint to get coach availability.
Args:
coach_id: The ID of the coach.
Returns:
Response: JSON with availability data.
"""
if current_user.role != 'player':
return jsonify({'error': 'Unauthorized'}), 403
availability = CoachAvailability.query.filter_by(coach_id=coach_id).all()
result = {}
for av in availability:
if av.day_of_week not in result:
result[av.day_of_week] = []
result[av.day_of_week].append({
'id': av.id,
'start_time': av.start_time.strftime('%H:%M'),
'end_time': av.end_time.strftime('%H:%M')
})
return jsonify(result)
@users_bp.route('/team-notes', methods=['GET', 'POST'])
@login_required
def manage_team_notes():
"""Manage team improvement notes (for coaches).
GET: Render the team notes management page.
POST: Create or update team notes.
Returns:
Response: Rendered management page or redirect.
"""
if current_user.role != 'coach':
flash('Only coaches can manage team notes.', 'danger')
return redirect(url_for('main.dashboard'))
# Get the coach's team
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if not org_team:
flash('You are not assigned to coach any team.', 'danger')
return redirect(url_for('main.dashboard'))
# Get existing team notes
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
if request.method == 'POST':
content = request.form.get('content', '').strip()
if content:
# Create new team note entry (keeps history)
note = TeamNote(
org_team_id=org_team.id,
coach_id=current_user.id,
content=content
)
db.session.add(note)
db.session.commit()
flash('Team notes added successfully!', 'success')
return redirect(url_for('users.manage_team_notes'))
return render_template('pages/team_notes.html',
org_team=org_team,
team_notes=team_notes)
@users_bp.route('/personal-notes', methods=['GET', 'POST'])
@login_required
def manage_personal_notes():
"""Manage personal notes for players (for coaches).
GET: Render the personal notes management page.
POST: Create a personal note for a player.
Returns:
Response: Rendered management page or redirect.
"""
if current_user.role != 'coach':
flash('Only coaches can manage personal notes.', 'danger')
return redirect(url_for('main.dashboard'))
# Get players on the coach's team
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
players = []
if org_team:
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all()
if request.method == 'POST':
player_id = request.form.get('player_id', type=int)
content = request.form.get('content', '').strip()
if not player_id or not content:
flash('Please select a player and enter note content.', 'danger')
return redirect(url_for('users.manage_personal_notes'))
# Verify player is on coach's team
if org_team and player_id not in [p.id for p in players]:
flash('You can only add notes for players on your team.', 'danger')
return redirect(url_for('users.manage_personal_notes'))
note = PersonalNote(
player_id=player_id,
coach_id=current_user.id,
content=content
)
db.session.add(note)
db.session.commit()
flash('Personal note added successfully!', 'success')
return redirect(url_for('users.manage_personal_notes'))
# Get all personal notes for players on this team
personal_notes = []
if org_team:
personal_notes = PersonalNote.query.filter(
PersonalNote.player_id.in_([p.id for p in players])
).order_by(PersonalNote.created_at.desc()).all()
return render_template('pages/personal_notes.html',
players=players,
personal_notes=personal_notes,
org_team=org_team)
# Note source types for filtering
NOTE_SOURCE_MATCH = 'match'
NOTE_SOURCE_TRYOUT = 'tryout'
NOTE_SOURCE_TEAM = 'team'
@users_bp.route('/my-notes')
@login_required
def my_notes():
"""View all notes for the current player.
Shows both personal notes and team notes that the player has received.
Personal notes are grouped by source (match, tryout, team).
Returns:
Response: Rendered my notes template.
"""
if current_user.role != 'player':
flash('Only players can view their notes.', 'danger')
return redirect(url_for('main.dashboard'))
# Get player's team and coach
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None
# Get all personal notes for this player with eager loading for relationships
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
# Get team notes for this player's team
team_notes = []
if org_team:
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
return render_template('pages/player_personal_notes.html',
org_team=org_team,
personal_notes=personal_notes,
team_notes=team_notes)
@users_bp.route('/notes/add', methods=['GET', 'POST'])
@login_required
def add_personal_note():
"""Add a personal note with optional context (for coaches and managers).
GET: Render the note creation form.
POST: Create a personal note, optionally linked to match/tryout/team.
Returns:
Response: Create form or redirect to notes view.
"""
if current_user.role not in ['coach', 'manager', 'president']:
flash('Only coaches and managers can add notes.', 'danger')
return redirect(url_for('main.dashboard'))
# Get players this user can manage
players = []
org_team = None
if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team:
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all()
elif current_user.role in ['president', 'manager']:
players = User.query.filter_by(role='player').order_by(User.full_name).all()
# Get available matches and tryouts for context
matches = []
tryouts = []
teams = []
if current_user.role == 'coach' and org_team:
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
matches = Match.query.join(Tryout).filter(Tryout.target_org_team_id == org_team.id).order_by(Match.date.desc()).all()
elif current_user.role in ['president', 'manager']:
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
matches = Match.query.order_by(Match.date.desc()).all()
teams = Team.query.order_by(Team.name).all()
if request.method == 'POST':
player_id = request.form.get('player_id', type=int)
content = request.form.get('content', '').strip()
match_id = request.form.get('match_id', type=int)
tryout_id = request.form.get('tryout_id', type=int)
team_id = request.form.get('team_id', type=int)
if not player_id or not content:
flash('Please select a player and enter note content.', 'danger')
return redirect(url_for('users.add_personal_note'))
# Verify player is on coach's team (if coach)
if current_user.role == 'coach' and org_team and player_id not in [p.id for p in players]:
flash('You can only add notes for players on your team.', 'danger')
return redirect(url_for('users.add_personal_note'))
# Validate context - ensure coach can access the match/tryout/team
if match_id:
match = Match.query.get(match_id)
match_tryout = None
if match:
match_tryout = Tryout.query.get(match.tryout_id)
if match_tryout and match_tryout.target_org_team_id and match_tryout.target_org_team_id != org_team.id:
flash('You can only add notes for matches in your team\'s tryouts.', 'danger')
return redirect(url_for('users.add_personal_note'))
if tryout_id and org_team:
tryout = Tryout.query.get(tryout_id)
if tryout and tryout.target_org_team_id and tryout.target_org_team_id != org_team.id:
flash('You can only add notes for your team\'s tryouts.', 'danger')
return redirect(url_for('users.add_personal_note'))
if team_id and org_team:
team = Team.query.get(team_id)
if team:
tryout = Tryout.query.get(team.tryout_id)
if tryout and tryout.target_org_team_id and tryout.target_org_team_id != org_team.id:
flash('You can only add notes for your team\'s tryouts.', 'danger')
return redirect(url_for('users.add_personal_note'))
note = PersonalNote(
player_id=player_id,
coach_id=current_user.id,
content=content,
match_id=match_id if match_id else None,
team_id=team_id if team_id else None,
tryout_id=tryout_id if tryout_id else None
)
db.session.add(note)
db.session.commit()
flash('Personal note added successfully!', 'success')
return redirect(url_for('users.my_notes'))
return render_template('pages/add_personal_note.html',
players=players,
matches=matches,
tryouts=tryouts,
teams=teams,
org_team=org_team)
@users_bp.route('/match/<int:match_id>/add-note', methods=['GET', 'POST'])
@login_required
def add_note_from_match(match_id):
"""Add a personal note from a match view context (for coaches).
GET: Render the note creation form pre-filled with match info.
POST: Create a personal note linked to the match.
Args:
match_id: The ID of the match to create note for.
Returns:
Response: Create form or redirect.
"""
if current_user.role not in ['coach', 'manager', 'president']:
flash('Only coaches can add notes from matches.', 'danger')
return redirect(url_for('main.dashboard'))
match = Match.query.get_or_404(match_id)
tryout = Tryout.query.get(match.tryout_id)
# Check permissions
if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if not org_team or (tryout.target_org_team_id and tryout.target_org_team_id != org_team.id):
flash('You do not have permission for this match.', 'danger')
return redirect(url_for('matches.calendar'))
# Get all participants for this match
participant_ids = []
if match.match_type == 'team_vs_team':
if match.team1_id:
team_members = TeamMember.query.filter_by(team_id=match.team1_id).all()
participant_ids.extend([m.player_id for m in team_members])
if match.team2_id:
team_members = TeamMember.query.filter_by(team_id=match.team2_id).all()
participant_ids.extend([m.player_id for m in team_members])
else:
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
participant_ids = [p.player_id for p in participants]
players = User.query.filter(User.id.in_(participant_ids)).order_by(User.full_name).all() if participant_ids else []
# Get team notes for context
team_notes = []
if tryout and tryout.target_org_team_id:
team_notes = TeamNote.query.filter_by(org_team_id=tryout.target_org_team_id).order_by(TeamNote.created_at.desc()).all()
if request.method == 'POST':
player_id = request.form.get('player_id', type=int)
content = request.form.get('content', '').strip()
if not player_id or not content:
flash('Please select a player and enter note content.', 'danger')
elif player_id not in participant_ids:
flash('Selected player is not in this match.', 'danger')
else:
note = PersonalNote(
player_id=player_id,
coach_id=current_user.id,
content=content,
match_id=match_id
)
db.session.add(note)
db.session.commit()
flash('Personal note added successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return render_template('pages/add_note_from_match.html',
match=match,
tryout=tryout,
players=players,
team_notes=team_notes)
@users_bp.route('/tryout/<int:tryout_id>/add-note', methods=['GET', 'POST'])
@login_required
def add_note_from_tryout(tryout_id):
"""Add a personal note from a tryout view context (for coaches).
GET: Render the note creation form pre-filled with tryout info.
POST: Create a personal note linked to the tryout.
Args:
tryout_id: The ID of the tryout to create note for.
Returns:
Response: Create form or redirect.
"""
if current_user.role not in ['coach', 'manager', 'president']:
flash('Only coaches can add notes from tryouts.', 'danger')
return redirect(url_for('main.dashboard'))
tryout = Tryout.query.get_or_404(tryout_id)
# Check permissions
if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if not org_team or (tryout.target_org_team_id and tryout.target_org_team_id != org_team.id):
flash('You do not have permission for this tryout.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
# Get all registered players in this tryout
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
player_ids = [r.player_id for r in registrations]
# If coach, filter to only their team players
if current_user.role == 'coach' and org_team:
players = User.query.filter(User.id.in_(player_ids), User.team_id == org_team.id).order_by(User.full_name).all()
else:
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all()
# Get team notes for context
team_notes = []
if tryout.target_org_team_id:
team_notes = TeamNote.query.filter_by(org_team_id=tryout.target_org_team_id).order_by(TeamNote.created_at.desc()).all()
if request.method == 'POST':
player_id = request.form.get('player_id', type=int)
content = request.form.get('content', '').strip()
if not player_id or not content:
flash('Please select a player and enter note content.', 'danger')
elif player_id not in [p.id for p in players]:
flash('Selected player is not registered for this tryout.', 'danger')
else:
note = PersonalNote(
player_id=player_id,
coach_id=current_user.id,
content=content,
tryout_id=tryout_id
)
db.session.add(note)
db.session.commit()
flash('Personal note added successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return render_template('pages/add_note_from_tryout.html',
tryout=tryout,
players=players,
team_notes=team_notes)