1146 lines
44 KiB
HTML
1146 lines
44 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% block title %}{% if match %}Edit Match{% else %}Schedule Match{% endif %} - {{ tryout.title }} - UdeS team manager{% endblock %}
|
|
{% block page_title %}{% if match %}Edit Match{% else %}Schedule Match{% endif %}{% 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>
|
|
{% if match %}
|
|
/ Edit Match
|
|
{% else %}
|
|
/ Schedule Match
|
|
{% endif %}
|
|
</span>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-futbol"></i> {% if match %}Edit Match{% elif is_practice %}Schedule Practice — {{ team.name }}{% else %}Schedule Match for {{ tryout.title }}{% endif %}</h3>
|
|
{% if match %}
|
|
<p class="text-muted small">Match Type: <strong>{{ match.match_type.replace('_', ' ') | title }}</strong></p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{% if match %}{{ url_for('matches.edit_match', match_id=match.id) }}{% elif is_practice %}{{ url_for('team_matches.create_match', team_id=team_id) }}{% else %}{{ url_for('matches.create_match', tryout_id=tryout.id) }}{% endif %}" class="form" id="matchForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
{% if is_practice %}
|
|
<input type="hidden" name="title" value="Practice">
|
|
{% endif %}
|
|
|
|
{% if not is_practice %}
|
|
{% if not match %}
|
|
<div class="form-group">
|
|
<label for="match_type">{{ _('Match Type') }}</label>
|
|
<select name="match_type" id="match_type" class="form-select" data-change="toggle-match-type" required>
|
|
<option value="team_vs_team">{{ _('Team vs Team') }}</option>
|
|
<option value="player_vs_player">{{ _('Player vs Player') }}</option>
|
|
<option value="player_scrim">{{ _('Player Scrim') }}</option>
|
|
</select>
|
|
</div>
|
|
{% else %}
|
|
<input type="hidden" name="match_type" value="{{ match.match_type }}">
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if not is_practice %}
|
|
<div class="form-group">
|
|
<label for="title">{{ _('Match Title') }}</label>
|
|
<input type="text" name="title" id="title" class="form-input" value="{{ match.title if match else '' }}" placeholder="{{ _('e.g., Alpha vs Bravo Scrimmage') }}" required>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<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') if match else (prefill_date if is_practice else tryout.date.strftime('%Y-%m-%d')) }}" required>
|
|
</div>
|
|
{% if match %}
|
|
<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>
|
|
{% endif %}
|
|
<div class="form-group">
|
|
<label for="location">{{ _('Location') }}</label>
|
|
<input type="text" name="location" id="location" class="form-input" value="{{ match.location or '' if match else '' }}" placeholder="{{ _('Match location') }}">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Merged Availability Grid -->
|
|
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
|
<hr class="section-divider">
|
|
<h4 class="section-title"><i class="fas fa-clock"></i> {{ _('Select Match Time') }}</h4>
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
|
<p class="text-muted small" style="margin:0;">{{ _('Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.') }}</p>
|
|
<button type="button" class="btn btn-sm btn-outline" data-action="clear-time-selection" title="{{ _('Reset time selection') }}">
|
|
<i class="fas fa-undo"></i> {{ _('Reset Time') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div id="merged-disponibility-grid" class="merged-disponibility-grid">
|
|
<p class="text-muted">{{ _('Loading...') }}</p>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="start_time">Start Time <span class="text-muted">(Required)</span></label>
|
|
<input type="time" name="start_time" id="start_time" class="form-input" value="{{ match.start_time.strftime('%H:%M') if match and match.start_time else '' }}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="end_time">End Time <span class="text-muted">(Set by clicking consecutive slots)</span></label>
|
|
<input type="time" name="end_time" id="end_time" class="form-input" value="{{ match.end_time.strftime('%H:%M') if match and match.end_time else '' }}" required>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<!-- Fallback time inputs for users without disponibility access -->
|
|
<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 and 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 and match.end_time else '' }}">
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="form-group">
|
|
<label for="description">{{ _('Description') }}</label>
|
|
<textarea name="description" id="description" class="form-textarea" placeholder="{{ _('Optional notes about this match') }}">{% if match %}{{ match.description or '' }}{% endif %}</textarea>
|
|
</div>
|
|
|
|
<!-- Team vs Team Selection -->
|
|
<div id="team-vs-team-section" {% if match and match.match_type != 'team_vs_team' %}class="hidden"{% endif %}>
|
|
<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 }}" {% if match and 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 and match.team2_id == team.id %}selected{% endif %}>{{ team.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{% if match and match.team1 %}
|
|
<div class="team-rosters mt-3">
|
|
<div class="team-roster">
|
|
<h5>{{ match.team1.name }}</h5>
|
|
<ul class="team-members-list">
|
|
{% for member in match.team1.members %}
|
|
<li>
|
|
{{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}
|
|
{% set pdata = participants_map.get(member.player_id) %}
|
|
{% if pdata %}
|
|
<span class="presence-badge {{ 'presence-confirmed' if pdata.attendance_confirmed else 'presence-pending' }}"
|
|
data-participant-id="{{ pdata.participant_id }}"
|
|
data-match-id="{{ match.id }}"
|
|
data-action="toggle-presence" data-match-id="{{ match.id }}" data-participant-id="{{ pdata.participant_id }}"
|
|
title="{{ _('Click to toggle presence') }}">
|
|
{{ '✅ Confirmed' if pdata.attendance_confirmed else '⏳ Pending' }}
|
|
</span>
|
|
{% endif %}
|
|
</li>
|
|
{% else %}
|
|
<li class="text-muted">{{ _('No players assigned') }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if match and match.team2 %}
|
|
<div class="team-rosters mt-3">
|
|
<div class="team-roster">
|
|
<h5>{{ match.team2.name }}</h5>
|
|
<ul class="team-members-list">
|
|
{% for member in match.team2.members %}
|
|
<li>
|
|
{{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}
|
|
{% set pdata = participants_map.get(member.player_id) %}
|
|
{% if pdata %}
|
|
<span class="presence-badge {{ 'presence-confirmed' if pdata.attendance_confirmed else 'presence-pending' }}"
|
|
data-participant-id="{{ pdata.participant_id }}"
|
|
data-match-id="{{ match.id }}"
|
|
data-action="toggle-presence" data-match-id="{{ match.id }}" data-participant-id="{{ pdata.participant_id }}"
|
|
title="{{ _('Click to toggle presence') }}">
|
|
{{ '✅ Confirmed' if pdata.attendance_confirmed else '⏳ Pending' }}
|
|
</span>
|
|
{% endif %}
|
|
</li>
|
|
{% else %}
|
|
<li class="text-muted">{{ _('No players assigned') }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Player vs Player Selection -->
|
|
<div id="player-vs-player-section" {% if match and match.match_type != 'player_vs_player' %}class="hidden"{% endif %}>
|
|
<hr class="section-divider">
|
|
<h4 class="section-title"><i class="fas fa-user-friends"></i> {{ _('Select Players') }}</h4>
|
|
|
|
<!-- Randomize Teams Section -->
|
|
<div class="randomize-section">
|
|
<div class="randomize-controls">
|
|
<label><i class="fas fa-random"></i> {{ _('Randomize Teams:') }}</label>
|
|
<input type="number" id="team1-size" class="randomize-input" min="1" value="1" data-change="update-randomize-preview">
|
|
<span>vs</span>
|
|
<span id="team2-size-preview" class="randomize-preview">0</span>
|
|
<button type="button" class="btn btn-sm randomize-btn" data-action="randomize-teams">
|
|
<i class="fas fa-random"></i> {{ _('Randomize') }}
|
|
</button>
|
|
</div>
|
|
<p class="text-muted small" id="randomize-hint">{{ _('Select players then click Randomize to split them into teams.') }}</p>
|
|
</div>
|
|
|
|
<div class="pvp-layout">
|
|
<div class="team-column">
|
|
<h5 class="team-header">{{ _('Team 1') }}</h5>
|
|
<div id="team1-selection" class="team-selection"></div>
|
|
</div>
|
|
|
|
<div class="player-pool">
|
|
<div class="player-pool-header">Available Players</div>
|
|
<div id="available-players-pool" class="available-players-pool">
|
|
<p class="text-muted small">{{ _('All registered players are shown. Click time slots to filter available players.') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="team-column">
|
|
<h5 class="team-header">{{ _('Team 2') }}</h5>
|
|
<div id="team2-selection" class="team-selection"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="hidden" name="team1_player_ids" id="team1-player-ids-input">
|
|
<input type="hidden" name="team2_player_ids" id="team2-player-ids-input">
|
|
</div>
|
|
|
|
<!-- Player Scrim Selection -->
|
|
<div id="player-scrim-section" {% if match and match.match_type != 'player_scrim' %}class="hidden"{% endif %}>
|
|
<hr class="section-divider">
|
|
<h4 class="section-title"><i class="fas fa-users"></i> {{ _('Select Players') }}</h4>
|
|
|
|
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
|
<p class="text-muted small"><i class="fas fa-info-circle"></i> {{ _('Green indicators show player availability for the match date/time') }}</p>
|
|
{% endif %}
|
|
|
|
<div class="checkbox-grid" id="scrim-players-list">
|
|
{% for player in all_players %}
|
|
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
|
<input type="checkbox" name="player_ids" value="{{ player.id }}" {% if match and player.id in current_player_ids %}checked{% endif %}>
|
|
{{ player.username }}
|
|
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save"></i> {% if match %}Save Changes{% else %}Schedule Match{% endif %}
|
|
</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 %}
|
|
<style>
|
|
.pvp-layout {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
.team-column {
|
|
flex: 1;
|
|
min-width: 150px;
|
|
}
|
|
.team-header {
|
|
text-align: center;
|
|
padding: 10px;
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.team-selection {
|
|
min-height: 200px;
|
|
border: 2px dashed var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
}
|
|
.team-selection .player-item {
|
|
padding: 8px 12px;
|
|
margin: 5px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
.team-selection .player-item:hover {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
.player-pool {
|
|
flex: 2;
|
|
min-width: 200px;
|
|
}
|
|
.player-pool-header {
|
|
text-align: center;
|
|
padding: 10px;
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
font-weight: bold;
|
|
}
|
|
.available-players-pool {
|
|
min-height: 200px;
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
.available-players-pool .player-item {
|
|
padding: 8px 12px;
|
|
margin: 5px 0;
|
|
background: var(--success-light);
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
.available-players-pool .player-item:hover {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
.available-players-pool .player-item.available {
|
|
background: var(--success-light);
|
|
border-color: var(--success);
|
|
}
|
|
.available-players-pool .player-item.unavailable {
|
|
background: var(--gray-100);
|
|
border-color: var(--gray-300);
|
|
opacity: 0.6;
|
|
}
|
|
.player-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.player-item .remove-btn {
|
|
opacity: 0.5;
|
|
cursor: pointer;
|
|
}
|
|
.player-item .remove-btn:hover {
|
|
opacity: 1;
|
|
}
|
|
.player-actions {
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
.player-actions .btn {
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
}
|
|
.player-name {
|
|
flex: 1;
|
|
}
|
|
.team-selection .player-actions {
|
|
display: none;
|
|
}
|
|
.team-selection .player-item:hover .remove-btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
.randomize-section {
|
|
margin: 15px 0;
|
|
padding: 10px;
|
|
background: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
}
|
|
.randomize-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.randomize-controls label {
|
|
margin: 0;
|
|
font-weight: 500;
|
|
}
|
|
.randomize-input {
|
|
width: 60px;
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
}
|
|
.randomize-preview {
|
|
font-weight: bold;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
}
|
|
.randomize-btn {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
.randomize-btn:hover {
|
|
background: var(--primary-dark);
|
|
}
|
|
|
|
/* Presence badge styles */
|
|
.presence-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
margin-left: 8px;
|
|
transition: all 0.2s ease;
|
|
user-select: none;
|
|
}
|
|
.presence-badge:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
}
|
|
.presence-confirmed {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
.presence-pending {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
border: 1px solid #ffeeba;
|
|
}
|
|
.team-members-list li {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
</style>
|
|
<script nonce="{{ csp_nonce }}">
|
|
// Player data as simple JS object (id -> full_name)
|
|
var playerDataById = {
|
|
player_data: {
|
|
{%- for p in all_players %}
|
|
{{ p.id }}: "{{ p.username | escape }}",
|
|
{%- endfor %}
|
|
}
|
|
};
|
|
|
|
// All registered player IDs
|
|
var allRegisteredPlayers = [
|
|
{%- for p in all_players %}
|
|
{{ p.id }},
|
|
{%- endfor %}
|
|
];
|
|
|
|
{% if match %}
|
|
// Current team assignments from server
|
|
var initialTeam1Ids = {{ team1_player_ids|tojson }};
|
|
var initialTeam2Ids = {{ team2_player_ids|tojson }};
|
|
{% endif %}
|
|
|
|
// Time slots from 12pm (12:00) to 12am (24:00)
|
|
var TIME_SLOTS = [];
|
|
for (var h = 12; 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' }
|
|
];
|
|
|
|
var selectedDate = '';
|
|
var selectedSlots = [];
|
|
var allDisponibilities = {};
|
|
var canViewDisponibilities = {% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}true{% else %}false{% endif %};
|
|
var totalPlayers = {{ all_players|length }};
|
|
var availablePlayersForSlots = []; // Players available in ALL selected slots
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
|
fetchDisponibilities();
|
|
{% endif %}
|
|
|
|
document.getElementById('date').addEventListener('change', function() {
|
|
selectedDate = this.value;
|
|
if (allDisponibilitiesInitialized()) {
|
|
renderMergedDisponibilityGrid();
|
|
{% if match %}
|
|
updatePlayerPool();
|
|
{% endif %}
|
|
}
|
|
});
|
|
|
|
selectedDate = document.getElementById('date').value;
|
|
{% if match %}
|
|
selectedStartTime = document.getElementById('start_time').value;
|
|
selectedEndTime = document.getElementById('end_time').value;
|
|
|
|
// Initialize selected slots from current match time
|
|
if (selectedStartTime) {
|
|
var startIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedStartTime; });
|
|
var endIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedEndTime; });
|
|
|
|
if (startIndex !== -1) {
|
|
// Calculate which slots are covered (each slot is 30 min, end time is 30 min after last slot)
|
|
if (endIndex === -1 || endIndex <= startIndex) {
|
|
selectedSlots = [selectedStartTime];
|
|
} else {
|
|
selectedSlots = [];
|
|
for (var i = startIndex; i <= endIndex; i++) {
|
|
selectedSlots.push(TIME_SLOTS[i].time);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Initialize team assignments for player_vs_player matches
|
|
// Populate Team 1
|
|
initialTeam1Ids.forEach(function(pid) {
|
|
var teamDiv = document.getElementById('team1-selection');
|
|
var playerName = playerDataById.player_data[pid];
|
|
if (playerName) {
|
|
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
|
html += playerName;
|
|
html += '<span class="remove-btn">↺</span>';
|
|
html += '</div>';
|
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
});
|
|
|
|
// Populate Team 2
|
|
initialTeam2Ids.forEach(function(pid) {
|
|
var teamDiv = document.getElementById('team2-selection');
|
|
var playerName = playerDataById.player_data[pid];
|
|
if (playerName) {
|
|
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
|
html += playerName;
|
|
html += '<span class="remove-btn">↺</span>';
|
|
html += '</div>';
|
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
});
|
|
|
|
updateHiddenInputs();
|
|
updateRandomizePreview();
|
|
{% endif %}
|
|
|
|
{% if not match %}
|
|
{% if is_practice %}
|
|
// Practices default to player_scrim mode
|
|
document.getElementById('player-scrim-section').classList.remove('hidden');
|
|
document.getElementById('team-vs-team-section').classList.add('hidden');
|
|
document.getElementById('player-vs-player-section').classList.add('hidden');
|
|
{% else %}
|
|
toggleMatchType();
|
|
// Show all players initially
|
|
updatePlayerPool();
|
|
{% endif %}
|
|
{% endif %}
|
|
});
|
|
|
|
function allDisponibilitiesInitialized() {
|
|
return Object.keys(allDisponibilities).length > 0;
|
|
}
|
|
|
|
function fetchDisponibilities() {
|
|
if (!canViewDisponibilities) return;
|
|
fetch('{{ url_for("users.get_disponibilities") }}')
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
allDisponibilities = data;
|
|
renderMergedDisponibilityGrid();
|
|
{% if match %}
|
|
updatePlayerPool();
|
|
{% endif %}
|
|
})
|
|
.catch(function(error) {
|
|
console.error('Error fetching disponibilities:', error);
|
|
});
|
|
}
|
|
|
|
function renderMergedDisponibilityGrid() {
|
|
var grid = document.getElementById('merged-disponibility-grid');
|
|
if (!grid || !canViewDisponibilities) return;
|
|
|
|
var dateParts = selectedDate.split('-');
|
|
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
|
var jsDay = dateObj.getDay();
|
|
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
|
|
|
var html = '<div style="margin-bottom: 10px;"><strong>{{ _('Click time slots consecutively to set match duration') }}</strong></div>';
|
|
|
|
DAYS.forEach(function(day) {
|
|
if (day.value !== dayOfWeek) return;
|
|
|
|
var dayRow = '<div class="merged-disponibility-day-row">';
|
|
dayRow += '<div class="merged-disponibility-day-label">' + day.name + '</div>';
|
|
dayRow += '<div class="merged-disponibility-time-blocks">';
|
|
|
|
// First pass: collect all counts to determine max
|
|
var slotCounts = [];
|
|
TIME_SLOTS.forEach(function(slot) {
|
|
var count = getAvailabilityCount(day.value, slot.time);
|
|
slotCounts.push({ time: slot.time, display: slot.display, count: count });
|
|
});
|
|
|
|
// Find max count to use as reference
|
|
var maxCount = 0;
|
|
slotCounts.forEach(function(s) { if (s.count > maxCount) maxCount = s.count; });
|
|
|
|
slotCounts.forEach(function(slotData) {
|
|
var count = slotData.count;
|
|
var cssClass = 'merged-disponibility-time-block';
|
|
|
|
// Only color slots with 2+ available players. Green = top count, yellow = medium, no class for low
|
|
if (count >= 2) {
|
|
if (maxCount > 0 && count === maxCount) {
|
|
cssClass += ' high-availability';
|
|
} else if (count >= Math.ceil(maxCount * 0.5)) {
|
|
cssClass += ' medium-availability';
|
|
}
|
|
}
|
|
// Slots with 0-1 players get no special color class (neutral)
|
|
|
|
if (selectedSlots.includes(slotData.time)) {
|
|
cssClass += ' selected';
|
|
}
|
|
|
|
dayRow += '<div class="' + cssClass + '" data-day="' + day.value + '" data-time="' + slotData.time + '" ' +
|
|
'data-action="toggle-time-slot">' +
|
|
slotData.display +
|
|
'<span class="merged-disponibility-count">' + count + '</span>' +
|
|
'</div>';
|
|
});
|
|
|
|
dayRow += '</div></div>';
|
|
html += dayRow;
|
|
});
|
|
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
function getAvailabilityCount(dayOfWeek, timeStr) {
|
|
var count = 0;
|
|
var timeParts = timeStr.split(':');
|
|
var minutes = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
|
|
|
|
for (var playerId in allDisponibilities) {
|
|
// Only count players registered for THIS tryout
|
|
if (!allRegisteredPlayers.includes(parseInt(playerId))) continue;
|
|
|
|
var playerData = allDisponibilities[playerId];
|
|
if (playerData && playerData.disponibilities) {
|
|
var isAvailable = playerData.disponibilities.some(function(d) {
|
|
if (d.day_of_week !== dayOfWeek) return false;
|
|
var startParts = d.start_time.split(':');
|
|
var endParts = d.end_time.split(':');
|
|
var dispMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
|
|
var endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
|
|
|
|
return minutes >= dispMinutes && minutes < endMinutes;
|
|
});
|
|
if (isAvailable) count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function getSlotsForPlayer(playerId, dayOfWeek) {
|
|
var playerData = allDisponibilities[playerId];
|
|
if (!playerData || !playerData.disponibilities) return [];
|
|
|
|
var availableSlots = [];
|
|
TIME_SLOTS.forEach(function(slot) {
|
|
var timeParts = slot.time.split(':');
|
|
var minutes = parseInt(timeParts[0]) * 60 + parseInt(timeParts[1]);
|
|
|
|
var isAvailable = playerData.disponibilities.some(function(d) {
|
|
if (d.day_of_week !== dayOfWeek) return false;
|
|
var startParts = d.start_time.split(':');
|
|
var endParts = d.end_time.split(':');
|
|
var dispMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
|
|
var endMinutes = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
|
|
|
|
return minutes >= dispMinutes && minutes < endMinutes;
|
|
});
|
|
|
|
if (isAvailable) {
|
|
availableSlots.push(slot.time);
|
|
}
|
|
});
|
|
|
|
return availableSlots;
|
|
}
|
|
|
|
function toggleTimeSlot(dayOfWeek, timeStr, element) {
|
|
var slotIndex = TIME_SLOTS.findIndex(function(s) { return s.time === timeStr; });
|
|
|
|
if (selectedSlots.length === 0) {
|
|
selectedSlots = [timeStr];
|
|
} else {
|
|
var firstSelectedIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedSlots[0]; });
|
|
|
|
if (slotIndex === firstSelectedIndex) {
|
|
selectedSlots = [timeStr];
|
|
} else if (slotIndex < firstSelectedIndex) {
|
|
var newSlots = [];
|
|
for (var i = slotIndex; i <= firstSelectedIndex; i++) {
|
|
newSlots.push(TIME_SLOTS[i].time);
|
|
}
|
|
selectedSlots = newSlots;
|
|
} else if (slotIndex > firstSelectedIndex) {
|
|
var newSlots = [];
|
|
for (var i = firstSelectedIndex; i <= slotIndex; i++) {
|
|
newSlots.push(TIME_SLOTS[i].time);
|
|
}
|
|
selectedSlots = newSlots;
|
|
} else {
|
|
selectedSlots = selectedSlots.filter(function(t) { return t !== timeStr; });
|
|
}
|
|
}
|
|
|
|
var startTime = selectedSlots[0] || '';
|
|
var endTime = selectedSlots[selectedSlots.length - 1] || '';
|
|
|
|
if (endTime) {
|
|
var timeParts = endTime.split(':');
|
|
var endHour = parseInt(timeParts[0]);
|
|
var endMin = parseInt(timeParts[1]) + 30;
|
|
if (endMin >= 60) {
|
|
endMin = 0;
|
|
endHour++;
|
|
}
|
|
endTime = (endHour < 10 ? '0' : '') + endHour + ':' + (endMin < 10 ? '0' : '') + endMin;
|
|
}
|
|
|
|
document.getElementById('start_time').value = startTime;
|
|
document.getElementById('end_time').value = endTime;
|
|
|
|
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
|
|
el.classList.remove('selected');
|
|
});
|
|
selectedSlots.forEach(function(slot) {
|
|
var selectedEl = document.querySelector('.merged-disponibility-time-block[data-time="' + slot + '"]');
|
|
if (selectedEl) selectedEl.classList.add('selected');
|
|
});
|
|
|
|
if (selectedSlots.length > 0) {
|
|
{% if match %}
|
|
fetchAvailablePlayersForSlots();
|
|
{% else %}
|
|
fetchAvailablePlayersForAllSlots();
|
|
{% endif %}
|
|
} else {
|
|
availablePlayersForSlots = [];
|
|
{% if match %}
|
|
updatePlayerPool();
|
|
{% endif %}
|
|
}
|
|
}
|
|
|
|
function formatTimeDisplay(timeStr) {
|
|
var parts = timeStr.split(':');
|
|
var h = parseInt(parts[0]);
|
|
var m = parts[1];
|
|
var displayHour;
|
|
var ampm;
|
|
if (h === 0) {
|
|
displayHour = 12;
|
|
ampm = 'AM';
|
|
} else if (h < 12) {
|
|
displayHour = h;
|
|
ampm = 'AM';
|
|
} else if (h === 12) {
|
|
displayHour = 12;
|
|
ampm = 'PM';
|
|
} else {
|
|
displayHour = h - 12;
|
|
ampm = 'PM';
|
|
}
|
|
return displayHour + ':' + m + ' ' + ampm;
|
|
}
|
|
|
|
function fetchAvailablePlayersForAllSlots() {
|
|
if (!selectedDate || selectedSlots.length === 0) {
|
|
availablePlayersForSlots = [];
|
|
updatePlayerPool();
|
|
return;
|
|
}
|
|
|
|
var dateParts = selectedDate.split('-');
|
|
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
|
var jsDay = dateObj.getDay();
|
|
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
|
|
|
// Find players available in ALL selected slots
|
|
availablePlayersForSlots = [];
|
|
for (var playerId in allDisponibilities) {
|
|
if (!allRegisteredPlayers.includes(parseInt(playerId))) continue;
|
|
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
|
var isAvailableInAll = selectedSlots.every(function(slot) {
|
|
return playerSlots.includes(slot);
|
|
});
|
|
if (isAvailableInAll) {
|
|
availablePlayersForSlots.push(parseInt(playerId));
|
|
}
|
|
}
|
|
|
|
// Preselect available players in Team 1
|
|
preselectAvailablePlayers();
|
|
}
|
|
|
|
function fetchAvailablePlayersForSlots() {
|
|
if (!selectedDate || selectedSlots.length === 0) return;
|
|
|
|
var dateParts = selectedDate.split('-');
|
|
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
|
var jsDay = dateObj.getDay();
|
|
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
|
|
|
// Find players available in ANY selected slot
|
|
var availableIds = [];
|
|
for (var playerId in allDisponibilities) {
|
|
if (!allRegisteredPlayers.includes(parseInt(playerId))) continue;
|
|
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
|
var hasSlot = selectedSlots.some(function(slot) {
|
|
return playerSlots.includes(slot);
|
|
});
|
|
if (hasSlot) {
|
|
availableIds.push(parseInt(playerId));
|
|
}
|
|
}
|
|
|
|
availablePlayersForSlots = availableIds;
|
|
// Update player pool for PvP section
|
|
updatePlayerPool();
|
|
updateRandomizePreview();
|
|
}
|
|
|
|
function preselectAvailablePlayers() {
|
|
// Just update the player pool - don't auto-assign to Team 1
|
|
// Players will be shown in the pool and can be manually assigned to either team
|
|
updatePlayerPool();
|
|
}
|
|
|
|
function updatePlayerPool() {
|
|
var pool = document.getElementById('available-players-pool');
|
|
if (!pool) return;
|
|
|
|
var team1Ids = getSelectedTeamIds(1);
|
|
var team2Ids = getSelectedTeamIds(2);
|
|
var assignedIds = [...team1Ids, ...team2Ids];
|
|
|
|
// Always show ALL registered players, not just available ones.
|
|
// Availability indicators are shown per-player, but coaches can still select any player.
|
|
|
|
if (allRegisteredPlayers.length === 0) {
|
|
pool.innerHTML = '<p class="text-muted small">{{ _('No players registered') }}</p>';
|
|
return;
|
|
}
|
|
|
|
var dateParts = selectedDate.split('-');
|
|
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
|
var jsDay = dateObj.getDay();
|
|
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
|
|
|
var html = '';
|
|
allRegisteredPlayers.forEach(function(pid) {
|
|
if (assignedIds.includes(pid)) return;
|
|
|
|
var playerName = playerDataById.player_data[pid];
|
|
if (!playerName) return;
|
|
|
|
// Check if player is available during selected time slots
|
|
var isAvailable = true;
|
|
if (selectedSlots.length > 0 && allDisponibilities[pid]) {
|
|
var playerSlots = getSlotsForPlayer(pid, dayOfWeek);
|
|
isAvailable = selectedSlots.every(function(slot) {
|
|
return playerSlots.includes(slot);
|
|
});
|
|
} else if (selectedSlots.length === 0) {
|
|
// No time slots selected yet: all players count as "available" (no filter active)
|
|
isAvailable = true;
|
|
}
|
|
|
|
var availabilityClass = isAvailable ? 'available' : 'unavailable';
|
|
|
|
html += '<div class="player-item ' + availabilityClass + '" data-player-id="' + pid + '">';
|
|
html += '<span class="player-name">' + playerName + '</span>';
|
|
html += '<div class="player-actions">';
|
|
html += '<button type="button" class="btn btn-sm btn-primary" data-action="assign-team" data-team-side="1">{{ _('T1') }}</button>';
|
|
html += '<button type="button" class="btn btn-sm btn-secondary" data-action="assign-team" data-team-side="2">{{ _('T2') }}</button>';
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
|
|
pool.innerHTML = html || '<p class="text-muted small">{{ _('No players available') }}</p>';
|
|
updateRandomizePreview();
|
|
}
|
|
|
|
function assignToTeam(playerId, teamSide) {
|
|
// First, remove player from any team they're already in
|
|
document.querySelectorAll('[data-player-id="' + playerId + '"]').forEach(function(el) {
|
|
el.remove();
|
|
});
|
|
|
|
var teamDiv = document.getElementById('team' + teamSide + '-selection');
|
|
var playerName = playerDataById.player_data[playerId];
|
|
if (!playerName) return;
|
|
|
|
var html = '<div class="player-item" data-player-id="' + playerId + '" data-action="return-to-pool">';
|
|
html += playerName;
|
|
html += '<span class="remove-btn">↺</span>';
|
|
html += '</div>';
|
|
|
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
|
updateHiddenInputs();
|
|
updatePlayerPool();
|
|
}
|
|
|
|
function returnToPool(element, event) {
|
|
// The clicked element already carries data-player-id: the generated
|
|
// markup sets it, so the value no longer has to be baked into an
|
|
// onclick attribute.
|
|
if (event) event.stopPropagation();
|
|
var playerId = element.getAttribute('data-player-id');
|
|
document.querySelectorAll('[data-player-id="' + playerId + '"]').forEach(function(el) {
|
|
el.remove();
|
|
});
|
|
updateHiddenInputs();
|
|
updatePlayerPool();
|
|
}
|
|
|
|
function getSelectedTeamIds(teamSide) {
|
|
var ids = [];
|
|
document.querySelectorAll('#team' + teamSide + '-selection [data-player-id]').forEach(function(el) {
|
|
ids.push(parseInt(el.getAttribute('data-player-id')));
|
|
});
|
|
return ids;
|
|
}
|
|
|
|
function updateHiddenInputs() {
|
|
var team1Ids = getSelectedTeamIds(1);
|
|
var team2Ids = getSelectedTeamIds(2);
|
|
document.getElementById('team1-player-ids-input').value = team1Ids.join(',');
|
|
document.getElementById('team2-player-ids-input').value = team2Ids.join(',');
|
|
}
|
|
|
|
function clearTimeSelection() {
|
|
selectedSlots = [];
|
|
document.getElementById('start_time').value = '';
|
|
document.getElementById('end_time').value = '';
|
|
document.querySelectorAll('.merged-disponibility-time-block').forEach(function(el) {
|
|
el.classList.remove('selected');
|
|
});
|
|
availablePlayersForSlots = [];
|
|
// Clear both team selections
|
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
|
el.remove();
|
|
});
|
|
updateHiddenInputs();
|
|
updatePlayerPool();
|
|
}
|
|
|
|
function toggleMatchType() {
|
|
var matchType = document.getElementById('match_type').value;
|
|
var teamSection = document.getElementById('team-vs-team-section');
|
|
var playerVsPlayerSection = document.getElementById('player-vs-player-section');
|
|
var scrimSection = document.getElementById('player-scrim-section');
|
|
|
|
teamSection.classList.add('hidden');
|
|
playerVsPlayerSection.classList.add('hidden');
|
|
scrimSection.classList.add('hidden');
|
|
|
|
if (matchType === 'team_vs_team') {
|
|
teamSection.classList.remove('hidden');
|
|
} else if (matchType === 'player_vs_player') {
|
|
playerVsPlayerSection.classList.remove('hidden');
|
|
updatePlayerPool();
|
|
} else if (matchType === 'player_scrim') {
|
|
scrimSection.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function updateRandomizePreview() {
|
|
var checkedCount = document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').length;
|
|
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
|
var team2Size = checkedCount - team1Size;
|
|
|
|
if (team2Size < 0) team2Size = 0;
|
|
|
|
document.getElementById('team2-size-preview').textContent = team2Size;
|
|
}
|
|
|
|
function randomizeTeams() {
|
|
var allAssigned = [];
|
|
|
|
// Get all players currently in either team
|
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
|
allAssigned.push(parseInt(el.getAttribute('data-player-id')));
|
|
});
|
|
|
|
if (allAssigned.length === 0) {
|
|
alert('Please assign at least one player before randomizing teams.');
|
|
return;
|
|
}
|
|
|
|
// Shuffle the array using Fisher-Yates
|
|
for (var i = allAssigned.length - 1; i > 0; i--) {
|
|
var j = Math.floor(Math.random() * (i + 1));
|
|
var temp = allAssigned[i];
|
|
allAssigned[i] = allAssigned[j];
|
|
allAssigned[j] = temp;
|
|
}
|
|
|
|
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
|
var team1Players = allAssigned.slice(0, team1Size);
|
|
var team2Players = allAssigned.slice(team1Size);
|
|
|
|
// Clear both teams
|
|
document.querySelectorAll('#team1-selection [data-player-id], #team2-selection [data-player-id]').forEach(function(el) {
|
|
el.remove();
|
|
});
|
|
|
|
// Assign shuffled players to teams
|
|
team1Players.forEach(function(pid) {
|
|
var teamDiv = document.getElementById('team1-selection');
|
|
var playerName = playerDataById.player_data[pid];
|
|
if (playerName) {
|
|
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
|
html += playerName;
|
|
html += '<span class="remove-btn">↺</span>';
|
|
html += '</div>';
|
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
});
|
|
|
|
team2Players.forEach(function(pid) {
|
|
var teamDiv = document.getElementById('team2-selection');
|
|
var playerName = playerDataById.player_data[pid];
|
|
if (playerName) {
|
|
var html = '<div class="player-item" data-player-id="' + pid + '" data-action="return-to-pool">';
|
|
html += playerName;
|
|
html += '<span class="remove-btn">↺</span>';
|
|
html += '</div>';
|
|
teamDiv.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
});
|
|
|
|
updateHiddenInputs();
|
|
updateRandomizePreview();
|
|
}
|
|
|
|
// Presence toggle function
|
|
function togglePresence(matchId, participantId, badgeEl) {
|
|
fetch('/matches/' + matchId + '/toggle-presence/' + participantId, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('[name="csrf_token"]').value
|
|
}
|
|
})
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
if (data.error) {
|
|
alert('Error: ' + data.error);
|
|
return;
|
|
}
|
|
if (data.attendance_confirmed) {
|
|
badgeEl.classList.remove('presence-pending');
|
|
badgeEl.classList.add('presence-confirmed');
|
|
badgeEl.textContent = '✅ Confirmed';
|
|
} else {
|
|
badgeEl.classList.remove('presence-confirmed');
|
|
badgeEl.classList.add('presence-pending');
|
|
badgeEl.textContent = '⏳ Pending';
|
|
}
|
|
})
|
|
.catch(function(error) {
|
|
console.error('Error toggling presence:', error);
|
|
});
|
|
}
|
|
|
|
// Behaviours declared in the markup, dispatched by the delegated listener
|
|
// in main.js. Inline onclick attributes cannot be authorised by a CSP nonce.
|
|
registerActions({
|
|
'toggle-match-type': toggleMatchType,
|
|
'clear-time-selection': clearTimeSelection,
|
|
'toggle-time-slot': function (element) {
|
|
toggleTimeSlot(parseInt(element.getAttribute('data-day'), 10),
|
|
element.getAttribute('data-time'),
|
|
element);
|
|
},
|
|
'update-randomize-preview': updateRandomizePreview,
|
|
'randomize-teams': randomizeTeams,
|
|
'return-to-pool': returnToPool,
|
|
'assign-team': function (element) {
|
|
var item = element.closest('[data-player-id]');
|
|
if (item) {
|
|
assignToTeam(item.getAttribute('data-player-id'),
|
|
element.getAttribute('data-team-side'));
|
|
}
|
|
},
|
|
'toggle-presence': function (element) {
|
|
togglePresence(element.getAttribute('data-match-id'),
|
|
element.getAttribute('data-participant-id'),
|
|
element);
|
|
},
|
|
});
|
|
</script>
|
|
{% endblock %}
|