Files
team-tryouts/app/templates/pages/coach_availability.html
T

249 lines
7.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layouts/base.html" %}
{% block title %}{{ _('Manage Availability') }} - UdeS team manager{% endblock %}
{% block page_title %}{{ _('Manage Availability') }}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">Home / Coach Availability</span>{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h3><i class="fas fa-clock"></i> {{ _('Set Your Weekly Availability') }}</h3>
<p class="text-muted small">{{ _('Select time slots when you\'re available for One on One sessions') }}</p>
</div>
<div class="card-body">
<div class="availability-grid" id="availability-grid">
<p class="text-muted">{{ _('Loading availability grid...') }}</p>
</div>
<div class="form-actions mt-4">
<button type="button" class="btn btn-primary" data-action="save-availability">
<i class="fas fa-save"></i> {{ _('Save Availability') }}
</button>
<button type="button" class="btn btn-secondary" data-action="clear-availability">
<i class="fas fa-trash"></i> {{ _('Clear All') }}
</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<style>
.availability-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12px;
margin-top: 10px;
}
.day-column {
background: var(--bg-secondary);
border-radius: 8px;
padding: 10px;
min-height: 300px;
}
.day-header {
text-align: center;
font-weight: 600;
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 10px;
color: var(--primary);
}
.time-slot {
padding: 6px 8px;
margin: 4px 0;
border-radius: 4px;
font-size: 0.8rem;
text-align: center;
cursor: pointer;
transition: var(--transition);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.time-slot:hover {
background: var(--primary-light);
border-color: var(--primary);
}
.time-slot.selected {
background: var(--primary);
color: white;
border-color: var(--primary-dark);
}
.time-slot.selected:hover {
background: var(--danger);
}
@media (max-width: 768px) {
.availability-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 480px) {
.availability-grid {
grid-template-columns: 1fr;
}
}
</style>
<script nonce="{{ csp_nonce }}">
// Time slots from 8:00 AM to 10:00 PM (30-minute intervals)
const TIME_SLOTS = [];
for (let h = 8; h <= 22; h++) {
for (let m = 0; m < 60; m += 30) {
const timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
const displayHour = h > 12 ? h - 12 : h;
const displayAmpm = h >= 12 ? 'PM' : 'AM';
const displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
TIME_SLOTS.push({ time: timeStr, display: displayTime });
}
}
// Day names
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
// Track selected slots: {day_of_week: [time_strings]}
let selectedSlots = {};
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadExistingAvailability();
renderGrid();
});
function loadExistingAvailability() {
// Load from existing data
{% for av in existing_availability %}
if (!selectedSlots[{{ av.day_of_week }}]) {
selectedSlots[{{ av.day_of_week }}] = [];
}
selectedSlots[{{ av.day_of_week }}].push('{{ av.start_time.strftime('%H:%M') }}');
{% endfor %}
}
function renderGrid() {
const grid = document.getElementById('availability-grid');
let html = '';
DAYS.forEach((day, dayIndex) => {
html += '<div class="day-column">';
html += '<div class="day-header">' + day.substring(0, 3) + '</div>';
TIME_SLOTS.forEach(slot => {
const isSelected = selectedSlots[dayIndex] && selectedSlots[dayIndex].includes(slot.time);
const cssClass = isSelected ? 'time-slot selected' : 'time-slot';
html += '<div class="' + cssClass + '" data-day="' + dayIndex + '" data-time="' + slot.time + '" data-action="toggle-slot">' + slot.display + '</div>';
});
html += '</div>';
});
grid.innerHTML = html;
}
function toggleSlot(element) {
const dayOfWeek = Number(element.getAttribute('data-day'));
const timeStr = element.getAttribute('data-time');
if (!selectedSlots[dayOfWeek]) {
selectedSlots[dayOfWeek] = [];
}
const index = selectedSlots[dayOfWeek].indexOf(timeStr);
if (index === -1) {
selectedSlots[dayOfWeek].push(timeStr);
element.classList.add('selected');
} else {
selectedSlots[dayOfWeek].splice(index, 1);
element.classList.remove('selected');
}
}
function saveAvailability() {
const slots = [];
for (let day in selectedSlots) {
selectedSlots[day].forEach(time => {
slots.push({ day_of_week: parseInt(day), start_time: time });
});
}
fetch('{{ url_for("users.manage_coach_availability") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ slots: slots })
})
.then(response => response.json())
.then(data => {
if (data.success) {
flash('Availability saved!', 'success');
} else {
flash(data.error || 'Error saving availability.', 'danger');
}
})
.catch(function(error) {
console.error('Save error:', error);
flash('Error saving availability.', 'danger');
});
}
function clearAllAvailability() {
if (!confirm('Are you sure you want to clear all your availability slots?')) {
return;
}
fetch('{{ url_for("users.clear_coach_availability") }}', {
method: 'POST',
headers: { 'X-CSRFToken': '{{ csrf_token() }}' }
})
.then(response => response.json())
.then(data => {
if (data.success) {
selectedSlots = {};
renderGrid();
flash('Availability cleared!', 'success');
}
});
}
function flash(message, type) {
const flashContainer = document.querySelector('.flash-messages');
const alert = document.createElement('div');
alert.className = 'alert alert-' + type + ' alert-dismissible';
const text = document.createElement('span');
text.textContent = message;
const close = document.createElement('button');
close.type = 'button';
close.className = 'alert-close';
close.dataset.action = 'remove-element';
close.textContent = '×';
alert.append(text, close);
flashContainer.appendChild(alert);
}
// Auto-save on change (debounced)
let saveTimeout;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('time-slot')) {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveAvailability, 1000);
}
});
// Behaviours are declared in the markup with data-action / data-change and
// dispatched by the delegated listener in main.js. This replaces inline
// onclick attributes, which no CSP nonce is able to authorise.
registerActions({
'save-availability': saveAvailability,
'clear-availability': clearAllAvailability,
'toggle-slot': toggleSlot,
});
</script>
{% endblock %}