diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index c56d9c3..cfa6192 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 737a8e0..ecf4b09 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/__pycache__/seed.cpython-313.pyc b/__pycache__/seed.cpython-313.pyc index 512ba58..7a933de 100644 Binary files a/__pycache__/seed.cpython-313.pyc and b/__pycache__/seed.cpython-313.pyc differ diff --git a/app.py b/app.py index b48276e..9b035d9 100644 --- a/app.py +++ b/app.py @@ -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('
'.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 diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index bec2e21..bbc885b 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/models.py b/models.py index 4f2a64a..f458d0c 100644 --- a/models.py +++ b/models.py @@ -665,4 +665,128 @@ class Contract(db.Model): bool: True if user is the player associated with the contract. """ # Only the player can upload their signed contract - return user.id == self.player_id \ No newline at end of file + 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]) diff --git a/requirements.txt b/requirements.txt index 8c8f4a9..5c75244 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/routes/__pycache__/teams.cpython-313.pyc b/routes/__pycache__/teams.cpython-313.pyc index 9457852..beb6f71 100644 Binary files a/routes/__pycache__/teams.cpython-313.pyc and b/routes/__pycache__/teams.cpython-313.pyc differ diff --git a/routes/__pycache__/users.cpython-313.pyc b/routes/__pycache__/users.cpython-313.pyc index b9cca18..5fd218c 100644 Binary files a/routes/__pycache__/users.cpython-313.pyc and b/routes/__pycache__/users.cpython-313.pyc differ diff --git a/routes/teams.py b/routes/teams.py index 3b01ddb..5fdb422 100644 --- a/routes/teams.py +++ b/routes/teams.py @@ -6,7 +6,7 @@ This module handles CRUD operations for organization teams and player assignment from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required, current_user from extensions import db -from models import OrgTeam, User +from models import OrgTeam, User, Team, TeamMember, PersonalNote, TeamNote, Tryout teams_bp = Blueprint('teams', __name__, url_prefix='/teams') @@ -220,4 +220,75 @@ def remove_player(team_id, player_id): player.team_id = None db.session.commit() flash(f'{player.full_name} removed from {team.name}.', 'success') - return redirect(url_for('teams.list_teams')) \ No newline at end of file + return redirect(url_for('teams.list_teams')) + + +# Note actions from team page + + +@teams_bp.route('//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('//add-player-note/', 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')) diff --git a/routes/users.py b/routes/users.py index 48fa505..1bdeb0f 100644 --- a/routes/users.py +++ b/routes/users.py @@ -8,9 +8,10 @@ import os from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask_login import login_required, current_user from extensions import db, hash_password -from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam +from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam, CoachAvailability, TeamNote, PersonalNote, OneOnOneRequest, Match, Team, TeamMember, MatchParticipant, Tryout from werkzeug.utils import secure_filename -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date as date_type +import requests users_bp = Blueprint('users', __name__, url_prefix='/users') @@ -716,4 +717,693 @@ def download_signed_contract(contract_id): flash('No signed contract available.', 'danger') return redirect(url_for('users.list_contracts')) - return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename) \ No newline at end of file + 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//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/') +@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//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//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) diff --git a/seed.py b/seed.py index 18b004b..09af244 100644 --- a/seed.py +++ b/seed.py @@ -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 ===") diff --git a/templates/layouts/base.html b/templates/layouts/base.html index 1106c27..f972f01 100644 --- a/templates/layouts/base.html +++ b/templates/layouts/base.html @@ -67,18 +67,52 @@ {% endif %} -
  • - - - Contracts - -
  • My Profile
  • + {% if current_user.role == 'player' %} +
  • + + + One on One + +
  • +
  • + + + My Notes + +
  • + {% endif %} + {% if current_user.role == 'coach' %} +
  • + + + Availability + +
  • +
  • + + + Team Notes + +
  • +
  • + + + Personal Notes + +
  • + {% endif %} +
  • + + + Contracts + +
  • diff --git a/templates/pages/add_note_from_match.html b/templates/pages/add_note_from_match.html new file mode 100644 index 0000000..f2eaedb --- /dev/null +++ b/templates/pages/add_note_from_match.html @@ -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 %}Home / Tryouts / {{ tryout.title }} / Add Note{% endblock %} + +{% block content %} +
    +
    +

    Add Note for Match: {{ match.title }}

    + {{ match.date.strftime('%m/%d/%Y') }} +
    +
    +
    + + +
    + + +
    + +
    + + +

    This note will be linked to this match and visible to the selected player.

    +
    + +
    + + + Back to Tryout + +
    +
    +
    +
    + +{% if team_notes %} +
    +
    +

    Team Notes Reference

    +
    +
    +
    + {% for note in team_notes %} +
    + + {{ note.coach.full_name if note.coach else 'Unknown Coach' }} + + {{ note.content | nl2br }} +
    + {% endfor %} +
    +
    +
    +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/pages/add_note_from_tryout.html b/templates/pages/add_note_from_tryout.html new file mode 100644 index 0000000..6591e6b --- /dev/null +++ b/templates/pages/add_note_from_tryout.html @@ -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 %}Home / Tryouts / {{ tryout.title }} / Add Note{% endblock %} + +{% block content %} +
    +
    +

    Add Note for Tryout: {{ tryout.title }}

    + {{ tryout.date.strftime('%m/%d/%Y') }} +
    +
    +
    + + +
    + + +
    + +
    + + +

    This note will be linked to this tryout and visible to the selected player.

    +
    + +
    + + + Back to Tryout + +
    +
    +
    +
    + +{% if team_notes %} +
    +
    +

    Team Notes Reference

    +
    +
    +
    + {% for note in team_notes %} +
    + + {{ note.coach.full_name if note.coach else 'Unknown Coach' }} + + {{ note.content | nl2br }} +
    + {% endfor %} +
    +
    +
    +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/pages/add_personal_note.html b/templates/pages/add_personal_note.html new file mode 100644 index 0000000..36084c2 --- /dev/null +++ b/templates/pages/add_personal_note.html @@ -0,0 +1,80 @@ +{% extends "layouts/base.html" %} +{% block title %}Add Note - TryoutPro{% endblock %} +{% block page_title %}Add Personal Note{% endblock %} +{% block breadcrumb %}Home / My Notes / Add Note{% endblock %} + +{% block content %} +
    +
    +

    Add Personal Note

    + {% if org_team %} + {{ org_team.name }} + {% endif %} +
    +
    +
    + + +
    + + +
    + +
    + + +

    These notes will only be visible to the selected player.

    +
    + +
    + +

    Link this note to a specific match, tryout, or team for better organization.

    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + + + Back to Notes + +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/pages/coach_availability.html b/templates/pages/coach_availability.html new file mode 100644 index 0000000..e154bd5 --- /dev/null +++ b/templates/pages/coach_availability.html @@ -0,0 +1,260 @@ +{% extends "layouts/base.html" %} +{% block title %}Manage Availability - TryoutPro{% endblock %} +{% block page_title %}Manage Availability{% endblock %} +{% block breadcrumb %}Home / Coach Availability{% endblock %} + +{% block content %} +
    +
    +

    Set Your Weekly Availability

    +

    Select time slots when you're available for One on One sessions

    +
    +
    +
    +

    Loading availability grid...

    +
    + +
    + +
    +
    +
    + +
    +
    +

    Current Availability

    +
    +
    + {% if existing_availability %} + + + + + + + + + + {% for av in existing_availability %} + + + + + + {% endfor %} + +
    DayTimeAction
    {{ ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'][av.day_of_week] }}{{ av.start_time.strftime('%I:%M %p') }} - {{ av.end_time.strftime('%I:%M %p') }} + +
    + {% else %} +

    No availability slots set. Use the grid above to add your available times.

    + {% endif %} +
    +
    +{% endblock %} + +{% block scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/create_match.html b/templates/pages/create_match.html index acaed23..b9aca6d 100644 --- a/templates/pages/create_match.html +++ b/templates/pages/create_match.html @@ -114,7 +114,19 @@

    Select Players

    -

    Click on players to assign them to Team 1 or Team 2. Players available in all selected time blocks are shown below.

    + +
    +
    + + + vs + 0 + +
    +

    Select players then click Randomize to split them into teams.

    +
    @@ -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); +} {% endblock %} \ No newline at end of file diff --git a/templates/pages/edit_match.html b/templates/pages/edit_match.html index 256cdeb..34ddcf0 100644 --- a/templates/pages/edit_match.html +++ b/templates/pages/edit_match.html @@ -7,6 +7,7 @@

    Edit Match

    +

    Match Type: {{ match.match_type.replace('_', ' ').title() }}

    @@ -36,7 +37,7 @@ {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}

    Select Match Time

    -

    Click time slots consecutively to set match duration. Available players will be auto-selected below.

    +

    Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.

    - {% if match.match_type == 'team_vs_team' %} -
    -

    Teams

    - -
    -
    - - +
    +
    +

    Teams

    + +
    +
    + + +
    +
    + + +
    -
    - - + + +
    + {% if match.team1 %} +
    +
    {{ match.team1.name }}
    +
      + {% for member in match.team1.members %} +
    • {{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %}
    • + {% else %} +
    • No players assigned
    • + {% endfor %} +
    +
    + {% endif %} + {% if match.team2 %} +
    +
    {{ match.team2.name }}
    +
      + {% for member in match.team2.members %} +
    • {{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %}
    • + {% else %} +
    • No players assigned
    • + {% endfor %} +
    +
    + {% endif %}
    - -
    - {% if match.team1 %} -
    -
    {{ match.team1.name }}
    -
      - {% for member in match.team1.members %} -
    • {{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %}
    • - {% else %} -
    • No players assigned
    • - {% endfor %} -
    -
    - {% endif %} - {% if match.team2 %} -
    -
    {{ match.team2.name }}
    -
      - {% for member in match.team2.members %} -
    • {{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %}
    • - {% else %} -
    • No players assigned
    • - {% endfor %} -
    -
    - {% endif %} -
    - - {% elif match.match_type == 'player_vs_player' %} -
    -

    Select Players

    - - -
    -
    - - - vs - 0 - +
    +
    +

    Select Players

    + + +
    +
    + + + vs + 0 + +
    +

    Select players then click Randomize to split them into teams.

    -

    Select players then click Randomize to split them into teams.

    -
    - - {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %} -

    Green indicators show player availability for the match date/time

    - {% endif %} - -
    -
    - -
    - {% for player in all_players %} - - {% endfor %} + +
    +
    +
    Team 1
    +
    -
    -
    - -
    - {% for player in all_players %} - - {% endfor %} + +
    +
    Available Players
    +
    +

    All registered players are shown. Click time slots to filter available players.

    +
    +
    + +
    +
    Team 2
    +
    + + +
    - {% else %} -
    -

    Select Players

    - - {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %} -

    Green indicators show player availability for the match date/time

    - {% endif %} - -
    - +
    +
    +

    Select Players

    + + {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %} +

    Green indicators show player availability for the match date/time

    + {% endif %} +
    {% for player in all_players %}
    - {% endif %}
    +
    + + {% else %} +

    You need to be assigned to a team with a coach to request a One on One session.

    + {% endif %} +
    +
    +
    + +{% if coach %} + + +{% endif %} +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/personal_notes.html b/templates/pages/personal_notes.html new file mode 100644 index 0000000..35efcd0 --- /dev/null +++ b/templates/pages/personal_notes.html @@ -0,0 +1,64 @@ +{% extends "layouts/base.html" %} +{% block title %}Personal Notes - TryoutPro{% endblock %} +{% block page_title %}Personal Notes{% endblock %} +{% block breadcrumb %}Home / Personal Notes{% endblock %} + +{% block content %} +
    +
    +

    Add Personal Note

    + {% if org_team %} + {{ org_team.name }} + {% endif %} +
    +
    +
    + + +
    + + +
    + +
    + + +

    These notes will only be visible to the selected player.

    +
    + +
    + +
    +
    +
    +
    + +{% if personal_notes %} +
    +
    +

    Recent Notes

    +
    +
    +
    + {% for note in personal_notes %} +
    + + {{ note.player.full_name if note.player else 'Unknown Player' }} - + {{ note.created_at.strftime('%B %d, %Y') if note.created_at else 'Unknown date' }} + + {{ note.content | nl2br if note.content else '' }} + From: {{ note.coach.full_name if note.coach else 'Unknown Coach' }} +
    + {% endfor %} +
    +
    +
    +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/pages/player_personal_notes.html b/templates/pages/player_personal_notes.html new file mode 100644 index 0000000..c9a69ae --- /dev/null +++ b/templates/pages/player_personal_notes.html @@ -0,0 +1,85 @@ +{% extends "layouts/base.html" %} +{% block title %}My Notes - TryoutPro{% endblock %} +{% block page_title %}My Notes{% endblock %} +{% block breadcrumb %}Home / One on One / My Notes{% endblock %} + +{% block content %} +
    + +
    +
    +

    Personal Notes

    + {% if org_team %} + Team: {{ org_team.name }} + {% endif %} +
    +
    + {% if personal_notes %} + {% for note in personal_notes %} +
    +
    + + Note from {{ note.coach.full_name if note.coach else 'Unknown Coach' }} + + {{ note.content | nl2br }} +
    + {% if note.match_id and note.match %} + + Match: {{ note.match.title }} + + {% endif %} + {% if note.team_id and note.team %} + + Team: {{ note.team.name }} + + {% endif %} + {% if note.tryout_id and note.tryout %} + + Tryout: {{ note.tryout.title }} + + {% endif %} +
    +
    +
    +

    Added: {{ note.created_at.strftime('%B %d, %Y at %I:%M %p') }}

    + {% endfor %} + {% else %} +

    No personal notes have been added yet. Your coach may provide individual feedback here.

    + {% endif %} +
    +
    + + +
    +
    +

    Team Notes

    + {% if org_team %} + {{ org_team.name }} + {% endif %} +
    +
    + {% if team_notes %} + {% for note in team_notes %} +
    +
    + Coach: {{ note.coach.full_name if note.coach else 'Unknown Coach' }} + {{ note.content | nl2br }} +
    +
    +

    Updated: {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') }}

    + {% endfor %} + {% else %} +

    No team notes have been added yet. Your coach will post improvement suggestions here.

    + {% endif %} +
    +
    +
    + + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/team_notes.html b/templates/pages/team_notes.html new file mode 100644 index 0000000..9e85fcd --- /dev/null +++ b/templates/pages/team_notes.html @@ -0,0 +1,58 @@ +{% extends "layouts/base.html" %} +{% block title %}Team Notes - TryoutPro{% endblock %} +{% block page_title %}Team Notes{% endblock %} +{% block breadcrumb %}Home / Team Notes{% endblock %} + +{% block content %} +
    +
    +

    Team Improvement Notes

    + {% if org_team %} + {{ org_team.name }} + {% endif %} +
    +
    +
    + + +
    + + +

    These notes will be visible to all players on your team.

    +
    + +
    + +
    +
    +
    +
    + +{% if team_notes %} +
    +
    +

    Note History

    +
    +
    + + + + + + + + + {% for note in team_notes %} + + + + + {% endfor %} + +
    Last UpdatedContent Preview
    {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') if note.updated_at else 'Unknown date' }}{{ note.content[:100] if note.content else '' }}{% if note.content and note.content|length > 100 %}...{% endif %}
    +
    +
    +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/pages/view_tryout.html b/templates/pages/view_tryout.html index e1a124b..9f0b80f 100644 --- a/templates/pages/view_tryout.html +++ b/templates/pages/view_tryout.html @@ -6,7 +6,7 @@ {% block content %}
    -
    +

    Tryout Details

    {% if can_edit %} @@ -32,7 +32,7 @@
    -
    +
    Game {{ tryout.game }} @@ -249,9 +249,14 @@

    Schedule

    {% if can_edit %} - - Schedule Match - + {% endif %}
    @@ -266,7 +271,7 @@
    {% endif %} -{% if matches %} + {% if matches %}
    @@ -301,59 +306,59 @@ - + {% endif %} @@ -433,8 +441,9 @@ + - -{% endblock %} +{% endblock %} \ No newline at end of file
    {{ m.date.strftime('%m/%d/%Y') }} - {% if m.match_type == 'team_vs_team' %} -
    -
    - {{ m.team1.name if m.team1 else 'Team 1' }} - {% if participants.team1_players %} -
      - {% for pl in participants.team1_players %} -
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • - {% endfor %} -
    - {% endif %} -
    -
    vs
    -
    - {{ m.team2.name if m.team2 else 'Team 2' }} - {% if participants.team2_players %} -
      - {% for pl in participants.team2_players %} -
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • - {% endfor %} -
    - {% endif %} -
    -
    - {% elif m.match_type == 'player_vs_player' %} -
    -
    - Team 1 - {% if participants.team1_players %} -
      - {% for pl in participants.team1_players %} -
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • - {% endfor %} -
    - {% endif %} -
    -
    vs
    -
    - Team 2 - {% if participants.team2_players %} -
      - {% for pl in participants.team2_players %} -
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • - {% endfor %} -
    - {% endif %} -
    -
    - {% else %} - {{ participants | join(', ') }} - {% endif %} -
    + {% if m.match_type == 'team_vs_team' %} +
    +
    + {{ m.team1.name if m.team1 else 'Team 1' }} + {% if participants.team1_players %} +
      + {% for pl in participants.team1_players %} +
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • + {% endfor %} +
    + {% endif %} +
    +
    vs
    +
    + {{ m.team2.name if m.team2 else 'Team 2' }} + {% if participants.team2_players %} +
      + {% for pl in participants.team2_players %} +
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • + {% endfor %} +
    + {% endif %} +
    +
    + {% elif m.match_type == 'player_vs_player' %} +
    +
    + Team 1 + {% if participants.team1_players %} +
      + {% for pl in participants.team1_players %} +
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • + {% endfor %} +
    + {% endif %} +
    +
    vs
    +
    + Team 2 + {% if participants.team2_players %} +
      + {% for pl in participants.team2_players %} +
    • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
    • + {% endfor %} +
    + {% endif %} +
    +
    + {% else %} + {{ participants | join(', ') }} + {% endif %} +
    {% if m.start_time and m.end_time %} {{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }} @@ -369,6 +374,9 @@ Edit + + Note +