"""Team match management routes for regular season matches. This module handles CRUD operations for team-specific matches that are not tied to tryouts. Players are pre-filled from the team roster. """ 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 OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer from datetime import datetime, timedelta from discord_bot import send_schedule_notification team_matches_bp = Blueprint('team_matches', __name__, url_prefix='/team-matches') def can_manage_team_match(team): """Check if current user can manage matches for this team. Returns: bool: True if user is president, manager, or a coach of this team. """ if current_user.role in ['admin']: return True if current_user.role == 'manager': return True if current_user.role == 'coach': if team.coaches.filter_by(id=current_user.id).first(): return True if team.coach_id == current_user.id: return True return False @team_matches_bp.route('') @login_required def list_matches(): """List all team matches visible to the current user. Supports optional ?team_id= query param to pre-filter by team. Returns: Response: Rendered team matches list template. """ # Optional pre-filter by team_id from query param filter_team_id = request.args.get('team_id', type=int) if current_user.role == 'admin': teams = OrgTeam.query.order_by(OrgTeam.name).all() matches_query = TeamMatch.query elif current_user.role == 'manager': teams = OrgTeam.query.order_by(OrgTeam.name).all() matches_query = TeamMatch.query elif current_user.role == 'coach': teams = OrgTeam.query.filter( db.or_( OrgTeam.coaches.any(id=current_user.id), OrgTeam.coach_id == current_user.id ) ).order_by(OrgTeam.name).all() team_ids = [t.id for t in teams] matches_query = TeamMatch.query.filter( TeamMatch.org_team_id.in_(team_ids) ) if team_ids else TeamMatch.query.filter(TeamMatch.id == -1) elif current_user.role == 'player': player_team_ids = [tp.org_team_id for tp in current_user.team_placements] teams = OrgTeam.query.filter(OrgTeam.id.in_(player_team_ids)).all() if player_team_ids else [] matches_query = TeamMatch.query.filter( TeamMatch.org_team_id.in_(player_team_ids) ) if player_team_ids else TeamMatch.query.filter(TeamMatch.id == -1) else: teams = [] matches_query = TeamMatch.query.filter(TeamMatch.id == -1) # Apply team_id filter if provided if filter_team_id: matches_query = matches_query.filter(TeamMatch.org_team_id == filter_team_id) matches = matches_query.order_by(TeamMatch.date.desc()).all() # Build participants map for each match match_data = [] for tm in matches: confirmed, total = tm.get_confirmed_count() participants = [] for p in tm.participants.all(): participants.append({ 'id': p.id, 'player': p.player, 'is_confirmed': p.is_confirmed }) match_data.append({ 'match': tm, 'participants': participants, 'confirmed_count': confirmed, 'total_count': total }) return render_template('pages/team_matches.html', teams=teams, match_data=match_data, now=datetime.utcnow()) @team_matches_bp.route('//create', methods=['GET', 'POST']) @login_required def create_match(team_id): """Create a new team match (regular season). GET: Render the match creation form with pre-filled team roster. POST: Create the match with all team players as participants. Args: team_id: The ID of the org team to create a match for. Returns: Response: Create form or redirect to team matches list. """ team = OrgTeam.query.get_or_404(team_id) if not can_manage_team_match(team): flash('You do not have permission to schedule matches for this team.', 'danger') return redirect(url_for('team_matches.list_matches')) team_players = [tp for tp in TeamPlayer.query.filter_by(org_team_id=team_id).all()] # Allow pre-filling the date from query param (e.g., from calendar click) prefill_date = request.args.get('date', '') # Check if this is a practice (no opponent) is_practice = request.args.get('type') == 'practice' default_title = 'Practice' if is_practice else f'Team Match — {team.name}' # For practices, render the tryout match form (with availability calendar) if is_practice and request.method == 'GET': # Build a lightweight proxy for the tryout object the template expects class TryoutProxy: def __init__(self, team_obj): self.id = 0 self.title = team_obj.name self.date = '' self.game = '' self.target_org_team = team_obj proxy_tryout = TryoutProxy(team) all_players = [tp.player for tp in team_players if tp.player] return render_template('pages/match_form.html', tryout=proxy_tryout, teams=[], all_players=all_players, prefill_date=prefill_date, is_practice=True, team_id=team_id, team=team) if request.method == 'POST': title = request.form.get('title', default_title) opponent = request.form.get('opponent', '').strip() if not is_practice else None 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', '') if not date_str: flash('Date is required.', 'danger') return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date) try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): flash('Invalid date format.', 'danger') return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice) start_time = None end_time = None if start_time_str: try: start_time = datetime.strptime(start_time_str, '%H:%M').time() if end_time_str: end_time = datetime.strptime(end_time_str, '%H:%M').time() else: 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/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice) team_match = TeamMatch( org_team_id=team_id, title=title, description=description or None, opponent=opponent or None, date=date_obj, start_time=start_time, end_time=end_time, location=location or None, created_by=current_user.id ) db.session.add(team_match) db.session.flush() # Get team_match.id # Auto-add all team players as participants notified_participant_ids = [] for tp in team_players: participant = TeamMatchParticipant( team_match_id=team_match.id, player_id=tp.player_id ) db.session.add(participant) db.session.flush() notified_participant_ids.append(participant.id) db.session.commit() # Send Discord notifications to players event_date_str = date_obj.strftime('%Y-%m-%d') event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD' for i, tp in enumerate(team_players): reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else team_match.id send_schedule_notification( user_id=tp.player_id, event_type='match', event_title=team_match.title, event_date=event_date_str, event_time=event_time_str, reference_id=reference_id ) flash(f'Team match "{title}" scheduled successfully!', 'success') return redirect(url_for('team_matches.list_matches')) return render_template('pages/team_match_form.html', team=team, team_players=team_players) @team_matches_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_match(match_id): """Edit an existing team match. GET: Render the edit form. POST: Update match details. Args: match_id: The ID of the team match to edit. Returns: Response: Edit form or redirect to team matches list. """ team_match = TeamMatch.query.get_or_404(match_id) team = team_match.org_team if not can_manage_team_match(team): flash('You do not have permission to edit this match.', 'danger') return redirect(url_for('team_matches.list_matches')) if request.method == 'POST': team_match.title = request.form.get('title', team_match.title) team_match.description = request.form.get('description', '') or None team_match.opponent = request.form.get('opponent', '').strip() or None date_str = request.form.get('date') if date_str: try: team_match.date = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): flash('Invalid date format.', 'danger') return redirect(url_for('team_matches.edit_match', match_id=match_id)) start_time_str = request.form.get('start_time') if start_time_str: try: team_match.start_time = datetime.strptime(start_time_str, '%H:%M').time() except ValueError: pass end_time_str = request.form.get('end_time') if end_time_str: try: team_match.end_time = datetime.strptime(end_time_str, '%H:%M').time() except ValueError: pass team_match.location = request.form.get('location', '') or None status = request.form.get('status') if status in ['scheduled', 'completed', 'cancelled']: team_match.status = status db.session.commit() flash('Match updated successfully!', 'success') return redirect(url_for('team_matches.list_matches')) return render_template('pages/team_match_form.html', match=team_match, team=team, team_players=[]) @team_matches_bp.route('//delete', methods=['POST']) @login_required def delete_match(match_id): """Delete a team match. Args: match_id: The ID of the team match to delete. Returns: Response: Redirect to team matches list. """ team_match = TeamMatch.query.get_or_404(match_id) team = team_match.org_team if not can_manage_team_match(team): flash('You do not have permission to delete this match.', 'danger') return redirect(url_for('team_matches.list_matches')) db.session.delete(team_match) db.session.commit() flash('Match deleted successfully.', 'success') return redirect(url_for('team_matches.list_matches')) @team_matches_bp.route('/api/manageable-teams') @login_required def api_manageable_teams(): """API endpoint returning teams the current user can schedule matches for. Used by the calendar's "Create Event" modal. Returns: Response: JSON array of {id, name}. """ if not current_user.can_schedule_matches(): return jsonify([]) if current_user.role in ['admin', 'manager']: teams = OrgTeam.query.order_by(OrgTeam.name).all() elif current_user.role == 'coach': teams = OrgTeam.query.filter( db.or_( OrgTeam.coaches.any(id=current_user.id), OrgTeam.coach_id == current_user.id ) ).order_by(OrgTeam.name).all() else: return jsonify([]) return jsonify([{'id': t.id, 'name': t.name} for t in teams]) @team_matches_bp.route('//toggle-presence/', methods=['POST']) @login_required def toggle_presence(match_id, participant_id): """Toggle the is_confirmed status for a team match participant. Accessible to team managers/coaches AND the player themselves. Args: match_id: The ID of the team match. participant_id: The ID of the TeamMatchParticipant record. Returns: Response: JSON with new status. """ team_match = TeamMatch.query.get_or_404(match_id) team = team_match.org_team participant = TeamMatchParticipant.query.get_or_404(participant_id) if participant.team_match_id != match_id: return jsonify({'error': 'Participant does not belong to this match'}), 400 # Allow manager, coach, president, or the player themselves can_toggle = can_manage_team_match(team) or participant.player_id == current_user.id if not can_toggle: return jsonify({'error': 'Unauthorized'}), 403 participant.is_confirmed = not participant.is_confirmed db.session.commit() return jsonify({ 'participant_id': participant.id, 'is_confirmed': participant.is_confirmed, 'player_name': participant.player.username if participant.player else 'Unknown' })