Ajout de fonctionnalités:
Ajout de notes personnels et de notes d'équipe avec historique. Ajout de prise de rendez-vous avec un coach selon ses disponibilités
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,21 @@ import os
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from extensions import db, login_manager, csrf, hash_password, check_password
|
from extensions import db, login_manager, csrf, hash_password, check_password
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
import markupsafe
|
||||||
|
|
||||||
|
|
||||||
|
def nl2br(value):
|
||||||
|
"""Convert newlines to HTML line breaks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: String value to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markup: HTML-safe string with line breaks.
|
||||||
|
"""
|
||||||
|
if value:
|
||||||
|
return markupsafe.Markup('<br>'.join(str(value).splitlines()))
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
@@ -51,6 +66,9 @@ def create_app():
|
|||||||
app.register_blueprint(teams_bp)
|
app.register_blueprint(teams_bp)
|
||||||
app.register_blueprint(matches_bp)
|
app.register_blueprint(matches_bp)
|
||||||
|
|
||||||
|
# Register custom Jinja filters
|
||||||
|
app.jinja_env.filters['nl2br'] = nl2br
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
import models
|
import models
|
||||||
from models import User, MatchParticipant
|
from models import User, MatchParticipant
|
||||||
|
|||||||
Binary file not shown.
@@ -665,4 +665,128 @@ class Contract(db.Model):
|
|||||||
bool: True if user is the player associated with the contract.
|
bool: True if user is the player associated with the contract.
|
||||||
"""
|
"""
|
||||||
# Only the player can upload their signed contract
|
# Only the player can upload their signed contract
|
||||||
return user.id == self.player_id
|
return user.id == self.player_id
|
||||||
|
|
||||||
|
|
||||||
|
class CoachAvailability(db.Model):
|
||||||
|
"""Coach availability in 30-minute time blocks for One on One sessions.
|
||||||
|
|
||||||
|
Allows coaches to specify when they're available for individual coaching sessions.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier.
|
||||||
|
coach_id: Foreign key to the coach.
|
||||||
|
day_of_week: Day of week (0=Monday, 6=Sunday).
|
||||||
|
start_time: Start time of availability block.
|
||||||
|
end_time: End time of availability block (always 30 min after start).
|
||||||
|
created_at: Timestamp of creation.
|
||||||
|
updated_at: Timestamp of last update.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'coach_availabilities'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
coach_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)
|
||||||
|
|
||||||
|
coach = db.relationship('User', backref='coach_availabilities')
|
||||||
|
|
||||||
|
|
||||||
|
class TeamNote(db.Model):
|
||||||
|
"""Team improvement notes from coach.
|
||||||
|
|
||||||
|
Contains coaching notes and suggestions for team improvement.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier.
|
||||||
|
org_team_id: Foreign key to the organization team.
|
||||||
|
coach_id: Foreign key to the coach who wrote the notes.
|
||||||
|
content: The note content.
|
||||||
|
created_at: Timestamp of creation.
|
||||||
|
updated_at: Timestamp of last update.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'team_notes'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
|
||||||
|
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
team = db.relationship('OrgTeam', backref='team_notes')
|
||||||
|
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNote(db.Model):
|
||||||
|
"""Personal notes from coach to individual player.
|
||||||
|
|
||||||
|
Contains individual feedback and coaching tips for players.
|
||||||
|
Can be linked to specific contexts: match, team, or tryout.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier.
|
||||||
|
player_id: Foreign key to the player.
|
||||||
|
coach_id: Foreign key to the coach who wrote the notes.
|
||||||
|
content: The note content.
|
||||||
|
created_at: Timestamp of creation.
|
||||||
|
updated_at: Timestamp of last update.
|
||||||
|
match_id: Optional foreign key to the match context.
|
||||||
|
team_id: Optional foreign key to the team context.
|
||||||
|
tryout_id: Optional foreign key to the tryout context.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'personal_notes'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Optional context linking
|
||||||
|
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=True)
|
||||||
|
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||||
|
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=True)
|
||||||
|
|
||||||
|
player = db.relationship('User', foreign_keys=[player_id], backref='personal_notes')
|
||||||
|
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||||
|
match = db.relationship('Match', foreign_keys=[match_id])
|
||||||
|
team = db.relationship('Team', foreign_keys=[team_id])
|
||||||
|
tryout = db.relationship('Tryout', foreign_keys=[tryout_id])
|
||||||
|
|
||||||
|
|
||||||
|
class OneOnOneRequest(db.Model):
|
||||||
|
"""Request from player to coach for a One on One session.
|
||||||
|
|
||||||
|
Tracks requests for individual coaching sessions with time slot selection.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Unique identifier.
|
||||||
|
player_id: Foreign key to the player requesting.
|
||||||
|
coach_id: Foreign key to the coach.
|
||||||
|
org_team_id: Foreign key to the player's team.
|
||||||
|
date: Requested date for the session.
|
||||||
|
start_time: Requested start time.
|
||||||
|
end_time: Requested end time.
|
||||||
|
points: What the player wants to discuss.
|
||||||
|
status: Request status (pending, approved, rejected, scheduled).
|
||||||
|
created_at: Timestamp of creation.
|
||||||
|
responded_at: Timestamp when coach responded.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'one_on_one_requests'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||||
|
date = db.Column(db.Date, nullable=False)
|
||||||
|
start_time = db.Column(db.Time, nullable=False)
|
||||||
|
end_time = db.Column(db.Time, nullable=False)
|
||||||
|
points = db.Column(db.Text, nullable=True)
|
||||||
|
status = db.Column(db.String(20), default='pending') # pending, approved, rejected, scheduled
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
responded_at = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
player = db.relationship('User', foreign_keys=[player_id], backref='one_on_one_requests')
|
||||||
|
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||||
|
team = db.relationship('OrgTeam', foreign_keys=[org_team_id])
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+73
-2
@@ -6,7 +6,7 @@ This module handles CRUD operations for organization teams and player assignment
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
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 OrgTeam, User
|
from models import OrgTeam, User, Team, TeamMember, PersonalNote, TeamNote, Tryout
|
||||||
|
|
||||||
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
|
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
|
||||||
|
|
||||||
@@ -220,4 +220,75 @@ def remove_player(team_id, player_id):
|
|||||||
player.team_id = None
|
player.team_id = None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f'{player.full_name} removed from {team.name}.', 'success')
|
flash(f'{player.full_name} removed from {team.name}.', 'success')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|
||||||
|
|
||||||
|
# Note actions from team page
|
||||||
|
|
||||||
|
|
||||||
|
@teams_bp.route('/<int:team_id>/add-team-note', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_team_note(team_id):
|
||||||
|
"""Add a team improvement note from the team page (for coaches).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_id: The ID of the team to add notes for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Redirect to teams list with status message.
|
||||||
|
"""
|
||||||
|
team = OrgTeam.query.get_or_404(team_id)
|
||||||
|
if current_user.role == 'coach' and team.coach_id != current_user.id:
|
||||||
|
flash('Only the coach of this team can add notes.', 'danger')
|
||||||
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
|
||||||
|
if content:
|
||||||
|
note = TeamNote(
|
||||||
|
org_team_id=team_id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Team notes added successfully!', 'success')
|
||||||
|
|
||||||
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|
||||||
|
|
||||||
|
@teams_bp.route('/<int:team_id>/add-player-note/<int:player_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_player_note(team_id, player_id):
|
||||||
|
"""Add a personal note for a player from the team page (for coaches).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_id: The ID of the team.
|
||||||
|
player_id: The ID of the player to add note for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Redirect to teams list with status message.
|
||||||
|
"""
|
||||||
|
team = OrgTeam.query.get_or_404(team_id)
|
||||||
|
if current_user.role == 'coach' and team.coach_id != current_user.id:
|
||||||
|
flash('Only the coach of this team can add notes.', 'danger')
|
||||||
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|
||||||
|
player = User.query.get_or_404(player_id)
|
||||||
|
if player.role != 'player':
|
||||||
|
flash('Can only add notes for players.', 'danger')
|
||||||
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
|
||||||
|
if content:
|
||||||
|
note = PersonalNote(
|
||||||
|
player_id=player_id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Note added for {player.full_name}!', 'success')
|
||||||
|
|
||||||
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|||||||
+693
-3
@@ -8,9 +8,10 @@ import os
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
||||||
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, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam
|
from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam, CoachAvailability, TeamNote, PersonalNote, OneOnOneRequest, Match, Team, TeamMember, MatchParticipant, Tryout
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, date as date_type
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||||
@@ -716,4 +717,693 @@ def download_signed_contract(contract_id):
|
|||||||
flash('No signed contract available.', 'danger')
|
flash('No signed contract available.', 'danger')
|
||||||
return redirect(url_for('users.list_contracts'))
|
return redirect(url_for('users.list_contracts'))
|
||||||
|
|
||||||
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
|
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
|
||||||
|
|
||||||
|
|
||||||
|
# One on One Functions
|
||||||
|
|
||||||
|
|
||||||
|
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '')
|
||||||
|
|
||||||
|
|
||||||
|
def send_discord_notification(player_name, points, date_str, start_time_str, end_time_str, team_name, coach_name, coach_discord):
|
||||||
|
"""Send a Discord webhook notification for a One on One request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
player_name: Name of the player making the request.
|
||||||
|
points: Discussion points from the player.
|
||||||
|
date_str: Date of the requested session.
|
||||||
|
start_time_str: Start time of the requested session.
|
||||||
|
end_time_str: End time of the requested session.
|
||||||
|
team_name: Name of the player's team.
|
||||||
|
coach_name: Name of the coach.
|
||||||
|
coach_discord: Coach's Discord username.
|
||||||
|
"""
|
||||||
|
if not DISCORD_WEBHOOK_URL:
|
||||||
|
return # No webhook configured, skip notification
|
||||||
|
|
||||||
|
embed = {
|
||||||
|
"embeds": [{
|
||||||
|
"title": "One on One Request",
|
||||||
|
"color": 3447003, # Blue color
|
||||||
|
"fields": [
|
||||||
|
{"name": "Player", "value": player_name, "inline": True},
|
||||||
|
{"name": "Team", "value": team_name or "Unknown Team", "inline": True},
|
||||||
|
{"name": "Date", "value": date_str, "inline": True},
|
||||||
|
{"name": "Time", "value": f"{start_time_str} - {end_time_str}", "inline": True},
|
||||||
|
{"name": "Discussion Points", "value": points or "No specific points provided", "inline": False}
|
||||||
|
],
|
||||||
|
"footer": {
|
||||||
|
"text": f"Coach: {coach_name}" + (f" (Discord: {coach_discord})" if coach_discord else "")
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5)
|
||||||
|
except Exception:
|
||||||
|
pass # Silently fail if webhook doesn't work
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/one-on-one', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def one_on_one():
|
||||||
|
"""One on One request page for players.
|
||||||
|
|
||||||
|
Players can view team notes, personal notes, and request One on One sessions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Rendered One on One page.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'player':
|
||||||
|
flash('Only players can request One on One sessions.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get player's team and coach
|
||||||
|
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None
|
||||||
|
coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None
|
||||||
|
|
||||||
|
if not coach:
|
||||||
|
flash('You do not have a coach assigned to your team.', 'info')
|
||||||
|
|
||||||
|
# Get team notes for this player's team
|
||||||
|
team_notes = []
|
||||||
|
if org_team:
|
||||||
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
# Get personal notes for this player
|
||||||
|
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
# Get coach's availability for the next 7 days
|
||||||
|
coach_availability = []
|
||||||
|
if coach:
|
||||||
|
coach_availability = CoachAvailability.query.filter_by(coach_id=coach.id).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Handle One on One request submission
|
||||||
|
date_str = request.form.get('date')
|
||||||
|
start_time_str = request.form.get('start_time')
|
||||||
|
end_time_str = request.form.get('end_time')
|
||||||
|
points = request.form.get('points', '').strip()
|
||||||
|
|
||||||
|
if not coach:
|
||||||
|
flash('Cannot request One on One - no coach assigned.', 'danger')
|
||||||
|
return redirect(url_for('users.one_on_one'))
|
||||||
|
|
||||||
|
# Validate date and time
|
||||||
|
try:
|
||||||
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
||||||
|
end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
flash('Invalid date or time format.', 'danger')
|
||||||
|
return redirect(url_for('users.one_on_one'))
|
||||||
|
|
||||||
|
# Check if the requested slot is within coach's availability
|
||||||
|
date_parts = date_str.split('-')
|
||||||
|
check_date = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2]))
|
||||||
|
# Python weekday: Monday=0, Sunday=6
|
||||||
|
day_of_week = check_date.weekday()
|
||||||
|
|
||||||
|
is_available = any(
|
||||||
|
av.day_of_week == day_of_week and
|
||||||
|
av.start_time <= start_time and
|
||||||
|
av.end_time >= end_time
|
||||||
|
for av in coach_availability
|
||||||
|
)
|
||||||
|
|
||||||
|
if not is_available:
|
||||||
|
flash('The requested time is not within the coach\'s availability.', 'danger')
|
||||||
|
return redirect(url_for('users.one_on_one'))
|
||||||
|
|
||||||
|
# Create the request
|
||||||
|
request_obj = OneOnOneRequest(
|
||||||
|
player_id=current_user.id,
|
||||||
|
coach_id=coach.id,
|
||||||
|
org_team_id=org_team.id if org_team else None,
|
||||||
|
date=date_obj,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
points=points if points else None
|
||||||
|
)
|
||||||
|
db.session.add(request_obj)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Send Discord notification
|
||||||
|
send_discord_notification(
|
||||||
|
player_name=current_user.full_name,
|
||||||
|
points=points,
|
||||||
|
date_str=date_str,
|
||||||
|
start_time_str=start_time_str,
|
||||||
|
end_time_str=end_time_str,
|
||||||
|
team_name=org_team.name if org_team else None,
|
||||||
|
coach_name=coach.full_name,
|
||||||
|
coach_discord=coach.discord_username
|
||||||
|
)
|
||||||
|
|
||||||
|
flash('One on One request sent to your coach!', 'success')
|
||||||
|
return redirect(url_for('users.one_on_one'))
|
||||||
|
|
||||||
|
# Calculate dates for the next week (starting from today)
|
||||||
|
dates = []
|
||||||
|
for i in range(7):
|
||||||
|
d = date_type.today() + timedelta(days=i)
|
||||||
|
dates.append({
|
||||||
|
'value': d.strftime('%Y-%m-%d'),
|
||||||
|
'display': d.strftime('%A, %b %d'),
|
||||||
|
'day_of_week': d.weekday()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Serialize coach availability for JavaScript
|
||||||
|
coach_availability_serialized = [
|
||||||
|
{
|
||||||
|
'id': av.id,
|
||||||
|
'day_of_week': av.day_of_week,
|
||||||
|
'start_time': av.start_time.strftime('%H:%M'),
|
||||||
|
'end_time': av.end_time.strftime('%H:%M')
|
||||||
|
}
|
||||||
|
for av in coach_availability
|
||||||
|
]
|
||||||
|
|
||||||
|
return render_template('pages/one_on_one.html',
|
||||||
|
org_team=org_team,
|
||||||
|
coach=coach,
|
||||||
|
team_notes=team_notes,
|
||||||
|
personal_notes=personal_notes,
|
||||||
|
coach_availability=coach_availability_serialized,
|
||||||
|
dates=dates)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/coach-availability', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def manage_coach_availability():
|
||||||
|
"""Manage coach availability (for coaches).
|
||||||
|
|
||||||
|
GET: Render the availability management page.
|
||||||
|
POST: Add availability slots via bulk.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Rendered management page or redirect.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'coach':
|
||||||
|
flash('Only coaches can manage availability.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.get_json()
|
||||||
|
slots = data.get('slots', [])
|
||||||
|
|
||||||
|
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 coach
|
||||||
|
existing = CoachAvailability.query.filter_by(
|
||||||
|
coach_id=current_user.id,
|
||||||
|
day_of_week=day_of_week,
|
||||||
|
start_time=start_time
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
availability = CoachAvailability(
|
||||||
|
coach_id=current_user.id,
|
||||||
|
day_of_week=day_of_week,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time
|
||||||
|
)
|
||||||
|
db.session.add(availability)
|
||||||
|
db.session.flush()
|
||||||
|
created.append({
|
||||||
|
'id': availability.id,
|
||||||
|
'day_of_week': availability.day_of_week,
|
||||||
|
'day_name': DAY_NAMES[availability.day_of_week],
|
||||||
|
'start_time': availability.start_time.strftime('%H:%M'),
|
||||||
|
'end_time': availability.end_time.strftime('%H:%M')
|
||||||
|
})
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True, 'created': created})
|
||||||
|
|
||||||
|
# Get existing availability
|
||||||
|
existing_availability = CoachAvailability.query.filter_by(coach_id=current_user.id).all()
|
||||||
|
|
||||||
|
return render_template('pages/coach_availability.html',
|
||||||
|
existing_availability=existing_availability)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/coach-availability/clear', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def clear_coach_availability():
|
||||||
|
"""Clear all coach availability slots.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: JSON with success status.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'coach':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
CoachAvailability.query.filter_by(coach_id=current_user.id).delete()
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/coach-availability/<int:availability_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def delete_coach_availability(availability_id):
|
||||||
|
"""Delete a coach availability slot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
availability_id: The ID of the availability slot to delete.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: JSON with success status or error.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'coach':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
availability = CoachAvailability.query.get_or_404(availability_id)
|
||||||
|
|
||||||
|
if availability.coach_id != current_user.id:
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
db.session.delete(availability)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/api/coach-availability/<int:coach_id>')
|
||||||
|
@login_required
|
||||||
|
def api_get_coach_availability(coach_id):
|
||||||
|
"""API endpoint to get coach availability.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coach_id: The ID of the coach.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: JSON with availability data.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'player':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
availability = CoachAvailability.query.filter_by(coach_id=coach_id).all()
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for av in availability:
|
||||||
|
if av.day_of_week not in result:
|
||||||
|
result[av.day_of_week] = []
|
||||||
|
result[av.day_of_week].append({
|
||||||
|
'id': av.id,
|
||||||
|
'start_time': av.start_time.strftime('%H:%M'),
|
||||||
|
'end_time': av.end_time.strftime('%H:%M')
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/team-notes', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def manage_team_notes():
|
||||||
|
"""Manage team improvement notes (for coaches).
|
||||||
|
|
||||||
|
GET: Render the team notes management page.
|
||||||
|
POST: Create or update team notes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Rendered management page or redirect.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'coach':
|
||||||
|
flash('Only coaches can manage team notes.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get the coach's team
|
||||||
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||||
|
if not org_team:
|
||||||
|
flash('You are not assigned to coach any team.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get existing team notes
|
||||||
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
|
||||||
|
if content:
|
||||||
|
# Create new team note entry (keeps history)
|
||||||
|
note = TeamNote(
|
||||||
|
org_team_id=org_team.id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Team notes added successfully!', 'success')
|
||||||
|
return redirect(url_for('users.manage_team_notes'))
|
||||||
|
|
||||||
|
return render_template('pages/team_notes.html',
|
||||||
|
org_team=org_team,
|
||||||
|
team_notes=team_notes)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/personal-notes', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def manage_personal_notes():
|
||||||
|
"""Manage personal notes for players (for coaches).
|
||||||
|
|
||||||
|
GET: Render the personal notes management page.
|
||||||
|
POST: Create a personal note for a player.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Rendered management page or redirect.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'coach':
|
||||||
|
flash('Only coaches can manage personal notes.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get players on the coach's team
|
||||||
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||||
|
players = []
|
||||||
|
if org_team:
|
||||||
|
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
player_id = request.form.get('player_id', type=int)
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
|
||||||
|
if not player_id or not content:
|
||||||
|
flash('Please select a player and enter note content.', 'danger')
|
||||||
|
return redirect(url_for('users.manage_personal_notes'))
|
||||||
|
|
||||||
|
# Verify player is on coach's team
|
||||||
|
if org_team and player_id not in [p.id for p in players]:
|
||||||
|
flash('You can only add notes for players on your team.', 'danger')
|
||||||
|
return redirect(url_for('users.manage_personal_notes'))
|
||||||
|
|
||||||
|
note = PersonalNote(
|
||||||
|
player_id=player_id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Personal note added successfully!', 'success')
|
||||||
|
return redirect(url_for('users.manage_personal_notes'))
|
||||||
|
|
||||||
|
# Get all personal notes for players on this team
|
||||||
|
personal_notes = []
|
||||||
|
if org_team:
|
||||||
|
personal_notes = PersonalNote.query.filter(
|
||||||
|
PersonalNote.player_id.in_([p.id for p in players])
|
||||||
|
).order_by(PersonalNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
return render_template('pages/personal_notes.html',
|
||||||
|
players=players,
|
||||||
|
personal_notes=personal_notes,
|
||||||
|
org_team=org_team)
|
||||||
|
|
||||||
|
|
||||||
|
# Note source types for filtering
|
||||||
|
NOTE_SOURCE_MATCH = 'match'
|
||||||
|
NOTE_SOURCE_TRYOUT = 'tryout'
|
||||||
|
NOTE_SOURCE_TEAM = 'team'
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/my-notes')
|
||||||
|
@login_required
|
||||||
|
def my_notes():
|
||||||
|
"""View all notes for the current player.
|
||||||
|
|
||||||
|
Shows both personal notes and team notes that the player has received.
|
||||||
|
Personal notes are grouped by source (match, tryout, team).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Rendered my notes template.
|
||||||
|
"""
|
||||||
|
if current_user.role != 'player':
|
||||||
|
flash('Only players can view their notes.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get player's team and coach
|
||||||
|
org_team = OrgTeam.query.get(current_user.team_id) if current_user.team_id else None
|
||||||
|
|
||||||
|
# Get all personal notes for this player with eager loading for relationships
|
||||||
|
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
# Get team notes for this player's team
|
||||||
|
team_notes = []
|
||||||
|
if org_team:
|
||||||
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
return render_template('pages/player_personal_notes.html',
|
||||||
|
org_team=org_team,
|
||||||
|
personal_notes=personal_notes,
|
||||||
|
team_notes=team_notes)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/notes/add', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def add_personal_note():
|
||||||
|
"""Add a personal note with optional context (for coaches and managers).
|
||||||
|
|
||||||
|
GET: Render the note creation form.
|
||||||
|
POST: Create a personal note, optionally linked to match/tryout/team.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Create form or redirect to notes view.
|
||||||
|
"""
|
||||||
|
if current_user.role not in ['coach', 'manager', 'president']:
|
||||||
|
flash('Only coaches and managers can add notes.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get players this user can manage
|
||||||
|
players = []
|
||||||
|
org_team = None
|
||||||
|
if current_user.role == 'coach':
|
||||||
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||||
|
if org_team:
|
||||||
|
players = User.query.filter_by(role='player', team_id=org_team.id).order_by(User.full_name).all()
|
||||||
|
elif current_user.role in ['president', 'manager']:
|
||||||
|
players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||||
|
|
||||||
|
# Get available matches and tryouts for context
|
||||||
|
matches = []
|
||||||
|
tryouts = []
|
||||||
|
teams = []
|
||||||
|
if current_user.role == 'coach' and org_team:
|
||||||
|
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
|
||||||
|
matches = Match.query.join(Tryout).filter(Tryout.target_org_team_id == org_team.id).order_by(Match.date.desc()).all()
|
||||||
|
elif current_user.role in ['president', 'manager']:
|
||||||
|
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
|
||||||
|
matches = Match.query.order_by(Match.date.desc()).all()
|
||||||
|
teams = Team.query.order_by(Team.name).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
player_id = request.form.get('player_id', type=int)
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
match_id = request.form.get('match_id', type=int)
|
||||||
|
tryout_id = request.form.get('tryout_id', type=int)
|
||||||
|
team_id = request.form.get('team_id', type=int)
|
||||||
|
|
||||||
|
if not player_id or not content:
|
||||||
|
flash('Please select a player and enter note content.', 'danger')
|
||||||
|
return redirect(url_for('users.add_personal_note'))
|
||||||
|
|
||||||
|
# Verify player is on coach's team (if coach)
|
||||||
|
if current_user.role == 'coach' and org_team and player_id not in [p.id for p in players]:
|
||||||
|
flash('You can only add notes for players on your team.', 'danger')
|
||||||
|
return redirect(url_for('users.add_personal_note'))
|
||||||
|
|
||||||
|
# Validate context - ensure coach can access the match/tryout/team
|
||||||
|
if match_id:
|
||||||
|
match = Match.query.get(match_id)
|
||||||
|
match_tryout = None
|
||||||
|
if match:
|
||||||
|
match_tryout = Tryout.query.get(match.tryout_id)
|
||||||
|
if match_tryout and match_tryout.target_org_team_id and match_tryout.target_org_team_id != org_team.id:
|
||||||
|
flash('You can only add notes for matches in your team\'s tryouts.', 'danger')
|
||||||
|
return redirect(url_for('users.add_personal_note'))
|
||||||
|
if tryout_id and org_team:
|
||||||
|
tryout = Tryout.query.get(tryout_id)
|
||||||
|
if tryout and tryout.target_org_team_id and tryout.target_org_team_id != org_team.id:
|
||||||
|
flash('You can only add notes for your team\'s tryouts.', 'danger')
|
||||||
|
return redirect(url_for('users.add_personal_note'))
|
||||||
|
if team_id and org_team:
|
||||||
|
team = Team.query.get(team_id)
|
||||||
|
if team:
|
||||||
|
tryout = Tryout.query.get(team.tryout_id)
|
||||||
|
if tryout and tryout.target_org_team_id and tryout.target_org_team_id != org_team.id:
|
||||||
|
flash('You can only add notes for your team\'s tryouts.', 'danger')
|
||||||
|
return redirect(url_for('users.add_personal_note'))
|
||||||
|
|
||||||
|
note = PersonalNote(
|
||||||
|
player_id=player_id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content,
|
||||||
|
match_id=match_id if match_id else None,
|
||||||
|
team_id=team_id if team_id else None,
|
||||||
|
tryout_id=tryout_id if tryout_id else None
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Personal note added successfully!', 'success')
|
||||||
|
return redirect(url_for('users.my_notes'))
|
||||||
|
|
||||||
|
return render_template('pages/add_personal_note.html',
|
||||||
|
players=players,
|
||||||
|
matches=matches,
|
||||||
|
tryouts=tryouts,
|
||||||
|
teams=teams,
|
||||||
|
org_team=org_team)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/match/<int:match_id>/add-note', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def add_note_from_match(match_id):
|
||||||
|
"""Add a personal note from a match view context (for coaches).
|
||||||
|
|
||||||
|
GET: Render the note creation form pre-filled with match info.
|
||||||
|
POST: Create a personal note linked to the match.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
match_id: The ID of the match to create note for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Create form or redirect.
|
||||||
|
"""
|
||||||
|
if current_user.role not in ['coach', 'manager', 'president']:
|
||||||
|
flash('Only coaches can add notes from matches.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
match = Match.query.get_or_404(match_id)
|
||||||
|
tryout = Tryout.query.get(match.tryout_id)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role == 'coach':
|
||||||
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||||
|
if not org_team or (tryout.target_org_team_id and tryout.target_org_team_id != org_team.id):
|
||||||
|
flash('You do not have permission for this match.', 'danger')
|
||||||
|
return redirect(url_for('matches.calendar'))
|
||||||
|
|
||||||
|
# Get all participants for this match
|
||||||
|
participant_ids = []
|
||||||
|
if match.match_type == 'team_vs_team':
|
||||||
|
if match.team1_id:
|
||||||
|
team_members = TeamMember.query.filter_by(team_id=match.team1_id).all()
|
||||||
|
participant_ids.extend([m.player_id for m in team_members])
|
||||||
|
if match.team2_id:
|
||||||
|
team_members = TeamMember.query.filter_by(team_id=match.team2_id).all()
|
||||||
|
participant_ids.extend([m.player_id for m in team_members])
|
||||||
|
else:
|
||||||
|
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
|
||||||
|
participant_ids = [p.player_id for p in participants]
|
||||||
|
|
||||||
|
players = User.query.filter(User.id.in_(participant_ids)).order_by(User.full_name).all() if participant_ids else []
|
||||||
|
|
||||||
|
# Get team notes for context
|
||||||
|
team_notes = []
|
||||||
|
if tryout and tryout.target_org_team_id:
|
||||||
|
team_notes = TeamNote.query.filter_by(org_team_id=tryout.target_org_team_id).order_by(TeamNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
player_id = request.form.get('player_id', type=int)
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
|
||||||
|
if not player_id or not content:
|
||||||
|
flash('Please select a player and enter note content.', 'danger')
|
||||||
|
elif player_id not in participant_ids:
|
||||||
|
flash('Selected player is not in this match.', 'danger')
|
||||||
|
else:
|
||||||
|
note = PersonalNote(
|
||||||
|
player_id=player_id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content,
|
||||||
|
match_id=match_id
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Personal note added successfully!', 'success')
|
||||||
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||||
|
|
||||||
|
return render_template('pages/add_note_from_match.html',
|
||||||
|
match=match,
|
||||||
|
tryout=tryout,
|
||||||
|
players=players,
|
||||||
|
team_notes=team_notes)
|
||||||
|
|
||||||
|
|
||||||
|
@users_bp.route('/tryout/<int:tryout_id>/add-note', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def add_note_from_tryout(tryout_id):
|
||||||
|
"""Add a personal note from a tryout view context (for coaches).
|
||||||
|
|
||||||
|
GET: Render the note creation form pre-filled with tryout info.
|
||||||
|
POST: Create a personal note linked to the tryout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tryout_id: The ID of the tryout to create note for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Create form or redirect.
|
||||||
|
"""
|
||||||
|
if current_user.role not in ['coach', 'manager', 'president']:
|
||||||
|
flash('Only coaches can add notes from tryouts.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
tryout = Tryout.query.get_or_404(tryout_id)
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
if current_user.role == 'coach':
|
||||||
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
||||||
|
if not org_team or (tryout.target_org_team_id and tryout.target_org_team_id != org_team.id):
|
||||||
|
flash('You do not have permission for this tryout.', 'danger')
|
||||||
|
return redirect(url_for('tryouts.list_tryouts'))
|
||||||
|
|
||||||
|
# Get all registered players in this tryout
|
||||||
|
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
||||||
|
player_ids = [r.player_id for r in registrations]
|
||||||
|
|
||||||
|
# If coach, filter to only their team players
|
||||||
|
if current_user.role == 'coach' and org_team:
|
||||||
|
players = User.query.filter(User.id.in_(player_ids), User.team_id == org_team.id).order_by(User.full_name).all()
|
||||||
|
else:
|
||||||
|
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all()
|
||||||
|
|
||||||
|
# Get team notes for context
|
||||||
|
team_notes = []
|
||||||
|
if tryout.target_org_team_id:
|
||||||
|
team_notes = TeamNote.query.filter_by(org_team_id=tryout.target_org_team_id).order_by(TeamNote.created_at.desc()).all()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
player_id = request.form.get('player_id', type=int)
|
||||||
|
content = request.form.get('content', '').strip()
|
||||||
|
|
||||||
|
if not player_id or not content:
|
||||||
|
flash('Please select a player and enter note content.', 'danger')
|
||||||
|
elif player_id not in [p.id for p in players]:
|
||||||
|
flash('Selected player is not registered for this tryout.', 'danger')
|
||||||
|
else:
|
||||||
|
note = PersonalNote(
|
||||||
|
player_id=player_id,
|
||||||
|
coach_id=current_user.id,
|
||||||
|
content=content,
|
||||||
|
tryout_id=tryout_id
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Personal note added successfully!', 'success')
|
||||||
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
|
return render_template('pages/add_note_from_tryout.html',
|
||||||
|
tryout=tryout,
|
||||||
|
players=players,
|
||||||
|
team_notes=team_notes)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ users, tryouts, teams, evaluations, and player disponibilities.
|
|||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from extensions import db, hash_password
|
from extensions import db, hash_password
|
||||||
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag
|
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag, CoachAvailability, TeamNote, PersonalNote, Match, MatchParticipant
|
||||||
from datetime import datetime, timedelta, time
|
from datetime import datetime, timedelta, time
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@@ -363,8 +363,141 @@ def seed_database():
|
|||||||
db.session.add(d)
|
db.session.add(d)
|
||||||
disponibilities.append(d)
|
disponibilities.append(d)
|
||||||
|
|
||||||
|
# Create sample coach availabilities
|
||||||
|
coach_availabilities = []
|
||||||
|
coach_avail_time_slots = [(16, 0), (16, 30), (17, 0), (17, 30), (18, 0), (18, 30), (19, 0), (19, 30), (20, 0), (20, 30), (21, 0), (21, 30)]
|
||||||
|
|
||||||
|
# Only coaches with teams get availabilities
|
||||||
|
for coach, org_team in zip(coaches[:3], org_teams[:3]):
|
||||||
|
if coach.id == coaches[2].id: # Skip coach3 as they don't have all days
|
||||||
|
days_available = [0, 1, 2, 3, 4] # Mon-Fri
|
||||||
|
else:
|
||||||
|
days_available = [0, 1, 2, 3, 4, 5] # Mon-Sat
|
||||||
|
|
||||||
|
for day in days_available:
|
||||||
|
num_slots = random.randint(3, 5)
|
||||||
|
chosen_slots = random.sample(coach_avail_time_slots, min(num_slots, len(coach_avail_time_slots)))
|
||||||
|
for hour, minute in chosen_slots:
|
||||||
|
start_time = time(hour, minute)
|
||||||
|
end_minute = minute + 30
|
||||||
|
end_hour = hour
|
||||||
|
if end_minute >= 60:
|
||||||
|
end_minute -= 60
|
||||||
|
end_hour += 1
|
||||||
|
end_time = time(end_hour, end_minute)
|
||||||
|
|
||||||
|
ca = CoachAvailability(
|
||||||
|
coach_id=coach.id,
|
||||||
|
day_of_week=day,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time
|
||||||
|
)
|
||||||
|
db.session.add(ca)
|
||||||
|
coach_availabilities.append(ca)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(f"[OK] Created {len(disponibilities)} player disponibilities")
|
print(f"[OK] Created {len(coach_availabilities)} coach availabilities")
|
||||||
|
|
||||||
|
# Create team notes for each org team
|
||||||
|
team_notes_data = [
|
||||||
|
{
|
||||||
|
'team': org_teams[0],
|
||||||
|
'coach': coaches[0],
|
||||||
|
'content': 'Team, focus on rotation and positioning during scrims. We need to improve our mechanical consistency and work on post-platoon transitions. Remember to communicate clearly and stay positive!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'team': org_teams[1],
|
||||||
|
'coach': coaches[1],
|
||||||
|
'content': 'Great progress this week! Keep working on your smoke lineups and utility usage. Individual practice on aim trainers is paying off. Next week we focus on map control and trading.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'team': org_teams[2],
|
||||||
|
'coach': coaches[2],
|
||||||
|
'content': 'Agent comp needs work. Make sure to stick to your roles and trust your teammates. Work on your crosshair placement and pre-aim common angles. Team chemistry is key!'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for note_data in team_notes_data:
|
||||||
|
note = TeamNote(
|
||||||
|
org_team_id=note_data['team'].id,
|
||||||
|
coach_id=note_data['coach'].id,
|
||||||
|
content=note_data['content']
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"[OK] Created {len(team_notes_data)} team notes")
|
||||||
|
|
||||||
|
# Create personal notes for players
|
||||||
|
personal_notes_data = [
|
||||||
|
{'player': players[0], 'coach': coaches[0], 'content': 'Your mechanics are improving! Focus on staying calm during high-pressure situations. Keep practicing those flip resets.'},
|
||||||
|
{'player': players[0], 'coach': coaches[0], 'content': 'Good positioning in last scrim. Work on your kickoffs - consistency will help the team.'},
|
||||||
|
{'player': players[1], 'coach': coaches[0], 'content': 'Your aerial game is strong. Try to be more aggressive on the ball when you have space.'},
|
||||||
|
{'player': players[3], 'coach': coaches[1], 'content': 'Need to work on your smoke grenade placement. Practice pre-aiming and strafe stopping.'},
|
||||||
|
{'player': players[4], 'coach': coaches[1], 'content': 'Good clutch performance! Keep your utility management consistent throughout rounds.'},
|
||||||
|
{'player': players[6], 'coach': coaches[2], 'content': 'Your aim trainer routine is paying off. Work on your agent abilities usage timing.'},
|
||||||
|
{'player': players[7], 'coach': coaches[2], 'content': 'Focus on communication in matches. Call out enemy positions clearly and ask for help when needed.'},
|
||||||
|
]
|
||||||
|
|
||||||
|
for note_data in personal_notes_data:
|
||||||
|
note = PersonalNote(
|
||||||
|
player_id=note_data['player'].id,
|
||||||
|
coach_id=note_data['coach'].id,
|
||||||
|
content=note_data['content']
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"[OK] Created {len(personal_notes_data)} personal notes")
|
||||||
|
|
||||||
|
# Create sample matches for tryouts (for calendar testing)
|
||||||
|
matches_data = [
|
||||||
|
{'tryout': tryouts[0], 'title': 'Alpha vs Bravo', 'date': tryouts[0].date, 'start_time': time(18, 0), 'end_time': time(18, 30), 'match_type': 'team_vs_team', 'team1_id': team1.id if 'team1' in dir() else None},
|
||||||
|
{'tryout': tryouts[0], 'title': 'Bravo vs Alpha', 'date': tryouts[0].date, 'start_time': time(19, 0), 'end_time': time(19, 30), 'match_type': 'team_vs_team'},
|
||||||
|
{'tryout': tryouts[1], 'title': 'Scrimmage', 'date': tryouts[1].date, 'start_time': time(17, 0), 'end_time': time(17, 30), 'match_type': 'player_scrim'},
|
||||||
|
{'tryout': tryouts[2], 'title': 'Team Alpha Scrim', 'date': tryouts[2].date, 'start_time': time(18, 30), 'end_time': time(19, 0), 'match_type': 'player_vs_player'},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Re-fetch teams after commit
|
||||||
|
team1 = Team.query.filter_by(name='Alpha Team').first()
|
||||||
|
team2 = Team.query.filter_by(name='Bravo Team').first()
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
for i, m_data in enumerate(matches_data):
|
||||||
|
m = Match(
|
||||||
|
tryout_id=m_data['tryout'].id,
|
||||||
|
title=m_data['title'],
|
||||||
|
date=m_data['date'],
|
||||||
|
start_time=m_data['start_time'],
|
||||||
|
end_time=m_data['end_time'],
|
||||||
|
match_type=m_data['match_type'],
|
||||||
|
created_by=president.id,
|
||||||
|
team1_id=m_data.get('team1_id') or (team1.id if i < 2 else None),
|
||||||
|
team2_id=team2.id if i < 2 else None
|
||||||
|
)
|
||||||
|
db.session.add(m)
|
||||||
|
matches.append(m)
|
||||||
|
db.session.commit()
|
||||||
|
print(f"[OK] Created {len(matches)} matches")
|
||||||
|
|
||||||
|
# Add match participants for scrim matches
|
||||||
|
player_scrim_match = matches[2] if len(matches) > 2 else None
|
||||||
|
if player_scrim_match:
|
||||||
|
for player in players[3:5]:
|
||||||
|
mp = MatchParticipant(match_id=player_scrim_match.id, player_id=player.id)
|
||||||
|
db.session.add(mp)
|
||||||
|
|
||||||
|
pvp_match = matches[3] if len(matches) > 3 else None
|
||||||
|
if pvp_match and team1:
|
||||||
|
for player in players[:2]:
|
||||||
|
mp = MatchParticipant(match_id=pvp_match.id, player_id=player.id, team_side=1)
|
||||||
|
db.session.add(mp)
|
||||||
|
for player in players[2:4]:
|
||||||
|
mp = MatchParticipant(match_id=pvp_match.id, player_id=player.id, team_side=2)
|
||||||
|
db.session.add(mp)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("[OK] Created match participants")
|
||||||
|
|
||||||
print("\n[SUCCESS] Database seeded successfully!")
|
print("\n[SUCCESS] Database seeded successfully!")
|
||||||
print("\n=== Login Credentials ===")
|
print("\n=== Login Credentials ===")
|
||||||
|
|||||||
@@ -67,18 +67,52 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
|
||||||
<a href="{{ url_for('users.list_contracts') }}" class="{% if request.endpoint and 'contracts' in request.endpoint %}active{% endif %}">
|
|
||||||
<i class="fas fa-file-contract"></i>
|
|
||||||
<span>Contracts</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('users.profile') }}" class="{% if request.endpoint == 'users.profile' %}active{% endif %}">
|
<a href="{{ url_for('users.profile') }}" class="{% if request.endpoint == 'users.profile' %}active{% endif %}">
|
||||||
<i class="fas fa-user"></i>
|
<i class="fas fa-user"></i>
|
||||||
<span>My Profile</span>
|
<span>My Profile</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if current_user.role == 'player' %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('users.one_on_one') }}" class="{% if request.endpoint == 'users.one_on_one' %}active{% endif %}">
|
||||||
|
<i class="fas fa-calendar-check"></i>
|
||||||
|
<span>One on One</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('users.my_notes') }}" class="{% if request.endpoint == 'users.my_notes' %}active{% endif %}">
|
||||||
|
<i class="fas fa-sticky-note"></i>
|
||||||
|
<span>My Notes</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.role == 'coach' %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('users.manage_coach_availability') }}" class="{% if request.endpoint == 'users.manage_coach_availability' %}active{% endif %}">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
<span>Availability</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('users.manage_team_notes') }}" class="{% if request.endpoint == 'users.manage_team_notes' %}active{% endif %}">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span>Team Notes</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('users.manage_personal_notes') }}" class="{% if request.endpoint == 'users.manage_personal_notes' %}active{% endif %}">
|
||||||
|
<i class="fas fa-sticky-note"></i>
|
||||||
|
<span>Personal Notes</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('users.list_contracts') }}" class="{% if request.endpoint and 'contracts' in request.endpoint %}active{% endif %}">
|
||||||
|
<i class="fas fa-file-contract"></i>
|
||||||
|
<span>Contracts</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-divider"></li>
|
<li class="nav-divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('auth.logout') }}" class="logout-link">
|
<a href="{{ url_for('auth.logout') }}" class="logout-link">
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Add Note from Match - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Add Note from Match{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Add Note</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-futbol"></i> Add Note for Match: {{ match.title }}</h3>
|
||||||
|
<span class="badge badge-info">{{ match.date.strftime('%m/%d/%Y') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('users.add_note_from_match', match_id=match.id) }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="player_id">Select Player</label>
|
||||||
|
<select name="player_id" id="player_id" class="form-select" required>
|
||||||
|
<option value="">-- Select a Player --</option>
|
||||||
|
{% for player in players %}
|
||||||
|
<option value="{{ player.id }}">{{ player.full_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content">Note Content</label>
|
||||||
|
<textarea name="content" id="content" class="form-textarea" rows="4" placeholder="Enter feedback or coaching tips for this player from the match..." required></textarea>
|
||||||
|
<p class="form-text">This note will be linked to this match and visible to the selected player.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Add Note
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Tryout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if team_notes %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-users"></i> Team Notes Reference</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="detail-grid">
|
||||||
|
{% for note in team_notes %}
|
||||||
|
<div class="detail-item full-width mb-3">
|
||||||
|
<span class="detail-label">
|
||||||
|
<i class="fas fa-user"></i> {{ note.coach.full_name if note.coach else 'Unknown Coach' }}
|
||||||
|
</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Add Note from Tryout - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Add Note from Tryout{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Add Note</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-calendar-alt"></i> Add Note for Tryout: {{ tryout.title }}</h3>
|
||||||
|
<span class="badge badge-success">{{ tryout.date.strftime('%m/%d/%Y') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('users.add_note_from_tryout', tryout_id=tryout.id) }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="player_id">Select Player</label>
|
||||||
|
<select name="player_id" id="player_id" class="form-select" required>
|
||||||
|
<option value="">-- Select a Player --</option>
|
||||||
|
{% for player in players %}
|
||||||
|
<option value="{{ player.id }}">{{ player.full_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content">Note Content</label>
|
||||||
|
<textarea name="content" id="content" class="form-textarea" rows="4" placeholder="Enter feedback or coaching tips for this player from the tryout..." required></textarea>
|
||||||
|
<p class="form-text">This note will be linked to this tryout and visible to the selected player.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Add Note
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Tryout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if team_notes %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-users"></i> Team Notes Reference</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="detail-grid">
|
||||||
|
{% for note in team_notes %}
|
||||||
|
<div class="detail-item full-width mb-3">
|
||||||
|
<span class="detail-label">
|
||||||
|
<i class="fas fa-user"></i> {{ note.coach.full_name if note.coach else 'Unknown Coach' }}
|
||||||
|
</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Add Note - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Add Personal Note{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('users.my_notes') }}">My Notes</a> / Add Note</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-sticky-note"></i> Add Personal Note</h3>
|
||||||
|
{% if org_team %}
|
||||||
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('users.add_personal_note') }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="player_id">Select Player</label>
|
||||||
|
<select name="player_id" id="player_id" class="form-select" required>
|
||||||
|
<option value="">-- Select a Player --</option>
|
||||||
|
{% for player in players %}
|
||||||
|
<option value="{{ player.id }}">{{ player.full_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content">Note Content</label>
|
||||||
|
<textarea name="content" id="content" class="form-textarea" rows="4" placeholder="Enter personal feedback or coaching tips for this player..." required></textarea>
|
||||||
|
<p class="form-text">These notes will only be visible to the selected player.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="context">Context (Optional)</label>
|
||||||
|
<p class="form-text text-muted">Link this note to a specific match, tryout, or team for better organization.</p>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="match_id">Match</label>
|
||||||
|
<select name="match_id" id="match_id" class="form-select">
|
||||||
|
<option value="">-- Select Match --</option>
|
||||||
|
{% for match in matches %}
|
||||||
|
<option value="{{ match.id }}">{{ match.title }} - {{ match.date.strftime('%m/%d/%Y') }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tryout_id">Tryout</label>
|
||||||
|
<select name="tryout_id" id="tryout_id" class="form-select">
|
||||||
|
<option value="">-- Select Tryout --</option>
|
||||||
|
{% for tryout in tryouts %}
|
||||||
|
<option value="{{ tryout.id }}">{{ tryout.title }} - {{ tryout.date.strftime('%m/%d/%Y') }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team_id">Team</label>
|
||||||
|
<select name="team_id" id="team_id" class="form-select">
|
||||||
|
<option value="">-- Select Team --</option>
|
||||||
|
{% for team in teams %}
|
||||||
|
<option value="{{ team.id }}">{{ team.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Add Note
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('users.my_notes') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Notes
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Manage Availability - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Manage Availability{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / Coach Availability</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-clock"></i> Set Your Weekly Availability</h3>
|
||||||
|
<p class="text-muted small">Select time slots when you're available for One on One sessions</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="availability-grid" id="availability-grid">
|
||||||
|
<p class="text-muted">Loading availability grid...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions mt-4">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="clearAllAvailability()">
|
||||||
|
<i class="fas fa-trash"></i> Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-list"></i> Current Availability</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if existing_availability %}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Day</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for av in existing_availability %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'][av.day_of_week] }}</td>
|
||||||
|
<td>{{ av.start_time.strftime('%I:%M %p') }} - {{ av.end_time.strftime('%I:%M %p') }}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="deleteAvailability({{ av.id }})">
|
||||||
|
<i class="fas fa-trash"></i> Remove
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No availability slots set. Use the grid above to add your available times.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<style>
|
||||||
|
.availability-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-header {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
background: white;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot:hover {
|
||||||
|
background: var(--primary-light);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot.selected {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-slot.selected:hover {
|
||||||
|
background: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.availability-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.availability-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
// Time slots from 8:00 AM to 10:00 PM (30-minute intervals)
|
||||||
|
const TIME_SLOTS = [];
|
||||||
|
for (let h = 8; h <= 22; h++) {
|
||||||
|
for (let m = 0; m < 60; m += 30) {
|
||||||
|
const timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
|
||||||
|
const displayHour = h > 12 ? h - 12 : h;
|
||||||
|
const displayAmpm = h >= 12 ? 'PM' : 'AM';
|
||||||
|
const displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
|
||||||
|
TIME_SLOTS.push({ time: timeStr, display: displayTime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day names
|
||||||
|
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
|
||||||
|
// Track selected slots: {day_of_week: [time_strings]}
|
||||||
|
let selectedSlots = {};
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadExistingAvailability();
|
||||||
|
renderGrid();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadExistingAvailability() {
|
||||||
|
// Load from existing data
|
||||||
|
{% for av in existing_availability %}
|
||||||
|
if (!selectedSlots[{{ av.day_of_week }}]) {
|
||||||
|
selectedSlots[{{ av.day_of_week }}] = [];
|
||||||
|
}
|
||||||
|
selectedSlots[{{ av.day_of_week }}].push('{{ av.start_time.strftime('%H:%M') }}');
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrid() {
|
||||||
|
const grid = document.getElementById('availability-grid');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
DAYS.forEach((day, dayIndex) => {
|
||||||
|
html += '<div class="day-column">';
|
||||||
|
html += '<div class="day-header">' + day.substring(0, 3) + '</div>';
|
||||||
|
|
||||||
|
TIME_SLOTS.forEach(slot => {
|
||||||
|
const isSelected = selectedSlots[dayIndex] && selectedSlots[dayIndex].includes(slot.time);
|
||||||
|
const cssClass = isSelected ? 'time-slot selected' : 'time-slot';
|
||||||
|
html += '<div class="' + cssClass + '" data-day="' + dayIndex + '" data-time="' + slot.time + '" onclick="toggleSlot(' + dayIndex + ', \'' + slot.time + '\', this)">' + slot.display + '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSlot(dayOfWeek, timeStr, element) {
|
||||||
|
if (!selectedSlots[dayOfWeek]) {
|
||||||
|
selectedSlots[dayOfWeek] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = selectedSlots[dayOfWeek].indexOf(timeStr);
|
||||||
|
if (index === -1) {
|
||||||
|
selectedSlots[dayOfWeek].push(timeStr);
|
||||||
|
element.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
selectedSlots[dayOfWeek].splice(index, 1);
|
||||||
|
element.classList.remove('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAvailability() {
|
||||||
|
const slots = [];
|
||||||
|
for (let day in selectedSlots) {
|
||||||
|
selectedSlots[day].forEach(time => {
|
||||||
|
slots.push({ day_of_week: parseInt(day), start_time: time });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('{{ url_for("users.manage_coach_availability") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ slots: slots })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
flash('Availability saved successfully!', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllAvailability() {
|
||||||
|
if (!confirm('Are you sure you want to clear all your availability slots?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('{{ url_for("users.clear_coach_availability") }}', { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
selectedSlots = {};
|
||||||
|
renderGrid();
|
||||||
|
flash('Availability cleared!', 'success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAvailability(availabilityId) {
|
||||||
|
fetch('{{ url_for("users.delete_coach_availability", availability_id=0) }}'.replace('/0', '/' + availabilityId), { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
flash('Availability slot removed!', 'success');
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function flash(message, type) {
|
||||||
|
const flashContainer = document.querySelector('.flash-messages');
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = 'alert alert-' + type + ' alert-dismissible';
|
||||||
|
alert.innerHTML = '<span>' + message + '</span><button type="button" class="alert-close" onclick="this.parentElement.remove()">×</button>';
|
||||||
|
flashContainer.appendChild(alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save on change (debounced)
|
||||||
|
let saveTimeout;
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.classList.contains('time-slot')) {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
saveTimeout = setTimeout(saveAvailability, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -114,7 +114,19 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<p class="text-muted small"><i class="fas fa-info-circle"></i> Click on players to assign them to Team 1 or Team 2. Players available in all selected time blocks are shown below.</p>
|
<!-- 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>
|
||||||
|
|
||||||
<div class="pvp-layout">
|
<div class="pvp-layout">
|
||||||
<div class="team-column">
|
<div class="team-column">
|
||||||
@@ -203,7 +215,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.team-selection .player-item:hover {
|
.team-selection .player-item:hover {
|
||||||
background: var(--accent-color);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.player-pool {
|
.player-pool {
|
||||||
@@ -277,6 +289,41 @@
|
|||||||
.team-selection .player-item:hover .remove-btn {
|
.team-selection .player-item:hover .remove-btn {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.randomize-section {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.randomize-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.randomize-controls label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.randomize-input {
|
||||||
|
width: 60px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.randomize-preview {
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.randomize-btn {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.randomize-btn:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
// Player data as simple JS object (id -> full_name)
|
// Player data as simple JS object (id -> full_name)
|
||||||
@@ -645,6 +692,7 @@ function updatePlayerPool() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pool.innerHTML = html || '<p class="text-muted small">No players available</p>';
|
pool.innerHTML = html || '<p class="text-muted small">No players available</p>';
|
||||||
|
updateRandomizePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignToTeam(playerId, teamSide) {
|
function assignToTeam(playerId, teamSide) {
|
||||||
@@ -727,5 +775,74 @@ function toggleMatchType() {
|
|||||||
scrimSection.classList.remove('hidden');
|
scrimSection.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateRandomizePreview() {
|
||||||
|
var checkedCount = document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').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 allAssigned = [];
|
||||||
|
|
||||||
|
// Get all players currently in either team
|
||||||
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
||||||
|
allAssigned.push(parseInt(el.getAttribute('data-player-id')));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allAssigned.length === 0) {
|
||||||
|
alert('Please assign at least one player before randomizing teams.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the array using Fisher-Yates
|
||||||
|
for (var i = allAssigned.length - 1; i > 0; i--) {
|
||||||
|
var j = Math.floor(Math.random() * (i + 1));
|
||||||
|
var temp = allAssigned[i];
|
||||||
|
allAssigned[i] = allAssigned[j];
|
||||||
|
allAssigned[j] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
||||||
|
var team1Players = allAssigned.slice(0, team1Size);
|
||||||
|
var team2Players = allAssigned.slice(team1Size);
|
||||||
|
|
||||||
|
// Clear both teams
|
||||||
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign shuffled players to teams
|
||||||
|
team1Players.forEach(function(pid) {
|
||||||
|
var teamDiv = document.getElementById('team1-selection');
|
||||||
|
var playerName = playerDataById.player_data[pid];
|
||||||
|
if (playerName) {
|
||||||
|
var html = '<div class="player-item" data-player-id="' + pid + '" onclick="returnToPool(' + pid + ', event)">';
|
||||||
|
html += playerName;
|
||||||
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
team2Players.forEach(function(pid) {
|
||||||
|
var teamDiv = document.getElementById('team2-selection');
|
||||||
|
var playerName = playerDataById.player_data[pid];
|
||||||
|
if (playerName) {
|
||||||
|
var html = '<div class="player-item" data-player-id="' + pid + '" onclick="returnToPool(' + pid + ', event)">';
|
||||||
|
html += playerName;
|
||||||
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateHiddenInputs();
|
||||||
|
updateRandomizePreview();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+429
-186
@@ -7,6 +7,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-futbol"></i> Edit Match</h3>
|
<h3><i class="fas fa-futbol"></i> Edit Match</h3>
|
||||||
|
<p class="text-muted small">Match Type: <strong>{{ match.match_type.replace('_', ' ').title() }}</strong></p>
|
||||||
</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" id="editMatchForm">
|
<form method="POST" action="{{ url_for('matches.edit_match', match_id=match.id) }}" class="form" id="editMatchForm">
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
||||||
<hr class="section-divider">
|
<hr class="section-divider">
|
||||||
<h4 class="section-title"><i class="fas fa-clock"></i> Select Match Time</h4>
|
<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>
|
<p class="text-muted small">Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.</p>
|
||||||
|
|
||||||
<div id="merged-disponibility-selected" class="merged-disponibility-selected-display hidden">
|
<div id="merged-disponibility-selected" class="merged-disponibility-selected-display hidden">
|
||||||
<span>Selected time:</span>
|
<span>Selected time:</span>
|
||||||
@@ -83,120 +84,111 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Team vs Team Selection -->
|
<!-- Team vs Team Selection -->
|
||||||
{% if match.match_type == 'team_vs_team' %}
|
<div id="team-vs-team-section" {% if match.match_type != 'team_vs_team' %}class="hidden"{% endif %}>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="team1_id">Team 1</label>
|
<label for="team1_id">Team 1</label>
|
||||||
<select name="team1_id" id="team1_id" class="form-select">
|
<select name="team1_id" id="team1_id" class="form-select">
|
||||||
<option value="">-- Select Team 1 --</option>
|
<option value="">-- Select Team 1 --</option>
|
||||||
{% for team in teams %}
|
{% for team in teams %}
|
||||||
<option value="{{ team.id }}" {% if match.team1_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
<option value="{{ team.id }}" {% if match.team1_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team2_id">Team 2</label>
|
||||||
|
<select name="team2_id" id="team2_id" class="form-select">
|
||||||
|
<option value="">-- Select Team 2 --</option>
|
||||||
|
{% for team in teams %}
|
||||||
|
<option value="{{ team.id }}" {% if match.team2_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="team2_id">Team 2</label>
|
<!-- Show team rosters -->
|
||||||
<select name="team2_id" id="team2_id" class="form-select">
|
<div class="team-rosters mt-3">
|
||||||
<option value="">-- Select Team 2 --</option>
|
{% if match.team1 %}
|
||||||
{% for team in teams %}
|
<div class="team-roster">
|
||||||
<option value="{{ team.id }}" {% if match.team2_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
<h5>{{ match.team1.name }}</h5>
|
||||||
{% endfor %}
|
<ul class="team-members-list">
|
||||||
</select>
|
{% for member in match.team1.members %}
|
||||||
|
<li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="text-muted">No players assigned</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if match.team2 %}
|
||||||
|
<div class="team-roster">
|
||||||
|
<h5>{{ match.team2.name }}</h5>
|
||||||
|
<ul class="team-members-list">
|
||||||
|
{% for member in match.team2.members %}
|
||||||
|
<li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="text-muted">No players assigned</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Show team rosters -->
|
|
||||||
<div class="team-rosters mt-3">
|
|
||||||
{% if match.team1 %}
|
|
||||||
<div class="team-roster">
|
|
||||||
<h5>{{ match.team1.name }}</h5>
|
|
||||||
<ul class="team-members-list">
|
|
||||||
{% for member in match.team1.members %}
|
|
||||||
<li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="text-muted">No players assigned</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if match.team2 %}
|
|
||||||
<div class="team-roster">
|
|
||||||
<h5>{{ match.team2.name }}</h5>
|
|
||||||
<ul class="team-members-list">
|
|
||||||
{% for member in match.team2.members %}
|
|
||||||
<li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="text-muted">No players assigned</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Player vs Player Selection -->
|
<!-- Player vs Player Selection -->
|
||||||
{% elif match.match_type == 'player_vs_player' %}
|
<div id="player-vs-player-section" {% if match.match_type != 'player_vs_player' %}class="hidden"{% endif %}>
|
||||||
<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 -->
|
<!-- Randomize Teams Section -->
|
||||||
<div class="randomize-section">
|
<div class="randomize-section">
|
||||||
<div class="randomize-controls">
|
<div class="randomize-controls">
|
||||||
<label><i class="fas fa-random"></i> Randomize Teams:</label>
|
<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()">
|
<input type="number" id="team1-size" class="randomize-input" min="1" value="1" onchange="updateRandomizePreview()">
|
||||||
<span>vs</span>
|
<span>vs</span>
|
||||||
<span id="team2-size-preview" class="randomize-preview">0</span>
|
<span id="team2-size-preview" class="randomize-preview">0</span>
|
||||||
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
|
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
|
||||||
<i class="fas fa-random"></i> Randomize
|
<i class="fas fa-random"></i> Randomize
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small" id="randomize-hint">Select players then click Randomize to split them into teams.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small" id="randomize-hint">Select players then click Randomize to split them into teams.</p>
|
|
||||||
</div>
|
<div class="pvp-layout">
|
||||||
|
<div class="team-column">
|
||||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
<h5 class="team-header">Team 1</h5>
|
||||||
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
|
<div id="team1-selection" class="team-selection"></div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Team 1 Players</label>
|
|
||||||
<div class="checkbox-grid" id="team1-players-list">
|
|
||||||
{% for player in all_players %}
|
|
||||||
<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)">
|
|
||||||
{{ player.full_name }}
|
|
||||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="player-pool">
|
||||||
<label>Team 2 Players</label>
|
<div class="player-pool-header">Available Players</div>
|
||||||
<div class="checkbox-grid" id="team2-players-list">
|
<div id="available-players-pool" class="available-players-pool">
|
||||||
{% for player in all_players %}
|
<p class="text-muted small">All registered players are shown. Click time slots to filter available players.</p>
|
||||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
</div>
|
||||||
<input type="checkbox" name="team2_player_ids" value="{{ player.id }}" {% if player.id in team2_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 2)">
|
</div>
|
||||||
{{ player.full_name }}
|
|
||||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
<div class="team-column">
|
||||||
</label>
|
<h5 class="team-header">Team 2</h5>
|
||||||
{% endfor %}
|
<div id="team2-selection" class="team-selection"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="team1_player_ids" id="team1-player-ids-input">
|
||||||
|
<input type="hidden" name="team2_player_ids" id="team2-player-ids-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Scrim Selection -->
|
<!-- Player Scrim Selection -->
|
||||||
{% else %}
|
<div id="player-scrim-section" {% if match.match_type != 'player_scrim' %}class="hidden"{% endif %}>
|
||||||
<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() %}
|
{% 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>
|
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Players</label>
|
|
||||||
<div class="checkbox-grid" id="scrim-players-list">
|
<div class="checkbox-grid" id="scrim-players-list">
|
||||||
{% for player in all_players %}
|
{% for player in all_players %}
|
||||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||||
@@ -207,7 +199,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
@@ -223,7 +214,169 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
<style>
|
||||||
|
.pvp-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.team-column {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
.team-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.team-selection {
|
||||||
|
min-height: 200px;
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.team-selection .player-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.team-selection .player-item:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.player-pool {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.player-pool-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.available-players-pool {
|
||||||
|
min-height: 200px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.available-players-pool .player-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: var(--success-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.available-players-pool .player-item:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
.available-players-pool .player-item.available {
|
||||||
|
background: var(--success-light);
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
.available-players-pool .player-item.unavailable {
|
||||||
|
background: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.player-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.player-item .remove-btn {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.player-item .remove-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.player-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.player-actions .btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.player-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.team-selection .player-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.team-selection .player-item:hover .remove-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.randomize-section {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.randomize-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.randomize-controls label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.randomize-input {
|
||||||
|
width: 60px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.randomize-preview {
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.randomize-btn {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.randomize-btn:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
// Player data as simple JS object (id -> full_name)
|
||||||
|
var playerDataById = {
|
||||||
|
player_data: {
|
||||||
|
{%- for p in all_players %}
|
||||||
|
{{ p.id }}: "{{ p.full_name | escape }}",
|
||||||
|
{%- endfor %}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// All registered player IDs
|
||||||
|
var allRegisteredPlayers = [
|
||||||
|
{%- for p in all_players %}
|
||||||
|
{{ p.id }},
|
||||||
|
{%- endfor %}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Current team assignments from server
|
||||||
|
var initialTeam1Ids = {{ team1_player_ids|tojson }};
|
||||||
|
var initialTeam2Ids = {{ team2_player_ids|tojson }};
|
||||||
|
|
||||||
// Time slots from 12pm (12:00) to 12am (24:00)
|
// Time slots from 12pm (12:00) to 12am (24:00)
|
||||||
var TIME_SLOTS = [];
|
var TIME_SLOTS = [];
|
||||||
for (var h = 12; h <= 24; h++) {
|
for (var h = 12; h <= 24; h++) {
|
||||||
@@ -258,10 +411,11 @@ var DAYS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
var selectedDate = '';
|
var selectedDate = '';
|
||||||
var selectedSlots = []; // Array of selected time slots: ['17:00', '17:30', '18:00']
|
var selectedSlots = [];
|
||||||
var allDisponibilities = {};
|
var allDisponibilities = {};
|
||||||
var canViewDisponibilities = {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}true{% else %}false{% endif %};
|
var canViewDisponibilities = {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}true{% else %}false{% endif %};
|
||||||
var totalPlayers = {{ all_players|length }};
|
var totalPlayers = {{ all_players|length }};
|
||||||
|
var availablePlayersForSlots = []; // Players available in ALL selected slots
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -273,6 +427,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
selectedDate = this.value;
|
selectedDate = this.value;
|
||||||
if (allDisponibilitiesInitialized()) {
|
if (allDisponibilitiesInitialized()) {
|
||||||
renderMergedDisponibilityGrid();
|
renderMergedDisponibilityGrid();
|
||||||
|
updatePlayerPool();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -302,8 +457,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMergedDisponibilityGrid();
|
// Initialize team assignments for player_vs_player matches
|
||||||
updateDisponibilityIndicators();
|
// Populate Team 1
|
||||||
|
initialTeam1Ids.forEach(function(pid) {
|
||||||
|
var teamDiv = document.getElementById('team1-selection');
|
||||||
|
var playerName = playerDataById.player_data[pid];
|
||||||
|
if (playerName) {
|
||||||
|
var html = '<div class="player-item" data-player-id="' + pid + '" onclick="returnToPool(' + pid + ', event)">';
|
||||||
|
html += playerName;
|
||||||
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate Team 2
|
||||||
|
initialTeam2Ids.forEach(function(pid) {
|
||||||
|
var teamDiv = document.getElementById('team2-selection');
|
||||||
|
var playerName = playerDataById.player_data[pid];
|
||||||
|
if (playerName) {
|
||||||
|
var html = '<div class="player-item" data-player-id="' + pid + '" onclick="returnToPool(' + pid + ', event)">';
|
||||||
|
html += playerName;
|
||||||
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateHiddenInputs();
|
||||||
updateRandomizePreview();
|
updateRandomizePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +499,7 @@ function fetchDisponibilities() {
|
|||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
allDisponibilities = data;
|
allDisponibilities = data;
|
||||||
renderMergedDisponibilityGrid();
|
renderMergedDisponibilityGrid();
|
||||||
|
updatePlayerPool();
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
console.error('Error fetching disponibilities:', error);
|
console.error('Error fetching disponibilities:', error);
|
||||||
@@ -518,8 +700,10 @@ function fetchAvailablePlayersForSlots() {
|
|||||||
var jsDay = dateObj.getDay();
|
var jsDay = dateObj.getDay();
|
||||||
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
||||||
|
|
||||||
|
// Find players available in ANY selected slot
|
||||||
var availableIds = [];
|
var availableIds = [];
|
||||||
for (var playerId in allDisponibilities) {
|
for (var playerId in allDisponibilities) {
|
||||||
|
if (!allRegisteredPlayers.includes(parseInt(playerId))) continue;
|
||||||
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
||||||
var hasSlot = selectedSlots.some(function(slot) {
|
var hasSlot = selectedSlots.some(function(slot) {
|
||||||
return playerSlots.includes(slot);
|
return playerSlots.includes(slot);
|
||||||
@@ -529,18 +713,8 @@ function fetchAvailablePlayersForSlots() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchType = '{{ match.match_type }}';
|
// Update player pool for PvP section
|
||||||
|
updatePlayerPool();
|
||||||
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();
|
updateRandomizePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,12 +726,111 @@ function clearTimeSelection() {
|
|||||||
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
|
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
|
||||||
el.classList.remove('selected');
|
el.classList.remove('selected');
|
||||||
});
|
});
|
||||||
|
availablePlayersForSlots = [];
|
||||||
|
updatePlayerPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlayerPool() {
|
||||||
|
var pool = document.getElementById('available-players-pool');
|
||||||
|
if (!pool) return;
|
||||||
|
|
||||||
|
var team1Ids = getSelectedTeamIds(1);
|
||||||
|
var team2Ids = getSelectedTeamIds(2);
|
||||||
|
var assignedIds = [...team1Ids, ...team2Ids];
|
||||||
|
|
||||||
|
// When time slots are selected, show only players available in all slots who aren't assigned yet
|
||||||
|
// When no time slots selected, show all registered players
|
||||||
|
var playersToShow = selectedSlots.length > 0 ? availablePlayersForSlots : allRegisteredPlayers;
|
||||||
|
|
||||||
|
if (playersToShow.length === 0) {
|
||||||
|
pool.innerHTML = '<p class="text-muted small">No players available</p>';
|
||||||
|
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 = '';
|
||||||
|
playersToShow.forEach(function(pid) {
|
||||||
|
if (assignedIds.includes(pid)) return;
|
||||||
|
|
||||||
|
var playerName = playerDataById.player_data[pid];
|
||||||
|
if (!playerName) return;
|
||||||
|
|
||||||
|
// Check if player is available during selected time slots
|
||||||
|
var isAvailable = true;
|
||||||
|
if (selectedSlots.length > 0 && allDisponibilities[pid]) {
|
||||||
|
var playerSlots = getSlotsForPlayer(pid, dayOfWeek);
|
||||||
|
isAvailable = selectedSlots.every(function(slot) {
|
||||||
|
return playerSlots.includes(slot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var availabilityClass = isAvailable ? 'available' : 'unavailable';
|
||||||
|
|
||||||
|
html += '<div class="player-item ' + availabilityClass + '" data-player-id="' + pid + '">';
|
||||||
|
html += '<span class="player-name">' + playerName + '</span>';
|
||||||
|
html += '<div class="player-actions">';
|
||||||
|
html += '<button type="button" class="btn btn-sm btn-primary" onclick="assignToTeam(' + pid + ', 1)">T1</button>';
|
||||||
|
html += '<button type="button" class="btn btn-sm btn-secondary" onclick="assignToTeam(' + pid + ', 2)">T2</button>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.innerHTML = html || '<p class="text-muted small">No players available</p>';
|
||||||
|
updateRandomizePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignToTeam(playerId, teamSide) {
|
||||||
|
// First, remove player from any team they're already in
|
||||||
|
document.querySelectorAll('[data-player-id="' + playerId + '"]').forEach(function(el) {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
var teamDiv = document.getElementById('team' + teamSide + '-selection');
|
||||||
|
var playerName = playerDataById.player_data[playerId];
|
||||||
|
if (!playerName) return;
|
||||||
|
|
||||||
|
var html = '<div class="player-item" data-player-id="' + playerId + '" onclick="returnToPool(' + playerId + ', event)">';
|
||||||
|
html += playerName;
|
||||||
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
|
updateHiddenInputs();
|
||||||
|
updatePlayerPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnToPool(playerId, event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
document.querySelectorAll('[data-player-id="' + playerId + '"]').forEach(function(el) {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
updateHiddenInputs();
|
||||||
|
updatePlayerPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedTeamIds(teamSide) {
|
||||||
|
var ids = [];
|
||||||
|
document.querySelectorAll('#team' + teamSide + '-selection [data-player-id]').forEach(function(el) {
|
||||||
|
ids.push(parseInt(el.getAttribute('data-player-id')));
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHiddenInputs() {
|
||||||
|
var team1Ids = getSelectedTeamIds(1);
|
||||||
|
var team2Ids = getSelectedTeamIds(2);
|
||||||
|
document.getElementById('team1-player-ids-input').value = team1Ids.join(',');
|
||||||
|
document.getElementById('team2-player-ids-input').value = team2Ids.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRandomizePreview() {
|
function updateRandomizePreview() {
|
||||||
var checkedCount = document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').length;
|
var assignedCount = document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').length;
|
||||||
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
||||||
var team2Size = checkedCount - team1Size;
|
var team2Size = assignedCount - team1Size;
|
||||||
|
|
||||||
if (team2Size < 0) team2Size = 0;
|
if (team2Size < 0) team2Size = 0;
|
||||||
|
|
||||||
@@ -565,92 +838,62 @@ function updateRandomizePreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function randomizeTeams() {
|
function randomizeTeams() {
|
||||||
var allChecked = [];
|
var allAssigned = [];
|
||||||
|
|
||||||
document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').forEach(function(cb) {
|
// Get all players currently in either team
|
||||||
allChecked.push(parseInt(cb.value));
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
||||||
|
allAssigned.push(parseInt(el.getAttribute('data-player-id')));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (allChecked.length === 0) {
|
if (allAssigned.length === 0) {
|
||||||
alert('Please select at least one player before randomizing teams.');
|
alert('Please assign at least one player before randomizing teams.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = allChecked.length - 1; i > 0; i--) {
|
// Shuffle the array using Fisher-Yates
|
||||||
|
for (var i = allAssigned.length - 1; i > 0; i--) {
|
||||||
var j = Math.floor(Math.random() * (i + 1));
|
var j = Math.floor(Math.random() * (i + 1));
|
||||||
var temp = allChecked[i];
|
var temp = allAssigned[i];
|
||||||
allChecked[i] = allChecked[j];
|
allAssigned[i] = allAssigned[j];
|
||||||
allChecked[j] = temp;
|
allAssigned[j] = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
||||||
var team1Players = allChecked.slice(0, team1Size);
|
var team1Players = allAssigned.slice(0, team1Size);
|
||||||
var team2Players = allChecked.slice(team1Size);
|
var team2Players = allAssigned.slice(team1Size);
|
||||||
|
|
||||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
// Clear both teams
|
||||||
cb.checked = false;
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
||||||
|
el.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assign shuffled players to teams
|
||||||
team1Players.forEach(function(pid) {
|
team1Players.forEach(function(pid) {
|
||||||
var cb = document.querySelector('input[name="team1_player_ids"][value="' + pid + '"]');
|
var teamDiv = document.getElementById('team1-selection');
|
||||||
if (cb) cb.checked = true;
|
var playerName = playerDataById.player_data[pid];
|
||||||
|
if (playerName) {
|
||||||
|
var html = '<div class="player-item" data-player-id="' + pid + '" onclick="returnToPool(' + pid + ', event)">';
|
||||||
|
html += playerName;
|
||||||
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
team2Players.forEach(function(pid) {
|
team2Players.forEach(function(pid) {
|
||||||
var cb = document.querySelector('input[name="team2_player_ids"][value="' + pid + '"]');
|
var teamDiv = document.getElementById('team2-selection');
|
||||||
if (cb) cb.checked = true;
|
var playerName = playerDataById.player_data[pid];
|
||||||
});
|
if (playerName) {
|
||||||
|
var html = '<div class="player-item" data-player-id="' + pid + '" onclick="returnToPool(' + pid + ', event)">';
|
||||||
updateRandomizePreview();
|
html += playerName;
|
||||||
}
|
html += '<span class="remove-btn">↺</span>';
|
||||||
|
html += '</div>';
|
||||||
function handleTeamSelection(checkbox, teamSide) {
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
||||||
var otherSide = teamSide === 1 ? 2 : 1;
|
|
||||||
var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]');
|
|
||||||
var playerId = checkbox.value;
|
|
||||||
|
|
||||||
otherCheckboxes.forEach(function(other) {
|
|
||||||
if (other.value === playerId) {
|
|
||||||
other.checked = false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateHiddenInputs();
|
||||||
updateRandomizePreview();
|
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 %}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}One on One - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}One on One{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / One on One</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<!-- Team Notes Section -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-users"></i> Team Notes</h3>
|
||||||
|
{% if org_team %}
|
||||||
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if team_notes %}
|
||||||
|
{% for note in team_notes %}
|
||||||
|
<div class="detail-grid mt-4">
|
||||||
|
<div class="detail-item full-width">
|
||||||
|
<span class="detail-label"><i class="fas fa-user"></i> Coach: {{ note.coach.full_name if note.coach else 'Unknown Coach' }}</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mt-2">Updated: {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No team notes have been added yet. Your coach will post improvement suggestions here.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Personal Notes Section -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-user"></i> Personal Notes</h3>
|
||||||
|
{% if coach %}
|
||||||
|
<span class="badge badge-coach">From: {{ coach.full_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if personal_notes %}
|
||||||
|
{% for note in personal_notes %}
|
||||||
|
<div class="detail-grid mt-4">
|
||||||
|
<div class="detail-item full-width">
|
||||||
|
<span class="detail-label"><i class="fas fa-sticky-note"></i> Note from {{ note.coach.full_name if note.coach else 'Unknown Coach' }}</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mt-2">Added: {{ note.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No personal notes have been added yet. Your coach may provide individual feedback here.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- One on One Request Section -->
|
||||||
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-calendar-check"></i> Request One on One Session</h3>
|
||||||
|
{% if coach %}
|
||||||
|
<span class="badge badge-info">Coach: {{ coach.full_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if coach %}
|
||||||
|
<form method="POST" action="{{ url_for('users.one_on_one') }}" class="form" id="oneOnOneForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="date">Select Date</label>
|
||||||
|
<select name="date" id="date" class="form-select" onchange="updateTimeSlots()" required>
|
||||||
|
{% for d in dates %}
|
||||||
|
<option value="{{ d.value }}" data-day="{{ d.day_of_week }}">{{ d.display }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="start_time">Start Time</label>
|
||||||
|
<select name="start_time" id="start_time" class="form-select" onchange="updateEndTimeOptions()" required>
|
||||||
|
<option value="">-- Select Date First --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="end_time">End Time</label>
|
||||||
|
<select name="end_time" id="end_time" class="form-select" required>
|
||||||
|
<option value="">-- Select Start Time First --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="points">Discussion Points <span class="text-muted">(What would you like to discuss?)</span></label>
|
||||||
|
<textarea name="points" id="points" class="form-textarea" placeholder="Enter topics you'd like to cover in your One on One session..." rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-paper-plane"></i> Send Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">You need to be assigned to a team with a coach to request a One on One session.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if coach %}
|
||||||
|
<!-- Hidden data for JavaScript -->
|
||||||
|
<script id="coach-availability-data" type="application/json">
|
||||||
|
{{ coach_availability | tojson }}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Time slots from 8:00 AM to 10:00 PM
|
||||||
|
const TIME_SLOTS = [];
|
||||||
|
for (let h = 8; h <= 22; h++) {
|
||||||
|
for (let m = 0; m < 60; m += 30) {
|
||||||
|
const displayHour = h > 12 ? h - 12 : h;
|
||||||
|
const displayAmpm = h >= 12 ? 'PM' : 'AM';
|
||||||
|
const timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
|
||||||
|
const displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
|
||||||
|
TIME_SLOTS.push({ time: timeStr, display: displayTime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coach availability data
|
||||||
|
let coachAvailability = [];
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadCoachAvailability();
|
||||||
|
updateTimeSlots();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadCoachAvailability() {
|
||||||
|
const dataEl = document.getElementById('coach-availability-data');
|
||||||
|
if (!dataEl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
coachAvailability = JSON.parse(dataEl.textContent);
|
||||||
|
} catch (e) {
|
||||||
|
coachAvailability = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimeSlots() {
|
||||||
|
const dateSelect = document.getElementById('date');
|
||||||
|
const startTimeSelect = document.getElementById('start_time');
|
||||||
|
|
||||||
|
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
|
||||||
|
const dayOfWeek = parseInt(selectedOption.getAttribute('data-day'));
|
||||||
|
|
||||||
|
// Get available time slots for this day
|
||||||
|
const dayAvailability = coachAvailability.filter(av => av.day_of_week === dayOfWeek);
|
||||||
|
|
||||||
|
// Build available slots - collect all available minutes then sort
|
||||||
|
const availableSlots = [];
|
||||||
|
const availableMinutes = [];
|
||||||
|
dayAvailability.forEach(av => {
|
||||||
|
const startMinutes = av.start_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
||||||
|
const endMinutes = av.end_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
||||||
|
|
||||||
|
// Add 30-minute slots
|
||||||
|
for (let m = startMinutes; m < endMinutes; m += 30) {
|
||||||
|
availableMinutes.push(m);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort minutes and convert to time strings
|
||||||
|
availableMinutes.sort((a, b) => a - b);
|
||||||
|
availableMinutes.forEach(m => {
|
||||||
|
const hour = Math.floor(m / 60);
|
||||||
|
const minute = m % 60;
|
||||||
|
const timeStr = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute;
|
||||||
|
availableSlots.push(timeStr);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update start time options (sorted)
|
||||||
|
startTimeSelect.innerHTML = '<option value="">-- Select Start Time --</option>';
|
||||||
|
availableSlots.forEach(slot => {
|
||||||
|
const slotData = TIME_SLOTS.find(s => s.time === slot);
|
||||||
|
if (slotData) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = slotData.time;
|
||||||
|
option.textContent = slotData.display;
|
||||||
|
startTimeSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset end time options
|
||||||
|
updateEndTimeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEndTimeOptions() {
|
||||||
|
const dateSelect = document.getElementById('date');
|
||||||
|
const startTimeSelect = document.getElementById('start_time');
|
||||||
|
const endTimeSelect = document.getElementById('end_time');
|
||||||
|
|
||||||
|
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
|
||||||
|
const dayOfWeek = parseInt(selectedOption.getAttribute('data-day'));
|
||||||
|
const selectedStart = startTimeSelect.value;
|
||||||
|
|
||||||
|
if (!selectedStart) {
|
||||||
|
endTimeSelect.innerHTML = '<option value="">-- Select Start Time First --</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available minutes for this day
|
||||||
|
const dayAvailability = coachAvailability.filter(av => av.day_of_week === dayOfWeek);
|
||||||
|
const availableMinutes = [];
|
||||||
|
dayAvailability.forEach(av => {
|
||||||
|
const startMinutes = av.start_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
||||||
|
const endMinutes = av.end_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
||||||
|
for (let m = startMinutes; m < endMinutes; m += 30) {
|
||||||
|
availableMinutes.push(m);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert selected start to minutes
|
||||||
|
const startMinutesVal = selectedStart.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
|
||||||
|
|
||||||
|
// Filter end times that are after start time
|
||||||
|
const validEndTimes = availableMinutes.filter(m => m > startMinutesVal);
|
||||||
|
validEndTimes.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// Update end time options
|
||||||
|
endTimeSelect.innerHTML = '<option value="">-- Select End Time --</option>';
|
||||||
|
validEndTimes.forEach(m => {
|
||||||
|
const hour = Math.floor(m / 60);
|
||||||
|
const minute = m % 60;
|
||||||
|
const timeStr = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute;
|
||||||
|
const slotData = TIME_SLOTS.find(s => s.time === timeStr);
|
||||||
|
if (slotData) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = slotData.time;
|
||||||
|
option.textContent = slotData.display;
|
||||||
|
endTimeSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Personal Notes - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Personal Notes{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / Personal Notes</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-user-friends"></i> Add Personal Note</h3>
|
||||||
|
{% if org_team %}
|
||||||
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('users.manage_personal_notes') }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="player_id">Select Player</label>
|
||||||
|
<select name="player_id" id="player_id" class="form-select" required>
|
||||||
|
<option value="">-- Select a Player --</option>
|
||||||
|
{% for player in players %}
|
||||||
|
<option value="{{ player.id }}">{{ player.full_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content">Note Content</label>
|
||||||
|
<textarea name="content" id="content" class="form-textarea" rows="4" placeholder="Enter personal feedback or coaching tips for this player..." required></textarea>
|
||||||
|
<p class="form-text">These notes will only be visible to the selected player.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Add Note
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if personal_notes %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-sticky-note"></i> Recent Notes</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="detail-grid">
|
||||||
|
{% for note in personal_notes %}
|
||||||
|
<div class="detail-item full-width mb-4">
|
||||||
|
<span class="detail-label">
|
||||||
|
<i class="fas fa-user"></i> {{ note.player.full_name if note.player else 'Unknown Player' }} -
|
||||||
|
<span class="text-muted">{{ note.created_at.strftime('%B %d, %Y') if note.created_at else 'Unknown date' }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br if note.content else '' }}</span>
|
||||||
|
<span class="detail-label text-muted small">From: {{ note.coach.full_name if note.coach else 'Unknown Coach' }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}My Notes - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}My Notes{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / One on One / My Notes</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<!-- Personal Notes Section -->
|
||||||
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-user"></i> Personal Notes</h3>
|
||||||
|
{% if org_team %}
|
||||||
|
<span class="badge badge-coach">Team: {{ org_team.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if personal_notes %}
|
||||||
|
{% for note in personal_notes %}
|
||||||
|
<div class="detail-grid mt-4">
|
||||||
|
<div class="detail-item full-width">
|
||||||
|
<span class="detail-label">
|
||||||
|
<i class="fas fa-sticky-note"></i> Note from {{ note.coach.full_name if note.coach else 'Unknown Coach' }}
|
||||||
|
</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
||||||
|
<div class="mt-2">
|
||||||
|
{% if note.match_id and note.match %}
|
||||||
|
<span class="badge badge-info" title="From match">
|
||||||
|
<i class="fas fa-futbol"></i> Match: {{ note.match.title }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if note.team_id and note.team %}
|
||||||
|
<span class="badge badge-warning" title="From team">
|
||||||
|
<i class="fas fa-users"></i> Team: {{ note.team.name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if note.tryout_id and note.tryout %}
|
||||||
|
<span class="badge badge-success" title="From tryout">
|
||||||
|
<i class="fas fa-calendar-alt"></i> Tryout: {{ note.tryout.title }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mt-2">Added: {{ note.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No personal notes have been added yet. Your coach may provide individual feedback here.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Team Notes Section -->
|
||||||
|
<div class="card" style="grid-column: 1 / -1;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-users"></i> Team Notes</h3>
|
||||||
|
{% if org_team %}
|
||||||
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if team_notes %}
|
||||||
|
{% for note in team_notes %}
|
||||||
|
<div class="detail-grid mt-4">
|
||||||
|
<div class="detail-item full-width">
|
||||||
|
<span class="detail-label"><i class="fas fa-user"></i> Coach: {{ note.coach.full_name if note.coach else 'Unknown Coach' }}</span>
|
||||||
|
<span class="detail-value">{{ note.content | nl2br }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mt-2">Updated: {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No team notes have been added yet. Your coach will post improvement suggestions here.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<a href="{{ url_for('users.one_on_one') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-calendar-check"></i> Request One on One Session
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Team Notes - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Team Notes{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / Team Notes</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-users"></i> Team Improvement Notes</h3>
|
||||||
|
{% if org_team %}
|
||||||
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('users.manage_team_notes') }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="content">Team Notes Content</label>
|
||||||
|
<textarea name="content" id="content" class="form-textarea" rows="6" placeholder="Enter improvement suggestions and notes for your team...">{{ team_notes[0].content if team_notes else '' }}</textarea>
|
||||||
|
<p class="form-text">These notes will be visible to all players on your team.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Add Team Notes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if team_notes %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-history"></i> Note History</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Last Updated</th>
|
||||||
|
<th>Content Preview</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for note in team_notes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') if note.updated_at else 'Unknown date' }}</td>
|
||||||
|
<td>{{ note.content[:100] if note.content else '' }}{% if note.content and note.content|length > 100 %}...{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="tryout-detail">
|
<div class="tryout-detail">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Tryout Details</h3>
|
<h3>Tryout Details</h3>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Game</span>
|
<span class="detail-label">Game</span>
|
||||||
<span class="detail-value">{{ tryout.game }}</span>
|
<span class="detail-value">{{ tryout.game }}</span>
|
||||||
@@ -249,9 +249,14 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-futbol"></i> Schedule</h3>
|
<h3><i class="fas fa-futbol"></i> Schedule</h3>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<a href="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="btn btn-sm btn-success">
|
<div class="card-actions">
|
||||||
<i class="fas fa-plus"></i> Schedule Match
|
<a href="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="btn btn-sm btn-success">
|
||||||
</a>
|
<i class="fas fa-plus"></i> Schedule Match
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('users.add_note_from_tryout', tryout_id=tryout.id) }}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-sticky-note"></i> Add Note
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -266,7 +271,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if matches %}
|
{% if matches %}
|
||||||
<!-- Matches Table -->
|
<!-- Matches Table -->
|
||||||
<div class="table-container mt-3">
|
<div class="table-container mt-3">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
@@ -301,59 +306,59 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ m.date.strftime('%m/%d/%Y') }}</td>
|
<td>{{ m.date.strftime('%m/%d/%Y') }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if m.match_type == 'team_vs_team' %}
|
{% if m.match_type == 'team_vs_team' %}
|
||||||
<div class="match-teams">
|
<div class="match-teams">
|
||||||
<div class="match-team">
|
<div class="match-team">
|
||||||
<span class="team-name">{{ m.team1.name if m.team1 else 'Team 1' }}</span>
|
<span class="team-name">{{ m.team1.name if m.team1 else 'Team 1' }}</span>
|
||||||
{% if participants.team1_players %}
|
{% if participants.team1_players %}
|
||||||
<ul class="team-players-list">
|
<ul class="team-players-list">
|
||||||
{% for pl in participants.team1_players %}
|
{% for pl in participants.team1_players %}
|
||||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="match-vs">vs</div>
|
<div class="match-vs">vs</div>
|
||||||
<div class="match-team">
|
<div class="match-team">
|
||||||
<span class="team-name">{{ m.team2.name if m.team2 else 'Team 2' }}</span>
|
<span class="team-name">{{ m.team2.name if m.team2 else 'Team 2' }}</span>
|
||||||
{% if participants.team2_players %}
|
{% if participants.team2_players %}
|
||||||
<ul class="team-players-list">
|
<ul class="team-players-list">
|
||||||
{% for pl in participants.team2_players %}
|
{% for pl in participants.team2_players %}
|
||||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% elif m.match_type == 'player_vs_player' %}
|
{% elif m.match_type == 'player_vs_player' %}
|
||||||
<div class="match-teams">
|
<div class="match-teams">
|
||||||
<div class="match-team">
|
<div class="match-team">
|
||||||
<span class="team-name">Team 1</span>
|
<span class="team-name">Team 1</span>
|
||||||
{% if participants.team1_players %}
|
{% if participants.team1_players %}
|
||||||
<ul class="team-players-list">
|
<ul class="team-players-list">
|
||||||
{% for pl in participants.team1_players %}
|
{% for pl in participants.team1_players %}
|
||||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="match-vs">vs</div>
|
<div class="match-vs">vs</div>
|
||||||
<div class="match-team">
|
<div class="match-team">
|
||||||
<span class="team-name">Team 2</span>
|
<span class="team-name">Team 2</span>
|
||||||
{% if participants.team2_players %}
|
{% if participants.team2_players %}
|
||||||
<ul class="team-players-list">
|
<ul class="team-players-list">
|
||||||
{% for pl in participants.team2_players %}
|
{% for pl in participants.team2_players %}
|
||||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ participants | join(', ') }}
|
{{ participants | join(', ') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if m.start_time and m.end_time %}
|
{% if m.start_time and m.end_time %}
|
||||||
{{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }}
|
{{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }}
|
||||||
@@ -369,6 +374,9 @@
|
|||||||
<a href="{{ url_for('matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline">
|
<a href="{{ url_for('matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline">
|
||||||
<i class="fas fa-edit"></i> Edit
|
<i class="fas fa-edit"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('users.add_note_from_match', match_id=m.id) }}" class="btn btn-sm btn-primary" title="Add Note for this Match">
|
||||||
|
<i class="fas fa-sticky-note"></i> Note
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -433,8 +441,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
||||||
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
||||||
|
|
||||||
@@ -452,7 +461,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
events: '/matches/api/events/{{ tryout.id }}',
|
events: '/matches/api/events/{{ tryout.id }}',
|
||||||
height: '300px',
|
height: '300px',
|
||||||
eventClick: function(info) {
|
eventClick: function(info) {
|
||||||
// Could show match details or edit link
|
|
||||||
if (info.event.extendedProps.match_id) {
|
if (info.event.extendedProps.match_id) {
|
||||||
window.location.href = '/matches/' + info.event.extendedProps.match_id + '/edit';
|
window.location.href = '/matches/' + info.event.extendedProps.match_id + '/edit';
|
||||||
}
|
}
|
||||||
@@ -462,4 +470,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user