Files
team-tryouts/templates/pages/create_match.html
T
2026-07-15 22:44:15 -04:00

731 lines
27 KiB
HTML

{% 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" id="createMatchForm">
<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_vs_player">Player vs Player</option>
<option value="player_scrim">Player Scrim</option>
</select>
</div>
<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-row">
<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 class="form-group">
<label for="location">Location</label>
<input type="text" name="location" id="location" class="form-input" 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>
<p class="text-muted small">Click time slots consecutively to set match duration. Players available in all selected time blocks will be preselected.</p>
<div id="merged-disponibility-selected" class="merged-disponibility-selected-display hidden">
<span>Selected time:</span>
<span class="selected-time-badge" id="selected-time-badge"></span>
<button type="button" class="btn btn-sm btn-outline" onclick="clearTimeSelection()">Change</button>
</div>
<div id="merged-disponibility-grid" class="merged-disponibility-grid">
<p class="text-muted">Loading availability...</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" 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" 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">
</div>
<div class="form-group">
<label for="end_time">End Time</label>
<input type="time" name="end_time" id="end_time" class="form-input">
</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"></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 vs Player Selection -->
<div id="player-vs-player-section" class="hidden">
<hr class="section-divider">
<h4 class="section-title"><i class="fas fa-user-friends"></i> Select Players</h4>
<p class="text-muted small"><i class="fas fa-info-circle"></i> Click on players to assign them to Team 1 or Team 2. Players available in all selected time blocks are shown below.</p>
<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" class="hidden">
<hr class="section-divider">
<h4 class="section-title"><i class="fas fa-users"></i> Select Players</h4>
<p class="text-muted small"><i class="fas fa-info-circle"></i> Green indicators show player availability for the match date/time</p>
<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 }}">
{{ player.full_name }}
<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> 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 %}
<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(--accent-color);
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;
}
</style>
<script>
// Player data as simple JS object (id -> full_name)
var playerDataById = {
player_data: {
{%- for p in all_players %}
{{ p.id }}: "{{ p.full_name | escape }}",
{%- endfor %}
}
};
// All registered player IDs
var allRegisteredPlayers = [
{%- for p in all_players %}
{{ p.id }},
{%- endfor %}
];
// 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();
}
});
selectedDate = document.getElementById('date').value;
toggleMatchType();
// Show all players initially
updatePlayerPool();
});
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();
})
.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">';
TIME_SLOTS.forEach(function(slot) {
var count = getAvailabilityCount(day.value, slot.time);
var percentage = totalPlayers > 0 ? count / totalPlayers : 0;
var cssClass = 'merged-disponibility-time-block';
if (percentage >= 0.75) {
cssClass += ' high-availability';
} else if (percentage >= 0.5) {
cssClass += ' medium-availability';
} else {
cssClass += ' low-availability';
}
if (selectedSlots.includes(slot.time)) {
cssClass += ' selected';
}
dayRow += '<div class="' + cssClass + '" data-day="' + day.value + '" data-time="' + slot.time + '" ' +
'onclick="toggleTimeSlot(' + day.value + ', \'' + slot.time + '\', this)">' +
slot.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) {
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;
if (selectedSlots.length > 0) {
document.getElementById('merged-disponibility-selected').classList.remove('hidden');
document.getElementById('selected-time-badge').textContent =
formatTimeDisplay(selectedSlots[0]) + ' - ' + formatTimeDisplay(endTime);
} else {
document.getElementById('merged-disponibility-selected').classList.add('hidden');
}
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) {
fetchAvailablePlayersForAllSlots();
} else {
availablePlayersForSlots = [];
updatePlayerPool();
}
}
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 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];
// When time slots are selected, show only players available in all slots who aren't assigned yet
// When no time slots selected, show all registered players
var playersToShow = selectedSlots.length > 0 ? availablePlayersForSlots : allRegisteredPlayers;
if (playersToShow.length === 0) {
pool.innerHTML = '<p class="text-muted small">No players available</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 = '';
playersToShow.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);
});
}
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" onclick="assignToTeam(' + pid + ', 1)">T1</button>';
html += '<button type="button" class="btn btn-sm btn-secondary" onclick="assignToTeam(' + pid + ', 2)">T2</button>';
html += '</div>';
html += '</div>';
});
pool.innerHTML = html || '<p class="text-muted small">No players available</p>';
}
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 + '" onclick="returnToPool(' + playerId + ', event)">';
html += playerName;
html += '<span class="remove-btn">↺</span>';
html += '</div>';
teamDiv.insertAdjacentHTML('beforeend', html);
updateHiddenInputs();
updatePlayerPool();
}
function returnToPool(playerId, event) {
if (event) event.stopPropagation();
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.getElementById('merged-disponibility-selected').classList.add('hidden');
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');
}
}
</script>
{% endblock %}