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.
251 lines
11 KiB
251 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> |
|
<!-- 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">×</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 %} |