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.
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.
+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_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
View File
@@ -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
View File
@@ -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()
+14 -12
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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 %}