ajout de présence des joueurs sur un matchs

This commit is contained in:
cedrick2711
2026-07-25 16:25:51 -04:00
parent e9f83709a4
commit de04ecdf11
19 changed files with 242 additions and 40 deletions
-1
View File
@@ -1 +0,0 @@
*.env
+2
View File
@@ -6,6 +6,8 @@ instance/
documents/ documents/
__pycache__/ __pycache__/
*.cpython-313.pyc
*.cpython-312.pyc
*.pyc *.pyc
.pytest_cache/ .pytest_cache/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -636,6 +636,7 @@ class MatchParticipant(db.Model):
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
team_side = db.Column(db.Integer, nullable=True) # 1 for team 1, 2 for team 2 (for player_vs_player matches) team_side = db.Column(db.Integer, nullable=True) # 1 for team 1, 2 for team 2 (for player_vs_player matches)
position = db.Column(db.String(50), nullable=True) # Position for this match position = db.Column(db.String(50), nullable=True) # Position for this match
attendance_confirmed = db.Column(db.Boolean, default=False) # Whether the player confirmed via Discord or manual toggle
added_at = db.Column(db.DateTime, default=datetime.utcnow) added_at = db.Column(db.DateTime, default=datetime.utcnow)
player = db.relationship('User') player = db.relationship('User')
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.
+1 -1
View File
@@ -6,7 +6,7 @@ This module provides the main dashboard view with role-specific statistics.
from flask import Blueprint, render_template, redirect, url_for, flash from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
from models import User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember, Match, MatchParticipant from models import User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember, Match, MatchParticipant, OrgTeam
from sqlalchemy import func from sqlalchemy import func
from datetime import datetime, date from datetime import datetime, date
+115 -24
View File
@@ -356,11 +356,22 @@ def create_match(tryout_id):
team2_id = request.form.get('team2_id') team2_id = request.form.get('team2_id')
match.team1_id = int(team1_id) if team1_id else None match.team1_id = int(team1_id) if team1_id else None
match.team2_id = int(team2_id) if team2_id else None match.team2_id = int(team2_id) if team2_id else None
# Get players from both teams # Create MatchParticipant records for all team members AND get notified player IDs
notified_participant_ids = []
if match.team1_id: if match.team1_id:
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()]) for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids.append(m.player_id)
if match.team2_id: if match.team2_id:
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()]) for m in TeamMember.query.filter_by(team_id=match.team2_id).all():
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids.append(m.player_id)
# Handle player vs player matches # Handle player vs player matches
elif match_type == 'player_vs_player': elif match_type == 'player_vs_player':
@@ -398,17 +409,17 @@ def create_match(tryout_id):
event_date_str = date_obj.strftime('%Y-%m-%d') event_date_str = date_obj.strftime('%Y-%m-%d')
event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD' event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD'
# For team_vs_team, notify all players but use match.id (no individual confirmation) # Send Discord notifications with proper participant reference IDs
# For player_vs_player/player_scrim, notify each player individually with participant ID
if match_type == 'team_vs_team': if match_type == 'team_vs_team':
for player_id in notified_player_ids: for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification( send_schedule_notification(
user_id=player_id, user_id=player_id,
event_type='match', event_type='match',
event_title=match.title, event_title=match.title,
event_date=event_date_str, event_date=event_date_str,
event_time=event_time_str, event_time=event_time_str,
reference_id=match.id reference_id=reference_id
) )
else: else:
for i, player_id in enumerate(notified_player_ids): for i, player_id in enumerate(notified_player_ids):
@@ -500,16 +511,38 @@ def edit_match(match_id):
notified_player_ids = [] notified_player_ids = []
# Handle team vs team matches # Handle team vs team matches
notified_participant_ids = []
if match.match_type == 'team_vs_team': if match.match_type == 'team_vs_team':
team1_id = request.form.get('team1_id') team1_id = request.form.get('team1_id')
team2_id = request.form.get('team2_id') team2_id = request.form.get('team2_id')
match.team1_id = int(team1_id) if team1_id else None new_team1_id = int(team1_id) if team1_id else None
match.team2_id = int(team2_id) if team2_id else None new_team2_id = int(team2_id) if team2_id else None
# Get players from both teams
if match.team1_id: # If teams changed, recreate MatchParticipant records
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()]) if new_team1_id != match.team1_id or new_team2_id != match.team2_id:
if match.team2_id: MatchParticipant.query.filter_by(match_id=match.id).delete()
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()]) match.team1_id = new_team1_id
match.team2_id = new_team2_id
if match.team1_id:
for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids.append(m.player_id)
if match.team2_id:
for m in TeamMember.query.filter_by(team_id=match.team2_id).all():
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids.append(m.player_id)
else:
# Teams didn't change, still get notified player IDs
if match.team1_id:
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()])
if match.team2_id:
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()])
# Handle player vs player matches - update participants # Handle player vs player matches - update participants
elif match.match_type == 'player_vs_player': elif match.match_type == 'player_vs_player':
@@ -565,20 +598,44 @@ def edit_match(match_id):
event_date_str = match.date.strftime('%Y-%m-%d') event_date_str = match.date.strftime('%Y-%m-%d')
end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}" if match.start_time and end_time_val else 'TBD' event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}" if match.start_time and end_time_val else 'TBD'
for player_id in notified_player_ids: if notified_participant_ids:
send_schedule_notification( for i, player_id in enumerate(notified_player_ids):
user_id=player_id, reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
event_type='match', send_schedule_notification(
event_title=match.title, user_id=player_id,
event_date=event_date_str, event_type='match',
event_time=event_time_str, event_title=match.title,
reference_id=match.id event_date=event_date_str,
) event_time=event_time_str,
reference_id=reference_id
)
else:
for player_id in notified_player_ids:
send_schedule_notification(
user_id=player_id,
event_type='match',
event_title=match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=match.id
)
flash('Match updated successfully!', 'success') flash('Match updated successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids, team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids) # Build participant attendance map for the template
participants_map = {}
for p in match.participants.all():
participants_map[p.player_id] = {
'participant_id': p.id,
'attendance_confirmed': p.attendance_confirmed,
'team_side': p.team_side
}
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams,
all_players=all_players, current_player_ids=current_player_ids,
team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids,
participants_map=participants_map)
@matches_bp.route('/<int:match_id>/delete', methods=['POST']) @matches_bp.route('/<int:match_id>/delete', methods=['POST'])
@@ -674,3 +731,37 @@ def api_available_players(date, time):
player_ids = get_players_available_at_time(date, time) player_ids = get_players_available_at_time(date, time)
return jsonify({'available_player_ids': player_ids}) return jsonify({'available_player_ids': player_ids})
@matches_bp.route('/<int:match_id>/toggle-presence/<int:participant_id>', methods=['POST'])
@login_required
def toggle_presence(match_id, participant_id):
"""Toggle the attendance_confirmed status for a match participant.
Accessible only to users who can manage the tryout.
Args:
match_id: The ID of the match.
participant_id: The ID of the MatchParticipant record.
Returns:
Response: JSON with new status.
"""
match = Match.query.get_or_404(match_id)
tryout = match.tryout
if not current_user.can_manage_this_tryout(tryout):
return jsonify({'error': 'Unauthorized'}), 403
participant = MatchParticipant.query.get_or_404(participant_id)
if participant.match_id != match_id:
return jsonify({'error': 'Participant does not belong to this match'}), 400
participant.attendance_confirmed = not participant.attendance_confirmed
db.session.commit()
return jsonify({
'participant_id': participant.id,
'attendance_confirmed': participant.attendance_confirmed,
'player_name': participant.player.username if participant.player else 'Unknown'
})
+9 -1
View File
@@ -271,6 +271,11 @@ def view_tryout(tryout_id):
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all() matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
match_data = [] match_data = []
for match in matches: for match in matches:
# Calculate presence stats
all_participants = list(match.participants.all())
confirmed_count = sum(1 for p in all_participants if p.attendance_confirmed)
total_count = len(all_participants)
if match.match_type == 'team_vs_team': if match.match_type == 'team_vs_team':
participants = { participants = {
'team1': match.team1.name if match.team1 else 'TBD', 'team1': match.team1.name if match.team1 else 'TBD',
@@ -289,9 +294,12 @@ def view_tryout(tryout_id):
} }
else: else:
participants = [p.player.username for p in match.participants.all()] participants = [p.player.username for p in match.participants.all()]
match_data.append({ match_data.append({
'match': match, 'match': match,
'participants': participants 'participants': participants,
'confirmed_count': confirmed_count,
'total_count': total_count
}) })
return render_template('pages/view_tryout.html', return render_template('pages/view_tryout.html',
+88 -3
View File
@@ -141,7 +141,19 @@
<h5>{{ match.team1.name }}</h5> <h5>{{ match.team1.name }}</h5>
<ul class="team-members-list"> <ul class="team-members-list">
{% for member in match.team1.members %} {% for member in match.team1.members %}
<li>{{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li> <li>
{{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}
{% set pdata = participants_map.get(member.player_id) %}
{% if pdata %}
<span class="presence-badge {{ 'presence-confirmed' if pdata.attendance_confirmed else 'presence-pending' }}"
data-participant-id="{{ pdata.participant_id }}"
data-match-id="{{ match.id }}"
onclick="togglePresence({{ match.id }}, {{ pdata.participant_id }}, this)"
title="Click to toggle presence">
{{ '✅ Confirmed' if pdata.attendance_confirmed else '⏳ Pending' }}
</span>
{% endif %}
</li>
{% else %} {% else %}
<li class="text-muted">No players assigned</li> <li class="text-muted">No players assigned</li>
{% endfor %} {% endfor %}
@@ -155,7 +167,19 @@
<h5>{{ match.team2.name }}</h5> <h5>{{ match.team2.name }}</h5>
<ul class="team-members-list"> <ul class="team-members-list">
{% for member in match.team2.members %} {% for member in match.team2.members %}
<li>{{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li> <li>
{{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}
{% set pdata = participants_map.get(member.player_id) %}
{% if pdata %}
<span class="presence-badge {{ 'presence-confirmed' if pdata.attendance_confirmed else 'presence-pending' }}"
data-participant-id="{{ pdata.participant_id }}"
data-match-id="{{ match.id }}"
onclick="togglePresence({{ match.id }}, {{ pdata.participant_id }}, this)"
title="Click to toggle presence">
{{ '✅ Confirmed' if pdata.attendance_confirmed else '⏳ Pending' }}
</span>
{% endif %}
</li>
{% else %} {% else %}
<li class="text-muted">No players assigned</li> <li class="text-muted">No players assigned</li>
{% endfor %} {% endfor %}
@@ -382,6 +406,38 @@
.randomize-btn:hover { .randomize-btn:hover {
background: var(--primary-dark); background: var(--primary-dark);
} }
/* Presence badge styles */
.presence-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
cursor: pointer;
margin-left: 8px;
transition: all 0.2s ease;
user-select: none;
}
.presence-badge:hover {
transform: scale(1.05);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.presence-confirmed {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.presence-pending {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
}
.team-members-list li {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
}
</style> </style>
<script> <script>
// Player data as simple JS object (id -> full_name) // Player data as simple JS object (id -> full_name)
@@ -491,7 +547,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize team assignments for player_vs_player matches // Initialize team assignments for player_vs_player matches
// Populate Team 1 // Populate Team 1
{% if match %}
initialTeam1Ids.forEach(function(pid) { initialTeam1Ids.forEach(function(pid) {
var teamDiv = document.getElementById('team1-selection'); var teamDiv = document.getElementById('team1-selection');
var playerName = playerDataById.player_data[pid]; var playerName = playerDataById.player_data[pid];
@@ -1007,5 +1062,35 @@ function randomizeTeams() {
updateHiddenInputs(); updateHiddenInputs();
updateRandomizePreview(); updateRandomizePreview();
} }
// Presence toggle function
function togglePresence(matchId, participantId, badgeEl) {
fetch('/matches/' + matchId + '/toggle-presence/' + participantId, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name="csrf_token"]').value
}
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.error) {
alert('Error: ' + data.error);
return;
}
if (data.attendance_confirmed) {
badgeEl.classList.remove('presence-pending');
badgeEl.classList.add('presence-confirmed');
badgeEl.textContent = '✅ Confirmed';
} else {
badgeEl.classList.remove('presence-confirmed');
badgeEl.classList.add('presence-pending');
badgeEl.textContent = '⏳ Pending';
}
})
.catch(function(error) {
console.error('Error toggling presence:', error);
});
}
</script> </script>
{% endblock %} {% endblock %}
+24 -8
View File
@@ -278,11 +278,12 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Match</th> <th>Match</th>
<th>Type</th> <th>Type</th>
<th>Date</th> <th>Date</th>
<th>Participants</th> <th>Participants</th>
<th>Time</th> <th>Time</th>
<th>Presence</th>
<th>Status</th> <th>Status</th>
{% if can_edit %} {% if can_edit %}
<th>Actions</th> <th>Actions</th>
@@ -307,6 +308,28 @@
</span> </span>
</td> </td>
<td>{{ m.date.strftime('%m/%d/%Y') }}</td> <td>{{ m.date.strftime('%m/%d/%Y') }}</td>
<td>
{% if m.start_time and m.end_time %}
{{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }}
{% else %}
TBD
{% endif %}
</td>
<td>
{% if item.total_count > 0 %}
<span class="presence-summary" title="{{ item.confirmed_count }} of {{ item.total_count }} confirmed">
{% if item.confirmed_count == item.total_count and item.total_count > 0 %}
<span class="badge badge-success">✅ {{ item.confirmed_count }}/{{ item.total_count }}</span>
{% elif item.confirmed_count > 0 %}
<span class="badge badge-warning">✅ {{ item.confirmed_count }}/{{ item.total_count }}</span>
{% else %}
<span class="badge badge-secondary">⏳ 0/{{ item.total_count }}</span>
{% endif %}
</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td> <td>
{% if m.match_type == 'team_vs_team' %} {% if m.match_type == 'team_vs_team' %}
<div class="match-teams"> <div class="match-teams">
@@ -360,13 +383,6 @@
{{ participants | join(', ') }} {{ participants | join(', ') }}
{% endif %} {% endif %}
</td> </td>
<td>
{% if m.start_time and m.end_time %}
{{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }}
{% else %}
TBD
{% endif %}
</td>
<td> <td>
<span class="badge badge-{{ m.status }}">{{ m.status }}</span> <span class="badge badge-{{ m.status }}">{{ m.status }}</span>
</td> </td>