Astreintes : sous-ligne pleine largeur sous la ligne du jour

Le bloc astreinte quitte la colonne après-midi (qui retrouvait une
hauteur normale quel que soit le contenu) pour une sous-ligne dédiée
sur toute la largeur du tableau, collée à la carte du jour sur mobile.
La sous-ligne se masque automatiquement quand le jour n'est pas
qualifié, y compris après les rafraîchissements HTMX.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-08 22:48:12 +02:00
parent 83f5512cb6
commit dc0e786428
4 changed files with 72 additions and 56 deletions

View File

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

View File

@ -471,7 +471,7 @@ def _as_block_html(date, jour):
le bouton, l'heure de retour (11h de repos) et le décompte restent visibles. 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.""" Rendu sur les jours qualifiés (astreinte ou lendemain) ; div vide ailleurs."""
if not jour.get("astreinte_qualifiee"): if not jour.get("astreinte_qualifiee"):
return f'<div id="as-block-{_e(date)}" hx-swap-oob="true"></div>' return f'<div id="as-block-{_e(date)}" class="as-block" hx-swap-oob="true"></div>'
events = jour.get("astreinte_events") or [] events = jour.get("astreinte_events") or []
rows = "".join(_as_event_html(date, ev) for ev in events) or _as_event_html(date, {}) rows = "".join(_as_event_html(date, ev) for ev in events) or _as_event_html(date, {})
hint = "" hint = ""

View File

@ -41,55 +41,6 @@
data-date="{{ j.date }}" value="{{ j.aprem_sortie or '' }}"> data-date="{{ j.date }}" value="{{ j.aprem_sortie or '' }}">
</div> </div>
</div> </div>
{% if j.astreinte_qualifiee %}
{% 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>
</div>
{% else %}
<div id="as-block-{{ j.date }}"></div>
{% endif %}
</td> </td>
<td class="col-calc" data-label="Travaillé"> <td class="col-calc" data-label="Travaillé">
@ -163,3 +114,57 @@
</div> </div>
</td> </td>
</tr> </tr>
{% if j.astreinte_qualifiee %}
{% set nb_as = (j.astreinte_events or []) | length %}
<tr class="as-row">
<td colspan="9">
<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>
</div>
</td>
</tr>
{% else %}
<tr class="as-row"><td colspan="9"><div id="as-block-{{ j.date }}" class="as-block"></div></td></tr>
{% endif %}

View File

@ -431,14 +431,17 @@
.btn-cg.btn-as { border-radius: 4px; } .btn-cg.btn-as { border-radius: 4px; }
.btn-cg.as-on { background: var(--accent); border-color: var(--accent); color: #fff; } .btn-cg.as-on { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Astreinte : heures d'intervention (début/fin/description) repliées dans un /* Astreinte : sous-ligne pleine largeur sous la ligne du jour (hors colonne
menu dépliant fermé par défaut ; retour 11h et total restent visibles. */ après-midi), heures d'intervention (début/fin/description) repliées dans un
menu dépliant fermé par défaut ; retour 11h et total restent visibles.
La sous-ligne se masque toute seule tant que le bloc est vide. */
.pointage-table tr.as-row > td { padding: 0; }
.pointage-table tr.as-row:has(.as-block:empty) { display: none; }
.as-block { .as-block {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: .3rem; gap: .3rem;
padding: .3rem .65rem .45rem; padding: .3rem .65rem .45rem;
border-top: 1px dashed var(--rule);
} }
.as-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; } .as-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.as-toggle { .as-toggle {
@ -580,6 +583,13 @@
background: var(--surface); background: var(--surface);
margin-bottom: .75rem; margin-bottom: .75rem;
} }
/* La sous-ligne astreinte prolonge la carte du jour : pas de bord haut,
marge haute négative pour absorber celle du jour. */
.pointage-table tbody tr.as-row {
border-top: none;
margin: -.75rem 0 .75rem 0;
}
.pointage-table tbody tr.as-row > td { padding: 0; }
.pointage-table tbody tr:not(:last-child) td { border-bottom: none; } .pointage-table tbody tr:not(:last-child) td { border-bottom: none; }
.pointage-table td { .pointage-table td {
border-bottom: 1px solid var(--rule-l) !important; border-bottom: 1px solid var(--rule-l) !important;