Force connection HTTPS, proxy-inversé, WSGI de production, reset cookie de conncection à chaque reconnection, limite sur les mdp, fichiers et One on One par minute, verification d'injection de SQL dans les champs d'entrées. renommage des fichiers lors du téléchargement, fichier de backup quotidien pour la bd et j'ai oublié quelque chose :(
219 lines
6.2 KiB
HTML
219 lines
6.2 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% block title %}Manage Availability - TryoutPro{% 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-secondary" onclick="clearAllAvailability()">
|
|
<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>
|
|
// 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 + '" onclick="toggleSlot(' + dayIndex + ', \'' + slot.time + '\', this)">' + slot.display + '</div>';
|
|
});
|
|
|
|
html += '</div>';
|
|
});
|
|
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
function toggleSlot(dayOfWeek, timeStr, element) {
|
|
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' },
|
|
body: JSON.stringify({ slots: slots })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
flash('Availability saved!', 'success');
|
|
}
|
|
})
|
|
.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' })
|
|
.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';
|
|
alert.innerHTML = '<span>' + message + '</span><button type="button" class="alert-close" onclick="this.parentElement.remove()">×</button>';
|
|
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);
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |