diff --git a/app/models/tryout/tryout.py b/app/models/tryout/tryout.py index 752ae81..cfcabde 100644 --- a/app/models/tryout/tryout.py +++ b/app/models/tryout/tryout.py @@ -11,6 +11,7 @@ class Tryout(db.Model): description = db.Column(db.Text, nullable=True) game = db.Column(db.String(50), nullable=False) date = db.Column(db.Date, nullable=False) + end_date = db.Column(db.Date, nullable=True) location = db.Column(db.String(200), nullable=True) status = db.Column(db.String(20), default='upcoming') max_players = db.Column(db.Integer, nullable=True) @@ -26,4 +27,14 @@ class Tryout(db.Model): registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic') evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic') teams = db.relationship('Team', backref='tryout', lazy='dynamic') - target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id]) \ No newline at end of file + target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id]) + + @property + def is_ended(self): + """Tryout is considered ended after its end_date passes. + Falls back to date if end_date is not set.""" + from datetime import date as date_type + today = date_type.today() + if self.end_date is not None: + return self.end_date < today + return self.date < today diff --git a/app/routes/matches.py b/app/routes/matches.py index 649183f..457cacb 100644 --- a/app/routes/matches.py +++ b/app/routes/matches.py @@ -46,19 +46,6 @@ def api_events(): 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', - 'extendedProps': { - 'location': tryout.location or 'TBD', - 'status': tryout.status, - 'description': tryout.description or '', - 'tryout_id': tryout.id, - }, - }) - for match in tryout.matches: match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b' match_desc = match.description or '' @@ -158,18 +145,7 @@ def api_events_for_tryout(tryout_id): if not can_view and not is_registered and not player_in_match: return jsonify([]) - events = [{ - 'id': f'tryout_{tryout.id}', - 'title': f'Tryout: {tryout.title}', - 'date': tryout.date.strftime('%Y-%m-%d'), - 'type': 'tryout', 'color': '#3b82f6', - 'extendedProps': { - 'location': tryout.location or 'TBD', - 'status': tryout.status, - 'description': tryout.description or '', - 'tryout_id': tryout.id, - }, - }] + events = [] for match in tryout.matches: match_color = '#10b981' if match.match_type in ('team_vs_team', 'player_vs_player') else '#f59e0b' @@ -221,6 +197,10 @@ def create_match(tryout_id): flash('You do not have permission to schedule matches for this tryout.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + if tryout.is_ended: + flash('This tryout has ended. Matches can no longer be created or modified.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + teams = Team.query.filter_by(tryout_id=tryout_id).all() registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() all_players = [User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)] @@ -348,6 +328,10 @@ def edit_match(match_id): flash('You do not have permission to edit this match.', 'danger') return redirect(url_for('matches.calendar')) + if tryout.is_ended: + flash('This tryout has ended. Matches can no longer be created or modified.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) + teams = Team.query.filter_by(tryout_id=tryout.id).all() registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all() all_players = [User.query.get(r.player_id) for r in registrations if r.player_id] @@ -503,6 +487,7 @@ def api_manageable_tryouts(): manageable.append({ 'id': t.id, 'title': t.title, 'date': t.date.strftime('%Y-%m-%d'), + 'end_date': t.end_date.strftime('%Y-%m-%d') if t.end_date else None, }) return jsonify(manageable) @@ -516,6 +501,9 @@ def delete_match(match_id): 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')) + if tryout.is_ended: + flash('This tryout has ended. Matches can no longer be deleted.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) db.session.delete(match) db.session.commit() flash('Match deleted successfully.', 'success') diff --git a/app/routes/tryouts.py b/app/routes/tryouts.py index c5f4a3a..8f0f26a 100644 --- a/app/routes/tryouts.py +++ b/app/routes/tryouts.py @@ -51,6 +51,7 @@ def create_tryout(): description = request.form.get('description') game = request.form.get('game') date_str = request.form.get('date') + end_date_str = request.form.get('end_date') location = request.form.get('location') max_players = request.form.get('max_players') target_org_team_id = request.form.get('target_org_team_id') @@ -60,12 +61,26 @@ def create_tryout(): try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash('Invalid start date format.', 'danger') return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + end_date_obj = None + if end_date_str: + try: + end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date() + if end_date_obj < date_obj: + flash('End date cannot be before start date.', 'danger') + return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + except (ValueError, TypeError): + flash('Invalid end date format.', 'danger') + return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + tryout = Tryout( title=title, description=description, game=game, date=date_obj, + end_date=end_date_obj, location=location, max_players=int(max_players) if max_players else None, created_by=current_user.id, status='upcoming', @@ -92,6 +107,10 @@ def edit_tryout(tryout_id): flash('You do not have permission to edit this tryout.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryout.is_ended: + flash('This tryout has ended and can no longer be modified.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) + org_teams = OrgTeam.query.order_by(OrgTeam.name).all() managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.full_name).all() coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all() @@ -101,6 +120,7 @@ def edit_tryout(tryout_id): description = request.form.get('description') game = request.form.get('game') date_str = request.form.get('date') + end_date_str = request.form.get('end_date') location = request.form.get('location') max_players = request.form.get('max_players') target_org_team_id = request.form.get('target_org_team_id') @@ -110,14 +130,28 @@ def edit_tryout(tryout_id): try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash('Invalid start date format.', 'danger') return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + end_date_obj = None + if end_date_str: + try: + end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date() + if end_date_obj < date_obj: + flash('End date cannot be before start date.', 'danger') + return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + except (ValueError, TypeError): + flash('Invalid end date format.', 'danger') + return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + tryout.title = title tryout.description = description tryout.game = game tryout.date = date_obj + tryout.end_date = end_date_obj tryout.location = location tryout.max_players = int(max_players) if max_players else None tryout.target_org_team_id = int(target_org_team_id) if target_org_team_id else None @@ -429,4 +463,43 @@ def add_to_team(tryout_id, team_id): db.session.add(member) db.session.commit() flash('Player added to team!', 'success') - return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) \ No newline at end of file + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + +@tryouts_bp.route('//delete', methods=['POST']) +@login_required +def delete_tryout(tryout_id): + """Delete a tryout and all associated data (matches, teams, registrations, evaluations).""" + tryout = Tryout.query.get_or_404(tryout_id) + if not current_user.can_manage_this_tryout(tryout): + flash('You do not have permission to delete this tryout.', 'danger') + return redirect(url_for('tryouts.list_tryouts')) + + # Delete match participants for all matches in this tryout + match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()] + if match_ids: + MatchParticipant.query.filter( + MatchParticipant.match_id.in_(match_ids) + ).delete(synchronize_session=False) + # Delete matches + Match.query.filter(Match.id.in_(match_ids)).delete(synchronize_session=False) + + # Delete team members for all teams in this tryout + team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()] + if team_ids: + TeamMember.query.filter( + TeamMember.team_id.in_(team_ids) + ).delete(synchronize_session=False) + # Delete teams + Team.query.filter(Team.id.in_(team_ids)).delete(synchronize_session=False) + + # Delete registrations + TryoutRegistration.query.filter_by(tryout_id=tryout_id).delete() + + # Delete evaluations + Evaluation.query.filter_by(tryout_id=tryout_id).delete() + + db.session.delete(tryout) + db.session.commit() + flash('Tryout deleted successfully.', 'success') + return redirect(url_for('tryouts.list_tryouts')) \ No newline at end of file diff --git a/app/routes/users.py b/app/routes/users.py index 8cbe3cf..4a0a38b 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -258,7 +258,15 @@ def profile(): contracts = Contract.query.filter_by( player_id=current_user.id, ).order_by(Contract.uploaded_at.desc()).all() - return render_template('pages/profile.html', user=current_user, contracts=contracts) + + existing_availability = None + if isinstance(current_user, Coach): + existing_availability = CoachAvailability.query.filter_by( + coach_id=current_user.id, + ).all() + + return render_template('pages/profile.html', user=current_user, contracts=contracts, + existing_availability=existing_availability) @users_bp.route('/profile/edit', methods=['GET', 'POST']) diff --git a/app/templates/pages/calendar.html b/app/templates/pages/calendar.html index d434cc0..b984ad0 100644 --- a/app/templates/pages/calendar.html +++ b/app/templates/pages/calendar.html @@ -197,7 +197,6 @@ function goToTeamMatch() { } function goToCreateTryout() { - // Tryout creation doesn't support pre-filling date easily, just navigate window.location.href = '/tryouts/create'; } @@ -208,8 +207,13 @@ function fetchTryoutOptions() { .then(function(data) { var sel = document.getElementById('createTryoutSelect'); sel.innerHTML = ''; + var today = new Date().toISOString().split('T')[0]; data.forEach(function(t) { - sel.innerHTML += ''; + // Only show tryouts that haven't ended + var tryoutEndDate = t.end_date || t.date; + if (tryoutEndDate >= today) { + sel.innerHTML += ''; + } }); }) .catch(function() {}); @@ -236,8 +240,8 @@ function showEventModal(event) { var content = '
'; content += '
Type'; - content += ''; - content += (type === 'tryout' ? 'Tryout' : (props.match_type === 'team_vs_team' ? 'Team Match' : (props.match_type === 'player_vs_player' ? 'Player Match' : 'Player Scrim'))) + ''; + content += ''; + content += (props.match_type === 'team_vs_team' ? 'Team Match' : (props.match_type === 'player_vs_player' ? 'Player Match' : 'Player Scrim')) + ''; content += '
'; content += '
Title' + title + '
'; content += '
Date' + date + '
'; @@ -250,7 +254,6 @@ function showEventModal(event) { if (type === 'match' && props.participants) { content += '
Teams'; if (props.match_type === 'team_vs_team') { - // For team vs team, participants is "Team1 vs Team2" var teams = props.participants.split(' vs '); if (teams.length >= 2) { content += '
'; @@ -262,8 +265,6 @@ function showEventModal(event) { content += props.participants; } } else if (props.match_type === 'player_vs_player') { - // For player vs player, we need to parse the participants - // The format is "player1, player2 vs player3, player4" var parts = props.participants.split(' vs '); if (parts.length >= 2) { content += '
'; @@ -275,7 +276,6 @@ function showEventModal(event) { content += props.participants; } } else { - // For player scrim, just show the list content += props.participants; } content += '
'; @@ -286,7 +286,7 @@ function showEventModal(event) { } content += '
'; - document.getElementById('modalTitle').textContent = type === 'tryout' ? 'Tryout Details' : 'Match Details'; + document.getElementById('modalTitle').textContent = 'Match Details'; document.getElementById('modalContent').innerHTML = content; // Reset buttons @@ -301,11 +301,11 @@ function showEventModal(event) { 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; + document.getElementById('deleteMatchBtn').style.display = 'inline-flex'; + document.getElementById('deleteMatchBtn').onclick = function() { + if (confirm('Are you sure you want to delete this match?')) { + deleteCalendarMatch(props.match_id); + } }; } else { document.getElementById('modalActions').style.display = 'none'; @@ -329,6 +329,27 @@ function showEventModal(event) { document.getElementById('eventModal').classList.remove('hidden'); } +function deleteCalendarMatch(matchId) { + fetch('/matches/' + matchId + '/delete', { + method: 'POST', + headers: { + 'X-CSRFToken': '{{ csrf_token() }}', + 'Content-Type': 'application/json' + } + }) + .then(function(r) { return r.json().catch(function() { return {}; }); }) + .then(function() { + hideEventModal(); + if (window.fcCalendar) { + window.fcCalendar.refetchEvents(); + } + }) + .catch(function(err) { + console.error('Error deleting match:', err); + alert('Failed to delete match.'); + }); +} + function toggleCalendarPresence(matchId, participantId, btn) { fetch('/matches/' + matchId + '/toggle-presence/' + participantId, { method: 'POST', diff --git a/app/templates/pages/edit_profile.html b/app/templates/pages/edit_profile.html index 5c95c29..e126814 100644 --- a/app/templates/pages/edit_profile.html +++ b/app/templates/pages/edit_profile.html @@ -104,25 +104,6 @@
- {% if user.role == 'player' %} -
-

My Disponibilities

-

Select your available time blocks for matches (5pm to 12am). Green = selected, Gray = available to select.

- -
-

Loading...

-
- -
- - -
- {% endif %} -
Cancel @@ -165,177 +146,6 @@ document.addEventListener('DOMContentLoaded', function() { // Show gamertag inputs for already selected games toggleGamertagInputs(); - - {% if user.role == 'player' %} - renderDisponibilityGrid(); - {% endif %} }); - -{% if user.role == 'player' %} -// Generate time slots from 5pm (17:00) to 12am (24:00) -var TIME_SLOTS = []; -for (var h = 17; h <= 24; h++) { - for (var m = 0; m < 60; m += 30) { - if (h === 24 && m > 0) continue; - var displayHour; - var displayAmpm; - if (h === 24) { - displayHour = 12; - displayAmpm = 'AM'; - } else if (h > 12) { - displayHour = h - 12; - displayAmpm = 'PM'; - } else { - displayHour = h; - displayAmpm = 'PM'; - } - var timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m; - var displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm; - TIME_SLOTS.push({ time: timeStr, display: displayTime }); - } -} - -var DAYS = [ - { value: 0, name: 'Monday' }, - { value: 1, name: 'Tuesday' }, - { value: 2, name: 'Wednesday' }, - { value: 3, name: 'Thursday' }, - { value: 4, name: 'Friday' }, - { value: 5, name: 'Saturday' }, - { value: 6, name: 'Sunday' } -]; - -// Store selected slots: {day: [time, time, ...]} -var selectedSlots = {}; - -function renderDisponibilityGrid() { - var grid = document.getElementById('disponibilities-grid'); - grid.innerHTML = '
Click time blocks to select your available hours
'; - - var container = document.createElement('div'); - container.className = 'disponibility-grid'; - - DAYS.forEach(function(day) { - var dayRow = document.createElement('div'); - dayRow.className = 'disponibility-day-row'; - - var dayLabel = document.createElement('div'); - dayLabel.className = 'disponibility-day-label'; - dayLabel.textContent = day.name; - dayRow.appendChild(dayLabel); - - var timeBlocks = document.createElement('div'); - timeBlocks.className = 'disponibility-time-blocks'; - - TIME_SLOTS.forEach(function(slot) { - var block = document.createElement('div'); - block.className = 'disponibility-time-block'; - block.dataset.day = day.value; - block.dataset.time = slot.time; - block.textContent = slot.display; - block.onclick = function() { - toggleSlot(day.value, slot.time, block); - }; - timeBlocks.appendChild(block); - }); - - dayRow.appendChild(timeBlocks); - container.appendChild(dayRow); - }); - - grid.appendChild(container); - loadMyDisponibilities(); -} - -function toggleSlot(day, time, element) { - if (!selectedSlots[day]) selectedSlots[day] = []; - - var index = selectedSlots[day].indexOf(time); - if (index > -1) { - selectedSlots[day].splice(index, 1); - element.classList.remove('selected'); - } else { - selectedSlots[day].push(time); - element.classList.add('selected'); - } -} - -function loadMyDisponibilities() { - fetch('{{ url_for("users.get_my_disponibilities") }}') - .then(function(response) { return response.json(); }) - .then(function(data) { - selectedSlots = {}; - - for (var day in data) { - var slots = data[day]; - slots.forEach(function(slot) { - selectedSlots[day] = selectedSlots[day] || []; - selectedSlots[day].push(slot.start_time); - }); - } - - document.querySelectorAll('.disponibility-time-block').forEach(function(block) { - var day = block.dataset.day; - var time = block.dataset.time; - if (selectedSlots[day] && selectedSlots[day].indexOf(time) > -1) { - block.classList.add('selected'); - } else { - block.classList.remove('selected'); - } - }); - }) - .catch(function(error) { - console.error('Error loading disponibilities:', error); - }); -} - -function saveDisponibilities() { - var slots = []; - for (var day in selectedSlots) { - selectedSlots[day].forEach(function(time) { - slots.push({ day_of_week: parseInt(day), start_time: time }); - }); - } - - fetch('{{ url_for("users.add_disponibilities_bulk") }}', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ slots: slots }) - }) - .then(function(response) { return response.json(); }) - .then(function(data) { - if (data.success) { - var msg = document.createElement('div'); - msg.className = 'alert alert-success'; - msg.style.marginTop = '10px'; - msg.innerHTML = ' Disponibilities saved successfully!'; - document.getElementById('disponibilities-grid').appendChild(msg); - setTimeout(function() { msg.remove(); }, 3000); - } - }) - .catch(function(error) { - console.error('Error saving disponibilities:', error); - }); -} - -function clearDisponibilities() { - if (!confirm('Are you sure you want to clear all your disponibilities?')) return; - - fetch('{{ url_for("users.clear_disponibilities") }}', { - method: 'POST' - }) - .then(function(response) { return response.json(); }) - .then(function(data) { - if (data.success) { - selectedSlots = {}; - document.querySelectorAll('.disponibility-time-block').forEach(function(block) { - block.classList.remove('selected'); - }); - } - }); -} -{% endif %} {% endblock %} \ No newline at end of file diff --git a/app/templates/pages/profile.html b/app/templates/pages/profile.html index 46f4e63..663e719 100644 --- a/app/templates/pages/profile.html +++ b/app/templates/pages/profile.html @@ -180,5 +180,363 @@
{% endif %} + + + {% if user.role == 'player' %} +
+
+

My Disponibilities

+
+
+

Select your available time blocks for matches (5pm to 12am). Green = selected, Gray = available to select.

+
+

Loading...

+
+
+ + +
+
+
+ {% endif %} + + + {% if user.role == 'coach' %} +
+
+

My Coaching Availability

+

Select time slots when you're available for One on One sessions (8am to 10pm).

+
+
+
+

Loading availability grid...

+
+
+ +
+
+
+ {% endif %} {% endblock %} + +{% block scripts %} +{% if user.role == 'coach' %} + + +{% endif %} + +{% if user.role == 'player' %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/pages/tryout_form.html b/app/templates/pages/tryout_form.html index 6401816..5ef8e34 100644 --- a/app/templates/pages/tryout_form.html +++ b/app/templates/pages/tryout_form.html @@ -34,10 +34,15 @@ {% endfor %} -
- +
+
+
+ + + Optional. Leave blank for single-day tryout. +
diff --git a/app/templates/pages/tryouts.html b/app/templates/pages/tryouts.html index bd49dde..ba0dd11 100644 --- a/app/templates/pages/tryouts.html +++ b/app/templates/pages/tryouts.html @@ -29,7 +29,13 @@
Date - {{ tryout.date.strftime('%b %d, %Y') }} + + {% if tryout.end_date and tryout.end_date != tryout.date %} + {{ tryout.date.strftime('%b %d') }} - {{ tryout.end_date.strftime('%b %d, %Y') }} + {% else %} + {{ tryout.date.strftime('%b %d, %Y') }} + {% endif %} +
diff --git a/app/templates/pages/view_tryout.html b/app/templates/pages/view_tryout.html index 565bfbd..cf7ffa5 100644 --- a/app/templates/pages/view_tryout.html +++ b/app/templates/pages/view_tryout.html @@ -9,7 +9,7 @@

Tryout Details

- {% if can_edit %} + {% if can_edit and not tryout.is_ended %} Schedule Match @@ -24,11 +24,19 @@ {% endif %} - {% if can_edit %} + {% if can_edit and not tryout.is_ended %} Edit {% endif %} + {% if can_edit %} +
+ + +
+ {% endif %}
@@ -39,7 +47,13 @@
Date - {{ tryout.date.strftime('%A, %B %d, %Y') }} + + {% if tryout.end_date and tryout.end_date != tryout.date %} + {{ tryout.date.strftime('%B %d') }} - {{ tryout.end_date.strftime('%B %d, %Y') }} + {% else %} + {{ tryout.date.strftime('%A, %B %d, %Y') }} + {% endif %} +
Location @@ -90,7 +104,7 @@
{% endif %} - {% if can_edit %} + {% if can_edit and not tryout.is_ended %}

Add Player to Tryout

@@ -176,12 +190,14 @@ {% endif %} + {% if not tryout.is_ended %}
+ {% endif %} {% endif %} @@ -201,14 +217,14 @@

Teams

- {% if can_edit %} + {% if can_edit and not tryout.is_ended %} {% endif %}
- {% if can_edit %} + {% if can_edit and not tryout.is_ended %}