Ajoute les jours fériés, l'astreinte week-end 0h-0h et les ratios éditables

- Bouton F : marque un jour férié (dû 0, sans consommer de congé) ; il
  qualifie les règles « ferie » et étend la fenêtre d'astreinte du jour.
- Samedi, dimanche et fériés : l'astreinte compte de 0h00 à 0h00 ; un jour
  AS déclaré garde sa borne du lendemain 18h pour ne pas tronquer les
  interventions nocturnes.
- Ratios d'astreinte éditables à la main dans Réglages (nom, jours, fenêtre,
  ratio), avec ajout/suppression de règles et validation.
- Un quart d'heure entamé par deux interventions qui se suivent n'est compté
  qu'une fois : les lignes d'événements somment au total du jour.
- Décompte mieux expliqué dans l'interface : quarts d'horloge (:00/:15/:30/:45),
  fenêtre couverte par événement, exemple chiffré.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-08 23:30:15 +02:00
parent 23271d736d
commit dc80e81db2
11 changed files with 599 additions and 111 deletions

View File

@ -54,24 +54,74 @@
<span class="hist-label">Ratios des heures d'astreinte</span>
<p class="settings-p">
Pondération des heures d'astreinte selon le type de jour et la fenêtre horaire :
le décompte se fait par tranches de 15 min (toute tranche entamée compte en
totalité) et chaque tranche est multipliée par le ratio de la première règle qui
la couvre. Une tranche hors règle compte ×1 (« Autre »). Règles globales à tous
les comptes, ajustables dans <code>data/config.json</code> (clé
<code>astreinte_facteurs</code>).
le décompte se fait par quarts d'heure de l'horloge (:00, :15, :30, :45) — tout
quart entamé compte 15 min entières (ex. 22:39 → 23:49 touche les quarts 22:30 à
23:45, soit 6 quarts = 1h30) — puis chaque quart est multiplié par le ratio de la
première règle qui le couvre. Un quart hors règle compte ×1 (« Autre ») ; la
première règle qui couvre un quart gagne, place les fenêtres spécifiques avant
les générales. Samedi, dimanche et jours fériés comptent de 0h00 à 0h00. Règles
globales à tous les comptes.
</p>
<div class="as-ratios">
{% for f in facteurs %}
<div class="as-ratio-row">
<span class="as-ratio-nom">{{ f.nom }}</span>
<span class="as-ratio-jours">{% for j in f.jours %}{{ {'semaine': 'Lun–Ven', 'sam': 'Samedi', 'dim': 'Dimanche', 'ferie': 'Férié'}[j] }}{% if not loop.last %} · {% endif %}{% endfor %}</span>
<span class="as-ratio-fenetre">{{ f.debut }} → {{ f.fin }}</span>
<span class="as-ratio-facteur">×{{ '%g' % f.facteur }}</span>
{% if facteurs_saved %}
<div class="settings-saved">Enregistré.</div>
{% endif %}
{% if facteurs_error %}
<div class="settings-error">Règles invalides : chaque règle doit avoir un nom, au moins un type de jour, une fenêtre HH:MM et un ratio &gt; 0 ; au moins une règle est requise (efface le nom pour supprimer une règle).</div>
{% endif %}
<form method="post" action="/settings/astreinte-facteurs" class="as-facteurs-form">
<div id="as-ratio-rows">
{% for f in facteurs %}
{% set i = loop.index0 %}
<div class="as-ratio-edit" data-idx="{{ i }}">
<input type="text" class="login-input as-f-nom" name="f_nom_{{ i }}" value="{{ f.nom }}" maxlength="40" placeholder="Nom de la règle">
<div class="as-f-jours">
{% for j, lbl in [('semaine', 'Lun–Ven'), ('sam', 'Sam'), ('dim', 'Dim'), ('ferie', 'Férié')] %}
<label class="as-f-jour" title="Types de jours couverts par la règle"><input type="checkbox" name="f_jours_{{ i }}" value="{{ j }}" {% if j in f.jours %}checked{% endif %}> {{ lbl }}</label>
{% endfor %}
</div>
<input class="login-input time-range-input as-f-h" type="time" name="f_debut_{{ i }}" value="{{ f.debut }}" aria-label="Début de fenêtre">
<span class="settings-range-sep">→</span>
<input class="login-input time-range-input as-f-h" type="time" name="f_fin_{{ i }}" value="{{ f.fin }}" aria-label="Fin de fenêtre (avant début = à cheval sur minuit)">
<label class="as-f-x">×<input class="login-input as-f-facteur" type="number" name="f_facteur_{{ i }}" value="{{ '%g' % f.facteur }}" step="0.05" min="0.05" aria-label="Ratio"></label>
<button type="button" class="as-f-del" title="Supprimer la règle" onclick="asDelRatio(this)">×</button>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="as-ratio-actions">
<button type="button" class="as-f-add" onclick="asAddRatio()">+ règle</button>
<button class="login-btn" type="submit">Enregistrer les ratios</button>
</div>
</form>
</div>
<script>
function asAddRatio() {
const rows = document.getElementById('as-ratio-rows');
const idx = Math.max(-1, ...[...rows.querySelectorAll('.as-ratio-edit')].map(r => +r.dataset.idx)) + 1;
const div = document.createElement('div');
div.className = 'as-ratio-edit';
div.dataset.idx = idx;
div.innerHTML =
'<input type="text" class="login-input as-f-nom" name="f_nom_' + idx + '" maxlength="40" placeholder="Nom de la règle">' +
'<div class="as-f-jours">' +
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="semaine" checked> Lun–Ven</label>' +
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="sam"> Sam</label>' +
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="dim"> Dim</label>' +
'<label class="as-f-jour"><input type="checkbox" name="f_jours_' + idx + '" value="ferie"> Férié</label>' +
'</div>' +
'<input class="login-input time-range-input as-f-h" type="time" name="f_debut_' + idx + '" value="20:00">' +
'<span class="settings-range-sep">→</span>' +
'<input class="login-input time-range-input as-f-h" type="time" name="f_fin_' + idx + '" value="08:00">' +
'<label class="as-f-x">×<input class="login-input as-f-facteur" type="number" name="f_facteur_' + idx + '" value="1.5" step="0.05" min="0.05"></label>' +
'<button type="button" class="as-f-del" title="Supprimer la règle" onclick="asDelRatio(this)">×</button>';
rows.appendChild(div);
div.querySelector('.as-f-nom').focus();
}
function asDelRatio(btn) { btn.closest('.as-ratio-edit').remove(); }
</script>
<div class="settings-card">
<span class="hist-label">Forfait par jour d'astreinte</span>
<p class="settings-p">
@ -226,22 +276,47 @@
border: 1px solid var(--rule-l);
padding: 0 .25rem;
}
.as-ratios { border: 1px solid var(--rule-l); display: flex; flex-direction: column; }
.as-ratio-row {
display: grid;
grid-template-columns: minmax(110px, auto) 1fr auto auto;
gap: .5rem 1rem;
align-items: baseline;
padding: .45rem .8rem;
.as-facteurs-form { display: flex; flex-direction: column; gap: .5rem; }
.as-ratio-edit {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .5rem .75rem;
padding: .5rem .8rem;
border: 1px solid var(--rule-l);
}
.as-ratio-row + .as-ratio-row { border-top: 1px solid var(--rule-l); }
.as-ratio-nom { font-size: 13px; font-weight: 500; }
.as-ratio-jours { font-size: 12px; color: var(--muted); }
.as-ratio-fenetre { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.as-ratio-facteur { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--accent-2); }
.as-ratio-edit + .as-ratio-edit { margin-top: .4rem; }
.as-ratio-edit .as-f-nom { width: 140px; flex-shrink: 0; }
.as-f-jours { display: flex; gap: .75rem; flex-wrap: wrap; flex: 1; }
.as-f-jour { font-size: 12px; color: var(--text); display: inline-flex; align-items: center; gap: .3rem; white-space: nowrap; }
.as-ratio-edit .as-f-h { width: 90px; flex-shrink: 0; }
.as-f-x { font-family: var(--mono); color: var(--muted); display: inline-flex; align-items: center; gap: .3rem; }
.as-ratio-edit .as-f-facteur { width: 70px; }
.as-f-del {
border: 1px solid var(--rule);
background: var(--surface);
color: var(--muted);
border-radius: 4px;
width: 22px;
height: 22px;
cursor: pointer;
line-height: 1;
flex-shrink: 0;
}
.as-f-del:hover { color: var(--neg); border-color: var(--neg); }
.as-ratio-actions { display: flex; gap: .75rem; align-items: center; margin-top: .3rem; }
.as-f-add {
border: 1px dashed var(--rule);
background: none;
color: var(--muted);
border-radius: 4px;
padding: .35rem .7rem;
cursor: pointer;
font-size: 12px;
}
.as-f-add:hover { color: var(--accent); border-color: var(--accent); }
@media (max-width: 600px) {
.as-ratio-row { grid-template-columns: 1fr auto; row-gap: .1rem; }
.as-ratio-jours, .as-ratio-fenetre { grid-column: 1 / -1; }
.as-f-jours { order: 3; width: 100%; }
}
.settings-form { display: flex; flex-direction: column; gap: .5rem; max-width: 420px; }
.settings-saved {