from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user from extensions import db from models import User, Tryout, Match, MatchParticipant, Team, TeamMember, OrgTeam, TryoutRegistration, PlayerDisponibility from datetime import datetime, time, timedelta matches_bp = Blueprint('matches', __name__, url_prefix='/matches') def can_schedule_match(): """Check if user can schedule matches (coaches and above).""" return current_user.role in ['president', 'manager', 'coach', 'scout'] @matches_bp.route('/calendar') @login_required def calendar(): """Calendar view showing tryouts and matches.""" return render_template('pages/calendar.html') @matches_bp.route('/api/events') @login_required def api_events(): """API endpoint returning calendar events.""" events = [] # Get tryouts based on user permissions tryouts = get_visible_tryouts_for_user() for tryout in tryouts: events.append({ 'id': f'tryout_{tryout.id}', 'title': tryout.title, 'date': tryout.date.strftime('%Y-%m-%d'), 'type': 'tryout', 'color': '#3b82f6', # Blue for tryouts 'extendedProps': { 'location': tryout.location or 'TBD', 'status': tryout.status, 'description': tryout.description or '', 'tryout_id': tryout.id } }) # Add matches for this tryout for match in tryout.matches: match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b' # Build match description with participants match_desc = match.description or '' participants_str = '' if match.match_type == 'team_vs_team': teams = [] if match.team1: teams.append(match.team1.name) if match.team2: teams.append(match.team2.name) participants_str = f"{' vs '.join(teams)}" match_desc = participants_str + (f"
{match.description}" if match.description else '') else: # Player scrim - show all participants player_names = [] for p in match.participants.all(): player_name = p.player.full_name if p.player else 'Unknown Player' player_names.append(player_name) participants_str = ', '.join(player_names) if player_names else 'No players' match_desc = participants_str + (f"
{match.description}" if match.description else '') # Include time for calendar display start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None events.append({ 'id': f'match_{match.id}', 'title': match.title, 'date': match.date.strftime('%Y-%m-%d'), 'type': 'match', 'color': match_color, 'extendedProps': { 'location': match.location or tryout.location or 'TBD', 'status': match.status, 'description': match_desc, 'match_type': match.match_type, 'tryout_id': tryout.id, 'match_id': match.id, 'start_time': start_time_str, 'end_time': end_time_str, 'participants': participants_str } }) return jsonify(events) @matches_bp.route('/api/events/') @login_required def api_events_for_tryout(tryout_id): """API endpoint returning calendar events for a specific tryout.""" tryout = Tryout.query.get_or_404(tryout_id) # Check if user can view this tryout can_view = current_user.can_manage_this_tryout(tryout) # For players, check if they're registered or participating in a match is_registered = False player_in_match = False if current_user.role == 'player': is_registered = TryoutRegistration.query.filter_by( tryout_id=tryout_id, player_id=current_user.id ).first() is not None # Check if player is participating in any matches for this tryout player_matches = Match.query.join(MatchParticipant).filter( MatchParticipant.player_id == current_user.id, Match.tryout_id == tryout_id ).all() player_in_match = len(player_matches) > 0 # Non-participating players cannot see the calendar if not can_view and not is_registered and not player_in_match: return jsonify([]) events = [] # Add tryout date as an event (read-only, for context) events.append({ 'id': f'tryout_{tryout.id}', 'title': f'Tryout: {tryout.title}', 'date': tryout.date.strftime('%Y-%m-%d'), 'type': 'tryout', 'color': '#3b82f6', # Blue for tryouts 'extendedProps': { 'location': tryout.location or 'TBD', 'status': tryout.status, 'description': tryout.description or '', 'tryout_id': tryout.id } }) for match in tryout.matches: # Determine color based on match type if match.match_type == 'team_vs_team' or match.match_type == 'player_vs_player': match_color = '#10b981' # Green for team matches else: match_color = '#f59e0b' # Orange for scrims # Build participant string with proper grouping participants_str = '' if match.match_type == 'team_vs_team': teams = [] if match.team1: teams.append(match.team1.name) if match.team2: teams.append(match.team2.name) participants_str = f"{' vs '.join(teams)}" elif match.match_type == 'player_vs_player': # Get players grouped by team side team1_players = [] for p in match.participants.filter_by(team_side=1).all(): if p.player: team1_players.append(p.player.full_name) team2_players = [] for p in match.participants.filter_by(team_side=2).all(): if p.player: team2_players.append(p.player.full_name) if team1_players and team2_players: participants_str = f"{', '.join(team1_players)} vs {', '.join(team2_players)}" else: participants_str = 'TBD vs TBD' else: player_names = [] for p in match.participants.all(): player_name = p.player.full_name if p.player else 'Unknown Player' player_names.append(player_name) participants_str = ', '.join(player_names) if player_names else 'No players' # Include time for calendar display start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None events.append({ 'id': f'match_{match.id}', 'title': match.title, 'date': match.date.strftime('%Y-%m-%d'), 'type': 'match', 'color': match_color, 'extendedProps': { 'location': match.location or tryout.location or 'TBD', 'status': match.status, 'match_type': match.match_type, 'tryout_id': tryout.id, 'match_id': match.id, 'participants': participants_str, 'start_time': start_time_str, 'end_time': end_time_str } }) return jsonify(events) def get_visible_tryouts_for_user(): """Get tryouts that the current user can see based on their role.""" if current_user.role == 'president': return Tryout.query.order_by(Tryout.date).all() elif current_user.role == 'manager': return Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date).all() elif current_user.role == 'coach': org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() if org_team: return Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date).all() return [] elif current_user.role == 'player': # Get tryouts player is registered for player_tryout_ids = [r.tryout_id for r in current_user.tryout_registrations.all()] tryouts = Tryout.query.filter(Tryout.id.in_(player_tryout_ids)).order_by(Tryout.date).all() if player_tryout_ids else [] # Also include matches where player is participating player_matches = Match.query.join(MatchParticipant).filter( MatchParticipant.player_id == current_user.id ).all() player_match_tryout_ids = list(set(m.tryout_id for m in player_matches)) additional_tryouts = Tryout.query.filter( Tryout.id.in_(player_match_tryout_ids) ).order_by(Tryout.date).all() if player_match_tryout_ids else [] # Combine and deduplicate all_tryouts = tryouts + [t for t in additional_tryouts if t.id not in player_tryout_ids] return all_tryouts else: # scout return Tryout.query.order_by(Tryout.date).all() @matches_bp.route('/create/', methods=['GET', 'POST']) @login_required def create_match(tryout_id): """Create a new match/scrimmage within a tryout.""" tryout = Tryout.query.get_or_404(tryout_id) # Check if user can manage this tryout (president, manager, or coach) if not current_user.can_manage_this_tryout(tryout): flash('You do not have permission to schedule matches for this tryout.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) teams = Team.query.filter_by(tryout_id=tryout_id).all() all_players = User.query.filter_by(role='player').order_by(User.full_name).all() if request.method == 'POST': title = request.form.get('title') description = request.form.get('description') date_str = request.form.get('date') start_time_str = request.form.get('start_time') end_time_str = request.form.get('end_time') location = request.form.get('location') match_type = request.form.get('match_type') # Start time is now mandatory if not start_time_str: flash('Start time is required. Please select a time slot.', 'danger') return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date except (ValueError, TypeError): flash('Invalid date format.', 'danger') return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) start_time = None end_time = None try: start_time = datetime.strptime(start_time_str, '%H:%M').time() # Auto-calculate end time if not provided (start + 30 minutes) if end_time_str: end_time = datetime.strptime(end_time_str, '%H:%M').time() else: # Auto-calculate end time as start + 30 minutes start_dt = datetime.combine(date_obj, start_time) end_dt = start_dt + timedelta(minutes=30) end_time = end_dt.time() except ValueError: flash('Invalid time format.', 'danger') return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) match = Match( tryout_id=tryout_id, title=title, description=description, date=date_obj, start_time=start_time, end_time=end_time, location=location, match_type=match_type, created_by=current_user.id ) db.session.add(match) db.session.flush() # Get match.id before commit # Handle team vs team matches if match_type == 'team_vs_team': team1_id = request.form.get('team1_id') team2_id = request.form.get('team2_id') match.team1_id = int(team1_id) if team1_id else None match.team2_id = int(team2_id) if team2_id else None # Handle player vs player matches elif match_type == 'player_vs_player': team1_player_ids = request.form.getlist('team1_player_ids') team2_player_ids = request.form.getlist('team2_player_ids') for pid in team1_player_ids: participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1) db.session.add(participant) for pid in team2_player_ids: participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=2) db.session.add(participant) # Handle player scrim matches elif match_type == 'player_scrim': player_ids = request.form.getlist('player_ids') for pid in player_ids: participant = MatchParticipant(match_id=match.id, player_id=int(pid)) db.session.add(participant) db.session.commit() flash('Match scheduled successfully!', 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) @matches_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_match(match_id): """Edit an existing match.""" match = Match.query.get_or_404(match_id) tryout = match.tryout if not current_user.can_manage_this_tryout(tryout): flash('You do not have permission to edit this match.', 'danger') return redirect(url_for('matches.calendar')) teams = Team.query.filter_by(tryout_id=tryout.id).all() all_players = User.query.filter_by(role='player').order_by(User.full_name).all() current_player_ids = [p.player_id for p in match.participants.all()] # Get players grouped by team side for player_vs_player matches team1_player_ids = [p.player_id for p in match.participants.filter_by(team_side=1).all()] team2_player_ids = [p.player_id for p in match.participants.filter_by(team_side=2).all()] if request.method == 'POST': match.title = request.form.get('title') match.description = request.form.get('description') date_str = request.form.get('date') start_time_str = request.form.get('start_time') end_time_str = request.form.get('end_time') location = request.form.get('location') status = request.form.get('status') try: match.date = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): flash('Invalid date format.', 'danger') return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) # Start time is now mandatory if not start_time_str: flash('Start time is required. Please select a time slot.', 'danger') return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) try: match.start_time = datetime.strptime(start_time_str, '%H:%M').time() # Auto-calculate end time if not provided (start + 30 minutes) if end_time_str: match.end_time = datetime.strptime(end_time_str, '%H:%M').time() else: # Auto-calculate end time as start + 30 minutes start_dt = datetime.combine(match.date, match.start_time) end_dt = start_dt + timedelta(minutes=30) match.end_time = end_dt.time() except ValueError: pass match.location = location if status in ['scheduled', 'completed', 'cancelled']: match.status = status # Handle team vs team matches if match.match_type == 'team_vs_team': team1_id = request.form.get('team1_id') team2_id = request.form.get('team2_id') match.team1_id = int(team1_id) if team1_id else None match.team2_id = int(team2_id) if team2_id else None # Handle player vs player matches - update participants elif match.match_type == 'player_vs_player': MatchParticipant.query.filter_by(match_id=match.id).delete() team1_player_ids = request.form.getlist('team1_player_ids') team2_player_ids = request.form.getlist('team2_player_ids') for pid in team1_player_ids: participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1) db.session.add(participant) for pid in team2_player_ids: participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=2) db.session.add(participant) # Handle player scrim matches - update participants elif match.match_type == 'player_scrim': MatchParticipant.query.filter_by(match_id=match.id).delete() player_ids = request.form.getlist('player_ids') for pid in player_ids: participant = MatchParticipant(match_id=match.id, player_id=int(pid)) db.session.add(participant) db.session.commit() flash('Match updated successfully!', 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids, team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids) @matches_bp.route('//delete', methods=['POST']) @login_required def delete_match(match_id): """Delete a match.""" match = Match.query.get_or_404(match_id) tryout = match.tryout if not current_user.can_manage_this_tryout(tryout): flash('You do not have permission to delete this match.', 'danger') return redirect(url_for('matches.calendar')) db.session.delete(match) db.session.commit() flash('Match deleted successfully.', 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) def get_players_available_at_time(date_str, time_str): """Get list of player IDs available at a specific date and time. Args: date_str: Date in YYYY-MM-DD format time_str: Time in HH:MM format Returns: List of player IDs who are available at that time """ try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() time_obj = datetime.strptime(time_str, '%H:%M').time() except (ValueError, TypeError): return [] # Calculate day of week (Python: 0=Monday, 6=Sunday) # JavaScript: 0=Sunday, 6=Saturday, so we convert date_parts = date_str.split('-') date_for_day = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2])) js_day = date_for_day.weekday() # Convert Python weekday (Mon=0) to our format (Mon=0) day_of_week = js_day # Get all active players players = User.query.filter_by(role='player', is_active_account=True).all() available_players = [] for player in players: # Check if player has disponibility at this time disponibilities = PlayerDisponibility.query.filter_by( player_id=player.id, day_of_week=day_of_week ).all() for disp in disponibilities: # Check if time falls within disponibility block disp_start = disp.start_time.hour * 60 + disp.start_time.minute disp_end = disp.end_time.hour * 60 + disp.end_time.minute match_time = time_obj.hour * 60 + time_obj.minute if disp_start <= match_time < disp_end: available_players.append(player.id) break return available_players @matches_bp.route('/api/available_players//