Correction des présences et correction de plusieurs erreurs mineur de déplacement/parcours de l'utilisateur, harmonisation des processus

This commit is contained in:
cedrick2711
2026-07-28 18:11:02 -04:00
parent b982cd0318
commit a0f18a886c
19 changed files with 250 additions and 116 deletions
Binary file not shown.
+9 -9
View File
@@ -11,7 +11,7 @@ from urllib.parse import quote
# Available user roles in the system
ROLES = ['president', 'manager', 'coach', 'player', 'scout']
ROLES = ['admin', 'manager', 'coach', 'player', 'scout']
# Popular E-Sports games list for player profiles
ESPORT_GAMES = [
@@ -123,7 +123,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user is president, manager, or coach.
"""
return self.role in ['president', 'manager', 'coach']
return self.role in ['admin', 'manager', 'coach']
def can_manage_users(self):
"""Check if user can manage (create/delete) other users.
@@ -131,7 +131,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user is president.
"""
return self.role == 'president'
return self.role == 'admin'
def can_manage_tryouts(self):
"""Check if user can manage tryouts.
@@ -139,7 +139,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user is president, manager or coach.
"""
return self.role in ['president', 'manager', 'coach']
return self.role in ['admin', 'manager', 'coach']
def can_manage_teams(self):
"""Check if user can manage organization teams.
@@ -147,7 +147,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user is president or manager.
"""
return self.role in ['president', 'manager']
return self.role in ['admin', 'manager']
def can_schedule_matches(self):
"""Check if user can schedule matches.
@@ -155,7 +155,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user is president, manager, or coach.
"""
return self.role in ['president', 'manager', 'coach']
return self.role in ['admin', 'manager', 'coach']
def can_manage_this_tryout(self, tryout):
"""Check if user can manage a specific tryout.
@@ -170,7 +170,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user has permission to manage the tryout.
"""
if self.role == 'president':
if self.role == 'admin':
return True
if self.role == 'manager' and (tryout.created_by == self.id or tryout.manager_id == self.id):
return True
@@ -203,7 +203,7 @@ class User(UserMixin, db.Model):
Returns:
bool: True if user has permission to manage the org team.
"""
if self.role == 'president':
if self.role == 'admin':
return True
if self.role == 'manager':
return True # Managers can manage all org teams (create/edit/delete)
@@ -778,7 +778,7 @@ class Contract(db.Model):
if user.id == self.player_id:
return True
# President can view all contracts
if user.role == 'president':
if user.role == 'admin':
return True
# Manager can view contracts for players on their teams
if user.role == 'manager':
+2 -2
View File
@@ -89,7 +89,7 @@ def list_evaluations():
else:
sort_expr = sort_expr.desc()
if user.role == 'president':
if user.role == 'admin':
evaluations = Evaluation.query \
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
@@ -227,7 +227,7 @@ def evaluate_player(tryout_id, player_id):
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
evaluators = None
if current_user.role == 'president':
if current_user.role == 'admin':
all_evaluations = Evaluation.query.filter_by(tryout_id=tryout_id, player_id=player_id).all()
evaluators = [{'evaluator': User.query.get(e.evaluator_id), 'eval': e} for e in all_evaluations]
+1 -1
View File
@@ -43,7 +43,7 @@ def dashboard():
user = current_user
stats = {}
if user.role == 'president':
if user.role == 'admin':
stats['total_users'] = User.query.count()
stats['total_players'] = User.query.filter_by(role='player').count()
stats['total_tryouts'] = Tryout.query.count()
+6 -4
View File
@@ -19,7 +19,7 @@ def can_schedule_match():
Returns:
bool: True if user is president, manager, coach, or scout.
"""
return current_user.role in ['president', 'manager', 'coach', 'scout']
return current_user.role in ['admin', 'manager', 'coach', 'scout']
@matches_bp.route('/calendar')
@@ -248,7 +248,7 @@ def get_visible_tryouts_for_user():
Returns:
list: Query result of Tryout objects.
"""
if current_user.role == 'president':
if current_user.role == 'admin':
return Tryout.query.order_by(Tryout.date).all()
elif current_user.role == 'manager':
return Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date).all()
@@ -558,8 +558,10 @@ def edit_match(match_id):
# Handle player vs player matches - update participants
elif match.match_type == 'player_vs_player':
MatchParticipant.query.filter_by(match_id=match.id).delete()
team1_player_ids = request.form.getlist('team1_player_ids')
team2_player_ids = request.form.getlist('team2_player_ids')
team1_str = request.form.get('team1_player_ids', '')
team2_str = request.form.get('team2_player_ids', '')
team1_player_ids = [p for p in team1_str.split(',') if p.strip()] if team1_str else []
team2_player_ids = [p for p in team2_str.split(',') if p.strip()] if team2_str else []
notified_participant_ids = []
for pid in team1_player_ids:
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1)
+3 -3
View File
@@ -20,7 +20,7 @@ def can_manage_team_match(team):
Returns:
bool: True if user is president, manager, or a coach of this team.
"""
if current_user.role in ['president']:
if current_user.role in ['admin']:
return True
if current_user.role == 'manager':
return True
@@ -45,7 +45,7 @@ def list_matches():
# Optional pre-filter by team_id from query param
filter_team_id = request.args.get('team_id', type=int)
if current_user.role == 'president':
if current_user.role == 'admin':
teams = OrgTeam.query.order_by(OrgTeam.name).all()
matches_query = TeamMatch.query
elif current_user.role == 'manager':
@@ -337,7 +337,7 @@ def api_manageable_teams():
if not current_user.can_schedule_matches():
return jsonify([])
if current_user.role in ['president', 'manager']:
if current_user.role in ['admin', 'manager']:
teams = OrgTeam.query.order_by(OrgTeam.name).all()
elif current_user.role == 'coach':
teams = OrgTeam.query.filter(
+42 -13
View File
@@ -24,7 +24,9 @@ def list_teams():
"""
can_manage = current_user.can_manage_teams()
if current_user.role == 'coach':
if current_user.role == 'admin':
teams = OrgTeam.query.order_by(OrgTeam.name).all()
elif current_user.role == 'coach':
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
@@ -181,18 +183,45 @@ def edit_team(team_id):
flash(f'Team "{name}" already exists.', 'danger')
return redirect(url_for('teams.list_teams'))
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
if coach_id:
coach_user = User.query.get(int(coach_id))
if coach_user and not team.coaches.filter_by(id=coach_user.id).first():
team.coaches.append(coach_user)
if manager_id:
manager_user = User.query.get(int(manager_id))
if manager_user and not team.managers.filter_by(id=manager_user.id).first():
team.managers.append(manager_user)
# Check if we're syncing staff (multi-select) or single legacy update
if request.form.get('sync_staff') == '1':
coach_ids = request.form.getlist('coach_ids')
manager_ids = request.form.getlist('manager_ids')
# Sync coaches many-to-many
team.coaches = []
for cid in coach_ids:
if cid and cid.strip():
coach_user = User.query.get(int(cid))
if coach_user and coach_user.role == 'coach':
team.coaches.append(coach_user)
# Update legacy coach_id with first coach
coach_list = team.coaches.all()
team.coach_id = coach_list[0].id if coach_list else None
# Sync managers many-to-many
team.managers = []
for mid in manager_ids:
if mid and mid.strip():
manager_user = User.query.get(int(mid))
if manager_user and manager_user.role == 'manager':
team.managers.append(manager_user)
# Update legacy manager_id with first manager
manager_list = team.managers.all()
team.manager_id = manager_list[0].id if manager_list else None
else:
# Legacy single dropdown update
team.coach_id = int(coach_id) if coach_id else None
team.manager_id = int(manager_id) if manager_id else None
if coach_id:
coach_user = User.query.get(int(coach_id))
if coach_user and not team.coaches.filter_by(id=coach_user.id).first():
team.coaches.append(coach_user)
if manager_id:
manager_user = User.query.get(int(manager_id))
if manager_user and not team.managers.filter_by(id=manager_user.id).first():
team.managers.append(manager_user)
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
+16 -4
View File
@@ -18,7 +18,7 @@ def can_manage():
Returns:
bool: True if user is president or manager.
"""
return current_user.role in ['president', 'manager']
return current_user.role in ['admin', 'manager']
@tryouts_bp.route('')
@@ -35,7 +35,7 @@ def list_tryouts():
Returns:
Response: Rendered tryouts list template.
"""
if current_user.role == 'president':
if current_user.role == 'admin':
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
elif current_user.role == 'manager':
tryouts = Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date.desc()).all()
@@ -192,7 +192,7 @@ def view_tryout(tryout_id):
# Check if user has permission to view this tryout
can_view = False
if current_user.role == 'president':
if current_user.role == 'admin':
can_view = True
elif current_user.role == 'manager' and tryout.created_by == current_user.id:
can_view = True
@@ -276,6 +276,17 @@ def view_tryout(tryout_id):
confirmed_count = sum(1 for p in all_participants if p.attendance_confirmed)
total_count = len(all_participants)
# Build per-player presence data for toggle buttons
player_presence = []
for p in all_participants:
if p.player:
player_presence.append({
'participant_id': p.id,
'player_id': p.player_id,
'player_name': p.player.username,
'attendance_confirmed': p.attendance_confirmed
})
if match.match_type == 'team_vs_team':
participants = {
'team1': match.team1.name if match.team1 else 'TBD',
@@ -299,7 +310,8 @@ def view_tryout(tryout_id):
'match': match,
'participants': participants,
'confirmed_count': confirmed_count,
'total_count': total_count
'total_count': total_count,
'player_presence': player_presence
})
return render_template('pages/view_tryout.html',
+13 -13
View File
@@ -69,7 +69,7 @@ def list_users():
Returns:
Response: Rendered users list template or redirect to dashboard.
"""
if current_user.role != 'president':
if current_user.role != 'admin':
flash('Only the president can manage users.', 'danger')
return redirect(url_for('main.dashboard'))
@@ -91,7 +91,7 @@ def edit_user(user_id):
Returns:
Response: Edit form or redirect to users list.
"""
if current_user.role != 'president':
if current_user.role != 'admin':
flash('Only the president can edit users.', 'danger')
return redirect(url_for('main.dashboard'))
@@ -149,7 +149,7 @@ def delete_user(user_id):
Returns:
Response: Redirect to users list with status message.
"""
if current_user.role != 'president':
if current_user.role != 'admin':
flash('Only the president can delete users.', 'danger')
return redirect(url_for('main.dashboard'))
@@ -229,7 +229,7 @@ def create_user():
Returns:
Response: Create form or redirect to users list.
"""
if current_user.role != 'president':
if current_user.role != 'admin':
flash('Only the president can create users.', 'danger')
return redirect(url_for('main.dashboard'))
@@ -588,7 +588,7 @@ def can_manage_player_contract(user, player_id):
bool: True if user has permission to manage the contract.
"""
# President can manage all contracts
if user.role == 'president':
if user.role == 'admin':
return True
# Manager can upload contracts for any player
@@ -621,7 +621,7 @@ def list_contracts():
if current_user.role == 'player':
# Players see their own contracts
contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all()
elif current_user.role in ['president', 'manager', 'coach']:
elif current_user.role in ['admin', 'manager', 'coach']:
# Superiors see contracts for players on their teams
players = []
if current_user.role == 'coach':
@@ -636,7 +636,7 @@ def list_contracts():
player_ids = [p.id for p in players]
contracts = Contract.query.filter(Contract.player_id.in_(player_ids)).order_by(Contract.uploaded_at.desc()).all()
return render_template('pages/contracts.html', contracts=contracts, players=players if current_user.role in ['president', 'manager', 'coach'] else None)
return render_template('pages/contracts.html', contracts=contracts, players=players if current_user.role in ['admin', 'manager', 'coach'] else None)
@users_bp.route('/contracts/upload', methods=['GET', 'POST'])
@@ -650,7 +650,7 @@ def upload_contract():
Returns:
Response: Upload form or redirect to contracts list.
"""
if current_user.role not in ['president', 'manager', 'coach']:
if current_user.role not in ['admin', 'manager', 'coach']:
flash('Only presidents, managers, and coaches can upload contracts.', 'danger')
return redirect(url_for('users.list_contracts'))
@@ -1395,7 +1395,7 @@ def add_personal_note():
Returns:
Response: Create form or redirect to notes view.
"""
if current_user.role not in ['coach', 'manager', 'president']:
if current_user.role not in ['coach', 'manager', 'admin']:
flash('Only coaches and managers can add notes.', 'danger')
return redirect(url_for('main.dashboard'))
@@ -1407,7 +1407,7 @@ def add_personal_note():
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)).order_by(User.username).all() if player_ids else []
elif current_user.role in ['president', 'manager']:
elif current_user.role in ['admin', 'manager']:
players = User.query.filter_by(role='player').order_by(User.username).all()
# Get available matches and tryouts for context
@@ -1417,7 +1417,7 @@ def add_personal_note():
if current_user.role == 'coach' and org_team:
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
matches = Match.query.join(Tryout).filter(Tryout.target_org_team_id == org_team.id).order_by(Match.date.desc()).all()
elif current_user.role in ['president', 'manager']:
elif current_user.role in ['admin', 'manager']:
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
matches = Match.query.order_by(Match.date.desc()).all()
teams = Team.query.order_by(Team.name).all()
@@ -1501,7 +1501,7 @@ def add_note_from_match(match_id):
Returns:
Response: Create form or redirect.
"""
if current_user.role not in ['coach', 'manager', 'president']:
if current_user.role not in ['coach', 'manager', 'admin']:
flash('Only coaches can add notes from matches.', 'danger')
return redirect(url_for('main.dashboard'))
@@ -1577,7 +1577,7 @@ def add_note_from_tryout(tryout_id):
Returns:
Response: Create form or redirect.
"""
if current_user.role not in ['coach', 'manager', 'president']:
if current_user.role not in ['coach', 'manager', 'admin']:
flash('Only coaches can add notes from tryouts.', 'danger')
return redirect(url_for('main.dashboard'))
+3 -3
View File
@@ -43,7 +43,7 @@ def seed_database():
# Create users with different roles
users_data = [
{'username': 'president', 'password': 'password', 'role': 'president', 'full_name': 'Sarah Johnson', 'email': '[email protected]', 'phone': '555-0101'},
{'username': 'admin', 'password': 'password', 'role': 'admin', 'full_name': 'Sarah Johnson', 'email': '[email protected]', 'phone': '555-0101'},
{'username': 'manager1', 'password': 'password', 'role': 'manager', 'full_name': 'Mike Williams', 'email': '[email protected]', 'phone': '555-0102'},
{'username': 'manager2', 'password': 'password', 'role': 'manager', 'full_name': 'Emily Davis', 'email': '[email protected]', 'phone': '555-0103'},
{'username': 'coach1', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Thompson', 'email': '[email protected]', 'phone': '555-0104', 'discord_user_id': '484107446298738689'},
@@ -117,7 +117,7 @@ def seed_database():
# Map users for easy access
user_map = {u.username: u for u in users}
president = user_map['president']
president = user_map['admin']
manager1 = user_map['manager1']
manager2 = user_map['manager2']
coaches = [user_map['coach1'], user_map['coach2'], user_map['coach3']]
@@ -507,7 +507,7 @@ def seed_database():
print("\n[SUCCESS] Database seeded successfully!")
print("\n=== Login Credentials ===")
print("President: username='president', password='password'")
print("President: username='admin', password='password'")
print("Manager: username='manager1', password='password'")
print("Coach: username='coach1', password='password' (assigned to Varsity)")
print("Coach: username='coach2', password='password' (assigned to Junior Varsity)")
+6 -6
View File
@@ -1231,9 +1231,9 @@ a:hover { color: var(--primary-dark); }
}
.merged-disponibility-time-block.selected {
background: var(--success);
color: white;
border-color: var(--success);
background: #6366f1 !important;
color: white !important;
border-color: #4f46e5 !important;
}
.merged-disponibility-time-block.high-availability {
@@ -1266,10 +1266,10 @@ a:hover { color: var(--primary-dark); }
/* Then specific states override the generic rule */
[data-theme="dark"] .merged-disponibility-time-block.selected {
background: var(--success) !important;
background: #6366f1 !important;
color: white !important;
border-color: var(--success) !important;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.5);
border-color: #818cf8 !important;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.5);
}
[data-theme="dark"] .merged-disponibility-time-block.high-availability {
+1 -1
View File
@@ -4,7 +4,7 @@
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('users.profile') }}">Profile</a> / Contracts</span>{% endblock %}
{% block header_actions %}
{% if current_user.role in ['president', 'manager', 'coach'] %}
{% if current_user.role in ['admin', 'manager', 'coach'] %}
<a href="{{ url_for('users.upload_contract') }}" class="btn btn-primary">
<i class="fas fa-upload"></i> Upload Contract
</a>
+1 -1
View File
@@ -5,7 +5,7 @@
{% block content %}
<div class="dashboard">
{% if user.role == 'president' %}
{% if user.role == 'admin' %}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon bg-primary">
+1 -1
View File
@@ -21,7 +21,7 @@
</th>
{% endmacro %}
{% if current_user.role == 'president' and player_scores %}
{% if current_user.role == 'admin' and player_scores %}
<div class="stats-grid mb-4">
{% for pid, data in player_scores.items() %}
<div class="stat-card stat-card-sm">
+6 -1
View File
@@ -75,7 +75,12 @@
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
<hr class="section-divider">
<h4 class="section-title"><i class="fas fa-clock"></i> Select Match Time</h4>
<p class="text-muted small">Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.</p>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<p class="text-muted small" style="margin:0;">Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.</p>
<button type="button" class="btn btn-sm btn-outline" onclick="clearTimeSelection()" title="Reset time selection">
<i class="fas fa-undo"></i> Reset Time
</button>
</div>
<div id="merged-disponibility-grid" class="merged-disponibility-grid">
<p class="text-muted">Loading...</p>
+4 -5
View File
@@ -77,12 +77,11 @@
<!-- Player presence details -->
<div class="presence-players">
{% for p in item.participants %}
<a href="{{ url_for('users.view_user', user_id=p.player.id) }}" class="presence-player-tag {% if p.is_confirmed %}confirmed{% else %}pending{% endif %}"
title="{{ p.player.username }}{% if p.is_confirmed %} - Confirmed{% else %} - Pending{% endif %}"
style="text-decoration: none;">
<span class="presence-player-tag {% if p.is_confirmed %}confirmed{% else %}pending{% endif %}"
title="{{ p.player.username }}{% if p.is_confirmed %} - Confirmed{% else %} - Pending{% endif %}">
{{ p.player.username[:2] | upper }} {{ p.player.username }}
{% if p.is_confirmed %}✅{% else %}⏳{% endif %}
</a>
</span>
{% endfor %}
</div>
{% else %}
@@ -93,7 +92,7 @@
<span class="badge badge-{{ m.status }}">{{ m.status }}</span>
</td>
<td class="eval-actions">
{% set can_manage_this = (current_user.role in ['president', 'manager']) or (current_user.role == 'coach' and m.org_team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and m.org_team.coach_id == current_user.id) %}
{% set can_manage_this = (current_user.role in ['admin', 'manager']) or (current_user.role == 'coach' and m.org_team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and m.org_team.coach_id == current_user.id) %}
{% if can_manage_this %}
<a href="{{ url_for('team_matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline" title="Edit Match">
<i class="fas fa-edit"></i>
+70 -47
View File
@@ -124,36 +124,10 @@
{% endif %}
</div>
</div>
{% if can_manage %}
<div class="staff-add-forms">
<form method="POST" action="{{ url_for('teams.add_coach', team_id=team.id) }}" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<select name="coach_id" class="form-select form-select-sm" onchange="if(this.value) this.form.submit()">
<option value="">+ Add Coach</option>
{% for c in coaches %}
{% if c.id not in (team_coaches | map(attribute='id') | list) %}
<option value="{{ c.id }}">{{ c.username }}</option>
{% endif %}
{% endfor %}
</select>
</form>
<form method="POST" action="{{ url_for('teams.add_manager', team_id=team.id) }}" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<select name="manager_id" class="form-select form-select-sm" onchange="if(this.value) this.form.submit()">
<option value="">+ Add Manager</option>
{% for m in managers %}
{% if m.id not in (team_managers | map(attribute='id') | list) %}
<option value="{{ m.id }}">{{ m.username }}</option>
{% endif %}
{% endfor %}
</select>
</form>
</div>
{% endif %}
<a href="{{ url_for('team_matches.list_matches') }}?team_id={{ team.id }}" class="btn btn-sm btn-outline" title="View Team Matches">
<i class="fas fa-futbol"></i> Matches
</a>
{% if current_user.can_schedule_matches() and (current_user.role in ['president', 'manager'] or (current_user.role == 'coach' and team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and team.coach_id == current_user.id)) %}
{% if current_user.can_schedule_matches() and (current_user.role in ['admin', 'manager'] or (current_user.role == 'coach' and team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and team.coach_id == current_user.id)) %}
<a href="{{ url_for('team_matches.create_match', team_id=team.id) }}" class="btn btn-sm btn-success" title="Schedule Team Match">
<i class="fas fa-plus"></i> Match
</a>
@@ -286,28 +260,37 @@
<div class="modal-body">
<form id="editTeamForm" method="POST" action="" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="sync_staff" value="1"/>
<div class="form-group">
<label for="edit_name">Team Name</label>
<input type="text" id="edit_name" name="name" required>
</div>
<hr class="section-divider">
<!-- Manage Coaches -->
<h5 class="mb-2"><i class="fas fa-chalkboard-teacher"></i> Coaches</h5>
<div class="form-group">
<label for="edit_coach_id">Assigned Coach</label>
<select id="edit_coach_id" name="coach_id" class="form-select">
<option value="">-- No coach assigned --</option>
<select name="coach_ids" id="edit-coach-select" class="form-select" multiple style="min-height: 100px; width: 100%;">
{% for coach in coaches %}
<option value="{{ coach.id }}">{{ coach.username }}</option>
<option value="{{ coach.id }}" class="coach-option">{{ coach.username }}</option>
{% endfor %}
</select>
<small class="form-text">Hold Ctrl/Cmd to select multiple. Only unassigned coaches shown.</small>
</div>
<!-- Manage Managers -->
<h5 class="mb-2"><i class="fas fa-user-tie"></i> Managers</h5>
<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>
<select name="manager_ids" id="edit-manager-select" class="form-select" multiple style="min-height: 100px; width: 100%;">
{% for manager in managers %}
<option value="{{ manager.id }}">{{ manager.username }}</option>
<option value="{{ manager.id }}" class="manager-option">{{ manager.username }}</option>
{% endfor %}
</select>
<small class="form-text">Hold Ctrl/Cmd to select multiple. Only unassigned managers shown.</small>
</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>
@@ -355,28 +338,68 @@ function toggleStatus(btn) {
});
}
var currentEditTeamId = null;
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.edit-team-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var teamId = this.getAttribute('data-team-id');
currentEditTeamId = 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('editTeamForm').action = '/teams/' + currentEditTeamId + '/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');
populateEditSelects(currentEditTeamId);
});
});
});
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');
function getCurrentStaffIds(teamId, type) {
// Get currently assigned coach/manager IDs from the staff bar pills
var ids = [];
var teamCard = document.querySelector('[data-team-id="' + teamId + '"]');
if (!teamCard) return ids;
var staffBar = teamCard.closest('.card').querySelector('.team-staff-bar');
if (!staffBar) return ids;
var groupIndex = type === 'coach' ? 0 : 1;
var group = staffBar.querySelectorAll('.staff-group')[groupIndex];
if (!group) return ids;
group.querySelectorAll('.staff-tag form input[name="coach_id"], .staff-tag form input[name="manager_id"]').forEach(function(input) {
ids.push(input.value);
});
return ids;
}
function populateEditSelects(teamId) {
var coachSelect = document.getElementById('edit-coach-select');
var managerSelect = document.getElementById('edit-manager-select');
var currentCoachIds = getCurrentStaffIds(teamId, 'coach');
var currentManagerIds = getCurrentStaffIds(teamId, 'manager');
// Show all coaches, but pre-select current ones and hide non-assigned
// Actually: show only currently-assigned options (pre-selected)
coachSelect.querySelectorAll('.coach-option').forEach(function(opt) {
var isAssigned = currentCoachIds.includes(opt.value);
opt.selected = isAssigned;
// Always show all options so user can add/remove
opt.style.display = '';
});
managerSelect.querySelectorAll('.manager-option').forEach(function(opt) {
var isAssigned = currentManagerIds.includes(opt.value);
opt.selected = isAssigned;
opt.style.display = '';
});
// Also update text to reflect current count
document.querySelector('#edit-coach-select + .form-text').textContent =
'Currently assigned: ' + currentCoachIds.length + '. Hold Ctrl/Cmd to select multiple.';
document.querySelector('#edit-manager-select + .form-text').textContent =
'Currently assigned: ' + currentManagerIds.length + '. Hold Ctrl/Cmd to select multiple.';
}
function hideEditForm() {
+64
View File
@@ -338,6 +338,18 @@
<span class="badge badge-secondary">⏳ 0/{{ item.total_count }}</span>
{% endif %}
</span>
<!-- Per-player presence toggles -->
{% if can_edit and item.player_presence %}
<div class="presence-players" style="margin-top:6px;">
{% for pp in item.player_presence %}
<button class="btn btn-xs presence-toggle-btn {% if pp.attendance_confirmed %}presence-confirmed-btn{% else %}presence-pending-btn{% endif %}"
title="{{ pp.player_name }}"
onclick="toggleTryoutPresence({{ m.id }}, {{ pp.participant_id }}, this)">
{{ pp.player_name[:2] | upper }} {% if pp.attendance_confirmed %}✅{% else %}⏳{% endif %}
</button>
{% endfor %}
</div>
{% endif %}
{% else %}
<span class="text-muted"></span>
{% endif %}
@@ -484,10 +496,62 @@
{% endif %}
</div>
<style>
.presence-toggle-btn {
padding: 2px 7px;
font-size: 0.7rem;
border-radius: 10px;
border: 1px solid #d1d5db;
cursor: pointer;
margin: 2px;
font-weight: 600;
transition: all 0.15s ease;
}
.presence-confirmed-btn {
background: #d1fae5;
color: #065f46;
border-color: #a7f3d0;
}
.presence-pending-btn {
background: #fef3c7;
color: #92400e;
border-color: #fde68a;
}
.presence-toggle-btn:hover {
transform: scale(1.08);
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
</style>
<script>
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
function toggleTryoutPresence(matchId, participantId, btn) {
fetch('/matches/' + matchId + '/toggle-presence/' + participantId, {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token() }}',
'Content-Type': 'application/json'
}
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.attendance_confirmed) {
btn.classList.add('presence-confirmed-btn');
btn.classList.remove('presence-pending-btn');
btn.innerHTML = btn.innerHTML.replace('⏳', '✅');
} else {
btn.classList.remove('presence-confirmed-btn');
btn.classList.add('presence-pending-btn');
btn.innerHTML = btn.innerHTML.replace('✅', '⏳');
}
location.reload();
})
.catch(function(err) {
console.error('Error toggling tryout presence:', err);
});
}
// Mini calendar for matches
document.addEventListener('DOMContentLoaded', function() {
var miniCalendarEl = document.getElementById('mini-calendar');
+2 -2
View File
@@ -265,7 +265,7 @@ class CreateUserSchema(StripMixin):
role = fields.String(
required=True,
validate=validate.OneOf(
['president', 'manager', 'coach', 'player', 'scout'],
['admin', 'manager', 'coach', 'player', 'scout'],
error='Invalid role selected.'
),
)
@@ -302,7 +302,7 @@ class EditUserSchema(StripMixin):
role = fields.String(
required=True,
validate=validate.OneOf(
['president', 'manager', 'coach', 'player', 'scout'],
['admin', 'manager', 'coach', 'player', 'scout'],
error='Invalid role selected.'
),
)