ajout d'un calendrier pour sceduler des match

This commit is contained in:
cedrick2711
2026-07-14 13:35:30 -04:00
parent acb9bc3256
commit 83ee10ca64
15 changed files with 968 additions and 6 deletions
Binary file not shown.
Binary file not shown.
+2
View File
@@ -20,6 +20,7 @@ def create_app():
from routes.users import users_bp
from routes.main import main_bp
from routes.teams import teams_bp
from routes.matches import matches_bp
app.register_blueprint(auth_bp)
app.register_blueprint(tryouts_bp)
@@ -27,6 +28,7 @@ def create_app():
app.register_blueprint(users_bp)
app.register_blueprint(main_bp)
app.register_blueprint(teams_bp)
app.register_blueprint(matches_bp)
with app.app_context():
import models
Binary file not shown.
+46 -1
View File
@@ -60,10 +60,13 @@ class User(UserMixin, db.Model):
return self.role == 'president'
def can_manage_tryouts(self):
return self.role in ['president', 'manager']
return self.role in ['president', 'manager', 'coach', 'scout']
def can_manage_teams(self):
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):
"""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)
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.
+340
View File
@@ -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
View File
@@ -1,7 +1,7 @@
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 User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant
from datetime import datetime
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
@@ -148,6 +148,22 @@ def view_tryout(tryout_id):
# Get all players (for manager registration dropdown)
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',
tryout=tryout,
registered_players=registered_players,
@@ -158,6 +174,8 @@ def view_tryout(tryout_id):
team_data=team_data,
can_edit=can_edit,
all_players=all_players,
matches=matches,
match_data=match_data,
now=datetime.utcnow())
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
+62 -2
View File
@@ -358,6 +358,8 @@ a:hover { color: var(--primary-dark); }
.badge-upcoming { background: var(--info-light); color: var(--info); }
.badge-in_progress { background: var(--warning-light); color: var(--warning); }
.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-manager { background: #ecfdf5; color: #059669; }
.badge-coach { background: #fffbeb; color: #d97706; }
@@ -888,7 +890,65 @@ a:hover { color: var(--primary-dark); }
.mr-2 { 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) {
.sidebar {
width: var(--sidebar-collapsed);
@@ -910,4 +970,4 @@ a:hover { color: var(--primary-dark); }
.content { padding: 16px; }
.top-bar { padding: 12px 16px; }
.flash-messages { padding: 0 16px; }
}
}
+7
View File
@@ -39,6 +39,12 @@
<span>Tryouts</span>
</a>
</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() %}
<li>
<a href="{{ url_for('evaluations.list_evaluations') }}" class="{% if request.endpoint and 'evaluations' in request.endpoint %}active{% endif %}">
@@ -130,5 +136,6 @@
{% endif %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+145
View File
@@ -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()">&times;</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 %}
+134
View File
@@ -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 %}
+108
View File
@@ -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 %}
+105 -2
View File
@@ -6,9 +6,14 @@
{% block content %}
<div class="tryout-detail">
<div class="card mb-4">
<div class="card-header">
<div class="card-header">
<h3>Tryout Details</h3>
<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 %}
<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() }}"/>
@@ -226,6 +231,80 @@
</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-header">
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
@@ -274,5 +353,29 @@
<script>
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('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>
{% endblock %}
{% endblock %}