Astreintes : interventions dans un menu dépliant fermé, cadre élargi

Les heures d'intervention (début, fin, description) passent en trois
colonnes alignées, cachées par défaut derrière un bouton « N
interventions » ; l'heure de retour et le total restent visibles.
L'état déplié survit aux rafraîchissements HTMX. Le conteneur passe
de 1100 à 1400 px pour que les lignes ne dépassent plus du cadre.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-08 22:40:06 +02:00
parent c208a17eec
commit 83f5512cb6
5 changed files with 147 additions and 64 deletions

View File

@ -182,8 +182,9 @@ demi-journées restantes.
## Astreinte & repos de 11h
Le bouton **AS** marque un jour d'astreinte. Ce jour et son lendemain (la nuit qui
suit), une liste d'événements apparaît sous l'après-midi : autant de lignes que
nécessaire (**+ intervention**), chacune avec un **début**, une **fin** et une
suit), un menu dépliant apparaît sous l'après-midi, fermé par défaut (seuls
l'heure de retour et le total restent visibles). Déplié, il offre autant de lignes
que nécessaire (**+ intervention**) sur trois colonnes : **début**, **fin** et
description libre. Une fin antérieure au début (fin < début) désigne une
intervention à cheval sur minuit.

View File

@ -452,22 +452,23 @@ def _btns_html(date, jour):
def _as_event_html(date, ev):
"""Une ligne d'intervention d'astreinte : début → fin, description, suppression."""
"""Une ligne d'intervention d'astreinte : début, fin, description (3 colonnes
de la grille), suppression."""
return (
f'<div class="as-event">'
f'<input class="time-input" type="time" name="astreinte_debut" data-date="{_e(date)}" value="{_e(ev.get("debut") or "")}">'
f'<span class="time-sep">→</span>'
f'<input class="time-input" type="time" name="astreinte_fin" data-date="{_e(date)}" value="{_e(ev.get("fin") or "")}">'
f'<div class="as-desc-cell">'
f'<input type="text" class="as-desc" name="astreinte_desc" data-date="{_e(date)}" maxlength="200" placeholder="description" value="{_e(ev.get("desc") or "")}">'
f'<button type="button" class="as-del" title="Supprimer" onclick="this.closest(\'.as-event\').remove()">×</button>'
f'</div>'
f'</div></div>'
)
def _as_block_html(date, jour):
"""Bloc astreinte d'une ligne : événements d'intervention (début/fin, nombre
libre, description), qui déclenchent les 11h de repos et décalent l'heure de
retour, plus le décompte du jour (compteur séparé, hors heures de semaine).
"""Bloc astreinte d'une ligne : les heures d'intervention (début, fin,
description) sont regroupées dans un menu dépliant fermé par défaut ; seuls
le bouton, l'heure de retour (11h de repos) et le décompte restent visibles.
Rendu sur les jours qualifiés (astreinte ou lendemain) ; div vide ailleurs."""
if not jour.get("astreinte_qualifiee"):
return f'<div id="as-block-{_e(date)}" hx-swap-oob="true"></div>'
@ -481,12 +482,22 @@ def _as_block_html(date, jour):
if jour.get("astreinte_min"):
total = (f'<span class="as-total" title="Compteur séparé, hors heures de semaine">'
f'astreinte : {_e(jour["astreinte_h"])}</span>')
nb = len(events)
label = f'{nb} intervention' + ('s' if nb != 1 else '')
return (
f'<div id="as-block-{_e(date)}" class="as-block" data-label="Astreinte" hx-swap-oob="true">'
f'<div class="as-events" id="as-events-{_e(date)}">{rows}</div>'
f'<div id="as-block-{_e(date)}" class="as-block" data-date="{_e(date)}" data-label="Astreinte" hx-swap-oob="true">'
f'<div class="as-head">'
f'<button type="button" class="as-toggle" data-date="{_e(date)}" aria-expanded="false" onclick="asToggle(this)">'
f'<span class="as-caret">▸</span> {label}</button>'
f'{hint}{total}'
f'</div>'
f'<div class="as-body">'
f'<div class="as-grid" id="as-events-{_e(date)}">'
f'<span class="as-th">Début</span><span class="as-th">Fin</span><span class="as-th">Description</span>'
f'{rows}'
f'</div>'
f'<button type="button" class="as-add" onclick="asAddEvent(\'{_e(date)}\')">+ intervention</button>'
f'{hint}'
f'{total}'
f'</div>'
f'</div>'
)

View File

@ -42,38 +42,50 @@
</div>
</div>
{% if j.astreinte_qualifiee %}
<div id="as-block-{{ j.date }}" class="as-block" data-label="Astreinte">
<div class="as-events" id="as-events-{{ j.date }}">
{% for ev in j.astreinte_events or [] %}
<div class="as-event">
<input class="time-input" type="time" name="astreinte_debut"
data-date="{{ j.date }}" value="{{ ev.debut or '' }}">
<span class="time-sep">→</span>
<input class="time-input" type="time" name="astreinte_fin"
data-date="{{ j.date }}" value="{{ ev.fin or '' }}">
<input type="text" class="as-desc" name="astreinte_desc"
data-date="{{ j.date }}" maxlength="200" placeholder="description"
value="{{ ev.desc or '' }}">
<button type="button" class="as-del" title="Supprimer"
onclick="this.closest('.as-event').remove()">×</button>
</div>
{% else %}
<div class="as-event">
<input class="time-input" type="time" name="astreinte_debut"
data-date="{{ j.date }}" value="">
<span class="time-sep">→</span>
<input class="time-input" type="time" name="astreinte_fin"
data-date="{{ j.date }}" value="">
<input type="text" class="as-desc" name="astreinte_desc"
data-date="{{ j.date }}" maxlength="200" placeholder="description" value="">
<button type="button" class="as-del" title="Supprimer"
onclick="this.closest('.as-event').remove()">×</button>
</div>
{% endfor %}
{% set nb_as = (j.astreinte_events or []) | length %}
<div id="as-block-{{ j.date }}" class="as-block" data-date="{{ j.date }}" data-label="Astreinte">
<div class="as-head">
<button type="button" class="as-toggle" data-date="{{ j.date }}" aria-expanded="false"
onclick="asToggle(this)">
<span class="as-caret">▸</span> {{ nb_as }} intervention{{ 's' if nb_as != 1 else '' }}
</button>
{% if j.retour_min is not none %}<span class="as-hint" title="11h de repos consécutif après la dernière activité">retour ≥ {{ j.retour }}</span>{% endif %}
{% if j.astreinte_min %}<span class="as-total" title="Compteur séparé, hors heures de semaine">astreinte : {{ j.astreinte_h }}</span>{% endif %}
</div>
<div class="as-body">
<div class="as-grid" id="as-events-{{ j.date }}">
<span class="as-th">Début</span><span class="as-th">Fin</span><span class="as-th">Description</span>
{% for ev in j.astreinte_events or [] %}
<div class="as-event">
<input class="time-input" type="time" name="astreinte_debut"
data-date="{{ j.date }}" value="{{ ev.debut or '' }}">
<input class="time-input" type="time" name="astreinte_fin"
data-date="{{ j.date }}" value="{{ ev.fin or '' }}">
<div class="as-desc-cell">
<input type="text" class="as-desc" name="astreinte_desc"
data-date="{{ j.date }}" maxlength="200" placeholder="description"
value="{{ ev.desc or '' }}">
<button type="button" class="as-del" title="Supprimer"
onclick="this.closest('.as-event').remove()">×</button>
</div>
</div>
{% else %}
<div class="as-event">
<input class="time-input" type="time" name="astreinte_debut"
data-date="{{ j.date }}" value="">
<input class="time-input" type="time" name="astreinte_fin"
data-date="{{ j.date }}" value="">
<div class="as-desc-cell">
<input type="text" class="as-desc" name="astreinte_desc"
data-date="{{ j.date }}" maxlength="200" placeholder="description" value="">
<button type="button" class="as-del" title="Supprimer"
onclick="this.closest('.as-event').remove()">×</button>
</div>
</div>
{% endfor %}
</div>
<button type="button" class="as-add" onclick="asAddEvent('{{ j.date }}')">+ intervention</button>
</div>
<button type="button" class="as-add" onclick="asAddEvent('{{ j.date }}')">+ intervention</button>
{% if j.retour_min is not none %}<span class="as-hint" title="11h de repos consécutif après la dernière activité">retour ≥ {{ j.retour }}</span>{% endif %}
{% if j.astreinte_min %}<span class="as-total" title="Compteur séparé, hors heures de semaine">astreinte : {{ j.astreinte_h }}</span>{% endif %}
</div>
{% else %}
<div id="as-block-{{ j.date }}"></div>

View File

@ -143,7 +143,7 @@
/* ── Layout ── */
.container {
max-width: 1100px;
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1.5rem;
display: flex;
@ -431,35 +431,62 @@
.btn-cg.btn-as { border-radius: 4px; }
.btn-cg.as-on { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Astreinte : opération hors horaires + heure de retour après 11h de repos */
/* Astreinte : heures d'intervention (début/fin/description) repliées dans un
menu dépliant fermé par défaut ; retour 11h et total restent visibles. */
.as-block {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
flex-direction: column;
gap: .3rem;
padding: .3rem .65rem .45rem;
border-top: 1px dashed var(--rule);
}
.as-label {
.as-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.as-toggle {
display: inline-flex;
align-items: center;
gap: .35rem;
border: 1px solid var(--rule);
background: transparent;
padding: .15rem .5rem;
cursor: pointer;
font-family: var(--mono);
font-size: 9px;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--accent-2);
white-space: nowrap;
transition: border-color .15s;
}
.as-toggle:hover { border-color: var(--accent-2); }
.as-caret { display: inline-block; transition: transform .15s; }
.as-block.open .as-caret { transform: rotate(90deg); }
.as-body { display: none; flex-direction: column; gap: .35rem; }
.as-block.open .as-body { display: flex; }
.as-grid {
display: grid;
grid-template-columns: 84px 84px minmax(110px, 1fr);
gap: 3px 6px;
align-items: center;
}
.as-grid .as-th {
font-family: var(--mono);
font-size: 9px;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--accent-2);
white-space: nowrap;
color: var(--muted);
}
.as-block .time-input { width: 68px; }
.as-grid .time-input { width: 100%; }
.as-event { display: contents; }
.as-desc-cell { display: flex; align-items: center; gap: 4px; min-width: 0; }
.as-hint {
font-family: var(--mono);
font-size: 10px;
color: var(--accent);
white-space: nowrap;
}
.as-events { display: flex; flex-direction: column; gap: 3px; width: 100%; }
.as-event { display: flex; align-items: center; gap: 4px; }
.as-desc {
flex: 1;
min-width: 60px;
min-width: 0;
border: 1px solid var(--rule);
border-radius: 0;
padding: .25rem .3rem;
@ -487,6 +514,7 @@
}
.as-del:hover { border-color: var(--neg); color: var(--neg); }
.as-add {
align-self: flex-start;
border: none;
background: none;
padding: 0;
@ -600,8 +628,8 @@
.pointage-table .btn-valider { width: 40px; height: 40px; font-size: 16px; }
.pointage-table .btn-cg { width: 34px; height: 34px; font-size: 11px; }
.pointage-table .btn-cg.btn-jour { width: 32px; height: 32px; }
.as-block { padding: .4rem .75rem .55rem; }
.as-block .time-input { width: 92px; }
.pointage-table .as-block { padding: .4rem .75rem .55rem; align-items: stretch; }
.as-grid { grid-template-columns: 82px 82px minmax(90px, 1fr); }
.as-desc { font-size: 13px; padding: .4rem .4rem; }
}

View File

@ -368,19 +368,50 @@ document.addEventListener('input', function(e) {
</script>
<script>
// Astreinte — ajoute une ligne d'intervention vierge dans le jour ciblé
// 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
// (le bloc entier est re-rendu côté serveur à chaque enregistrement).
var asOpen = {};
window.asToggle = function (btn) {
var block = document.getElementById('as-block-' + btn.dataset.date);
if (!block) return;
block.classList.toggle('open');
asSync(block);
};
function asSync(block) {
var open = block.classList.contains('open');
asOpen[block.dataset.date] = open;
var btn = block.querySelector('.as-toggle');
if (btn) btn.setAttribute('aria-expanded', String(open));
var caret = block.querySelector('.as-caret');
if (caret) caret.textContent = open ? '▾' : '▸';
}
function asRestore() {
document.querySelectorAll('.as-block[data-date]').forEach(function (block) {
if (asOpen[block.dataset.date]) block.classList.add('open');
asSync(block);
});
}
asRestore();
document.addEventListener('htmx:afterSwap', asRestore);
// Ajoute une ligne d'intervention vierge dans le jour ciblé
window.asAddEvent = function (date) {
var wrap = document.getElementById('as-events-' + date);
if (!wrap) return;
var grid = document.getElementById('as-events-' + date);
if (!grid) return;
var ev = document.createElement('div');
ev.className = 'as-event';
ev.innerHTML =
'<input class="time-input" type="time" name="astreinte_debut" data-date="' + date + '" value="">' +
'<span class="time-sep">→</span>' +
'<input class="time-input" type="time" name="astreinte_fin" data-date="' + date + '" value="">' +
'<input type="text" class="as-desc" name="astreinte_desc" data-date="' + date + '" maxlength="200" placeholder="description" value="">' +
'<button type="button" class="as-del" title="Supprimer" onclick="this.closest(\'.as-event\').remove()">×</button>';
wrap.appendChild(ev);
'<div class="as-desc-cell">' +
'<input type="text" class="as-desc" name="astreinte_desc" data-date="' + date + '" maxlength="200" placeholder="description" value="">' +
'<button type="button" class="as-del" title="Supprimer" onclick="this.closest(\'.as-event\').remove()">×</button>' +
'</div>';
grid.appendChild(ev);
};
</script>
{% endblock %}