You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

336 lines
16 KiB

{% extends 'base.html' %}
{% block content %}
{% load static %}
{% load temps_format %}
<div class="container-fluid mt-4">
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Gestion de la course</h6>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% if not is_started %}
<button type="submit" name="start" class="btn btn-success">Départ
<i class="fa-solid fa-play"></i>
</button>
{% elif not is_finished %}
<button type="button" id="btnFinish" class="btn btn-danger">Fin course
<i class="fa-solid fa-stop"></i>
</button>
<a href="{% url 'scan' %}?course_id={{ course.id }}" class="btn btn-info ml-2">Accès au scan <i class="fas fa-qrcode"></i></a>
<!-- Modal confirmation fin de course -->
<div class="modal fade" id="finishModal" tabindex="-1" role="dialog" aria-labelledby="finishModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="finishModalLabel">Confirmer la fin de la course</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Êtes-vous sûr de vouloir terminer la course ? Cette action est irréversible.
</div>
<div class="modal-footer">
<form method="post" style="margin:0;">
{% csrf_token %}
<button type="submit" name="finish" class="btn btn-danger">Valider la fin</button>
</form>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
</div>
</div>
</div>
</div>
{% else %}
<span class="badge badge-secondary">Course terminée</span>
{% endif %}
</form>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Arrivées</h6>
<div>
{% if course.type == 'multi' %}
<button id="btnGroup" type="button" class="btn btn-info mb-2">Grouper par coureur</button>
{% endif %}
{% if classes %}
<label for="filterClasse" class="mb-0 mr-2">Filtrer par classe :</label>
<select id="filterClasse" class="form-control d-inline-block mr-2" style="width:auto;">
<option value="">Toutes</option>
{% for classe in classes %}
<option value="{{ classe }}">{{ classe }}</option>
{% endfor %}
</select>
{% endif %}
{% if export_options %}
<!-- Button to open exports modal -->
<button id="btnMoreExports" type="button" class="btn btn-secondary mb-2" data-toggle="modal" data-target="#exportsModal">
<i class="fas fa-file-export"></i>
</button>
<!-- Exports modal -->
<div class="modal fade" id="exportsModal" tabindex="-1" role="dialog" aria-labelledby="exportsModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportsModalLabel">Choisir un export</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="list-group">
{% for opt in export_options %}
{% if opt.url_name == 'export_csv' %}
{% url 'export_csv' course.id as opt_url %}
{% elif opt.url_name == 'export_pdf' %}
{% url 'export_pdf' course.id as opt_url %}
{% else %}
{% url opt.url_name course.id as opt_url %}
{% endif %}
<div class="d-flex align-items-center justify-content-between">
<button type="button" class="list-group-item list-group-item-action export-option-btn flex-grow-1 mr-2" data-url="{{ opt_url }}">{{ opt.label }}</button>
<button type="button" class="btn btn-sm btn-outline-secondary help-btn" data-help="{{ opt.help|escapejs }}" title="Aide">?</button>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped" id="arriveesTable">
<thead>
<tr>
<th style="width: 1%; white-space: nowrap;">{% if course.type == 'multi' %}Tour{% else %}Rang{% endif %}</th>
<th>Nom</th>
<th>Prénom</th>
<th>Classe</th>
<th>Temps</th>
</tr>
</thead>
<tbody>
{% for a in arrivees %}
<tr>
<td>{% if course.type == 'multi' %}{{ a.tour }}{% else %}{{ a.rang }}{% endif %}</td>
<td>{{ a.coureur.nom }}</td>
<td>{{ a.coureur.prenom }}</td>
<td>{{ a.coureur.classe }}</td>
<td>{% if a.temps %}{{ a.temps|seconds_to_hms }}{% endif %}</td>
</tr>
{% empty %}
<tr>
<td>Aucun coureur arrivé.</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- DataTables JS & CSS -->
<link rel="stylesheet" href="{% static 'bootstrap/dataTables.bootstrap4.min.css' %}">
<script src="{% static 'jquery/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'bootstrap/dataTables.bootstrap4.min.js' %}"></script>
<script src="{% static 'jquery/datatables.fr.js' %}"></script>
<script>
// Style for inserted group header rows
$('<style>').prop('type', 'text/css').html('\n #arriveesTable tbody tr.group td { background: #f8f9fa; font-weight: 600; }\n').appendTo('head');
const courseId = "{{ course.id }}";
const courseType = "{{ course.type }}";
const wsScheme = window.location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${wsScheme}://${window.location.host}/ws/course/${courseId}/`;
const socket = new WebSocket(wsUrl);
socket.onmessage = function(e) {
// Ajoute dynamiquement la nouvelle ligne reçue via WebSocket
let data;
try {
data = JSON.parse(e.data);
} catch {
return;
}
// Vérifie le format des données reçues
let rowData;
if (typeof data === 'object' && data !== null) {
// Transforme l'objet en tableau dans l'ordre attendu (tour/rang, nom, prenom, classe, temps)
let firstCell = (courseType === 'multi') ? data.tour : (data.rang || data.tour);
rowData = [
firstCell,
data.nom || (data.coureur && data.coureur.nom),
data.prenom || (data.coureur && data.coureur.prenom),
data.classe || (data.coureur && data.coureur.classe),
data.temps
];
} else {
// Format inconnu, ignore
return;
}
var dt = $('#arriveesTable').DataTable();
dt.row.add(rowData).draw(false);
};
// Modal confirmation fin de course (attach handler only if button exists)
var finishBtn = document.getElementById('btnFinish');
if (finishBtn) {
finishBtn.addEventListener('click', function() {
$('#finishModal').modal('show');
});
}
// Initialisation DataTables au chargement
$(document).ready(function() {
// Initialize DataTable with options
var groupBy = false;
var table = $('#arriveesTable').DataTable({
order: [],
pageLength: 25,
drawCallback: function(settings) {
var api = this.api();
// Remove previously inserted group rows
api.rows().nodes().to$().filter('tr.group').remove();
if (!groupBy) return;
var rows = api.rows({page:'current'}).nodes();
var last = null;
api.column(1, {page:'current'} ).data().each(function(name, i){
var prenom = api.column(2, {page:'current'} ).data()[i];
var key = name + '|' + prenom;
if (last !== key) {
$(rows).eq(i).before(
'<tr class="group"><td colspan="5">'+ $('<div>').text(name + ' ' + prenom).html() +'</td></tr>'
);
last = key;
}
});
}
});
// Keep the current ordering so we can restore it
var storedOrder = table.order();
// Expose grouping state to the global scope so exports can read it
window.courseGroupBy = groupBy;
$('#btnGroup').on('click', function(){
groupBy = !groupBy;
window.courseGroupBy = groupBy;
$(this).text(groupBy ? 'Désactiver le groupement' : 'Grouper par coureur');
if (groupBy) {
// Sort by Nom (col 1) then Prénom (col 2) to make grouping contiguous
table.order([[1, 'asc'], [2, 'asc']]);
} else {
// Restore previous ordering
table.order(storedOrder);
}
table.draw();
});
// Filtre par classe (colonne 3) — comportement identique à dossards.html
$('#filterClasse').on('change', function() {
var val = $(this).val();
if (val) {
table.column(3).search('^'+val+'$', true, false).draw();
} else {
table.column(3).search('').draw();
}
// If grouping/selection logic needs to mark visible rows as selected, do it here
try {
if (table.rows) {
// select all visible rows to match dossards behaviour
table.rows({search:'applied'}).select();
}
} catch (e) {}
});
});
// Export CSV/PDF des données filtrées
function getVisibleRows() {
var dt = $('#arriveesTable').DataTable();
var rows = dt.rows({search: 'applied'}).data().toArray();
// If grouping is active, inject group-header marker rows into the exported data
try {
if (window.courseGroupBy) {
var output = [];
var lastKey = null;
for (var i = 0; i < rows.length; i++) {
var name = rows[i][1] || '';
var prenom = rows[i][2] || '';
var key = name + '|' + prenom;
if (key !== lastKey) {
// marker row: first cell '__GROUP__', second cell holds the group label
output.push(['__GROUP__', name + ' ' + prenom, '', '', '']);
lastKey = key;
}
output.push(rows[i]);
}
return JSON.stringify(output);
}
} catch (e) {
// fallback to default rows if anything goes wrong
}
return JSON.stringify(rows);
}
// Handle custom export option buttons in the modal
$('.export-option-btn').on('click', function() {
var url = $(this).data('url');
if (!url) return;
// Create a temporary form and submit POST with rows
var form = $('<form method="post"></form>');
form.attr('action', url);
// Insert CSRF token from cookie
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrf = getCookie('csrftoken');
if (csrf) {
form.append($('<input type="hidden" name="csrfmiddlewaretoken">').val(csrf));
}
var input = $('<input type="hidden" name="rows">').val(getVisibleRows());
form.append(input);
// Append to body and submit
$('body').append(form);
form.submit();
});
// Help button handler: show help text in a small modal
$(document).on('click', '.help-btn', function(e){
e.stopPropagation(); // prevent triggering parent button
var helpText = $(this).data('help') || '';
var modalHtml = '\n <div class="modal fade" id="exportHelpModal" tabindex="-1" role="dialog" aria-hidden="true">\n <div class="modal-dialog" role="document">\n <div class="modal-content">\n <div class="modal-header">\n <h5 class="modal-title">Détails de l\'export</h5>\n <button type="button" class="close" data-dismiss="modal" aria-label="Close">\n <span aria-hidden="true">&times;</span>\n </button>\n </div>\n <div class="modal-body">\n ' + $('<div>').text(helpText).html() + '\n </div>\n <div class="modal-footer">\n <button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>\n </div>\n </div>\n </div>\n </div>';
// Remove any existing help modal to avoid duplicates
$('#exportHelpModal').remove();
$('body').append(modalHtml);
$('#exportHelpModal').modal('show');
});
</script>
{% endblock %}