Refonte de l'interface : sauvegarde automatique, menu d'état unique et panneau aujourd'hui

- Chaque heure validée enregistre le jour (plus de bouton ✓), avec un témoin
  « enregistré » et conservation du focus entre les champs
- Les boutons MA/AM/J/F/AS deviennent un bouton d'état unique à pastilles
  ouvrant un menu déroulant (congés, férié, astreinte)
- Panneau Aujourd'hui : sortie cible en grand et progression travaillé/dû
  de la semaine, rafraîchis à chaque saisie
- Polish général : coins arrondis, ombres douces, hiérarchie des soldes,
  états de focus visibles

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-09 20:34:27 +02:00
parent 791b48b320
commit e9983a28c7
8 changed files with 565 additions and 98 deletions

View File

@ -54,8 +54,7 @@
<th class="g-calc" rowspan="2">Δ jour</th>
<th class="g-cumul" rowspan="2">Δ sem.</th>
<th class="g-cible" rowspan="2">Cible</th>
<th class="g-save" rowspan="2"></th>
<th class="g-conge" rowspan="2">Congé</th>
<th class="g-conge" rowspan="2">État</th>
</tr>
<tr class="th-sub">
<th class="s-matin">entrée &rarr; sortie</th>
@ -367,6 +366,106 @@ document.addEventListener('input', function(e) {
});
</script>
<script>
// Auto-enregistrement — chaque champ validé (change = blur, Entrée ou picker)
// programme la sauvegarde du jour ; un léger délai regroupe les saisies d'affilée.
// La requête part du témoin .autosave de la ligne (attributs hx-*), qui porte
// aussi les états « enregistrement… / ✓ enregistré ».
(function () {
var timer = null;
var pendingDate = null;
function triggerSave(date) {
var trig = document.getElementById('save-' + date);
if (trig) htmx.trigger(trig, 'save-day');
}
// Le swap OOB remplace les champs du jour : si le focus était dedans
// (ex. tabulation entrée → sortie), on le restaure sur le champ équivalent.
var focusKey = null;
document.addEventListener('change', function (e) {
var t = e.target;
if (!t.matches('.time-input, .as-desc')) return;
var date = t.dataset.date;
if (!date) return;
var a = document.activeElement;
if (a && a.dataset && a.dataset.date === date && a.name) {
focusKey = a.name + '|' + a.dataset.date;
}
pendingDate = date;
clearTimeout(timer);
timer = setTimeout(function () { triggerSave(pendingDate); }, 350);
});
document.body.addEventListener('htmx:afterSwap', function () {
if (!focusKey) return;
var parts = focusKey.split('|');
focusKey = null;
if (document.activeElement && document.activeElement !== document.body) return;
var el = document.querySelector('[name="' + parts[0] + '"][data-date="' + parts[1] + '"]');
if (el) el.focus();
});
document.body.addEventListener('htmx:beforeRequest', function (e) {
var el = e.target;
if (el.classList && el.classList.contains('autosave')) el.classList.add('busy');
});
document.body.addEventListener('htmx:afterOnLoad', function (e) {
var el = e.target;
if (!el.classList || !el.classList.contains('autosave')) return;
el.classList.remove('busy');
el.classList.add('ok');
clearTimeout(el._okTimer);
el._okTimer = setTimeout(function () { el.classList.remove('ok'); }, 2000);
});
document.body.addEventListener('htmx:responseError', function (e) {
var el = e.target;
if (el.classList && el.classList.contains('autosave')) el.classList.remove('busy');
});
})();
</script>
<script>
// Menu congé/férié/astreinte — un seul ouvert à la fois, fermeture au clic
// extérieur ; après une bascule, le swap OOB remplace le bouton (menu refermé).
// Le panneau s'ouvre en position fixe sous le bouton : ancré en absolute il
// serait rogné par le scroll horizontal du conteneur de la table.
(function () {
function placePop(d) {
var pop = d.querySelector('.cg-pop');
if (!pop) return;
if (d.open) {
var r = d.getBoundingClientRect();
pop.style.position = 'fixed';
pop.style.top = (r.bottom + 6) + 'px';
var w = Math.max(178, r.width);
pop.style.left = Math.max(8, Math.min(window.innerWidth - w - 8, r.right - w)) + 'px';
} else {
pop.style.position = '';
pop.style.top = '';
pop.style.left = '';
}
}
document.addEventListener('click', function (e) {
document.querySelectorAll('details.cg-menu[open]').forEach(function (d) {
if (!d.contains(e.target)) d.removeAttribute('open');
});
});
document.addEventListener('toggle', function (e) {
var d = e.target;
if (!(d instanceof HTMLDetailsElement) || !d.classList.contains('cg-menu')) return;
if (d.open) {
document.querySelectorAll('details.cg-menu[open]').forEach(function (o) {
if (o !== d) o.removeAttribute('open');
});
}
placePop(d);
}, true);
})();
</script>
<script>
// Astreinte — heures d'intervention dans un menu dépliant fermé par défaut.
// L'état ouvert est mémorisé par date et restauré après les swaps HTMX