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:
cedrick2711
2026-07-16 20:34:56 -04:00
parent b02fe29e30
commit 24fcc9a048
24 changed files with 2625 additions and 264 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18
View File
@@ -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.
+124
View File
@@ -666,3 +666,127 @@ class Contract(db.Model):
"""
# Only the player can upload their signed contract
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])
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+72 -1
View File
@@ -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')
@@ -221,3 +221,74 @@ def remove_player(team_id, player_id):
db.session.commit()
flash(f'{player.full_name} removed from {team.name}.', 'success')
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'))
+692 -2
View File
@@ -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')
@@ -717,3 +718,692 @@ def download_signed_contract(contract_id):
return redirect(url_for('users.list_contracts'))
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)
+135 -2
View File
@@ -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 ===")
+40 -6
View File
@@ -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">
+63
View File
@@ -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 %}
+63
View File
@@ -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 %}
+80
View File
@@ -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 %}
+260
View File
@@ -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()">&times;</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 %}
+119 -2
View File
@@ -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 %}
+422 -179
View File
@@ -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 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 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>
</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>
<!-- 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>
{% 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>
<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>
<!-- 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="pvp-layout">
<div class="team-column">
<h5 class="team-header">Team 1</h5>
<div id="team1-selection" class="team-selection"></div>
</div>
<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="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>
<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="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>
<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 %}
{% 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 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 %}
+250
View File
@@ -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 %}
+64
View File
@@ -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 %}
+58
View File
@@ -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 %}
+69 -61
View File
@@ -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';
}