Ajout du statut remplaçant pour les joueurs
Possibilité d'ajouter/enlever coach et manager des équipes
les joueurs peuvent être associés à plusieurs équipes au lieu d'une seule
This commit is contained in:
cedrick2711
2026-07-20 17:41:22 -04:00
parent 027e529885
commit 346229778f
18 changed files with 343 additions and 69 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+58 -4
View File
@@ -65,7 +65,6 @@ class User(UserMixin, db.Model):
full_name: User's display name. full_name: User's display name.
email: User's email address. email: User's email address.
phone: Optional phone number. phone: Optional phone number.
team_id: Foreign key to the user's organization team.
is_active_account: Whether the account is active. is_active_account: Whether the account is active.
games: Comma-separated list of games the user plays. games: Comma-separated list of games the user plays.
discord_username: User's Discord handle. discord_username: User's Discord handle.
@@ -79,7 +78,6 @@ class User(UserMixin, db.Model):
full_name = db.Column(db.String(100), nullable=False) full_name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False)
phone = db.Column(db.String(20), nullable=True) phone = db.Column(db.String(20), nullable=True)
team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
is_active_account = db.Column(db.Boolean, default=True) is_active_account = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
@@ -205,6 +203,14 @@ class User(UserMixin, db.Model):
""" """
return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags} return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags}
def get_org_teams(self):
"""Return all organization teams this player belongs to.
Returns:
list: List of OrgTeam objects the player is assigned to.
"""
return [tp.org_team for tp in self.team_placements]
# Platform options for games that require platform specification # Platform options for games that require platform specification
GAME_PLATFORMS = { GAME_PLATFORMS = {
@@ -296,6 +302,35 @@ class UserGamertag(db.Model):
return url return url
class TeamPlayer(db.Model):
"""Many-to-many relationship between players and organization teams.
Allows a player to be in multiple teams with a status of starter or substitute.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
org_team_id: Foreign key to the organization team.
status: Player status on the team ('starter' or 'substitute').
position: Player's position on the team (optional).
added_at: Timestamp when player was added.
"""
__tablename__ = 'team_players'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
status = db.Column(db.String(20), nullable=False, default='starter') # 'starter' or 'substitute'
position = db.Column(db.String(50), nullable=True)
added_at = db.Column(db.DateTime, default=datetime.utcnow)
player = db.relationship('User', foreign_keys=[player_id], backref='team_placements')
org_team = db.relationship('OrgTeam', foreign_keys=[org_team_id], backref='team_players')
__table_args__ = (
db.UniqueConstraint('player_id', 'org_team_id', name='unique_player_org_team'),
)
class OrgTeam(db.Model): class OrgTeam(db.Model):
"""Persistent organization teams (e.g., Varsity, JV) that exist across tryouts. """Persistent organization teams (e.g., Varsity, JV) that exist across tryouts.
@@ -306,6 +341,7 @@ class OrgTeam(db.Model):
id: Unique identifier. id: Unique identifier.
name: Team name (e.g., "Varsity", "Junior Varsity"). name: Team name (e.g., "Varsity", "Junior Varsity").
coach_id: Foreign key to the assigned coach. coach_id: Foreign key to the assigned coach.
manager_id: Foreign key to the assigned manager.
created_by: Foreign key to the user who created the team. created_by: Foreign key to the user who created the team.
created_at: Timestamp of team creation. created_at: Timestamp of team creation.
""" """
@@ -313,12 +349,30 @@ class OrgTeam(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True) name = db.Column(db.String(100), nullable=False, unique=True)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_org_team', uselist=False) coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_org_team', uselist=False)
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_org_team', uselist=False)
creator = db.relationship('User', foreign_keys=[created_by]) creator = db.relationship('User', foreign_keys=[created_by])
players = db.relationship('User', foreign_keys='User.team_id', backref='org_team', lazy='dynamic')
@property
def players(self):
"""Get all players assigned to this team.
Returns:
list: List of User objects assigned to this team.
"""
return [tp.player for tp in self.team_players]
def get_players_with_status(self):
"""Get all players with their status on this team.
Returns:
list: List of dicts with 'player' and 'status' keys.
"""
return [{'player': tp.player, 'status': tp.status, 'position': tp.position} for tp in self.team_players]
class Tryout(db.Model): class Tryout(db.Model):
@@ -645,7 +699,7 @@ class Contract(db.Model):
# Manager can view contracts for players on their teams # Manager can view contracts for players on their teams
if user.role == 'manager': if user.role == 'manager':
player = User.query.get(self.player_id) player = User.query.get(self.player_id)
if player and player.team_id: if player and player.get_org_teams():
return True return True
# Coach can view contracts for players on their team # Coach can view contracts for players on their team
if user.role == 'coach': if user.role == 'coach':
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -6,7 +6,7 @@ This module handles player evaluation creation, management, and viewing.
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
from models import User, Tryout, Evaluation, TryoutRegistration from models import User, Tryout, Evaluation, TryoutRegistration, GAME_POSITIONS
from sqlalchemy import func from sqlalchemy import func
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations') evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
@@ -178,7 +178,8 @@ def evaluate_player(tryout_id, player_id):
tryout=tryout, tryout=tryout,
player=player, player=player,
existing_eval=existing_eval, existing_eval=existing_eval,
evaluators=evaluators) evaluators=evaluators,
game_positions=GAME_POSITIONS)
@evaluations_bp.route('/<int:tryout_id>/players') @evaluations_bp.route('/<int:tryout_id>/players')
+108 -14
View File
@@ -3,10 +3,10 @@
This module handles CRUD operations for organization teams and player assignments. This module handles CRUD operations for organization teams and player assignments.
""" """
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
from models import OrgTeam, User, Team, TeamMember, PersonalNote, TeamNote, Tryout from models import OrgTeam, User, Team, TeamMember, PersonalNote, TeamNote, Tryout, TeamPlayer
teams_bp = Blueprint('teams', __name__, url_prefix='/teams') teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@@ -35,8 +35,9 @@ def list_teams():
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
coaches = User.query.filter_by(role='coach').order_by(User.full_name).all() coaches = User.query.filter_by(role='coach').order_by(User.full_name).all()
managers = User.query.filter_by(role='manager').order_by(User.full_name).all()
all_players = User.query.filter_by(role='player').order_by(User.full_name).all() all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
return render_template('pages/teams.html', teams=teams, coaches=coaches, all_players=all_players, can_manage=can_manage) return render_template('pages/teams.html', teams=teams, coaches=coaches, managers=managers, all_players=all_players, can_manage=can_manage)
@teams_bp.route('/create', methods=['POST']) @teams_bp.route('/create', methods=['POST'])
@@ -47,6 +48,7 @@ def create_team():
Args: Args:
name: Team name from form. name: Team name from form.
coach_id: Optional coach assignment from form. coach_id: Optional coach assignment from form.
manager_id: Optional manager assignment from form.
Returns: Returns:
Response: Redirect to teams list with status message. Response: Redirect to teams list with status message.
@@ -57,6 +59,7 @@ def create_team():
name = request.form.get('name') name = request.form.get('name')
coach_id = request.form.get('coach_id') coach_id = request.form.get('coach_id')
manager_id = request.form.get('manager_id')
if not name: if not name:
flash('Team name is required.', 'danger') flash('Team name is required.', 'danger')
@@ -70,6 +73,7 @@ def create_team():
team = OrgTeam( team = OrgTeam(
name=name, name=name,
coach_id=int(coach_id) if coach_id else None, coach_id=int(coach_id) if coach_id else None,
manager_id=int(manager_id) if manager_id else None,
created_by=current_user.id created_by=current_user.id
) )
db.session.add(team) db.session.add(team)
@@ -87,6 +91,7 @@ def edit_team(team_id):
team_id: The ID of the team to edit. team_id: The ID of the team to edit.
name: New team name from form. name: New team name from form.
coach_id: New coach assignment from form. coach_id: New coach assignment from form.
manager_id: New manager assignment from form.
Returns: Returns:
Response: Redirect to teams list with status message. Response: Redirect to teams list with status message.
@@ -98,6 +103,7 @@ def edit_team(team_id):
name = request.form.get('name') name = request.form.get('name')
coach_id = request.form.get('coach_id') coach_id = request.form.get('coach_id')
manager_id = request.form.get('manager_id')
if not name: if not name:
flash('Team name is required.', 'danger') flash('Team name is required.', 'danger')
@@ -110,6 +116,7 @@ def edit_team(team_id):
team.name = name team.name = name
team.coach_id = int(coach_id) if coach_id else None team.coach_id = int(coach_id) if coach_id else None
team.manager_id = int(manager_id) if manager_id else None
db.session.commit() db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success') flash(f'Team "{name}" updated successfully!', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@@ -144,22 +151,71 @@ def delete_team(team_id):
t.target_org_team_id = None t.target_org_team_id = None
db.session.commit() db.session.commit()
# Remove all team_players associations
TeamPlayer.query.filter_by(org_team_id=team_id).delete()
db.session.commit()
db.session.delete(team) db.session.delete(team)
db.session.commit() db.session.commit()
flash(f'Team "{name}" deleted successfully.', 'success') flash(f'Team "{name}" deleted successfully.', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_coach', methods=['POST'])
@login_required
def remove_coach(team_id):
"""Remove the coach from an organization team.
Args:
team_id: The ID of the team.
Returns:
Response: Redirect to teams list with status message.
"""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
team.coach_id = None
db.session.commit()
flash(f'Coach removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_manager', methods=['POST'])
@login_required
def remove_manager(team_id):
"""Remove the manager from an organization team.
Args:
team_id: The ID of the team.
Returns:
Response: Redirect to teams list with status message.
"""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
team.manager_id = None
db.session.commit()
flash(f'Manager removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add_player', methods=['POST']) @teams_bp.route('/<int:team_id>/add_player', methods=['POST'])
@login_required @login_required
def add_player(team_id): def add_player(team_id):
"""Add a player to an organization team. """Add a player to an organization team.
If player is already on another team, they will be moved. Players can now be in multiple teams.
Args: Args:
team_id: The ID of the team to add the player to. team_id: The ID of the team to add the player to.
player_id: The ID of the player to add. player_id: The ID of the player to add.
status: The player's status (starter or substitute).
Returns: Returns:
Response: Redirect to teams list with status message. Response: Redirect to teams list with status message.
@@ -169,7 +225,7 @@ def add_player(team_id):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
player_id = request.form.get('player_id') player_id = request.form.get('player_id')
position = request.form.get('position', '') status = request.form.get('status', 'starter')
if not player_id: if not player_id:
flash('Please select a player.', 'danger') flash('Please select a player.', 'danger')
@@ -180,15 +236,19 @@ def add_player(team_id):
flash('Can only assign players to teams.', 'danger') flash('Can only assign players to teams.', 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
# If player already belongs to a different team, remove them first # Check if player is already on this team
if player.team_id and player.team_id != team_id: existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first()
old_team = OrgTeam.query.get(player.team_id) if existing:
flash(f'{player.full_name} was moved from {old_team.name} to {team.name}.', 'info')
elif player.team_id == team_id:
flash(f'{player.full_name} is already on {team.name}.', 'info') flash(f'{player.full_name} is already on {team.name}.', 'info')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
player.team_id = team_id # Add player to the team using TeamPlayer model (allows multiple teams)
tp = TeamPlayer(
player_id=player.id,
org_team_id=team.id,
status=status
)
db.session.add(tp)
db.session.commit() db.session.commit()
flash(f'{player.full_name} added to {team.name}!', 'success') flash(f'{player.full_name} added to {team.name}!', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@@ -213,16 +273,49 @@ def remove_player(team_id, player_id):
player = User.query.get_or_404(player_id) player = User.query.get_or_404(player_id)
if player.team_id != team_id: tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
if not tp:
flash(f'{player.full_name} is not on {team.name}.', 'danger') flash(f'{player.full_name} is not on {team.name}.', 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
player.team_id = None db.session.delete(tp)
db.session.commit() db.session.commit()
flash(f'{player.full_name} removed from {team.name}.', 'success') flash(f'{player.full_name} removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/toggle_status/<int:player_id>', methods=['POST'])
@login_required
def toggle_player_status(team_id, player_id):
"""Toggle a player's status between starter and substitute.
Args:
team_id: The ID of the team.
player_id: The ID of the player.
Returns:
Response: JSON with new status.
"""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
return jsonify({'error': 'Permission denied'}), 403
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
if not tp:
return jsonify({'error': 'Player not found on this team'}), 404
# Toggle status
tp.status = 'substitute' if tp.status == 'starter' else 'starter'
db.session.commit()
return jsonify({
'success': True,
'player_id': player_id,
'new_status': tp.status,
'player_name': tp.player.full_name
})
# Note actions from team page # Note actions from team page
@@ -284,7 +377,8 @@ def add_player_note(team_id, player_id):
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
# Verify player belongs to this team # Verify player belongs to this team
if player.team_id != team_id: tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
if not tp:
flash(f'{player.full_name} is not on {team.name}.', 'danger') flash(f'{player.full_name} is not on {team.name}.', 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
+29 -16
View File
@@ -8,7 +8,7 @@ import os
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db, hash_password from extensions import db, hash_password
from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam, CoachAvailability, TeamNote, PersonalNote, OneOnOneRequest, Match, Team, TeamMember, MatchParticipant, Tryout, TryoutRegistration from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam, CoachAvailability, TeamNote, PersonalNote, OneOnOneRequest, Match, Team, TeamMember, MatchParticipant, Tryout, TryoutRegistration, TeamPlayer
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from datetime import datetime, timedelta, date as date_type from datetime import datetime, timedelta, date as date_type
import requests import requests
@@ -514,8 +514,8 @@ def can_manage_player_contract(user, player_id):
if user.role == 'coach': if user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=user.id).first() org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
if org_team: if org_team:
player = User.query.get(player_id) tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=org_team.id).first()
if player and player.team_id == org_team.id: if tp:
return True return True
return False return False
@@ -542,7 +542,8 @@ def list_contracts():
if current_user.role == 'coach': if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team: if org_team:
players = User.query.filter_by(role='player', team_id=org_team.id).all() player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else []
else: # president or manager else: # president or manager
players = User.query.filter_by(role='player').all() players = User.query.filter_by(role='player').all()
@@ -571,7 +572,11 @@ def upload_contract():
# Get players this user can manage # Get players this user can manage
if current_user.role == 'coach': if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
players = User.query.filter_by(role='player', team_id=org_team.id).all() if org_team else [] if org_team:
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else []
else:
players = []
else: else:
players = User.query.filter_by(role='player').all() players = User.query.filter_by(role='player').all()
@@ -598,7 +603,8 @@ def upload_contract():
# Get player info for folder structure # Get player info for folder structure
player = User.query.get_or_404(player_id) player = User.query.get_or_404(player_id)
team = OrgTeam.query.get(player.team_id) if player.team_id else None player_teams = player.get_org_teams()
team = player_teams[0] if player_teams else None
# Create team folder if team exists # Create team folder if team exists
if team: if team:
@@ -621,7 +627,7 @@ def upload_contract():
# Create database record # Create database record
contract = Contract( contract = Contract(
player_id=player_id, player_id=player_id,
team_id=player.team_id, team_id=team.id if team else None,
uploaded_by_id=current_user.id, uploaded_by_id=current_user.id,
original_filename=original_filename, original_filename=original_filename,
stored_filename=stored_filename, stored_filename=stored_filename,
@@ -822,8 +828,9 @@ def one_on_one():
flash('Only players can request One on One sessions.', 'danger') flash('Only players can request One on One sessions.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
# Get player's team and coach # Get player's teams and coach (use the first team the player is on)
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None org_teams = current_user.get_org_teams()
org_team = org_teams[0] if org_teams else None
coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None
if not coach: if not coach:
@@ -1062,7 +1069,8 @@ def api_get_coach_availability(coach_id):
return jsonify({'error': 'Unauthorized'}), 403 return jsonify({'error': 'Unauthorized'}), 403
# Players can only view their own coach's availability # Players can only view their own coach's availability
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None player_teams = current_user.get_org_teams()
org_team = player_teams[0] if player_teams else None
if org_team and org_team.coach_id != coach_id: if org_team and org_team.coach_id != coach_id:
return jsonify({'error': 'Unauthorized'}), 403 return jsonify({'error': 'Unauthorized'}), 403
@@ -1144,7 +1152,8 @@ def manage_personal_notes():
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
players = [] players = []
if org_team: if org_team:
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all() player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() if player_ids else []
if request.method == 'POST': if request.method == 'POST':
player_id = request.form.get('player_id', type=int) player_id = request.form.get('player_id', type=int)
@@ -1200,7 +1209,8 @@ def notes_dashboard():
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
players = [] players = []
if org_team: if org_team:
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all() player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() if player_ids else []
# Get existing team notes # Get existing team notes
team_notes = [] team_notes = []
@@ -1257,8 +1267,9 @@ def my_notes():
flash('Only players can view their notes.', 'danger') flash('Only players can view their notes.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
# Get player's team and coach # Get player's teams and coach (use the first team the player is on)
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None player_teams = current_user.get_org_teams()
org_team = player_teams[0] if player_teams else None
# Get all personal notes for this player with eager loading for relationships # 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() personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
@@ -1295,7 +1306,8 @@ def add_personal_note():
if current_user.role == 'coach': if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team: if org_team:
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all() player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() if player_ids else []
elif current_user.role in ['president', 'manager']: elif current_user.role in ['president', 'manager']:
players = User.query.filter_by(role='player').order_by(User.full_name).all() players = User.query.filter_by(role='player').order_by(User.full_name).all()
@@ -1484,7 +1496,8 @@ def add_note_from_tryout(tryout_id):
# If coach, filter to only their team players; otherwise include all # If coach, filter to only their team players; otherwise include all
if org_team: if org_team:
players = User.query.filter(User.id.in_(player_ids), User.team_id == org_team.id).order_by(User.full_name).all() team_player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids), User.id.in_(team_player_ids)).order_by(User.full_name).all()
else: else:
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all()
+14 -12
View File
@@ -6,7 +6,7 @@ users, tryouts, teams, evaluations, and player disponibilities.
from sqlalchemy import text from sqlalchemy import text
from extensions import db, hash_password from extensions import db, hash_password
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag, CoachAvailability, TeamNote, PersonalNote, Match, MatchParticipant from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag, CoachAvailability, TeamNote, PersonalNote, Match, MatchParticipant, TeamPlayer
from datetime import datetime, timedelta, time from datetime import datetime, timedelta, time
import random import random
@@ -30,6 +30,7 @@ def seed_database():
db.session.execute(text('DELETE FROM player_disponibilities')) db.session.execute(text('DELETE FROM player_disponibilities'))
db.session.execute(text('DELETE FROM match_participants')) db.session.execute(text('DELETE FROM match_participants'))
db.session.execute(text('DELETE FROM matches')) db.session.execute(text('DELETE FROM matches'))
db.session.execute(text('DELETE FROM team_players'))
db.session.execute(text('DELETE FROM team_members')) db.session.execute(text('DELETE FROM team_members'))
db.session.execute(text('DELETE FROM teams')) db.session.execute(text('DELETE FROM teams'))
db.session.execute(text('DELETE FROM evaluations')) db.session.execute(text('DELETE FROM evaluations'))
@@ -323,19 +324,20 @@ def seed_database():
db.session.commit() db.session.commit()
print("[OK] Created 2 tryout-specific teams with player assignments") print("[OK] Created 2 tryout-specific teams with player assignments")
# Assign players to organization teams # Assign players to organization teams (using TeamPlayer for many-to-many)
org_team_assignments = [ org_team_assignments = [
(org_teams[0], players[0]), (org_teams[0], players[0], 'starter'),
(org_teams[0], players[1]), (org_teams[0], players[1], 'starter'),
(org_teams[0], players[2]), (org_teams[0], players[2], 'substitute'),
(org_teams[1], players[3]), (org_teams[1], players[3], 'starter'),
(org_teams[1], players[4]), (org_teams[1], players[4], 'starter'),
(org_teams[1], players[5]), (org_teams[1], players[5], 'substitute'),
(org_teams[2], players[6]), (org_teams[2], players[6], 'starter'),
(org_teams[2], players[7]), (org_teams[2], players[7], 'substitute'),
] ]
for org_team, player in org_team_assignments: for org_team, player, status in org_team_assignments:
player.team_id = org_team.id tp = TeamPlayer(player_id=player.id, org_team_id=org_team.id, status=status)
db.session.add(tp)
db.session.commit() db.session.commit()
# Create sample disponibilities for players # Create sample disponibilities for players
+1 -1
View File
@@ -100,7 +100,7 @@
</div> </div>
</div> </div>
{% set positions = GAME_POSITIONS.get(tryout.game, []) %} {% set positions = game_positions.get(tryout.game, []) %}
<div class="form-row"> <div class="form-row">
<div class="form-group col-12"> <div class="form-group col-12">
<label for="position_recommendation">Recommended Position</label> <label for="position_recommendation">Recommended Position</label>
+129 -19
View File
@@ -21,11 +21,11 @@
<form method="POST" action="{{ url_for('teams.create_team') }}" class="form"> <form method="POST" action="{{ url_for('teams.create_team') }}" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-row"> <div class="form-row">
<div class="form-group col-6"> <div class="form-group col-4">
<label for="name">Team Name</label> <label for="name">Team Name</label>
<input type="text" id="name" name="name" placeholder="e.g., Varsity, JV, U14" required> <input type="text" id="name" name="name" placeholder="e.g., Varsity, JV, U14" required>
</div> </div>
<div class="form-group col-6"> <div class="form-group col-4">
<label for="coach_id">Assigned Coach</label> <label for="coach_id">Assigned Coach</label>
<select id="coach_id" name="coach_id" class="form-select"> <select id="coach_id" name="coach_id" class="form-select">
<option value="">-- No coach assigned --</option> <option value="">-- No coach assigned --</option>
@@ -34,6 +34,15 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-group col-4">
<label for="manager_id">Assigned Manager</label>
<select id="manager_id" name="manager_id" class="form-select">
<option value="">-- No manager assigned --</option>
{% for manager in managers %}
<option value="{{ manager.id }}">{{ manager.full_name }}</option>
{% endfor %}
</select>
</div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="hideCreateForm()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideCreateForm()">Cancel</button>
@@ -50,13 +59,36 @@
<div class="card-header"> <div class="card-header">
<h3><i class="fas fa-users-cog"></i> {{ team.name }}</h3> <h3><i class="fas fa-users-cog"></i> {{ team.name }}</h3>
<div class="card-actions"> <div class="card-actions">
{% if team.coach %} <div class="team-staff">
<span class="text-muted">Coach: {{ team.coach.full_name }}</span> {% if team.coach %}
{% else %} <span class="text-muted">Coach: {{ team.coach.full_name }}</span>
<span class="text-muted">No coach assigned</span> {% if can_manage %}
{% endif %} <form method="POST" action="{{ url_for('teams.remove_coach', team_id=team.id) }}" class="inline-form" onsubmit="return confirm('Remove coach {{ team.coach.full_name }} from {{ team.name }}?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-sm btn-outline-danger" title="Remove Coach">
<i class="fas fa-user-minus"></i>
</button>
</form>
{% endif %}
{% else %}
<span class="text-muted">No coach assigned</span>
{% endif %}
{% if team.manager %}
<span class="text-muted ml-2">Manager: {{ team.manager.full_name }}</span>
{% if can_manage %}
<form method="POST" action="{{ url_for('teams.remove_manager', team_id=team.id) }}" class="inline-form" onsubmit="return confirm('Remove manager {{ team.manager.full_name }} from {{ team.name }}?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-sm btn-outline-danger" title="Remove Manager">
<i class="fas fa-user-minus"></i>
</button>
</form>
{% endif %}
{% else %}
<span class="text-muted ml-2">No manager assigned</span>
{% endif %}
</div>
{% if can_manage %} {% if can_manage %}
<button class="btn btn-sm btn-outline edit-team-btn" data-team-id="{{ team.id }}" data-team-name="{{ team.name }}" data-coach-id="{{ team.coach_id or '' }}"> <button class="btn btn-sm btn-outline edit-team-btn" data-team-id="{{ team.id }}" data-team-name="{{ team.name }}" data-coach-id="{{ team.coach_id or '' }}" data-manager-id="{{ team.manager_id or '' }}">
<i class="fas fa-edit"></i> Edit <i class="fas fa-edit"></i> Edit
</button> </button>
<form method="POST" action="{{ url_for('teams.delete_team', team_id=team.id) }}" class="inline-form" onsubmit="return confirm('Delete team {{ team.name }}? This will unassign it from any linked tryouts.')"> <form method="POST" action="{{ url_for('teams.delete_team', team_id=team.id) }}" class="inline-form" onsubmit="return confirm('Delete team {{ team.name }}? This will unassign it from any linked tryouts.')">
@@ -79,6 +111,7 @@
<thead> <thead>
<tr> <tr>
<th>Player</th> <th>Player</th>
<th>Status</th>
<th>Position / Role</th> <th>Position / Role</th>
<th>Email</th> <th>Email</th>
<th>Phone</th> <th>Phone</th>
@@ -88,20 +121,34 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for player in team.players %} {% for entry in team.get_players_with_status() %}
<tr> <tr>
<td> <td>
<div class="user-mini"> <div class="user-mini">
<div class="avatar-sm">{{ player.full_name[:2] | upper }}</div> <div class="avatar-sm">{{ entry.player.full_name[:2] | upper }}</div>
<span>{{ player.full_name }}</span> <span>{{ entry.player.full_name }}</span>
</div> </div>
</td> </td>
<td>{{ player.role | capitalize }}</td> <td>
<td>{{ player.email }}</td> {% if can_manage_team %}
<td>{{ player.phone or '-' }}</td> <button class="btn btn-sm status-toggle-btn {% if entry.status == 'starter' %}btn-success{% else %}btn-warning{% endif %}"
data-team-id="{{ team.id }}"
data-player-id="{{ entry.player.id }}"
onclick="toggleStatus(this)">
{{ entry.status | capitalize }}
</button>
{% else %}
<span class="badge {% if entry.status == 'starter' %}badge-success{% else %}badge-warning{% endif %}">
{{ entry.status | capitalize }}
</span>
{% endif %}
</td>
<td>{{ entry.player.role | capitalize }}</td>
<td>{{ entry.player.email }}</td>
<td>{{ entry.player.phone or '-' }}</td>
{% if can_manage_team %} {% if can_manage_team %}
<td> <td>
<form method="POST" action="{{ url_for('teams.remove_player', team_id=team.id, player_id=player.id) }}" class="inline-form" onsubmit="return confirm('Remove {{ player.full_name }} from {{ team.name }}?')"> <form method="POST" action="{{ url_for('teams.remove_player', team_id=team.id, player_id=entry.player.id) }}" class="inline-form" onsubmit="return confirm('Remove {{ entry.player.full_name }} from {{ team.name }}?')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-sm btn-danger"> <button type="submit" class="btn btn-sm btn-danger">
<i class="fas fa-user-minus"></i> Remove <i class="fas fa-user-minus"></i> Remove
@@ -112,7 +159,7 @@
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="{% if can_manage_team %}5{% else %}4{% endif %}" class="text-center"> <td colspan="{% if can_manage_team %}6{% else %}5{% endif %}" class="text-center">
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-users-slash"></i> <i class="fas fa-users-slash"></i>
<h4>No players assigned</h4> <h4>No players assigned</h4>
@@ -136,11 +183,15 @@
<select name="player_id" class="form-select" required> <select name="player_id" class="form-select" required>
<option value="">-- Select a player --</option> <option value="">-- Select a player --</option>
{% for p in all_players %} {% for p in all_players %}
{% if p.team_id != team.id %} {% if p.id not in team.players | map(attribute='id') %}
<option value="{{ p.id }}">{{ p.full_name }} {% if p.org_team %}(currently in {{ p.org_team.name }}){% endif %}</option> <option value="{{ p.id }}">{{ p.full_name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
<select name="status" class="form-select ml-2">
<option value="starter">Starter</option>
<option value="substitute">Substitute</option>
</select>
<button type="submit" class="btn btn-sm btn-primary ml-2"> <button type="submit" class="btn btn-sm btn-primary ml-2">
<i class="fas fa-plus"></i> Add to Team <i class="fas fa-plus"></i> Add to Team
</button> </button>
@@ -190,6 +241,15 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-group">
<label for="edit_manager_id">Assigned Manager</label>
<select id="edit_manager_id" name="manager_id" class="form-select">
<option value="">-- No manager assigned --</option>
{% for manager in managers %}
<option value="{{ manager.id }}">{{ manager.full_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="hideEditForm()">Cancel</button> <button type="button" class="btn btn-secondary" onclick="hideEditForm()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button> <button type="submit" class="btn btn-primary">Save Changes</button>
@@ -208,24 +268,56 @@ function hideCreateForm() {
document.getElementById('createTeamForm').classList.add('hidden'); document.getElementById('createTeamForm').classList.add('hidden');
} }
function toggleStatus(btn) {
var teamId = btn.getAttribute('data-team-id');
var playerId = btn.getAttribute('data-player-id');
fetch('/teams/' + teamId + '/toggle_status/' + playerId, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() }}',
'Content-Type': 'application/json'
}
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
btn.textContent = data.new_status.charAt(0).toUpperCase() + data.new_status.slice(1);
if (data.new_status === 'starter') {
btn.classList.remove('btn-warning');
btn.classList.add('btn-success');
} else {
btn.classList.remove('btn-success');
btn.classList.add('btn-warning');
}
}
})
.catch(function(error) {
console.error('Error:', error);
});
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.edit-team-btn').forEach(function(btn) { document.querySelectorAll('.edit-team-btn').forEach(function(btn) {
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var teamId = this.getAttribute('data-team-id'); var teamId = this.getAttribute('data-team-id');
var teamName = this.getAttribute('data-team-name'); var teamName = this.getAttribute('data-team-name');
var coachId = this.getAttribute('data-coach-id'); var coachId = this.getAttribute('data-coach-id');
var managerId = this.getAttribute('data-manager-id');
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit'; document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
document.getElementById('edit_name').value = teamName; document.getElementById('edit_name').value = teamName;
document.getElementById('edit_coach_id').value = coachId || ''; document.getElementById('edit_coach_id').value = coachId || '';
document.getElementById('edit_manager_id').value = managerId || '';
document.getElementById('editTeamModal').classList.remove('hidden'); document.getElementById('editTeamModal').classList.remove('hidden');
}); });
}); });
}); });
function showEditForm(teamId, teamName, coachId) { function showEditForm(teamId, teamName, coachId, managerId) {
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit'; document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
document.getElementById('edit_name').value = teamName; document.getElementById('edit_name').value = teamName;
document.getElementById('edit_coach_id').value = coachId || ''; document.getElementById('edit_coach_id').value = coachId || '';
document.getElementById('edit_manager_id').value = managerId || '';
document.getElementById('editTeamModal').classList.remove('hidden'); document.getElementById('editTeamModal').classList.remove('hidden');
} }
@@ -233,4 +325,22 @@ function hideEditForm() {
document.getElementById('editTeamModal').classList.add('hidden'); document.getElementById('editTeamModal').classList.add('hidden');
} }
</script> </script>
<style>
.team-staff {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.team-staff .text-muted {
font-size: 0.85rem;
}
.ml-2 {
margin-left: 8px;
}
.status-toggle-btn {
cursor: pointer;
min-width: 90px;
}
</style>
{% endblock %} {% endblock %}