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.
 
 
 
 
 

252 lines
11 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 %}
<form id="exportCsvForm" method="post" action="{% url 'export_csv' course.id %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="rows" id="csvRowsInput">
<button type="submit" class="btn btn-success mb-2" id="btnExportCsv">
<i class="fas fa-file-csv" title="Exporter en CSV"></i>
</button>
</form>
<form id="exportPdfForm" method="post" action="{% url 'export_pdf' course.id %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="rows" id="pdfRowsInput">
<button type="submit" class="btn btn-danger mb-2" id="btnExportPdf">
<i class="fas fa-file-pdf" title="Exporter en PDF"></i>
</button>
</form>
</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();
});
});
// 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);
}
$('#exportCsvForm').on('submit', function(e) {
$('#csvRowsInput').val(getVisibleRows());
});
$('#exportPdfForm').on('submit', function(e) {
$('#pdfRowsInput').val(getVisibleRows());
});
</script>
{% endblock %}