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.
 
 
 
 
 

258 lines
10 KiB

{% extends 'base.html' %}
{% block content %}
{% load static %}
<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">Scanner un coureur</h6>
</div>
<div class="card-body">
{% if course and course.fin %}
<div class="alert alert-danger text-center mb-0">Cette course est terminée. Le scan n'est plus possible.</div>
{% elif course and not course.depart %}
<div class="alert alert-warning text-center mb-0">Cette course n'a pas encore démarré. Le scan n'est pas possible.</div>
{% else %}
<div id="reader" style="width:100%; max-width:400px; margin:auto;"></div>
<div class="mt-4 position-relative" style="max-width:400px; margin:auto;">
<label id="manualCoureurLabel">Saisie manuelle :</label>
<div class="input-group align-items-center">
<input type="text" id="manualCoureurInput" class="form-control" placeholder="Nom ou dossard..." autocomplete="off" aria-label="Saisir un coureur" aria-describedby="manualCoureurLabel">
<!-- plus de bouton OK, déclenchement direct sur sélection -->
</div>
<div id="manualCoureurSuggestions" class="list-group position-absolute w-100" style="z-index:1000;"></div>
</div>
<div id="scanResult" class="mt-3"></div>
{% if error %}
<div class="alert alert-danger mt-3">{{ error }}</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Actions</h6>
</div>
<div class="card-body d-flex justify-content-center gap-3">
<a href="/" class="btn btn-secondary" title="Accueil">
<i class="fas fa-home mx-auto"></i>
</a>&nbsp;
<button id="toggleBeep" type="button" class="btn btn-info " title="Bip scan" >
<i id="beepIcon" class="fas fa-volume-up mx-auto"></i>
</button>&nbsp;
<button id="showCameras" type="button" class="btn btn-warning " title="Changer de caméra" data-toggle="modal" data-target="#cameraModal">
<i class="fas fa-camera mx-auto"></i>
</button>
</div>
<!-- Modal pour la liste des caméras -->
<div class="modal fade" id="cameraModal" tabindex="-1" role="dialog" aria-labelledby="cameraModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cameraModalLabel">Sélectionner une caméra</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul id="cameraList" class="list-group"></ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'html5-qrcode/html5-qrcode.min.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let selectedCoureurId = null;
const input = document.getElementById('manualCoureurInput');
const suggestions = document.getElementById('manualCoureurSuggestions');
if (input) {
input.addEventListener('input', function() {
const val = this.value.trim();
selectedCoureurId = null;
suggestions.innerHTML = '';
if (val.length < 2) return;
fetch('/ajax/coureur_autocomplete/?q=' + encodeURIComponent(val))
.then(r => r.json())
.then(data => {
suggestions.innerHTML = '';
data.forEach(c => {
const item = document.createElement('a');
item.className = 'list-group-item list-group-item-action';
item.textContent = c.label;
item.style.cursor = 'pointer';
item.onclick = function() {
input.value = c.label;
selectedCoureurId = c.id;
suggestions.innerHTML = '';
onScanSuccess(selectedCoureurId, "");
setTimeout(() => { input.value = ''; }, 100);
};
suggestions.appendChild(item);
});
});
});
// Cacher suggestions si clic ailleurs
document.addEventListener('click', function(e) {
if (!input.contains(e.target) && !suggestions.contains(e.target)) {
suggestions.innerHTML = '';
}
});
}
// plus de bouton OK, plus de gestion de clic
});
function playSound(type) {
if (typeof window.beepEnabled !== 'undefined' && !window.beepEnabled) return;
let audio;
if (type === 'ok') {
audio = new Audio("{% static 'mp3/ok.mp3' %}");
} else {
audio = new Audio("{% static 'mp3/error.mp3' %}");
}
audio.play();
}
let lastScanned = '';
let html5Qrcode;
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 = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let camerasAvailable = [];
let currentCameraId = null;
document.addEventListener('DOMContentLoaded', function() {
html5Qrcode = new Html5Qrcode("reader");
Html5Qrcode.getCameras().then(cameras => {
camerasAvailable = cameras || [];
if (camerasAvailable.length) {
currentCameraId = camerasAvailable[0].id;
html5Qrcode.start(
currentCameraId,
{ fps: 10, qrbox: 250 },
onScanSuccess
);
}
});
// Remplir la liste des caméras à chaque ouverture du modal
$('#cameraModal').on('show.bs.modal', function () {
const cameraList = document.getElementById('cameraList');
cameraList.innerHTML = '';
camerasAvailable.forEach((cam, idx) => {
const li = document.createElement('li');
li.className = 'list-group-item list-group-item-action';
li.textContent = cam.label || `Caméra ${idx+1}`;
li.style.cursor = 'pointer';
li.onclick = function() {
if (currentCameraId !== cam.id) {
html5Qrcode.stop().then(() => {
currentCameraId = cam.id;
html5Qrcode.start(
currentCameraId,
{ fps: 10, qrbox: 250 },
onScanSuccess
);
});
}
// Fermer le modal
$('#cameraModal').modal('hide');
};
cameraList.appendChild(li);
});
});
});
function getCourseIdFromUrl() {
const params = new URLSearchParams(window.location.search);
return params.get('course_id');
}
function onScanSuccess(decodedText, decodedResult) {
console.log('Scan détecté :', decodedText, 'Course:', getCourseIdFromUrl());
if (decodedText === lastScanned || window.scanDebounce) return;
window.scanDebounce = true;
lastScanned = decodedText;
const courseId = getCourseIdFromUrl();
if (!courseId) {
window.scanDebounce = false;
return;
}
fetch("{% url 'scan' %}" + window.location.search, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': getCookie('csrftoken'),
'X-Requested-With': 'XMLHttpRequest'
},
body: `course_id=${courseId}&qrcode=${encodeURIComponent(decodedText)}`
})
.then(response => response.text())
.then(html => {
document.getElementById('scanResult').innerHTML = html;
// Détecte succès ou erreur selon la présence d'une classe d'erreur dans la réponse
if (html.includes('alert-danger')) {
playSound('error');
} else {
playSound('ok');
}
setTimeout(function() {
window.scanDebounce = false;
}, 100); // 100ms de délai avant d'autoriser un nouveau scan
})
.catch(() => {
playSound('error');
window.scanDebounce = false;
});
}
document.addEventListener('DOMContentLoaded', function() {
const beepBtn = document.getElementById('toggleBeep');
const beepIcon = document.getElementById('beepIcon');
window.beepEnabled = true;
beepBtn.onclick = function() {
window.beepEnabled = !window.beepEnabled;
if (window.beepEnabled) {
beepIcon.classList.remove('fa-volume-mute');
beepIcon.classList.add('fa-volume-up');
beepBtn.title = 'Désactiver bip scan';
} else {
beepIcon.classList.remove('fa-volume-up');
beepIcon.classList.add('fa-volume-mute');
beepBtn.title = 'Activer bip scan';
}
};
});
Html5Qrcode.getCameras().then(cameras => {
if (cameras && cameras.length) {
html5Qrcode.start(
cameras[0].id,
{ fps: 10, qrbox: 250 },
onScanSuccess
);
}
});
</script>
{% endblock %}