ajout des disponibilités de joueurs et ajouts d'un tableau qui montre les dispo des joueurs lors de la création d'un match
This commit is contained in:
@@ -9,10 +9,10 @@
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
@@ -21,32 +21,61 @@
|
||||
</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="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 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. Available players will be auto-selected below.</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 (Optional)</label>
|
||||
<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 (Optional)</label>
|
||||
<label for="end_time">End Time</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>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
@@ -85,25 +114,43 @@
|
||||
<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" onchange="updateRandomizePreview()">
|
||||
<span>vs</span>
|
||||
<span id="team2-size-preview" class="randomize-preview">0</span>
|
||||
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
|
||||
<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>
|
||||
|
||||
<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="form-row">
|
||||
<div class="form-group">
|
||||
<label>Team 1 Players</label>
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid" id="team1-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label">
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
<input type="checkbox" name="team1_player_ids" value="{{ player.id }}" onchange="handleTeamSelection(this, 1)">
|
||||
{{ player.full_name }}
|
||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Team 2 Players</label>
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid" id="team2-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label">
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
<input type="checkbox" name="team2_player_ids" value="{{ player.id }}" onchange="handleTeamSelection(this, 2)">
|
||||
{{ player.full_name }}
|
||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -116,13 +163,16 @@
|
||||
<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="form-group">
|
||||
<label>Players</label>
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid" id="scrim-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label">
|
||||
<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>
|
||||
@@ -144,6 +194,388 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// 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' }
|
||||
];
|
||||
|
||||
var selectedDate = '';
|
||||
var selectedSlots = []; // Array of selected time slots: ['17:00', '17:30', '18:00']
|
||||
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 }};
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Only load disponibilities if user has permission
|
||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
||||
fetchDisponibilities();
|
||||
{% endif %}
|
||||
|
||||
// Set up date change listener
|
||||
document.getElementById('date').addEventListener('change', function() {
|
||||
selectedDate = this.value;
|
||||
if (allDisponibilitiesInitialized()) {
|
||||
renderMergedDisponibilityGrid();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize date
|
||||
selectedDate = document.getElementById('date').value;
|
||||
|
||||
// Initialize match type sections
|
||||
toggleMatchType();
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
// Calculate day of week from selected date
|
||||
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; // Convert to Mon=0 format
|
||||
|
||||
var html = '<div style="margin-bottom: 10px;"><strong>Click time slots consecutively to set match duration</strong></div>';
|
||||
|
||||
// Always show only the selected day (not all days)
|
||||
DAYS.forEach(function(day) {
|
||||
if (day.value !== dayOfWeek) return; // Skip days that don't match
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
// Check if this slot is in selectedSlots
|
||||
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) {
|
||||
/**Get list of available time slots for a specific player on a given day.
|
||||
* Returns array of time strings in 'HH:MM' format.
|
||||
*/
|
||||
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) {
|
||||
// First click - start new selection
|
||||
selectedSlots = [timeStr];
|
||||
} else {
|
||||
var firstSelectedIndex = TIME_SLOTS.findIndex(function(s) { return s.time === selectedSlots[0]; });
|
||||
|
||||
if (slotIndex === firstSelectedIndex) {
|
||||
// Clicking the first selected slot again - keep it as single slot
|
||||
selectedSlots = [timeStr];
|
||||
} else if (slotIndex < firstSelectedIndex) {
|
||||
// Clicking before the first slot - extend selection backward
|
||||
var newSlots = [];
|
||||
for (var i = slotIndex; i <= firstSelectedIndex; i++) {
|
||||
newSlots.push(TIME_SLOTS[i].time);
|
||||
}
|
||||
selectedSlots = newSlots;
|
||||
} else if (slotIndex > firstSelectedIndex) {
|
||||
// Clicking after the first slot - extend selection forward
|
||||
var newSlots = [];
|
||||
for (var i = firstSelectedIndex; i <= slotIndex; i++) {
|
||||
newSlots.push(TIME_SLOTS[i].time);
|
||||
}
|
||||
selectedSlots = newSlots;
|
||||
} else {
|
||||
// Already selected, toggle off
|
||||
selectedSlots = selectedSlots.filter(function(t) { return t !== timeStr; });
|
||||
}
|
||||
}
|
||||
|
||||
// Update form inputs
|
||||
var startTime = selectedSlots[0] || '';
|
||||
var endTime = selectedSlots[selectedSlots.length - 1] || '';
|
||||
|
||||
// Calculate end time as 30 min after last slot (end of that block)
|
||||
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;
|
||||
|
||||
// Update selected display
|
||||
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');
|
||||
}
|
||||
|
||||
// Update grid highlighting
|
||||
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');
|
||||
});
|
||||
|
||||
// Fetch and auto-select available players for all selected slots
|
||||
if (selectedSlots.length > 0) {
|
||||
fetchAvailablePlayersForSlots();
|
||||
}
|
||||
}
|
||||
|
||||
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 fetchAvailablePlayersForSlots() {
|
||||
if (!selectedDate || selectedSlots.length === 0) return;
|
||||
|
||||
// Get day of week
|
||||
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 at ANY of the selected slots
|
||||
var availableIds = [];
|
||||
for (var playerId in allDisponibilities) {
|
||||
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
||||
var hasSlot = selectedSlots.some(function(slot) {
|
||||
return playerSlots.includes(slot);
|
||||
});
|
||||
if (hasSlot) {
|
||||
availableIds.push(parseInt(playerId));
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select available players
|
||||
var matchType = document.getElementById('match_type').value;
|
||||
|
||||
if (matchType === 'player_vs_player') {
|
||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
||||
cb.checked = availableIds.includes(parseInt(cb.value));
|
||||
});
|
||||
} else if (matchType === 'player_scrim') {
|
||||
document.querySelectorAll('input[name="player_ids"]').forEach(function(cb) {
|
||||
cb.checked = availableIds.includes(parseInt(cb.value));
|
||||
});
|
||||
}
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
function updateRandomizePreview() {
|
||||
var checkedCount = document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="player_ids"]:checked').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 allChecked = [];
|
||||
|
||||
// Get all checked players
|
||||
document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').forEach(function(cb) {
|
||||
allChecked.push(parseInt(cb.value));
|
||||
});
|
||||
|
||||
if (allChecked.length === 0) {
|
||||
alert('Please select at least one player before randomizing teams.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Shuffle players
|
||||
for (var i = allChecked.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = allChecked[i];
|
||||
allChecked[i] = allChecked[j];
|
||||
allChecked[j] = temp;
|
||||
}
|
||||
|
||||
// Split based on team1-size input
|
||||
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
||||
var team1Players = allChecked.slice(0, team1Size);
|
||||
var team2Players = allChecked.slice(team1Size);
|
||||
|
||||
// Uncheck all first
|
||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
|
||||
// Check players in their respective teams
|
||||
team1Players.forEach(function(pid) {
|
||||
var cb = document.querySelector('input[name="team1_player_ids"][value="' + pid + '"]');
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
team2Players.forEach(function(pid) {
|
||||
var cb = document.querySelector('input[name="team2_player_ids"][value="' + pid + '"]');
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
function toggleMatchType() {
|
||||
var matchType = document.getElementById('match_type').value;
|
||||
var teamSection = document.getElementById('team-vs-team-section');
|
||||
@@ -163,25 +595,61 @@ function toggleMatchType() {
|
||||
} else if (matchType === 'player_scrim') {
|
||||
scrimSection.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update indicators when match type changes
|
||||
updateDisponibilityIndicators();
|
||||
}
|
||||
|
||||
function handleTeamSelection(checkbox, teamSide) {
|
||||
// When selecting from one team, deselect from the other team
|
||||
var otherSide = teamSide === 1 ? 2 : 1;
|
||||
var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]');
|
||||
var playerId = checkbox.value;
|
||||
|
||||
// Find the corresponding checkbox in the other team and uncheck it
|
||||
otherCheckboxes.forEach(function(other) {
|
||||
if (other.value === playerId) {
|
||||
other.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleMatchType();
|
||||
});
|
||||
function updateDisponibilityIndicators() {
|
||||
if (!canViewDisponibilities) return;
|
||||
|
||||
var dateInput = document.getElementById('date').value;
|
||||
var startTimeInput = document.getElementById('start_time').value;
|
||||
|
||||
if (!dateInput || !startTimeInput) return;
|
||||
|
||||
var dateParts = dateInput.split('-');
|
||||
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
||||
var jsDay = dateObj.getDay();
|
||||
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
||||
|
||||
// Find the first selected start slot
|
||||
var slotIndex = TIME_SLOTS.findIndex(function(s) { return s.time === startTimeInput; });
|
||||
|
||||
document.querySelectorAll('.disponibility-indicator').forEach(function(indicator) {
|
||||
var playerId = indicator.getAttribute('data-player-id');
|
||||
var playerData = allDisponibilities[playerId];
|
||||
|
||||
if (playerData && playerData.disponibilities) {
|
||||
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
||||
var isAvailable = selectedSlots.some(function(slot) {
|
||||
return playerSlots.includes(slot);
|
||||
});
|
||||
|
||||
indicator.style.display = 'inline-block';
|
||||
indicator.style.width = '12px';
|
||||
indicator.style.height = '12px';
|
||||
indicator.style.borderRadius = '50%';
|
||||
indicator.style.marginLeft = '8px';
|
||||
indicator.style.backgroundColor = isAvailable ? '#10b981' : '#9ca3af';
|
||||
} else {
|
||||
indicator.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
+482
-13
@@ -9,7 +9,7 @@
|
||||
<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">
|
||||
<form method="POST" action="{{ url_for('matches.edit_match', match_id=match.id) }}" class="form" id="editMatchForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -32,6 +32,34 @@
|
||||
</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. Available players will be auto-selected below.</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...</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.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.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>
|
||||
@@ -42,6 +70,7 @@
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="location">Location</label>
|
||||
@@ -53,7 +82,7 @@
|
||||
<textarea name="description" id="description" class="form-textarea" placeholder="Optional notes about this match">{{ match.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Team vs Team Selection -->
|
||||
<!-- Team vs Team Selection -->
|
||||
{% if match.match_type == 'team_vs_team' %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-users"></i> Teams</h4>
|
||||
@@ -86,7 +115,7 @@
|
||||
<h5>{{ match.team1.name }} Players</h5>
|
||||
<ul class="team-members-list">
|
||||
{% for member in match.team1.members %}
|
||||
<li>{{ member.player.full_name }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
||||
<li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
||||
{% else %}
|
||||
<li class="text-muted">No players assigned</li>
|
||||
{% endfor %}
|
||||
@@ -98,7 +127,7 @@
|
||||
<h5>{{ match.team2.name }} Players</h5>
|
||||
<ul class="team-members-list">
|
||||
{% for member in match.team2.members %}
|
||||
<li>{{ member.player.full_name }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
||||
<li>{{ member.player.full_name if member.player else 'Unknown Player' }}{% if member.position %} <span class="position-tag">{{ member.position }}</span>{% endif %}</li>
|
||||
{% else %}
|
||||
<li class="text-muted">No players assigned</li>
|
||||
{% endfor %}
|
||||
@@ -112,25 +141,45 @@
|
||||
<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" onchange="updateRandomizePreview()">
|
||||
<span>vs</span>
|
||||
<span id="team2-size-preview" class="randomize-preview">0</span>
|
||||
<button type="button" class="btn btn-sm randomize-btn" onclick="randomizeTeams()">
|
||||
<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>
|
||||
|
||||
{% 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="form-row">
|
||||
<div class="form-group">
|
||||
<label>Team 1 Players</label>
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid" id="team1-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label">
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
<input type="checkbox" name="team1_player_ids" value="{{ player.id }}" {% if player.id in team1_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 1)">
|
||||
{{ player.full_name }}
|
||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Team 2 Players</label>
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid" id="team2-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label">
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
<input type="checkbox" name="team2_player_ids" value="{{ player.id }}" {% if player.id in team2_player_ids %}checked{% endif %} onchange="handleTeamSelection(this, 2)">
|
||||
{{ player.full_name }}
|
||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -142,13 +191,18 @@
|
||||
<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="form-group">
|
||||
<label>Players</label>
|
||||
<div class="checkbox-grid">
|
||||
<div class="checkbox-grid" id="scrim-players-list">
|
||||
{% for player in all_players %}
|
||||
<label class="checkbox-label">
|
||||
<label class="checkbox-label player-checkbox" data-player-id="{{ player.id }}">
|
||||
<input type="checkbox" name="player_ids" value="{{ player.id }}" {% if player.id in current_player_ids %}checked{% endif %}>
|
||||
{{ player.full_name }}
|
||||
<span class="disponibility-indicator" data-player-id="{{ player.id }}"></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -170,18 +224,433 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// 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' }
|
||||
];
|
||||
|
||||
var selectedDate = '';
|
||||
var selectedSlots = []; // Array of selected time slots: ['17:00', '17:30', '18:00']
|
||||
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 }};
|
||||
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('merged-disponibility-selected').classList.remove('hidden');
|
||||
document.getElementById('selected-time-badge').textContent =
|
||||
formatTimeDisplay(selectedStartTime) + ' - ' + formatTimeDisplay(selectedEndTime);
|
||||
}
|
||||
}
|
||||
|
||||
renderMergedDisponibilityGrid();
|
||||
updateDisponibilityIndicators();
|
||||
updateRandomizePreview();
|
||||
});
|
||||
|
||||
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) {
|
||||
fetchAvailablePlayersForSlots();
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
var availableIds = [];
|
||||
for (var playerId in allDisponibilities) {
|
||||
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
||||
var hasSlot = selectedSlots.some(function(slot) {
|
||||
return playerSlots.includes(slot);
|
||||
});
|
||||
if (hasSlot) {
|
||||
availableIds.push(parseInt(playerId));
|
||||
}
|
||||
}
|
||||
|
||||
var matchType = '{{ match.match_type }}';
|
||||
|
||||
if (matchType === 'player_vs_player') {
|
||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
||||
cb.checked = availableIds.includes(parseInt(cb.value));
|
||||
});
|
||||
} else if (matchType === 'player_scrim') {
|
||||
document.querySelectorAll('input[name="player_ids"]').forEach(function(cb) {
|
||||
cb.checked = availableIds.includes(parseInt(cb.value));
|
||||
});
|
||||
}
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
function updateRandomizePreview() {
|
||||
var checkedCount = document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="player_ids"]:checked').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 allChecked = [];
|
||||
|
||||
document.querySelectorAll('input[name="team1_player_ids"]:checked, input[name="team2_player_ids"]:checked').forEach(function(cb) {
|
||||
allChecked.push(parseInt(cb.value));
|
||||
});
|
||||
|
||||
if (allChecked.length === 0) {
|
||||
alert('Please select at least one player before randomizing teams.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = allChecked.length - 1; i > 0; i--) {
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = allChecked[i];
|
||||
allChecked[i] = allChecked[j];
|
||||
allChecked[j] = temp;
|
||||
}
|
||||
|
||||
var team1Size = parseInt(document.getElementById('team1-size').value) || 1;
|
||||
var team1Players = allChecked.slice(0, team1Size);
|
||||
var team2Players = allChecked.slice(team1Size);
|
||||
|
||||
document.querySelectorAll('input[name="team1_player_ids"], input[name="team2_player_ids"]').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
|
||||
team1Players.forEach(function(pid) {
|
||||
var cb = document.querySelector('input[name="team1_player_ids"][value="' + pid + '"]');
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
team2Players.forEach(function(pid) {
|
||||
var cb = document.querySelector('input[name="team2_player_ids"][value="' + pid + '"]');
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
function handleTeamSelection(checkbox, teamSide) {
|
||||
// When selecting from one team, deselect from the other team
|
||||
var otherSide = teamSide === 1 ? 2 : 1;
|
||||
var otherCheckboxes = document.querySelectorAll('input[name="team' + otherSide + '_player_ids"]');
|
||||
var playerId = checkbox.value;
|
||||
|
||||
// Find the corresponding checkbox in the other team and uncheck it
|
||||
otherCheckboxes.forEach(function(other) {
|
||||
if (other.value === playerId) {
|
||||
other.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
updateRandomizePreview();
|
||||
}
|
||||
|
||||
function updateDisponibilityIndicators() {
|
||||
if (!canViewDisponibilities) return;
|
||||
|
||||
var dateInput = document.getElementById('date').value;
|
||||
var startTimeInput = document.getElementById('start_time').value;
|
||||
|
||||
if (!dateInput || !startTimeInput) return;
|
||||
|
||||
var dateParts = dateInput.split('-');
|
||||
var dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
|
||||
var jsDay = dateObj.getDay();
|
||||
var dayOfWeek = jsDay === 0 ? 6 : jsDay - 1;
|
||||
|
||||
document.querySelectorAll('.disponibility-indicator').forEach(function(indicator) {
|
||||
var playerId = indicator.getAttribute('data-player-id');
|
||||
var playerData = allDisponibilities[playerId];
|
||||
|
||||
if (playerData && playerData.disponibilities) {
|
||||
var playerSlots = getSlotsForPlayer(parseInt(playerId), dayOfWeek);
|
||||
var isAvailable = selectedSlots.some(function(slot) {
|
||||
return playerSlots.includes(slot);
|
||||
});
|
||||
|
||||
indicator.style.display = 'inline-block';
|
||||
indicator.style.width = '12px';
|
||||
indicator.style.height = '12px';
|
||||
indicator.style.borderRadius = '50%';
|
||||
indicator.style.marginLeft = '8px';
|
||||
indicator.style.backgroundColor = isAvailable ? '#10b981' : '#9ca3af';
|
||||
} else {
|
||||
indicator.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -65,6 +65,26 @@
|
||||
<label for="password">New Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Enter new password">
|
||||
</div>
|
||||
|
||||
{% if user.role == 'player' %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-clock"></i> My Disponibilities</h4>
|
||||
<p class="text-muted small">Select your available time blocks for matches (5pm to 12am). Green = selected, Gray = available to select.</p>
|
||||
|
||||
<div id="disponibilities-grid">
|
||||
<p class="text-muted">Loading...</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-primary" onclick="saveDisponibilities()">
|
||||
<i class="fas fa-save"></i> Save Disponibilities
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="clearDisponibilities()">
|
||||
<i class="fas fa-trash"></i> Clear All
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('users.profile') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
@@ -72,4 +92,218 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.disponibility-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.disponibility-day-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.disponibility-day-label {
|
||||
width: 100px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.disponibility-time-blocks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
.disponibility-time-block {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #f3f4f6;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.disponibility-time-block:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.disponibility-time-block.selected {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-color: #059669;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
{% 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 = '<div style="margin-bottom: 10px;"><strong>Click time blocks to select your available hours</strong></div>';
|
||||
|
||||
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 = '<i class="fas fa-check"></i> 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');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', renderDisponibilityGrid);
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user