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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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.
@@ -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'
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+103
-11
@@ -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
|
||||||
|
|
||||||
@@ -400,4 +432,64 @@ def delete_match(match_id):
|
|||||||
db.session.delete(match)
|
db.session.delete(match)
|
||||||
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
@@ -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})
|
||||||
|
|||||||
@@ -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")
|
|
||||||
|
# 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🎉 Database seeded successfully!")
|
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__':
|
||||||
seed_database()
|
from app import create_app
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
seed_database()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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-group">
|
||||||
|
<label for="title">Match Title</label>
|
||||||
|
<input type="text" name="title" id="title" class="form-input" placeholder="e.g., Alpha vs Bravo Scrimmage" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
|
||||||
<label for="title">Match Title</label>
|
|
||||||
<input type="text" name="title" id="title" class="form-input" placeholder="e.g., Alpha vs Bravo Scrimmage" required>
|
|
||||||
</div>
|
|
||||||
<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 class="form-group">
|
||||||
|
<label for="location">Location</label>
|
||||||
|
<input type="text" name="location" id="location" class="form-input" placeholder="Match location">
|
||||||
|
</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>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="start_time">Start Time (Optional)</label>
|
<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">
|
<input type="time" name="start_time" id="start_time" class="form-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="end_time">End Time (Optional)</label>
|
<label for="end_time">End Time</label>
|
||||||
<input type="time" name="end_time" id="end_time" class="form-input">
|
<input type="time" name="end_time" id="end_time" class="form-input">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="form-group">
|
|
||||||
<label for="location">Location</label>
|
|
||||||
<input type="text" name="location" id="location" class="form-input" placeholder="Match location">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 %}
|
||||||
+482
-13
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user