ajout de présence des joueurs sur un matchs
This commit is contained in:
@@ -6,6 +6,8 @@ instance/
|
||||
documents/
|
||||
|
||||
__pycache__/
|
||||
*.cpython-313.pyc
|
||||
*.cpython-312.pyc
|
||||
*.pyc
|
||||
|
||||
.pytest_cache/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -636,6 +636,7 @@ class MatchParticipant(db.Model):
|
||||
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)
|
||||
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)
|
||||
|
||||
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
@@ -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_login import login_required, current_user
|
||||
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 datetime import datetime, date
|
||||
|
||||
|
||||
+102
-11
@@ -356,11 +356,22 @@ def create_match(tryout_id):
|
||||
team2_id = request.form.get('team2_id')
|
||||
match.team1_id = int(team1_id) if team1_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:
|
||||
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:
|
||||
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
|
||||
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_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)
|
||||
# For player_vs_player/player_scrim, notify each player individually with participant ID
|
||||
# Send Discord notifications with proper participant reference IDs
|
||||
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(
|
||||
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
|
||||
reference_id=reference_id
|
||||
)
|
||||
else:
|
||||
for i, player_id in enumerate(notified_player_ids):
|
||||
@@ -500,12 +511,34 @@ def edit_match(match_id):
|
||||
notified_player_ids = []
|
||||
|
||||
# Handle team vs team matches
|
||||
notified_participant_ids = []
|
||||
if match.match_type == 'team_vs_team':
|
||||
team1_id = request.form.get('team1_id')
|
||||
team2_id = request.form.get('team2_id')
|
||||
match.team1_id = int(team1_id) if team1_id else None
|
||||
match.team2_id = int(team2_id) if team2_id else None
|
||||
# Get players from both teams
|
||||
new_team1_id = int(team1_id) if team1_id else None
|
||||
new_team2_id = int(team2_id) if team2_id else None
|
||||
|
||||
# If teams changed, recreate MatchParticipant records
|
||||
if new_team1_id != match.team1_id or new_team2_id != match.team2_id:
|
||||
MatchParticipant.query.filter_by(match_id=match.id).delete()
|
||||
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:
|
||||
@@ -565,6 +598,18 @@ def edit_match(match_id):
|
||||
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
|
||||
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'
|
||||
if notified_participant_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(
|
||||
user_id=player_id,
|
||||
event_type='match',
|
||||
event_title=match.title,
|
||||
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,
|
||||
@@ -578,7 +623,19 @@ def edit_match(match_id):
|
||||
flash('Match updated successfully!', 'success')
|
||||
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'])
|
||||
@@ -674,3 +731,37 @@ def api_available_players(date, time):
|
||||
|
||||
player_ids = get_players_available_at_time(date, time)
|
||||
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
@@ -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()
|
||||
match_data = []
|
||||
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':
|
||||
participants = {
|
||||
'team1': match.team1.name if match.team1 else 'TBD',
|
||||
@@ -289,9 +294,12 @@ def view_tryout(tryout_id):
|
||||
}
|
||||
else:
|
||||
participants = [p.player.username for p in match.participants.all()]
|
||||
|
||||
match_data.append({
|
||||
'match': match,
|
||||
'participants': participants
|
||||
'participants': participants,
|
||||
'confirmed_count': confirmed_count,
|
||||
'total_count': total_count
|
||||
})
|
||||
|
||||
return render_template('pages/view_tryout.html',
|
||||
|
||||
@@ -141,7 +141,19 @@
|
||||
<h5>{{ match.team1.name }}</h5>
|
||||
<ul class="team-members-list">
|
||||
{% 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 %}
|
||||
<li class="text-muted">No players assigned</li>
|
||||
{% endfor %}
|
||||
@@ -155,7 +167,19 @@
|
||||
<h5>{{ match.team2.name }}</h5>
|
||||
<ul class="team-members-list">
|
||||
{% 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 %}
|
||||
<li class="text-muted">No players assigned</li>
|
||||
{% endfor %}
|
||||
@@ -382,6 +406,38 @@
|
||||
.randomize-btn:hover {
|
||||
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>
|
||||
<script>
|
||||
// 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
|
||||
// Populate Team 1
|
||||
{% if match %}
|
||||
initialTeam1Ids.forEach(function(pid) {
|
||||
var teamDiv = document.getElementById('team1-selection');
|
||||
var playerName = playerDataById.player_data[pid];
|
||||
@@ -1007,5 +1062,35 @@ function randomizeTeams() {
|
||||
updateHiddenInputs();
|
||||
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>
|
||||
{% endblock %}
|
||||
@@ -283,6 +283,7 @@
|
||||
<th>Date</th>
|
||||
<th>Participants</th>
|
||||
<th>Time</th>
|
||||
<th>Presence</th>
|
||||
<th>Status</th>
|
||||
{% if can_edit %}
|
||||
<th>Actions</th>
|
||||
@@ -307,6 +308,28 @@
|
||||
</span>
|
||||
</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>
|
||||
{% if m.match_type == 'team_vs_team' %}
|
||||
<div class="match-teams">
|
||||
@@ -360,13 +383,6 @@
|
||||
{{ participants | join(', ') }}
|
||||
{% endif %}
|
||||
</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>
|
||||
<span class="badge badge-{{ m.status }}">{{ m.status }}</span>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user