Projette les cibles en temps réel et corrige le fuseau horaire

Calcule les heures travaillées et cibles d'entrée/sortie même quand une
demi-journée est en cours (pas encore de pointage de sortie), au lieu
d'attendre la fin de journée. Ajoute un endpoint /refresh pour
rafraîchir l'affichage sans repointer. Désambiguïse les libellés de
semaine sur plusieurs années et force TZ=Europe/Paris dans le conteneur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2026-07-16 14:12:03 +02:00
parent b6e0fcd925
commit 761a86986a
8 changed files with 400 additions and 24 deletions

View File

@ -204,6 +204,117 @@
})();
</script>
<script>
// Alerte départ — surveille l'heure cible du jour courant
(function () {
var _alerted = false;
var _lastCible = null;
function todayStr() {
var d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
function getCible() {
var el = document.querySelector('#calc-' + todayStr() + '-cible .cible-num');
if (!el) return null;
var txt = el.textContent.trim();
// Formats: "17:13" ou "16:15→17:00" — on veut l'heure de DÉPART (dernière)
var time = txt.indexOf('→') >= 0 ? txt.split('→').pop().trim() : txt;
return /^\d{2}:\d{2}$/.test(time) ? time : null;
}
function nowMin() {
var d = new Date();
return d.getHours() * 60 + d.getMinutes();
}
function toMin(t) {
var p = t.split(':');
return parseInt(p[0], 10) * 60 + parseInt(p[1], 10);
}
function getSolde() {
var el = document.querySelector('#soldes-section .solde-item:last-child .solde-num');
return el ? el.textContent.trim() : null;
}
function showAlert(time) {
if (document.getElementById('alerte-depart')) return;
var solde = getSolde();
var soldeHtml = '';
if (solde && solde.charAt(0) === '+' && solde !== '+0h00') {
soldeHtml = '<div class="alerte-supp">' + solde + ' de supp.</div>';
}
var notifBody = 'Heure de départ : ' + time;
if (solde && solde.charAt(0) === '+') notifBody += ' · ' + solde + ' de supp.';
var el = document.createElement('div');
el.id = 'alerte-depart';
el.innerHTML =
'<div class="alerte-inner">' +
'<span class="alerte-icon">⏰</span>' +
'<div class="alerte-body">' +
'<div class="alerte-label">Il faut partir</div>' +
'<div class="alerte-time">' + time + '</div>' +
soldeHtml +
'</div>' +
'<button class="alerte-close" title="Fermer" ' +
'onclick="document.getElementById(\'alerte-depart\').remove();' +
'window._alerteDismissed=true;">✕</button>' +
'</div>';
document.body.appendChild(el);
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('⏰ Il faut partir !', { body: notifBody, tag: 'depart' });
}
}
function check() {
if (window._alerteDismissed) return;
var cible = getCible();
if (!cible) return;
if (cible !== _lastCible) {
_lastCible = cible;
if (toMin(cible) > nowMin()) _alerted = false;
}
if (!_alerted && nowMin() >= toMin(cible)) {
_alerted = true;
showAlert(cible);
}
}
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
check();
setInterval(check, 30000);
})();
</script>
<script>
// Rafraîchit automatiquement les compteurs (travaillé/delta/soldes) du jour courant
(function () {
function todayStr() {
var d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
function refresh() {
var date = todayStr();
if (!document.getElementById('row-' + date)) return;
htmx.ajax('GET', '/refresh/' + date, { swap: 'none' });
}
setInterval(refresh, 60000);
})();
</script>
<script>
// Live warn on time inputs that violate minimum directives
document.addEventListener('input', function(e) {