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:
@ -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.
|
||||
|
||||
|
||||
31
app/main.py
31
app/main.py
@ -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>'
|
||||
)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user