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 extensions import db, login_manager, csrf, hash_password, check_password
|
||||
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():
|
||||
@@ -51,6 +66,9 @@ def create_app():
|
||||
app.register_blueprint(teams_bp)
|
||||
app.register_blueprint(matches_bp)
|
||||
|
||||
# Register custom Jinja filters
|
||||
app.jinja_env.filters['nl2br'] = nl2br
|
||||
|
||||
with app.app_context():
|
||||
import models
|
||||
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.
|
||||
"""
|
||||
# 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_login import login_required, current_user
|
||||
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')
|
||||
|
||||
@@ -220,4 +220,75 @@ def remove_player(team_id, player_id):
|
||||
player.team_id = None
|
||||
db.session.commit()
|
||||
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_login import login_required, current_user
|
||||
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 datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date as date_type
|
||||
import requests
|
||||
|
||||
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
@@ -716,4 +717,693 @@ def download_signed_contract(contract_id):
|
||||
flash('No signed contract available.', 'danger')
|
||||
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 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
|
||||
import random
|
||||
|
||||
@@ -363,8 +363,141 @@ def seed_database():
|
||||
db.session.add(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()
|
||||
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=== Login Credentials ===")
|
||||
|
||||
@@ -67,18 +67,52 @@
|
||||
</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>
|
||||
<a href="{{ url_for('users.profile') }}" class="{% if request.endpoint == 'users.profile' %}active{% endif %}">
|
||||
<i class="fas fa-user"></i>
|
||||
<span>My Profile</span>
|
||||
</a>
|
||||
</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>
|
||||
<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">
|
||||
<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="team-column">
|
||||
@@ -203,7 +215,7 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
.team-selection .player-item:hover {
|
||||
background: var(--accent-color);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
.player-pool {
|
||||
@@ -277,6 +289,41 @@
|
||||
.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>
|
||||
// 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>';
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
function assignToTeam(playerId, teamSide) {
|
||||
@@ -727,5 +775,74 @@ function toggleMatchType() {
|
||||
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>
|
||||
{% endblock %}
|
||||
+429
-186
@@ -7,6 +7,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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 class="card-body">
|
||||
<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() %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-clock"></i> Select Match Time</h4>
|
||||
<p class="text-muted small">Click time slots consecutively to set match duration. Available players will be auto-selected below.</p>
|
||||
<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">
|
||||
<span>Selected time:</span>
|
||||
@@ -83,120 +84,111 @@
|
||||
</div>
|
||||
|
||||
<!-- Team vs Team Selection -->
|
||||
{% if match.match_type == 'team_vs_team' %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-users"></i> Teams</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="team1_id">Team 1</label>
|
||||
<select name="team1_id" id="team1_id" class="form-select">
|
||||
<option value="">-- Select Team 1 --</option>
|
||||
{% for team in teams %}
|
||||
<option value="{{ team.id }}" {% if match.team1_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="team-vs-team-section" {% if match.match_type != 'team_vs_team' %}class="hidden"{% endif %}>
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-users"></i> Teams</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="team1_id">Team 1</label>
|
||||
<select name="team1_id" id="team1_id" class="form-select">
|
||||
<option value="">-- Select Team 1 --</option>
|
||||
{% for team in teams %}
|
||||
<option value="{{ team.id }}" {% if match.team1_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
||||
{% endfor %}
|
||||
</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 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>
|
||||
|
||||
<!-- 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>
|
||||
</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 -->
|
||||
{% elif match.match_type == 'player_vs_player' %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4>
|
||||
|
||||
<!-- Randomize Teams Section -->
|
||||
<div class="randomize-section">
|
||||
<div class="randomize-controls">
|
||||
<label><i class="fas fa-random"></i> Randomize Teams:</label>
|
||||
<input type="number" id="team1-size" class="randomize-input" min="1" value="1" onchange="updateRandomizePreview()">
|
||||
<span>vs</span>
|
||||
<span id="team2-size-preview" class="randomize-preview">0</span>
|
||||
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
|
||||
<i class="fas fa-random"></i> Randomize
|
||||
</button>
|
||||
<div id="player-vs-player-section" {% if match.match_type != 'player_vs_player' %}class="hidden"{% endif %}>
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4>
|
||||
|
||||
<!-- Randomize Teams Section -->
|
||||
<div class="randomize-section">
|
||||
<div class="randomize-controls">
|
||||
<label><i class="fas fa-random"></i> Randomize Teams:</label>
|
||||
<input type="number" id="team1-size" class="randomize-input" min="1" value="1" onchange="updateRandomizePreview()">
|
||||
<span>vs</span>
|
||||
<span id="team2-size-preview" class="randomize-preview">0</span>
|
||||
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
|
||||
<i class="fas fa-random"></i> Randomize
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-muted small" id="randomize-hint">Select players then click Randomize to split them into teams.</p>
|
||||
</div>
|
||||
<p class="text-muted small" id="randomize-hint">Select players then click Randomize to split them into teams.</p>
|
||||
</div>
|
||||
|
||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
||||
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-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 class="pvp-layout">
|
||||
<div class="team-column">
|
||||
<h5 class="team-header">Team 1</h5>
|
||||
<div id="team1-selection" class="team-selection"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Team 2 Players</label>
|
||||
<div class="checkbox-grid" id="team2-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
<input type="checkbox" name="team2_player_ids" value="{{ player.id }}" {% if player.id in team2_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 2)">
|
||||
{{ player.full_name }}
|
||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
||||
<div class="player-pool">
|
||||
<div class="player-pool-header">Available Players</div>
|
||||
<div id="available-players-pool" class="available-players-pool">
|
||||
<p class="text-muted small">All registered players are shown. Click time slots to filter available players.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="team-column">
|
||||
<h5 class="team-header">Team 2</h5>
|
||||
<div id="team2-selection" class="team-selection"></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>
|
||||
|
||||
<!-- Player Scrim Selection -->
|
||||
{% else %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4>
|
||||
|
||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
||||
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label>Players</label>
|
||||
<div id="player-scrim-section" {% if match.match_type != 'player_scrim' %}class="hidden"{% endif %}>
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4>
|
||||
|
||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
||||
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="checkbox-grid" id="scrim-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
@@ -207,7 +199,6 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@@ -223,7 +214,169 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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>
|
||||
// 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)
|
||||
var TIME_SLOTS = [];
|
||||
for (var h = 12; h <= 24; h++) {
|
||||
@@ -258,10 +411,11 @@ var DAYS = [
|
||||
];
|
||||
|
||||
var selectedDate = '';
|
||||
var selectedSlots = []; // Array of selected time slots: ['17:00', '17:30', '18:00']
|
||||
var selectedSlots = [];
|
||||
var allDisponibilities = {};
|
||||
var canViewDisponibilities = {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}true{% else %}false{% endif %};
|
||||
var totalPlayers = {{ all_players|length }};
|
||||
var availablePlayersForSlots = []; // Players available in ALL selected slots
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -273,6 +427,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
selectedDate = this.value;
|
||||
if (allDisponibilitiesInitialized()) {
|
||||
renderMergedDisponibilityGrid();
|
||||
updatePlayerPool();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -302,8 +457,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
|
||||
renderMergedDisponibilityGrid();
|
||||
updateDisponibilityIndicators();
|
||||
// Initialize team assignments for player_vs_player matches
|
||||
// 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();
|
||||
});
|
||||
|
||||
@@ -318,6 +499,7 @@ function fetchDisponibilities() {
|
||||
.then(function(data) {
|
||||
allDisponibilities = data;
|
||||
renderMergedDisponibilityGrid();
|
||||
updatePlayerPool();
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Error fetching disponibilities:', error);
|
||||
@@ -518,8 +700,10 @@ function fetchAvailablePlayersForSlots() {
|
||||
var jsDay = dateObj.getDay();
|
||||
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
||||
|
||||
// Find players available in ANY selected slot
|
||||
var availableIds = [];
|
||||
for (var playerId in allDisponibilities) {
|
||||
if (!allRegisteredPlayers.includes(parseInt(playerId))) continue;
|
||||
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
||||
var hasSlot = selectedSlots.some(function(slot) {
|
||||
return playerSlots.includes(slot);
|
||||
@@ -529,18 +713,8 @@ function fetchAvailablePlayersForSlots() {
|
||||
}
|
||||
}
|
||||
|
||||
var matchType = '{{ match.match_type }}';
|
||||
|
||||
if (matchType === 'player_vs_player') {
|
||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
||||
cb.checked = availableIds.includes(parseInt(cb.value));
|
||||
});
|
||||
} else if (matchType === 'player_scrim') {
|
||||
document.querySelectorAll('input[name="player_ids"]').forEach(function(cb) {
|
||||
cb.checked = availableIds.includes(parseInt(cb.value));
|
||||
});
|
||||
}
|
||||
|
||||
// Update player pool for PvP section
|
||||
updatePlayerPool();
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
@@ -552,12 +726,111 @@ function clearTimeSelection() {
|
||||
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
|
||||
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() {
|
||||
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 team2Size = checkedCount - team1Size;
|
||||
var team2Size = assignedCount - team1Size;
|
||||
|
||||
if (team2Size < 0) team2Size = 0;
|
||||
|
||||
@@ -565,92 +838,62 @@ function updateRandomizePreview() {
|
||||
}
|
||||
|
||||
function randomizeTeams() {
|
||||
var allChecked = [];
|
||||
var allAssigned = [];
|
||||
|
||||
document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').forEach(function(cb) {
|
||||
allChecked.push(parseInt(cb.value));
|
||||
// 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 (allChecked.length === 0) {
|
||||
alert('Please select at least one player before randomizing teams.');
|
||||
if (allAssigned.length === 0) {
|
||||
alert('Please assign at least one player before randomizing teams.');
|
||||
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 temp = allChecked[i];
|
||||
allChecked[i] = allChecked[j];
|
||||
allChecked[j] = temp;
|
||||
var temp = allAssigned[i];
|
||||
allAssigned[i] = allAssigned[j];
|
||||
allAssigned[j] = temp;
|
||||
}
|
||||
|
||||
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
||||
var team1Players = allChecked.slice(0, team1Size);
|
||||
var team2Players = allChecked.slice(team1Size);
|
||||
var team1Players = allAssigned.slice(0, team1Size);
|
||||
var team2Players = allAssigned.slice(team1Size);
|
||||
|
||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
// 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 cb = document.querySelector('input[name="team1_player_ids"][value="' + pid + '"]');
|
||||
if (cb) cb.checked = true;
|
||||
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 cb = document.querySelector('input[name="team2_player_ids"][value="' + pid + '"]');
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
function handleTeamSelection(checkbox, teamSide) {
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
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>
|
||||
{% 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 %}
|
||||
<div class="tryout-detail">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="card-header">
|
||||
<h3>Tryout Details</h3>
|
||||
<div class="card-actions">
|
||||
{% if can_edit %}
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Game</span>
|
||||
<span class="detail-value">{{ tryout.game }}</span>
|
||||
@@ -249,9 +249,14 @@
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-futbol"></i> Schedule</h3>
|
||||
{% if can_edit %}
|
||||
<a href="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="btn btn-sm btn-success">
|
||||
<i class="fas fa-plus"></i> Schedule Match
|
||||
</a>
|
||||
<div class="card-actions">
|
||||
<a href="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="btn btn-sm btn-success">
|
||||
<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 %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -266,7 +271,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if matches %}
|
||||
{% if matches %}
|
||||
<!-- Matches Table -->
|
||||
<div class="table-container mt-3">
|
||||
<table class="table">
|
||||
@@ -301,59 +306,59 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ m.date.strftime('%m/%d/%Y') }}</td>
|
||||
<td>
|
||||
{% if m.match_type == 'team_vs_team' %}
|
||||
<div class="match-teams">
|
||||
<div class="match-team">
|
||||
<span class="team-name">{{ m.team1.name if m.team1 else 'Team 1' }}</span>
|
||||
{% if participants.team1_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team1_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="match-vs">vs</div>
|
||||
<div class="match-team">
|
||||
<span class="team-name">{{ m.team2.name if m.team2 else 'Team 2' }}</span>
|
||||
{% if participants.team2_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team2_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif m.match_type == 'player_vs_player' %}
|
||||
<div class="match-teams">
|
||||
<div class="match-team">
|
||||
<span class="team-name">Team 1</span>
|
||||
{% if participants.team1_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team1_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="match-vs">vs</div>
|
||||
<div class="match-team">
|
||||
<span class="team-name">Team 2</span>
|
||||
{% if participants.team2_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team2_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ participants | join(', ') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if m.match_type == 'team_vs_team' %}
|
||||
<div class="match-teams">
|
||||
<div class="match-team">
|
||||
<span class="team-name">{{ m.team1.name if m.team1 else 'Team 1' }}</span>
|
||||
{% if participants.team1_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team1_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="match-vs">vs</div>
|
||||
<div class="match-team">
|
||||
<span class="team-name">{{ m.team2.name if m.team2 else 'Team 2' }}</span>
|
||||
{% if participants.team2_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team2_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif m.match_type == 'player_vs_player' %}
|
||||
<div class="match-teams">
|
||||
<div class="match-team">
|
||||
<span class="team-name">Team 1</span>
|
||||
{% if participants.team1_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team1_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="match-vs">vs</div>
|
||||
<div class="match-team">
|
||||
<span class="team-name">Team 2</span>
|
||||
{% if participants.team2_players %}
|
||||
<ul class="team-players-list">
|
||||
{% for pl in participants.team2_players %}
|
||||
<li>{{ pl.name }}{% if pl.position %} <span class="position-tag">{{ pl.position }}</span>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ participants | join(', ') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if m.start_time and m.end_time %}
|
||||
{{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }}
|
||||
@@ -369,6 +374,9 @@
|
||||
<a href="{{ url_for('matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</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>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -433,8 +441,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
||||
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
||||
|
||||
@@ -452,7 +461,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
events: '/matches/api/events/{{ tryout.id }}',
|
||||
height: '300px',
|
||||
eventClick: function(info) {
|
||||
// Could show match details or edit link
|
||||
if (info.event.extendedProps.match_id) {
|
||||
window.location.href = '/matches/' + info.event.extendedProps.match_id + '/edit';
|
||||
}
|
||||
@@ -462,4 +470,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user