"""Team match management routes for regular season matches. Uses polymorphic isinstance checks instead of role-string comparisons. """ from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, OrgTeam, TeamMatch, TeamMatchParticipant, TeamPlayer, ) from datetime import datetime, timedelta from app.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.""" if isinstance(current_user, Admin): return True if isinstance(current_user, Manager): return True if isinstance(current_user, 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.""" filter_team_id = request.args.get('team_id', type=int) if isinstance(current_user, Admin): teams = OrgTeam.query.order_by(OrgTeam.name).all() matches_query = TeamMatch.query elif isinstance(current_user, Manager): teams = OrgTeam.query.order_by(OrgTeam.name).all() matches_query = TeamMatch.query elif isinstance(current_user, 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 isinstance(current_user, 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) 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() 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 regular-season team match.""" 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()] prefill_date = request.args.get('date', '') is_practice = request.args.get('type') == 'practice' default_title = 'Practice' if is_practice else f'Team Match — {team.name}' if is_practice and request.method == 'GET': 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() 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() # Discord notifications 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.""" 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.""" 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.""" if not current_user.can_schedule_matches(): return jsonify([]) if isinstance(current_user, (Admin, Manager)): teams = OrgTeam.query.order_by(OrgTeam.name).all() elif isinstance(current_user, 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 is_confirmed for a team match participant.""" 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 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', })