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:
@ -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 → 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
|
||||
|
||||
Reference in New Issue
Block a user