ajout d'un calendrier pour sceduler des match
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -20,6 +20,7 @@ def create_app():
|
|||||||
from routes.users import users_bp
|
from routes.users import users_bp
|
||||||
from routes.main import main_bp
|
from routes.main import main_bp
|
||||||
from routes.teams import teams_bp
|
from routes.teams import teams_bp
|
||||||
|
from routes.matches import matches_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(tryouts_bp)
|
app.register_blueprint(tryouts_bp)
|
||||||
@@ -27,6 +28,7 @@ def create_app():
|
|||||||
app.register_blueprint(users_bp)
|
app.register_blueprint(users_bp)
|
||||||
app.register_blueprint(main_bp)
|
app.register_blueprint(main_bp)
|
||||||
app.register_blueprint(teams_bp)
|
app.register_blueprint(teams_bp)
|
||||||
|
app.register_blueprint(matches_bp)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
import models
|
import models
|
||||||
|
|||||||
Binary file not shown.
@@ -60,10 +60,13 @@ class User(UserMixin, db.Model):
|
|||||||
return self.role == 'president'
|
return self.role == 'president'
|
||||||
|
|
||||||
def can_manage_tryouts(self):
|
def can_manage_tryouts(self):
|
||||||
return self.role in ['president', 'manager']
|
return self.role in ['president', 'manager', 'coach', 'scout']
|
||||||
|
|
||||||
def can_manage_teams(self):
|
def can_manage_teams(self):
|
||||||
return self.role in ['president', 'manager']
|
return self.role in ['president', 'manager']
|
||||||
|
|
||||||
|
def can_schedule_matches(self):
|
||||||
|
return self.role in ['president', 'manager', 'coach']
|
||||||
|
|
||||||
def can_manage_this_tryout(self, tryout):
|
def can_manage_this_tryout(self, tryout):
|
||||||
"""Check if user can manage a specific tryout (president, or manager/coach in charge of it)."""
|
"""Check if user can manage a specific tryout (president, or manager/coach in charge of it)."""
|
||||||
@@ -181,3 +184,45 @@ class TeamMember(db.Model):
|
|||||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
player = db.relationship('User', overlaps="player_ref,team_assignments") # Many-to-one, no dynamic loader
|
player = db.relationship('User', overlaps="player_ref,team_assignments") # Many-to-one, no dynamic loader
|
||||||
|
|
||||||
|
|
||||||
|
class Match(db.Model):
|
||||||
|
"""Matches/scrimmages scheduled within tryouts."""
|
||||||
|
__tablename__ = 'matches'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
date = db.Column(db.Date, nullable=False)
|
||||||
|
start_time = db.Column(db.Time, nullable=True)
|
||||||
|
end_time = db.Column(db.Time, nullable=True)
|
||||||
|
location = db.Column(db.String(200), nullable=True)
|
||||||
|
status = db.Column(db.String(20), default='scheduled') # scheduled, completed, cancelled
|
||||||
|
match_type = db.Column(db.String(20), nullable=False) # team_vs_team, player_scrim
|
||||||
|
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# For team vs team matches
|
||||||
|
team1_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||||
|
team2_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||||
|
|
||||||
|
creator = db.relationship('User', backref='created_matches')
|
||||||
|
tryout = db.relationship('Tryout', backref='matches')
|
||||||
|
team1 = db.relationship('Team', foreign_keys=[team1_id], backref='matches_as_team1')
|
||||||
|
team2 = db.relationship('Team', foreign_keys=[team2_id], backref='matches_as_team2')
|
||||||
|
participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic')
|
||||||
|
|
||||||
|
def get_participating_players(self):
|
||||||
|
"""Return list of players participating in this match."""
|
||||||
|
return [p.player_id for p in self.participants.all()]
|
||||||
|
|
||||||
|
|
||||||
|
class MatchParticipant(db.Model):
|
||||||
|
"""Players participating in player scrimmage matches."""
|
||||||
|
__tablename__ = 'match_participants'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False)
|
||||||
|
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
player = db.relationship('User')
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,340 @@
|
|||||||
|
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
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
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"<br>{match.description}" if match.description else '')
|
||||||
|
else:
|
||||||
|
# Player scrim - show all participants
|
||||||
|
player_names = [p.player.full_name for p in match.participants.all()]
|
||||||
|
participants_str = ', '.join(player_names) if player_names else 'No players'
|
||||||
|
match_desc = participants_str + (f"<br>{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 + ' (' + participants_str + ')',
|
||||||
|
'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/<int:tryout_id>')
|
||||||
|
@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
|
||||||
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
|
# For players, check if they're registered or participating in the match
|
||||||
|
if current_user.role == 'player':
|
||||||
|
# Check if player is registered for this tryout
|
||||||
|
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
|
||||||
|
match_participation = MatchParticipant.query.filter(
|
||||||
|
MatchParticipant.match_id == MatchParticipant.match_id
|
||||||
|
).join(Match).filter(Match.tryout_id == tryout_id).all()
|
||||||
|
|
||||||
|
player_in_match = any(mp.player_id == current_user.id for mp in match_participation)
|
||||||
|
|
||||||
|
if not is_registered and not player_in_match:
|
||||||
|
return jsonify([])
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
for match in tryout.matches:
|
||||||
|
match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b'
|
||||||
|
|
||||||
|
# Build participant string
|
||||||
|
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)}"
|
||||||
|
else:
|
||||||
|
player_names = [p.player.full_name for p in match.participants.all()]
|
||||||
|
participants_str = ', '.join(player_names) if player_names else 'No players'
|
||||||
|
|
||||||
|
events.append({
|
||||||
|
'id': f'match_{match.id}',
|
||||||
|
'title': match.title + ' (' + participants_str + ')',
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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/<int:tryout_id>', 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')
|
||||||
|
|
||||||
|
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:
|
||||||
|
if start_time_str:
|
||||||
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
||||||
|
if end_time_str:
|
||||||
|
end_time = datetime.strptime(end_time_str, '%H:%M').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 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('/<int:match_id>/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()]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if start_time_str:
|
||||||
|
match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
||||||
|
if end_time_str:
|
||||||
|
match.end_time = datetime.strptime(end_time_str, '%H:%M').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 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)
|
||||||
|
|
||||||
|
|
||||||
|
@matches_bp.route('/<int:match_id>/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))
|
||||||
+19
-1
@@ -1,7 +1,7 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from extensions import db
|
from extensions import db
|
||||||
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam
|
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
||||||
@@ -148,6 +148,22 @@ def view_tryout(tryout_id):
|
|||||||
# Get all players (for manager registration dropdown)
|
# Get all players (for manager registration dropdown)
|
||||||
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
||||||
|
|
||||||
|
# Get matches for this tryout with participant info
|
||||||
|
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date).all()
|
||||||
|
match_data = []
|
||||||
|
for match in matches:
|
||||||
|
if match.match_type == 'team_vs_team':
|
||||||
|
participants = {
|
||||||
|
'team1': match.team1.name if match.team1 else 'TBD',
|
||||||
|
'team2': match.team2.name if match.team2 else 'TBD'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
participants = [p.player.full_name for p in match.participants.all()]
|
||||||
|
match_data.append({
|
||||||
|
'match': match,
|
||||||
|
'participants': participants
|
||||||
|
})
|
||||||
|
|
||||||
return render_template('pages/view_tryout.html',
|
return render_template('pages/view_tryout.html',
|
||||||
tryout=tryout,
|
tryout=tryout,
|
||||||
registered_players=registered_players,
|
registered_players=registered_players,
|
||||||
@@ -158,6 +174,8 @@ def view_tryout(tryout_id):
|
|||||||
team_data=team_data,
|
team_data=team_data,
|
||||||
can_edit=can_edit,
|
can_edit=can_edit,
|
||||||
all_players=all_players,
|
all_players=all_players,
|
||||||
|
matches=matches,
|
||||||
|
match_data=match_data,
|
||||||
now=datetime.utcnow())
|
now=datetime.utcnow())
|
||||||
|
|
||||||
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
|
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
|
||||||
|
|||||||
+62
-2
@@ -358,6 +358,8 @@ a:hover { color: var(--primary-dark); }
|
|||||||
.badge-upcoming { background: var(--info-light); color: var(--info); }
|
.badge-upcoming { background: var(--info-light); color: var(--info); }
|
||||||
.badge-in_progress { background: var(--warning-light); color: var(--warning); }
|
.badge-in_progress { background: var(--warning-light); color: var(--warning); }
|
||||||
.badge-completed { background: var(--success-light); color: var(--success); }
|
.badge-completed { background: var(--success-light); color: var(--success); }
|
||||||
|
.badge-scheduled { background: var(--info-light); color: var(--info); }
|
||||||
|
.badge-cancelled { background: var(--danger-light); color: var(--danger); }
|
||||||
.badge-president { background: #eef2ff; color: #4338ca; }
|
.badge-president { background: #eef2ff; color: #4338ca; }
|
||||||
.badge-manager { background: #ecfdf5; color: #059669; }
|
.badge-manager { background: #ecfdf5; color: #059669; }
|
||||||
.badge-coach { background: #fffbeb; color: #d97706; }
|
.badge-coach { background: #fffbeb; color: #d97706; }
|
||||||
@@ -888,7 +890,65 @@ a:hover { color: var(--primary-dark); }
|
|||||||
.mr-2 { margin-right: 8px; }
|
.mr-2 { margin-right: 8px; }
|
||||||
.mx-2 { margin-left: 8px; margin-right: 8px; }
|
.mx-2 { margin-left: 8px; margin-right: 8px; }
|
||||||
|
|
||||||
/* Responsive */
|
/* Calendar Styles */
|
||||||
|
#calendar {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-toolbar-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button {
|
||||||
|
background: var(--primary) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: var(--radius-sm) !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button:hover {
|
||||||
|
background: var(--primary-dark) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc-button-primary:not(:disabled):active {
|
||||||
|
background: var(--primary-dark) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day {
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day:hover {
|
||||||
|
background: var(--gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event {
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: var(--sidebar-collapsed);
|
width: var(--sidebar-collapsed);
|
||||||
@@ -910,4 +970,4 @@ a:hover { color: var(--primary-dark); }
|
|||||||
.content { padding: 16px; }
|
.content { padding: 16px; }
|
||||||
.top-bar { padding: 12px 16px; }
|
.top-bar { padding: 12px 16px; }
|
||||||
.flash-messages { padding: 0 16px; }
|
.flash-messages { padding: 0 16px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,12 @@
|
|||||||
<span>Tryouts</span>
|
<span>Tryouts</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('matches.calendar') }}" class="{% if request.endpoint and 'calendar' in request.endpoint %}active{% endif %}">
|
||||||
|
<i class="fas fa-calendar"></i>
|
||||||
|
<span>Calendar</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% if current_user.can_evaluate() %}
|
{% if current_user.can_evaluate() %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('evaluations.list_evaluations') }}" class="{% if request.endpoint and 'evaluations' in request.endpoint %}active{% endif %}">
|
<a href="{{ url_for('evaluations.list_evaluations') }}" class="{% if request.endpoint and 'evaluations' in request.endpoint %}active{% endif %}">
|
||||||
@@ -130,5 +136,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Calendar - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Calendar{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / Calendar</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-calendar-alt"></i> Schedule</h3>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline" onclick="changeView('dayGridMonth')">
|
||||||
|
<i class="fas fa-calendar"></i> Month
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline" onclick="changeView('timeGridWeek')">
|
||||||
|
<i class="fas fa-calendar-week"></i> Week
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline" onclick="changeView('timeGridDay')">
|
||||||
|
<i class="fas fa-calendar-day"></i> Day
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline" onclick="changeView('listMonth')">
|
||||||
|
<i class="fas fa-list"></i> List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Details Modal -->
|
||||||
|
<div id="eventModal" class="modal hidden">
|
||||||
|
<div class="modal-backdrop" onclick="hideEventModal()"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modalTitle">Event Details</h3>
|
||||||
|
<button class="modal-close" onclick="hideEventModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="modalContent"></div>
|
||||||
|
<div id="modalActions" class="form-actions mt-3" style="display: none;">
|
||||||
|
<button class="btn btn-sm btn-danger" id="deleteMatchBtn" style="display: none;">
|
||||||
|
<i class="fas fa-trash"></i> Delete Match
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-primary" id="editMatchBtn" style="display: none;">
|
||||||
|
<i class="fas fa-edit"></i> Edit Match
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-primary" id="viewTryoutBtn" style="display: none;">
|
||||||
|
<i class="fas fa-eye"></i> View Tryout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.css" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var canScheduleMatches = {{ 'true' if current_user.can_schedule_matches() else 'false' }};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
|
||||||
|
},
|
||||||
|
events: '/matches/api/events',
|
||||||
|
eventClick: function(info) {
|
||||||
|
showEventModal(info.event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
// Store calendar in window for access
|
||||||
|
window.fcCalendar = calendar;
|
||||||
|
});
|
||||||
|
|
||||||
|
function changeView(viewName) {
|
||||||
|
if (window.fcCalendar) {
|
||||||
|
window.fcCalendar.changeView(viewName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEventModal(event) {
|
||||||
|
var props = event.extendedProps;
|
||||||
|
var title = event.title;
|
||||||
|
var type = props.type;
|
||||||
|
var date = event.start ? event.start.toDateString() : '';
|
||||||
|
|
||||||
|
var content = '<div class="detail-grid">';
|
||||||
|
content += '<div class="detail-item"><span class="detail-label">Type</span><span class="detail-value">';
|
||||||
|
content += '<span class="badge badge-' + (type === 'tryout' ? 'info' : (props.match_type === 'team_vs_team' ? 'success' : 'warning')) + '">';
|
||||||
|
content += (type === 'tryout' ? 'Tryout' : (props.match_type === 'team_vs_team' ? 'Team Match' : 'Player Scrim')) + '</span>';
|
||||||
|
content += '</span></div>';
|
||||||
|
content += '<div class="detail-item"><span class="detail-label">Title</span><span class="detail-value">' + title + '</span></div>';
|
||||||
|
content += '<div class="detail-item"><span class="detail-label">Date</span><span class="detail-value">' + date + '</span></div>';
|
||||||
|
content += '<div class="detail-item"><span class="detail-label">Location</span><span class="detail-value">' + (props.location || 'TBD') + '</span></div>';
|
||||||
|
content += '<div class="detail-item"><span class="detail-label">Status</span><span class="detail-value">';
|
||||||
|
content += '<span class="badge badge-' + (props.status || 'scheduled') + '">' + (props.status || 'scheduled') + '</span>';
|
||||||
|
content += '</span></div>';
|
||||||
|
|
||||||
|
if (props.description) {
|
||||||
|
content += '<div class="detail-item full-width"><span class="detail-label">Description</span><span class="detail-value">' + props.description + '</span></div>';
|
||||||
|
}
|
||||||
|
content += '</div>';
|
||||||
|
|
||||||
|
document.getElementById('modalTitle').textContent = type === 'tryout' ? 'Tryout Details' : 'Match Details';
|
||||||
|
document.getElementById('modalContent').innerHTML = content;
|
||||||
|
|
||||||
|
// Reset buttons
|
||||||
|
document.getElementById('deleteMatchBtn').style.display = 'none';
|
||||||
|
document.getElementById('editMatchBtn').style.display = 'none';
|
||||||
|
document.getElementById('viewTryoutBtn').style.display = 'none';
|
||||||
|
|
||||||
|
// Show action buttons for matches (coaches and above)
|
||||||
|
if (type === 'match' && canScheduleMatches) {
|
||||||
|
document.getElementById('modalActions').style.display = 'flex';
|
||||||
|
document.getElementById('editMatchBtn').style.display = 'inline-flex';
|
||||||
|
document.getElementById('editMatchBtn').onclick = function() {
|
||||||
|
window.location.href = '/matches/' + props.match_id + '/edit';
|
||||||
|
};
|
||||||
|
} else if (type === 'tryout' && canScheduleMatches) {
|
||||||
|
document.getElementById('modalActions').style.display = 'flex';
|
||||||
|
document.getElementById('viewTryoutBtn').style.display = 'inline-flex';
|
||||||
|
document.getElementById('viewTryoutBtn').onclick = function() {
|
||||||
|
window.location.href = '/tryouts/' + props.tryout_id;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
document.getElementById('modalActions').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('eventModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideEventModal() {
|
||||||
|
document.getElementById('eventModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Schedule Match - {{ tryout.title }} - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Schedule Match{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Schedule Match</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-futbol"></i> Schedule Match for {{ tryout.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="match_type">Match Type</label>
|
||||||
|
<select name="match_type" id="match_type" class="form-select" onchange="toggleMatchType()" required>
|
||||||
|
<option value="team_vs_team">Team vs Team</option>
|
||||||
|
<option value="player_scrim">Player Scrim</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Match Title</label>
|
||||||
|
<input type="text" name="title" id="title" class="form-input" placeholder="e.g., Alpha vs Bravo Scrimmage" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="date">Date</label>
|
||||||
|
<input type="date" name="date" id="date" class="form-input" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="start_time">Start Time (Optional)</label>
|
||||||
|
<input type="time" name="start_time" id="start_time" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="end_time">End Time (Optional)</label>
|
||||||
|
<input type="time" name="end_time" id="end_time" class="form-input">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location">Location</label>
|
||||||
|
<input type="text" name="location" id="location" class="form-input" placeholder="Match location">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" id="description" class="form-textarea" placeholder="Optional notes about this match"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Team vs Team Selection -->
|
||||||
|
<div id="team-vs-team-section">
|
||||||
|
<hr class="section-divider">
|
||||||
|
<h4 class="section-title"><i class="fas fa-users"></i> Select Teams</h4>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team1_id">Team 1</label>
|
||||||
|
<select name="team1_id" id="team1_id" class="form-select">
|
||||||
|
<option value="">-- Select Team 1 --</option>
|
||||||
|
{% for team in teams %}
|
||||||
|
<option value="{{ team.id }}">{{ team.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team2_id">Team 2</label>
|
||||||
|
<select name="team2_id" id="team2_id" class="form-select">
|
||||||
|
<option value="">-- Select Team 2 --</option>
|
||||||
|
{% for team in teams %}
|
||||||
|
<option value="{{ team.id }}">{{ team.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Player Scrim Selection -->
|
||||||
|
<div id="player-scrim-section" class="hidden">
|
||||||
|
<hr class="section-divider">
|
||||||
|
<h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Players</label>
|
||||||
|
<div class="checkbox-grid">
|
||||||
|
{% for player in all_players %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="player_ids" value="{{ player.id }}">
|
||||||
|
{{ player.full_name }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Schedule Match
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function toggleMatchType() {
|
||||||
|
var matchType = document.getElementById('match_type').value;
|
||||||
|
var teamSection = document.getElementById('team-vs-team-section');
|
||||||
|
var playerSection = document.getElementById('player-scrim-section');
|
||||||
|
|
||||||
|
if (matchType === 'team_vs_team') {
|
||||||
|
teamSection.classList.remove('hidden');
|
||||||
|
playerSection.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
teamSection.classList.add('hidden');
|
||||||
|
playerSection.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
toggleMatchType();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Edit Match - TryoutPro{% endblock %}
|
||||||
|
{% block page_title %}Edit Match{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.list_tryouts') }}">Tryouts</a> / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Edit Match</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-futbol"></i> Edit Match</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('matches.edit_match', match_id=match.id) }}" class="form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Match Title</label>
|
||||||
|
<input type="text" name="title" id="title" class="form-input" value="{{ match.title }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="date">Date</label>
|
||||||
|
<input type="date" name="date" id="date" class="form-input" value="{{ match.date.strftime('%Y-%m-%d') }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="status">Status</label>
|
||||||
|
<select name="status" id="status" class="form-select">
|
||||||
|
<option value="scheduled" {% if match.status == 'scheduled' %}selected{% endif %}>Scheduled</option>
|
||||||
|
<option value="completed" {% if match.status == 'completed' %}selected{% endif %}>Completed</option>
|
||||||
|
<option value="cancelled" {% if match.status == 'cancelled' %}selected{% endif %}>Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="start_time">Start Time</label>
|
||||||
|
<input type="time" name="start_time" id="start_time" class="form-input" value="{{ match.start_time.strftime('%H:%M') if match.start_time else '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="end_time">End Time</label>
|
||||||
|
<input type="time" name="end_time" id="end_time" class="form-input" value="{{ match.end_time.strftime('%H:%M') if match.end_time else '' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location">Location</label>
|
||||||
|
<input type="text" name="location" id="location" class="form-input" value="{{ match.location or '' }}" placeholder="Match location">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea name="description" id="description" class="form-textarea" placeholder="Optional notes about this match">{{ match.description or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if match.match_type == 'team_vs_team' %}
|
||||||
|
<hr class="section-divider">
|
||||||
|
<h4 class="section-title"><i class="fas fa-users"></i> Teams</h4>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team1_id">Team 1</label>
|
||||||
|
<select name="team1_id" id="team1_id" class="form-select">
|
||||||
|
<option value="">-- Select Team 1 --</option>
|
||||||
|
{% for team in teams %}
|
||||||
|
<option value="{{ team.id }}" {% if match.team1_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="team2_id">Team 2</label>
|
||||||
|
<select name="team2_id" id="team2_id" class="form-select">
|
||||||
|
<option value="">-- Select Team 2 --</option>
|
||||||
|
{% for team in teams %}
|
||||||
|
<option value="{{ team.id }}" {% if match.team2_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<hr class="section-divider">
|
||||||
|
<h4 class="section-title"><i class="fas fa-user-friends"></i> Players</h4>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Players</label>
|
||||||
|
<div class="checkbox-grid">
|
||||||
|
{% for player in all_players %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="player_ids" value="{{ player.id }}" {% if player.id in current_player_ids %}checked{% endif %}>
|
||||||
|
{{ player.full_name }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Save Changes
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,9 +6,14 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="tryout-detail">
|
<div class="tryout-detail">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Tryout Details</h3>
|
<h3>Tryout Details</h3>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
{% if can_edit %}
|
||||||
|
<a href="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="btn btn-sm btn-success">
|
||||||
|
<i class="fas fa-futbol"></i> Schedule Match
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<form method="POST" action="{{ url_for('tryouts.update_status', tryout_id=tryout.id) }}" class="inline-form">
|
<form method="POST" action="{{ url_for('tryouts.update_status', tryout_id=tryout.id) }}" class="inline-form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
@@ -226,6 +231,80 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if matches %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-futbol"></i> Scheduled Matches</h3>
|
||||||
|
{% if can_edit %}
|
||||||
|
<a href="{{ url_for('matches.create_match', tryout_id=tryout.id) }}" class="btn btn-sm btn-success">
|
||||||
|
<i class="fas fa-plus"></i> Schedule Match
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Mini Calendar -->
|
||||||
|
<div id="mini-calendar" style="min-height: 300px;"></div>
|
||||||
|
|
||||||
|
<!-- Matches Table -->
|
||||||
|
<div class="table-container mt-3">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Match</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Participants</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Status</th>
|
||||||
|
{% if can_edit %}
|
||||||
|
<th>Actions</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in match_data %}
|
||||||
|
{% set m = item.match %}
|
||||||
|
<tr>
|
||||||
|
<td class="cell-title">{{ m.title }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-{{ 'success' if m.match_type == 'team_vs_team' else 'warning' }}">
|
||||||
|
{{ 'Team vs Team' if m.match_type == 'team_vs_team' else 'Player Scrim' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ m.date.strftime('%m/%d/%Y') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if m.match_type == 'team_vs_team' %}
|
||||||
|
{{ item.participants.team1 }} vs {{ item.participants.team2 }}
|
||||||
|
{% else %}
|
||||||
|
{{ item.participants | join(', ') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if m.start_time and m.end_time %}
|
||||||
|
{{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }}
|
||||||
|
{% else %}
|
||||||
|
TBD
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-{{ m.status }}">{{ m.status }}</span>
|
||||||
|
</td>
|
||||||
|
{% if can_edit %}
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="card mt-4">
|
<div class="card mt-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
|
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
|
||||||
@@ -274,5 +353,29 @@
|
|||||||
<script>
|
<script>
|
||||||
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
||||||
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
||||||
|
|
||||||
|
// Mini calendar for matches
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var miniCalendarEl = document.getElementById('mini-calendar');
|
||||||
|
if (miniCalendarEl) {
|
||||||
|
var miniCalendar = new FullCalendar.Calendar(miniCalendarEl, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek'
|
||||||
|
},
|
||||||
|
events: '/matches/api/events/{{ tryout.id }}',
|
||||||
|
height: '300px',
|
||||||
|
eventClick: function(info) {
|
||||||
|
// Could show match details or edit link
|
||||||
|
if (info.event.extendedProps.match_id) {
|
||||||
|
window.location.href = '/matches/' + info.event.extendedProps.match_id + '/edit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
miniCalendar.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user