ajout d'un paneau admin
This commit is contained in:
+181
-158
@@ -4,60 +4,8 @@
|
||||
{% 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 mb-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-list-check"></i> My One on One Requests</h3>
|
||||
</div>
|
||||
@@ -67,6 +15,8 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Coach</th>
|
||||
<th>Team</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Discussion Points</th>
|
||||
@@ -77,6 +27,8 @@
|
||||
<tbody>
|
||||
{% for req in my_requests %}
|
||||
<tr>
|
||||
<td>{{ req.coach.full_name if req.coach else 'Unknown Coach' }}</td>
|
||||
<td>{{ req.team.name if req.team else '—' }}</td>
|
||||
<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>
|
||||
@@ -113,71 +65,78 @@
|
||||
</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" onchange="updateTimeSlots()" 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" onchange="updateEndTimeOptions()" 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>
|
||||
<!-- One on One Request Section -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-calendar-check"></i> Request One on One Session</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if coach_options %}
|
||||
<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 col-6">
|
||||
<label for="coach_id">Select Coach</label>
|
||||
<select name="coach_id" id="coach_id" class="form-select" onchange="onCoachChange()" required>
|
||||
<option value="">-- Select Coach --</option>
|
||||
{% for c in coach_options %}
|
||||
<option value="{{ c.coach_id }}">{{ c.coach_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-6">
|
||||
<label for="org_team_id">Select Team <span class="text-muted">(optional)</span></label>
|
||||
<select name="org_team_id" id="org_team_id" class="form-select">
|
||||
<option value="">-- Any Team --</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<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>
|
||||
<label for="date">Select Date</label>
|
||||
<select name="date" id="date" class="form-select" onchange="updateTimeSlots()" required>
|
||||
<option value="">-- Select Coach First --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="start_time">Start Time</label>
|
||||
<select name="start_time" id="start_time" class="form-select" onchange="updateEndTimeOptions()" 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-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 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>
|
||||
|
||||
{% if coach %}
|
||||
<!-- Hidden data for JavaScript -->
|
||||
<script id="coach-availability-data" type="application/json">
|
||||
{{ coach_availability | tojson }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Coach options injected server-side
|
||||
const COACH_OPTIONS = {{ coach_options | tojson }};
|
||||
|
||||
// Time slots from 8:00 AM to 10:00 PM
|
||||
const TIME_SLOTS = [];
|
||||
for (let h = 8; h <= 22; h++) {
|
||||
@@ -190,62 +149,122 @@ for (let h = 8; h <= 22; h++) {
|
||||
}
|
||||
}
|
||||
|
||||
// Coach availability data
|
||||
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
let coachAvailability = [];
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadCoachAvailability();
|
||||
updateTimeSlots();
|
||||
});
|
||||
// Helper to convert "HH:MM" to minutes since midnight
|
||||
function toMinutes(timeStr) {
|
||||
const parts = timeStr.split(':');
|
||||
return parseInt(parts[0]) * 60 + parseInt(parts[1]);
|
||||
}
|
||||
|
||||
function loadCoachAvailability() {
|
||||
const dataEl = document.getElementById('coach-availability-data');
|
||||
if (!dataEl) return;
|
||||
|
||||
try {
|
||||
coachAvailability = JSON.parse(dataEl.textContent);
|
||||
} catch (e) {
|
||||
coachAvailability = [];
|
||||
// Build date dropdown for the next 14 days filtered to days the coach has availability
|
||||
function buildDateOptions() {
|
||||
const dateSelect = document.getElementById('date');
|
||||
dateSelect.innerHTML = '<option value="">-- Select Date --</option>';
|
||||
|
||||
const availableDays = new Set(coachAvailability.map(av => av.day_of_week));
|
||||
if (availableDays.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(today.getDate() + i);
|
||||
if (availableDays.has(d.getDay())) {
|
||||
const value = d.toISOString().split('T')[0];
|
||||
const display = d.toLocaleDateString('en-US', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
});
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.setAttribute('data-day', d.getDay());
|
||||
option.textContent = display;
|
||||
dateSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset dependent dropdowns
|
||||
document.getElementById('start_time').innerHTML = '<option value="">-- Select Date First --</option>';
|
||||
document.getElementById('end_time').innerHTML = '<option value="">-- Select Start Time First --</option>';
|
||||
}
|
||||
|
||||
// Populate team dropdown for the selected coach
|
||||
function updateTeamOptions(coachId) {
|
||||
const teamSelect = document.getElementById('org_team_id');
|
||||
teamSelect.innerHTML = '<option value="">-- Any Team --</option>';
|
||||
|
||||
const coach = COACH_OPTIONS.find(c => c.coach_id === coachId);
|
||||
if (!coach) return;
|
||||
|
||||
coach.teams.forEach(team => {
|
||||
const option = document.createElement('option');
|
||||
option.value = team.team_id;
|
||||
option.textContent = team.team_name;
|
||||
teamSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch availability for the selected coach
|
||||
function fetchCoachAvailability(coachId) {
|
||||
coachAvailability = [];
|
||||
if (!coachId) {
|
||||
buildDateOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/users/one-on-one/coaches/${coachId}/availability`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
coachAvailability = data.availability || [];
|
||||
buildDateOptions();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading coach availability:', error);
|
||||
coachAvailability = [];
|
||||
buildDateOptions();
|
||||
});
|
||||
}
|
||||
|
||||
function onCoachChange() {
|
||||
const coachId = parseInt(document.getElementById('coach_id').value);
|
||||
updateTeamOptions(coachId);
|
||||
fetchCoachAvailability(coachId);
|
||||
}
|
||||
|
||||
function updateTimeSlots() {
|
||||
const dateSelect = document.getElementById('date');
|
||||
const startTimeSelect = document.getElementById('start_time');
|
||||
|
||||
|
||||
const selectedOption = dateSelect.options[dateSelect.selectedIndex];
|
||||
if (!selectedOption || !selectedOption.value) {
|
||||
startTimeSelect.innerHTML = '<option value="">-- Select Date First --</option>';
|
||||
document.getElementById('end_time').innerHTML = '<option value="">-- Select Start Time First --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
const startMinutes = toMinutes(av.start_time);
|
||||
const endMinutes = toMinutes(av.end_time);
|
||||
for (let m = startMinutes; m < endMinutes; m += 30) {
|
||||
availableMinutes.push(m);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort minutes and convert to time strings
|
||||
availableMinutes.sort((a, b) => a - b);
|
||||
|
||||
startTimeSelect.innerHTML = '<option value="">-- Select Start Time --</option>';
|
||||
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);
|
||||
const slotData = TIME_SLOTS.find(s => s.time === timeStr);
|
||||
if (slotData) {
|
||||
const option = document.createElement('option');
|
||||
option.value = slotData.time;
|
||||
@@ -253,8 +272,7 @@ function updateTimeSlots() {
|
||||
startTimeSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset end time options
|
||||
|
||||
updateEndTimeOptions();
|
||||
}
|
||||
|
||||
@@ -262,35 +280,34 @@ 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];
|
||||
if (!selectedOption || !selectedOption.value) {
|
||||
endTimeSelect.innerHTML = '<option value="">-- Select Start Time First --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
const startMinutes = toMinutes(av.start_time);
|
||||
const endMinutes = toMinutes(av.end_time);
|
||||
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
|
||||
|
||||
const startMinutesVal = toMinutes(selectedStart);
|
||||
const validEndTimes = availableMinutes.filter(m => m > startMinutesVal).sort((a, b) => a - b);
|
||||
|
||||
endTimeSelect.innerHTML = '<option value="">-- Select End Time --</option>';
|
||||
validEndTimes.forEach(m => {
|
||||
const hour = Math.floor(m / 60);
|
||||
@@ -305,5 +322,11 @@ function updateEndTimeOptions() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (COACH_OPTIONS.length > 0) {
|
||||
onCoachChange();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user