ajout des disponibilités de joueurs et ajouts d'un tableau qui montre les dispo des joueurs lors de la création d'un match

This commit is contained in:
cedrick2711
2026-07-14 18:46:45 -04:00
parent 4e7ecf0ab9
commit b19d5db8db
17 changed files with 1706 additions and 218 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -32,11 +32,11 @@ def create_app():
with app.app_context(): with app.app_context():
import models import models
from models import User from models import User, MatchParticipant
try: try:
# Check if the database schema is up to date by testing a query # Check if the database schema is up to date by testing a query
# that uses all model columns # that uses all model columns including team_side for match participants
db.session.execute(text('SELECT games FROM users LIMIT 1')) db.session.execute(text('SELECT games, team_side FROM match_participants LIMIT 1'))
db.create_all() db.create_all()
except Exception: except Exception:
# If there's a schema mismatch, drop and recreate all tables # If there's a schema mismatch, drop and recreate all tables
-147
View File
@@ -1,147 +0,0 @@
{% extends "layouts/base.html" %}
{% block title %}My Profile - TryoutPro{% endblock %}
{% block page_title %}My Profile{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">Home / Profile</span>{% endblock %}
{% block header_actions %}
<div class="header-actions">
<a href="{{ url_for('users.edit_profile') }}" class="btn btn-sm btn-primary">
<i class="fas fa-edit"></i> Edit Profile
</a>
</div>
{% endblock %}
{% block content %}
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<h3><i class="fas fa-user-circle"></i> Account Information</h3>
</div>
<div class="card-body">
<div class="profile-header">
<div class="user-avatar avatar-xl">{{ user.full_name[:2] | upper }}</div>
<div class="profile-info">
<h2>{{ user.full_name }}</h2>
<span class="badge badge-{{ user.role }} badge-lg">{{ user.role | capitalize }}</span>
<p class="text-muted">Member since {% if user.created_at %}{{ user.created_at.strftime('%B %Y') }}{% else %}Unknown{% endif %}</p>
</div>
</div>
<div class="detail-grid mt-4">
<div class="detail-item">
<span class="detail-label">Username</span>
<span class="detail-value">{{ user.username }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Email</span>
<span class="detail-value">{{ user.email }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Phone</span>
<span class="detail-value">{{ user.phone or 'Not provided' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Account Status</span>
<span class="detail-value">
{% if user.is_active_account %}
<span class="badge badge-success">Active</span>
{% else %}
<span class="badge badge-danger">Inactive</span>
{% endif %}
</span>
</div>
</div>
</div>
</div>
<!-- E-Sports Profile Card -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-gamepad"></i> E-Sports Profile</h3>
</div>
<div class="card-body">
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label"><i class="fas fa-headset"></i> Games</span>
<span class="detail-value">
{% set games_list = user.get_games_list() %}
{% if games_list %}
{% for game in games_list %}
<span class="badge badge-esport">{{ game }}</span>
{% endfor %}
{% else %}
<span class="text-muted">Not specified</span>
{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label"><i class="fas fa-chart-line"></i> TRN (Tracker Network)</span>
<span class="detail-value">
{% if user.trn_username %}
<a href="https://tracker.gg" target="_blank" rel="noopener noreferrer" class="trn-link">
<i class="fas fa-external-link-alt"></i> {{ user.trn_username }}
</a>
{% else %}
<span class="text-muted">Not connected</span>
{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label"><i class="fab fa-discord"></i> Discord</span>
<span class="detail-value">
{% if user.discord_username %}
{{ user.discord_username }}
{% else %}
<span class="text-muted">Not connected</span>
{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label"><i class="fas fa-link"></i> League OS</span>
<span class="detail-value">
{% if user.league_os_profile %}
<a href="{{ user.league_os_profile }}" target="_blank" rel="noopener noreferrer" class="trn-link">
<i class="fas fa-external-link-alt"></i> {{ user.league_os_profile }}
</a>
{% else %}
<span class="text-muted">Not connected</span>
{% endif %}
</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3><i class="fas fa-chart-bar"></i> My Statistics</h3>
</div>
<div class="card-body">
{% if user.role == 'player' %}
<div class="profile-stats">
<div class="stat-item">
<h4>{{ user.tryout_registrations.count() }}</h4>
<p>Tryouts Registered</p>
</div>
<div class="stat-item">
<h4>{{ user.evaluations_received.count() }}</h4>
<p>Evaluations Received</p>
</div>
<div class="stat-item">
<h4>{{ user.team_assignments.count() }}</h4>
<p>Team Assignments</p>
</div>
</div>
{% elif user.can_evaluate() %}
<div class="profile-stats">
<div class="stat-item">
<h4>{{ user.evaluations_given.count() }}</h4>
<p>Evaluations Given</p>
</div>
</div>
{% else %}
<p class="text-muted">No statistics available for this role.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
Binary file not shown.
+14
View File
@@ -217,6 +217,20 @@ class Match(db.Model):
return [p.player_id for p in self.participants.all()] return [p.player_id for p in self.participants.all()]
class PlayerDisponibility(db.Model):
"""Player availability in 30-minute time blocks for scheduling matches."""
__tablename__ = 'player_disponibilities'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
day_of_week = db.Column(db.Integer, nullable=False) # 0=Monday, 6=Sunday
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False) # Always 30 minutes after start_time
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
player = db.relationship('User', backref='disponibilities')
class MatchParticipant(db.Model): class MatchParticipant(db.Model):
"""Players participating in player scrimmage matches.""" """Players participating in player scrimmage matches."""
__tablename__ = 'match_participants' __tablename__ = 'match_participants'
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+100 -8
View File
@@ -1,8 +1,8 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
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, Match, MatchParticipant, Team, TeamMember, OrgTeam, TryoutRegistration from models import User, Tryout, Match, MatchParticipant, Team, TeamMember, OrgTeam, TryoutRegistration, PlayerDisponibility
from datetime import datetime, time from datetime import datetime, time, timedelta
matches_bp = Blueprint('matches', __name__, url_prefix='/matches') matches_bp = Blueprint('matches', __name__, url_prefix='/matches')
@@ -60,7 +60,10 @@ def api_events():
match_desc = participants_str + (f"<br>{match.description}" if match.description else '') match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
else: else:
# Player scrim - show all participants # Player scrim - show all participants
player_names = [p.player.full_name for p in match.participants.all()] player_names = []
for p in match.participants.all():
player_name = p.player.full_name if p.player else 'Unknown Player'
player_names.append(player_name)
participants_str = ', '.join(player_names) if player_names else 'No players' participants_str = ', '.join(player_names) if player_names else 'No players'
match_desc = participants_str + (f"<br>{match.description}" if match.description else '') match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
@@ -153,14 +156,23 @@ def api_events_for_tryout(tryout_id):
participants_str = f"{' vs '.join(teams)}" participants_str = f"{' vs '.join(teams)}"
elif match.match_type == 'player_vs_player': elif match.match_type == 'player_vs_player':
# Get players grouped by team side # Get players grouped by team side
team1_players = [p.player.full_name for p in match.participants.filter_by(team_side=1).all()] team1_players = []
team2_players = [p.player.full_name for p in match.participants.filter_by(team_side=2).all()] for p in match.participants.filter_by(team_side=1).all():
if p.player:
team1_players.append(p.player.full_name)
team2_players = []
for p in match.participants.filter_by(team_side=2).all():
if p.player:
team2_players.append(p.player.full_name)
if team1_players and team2_players: if team1_players and team2_players:
participants_str = f"{', '.join(team1_players)} vs {', '.join(team2_players)}" participants_str = f"{', '.join(team1_players)} vs {', '.join(team2_players)}"
else: else:
participants_str = 'TBD vs TBD' participants_str = 'TBD vs TBD'
else: else:
player_names = [p.player.full_name for p in match.participants.all()] player_names = []
for p in match.participants.all():
player_name = p.player.full_name if p.player else 'Unknown Player'
player_names.append(player_name)
participants_str = ', '.join(player_names) if player_names else 'No players' participants_str = ', '.join(player_names) if player_names else 'No players'
# Include time for calendar display # Include time for calendar display
@@ -244,6 +256,11 @@ def create_match(tryout_id):
location = request.form.get('location') location = request.form.get('location')
match_type = request.form.get('match_type') match_type = request.form.get('match_type')
# Start time is now mandatory
if not start_time_str:
flash('Start time is required. Please select a time slot.', 'danger')
return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players)
try: try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date
except (ValueError, TypeError): except (ValueError, TypeError):
@@ -253,10 +270,15 @@ def create_match(tryout_id):
start_time = None start_time = None
end_time = None end_time = None
try: try:
if start_time_str:
start_time = datetime.strptime(start_time_str, '%H:%M').time() start_time = datetime.strptime(start_time_str, '%H:%M').time()
# Auto-calculate end time if not provided (start + 30 minutes)
if end_time_str: if end_time_str:
end_time = datetime.strptime(end_time_str, '%H:%M').time() end_time = datetime.strptime(end_time_str, '%H:%M').time()
else:
# Auto-calculate end time as start + 30 minutes
start_dt = datetime.combine(date_obj, start_time)
end_dt = start_dt + timedelta(minutes=30)
end_time = end_dt.time()
except ValueError: except ValueError:
flash('Invalid time format.', 'danger') flash('Invalid time format.', 'danger')
return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players)
@@ -340,11 +362,21 @@ def edit_match(match_id):
flash('Invalid date format.', 'danger') flash('Invalid date format.', 'danger')
return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
# Start time is now mandatory
if not start_time_str:
flash('Start time is required. Please select a time slot.', 'danger')
return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
try: try:
if start_time_str:
match.start_time = datetime.strptime(start_time_str, '%H:%M').time() match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
# Auto-calculate end time if not provided (start + 30 minutes)
if end_time_str: if end_time_str:
match.end_time = datetime.strptime(end_time_str, '%H:%M').time() match.end_time = datetime.strptime(end_time_str, '%H:%M').time()
else:
# Auto-calculate end time as start + 30 minutes
start_dt = datetime.combine(match.date, match.start_time)
end_dt = start_dt + timedelta(minutes=30)
match.end_time = end_dt.time()
except ValueError: except ValueError:
pass pass
@@ -401,3 +433,63 @@ def delete_match(match_id):
db.session.commit() db.session.commit()
flash('Match deleted successfully.', 'success') flash('Match deleted successfully.', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
def get_players_available_at_time(date_str, time_str):
"""Get list of player IDs available at a specific date and time.
Args:
date_str: Date in YYYY-MM-DD format
time_str: Time in HH:MM format
Returns:
List of player IDs who are available at that time
"""
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
time_obj = datetime.strptime(time_str, '%H:%M').time()
except (ValueError, TypeError):
return []
# Calculate day of week (Python: 0=Monday, 6=Sunday)
# JavaScript: 0=Sunday, 6=Saturday, so we convert
date_parts = date_str.split('-')
date_for_day = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2]))
js_day = date_for_day.weekday()
# Convert Python weekday (Mon=0) to our format (Mon=0)
day_of_week = js_day
# Get all active players
players = User.query.filter_by(role='player', is_active_account=True).all()
available_players = []
for player in players:
# Check if player has disponibility at this time
disponibilities = PlayerDisponibility.query.filter_by(
player_id=player.id,
day_of_week=day_of_week
).all()
for disp in disponibilities:
# Check if time falls within disponibility block
disp_start = disp.start_time.hour * 60 + disp.start_time.minute
disp_end = disp.end_time.hour * 60 + disp.end_time.minute
match_time = time_obj.hour * 60 + time_obj.minute
if disp_start <= match_time < disp_end:
available_players.append(player.id)
break
return available_players
@matches_bp.route('/api/available_players/<date>/<time>')
@login_required
def api_available_players(date, time):
"""API endpoint to get players available at a specific date/time slot."""
if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
return jsonify({'error': 'Unauthorized'}), 403
player_ids = get_players_available_at_time(date, time)
return jsonify({'available_player_ids': player_ids})
+176 -5
View File
@@ -1,7 +1,8 @@
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 flask_login import login_required, current_user
from extensions import db, hash_password from extensions import db, hash_password
from models import User, ROLES, ESPORT_GAMES from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility
from datetime import datetime, timedelta
users_bp = Blueprint('users', __name__, url_prefix='/users') users_bp = Blueprint('users', __name__, url_prefix='/users')
@@ -35,7 +36,6 @@ def edit_user(user_id):
flash('Invalid role selected.', 'danger') flash('Invalid role selected.', 'danger')
return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES) return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES)
# E-Sports fields
selected_games = request.form.getlist('games') selected_games = request.form.getlist('games')
trn_username = request.form.get('trn_username', '').strip() trn_username = request.form.get('trn_username', '').strip()
discord_username = request.form.get('discord_username', '').strip() discord_username = request.form.get('discord_username', '').strip()
@@ -134,13 +134,11 @@ def edit_profile():
email = request.form.get('email') email = request.form.get('email')
phone = request.form.get('phone') phone = request.form.get('phone')
# E-Sports fields
selected_games = request.form.getlist('games') selected_games = request.form.getlist('games')
trn_username = request.form.get('trn_username', '').strip() trn_username = request.form.get('trn_username', '').strip()
discord_username = request.form.get('discord_username', '').strip() discord_username = request.form.get('discord_username', '').strip()
league_os_profile = request.form.get('league_os_profile', '').strip() league_os_profile = request.form.get('league_os_profile', '').strip()
# Check email uniqueness if changed
if email != current_user.email and User.query.filter_by(email=email).first(): if email != current_user.email and User.query.filter_by(email=email).first():
flash('Email already in use.', 'danger') flash('Email already in use.', 'danger')
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES) return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES)
@@ -162,3 +160,176 @@ def edit_profile():
return redirect(url_for('users.profile')) return redirect(url_for('users.profile'))
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES) return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES)
# Day names for disponibility
DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
def add_30_minutes(t):
"""Add 30 minutes to a time object."""
return (datetime.combine(datetime.today(), t) + timedelta(minutes=30)).time()
@users_bp.route('/disponibilities')
@login_required
def get_disponibilities():
"""API endpoint to get all player disponibilities for scheduling."""
# Only managers and above can view disponibilities for scheduling
if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
return jsonify({'error': 'Unauthorized'}), 403
players = User.query.filter_by(role='player', is_active_account=True).order_by(User.full_name).all()
result = {}
for player in players:
disponibilities = list(player.disponibilities)
result[player.id] = {
'full_name': player.full_name,
'disponibilities': [
{
'id': d.id,
'day_of_week': d.day_of_week,
'day_name': DAY_NAMES[d.day_of_week],
'start_time': d.start_time.strftime('%H:%M'),
'end_time': d.end_time.strftime('%H:%M')
}
for d in disponibilities
]
}
return jsonify(result)
@users_bp.route('/disponibilities/my')
@login_required
def get_my_disponibilities():
"""API endpoint for players to get their own disponibilities."""
disponibilities = PlayerDisponibility.query.filter_by(player_id=current_user.id).all()
# Group by day for easier display
result = {}
for d in disponibilities:
day = d.day_of_week
if day not in result:
result[day] = []
result[day].append({
'id': d.id,
'day_of_week': d.day_of_week,
'day_name': DAY_NAMES[d.day_of_week],
'start_time': d.start_time.strftime('%H:%M'),
'end_time': d.end_time.strftime('%H:%M')
})
return jsonify(result)
@users_bp.route('/disponibilities/add', methods=['POST'])
@login_required
def add_disponibility():
"""Add a disponibility block for the current player."""
day_of_week = request.form.get('day_of_week', type=int)
start_time_str = request.form.get('start_time')
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
return jsonify({'error': 'Invalid day of week'}), 400
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
except (ValueError, TypeError):
return jsonify({'error': 'Invalid time format'}), 400
end_time = add_30_minutes(start_time)
disponibility = PlayerDisponibility(
player_id=current_user.id,
day_of_week=day_of_week,
start_time=start_time,
end_time=end_time
)
db.session.add(disponibility)
db.session.commit()
return jsonify({
'id': disponibility.id,
'day_of_week': disponibility.day_of_week,
'day_name': DAY_NAMES[disponibility.day_of_week],
'start_time': disponibility.start_time.strftime('%H:%M'),
'end_time': disponibility.end_time.strftime('%H:%M')
})
@users_bp.route('/disponibilities/add_bulk', methods=['POST'])
@login_required
def add_disponibilities_bulk():
"""Add multiple disponibility blocks at once (for grid selection)."""
data = request.get_json()
slots = data.get('slots', []) # List of {day_of_week, start_time}
created = []
for slot in slots:
day_of_week = slot.get('day_of_week')
start_time_str = slot.get('start_time')
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
continue
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
except (ValueError, TypeError):
continue
end_time = add_30_minutes(start_time)
# Check if this slot already exists for this player
existing = PlayerDisponibility.query.filter_by(
player_id=current_user.id,
day_of_week=day_of_week,
start_time=start_time
).first()
if not existing:
disponibility = PlayerDisponibility(
player_id=current_user.id,
day_of_week=day_of_week,
start_time=start_time,
end_time=end_time
)
db.session.add(disponibility)
db.session.flush()
created.append({
'id': disponibility.id,
'day_of_week': disponibility.day_of_week,
'day_name': DAY_NAMES[disponibility.day_of_week],
'start_time': disponibility.start_time.strftime('%H:%M'),
'end_time': disponibility.start_time.strftime('%H:%M')
})
db.session.commit()
return jsonify({'success': True, 'created': created})
@users_bp.route('/disponibilities/clear', methods=['POST'])
@login_required
def clear_disponibilities():
"""Clear all disponibilities for the current player (for resetting)."""
PlayerDisponibility.query.filter_by(player_id=current_user.id).delete()
db.session.commit()
return jsonify({'success': True})
@users_bp.route('/disponibilities/<int:disponibility_id>/delete', methods=['POST'])
@login_required
def delete_disponibility(disponibility_id):
"""Delete a disponibility block."""
disponibility = PlayerDisponibility.query.get_or_404(disponibility_id)
# Only the owner can delete their disponibility
if disponibility.player_id != current_user.id:
return jsonify({'error': 'Unauthorized'}), 403
db.session.delete(disponibility)
db.session.commit()
return jsonify({'success': True})
+50 -12
View File
@@ -1,11 +1,15 @@
from extensions import db, hash_password
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam
from datetime import datetime, timedelta
import random
from sqlalchemy import text from sqlalchemy import text
from extensions import db, hash_password
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility
from datetime import datetime, timedelta, time
import random
def seed_database(): def seed_database():
# Clear existing data # Clear existing data
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_members')) db.session.execute(text('DELETE FROM team_members'))
db.session.execute(text('DELETE FROM teams')) db.session.execute(text('DELETE FROM teams'))
db.session.execute(text('DELETE FROM evaluations')) db.session.execute(text('DELETE FROM evaluations'))
@@ -67,7 +71,7 @@ def seed_database():
users.append(user) users.append(user)
db.session.commit() db.session.commit()
print(f" Created {len(users)} users") print(f"[OK] Created {len(users)} users")
# Map users for easy access # Map users for easy access
user_map = {u.username: u for u in users} user_map = {u.username: u for u in users}
@@ -96,7 +100,7 @@ def seed_database():
db.session.add(ot) db.session.add(ot)
org_teams.append(ot) org_teams.append(ot)
db.session.commit() db.session.commit()
print(f" Created {len(org_teams)} organization teams") print(f"[OK] Created {len(org_teams)} organization teams")
# Create tryouts # Create tryouts
tryouts_data = [ tryouts_data = [
@@ -121,7 +125,7 @@ def seed_database():
db.session.add(t) db.session.add(t)
tryouts.append(t) tryouts.append(t)
db.session.commit() db.session.commit()
print(f" Created {len(tryouts)} tryouts") print(f"[OK] Created {len(tryouts)} tryouts")
# Register players for tryouts # Register players for tryouts
registrations_data = [ registrations_data = [
@@ -142,7 +146,7 @@ def seed_database():
db.session.add(reg) db.session.add(reg)
regs.append(reg) regs.append(reg)
db.session.commit() db.session.commit()
print(f" Created {len(regs)} registrations") print(f"[OK] Created {len(regs)} registrations")
# Create evaluations (for in_progress and completed tryouts) # Create evaluations (for in_progress and completed tryouts)
eval_data = [] eval_data = []
@@ -200,7 +204,7 @@ def seed_database():
eval_data.append(eval_entry) eval_data.append(eval_entry)
db.session.commit() db.session.commit()
print(f" Created {len(eval_data)} evaluations") print(f"[OK] Created {len(eval_data)} evaluations")
# Create teams for the completed tryout (tryout-specific teams) # Create teams for the completed tryout (tryout-specific teams)
team1 = Team(tryout_id=tryouts[2].id, name='Alpha Team', created_by=president.id) team1 = Team(tryout_id=tryouts[2].id, name='Alpha Team', created_by=president.id)
@@ -222,7 +226,7 @@ def seed_database():
tm = TeamMember(team_id=team_id, player_id=player_id, position=position) tm = TeamMember(team_id=team_id, player_id=player_id, position=position)
db.session.add(tm) db.session.add(tm)
db.session.commit() db.session.commit()
print(" Created 2 tryout-specific teams with player assignments") print("[OK] Created 2 tryout-specific teams with player assignments")
# Assign players to organization teams # Assign players to organization teams
org_team_assignments = [ org_team_assignments = [
@@ -238,9 +242,40 @@ def seed_database():
for org_team, player in org_team_assignments: for org_team, player in org_team_assignments:
player.team_id = org_team.id player.team_id = org_team.id
db.session.commit() db.session.commit()
print("✓ Assigned players to organization teams")
print("\n🎉 Database seeded successfully!") # Create sample disponibilities for players
# Time slots from 5pm to 11pm (stored as 17:00-23:00)
time_slots = [(17, 0), (17, 30), (18, 0), (18, 30), (19, 0), (19, 30), (20, 0), (20, 30), (21, 0), (21, 30), (22, 0), (22, 30), (23, 0)]
disponibilities = []
for player in players:
# Each player gets random disponibilities
for day in range(7): # All days of the week
num_slots = random.randint(3, 8)
chosen_slots = random.sample(time_slots, min(num_slots, len(time_slots)))
for hour, minute in chosen_slots:
start_time = time(hour, minute)
# End time is start_time + 30 minutes
end_minute = minute + 30
end_hour = hour
if end_minute >= 60:
end_minute -= 60
end_hour += 1
end_time = time(end_hour, end_minute)
d = PlayerDisponibility(
player_id=player.id,
day_of_week=day,
start_time=start_time,
end_time=end_time
)
db.session.add(d)
disponibilities.append(d)
db.session.commit()
print(f"[OK] Created {len(disponibilities)} player disponibilities")
print("\n[SUCCESS] Database seeded successfully!")
print("\n=== Login Credentials ===") print("\n=== Login Credentials ===")
print("President: username='president', password='password'") print("President: username='president', password='password'")
print("Manager: username='manager1', password='password'") print("Manager: username='manager1', password='password'")
@@ -251,4 +286,7 @@ def seed_database():
print("Scout: username='scout1', password='password'") print("Scout: username='scout1', password='password'")
if __name__ == '__main__': if __name__ == '__main__':
from app import create_app
app = create_app()
with app.app_context():
seed_database() seed_database()
+149
View File
@@ -1012,3 +1012,152 @@ a:hover { color: var(--primary-dark); }
.flash-messages { padding: 0 16px; } .flash-messages { padding: 0 16px; }
.team-rosters { flex-direction: column; } .team-rosters { flex-direction: column; }
} }
/* Merged Availability Grid for Match Scheduling */
.merged-disponibility-grid {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.merged-disponibility-day-row {
display: flex;
align-items: center;
gap: 12px;
}
.merged-disponibility-day-label {
width: 100px;
font-weight: 600;
font-size: 0.9rem;
color: var(--gray-700);
}
.merged-disponibility-time-blocks {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
}
.merged-disponibility-time-block {
width: 90px;
height: 36px;
border-radius: var(--radius-sm);
border: 1px solid var(--gray-300);
background: var(--gray-100);
text-align: center;
line-height: 36px;
font-size: 11px;
cursor: pointer;
user-select: none;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.merged-disponibility-time-block:hover {
transform: scale(1.05);
box-shadow: var(--shadow-md);
}
.merged-disponibility-time-block.selected {
background: var(--success);
color: white;
border-color: var(--success);
}
.merged-disponibility-time-block.high-availability {
background: var(--success);
color: white;
border-color: var(--success);
}
.merged-disponibility-time-block.medium-availability {
background: var(--warning);
color: white;
border-color: var(--warning);
}
.merged-disponibility-time-block.low-availability {
background: var(--gray-200);
color: var(--gray-600);
}
.merged-disponibility-count {
position: absolute;
top: 2px;
right: 4px;
font-size: 10px;
font-weight: 600;
opacity: 0.8;
}
.merged-disponibility-selected-display {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: var(--success-light);
border-radius: var(--radius-sm);
margin-bottom: 16px;
}
.merged-disponibility-selected-display.hidden {
display: none;
}
.time-selection-hint {
font-size: 0.85rem;
color: var(--gray-500);
margin-top: 4px;
}
.randomize-section {
background: var(--gray-50);
border-radius: var(--radius-sm);
padding: 16px;
margin-bottom: 16px;
}
.randomize-controls {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.randomize-input {
width: 60px;
padding: 6px 8px;
border: 1px solid var(--gray-300);
border-radius: var(--radius-sm);
font-size: 0.9rem;
}
.randomize-preview {
font-size: 0.85rem;
color: var(--gray-600);
}
.randomize-btn {
background: var(--warning);
color: white;
}
.randomize-btn:hover {
background: #d97706;
}
.selected-time-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: var(--success);
color: white;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
+496 -28
View File
@@ -9,10 +9,10 @@
<h3><i class="fas fa-futbol"></i> Schedule Match for {{ tryout.title }}</h3> <h3><i class="fas fa-futbol"></i> Schedule Match for {{ tryout.title }}</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="form"> <form method="POST" action="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="form" id="createMatchForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-group"> <div class="form-group">
<label for="match_type">Match Type</label> <label for="match_type">Match Type</label>
<select name="match_type" id="match_type" class="form-select" onchange="toggleMatchType()" required> <select name="match_type" id="match_type" class="form-select" onchange="toggleMatchType()" required>
<option value="team_vs_team">Team vs Team</option> <option value="team_vs_team">Team vs Team</option>
@@ -21,32 +21,61 @@
</select> </select>
</div> </div>
<div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="title">Match Title</label> <label for="title">Match Title</label>
<input type="text" name="title" id="title" class="form-input" placeholder="e.g., Alpha vs Bravo Scrimmage" required> <input type="text" name="title" id="title" class="form-input" placeholder="e.g., Alpha vs Bravo Scrimmage" required>
</div> </div>
<div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="date">Date</label> <label for="date">Date</label>
<input type="date" name="date" id="date" class="form-input" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required> <input type="date" name="date" id="date" class="form-input" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required>
</div> </div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_time">Start Time (Optional)</label>
<input type="time" name="start_time" id="start_time" class="form-input">
</div>
<div class="form-group">
<label for="end_time">End Time (Optional)</label>
<input type="time" name="end_time" id="end_time" class="form-input">
</div>
</div>
<div class="form-group"> <div class="form-group">
<label for="location">Location</label> <label for="location">Location</label>
<input type="text" name="location" id="location" class="form-input" placeholder="Match location"> <input type="text" name="location" id="location" class="form-input" placeholder="Match location">
</div> </div>
</div>
<!-- Merged Availability Grid -->
{% 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. Available players will be auto-selected below.</p>
<div id="merged-disponibility-selected" class="merged-disponibility-selected-display hidden">
<span>Selected time:</span>
<span class="selected-time-badge" id="selected-time-badge"></span>
<button type="button" class="btn btn-sm btn-outline" onclick="clearTimeSelection()">Change</button>
</div>
<div id="merged-disponibility-grid" class="merged-disponibility-grid">
<p class="text-muted">Loading availability...</p>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_time">Start Time <span class="text-muted">(Required)</span></label>
<input type="time" name="start_time" id="start_time" class="form-input" required>
</div>
<div class="form-group">
<label for="end_time">End Time <span class="text-muted">(Set by clicking consecutive slots)</span></label>
<input type="time" name="end_time" id="end_time" class="form-input" required>
</div>
</div>
{% else %}
<!-- Fallback time inputs for users without disponibility access -->
<div class="form-row">
<div class="form-group">
<label for="start_time">Start Time</label>
<input type="time" name="start_time" id="start_time" class="form-input">
</div>
<div class="form-group">
<label for="end_time">End Time</label>
<input type="time" name="end_time" id="end_time" class="form-input">
</div>
</div>
{% endif %}
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
@@ -85,25 +114,43 @@
<hr class="section-divider"> <hr class="section-divider">
<h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4> <h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4>
<!-- Randomize Teams Section -->
<div class="randomize-section">
<div class="randomize-controls">
<label><i class="fas fa-random"></i> Randomize Teams:</label>
<input type="number" id="team1-size" class="randomize-input" min="1" value="1" onchange="updateRandomizePreview()">
<span>vs</span>
<span id="team2-size-preview" class="randomize-preview">0</span>
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
<i class="fas fa-random"></i> Randomize
</button>
</div>
<p class="text-muted small" id="randomize-hint">Select players then click Randomize to split them into teams.</p>
</div>
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Team 1 Players</label> <label>Team 1 Players</label>
<div class="checkbox-grid"> <div class="checkbox-grid" id="team1-players-list">
{% for player in all_players %} {% for player in all_players %}
<label class="checkbox-label"> <label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
<input type="checkbox" name="team1_player_ids" value="{{ player.id }}" onchange="handleTeamSelection(this, 1)"> <input type="checkbox" name="team1_player_ids" value="{{ player.id }}" onchange="handleTeamSelection(this, 1)">
{{ player.full_name }} {{ player.full_name }}
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Team 2 Players</label> <label>Team 2 Players</label>
<div class="checkbox-grid"> <div class="checkbox-grid" id="team2-players-list">
{% for player in all_players %} {% for player in all_players %}
<label class="checkbox-label"> <label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
<input type="checkbox" name="team2_player_ids" value="{{ player.id }}" onchange="handleTeamSelection(this, 2)"> <input type="checkbox" name="team2_player_ids" value="{{ player.id }}" onchange="handleTeamSelection(this, 2)">
{{ player.full_name }} {{ player.full_name }}
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
@@ -116,13 +163,16 @@
<hr class="section-divider"> <hr class="section-divider">
<h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4> <h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4>
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
<div class="form-group"> <div class="form-group">
<label>Players</label> <label>Players</label>
<div class="checkbox-grid"> <div class="checkbox-grid" id="scrim-players-list">
{% for player in all_players %} {% for player in all_players %}
<label class="checkbox-label"> <label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
<input type="checkbox" name="player_ids" value="{{ player.id }}"> <input type="checkbox" name="player_ids" value="{{ player.id }}">
{{ player.full_name }} {{ player.full_name }}
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
@@ -144,6 +194,388 @@
{% block scripts %} {% block scripts %}
<script> <script>
// Time slots from 5pm (17:00) to 12am (24:00)
var TIME_SLOTS = [];
for (var h = 17; h <= 24; h++) {
for (var m = 0; m < 60; m += 30) {
if (h === 24 && m > 0) continue;
var displayHour;
var displayAmpm;
if (h === 24) {
displayHour = 12;
displayAmpm = 'AM';
} else if (h > 12) {
displayHour = h - 12;
displayAmpm = 'PM';
} else {
displayHour = h;
displayAmpm = 'PM';
}
var timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
var displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
TIME_SLOTS.push({ time: timeStr, display: displayTime });
}
}
var DAYS = [
{ value: 0, name: 'Monday' },
{ value: 1, name: 'Tuesday' },
{ value: 2, name: 'Wednesday' },
{ value: 3, name: 'Thursday' },
{ value: 4, name: 'Friday' },
{ value: 5, name: 'Saturday' },
{ value: 6, name: 'Sunday' }
];
var selectedDate = '';
var selectedSlots = []; // Array of selected time slots: ['17:00', '17:30', '18:00']
var allDisponibilities = {};
var canViewDisponibilities = {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}true{% else %}false{% endif %};
var totalPlayers = {{ all_players|length }};
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Only load disponibilities if user has permission
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
fetchDisponibilities();
{% endif %}
// Set up date change listener
document.getElementById('date').addEventListener('change', function() {
selectedDate = this.value;
if (allDisponibilitiesInitialized()) {
renderMergedDisponibilityGrid();
}
});
// Initialize date
selectedDate = document.getElementById('date').value;
// Initialize match type sections
toggleMatchType();
});
function allDisponibilitiesInitialized() {
return Object.keys(allDisponibilities).length > 0;
}
function fetchDisponibilities() {
if (!canViewDisponibilities) return;
fetch('{{ url_for("users.get_disponibilities") }}')
.then(function(response) { return response.json(); })
.then(function(data) {
allDisponibilities = data;
renderMergedDisponibilityGrid();
})
.catch(function(error) {
console.error('Error fetching disponibilities:', error);
});
}
function renderMergedDisponibilityGrid() {
var grid = document.getElementById('merged-disponibility-grid');
if (!grid || !canViewDisponibilities) return;
// Calculate day of week from selected date
var dateParts = selectedDate.split('-');
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
var jsDay = dateObj.getDay();
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1; // Convert to Mon=0 format
var html = '<div style="margin-bottom: 10px;"><strong>Click time slots consecutively to set match duration</strong></div>';
// Always show only the selected day (not all days)
DAYS.forEach(function(day) {
if (day.value !== dayOfWeek) return; // Skip days that don't match
var dayRow = '<div class="merged-disponibility-day-row">';
dayRow += '<div class="merged-disponibility-day-label">' + day.name + '</div>';
dayRow += '<div class="merged-disponibility-time-blocks">';
TIME_SLOTS.forEach(function(slot) {
var count = getAvailabilityCount(day.value, slot.time);
var percentage = totalPlayers > 0 ? count / totalPlayers : 0;
var cssClass = 'merged-disponibility-time-block';
if (percentage >= 0.75) {
cssClass += ' high-availability';
} else if (percentage >= 0.5) {
cssClass += ' medium-availability';
} else {
cssClass += ' low-availability';
}
// Check if this slot is in selectedSlots
if (selectedSlots.includes(slot.time)) {
cssClass += ' selected';
}
dayRow += '<div class="' + cssClass + '" data-day="' + day.value + '" data-time="' + slot.time + '" ' +
'onclick="toggleTimeSlot(' + day.value + ', \'' + slot.time + '\', this)">' +
slot.display +
'<span class="merged-disponibility-count">' + count + '</span>' +
'</div>';
});
dayRow += '</div></div>';
html += dayRow;
});
grid.innerHTML = html;
}
function getAvailabilityCount(dayOfWeek, timeStr) {
var count = 0;
var timeParts = timeStr.split(':');
var minutes = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
for (var playerId in allDisponibilities) {
var playerData = allDisponibilities[playerId];
if (playerData && playerData.disponibilities) {
var isAvailable = playerData.disponibilities.some(function(d) {
if (d.day_of_week !== dayOfWeek) return false;
var startParts = d.start_time.split(':');
var endParts = d.end_time.split(':');
var dispMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
var endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
return minutes >= dispMinutes && minutes < endMinutes;
});
if (isAvailable) count++;
}
}
return count;
}
function getSlotsForPlayer(playerId, dayOfWeek) {
/**Get list of available time slots for a specific player on a given day.
* Returns array of time strings in 'HH:MM' format.
*/
var playerData = allDisponibilities[playerId];
if (!playerData || !playerData.disponibilities) return [];
var availableSlots = [];
TIME_SLOTS.forEach(function(slot) {
var timeParts = slot.time.split(':');
var minutes = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
var isAvailable = playerData.disponibilities.some(function(d) {
if (d.day_of_week !== dayOfWeek) return false;
var startParts = d.start_time.split(':');
var endParts = d.end_time.split(':');
var dispMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
var endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
return minutes >= dispMinutes && minutes < endMinutes;
});
if (isAvailable) {
availableSlots.push(slot.time);
}
});
return availableSlots;
}
function toggleTimeSlot(dayOfWeek, timeStr, element) {
var slotIndex = TIME_SLOTS.findIndex(function(s) { return s.time === timeStr; });
if (selectedSlots.length === 0) {
// First click - start new selection
selectedSlots = [timeStr];
} else {
var firstSelectedIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedSlots[0]; });
if (slotIndex === firstSelectedIndex) {
// Clicking the first selected slot again - keep it as single slot
selectedSlots = [timeStr];
} else if (slotIndex < firstSelectedIndex) {
// Clicking before the first slot - extend selection backward
var newSlots = [];
for (var i = slotIndex; i <= firstSelectedIndex; i++) {
newSlots.push(TIME_SLOTS[i].time);
}
selectedSlots = newSlots;
} else if (slotIndex > firstSelectedIndex) {
// Clicking after the first slot - extend selection forward
var newSlots = [];
for (var i = firstSelectedIndex; i <= slotIndex; i++) {
newSlots.push(TIME_SLOTS[i].time);
}
selectedSlots = newSlots;
} else {
// Already selected, toggle off
selectedSlots = selectedSlots.filter(function(t) { return t !== timeStr; });
}
}
// Update form inputs
var startTime = selectedSlots[0] || '';
var endTime = selectedSlots[selectedSlots.length - 1] || '';
// Calculate end time as 30 min after last slot (end of that block)
if (endTime) {
var timeParts = endTime.split(':');
var endHour = parseInt(timeParts[0]);
var endMin = parseInt(timeParts[1]) + 30;
if (endMin >= 60) {
endMin = 0;
endHour++;
}
endTime = (endHour < 10 ? '0' : '') + endHour + ':' + (endMin < 10 ? '0' : '') + endMin;
}
document.getElementById('start_time').value = startTime;
document.getElementById('end_time').value = endTime;
// Update selected display
if (selectedSlots.length > 0) {
document.getElementById('merged-disponibility-selected').classList.remove('hidden');
document.getElementById('selected-time-badge').textContent =
formatTimeDisplay(selectedSlots[0]) + ' - ' + formatTimeDisplay(endTime);
} else {
document.getElementById('merged-disponibility-selected').classList.add('hidden');
}
// Update grid highlighting
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
el.classList.remove('selected');
});
selectedSlots.forEach(function(slot) {
var selectedEl = document.querySelector('.merged-disponibility-time-block[data-time="' + slot + '"]');
if (selectedEl) selectedEl.classList.add('selected');
});
// Fetch and auto-select available players for all selected slots
if (selectedSlots.length > 0) {
fetchAvailablePlayersForSlots();
}
}
function formatTimeDisplay(timeStr) {
var parts = timeStr.split(':');
var h = parseInt(parts[0]);
var m = parts[1];
var displayHour;
var ampm;
if (h === 0) {
displayHour = 12;
ampm = 'AM';
} else if (h < 12) {
displayHour = h;
ampm = 'AM';
} else if (h === 12) {
displayHour = 12;
ampm = 'PM';
} else {
displayHour = h - 12;
ampm = 'PM';
}
return displayHour + ':' + m + ' ' + ampm;
}
function fetchAvailablePlayersForSlots() {
if (!selectedDate || selectedSlots.length === 0) return;
// Get day of week
var dateParts = selectedDate.split('-');
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
var jsDay = dateObj.getDay();
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
// Find players available at ANY of the selected slots
var availableIds = [];
for (var playerId in allDisponibilities) {
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
var hasSlot = selectedSlots.some(function(slot) {
return playerSlots.includes(slot);
});
if (hasSlot) {
availableIds.push(parseInt(playerId));
}
}
// Auto-select available players
var matchType = document.getElementById('match_type').value;
if (matchType === 'player_vs_player') {
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
cb.checked = availableIds.includes(parseInt(cb.value));
});
} else if (matchType === 'player_scrim') {
document.querySelectorAll('input[name="player_ids"]').forEach(function(cb) {
cb.checked = availableIds.includes(parseInt(cb.value));
});
}
updateRandomizePreview();
}
function clearTimeSelection() {
selectedSlots = [];
document.getElementById('start_time').value = '';
document.getElementById('end_time').value = '';
document.getElementById('merged-disponibility-selected').classList.add('hidden');
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
el.classList.remove('selected');
});
}
function updateRandomizePreview() {
var checkedCount = document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="player_ids"]:checked').length;
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
var team2Size = checkedCount - team1Size;
if (team2Size < 0) team2Size = 0;
document.getElementById('team2-size-preview').textContent = team2Size;
}
function randomizeTeams() {
var allChecked = [];
// Get all checked players
document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').forEach(function(cb) {
allChecked.push(parseInt(cb.value));
});
if (allChecked.length === 0) {
alert('Please select at least one player before randomizing teams.');
return;
}
// Shuffle players
for (var i = allChecked.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = allChecked[i];
allChecked[i] = allChecked[j];
allChecked[j] = temp;
}
// Split based on team1-size input
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
var team1Players = allChecked.slice(0, team1Size);
var team2Players = allChecked.slice(team1Size);
// Uncheck all first
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
cb.checked = false;
});
// Check players in their respective teams
team1Players.forEach(function(pid) {
var cb = document.querySelector('input[name="team1_player_ids"][value="' + pid + '"]');
if (cb) cb.checked = true;
});
team2Players.forEach(function(pid) {
var cb = document.querySelector('input[name="team2_player_ids"][value="' + pid + '"]');
if (cb) cb.checked = true;
});
updateRandomizePreview();
}
function toggleMatchType() { function toggleMatchType() {
var matchType = document.getElementById('match_type').value; var matchType = document.getElementById('match_type').value;
var teamSection = document.getElementById('team-vs-team-section'); var teamSection = document.getElementById('team-vs-team-section');
@@ -163,25 +595,61 @@ function toggleMatchType() {
} else if (matchType === 'player_scrim') { } else if (matchType === 'player_scrim') {
scrimSection.classList.remove('hidden'); scrimSection.classList.remove('hidden');
} }
// Update indicators when match type changes
updateDisponibilityIndicators();
} }
function handleTeamSelection(checkbox, teamSide) { function handleTeamSelection(checkbox, teamSide) {
// When selecting from one team, deselect from the other team
var otherSide = teamSide === 1 ? 2 : 1; var otherSide = teamSide === 1 ? 2 : 1;
var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]'); var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]');
var playerId = checkbox.value; var playerId = checkbox.value;
// Find the corresponding checkbox in the other team and uncheck it
otherCheckboxes.forEach(function(other) { otherCheckboxes.forEach(function(other) {
if (other.value === playerId) { if (other.value === playerId) {
other.checked = false; other.checked = false;
} }
}); });
updateRandomizePreview();
} }
// Initialize on page load function updateDisponibilityIndicators() {
document.addEventListener('DOMContentLoaded', function() { if (!canViewDisponibilities) return;
toggleMatchType();
}); var dateInput = document.getElementById('date').value;
var startTimeInput = document.getElementById('start_time').value;
if (!dateInput || !startTimeInput) return;
var dateParts = dateInput.split('-');
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
var jsDay = dateObj.getDay();
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
// Find the first selected start slot
var slotIndex = TIME_SLOTS.findIndex(function(s) { return s.time === startTimeInput; });
document.querySelectorAll('.disponibility-indicator').forEach(function(indicator) {
var playerId = indicator.getAttribute('data-player-id');
var playerData = allDisponibilities[playerId];
if (playerData && playerData.disponibilities) {
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
var isAvailable = selectedSlots.some(function(slot) {
return playerSlots.includes(slot);
});
indicator.style.display = 'inline-block';
indicator.style.width = '12px';
indicator.style.height = '12px';
indicator.style.borderRadius = '50%';
indicator.style.marginLeft = '8px';
indicator.style.backgroundColor = isAvailable ? '#10b981' : '#9ca3af';
} else {
indicator.style.display = 'none';
}
});
}
</script> </script>
{% endblock %} {% endblock %}
+481 -12
View File
@@ -9,7 +9,7 @@
<h3><i class="fas fa-futbol"></i> Edit Match</h3> <h3><i class="fas fa-futbol"></i> Edit Match</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ url_for('matches.edit_match', match_id=match.id) }}" class="form"> <form method="POST" action="{{ url_for('matches.edit_match', match_id=match.id) }}" class="form" id="editMatchForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-group"> <div class="form-group">
@@ -32,6 +32,34 @@
</div> </div>
</div> </div>
<!-- Merged Availability Grid -->
{% 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. Available players will be auto-selected below.</p>
<div id="merged-disponibility-selected" class="merged-disponibility-selected-display hidden">
<span>Selected time:</span>
<span class="selected-time-badge" id="selected-time-badge"></span>
<button type="button" class="btn btn-sm btn-outline" onclick="clearTimeSelection()">Change</button>
</div>
<div id="merged-disponibility-grid" class="merged-disponibility-grid">
<p class="text-muted">Loading...</p>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_time">Start Time <span class="text-muted">(Required)</span></label>
<input type="time" name="start_time" id="start_time" class="form-input" value="{{ match.start_time.strftime('%H:%M') if match.start_time else '' }}" required>
</div>
<div class="form-group">
<label for="end_time">End Time <span class="text-muted">(Set by clicking consecutive slots)</span></label>
<input type="time" name="end_time" id="end_time" class="form-input" value="{{ match.end_time.strftime('%H:%M') if match.end_time else '' }}" required>
</div>
</div>
{% else %}
<!-- Fallback time inputs for users without disponibility access -->
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="start_time">Start Time</label> <label for="start_time">Start Time</label>
@@ -42,6 +70,7 @@
<input type="time" name="end_time" id="end_time" class="form-input" value="{{ match.end_time.strftime('%H:%M') if match.end_time else '' }}"> <input type="time" name="end_time" id="end_time" class="form-input" value="{{ match.end_time.strftime('%H:%M') if match.end_time else '' }}">
</div> </div>
</div> </div>
{% endif %}
<div class="form-group"> <div class="form-group">
<label for="location">Location</label> <label for="location">Location</label>
@@ -53,7 +82,7 @@
<textarea name="description" id="description" class="form-textarea" placeholder="Optional notes about this match">{{ match.description or '' }}</textarea> <textarea name="description" id="description" class="form-textarea" placeholder="Optional notes about this match">{{ match.description or '' }}</textarea>
</div> </div>
<!-- Team vs Team Selection --> <!-- Team vs Team Selection -->
{% if match.match_type == 'team_vs_team' %} {% if match.match_type == 'team_vs_team' %}
<hr class="section-divider"> <hr class="section-divider">
<h4 class="section-title"><i class="fas fa-users"></i> Teams</h4> <h4 class="section-title"><i class="fas fa-users"></i> Teams</h4>
@@ -86,7 +115,7 @@
<h5>{{ match.team1.name }} Players</h5> <h5>{{ match.team1.name }} Players</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.full_name }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li> <li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
{% else %} {% else %}
<li class="text-muted">No players assigned</li> <li class="text-muted">No players assigned</li>
{% endfor %} {% endfor %}
@@ -98,7 +127,7 @@
<h5>{{ match.team2.name }} Players</h5> <h5>{{ match.team2.name }} Players</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.full_name }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li> <li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
{% else %} {% else %}
<li class="text-muted">No players assigned</li> <li class="text-muted">No players assigned</li>
{% endfor %} {% endfor %}
@@ -112,25 +141,45 @@
<hr class="section-divider"> <hr class="section-divider">
<h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4> <h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4>
<!-- Randomize Teams Section -->
<div class="randomize-section">
<div class="randomize-controls">
<label><i class="fas fa-random"></i> Randomize Teams:</label>
<input type="number" id="team1-size" class="randomize-input" min="1" value="1" onchange="updateRandomizePreview()">
<span>vs</span>
<span id="team2-size-preview" class="randomize-preview">0</span>
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
<i class="fas fa-random"></i> Randomize
</button>
</div>
<p class="text-muted small" id="randomize-hint">Select players then click Randomize to split them into teams.</p>
</div>
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
{% endif %}
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Team 1 Players</label> <label>Team 1 Players</label>
<div class="checkbox-grid"> <div class="checkbox-grid" id="team1-players-list">
{% for player in all_players %} {% for player in all_players %}
<label class="checkbox-label"> <label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
<input type="checkbox" name="team1_player_ids" value="{{ player.id }}" {% if player.id in team1_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 1)"> <input type="checkbox" name="team1_player_ids" value="{{ player.id }}" {% if player.id in team1_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 1)">
{{ player.full_name }} {{ player.full_name }}
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Team 2 Players</label> <label>Team 2 Players</label>
<div class="checkbox-grid"> <div class="checkbox-grid" id="team2-players-list">
{% for player in all_players %} {% for player in all_players %}
<label class="checkbox-label"> <label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
<input type="checkbox" name="team2_player_ids" value="{{ player.id }}" {% if player.id in team2_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 2)"> <input type="checkbox" name="team2_player_ids" value="{{ player.id }}" {% if player.id in team2_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 2)">
{{ player.full_name }} {{ player.full_name }}
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
@@ -142,13 +191,18 @@
<hr class="section-divider"> <hr class="section-divider">
<h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4> <h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4>
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
{% endif %}
<div class="form-group"> <div class="form-group">
<label>Players</label> <label>Players</label>
<div class="checkbox-grid"> <div class="checkbox-grid" id="scrim-players-list">
{% for player in all_players %} {% for player in all_players %}
<label class="checkbox-label"> <label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
<input type="checkbox" name="player_ids" value="{{ player.id }}" {% if player.id in current_player_ids %}checked{% endif %}> <input type="checkbox" name="player_ids" value="{{ player.id }}" {% if player.id in current_player_ids %}checked{% endif %}>
{{ player.full_name }} {{ player.full_name }}
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
@@ -170,18 +224,433 @@
{% block scripts %} {% block scripts %}
<script> <script>
// Time slots from 5pm (17:00) to 12am (24:00)
var TIME_SLOTS = [];
for (var h = 17; h <= 24; h++) {
for (var m = 0; m < 60; m += 30) {
if (h === 24 && m > 0) continue;
var displayHour;
var displayAmpm;
if (h === 24) {
displayHour = 12;
displayAmpm = 'AM';
} else if (h > 12) {
displayHour = h - 12;
displayAmpm = 'PM';
} else {
displayHour = h;
displayAmpm = 'PM';
}
var timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
var displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
TIME_SLOTS.push({ time: timeStr, display: displayTime });
}
}
var DAYS = [
{ value: 0, name: 'Monday' },
{ value: 1, name: 'Tuesday' },
{ value: 2, name: 'Wednesday' },
{ value: 3, name: 'Thursday' },
{ value: 4, name: 'Friday' },
{ value: 5, name: 'Saturday' },
{ value: 6, name: 'Sunday' }
];
var selectedDate = '';
var selectedSlots = []; // Array of selected time slots: ['17:00', '17:30', '18:00']
var allDisponibilities = {};
var canViewDisponibilities = {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}true{% else %}false{% endif %};
var totalPlayers = {{ all_players|length }};
// Initialize
document.addEventListener('DOMContentLoaded', function() {
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
fetchDisponibilities();
{% endif %}
document.getElementById('date').addEventListener('change', function() {
selectedDate = this.value;
if (allDisponibilitiesInitialized()) {
renderMergedDisponibilityGrid();
}
});
selectedDate = document.getElementById('date').value;
selectedStartTime = document.getElementById('start_time').value;
selectedEndTime = document.getElementById('end_time').value;
// Initialize selected slots from current match time
if (selectedStartTime) {
var startIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedStartTime; });
var endIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedEndTime; });
if (startIndex !== -1) {
// Calculate which slots are covered (each slot is 30 min, end time is 30 min after last slot)
if (endIndex === -1 || endIndex <= startIndex) {
selectedSlots = [selectedStartTime];
} else {
selectedSlots = [];
for (var i = startIndex; i <= endIndex; i++) {
selectedSlots.push(TIME_SLOTS[i].time);
}
}
document.getElementById('merged-disponibility-selected').classList.remove('hidden');
document.getElementById('selected-time-badge').textContent =
formatTimeDisplay(selectedStartTime) + ' - ' + formatTimeDisplay(selectedEndTime);
}
}
renderMergedDisponibilityGrid();
updateDisponibilityIndicators();
updateRandomizePreview();
});
function allDisponibilitiesInitialized() {
return Object.keys(allDisponibilities).length > 0;
}
function fetchDisponibilities() {
if (!canViewDisponibilities) return;
fetch('{{ url_for("users.get_disponibilities") }}')
.then(function(response) { return response.json(); })
.then(function(data) {
allDisponibilities = data;
renderMergedDisponibilityGrid();
})
.catch(function(error) {
console.error('Error fetching disponibilities:', error);
});
}
function renderMergedDisponibilityGrid() {
var grid = document.getElementById('merged-disponibility-grid');
if (!grid || !canViewDisponibilities) return;
var dateParts = selectedDate.split('-');
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
var jsDay = dateObj.getDay();
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
var html = '<div style="margin-bottom: 10px;"><strong>Click time slots consecutively to set match duration</strong></div>';
DAYS.forEach(function(day) {
if (day.value !== dayOfWeek) return;
var dayRow = '<div class="merged-disponibility-day-row">';
dayRow += '<div class="merged-disponibility-day-label">' + day.name + '</div>';
dayRow += '<div class="merged-disponibility-time-blocks">';
TIME_SLOTS.forEach(function(slot) {
var count = getAvailabilityCount(day.value, slot.time);
var percentage = totalPlayers > 0 ? count / totalPlayers : 0;
var cssClass = 'merged-disponibility-time-block';
if (percentage >= 0.75) {
cssClass += ' high-availability';
} else if (percentage >= 0.5) {
cssClass += ' medium-availability';
} else {
cssClass += ' low-availability';
}
if (selectedSlots.includes(slot.time)) {
cssClass += ' selected';
}
dayRow += '<div class="' + cssClass + '" data-day="' + day.value + '" data-time="' + slot.time + '" ' +
'onclick="toggleTimeSlot(' + day.value + ', \'' + slot.time + '\', this)">' +
slot.display +
'<span class="merged-disponibility-count">' + count + '</span>' +
'</div>';
});
dayRow += '</div></div>';
html += dayRow;
});
grid.innerHTML = html;
}
function getAvailabilityCount(dayOfWeek, timeStr) {
var count = 0;
var timeParts = timeStr.split(':');
var minutes = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
for (var playerId in allDisponibilities) {
var playerData = allDisponibilities[playerId];
if (playerData && playerData.disponibilities) {
var isAvailable = playerData.disponibilities.some(function(d) {
if (d.day_of_week !== dayOfWeek) return false;
var startParts = d.start_time.split(':');
var endParts = d.end_time.split(':');
var dispMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
var endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
return minutes >= dispMinutes && minutes < endMinutes;
});
if (isAvailable) count++;
}
}
return count;
}
function getSlotsForPlayer(playerId, dayOfWeek) {
var playerData = allDisponibilities[playerId];
if (!playerData || !playerData.disponibilities) return [];
var availableSlots = [];
TIME_SLOTS.forEach(function(slot) {
var timeParts = slot.time.split(':');
var minutes = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
var isAvailable = playerData.disponibilities.some(function(d) {
if (d.day_of_week !== dayOfWeek) return false;
var startParts = d.start_time.split(':');
var endParts = d.end_time.split(':');
var dispMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
var endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
return minutes >= dispMinutes && minutes < endMinutes;
});
if (isAvailable) {
availableSlots.push(slot.time);
}
});
return availableSlots;
}
function toggleTimeSlot(dayOfWeek, timeStr, element) {
var slotIndex = TIME_SLOTS.findIndex(function(s) { return s.time === timeStr; });
if (selectedSlots.length === 0) {
selectedSlots = [timeStr];
} else {
var firstSelectedIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedSlots[0]; });
if (slotIndex === firstSelectedIndex) {
selectedSlots = [timeStr];
} else if (slotIndex < firstSelectedIndex) {
var newSlots = [];
for (var i = slotIndex; i <= firstSelectedIndex; i++) {
newSlots.push(TIME_SLOTS[i].time);
}
selectedSlots = newSlots;
} else if (slotIndex > firstSelectedIndex) {
var newSlots = [];
for (var i = firstSelectedIndex; i <= slotIndex; i++) {
newSlots.push(TIME_SLOTS[i].time);
}
selectedSlots = newSlots;
} else {
selectedSlots = selectedSlots.filter(function(t) { return t !== timeStr; });
}
}
var startTime = selectedSlots[0] || '';
var endTime = selectedSlots[selectedSlots.length - 1] || '';
if (endTime) {
var timeParts = endTime.split(':');
var endHour = parseInt(timeParts[0]);
var endMin = parseInt(timeParts[1]) + 30;
if (endMin >= 60) {
endMin = 0;
endHour++;
}
endTime = (endHour < 10 ? '0' : '') + endHour + ':' + (endMin < 10 ? '0' : '') + endMin;
}
document.getElementById('start_time').value = startTime;
document.getElementById('end_time').value = endTime;
if (selectedSlots.length > 0) {
document.getElementById('merged-disponibility-selected').classList.remove('hidden');
document.getElementById('selected-time-badge').textContent =
formatTimeDisplay(selectedSlots[0]) + ' - ' + formatTimeDisplay(endTime);
} else {
document.getElementById('merged-disponibility-selected').classList.add('hidden');
}
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
el.classList.remove('selected');
});
selectedSlots.forEach(function(slot) {
var selectedEl = document.querySelector('.merged-disponibility-time-block[data-time="' + slot + '"]');
if (selectedEl) selectedEl.classList.add('selected');
});
if (selectedSlots.length > 0) {
fetchAvailablePlayersForSlots();
}
}
function formatTimeDisplay(timeStr) {
var parts = timeStr.split(':');
var h = parseInt(parts[0]);
var m = parts[1];
var displayHour;
var ampm;
if (h === 0) {
displayHour = 12;
ampm = 'AM';
} else if (h < 12) {
displayHour = h;
ampm = 'AM';
} else if (h === 12) {
displayHour = 12;
ampm = 'PM';
} else {
displayHour = h - 12;
ampm = 'PM';
}
return displayHour + ':' + m + ' ' + ampm;
}
function fetchAvailablePlayersForSlots() {
if (!selectedDate || selectedSlots.length === 0) return;
var dateParts = selectedDate.split('-');
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
var jsDay = dateObj.getDay();
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
var availableIds = [];
for (var playerId in allDisponibilities) {
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
var hasSlot = selectedSlots.some(function(slot) {
return playerSlots.includes(slot);
});
if (hasSlot) {
availableIds.push(parseInt(playerId));
}
}
var matchType = '{{ match.match_type }}';
if (matchType === 'player_vs_player') {
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
cb.checked = availableIds.includes(parseInt(cb.value));
});
} else if (matchType === 'player_scrim') {
document.querySelectorAll('input[name="player_ids"]').forEach(function(cb) {
cb.checked = availableIds.includes(parseInt(cb.value));
});
}
updateRandomizePreview();
}
function clearTimeSelection() {
selectedSlots = [];
document.getElementById('start_time').value = '';
document.getElementById('end_time').value = '';
document.getElementById('merged-disponibility-selected').classList.add('hidden');
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
el.classList.remove('selected');
});
}
function updateRandomizePreview() {
var checkedCount = document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="player_ids"]:checked').length;
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
var team2Size = checkedCount - team1Size;
if (team2Size < 0) team2Size = 0;
document.getElementById('team2-size-preview').textContent = team2Size;
}
function randomizeTeams() {
var allChecked = [];
document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').forEach(function(cb) {
allChecked.push(parseInt(cb.value));
});
if (allChecked.length === 0) {
alert('Please select at least one player before randomizing teams.');
return;
}
for (var i = allChecked.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = allChecked[i];
allChecked[i] = allChecked[j];
allChecked[j] = temp;
}
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
var team1Players = allChecked.slice(0, team1Size);
var team2Players = allChecked.slice(team1Size);
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
cb.checked = false;
});
team1Players.forEach(function(pid) {
var cb = document.querySelector('input[name="team1_player_ids"][value="' + pid + '"]');
if (cb) cb.checked = true;
});
team2Players.forEach(function(pid) {
var cb = document.querySelector('input[name="team2_player_ids"][value="' + pid + '"]');
if (cb) cb.checked = true;
});
updateRandomizePreview();
}
function handleTeamSelection(checkbox, teamSide) { function handleTeamSelection(checkbox, teamSide) {
// When selecting from one team, deselect from the other team
var otherSide = teamSide === 1 ? 2 : 1; var otherSide = teamSide === 1 ? 2 : 1;
var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]'); var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]');
var playerId = checkbox.value; var playerId = checkbox.value;
// Find the corresponding checkbox in the other team and uncheck it
otherCheckboxes.forEach(function(other) { otherCheckboxes.forEach(function(other) {
if (other.value === playerId) { if (other.value === playerId) {
other.checked = false; other.checked = false;
} }
}); });
updateRandomizePreview();
}
function updateDisponibilityIndicators() {
if (!canViewDisponibilities) return;
var dateInput = document.getElementById('date').value;
var startTimeInput = document.getElementById('start_time').value;
if (!dateInput || !startTimeInput) return;
var dateParts = dateInput.split('-');
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
var jsDay = dateObj.getDay();
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
document.querySelectorAll('.disponibility-indicator').forEach(function(indicator) {
var playerId = indicator.getAttribute('data-player-id');
var playerData = allDisponibilities[playerId];
if (playerData && playerData.disponibilities) {
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
var isAvailable = selectedSlots.some(function(slot) {
return playerSlots.includes(slot);
});
indicator.style.display = 'inline-block';
indicator.style.width = '12px';
indicator.style.height = '12px';
indicator.style.borderRadius = '50%';
indicator.style.marginLeft = '8px';
indicator.style.backgroundColor = isAvailable ? '#10b981' : '#9ca3af';
} else {
indicator.style.display = 'none';
}
});
} }
</script> </script>
{% endblock %} {% endblock %}
+234
View File
@@ -65,6 +65,26 @@
<label for="password">New Password</label> <label for="password">New Password</label>
<input type="password" id="password" name="password" placeholder="Enter new password"> <input type="password" id="password" name="password" placeholder="Enter new password">
</div> </div>
{% if user.role == 'player' %}
<hr class="section-divider">
<h4 class="section-title"><i class="fas fa-clock"></i> My Disponibilities</h4>
<p class="text-muted small">Select your available time blocks for matches (5pm to 12am). Green = selected, Gray = available to select.</p>
<div id="disponibilities-grid">
<p class="text-muted">Loading...</p>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="saveDisponibilities()">
<i class="fas fa-save"></i> Save Disponibilities
</button>
<button type="button" class="btn btn-secondary" onclick="clearDisponibilities()">
<i class="fas fa-trash"></i> Clear All
</button>
</div>
{% endif %}
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('users.profile') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for('users.profile') }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button> <button type="submit" class="btn btn-primary">Save Changes</button>
@@ -72,4 +92,218 @@
</form> </form>
</div> </div>
</div> </div>
<style>
.disponibility-grid {
display: flex;
flex-direction: column;
gap: 15px;
}
.disponibility-day-row {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.disponibility-day-label {
width: 100px;
font-weight: bold;
}
.disponibility-time-blocks {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.disponibility-time-block {
width: 80px;
height: 32px;
border-radius: 4px;
border: 1px solid #d1d5db;
background: #f3f4f6;
text-align: center;
line-height: 32px;
font-size: 12px;
cursor: pointer;
user-select: none;
transition: all 0.2s;
}
.disponibility-time-block:hover {
background: #e5e7eb;
}
.disponibility-time-block.selected {
background: #10b981;
color: white;
border-color: #059669;
}
</style>
<script>
{% if user.role == 'player' %}
// Generate time slots from 5pm (17:00) to 12am (24:00)
var TIME_SLOTS = [];
for (var h = 17; h <= 24; h++) {
for (var m = 0; m < 60; m += 30) {
if (h === 24 && m > 0) continue;
var displayHour;
var displayAmpm;
if (h === 24) {
displayHour = 12;
displayAmpm = 'AM';
} else if (h > 12) {
displayHour = h - 12;
displayAmpm = 'PM';
} else {
displayHour = h;
displayAmpm = 'PM';
}
var timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
var displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
TIME_SLOTS.push({ time: timeStr, display: displayTime });
}
}
var DAYS = [
{ value: 0, name: 'Monday' },
{ value: 1, name: 'Tuesday' },
{ value: 2, name: 'Wednesday' },
{ value: 3, name: 'Thursday' },
{ value: 4, name: 'Friday' },
{ value: 5, name: 'Saturday' },
{ value: 6, name: 'Sunday' }
];
// Store selected slots: {day: [time, time, ...]}
var selectedSlots = {};
function renderDisponibilityGrid() {
var grid = document.getElementById('disponibilities-grid');
grid.innerHTML = '<div style="margin-bottom: 10px;"><strong>Click time blocks to select your available hours</strong></div>';
var container = document.createElement('div');
container.className = 'disponibility-grid';
DAYS.forEach(function(day) {
var dayRow = document.createElement('div');
dayRow.className = 'disponibility-day-row';
var dayLabel = document.createElement('div');
dayLabel.className = 'disponibility-day-label';
dayLabel.textContent = day.name;
dayRow.appendChild(dayLabel);
var timeBlocks = document.createElement('div');
timeBlocks.className = 'disponibility-time-blocks';
TIME_SLOTS.forEach(function(slot) {
var block = document.createElement('div');
block.className = 'disponibility-time-block';
block.dataset.day = day.value;
block.dataset.time = slot.time;
block.textContent = slot.display;
block.onclick = function() {
toggleSlot(day.value, slot.time, block);
};
timeBlocks.appendChild(block);
});
dayRow.appendChild(timeBlocks);
container.appendChild(dayRow);
});
grid.appendChild(container);
loadMyDisponibilities();
}
function toggleSlot(day, time, element) {
if (!selectedSlots[day]) selectedSlots[day] = [];
var index = selectedSlots[day].indexOf(time);
if (index > -1) {
selectedSlots[day].splice(index, 1);
element.classList.remove('selected');
} else {
selectedSlots[day].push(time);
element.classList.add('selected');
}
}
function loadMyDisponibilities() {
fetch('{{ url_for("users.get_my_disponibilities") }}')
.then(function(response) { return response.json(); })
.then(function(data) {
selectedSlots = {};
for (var day in data) {
var slots = data[day];
slots.forEach(function(slot) {
selectedSlots[day] = selectedSlots[day] || [];
selectedSlots[day].push(slot.start_time);
});
}
document.querySelectorAll('.disponibility-time-block').forEach(function(block) {
var day = block.dataset.day;
var time = block.dataset.time;
if (selectedSlots[day] && selectedSlots[day].indexOf(time) > -1) {
block.classList.add('selected');
} else {
block.classList.remove('selected');
}
});
})
.catch(function(error) {
console.error('Error loading disponibilities:', error);
});
}
function saveDisponibilities() {
var slots = [];
for (var day in selectedSlots) {
selectedSlots[day].forEach(function(time) {
slots.push({ day_of_week: parseInt(day), start_time: time });
});
}
fetch('{{ url_for("users.add_disponibilities_bulk") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ slots: slots })
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
var msg = document.createElement('div');
msg.className = 'alert alert-success';
msg.style.marginTop = '10px';
msg.innerHTML = '<i class="fas fa-check"></i> Disponibilities saved successfully!';
document.getElementById('disponibilities-grid').appendChild(msg);
setTimeout(function() { msg.remove(); }, 3000);
}
})
.catch(function(error) {
console.error('Error saving disponibilities:', error);
});
}
function clearDisponibilities() {
if (!confirm('Are you sure you want to clear all your disponibilities?')) return;
fetch('{{ url_for("users.clear_disponibilities") }}', {
method: 'POST'
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
selectedSlots = {};
document.querySelectorAll('.disponibility-time-block').forEach(function(block) {
block.classList.remove('selected');
});
}
});
}
document.addEventListener('DOMContentLoaded', renderDisponibilityGrid);
{% endif %}
</script>
{% endblock %} {% endblock %}