Files

318 lines
14 KiB
HTML

{% extends "layouts/base.html" %}
{% block title %}{{ _('One on One') }} - UdeS team manager{% endblock %}
{% block page_title %}{{ _('One on One') }}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">Home / One on One</span>{% endblock %}
{% block content %}
<div class="dashboard-grid">
<!-- Team Notes Section -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-users"></i> {{ _('Team Notes') }}</h3>
{% if org_team %}
<span class="badge badge-esport">{{ org_team.name }}</span>
{% endif %}
</div>
<div class="card-body">
{% if team_notes %}
{% for note in team_notes %}
<div class="detail-grid mt-4">
<div class="detail-item full-width">
<span class="detail-label"><i class="fas fa-user"></i> Coach: {{ note.coach.username if note.coach else 'Unknown Coach' }}</span>
<span class="detail-value">{{ note.content | nl2br }}</span>
</div>
</div>
<p class="text-muted small mt-2">Updated: {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
{% endfor %}
{% else %}
<p class="text-muted">{{ _('No team notes have been added yet. Your coach will post improvement suggestions here.') }}</p>
{% endif %}
</div>
</div>
<!-- Personal Notes Section -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-user"></i> {{ _('Personal Notes') }}</h3>
{% if coach %}
<span class="badge badge-coach">From: {{ coach.username }}</span>
{% endif %}
</div>
<div class="card-body">
{% if personal_notes %}
{% for note in personal_notes %}
<div class="detail-grid mt-4">
<div class="detail-item full-width">
<span class="detail-label"><i class="fas fa-sticky-note"></i> Note from {{ note.coach.username if note.coach else 'Unknown Coach' }}</span>
<span class="detail-value">{{ note.content | nl2br }}</span>
</div>
</div>
<p class="text-muted small mt-2">Added: {{ note.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
{% endfor %}
{% else %}
<p class="text-muted">{{ _('No personal notes have been added yet. Your coach may provide individual feedback here.') }}</p>
{% endif %}
</div>
</div>
</div>
<!-- My One on One Requests Tracker -->
<div class="card mt-4">
<div class="card-header">
<h3><i class="fas fa-list-check"></i> {{ _('My One on One Requests') }}</h3>
</div>
<div class="card-body">
{% if my_requests %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ _('Date') }}</th>
<th>{{ _('Time') }}</th>
<th>{{ _('Discussion Points') }}</th>
<th>{{ _('Status') }}</th>
<th>{{ _('Coach Response') }}</th>
</tr>
</thead>
<tbody>
{% for req in my_requests %}
<tr>
<td>{{ req.date.strftime('%b %d, %Y') }}</td>
<td>{{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }}</td>
<td>{{ req.points or 'N/A' }}</td>
<td>
{% if req.status == 'pending' %}
<span class="badge badge-warning"><i class="fas fa-clock"></i> {{ _('Pending') }}</span>
{% elif req.status == 'approved' %}
<span class="badge badge-success"><i class="fas fa-check-circle"></i> {{ _('Approved') }}</span>
{% elif req.status == 'rejected' %}
<span class="badge badge-danger"><i class="fas fa-times-circle"></i> {{ _('Rejected') }}</span>
{% endif %}
</td>
<td>
{% if req.status == 'approved' %}
<span class="text-success">Session confirmed!</span>
{% elif req.status == 'rejected' %}
{% if req.coach_rejection_message %}
<span class="text-danger small">{{ req.coach_rejection_message }}</span>
{% else %}
<span class="text-muted small">Coach is unavailable</span>
{% endif %}
{% else %}
<span class="text-muted small">Awaiting coach response...</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">{{ _('You haven\'t made any One on One requests yet.') }}</p>
{% endif %}
</div>
</div>
<div class="dashboard-grid mt-4">
<!-- One on One Request Section -->
<div class="card" style="grid-column: 1 / -1;">
<div class="card-header">
<h3><i class="fas fa-calendar-check"></i> {{ _('Request One on One Session') }}</h3>
{% if coach %}
<span class="badge badge-info">Coach: {{ coach.username }}</span>
{% endif %}
</div>
<div class="card-body">
{% if coach %}
<form method="POST" action="{{ url_for('users.one_on_one') }}" class="form" id="oneOnOneForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-row">
<div class="form-group">
<label for="date">{{ _('Select Date') }}</label>
<select name="date" id="date" class="form-select" data-change="update-time-slots" required>
{% for d in dates %}
<option value="{{ d.value }}" data-day="{{ d.day_of_week }}">{{ d.display }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="start_time">{{ _('Start Time') }}</label>
<select name="start_time" id="start_time" class="form-select" data-change="update-end-times" required>
<option value="">{{ _('-- Select Date First --') }}</option>
</select>
</div>
<div class="form-group">
<label for="end_time">{{ _('End Time') }}</label>
<select name="end_time" id="end_time" class="form-select" required>
<option value="">{{ _('-- Select Start Time First --') }}</option>
</select>
</div>
</div>
<div class="form-group">
<label for="points">Discussion Points <span class="text-muted">(What would you like to discuss?)</span></label>
<textarea name="points" id="points" class="form-textarea" placeholder="{{ _('Enter topics you\'d like to cover in your One on One session...') }}" rows="4"></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> {{ _('Send Request') }}
</button>
</div>
</form>
{% else %}
<p class="text-muted">{{ _('You need to be assigned to a team with a coach to request a One on One session.') }}</p>
{% endif %}
</div>
</div>
</div>
{% if coach %}
<!-- Hidden data for JavaScript -->
<script id="coach-availability-data" type="application/json" nonce="{{ csp_nonce }}">
{{ coach_availability | tojson }}
</script>
{% endif %}
{% endblock %}
{% block scripts %}
<script nonce="{{ csp_nonce }}">
// Time slots from 8:00 AM to 10:00 PM
const TIME_SLOTS = [];
for (let h = 8; h <= 22; h++) {
for (let m = 0; m < 60; m += 30) {
const displayHour = h > 12 ? h - 12 : h;
const displayAmpm = h >= 12 ? 'PM' : 'AM';
const timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
const displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm;
TIME_SLOTS.push({ time: timeStr, display: displayTime });
}
}
// Coach availability data
let coachAvailability = [];
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadCoachAvailability();
updateTimeSlots();
});
function loadCoachAvailability() {
const dataEl = document.getElementById('coach-availability-data');
if (!dataEl) return;
try {
coachAvailability = JSON.parse(dataEl.textContent);
} catch (e) {
coachAvailability = [];
}
}
function updateTimeSlots() {
const dateSelect = document.getElementById('date');
const startTimeSelect = document.getElementById('start_time');
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
const dayOfWeek = parseInt(selectedOption.getAttribute('data-day'));
// Get available time slots for this day
const dayAvailability = coachAvailability.filter(av => av.day_of_week === dayOfWeek);
// Build available slots - collect all available minutes then sort
const availableSlots = [];
const availableMinutes = [];
dayAvailability.forEach(av => {
const startMinutes = av.start_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
const endMinutes = av.end_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
// Add 30-minute slots
for (let m = startMinutes; m < endMinutes; m += 30) {
availableMinutes.push(m);
}
});
// Sort minutes and convert to time strings
availableMinutes.sort((a, b) => a - b);
availableMinutes.forEach(m => {
const hour = Math.floor(m / 60);
const minute = m % 60;
const timeStr = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute;
availableSlots.push(timeStr);
});
// Update start time options (sorted)
startTimeSelect.innerHTML = '<option value="">{{ _('-- Select Start Time --') }}</option>';
availableSlots.forEach(slot => {
const slotData = TIME_SLOTS.find(s => s.time === slot);
if (slotData) {
const option = document.createElement('option');
option.value = slotData.time;
option.textContent = slotData.display;
startTimeSelect.appendChild(option);
}
});
// Reset end time options
updateEndTimeOptions();
}
function updateEndTimeOptions() {
const dateSelect = document.getElementById('date');
const startTimeSelect = document.getElementById('start_time');
const endTimeSelect = document.getElementById('end_time');
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
const dayOfWeek = parseInt(selectedOption.getAttribute('data-day'));
const selectedStart = startTimeSelect.value;
if (!selectedStart) {
endTimeSelect.innerHTML = '<option value="">{{ _('-- Select Start Time First --') }}</option>';
return;
}
// Get available minutes for this day
const dayAvailability = coachAvailability.filter(av => av.day_of_week === dayOfWeek);
const availableMinutes = [];
dayAvailability.forEach(av => {
const startMinutes = av.start_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
const endMinutes = av.end_time.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
for (let m = startMinutes; m < endMinutes; m += 30) {
availableMinutes.push(m);
}
});
// Convert selected start to minutes
const startMinutesVal = selectedStart.split(':').reduce((acc, val, i) => acc + parseInt(val) * (i === 0 ? 60 : 1), 0);
// Filter end times that are after start time
const validEndTimes = availableMinutes.filter(m => m > startMinutesVal);
validEndTimes.sort((a, b) => a - b);
// Update end time options
endTimeSelect.innerHTML = '<option value="">{{ _('-- Select End Time --') }}</option>';
validEndTimes.forEach(m => {
const hour = Math.floor(m / 60);
const minute = m % 60;
const timeStr = (hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') + minute;
const slotData = TIME_SLOTS.find(s => s.time === timeStr);
if (slotData) {
const option = document.createElement('option');
option.value = slotData.time;
option.textContent = slotData.display;
endTimeSelect.appendChild(option);
}
});
}
// 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({
'update-time-slots': updateTimeSlots,
'update-end-times': updateEndTimeOptions,
});
</script>
{% endblock %}