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:
Jacquin Antoine
2026-09-09 20:34:27 +02:00
parent 791b48b320
commit e9983a28c7
8 changed files with 565 additions and 98 deletions

View File

@ -314,6 +314,21 @@ def _build_soldes_ctx(year, week, conges, h_jour, user_id):
]
result = compute_week(pointages, conges, h_jour, load_plages(user_id), load_astreintes(user_id), load_facteurs_astreinte(), load_feries(user_id))
result["heures_jour"] = minutes_to_hhmm(h_jour)
# Panneau « aujourd'hui » : uniquement si le jour courant appartient à la
# semaine affichée (re-rendu à chaque enregistrement via _soldes.html).
today_str = datetime.today().date().isoformat()
jour_today = next((j for j in result["jours"] if j["date"] == today_str), None)
if jour_today is not None:
trav_total = sum(j["travaille_min"] for j in result["jours"])
du_total = trav_total - result["solde_min"]
pct = min(100, round(trav_total * 100 / du_total)) if du_total > 0 else None
result["today"] = {
"nom": JOURS_FR[datetime.strptime(today_str, "%Y-%m-%d").weekday()],
"jour": jour_today,
"pct": pct,
"trav_total": minutes_to_hhmm(trav_total),
"du_total": minutes_to_hhmm(du_total),
}
return {"result": result}
@ -445,22 +460,48 @@ def _oob_time_cells(date, jour, plages):
def _btns_html(date, jour):
"""Boutons congé + astreinte + férié d'une ligne (cible du swap HTMX)."""
"""Bouton d'état d'une ligne (cible du swap HTMX) : pastilles résumant
congés / férié / astreinte, menu déroulant des cinq bascules. Doit rester
synchronisé avec le rendu Jinja de _ligne.html."""
cg = jour["conge"]
btn_ma_on = " on" if "matin" in cg else ""
btn_am_on = " on" if "aprem" in cg else ""
btn_j_on = " jour-on" if "jour" in cg else ""
btn_as_on = " as-on" if jour.get("astreinte") else ""
btn_f_on = " ferie-on" if jour.get("ferie") else ""
chips = []
if "jour" in cg:
chips.append('<span class="cg-chip st-jour">J</span>')
if "matin" in cg:
chips.append('<span class="cg-chip st-ma">MA</span>')
if "aprem" in cg:
chips.append('<span class="cg-chip st-am">AM</span>')
if jour.get("ferie"):
chips.append('<span class="cg-chip st-ferie">F</span>')
if jour.get("astreinte"):
chips.append('<span class="cg-chip st-as">AS</span>')
state = "".join(chips) or '<span class="cg-none">—</span>'
d = _e(date)
def opt(url, chip_cls, chip_txt, label, on, title=""):
t = f' title="{title}"' if title else ""
return (f'<button hx-post="{url}" hx-swap="none" class="cg-opt{" on" if on else ""}"{t}>'
f'<span class="cg-opt-chip {chip_cls}">{chip_txt}</span> {label}</button>')
return (
f'<div id="cg-btn-{_e(date)}" hx-swap-oob="true">'
f'<div class="conge-btns">'
f'<button hx-post="/conge/{_e(date)}/matin" hx-swap="none" class="btn-cg{btn_ma_on}" title="Congé matin">MA</button>'
f'<button hx-post="/conge/{_e(date)}/aprem" hx-swap="none" class="btn-cg{btn_am_on}" title="Congé après-midi">AM</button>'
f'<button hx-post="/conge/{_e(date)}/jour" hx-swap="none" class="btn-cg btn-jour{btn_j_on}" title="Journée entière">J</button>'
f'<button hx-post="/ferie/{_e(date)}/toggle" hx-swap="none" class="btn-cg btn-ferie{btn_f_on}" title="Jour férié (dû 0, astreinte comptée 0h00 → 0h00)">F</button>'
f'<button hx-post="/astreinte/{_e(date)}/toggle" hx-swap="none" class="btn-cg btn-as{btn_as_on}" title="Jour d&#39;astreinte">AS</button>'
f'</div></div>'
f'<div id="cg-btn-{d}" class="cg-cell" hx-swap-oob="true">'
f'<details class="cg-menu">'
f'<summary class="cg-trigger" title="Congés, jour férié, astreinte">'
f'<span class="cg-state">{state}</span>'
f'<span class="cg-caret">▾</span>'
f'</summary>'
f'<div class="cg-pop">'
+ opt(f"/conge/{d}/matin", "ma", "MA", "Congé matin", "matin" in cg)
+ opt(f"/conge/{d}/aprem", "am", "AM", "Congé après-midi", "aprem" in cg)
+ opt(f"/conge/{d}/jour", "jour", "J", "Journée entière", "jour" in cg)
+ '<div class="cg-opt-sep"></div>'
+ opt(f"/ferie/{d}/toggle", "ferie", "F", "Jour férié", jour.get("ferie"),
"Jour férié : dû 0, astreinte comptée 0h00 → 0h00")
+ opt(f"/astreinte/{d}/toggle", "as", "AS", "Jour d&#39;astreinte", jour.get("astreinte"),
"Jour d&#39;astreinte (18h → 18h, ou 0h → 0h sam/dim/férié)")
+ '</div>'
f'</details>'
f'</div>'
)