Bug fix
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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -65,7 +65,6 @@ class User(UserMixin, db.Model):
|
||||
full_name: User's display name.
|
||||
email: User's email address.
|
||||
phone: Optional phone number.
|
||||
team_id: Foreign key to the user's organization team.
|
||||
is_active_account: Whether the account is active.
|
||||
games: Comma-separated list of games the user plays.
|
||||
discord_username: User's Discord handle.
|
||||
@@ -79,7 +78,6 @@ class User(UserMixin, db.Model):
|
||||
full_name = db.Column(db.String(100), nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
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)
|
||||
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}
|
||||
|
||||
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
|
||||
GAME_PLATFORMS = {
|
||||
@@ -296,6 +302,35 @@ class UserGamertag(db.Model):
|
||||
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):
|
||||
"""Persistent organization teams (e.g., Varsity, JV) that exist across tryouts.
|
||||
|
||||
@@ -306,6 +341,7 @@ class OrgTeam(db.Model):
|
||||
id: Unique identifier.
|
||||
name: Team name (e.g., "Varsity", "Junior Varsity").
|
||||
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_at: Timestamp of team creation.
|
||||
"""
|
||||
@@ -313,12 +349,30 @@ class OrgTeam(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=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_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
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])
|
||||
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):
|
||||
@@ -645,7 +699,7 @@ class Contract(db.Model):
|
||||
# Manager can view contracts for players on their teams
|
||||
if user.role == 'manager':
|
||||
player = User.query.get(self.player_id)
|
||||
if player and player.team_id:
|
||||
if player and player.get_org_teams():
|
||||
return True
|
||||
# Coach can view contracts for players on their team
|
||||
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.
@@ -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_login import login_required, current_user
|
||||
from extensions import db
|
||||
from models import User, Tryout, Evaluation, TryoutRegistration
|
||||
from models import User, Tryout, Evaluation, TryoutRegistration, GAME_POSITIONS
|
||||
from sqlalchemy import func
|
||||
|
||||
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
||||
@@ -178,7 +178,8 @@ def evaluate_player(tryout_id, player_id):
|
||||
tryout=tryout,
|
||||
player=player,
|
||||
existing_eval=existing_eval,
|
||||
evaluators=evaluators)
|
||||
evaluators=evaluators,
|
||||
game_positions=GAME_POSITIONS)
|
||||
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/players')
|
||||
|
||||
+108
-14
@@ -3,10 +3,10 @@
|
||||
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 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')
|
||||
|
||||
@@ -35,8 +35,9 @@ def list_teams():
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
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()
|
||||
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'])
|
||||
@@ -47,6 +48,7 @@ def create_team():
|
||||
Args:
|
||||
name: Team name from form.
|
||||
coach_id: Optional coach assignment from form.
|
||||
manager_id: Optional manager assignment from form.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to teams list with status message.
|
||||
@@ -57,6 +59,7 @@ def create_team():
|
||||
|
||||
name = request.form.get('name')
|
||||
coach_id = request.form.get('coach_id')
|
||||
manager_id = request.form.get('manager_id')
|
||||
|
||||
if not name:
|
||||
flash('Team name is required.', 'danger')
|
||||
@@ -70,6 +73,7 @@ def create_team():
|
||||
team = OrgTeam(
|
||||
name=name,
|
||||
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
|
||||
)
|
||||
db.session.add(team)
|
||||
@@ -87,6 +91,7 @@ def edit_team(team_id):
|
||||
team_id: The ID of the team to edit.
|
||||
name: New team name from form.
|
||||
coach_id: New coach assignment from form.
|
||||
manager_id: New manager assignment from form.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to teams list with status message.
|
||||
@@ -98,6 +103,7 @@ def edit_team(team_id):
|
||||
|
||||
name = request.form.get('name')
|
||||
coach_id = request.form.get('coach_id')
|
||||
manager_id = request.form.get('manager_id')
|
||||
|
||||
if not name:
|
||||
flash('Team name is required.', 'danger')
|
||||
@@ -110,6 +116,7 @@ def edit_team(team_id):
|
||||
|
||||
team.name = name
|
||||
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()
|
||||
flash(f'Team "{name}" updated successfully!', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
@@ -144,22 +151,71 @@ def delete_team(team_id):
|
||||
t.target_org_team_id = None
|
||||
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.commit()
|
||||
flash(f'Team "{name}" deleted successfully.', 'success')
|
||||
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'])
|
||||
@login_required
|
||||
def add_player(team_id):
|
||||
"""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:
|
||||
team_id: The ID of the team to add the player to.
|
||||
player_id: The ID of the player to add.
|
||||
status: The player's status (starter or substitute).
|
||||
|
||||
Returns:
|
||||
Response: Redirect to teams list with status message.
|
||||
@@ -169,7 +225,7 @@ def add_player(team_id):
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
player_id = request.form.get('player_id')
|
||||
position = request.form.get('position', '')
|
||||
status = request.form.get('status', 'starter')
|
||||
|
||||
if not player_id:
|
||||
flash('Please select a player.', 'danger')
|
||||
@@ -180,15 +236,19 @@ def add_player(team_id):
|
||||
flash('Can only assign players to teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
# If player already belongs to a different team, remove them first
|
||||
if player.team_id and player.team_id != team_id:
|
||||
old_team = OrgTeam.query.get(player.team_id)
|
||||
flash(f'{player.full_name} was moved from {old_team.name} to {team.name}.', 'info')
|
||||
elif player.team_id == team_id:
|
||||
# Check if player is already on this team
|
||||
existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first()
|
||||
if existing:
|
||||
flash(f'{player.full_name} is already on {team.name}.', 'info')
|
||||
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()
|
||||
flash(f'{player.full_name} added to {team.name}!', 'success')
|
||||
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)
|
||||
|
||||
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')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
player.team_id = None
|
||||
db.session.delete(tp)
|
||||
db.session.commit()
|
||||
flash(f'{player.full_name} removed from {team.name}.', 'success')
|
||||
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
|
||||
|
||||
|
||||
@@ -284,7 +377,8 @@ def add_player_note(team_id, player_id):
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
# 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')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
+29
-16
@@ -8,7 +8,7 @@ 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, 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 datetime import datetime, timedelta, date as date_type
|
||||
import requests
|
||||
@@ -514,8 +514,8 @@ def can_manage_player_contract(user, player_id):
|
||||
if user.role == 'coach':
|
||||
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
|
||||
if org_team:
|
||||
player = User.query.get(player_id)
|
||||
if player and player.team_id == org_team.id:
|
||||
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=org_team.id).first()
|
||||
if tp:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -542,7 +542,8 @@ def list_contracts():
|
||||
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).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
|
||||
players = User.query.filter_by(role='player').all()
|
||||
|
||||
@@ -571,7 +572,11 @@ def upload_contract():
|
||||
# Get players this user can manage
|
||||
if current_user.role == 'coach':
|
||||
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:
|
||||
players = User.query.filter_by(role='player').all()
|
||||
|
||||
@@ -598,7 +603,8 @@ def upload_contract():
|
||||
|
||||
# Get player info for folder structure
|
||||
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
|
||||
if team:
|
||||
@@ -621,7 +627,7 @@ def upload_contract():
|
||||
# Create database record
|
||||
contract = Contract(
|
||||
player_id=player_id,
|
||||
team_id=player.team_id,
|
||||
team_id=team.id if team else None,
|
||||
uploaded_by_id=current_user.id,
|
||||
original_filename=original_filename,
|
||||
stored_filename=stored_filename,
|
||||
@@ -822,8 +828,9 @@ def one_on_one():
|
||||
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
|
||||
# Get player's teams and coach (use the first team the player is on)
|
||||
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
|
||||
|
||||
if not coach:
|
||||
@@ -1062,7 +1069,8 @@ def api_get_coach_availability(coach_id):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
# 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:
|
||||
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()
|
||||
players = []
|
||||
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':
|
||||
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()
|
||||
players = []
|
||||
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
|
||||
team_notes = []
|
||||
@@ -1257,8 +1267,9 @@ def my_notes():
|
||||
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 player's teams and coach (use the first team the player is on)
|
||||
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
|
||||
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':
|
||||
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()
|
||||
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']:
|
||||
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 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:
|
||||
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ users, tryouts, teams, evaluations, and player disponibilities.
|
||||
|
||||
from sqlalchemy import text
|
||||
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
|
||||
import random
|
||||
|
||||
@@ -30,6 +30,7 @@ def seed_database():
|
||||
db.session.execute(text('DELETE FROM player_disponibilities'))
|
||||
db.session.execute(text('DELETE FROM match_participants'))
|
||||
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 teams'))
|
||||
db.session.execute(text('DELETE FROM evaluations'))
|
||||
@@ -323,19 +324,20 @@ def seed_database():
|
||||
db.session.commit()
|
||||
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_teams[0], players[0]),
|
||||
(org_teams[0], players[1]),
|
||||
(org_teams[0], players[2]),
|
||||
(org_teams[1], players[3]),
|
||||
(org_teams[1], players[4]),
|
||||
(org_teams[1], players[5]),
|
||||
(org_teams[2], players[6]),
|
||||
(org_teams[2], players[7]),
|
||||
(org_teams[0], players[0], 'starter'),
|
||||
(org_teams[0], players[1], 'starter'),
|
||||
(org_teams[0], players[2], 'substitute'),
|
||||
(org_teams[1], players[3], 'starter'),
|
||||
(org_teams[1], players[4], 'starter'),
|
||||
(org_teams[1], players[5], 'substitute'),
|
||||
(org_teams[2], players[6], 'starter'),
|
||||
(org_teams[2], players[7], 'substitute'),
|
||||
]
|
||||
for org_team, player in org_team_assignments:
|
||||
player.team_id = org_team.id
|
||||
for org_team, player, status in org_team_assignments:
|
||||
tp = TeamPlayer(player_id=player.id, org_team_id=org_team.id, status=status)
|
||||
db.session.add(tp)
|
||||
db.session.commit()
|
||||
|
||||
# Create sample disponibilities for players
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set positions = GAME_POSITIONS.get(tryout.game, []) %}
|
||||
{% set positions = game_positions.get(tryout.game, []) %}
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<label for="position_recommendation">Recommended Position</label>
|
||||
|
||||
+124
-14
@@ -21,11 +21,11 @@
|
||||
<form method="POST" action="{{ url_for('teams.create_team') }}" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<div class="form-group col-4">
|
||||
<label for="name">Team Name</label>
|
||||
<input type="text" id="name" name="name" placeholder="e.g., Varsity, JV, U14" required>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<div class="form-group col-4">
|
||||
<label for="coach_id">Assigned Coach</label>
|
||||
<select id="coach_id" name="coach_id" class="form-select">
|
||||
<option value="">-- No coach assigned --</option>
|
||||
@@ -34,6 +34,15 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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 class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideCreateForm()">Cancel</button>
|
||||
@@ -50,13 +59,36 @@
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users-cog"></i> {{ team.name }}</h3>
|
||||
<div class="card-actions">
|
||||
<div class="team-staff">
|
||||
{% if team.coach %}
|
||||
<span class="text-muted">Coach: {{ team.coach.full_name }}</span>
|
||||
{% if can_manage %}
|
||||
<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 %}
|
||||
<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 '' }}">
|
||||
<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 %}
|
||||
<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
|
||||
</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.')">
|
||||
@@ -79,6 +111,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Status</th>
|
||||
<th>Position / Role</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
@@ -88,20 +121,34 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player in team.players %}
|
||||
{% for entry in team.get_players_with_status() %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-mini">
|
||||
<div class="avatar-sm">{{ player.full_name[:2] | upper }}</div>
|
||||
<span>{{ player.full_name }}</span>
|
||||
<div class="avatar-sm">{{ entry.player.full_name[:2] | upper }}</div>
|
||||
<span>{{ entry.player.full_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ player.role | capitalize }}</td>
|
||||
<td>{{ player.email }}</td>
|
||||
<td>{{ player.phone or '-' }}</td>
|
||||
<td>
|
||||
{% if can_manage_team %}
|
||||
<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 %}
|
||||
<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() }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-user-minus"></i> Remove
|
||||
@@ -112,7 +159,7 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<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">
|
||||
<i class="fas fa-users-slash"></i>
|
||||
<h4>No players assigned</h4>
|
||||
@@ -136,11 +183,15 @@
|
||||
<select name="player_id" class="form-select" required>
|
||||
<option value="">-- Select a player --</option>
|
||||
{% for p in all_players %}
|
||||
{% if p.team_id != team.id %}
|
||||
<option value="{{ p.id }}">{{ p.full_name }} {% if p.org_team %}(currently in {{ p.org_team.name }}){% endif %}</option>
|
||||
{% if p.id not in team.players | map(attribute='id') %}
|
||||
<option value="{{ p.id }}">{{ p.full_name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</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">
|
||||
<i class="fas fa-plus"></i> Add to Team
|
||||
</button>
|
||||
@@ -190,6 +241,15 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideEditForm()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
@@ -208,24 +268,56 @@ function hideCreateForm() {
|
||||
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.querySelectorAll('.edit-team-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var teamId = this.getAttribute('data-team-id');
|
||||
var teamName = this.getAttribute('data-team-name');
|
||||
var coachId = this.getAttribute('data-coach-id');
|
||||
var managerId = this.getAttribute('data-manager-id');
|
||||
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
|
||||
document.getElementById('edit_name').value = teamName;
|
||||
document.getElementById('edit_coach_id').value = coachId || '';
|
||||
document.getElementById('edit_manager_id').value = managerId || '';
|
||||
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('edit_name').value = teamName;
|
||||
document.getElementById('edit_coach_id').value = coachId || '';
|
||||
document.getElementById('edit_manager_id').value = managerId || '';
|
||||
document.getElementById('editTeamModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -233,4 +325,22 @@ function hideEditForm() {
|
||||
document.getElementById('editTeamModal').classList.add('hidden');
|
||||
}
|
||||
</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 %}
|
||||
Reference in New Issue
Block a user