OPS-010, suite. 76 gestionnaires en ligne -> 52, dans 5 gabarits au lieu de
15. Le cliquet de tests/test_csp.py est abaisse en consequence.
Six motifs recurrents, generalises dans main.js plutot que traites un a un
data-action clic, resolu par un ecouteur delegue
data-change changement -- attribut distinct du clic, sans quoi un
<select> declencherait son gestionnaire des le clic
qui l'ouvre
data-confirm confirmation avant un envoi destructeur, en
remplacement de onsubmit="return confirm(...)". Le
texte reste dans le markup, donc traduisible.
data-navigate navigation sur selection, {value} etant encode
remove-element suppression d'un ancetre designe par data-remove
history-back retour arriere
registerActions()
Les fonctions propres a une page vivent dans son bloc de script et ne
peuvent donc pas figurer dans la table globale. Chaque page declare les
siennes, l'ecouteur delegue reste unique.
Cas particulier, coach_availability
Le gestionnaire y etait construit dans une chaine JavaScript, au moment
de generer la grille de creneaux. Le markup portait deja data-day et
data-time : toggleSlot lit desormais ses arguments depuis l'element, ce
qui supprime a la fois l'attribut en ligne et la concatenation.
Gabarits migres : my_teams, register, users, view_user, one_on_one,
coach_availability, profile, team_matches, notes, contracts.
Restent, par ordre decroissant : match_form 13, calendar 11, teams 11,
evaluate_player 9, view_tryout 8.
Syntaxe JavaScript de chaque bloc modifie verifiee par node --check.
A noter : la traduction de ces dix gabarits reste a faire. Seules les deux
chaines devenues visibles dans le markup au cours de cette migration -- les
messages de confirmation de suppression -- sont balisees et traduites.
192 tests.
Co-Authored-By: Claude Opus 5 <[email protected]>
613 lines
23 KiB
HTML
613 lines
23 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% block title %}My Profile - TryoutPro{% endblock %}
|
|
{% block page_title %}My Profile{% endblock %}
|
|
{% block breadcrumb %}<span class="breadcrumb">Home / Profile</span>{% endblock %}
|
|
|
|
{% block header_actions %}
|
|
<div class="header-actions">
|
|
<a href="{{ url_for('users.list_contracts') }}" class="btn btn-sm btn-info">
|
|
<i class="fas fa-file-contract"></i> Contracts
|
|
</a>
|
|
<a href="{{ url_for('users.edit_profile') }}" class="btn btn-sm btn-primary">
|
|
<i class="fas fa-edit"></i> Edit Profile
|
|
</a>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="dashboard-grid">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-user-circle"></i> Account Information</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="profile-header">
|
|
<div class="user-avatar avatar-xl">{{ user.username[:2] | upper }}</div>
|
|
<div class="profile-info">
|
|
<h2>{{ user.username }}</h2>
|
|
<span class="text-muted" style="font-size: 1.1em;">{{ user.full_name }}</span>
|
|
<span class="badge badge-{{ user.role }} badge-lg">{{ user.role | capitalize }}</span>
|
|
<p class="text-muted">Member since {{ user.created_at.strftime('%B %Y') }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="detail-grid mt-4">
|
|
<div class="detail-item">
|
|
<span class="detail-label">Username</span>
|
|
<span class="detail-value">{{ user.username }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Email</span>
|
|
<span class="detail-value">{{ user.email }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Phone</span>
|
|
<span class="detail-value">{{ user.phone or 'Not provided' }}</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Account Status</span>
|
|
<span class="detail-value">
|
|
{% if user.is_active_account %}
|
|
<span class="badge badge-success">Active</span>
|
|
{% else %}
|
|
<span class="badge badge-danger">Inactive</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- E-Sports Profile Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-gamepad"></i> E-Sports Profile</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<span class="detail-label"><i class="fas fa-gamepad"></i> Gamertags</span>
|
|
<span class="detail-value">
|
|
{% set games_list = user.get_games_list() %}
|
|
{% if games_list %}
|
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
{% for game in games_list %}
|
|
{% set gt = user.gamertags | selectattr('game', 'equalto', game) | first %}
|
|
<div>
|
|
{% if gt and gt.get_trn_url() %}
|
|
<a href="{{ gt.get_trn_url() }}" target="_blank" rel="noopener noreferrer" class="trn-link">
|
|
<span class="badge badge-esport" style="margin-right: 8px;">{{ game }}</span>
|
|
<i class="fas fa-external-link-alt"></i> {{ gt.gamertag }}
|
|
{% if gt.platform %}<small>({{ gt.platform }})</small>{% endif %}
|
|
</a>
|
|
{% else %}
|
|
<span class="badge badge-esport">{{ game }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">Not specified</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label"><i class="fab fa-discord"></i> Discord</span>
|
|
<span class="detail-value">
|
|
{% if user.discord_username %}
|
|
{{ user.discord_username }}
|
|
{% else %}
|
|
<span class="text-muted">Not connected</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label"><i class="fas fa-link"></i> League OS</span>
|
|
<span class="detail-value">
|
|
{% if user.league_os_profile %}
|
|
<a href="{{ user.league_os_profile }}" target="_blank" rel="noopener noreferrer" class="trn-link">
|
|
<i class="fas fa-external-link-alt"></i> {{ user.league_os_profile }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted">Not connected</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-chart-bar"></i> My Statistics</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if user.role == 'player' %}
|
|
<div class="profile-stats">
|
|
<div class="stat-item">
|
|
<h4>{{ user.tryout_registrations.count() }}</h4>
|
|
<p>Tryouts Registered</p>
|
|
</div>
|
|
<div class="stat-item">
|
|
<h4>{{ user.team_assignments.count() }}</h4>
|
|
<p>Team Assignments</p>
|
|
</div>
|
|
</div>
|
|
{% elif user.can_evaluate() %}
|
|
<div class="profile-stats">
|
|
<div class="stat-item">
|
|
<h4>{{ user.evaluations_given.count() }}</h4>
|
|
<p>Evaluations Given</p>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted">No statistics available for this role.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contracts Card -->
|
|
{% if user.role == 'player' %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-file-contract"></i> My Contracts</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if contracts %}
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<span class="detail-label">Contracts Pending</span>
|
|
<span class="detail-value">
|
|
{% set pending = contracts | selectattr('status', 'equalto', 'pending') | list %}
|
|
<span class="badge badge-{{ 'warning' if pending|length > 0 else 'success' }}">{{ pending | length }}</span>
|
|
</span>
|
|
</div>
|
|
<div class="detail-item">
|
|
<span class="detail-label">Contracts Signed</span>
|
|
<span class="detail-value">
|
|
{% set signed = contracts | selectattr('status', 'equalto', 'signed') | list %}
|
|
<span class="badge {% if signed %}badge-success{% else %}badge-secondary{% endif %}">{{ signed | length }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<a href="{{ url_for('users.list_contracts') }}" class="btn btn-sm btn-primary">
|
|
<i class="fas fa-folder-open"></i> View All Contracts
|
|
</a>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted">No contracts have been uploaded for you yet.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Disponibilities Card (Players only) -->
|
|
{% if user.role == 'player' %}
|
|
<div class="card" style="grid-column: 1 / -1;">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-clock"></i> My Disponibilities</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<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" data-action="save-disponibilities">
|
|
<i class="fas fa-save"></i> Save Disponibilities
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" data-action="clear-disponibilities">
|
|
<i class="fas fa-trash"></i> Clear All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Coach Availability Card (Coaches only) -->
|
|
{% if user.role == 'coach' %}
|
|
<div class="card" style="grid-column: 1 / -1;">
|
|
<div class="card-header">
|
|
<h3><i class="fas fa-clock"></i> My Coaching Availability</h3>
|
|
<p class="text-muted small">Select time slots when you're available for One on One sessions (8am to 10pm).</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-3">
|
|
<button type="button" class="btn btn-secondary" data-action="clear-availability">
|
|
<i class="fas fa-trash"></i> Clear All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{% if user.role == 'coach' %}
|
|
<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 }}">
|
|
const COACH_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;
|
|
COACH_TIME_SLOTS.push({ time: timeStr, display: displayTime });
|
|
}
|
|
}
|
|
const COACH_DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
let coachSelectedSlots = {};
|
|
|
|
// Drag state for coach grid
|
|
let coachDragMode = false;
|
|
let coachDragAction = null;
|
|
|
|
function applyCoachSlotAction(dayOfWeek, timeStr, element) {
|
|
if (!coachSelectedSlots[dayOfWeek]) coachSelectedSlots[dayOfWeek] = [];
|
|
const index = coachSelectedSlots[dayOfWeek].indexOf(timeStr);
|
|
if (coachDragAction === 'select' && index === -1) {
|
|
coachSelectedSlots[dayOfWeek].push(timeStr);
|
|
element.classList.add('selected');
|
|
} else if (coachDragAction === 'deselect' && index > -1) {
|
|
coachSelectedSlots[dayOfWeek].splice(index, 1);
|
|
element.classList.remove('selected');
|
|
}
|
|
}
|
|
|
|
function loadCoachAvailability() {
|
|
{% for av in existing_availability %}
|
|
if (!coachSelectedSlots[{{ av.day_of_week }}]) coachSelectedSlots[{{ av.day_of_week }}] = [];
|
|
coachSelectedSlots[{{ av.day_of_week }}].push('{{ av.start_time.strftime('%H:%M') }}');
|
|
{% endfor %}
|
|
}
|
|
|
|
function renderCoachGrid() {
|
|
const grid = document.getElementById('availability-grid');
|
|
let html = '';
|
|
COACH_DAYS.forEach((day, dayIndex) => {
|
|
html += '<div class="day-column">';
|
|
html += '<div class="day-header">' + day.substring(0, 3) + '</div>';
|
|
COACH_TIME_SLOTS.forEach(slot => {
|
|
const isSelected = coachSelectedSlots[dayIndex] && coachSelectedSlots[dayIndex].includes(slot.time);
|
|
const cssClass = isSelected ? 'time-slot selected' : 'time-slot';
|
|
html += '<div class="' + cssClass + '" data-day="' + dayIndex + '" data-time="' + slot.time + '">' + slot.display + '</div>';
|
|
});
|
|
html += '</div>';
|
|
});
|
|
grid.innerHTML = html;
|
|
|
|
// Drag event listeners
|
|
grid.addEventListener('mousedown', function(e) {
|
|
const slot = e.target.closest('.time-slot');
|
|
if (!slot) return;
|
|
e.preventDefault();
|
|
coachDragMode = true;
|
|
const day = parseInt(slot.dataset.day);
|
|
const time = slot.dataset.time;
|
|
if (!coachSelectedSlots[day]) coachSelectedSlots[day] = [];
|
|
const isSelected = coachSelectedSlots[day].indexOf(time) > -1;
|
|
coachDragAction = isSelected ? 'deselect' : 'select';
|
|
applyCoachSlotAction(day, time, slot);
|
|
});
|
|
|
|
grid.addEventListener('mousemove', function(e) {
|
|
if (!coachDragMode) return;
|
|
const slot = e.target.closest('.time-slot');
|
|
if (!slot) return;
|
|
applyCoachSlotAction(parseInt(slot.dataset.day), slot.dataset.time, slot);
|
|
});
|
|
|
|
document.addEventListener('mouseup', function() {
|
|
if (coachDragMode) {
|
|
coachDragMode = false;
|
|
coachDragAction = null;
|
|
saveCoachAvailability();
|
|
}
|
|
});
|
|
}
|
|
|
|
function saveCoachAvailability() {
|
|
const slots = [];
|
|
for (let day in coachSelectedSlots) {
|
|
coachSelectedSlots[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' },
|
|
body: JSON.stringify({ slots: slots })
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const msg = document.createElement('div');
|
|
msg.className = 'alert alert-success';
|
|
msg.style.marginTop = '10px';
|
|
msg.innerHTML = '<i class="fas fa-check"></i> Availability saved!';
|
|
document.getElementById('availability-grid').appendChild(msg);
|
|
setTimeout(() => msg.remove(), 3000);
|
|
}
|
|
})
|
|
.catch(err => console.error('Save error:', err));
|
|
}
|
|
|
|
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' })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
coachSelectedSlots = {};
|
|
renderCoachGrid();
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadCoachAvailability();
|
|
renderCoachGrid();
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
|
|
{% if user.role == 'player' %}
|
|
<script nonce="{{ csp_nonce }}">
|
|
// 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 = {};
|
|
|
|
// Drag state
|
|
var dispDragMode = false;
|
|
var dispDragAction = null; // 'select' or 'deselect'
|
|
|
|
function applySlotAction(block, action) {
|
|
var day = parseInt(block.dataset.day);
|
|
var time = block.dataset.time;
|
|
if (!selectedSlots[day]) selectedSlots[day] = [];
|
|
var index = selectedSlots[day].indexOf(time);
|
|
if (action === 'select' && index === -1) {
|
|
selectedSlots[day].push(time);
|
|
block.classList.add('selected');
|
|
} else if (action === 'deselect' && index > -1) {
|
|
selectedSlots[day].splice(index, 1);
|
|
block.classList.remove('selected');
|
|
}
|
|
}
|
|
|
|
function renderDisponibilityGrid() {
|
|
var grid = document.getElementById('disponibilities-grid');
|
|
grid.innerHTML = '<div style="margin-bottom: 10px;"><strong>Click or click-and-drag 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;
|
|
timeBlocks.appendChild(block);
|
|
});
|
|
|
|
dayRow.appendChild(timeBlocks);
|
|
container.appendChild(dayRow);
|
|
});
|
|
|
|
// Drag event listeners on the container
|
|
container.addEventListener('mousedown', function(e) {
|
|
var block = e.target.closest('.disponibility-time-block');
|
|
if (!block) return;
|
|
e.preventDefault();
|
|
dispDragMode = true;
|
|
var day = parseInt(block.dataset.day);
|
|
var time = block.dataset.time;
|
|
if (!selectedSlots[day]) selectedSlots[day] = [];
|
|
var isSelected = selectedSlots[day].indexOf(time) > -1;
|
|
dispDragAction = isSelected ? 'deselect' : 'select';
|
|
applySlotAction(block, dispDragAction);
|
|
});
|
|
|
|
container.addEventListener('mousemove', function(e) {
|
|
if (!dispDragMode) return;
|
|
var block = e.target.closest('.disponibility-time-block');
|
|
if (!block) return;
|
|
applySlotAction(block, dispDragAction);
|
|
});
|
|
|
|
document.addEventListener('mouseup', function() {
|
|
if (dispDragMode) {
|
|
dispDragMode = false;
|
|
dispDragAction = null;
|
|
saveDisponibilities();
|
|
}
|
|
});
|
|
|
|
grid.appendChild(container);
|
|
loadMyDisponibilities();
|
|
}
|
|
|
|
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',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
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',
|
|
headers: {
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
}
|
|
})
|
|
.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');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
renderDisponibilityGrid();
|
|
});
|
|
|
|
// 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-disponibilities': saveDisponibilities,
|
|
'clear-disponibilities': clearDisponibilities,
|
|
'clear-availability': clearAllAvailability,
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
{% endblock %} |