Cibles : reprise ancrée au présent et rafraîchissement en direct

L'heure de reprise de pause n'est qu'indicative : le jour même, une
fois l'heure désirée passée, la cible d'un après-midi non commencé
ancre la reprise à maintenant et la sortie s'ajuste au reste à faire —
suivre les cibles mène au solde 0 exact, et aucune cible ne recommande
de reprendre dans le passé.

La page semaine rafraîchit cibles, panneau Aujourd'hui et compteurs
toutes les 30 s, et immédiatement au retour sur l'onglet (les
minuteurs des navigateurs sont gelés en arrière-plan, l'affichage ne
peit plus rester périmé).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-11 14:29:50 +02:00
parent 45b450c1ca
commit e797098e79
5 changed files with 99 additions and 15 deletions

View File

@ -580,8 +580,7 @@ def test_en_cours_sature_matin_pas_de_double_comptage(monkeypatch):
dans le travaillé, une fois réintégré au plan — si bien que suivre les
cibles sur-livrait de 105 min. Désormais le créneau saturé propose
« sortie maintenant », ne réintègre rien, et l'après-midi est allégé
d'autant : suivi des cibles → solde 0 (à ~12 min près bornées par les
plages : reprise ≥ maintenant, plancher 17:00)."""
d'autant : suivi des cibles → solde 0 exactement."""
_Freezer.freeze(monkeypatch, 13, 45)
pointages = [
_pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"),
@ -595,16 +594,16 @@ def test_en_cours_sature_matin_pas_de_double_comptage(monkeypatch):
# Sortie du matin = maintenant (285 min déjà faites, créneau saturé)
assert vendredi["cible_matin"] == "13:45"
# L'après-midi absorbe l'excédent du matin : départ au plancher 17:00
# (nominal 288 − 78 de marge) et reprise retardée à maintenant.
assert vendredi["cible_aprem_entree"] == "13:45"
# (nominal 288 − 93 de marge), reprise ancrée à maintenant puis retardée
# de 12 min dans la marge avant le début de présence (14:00).
assert vendredi["cible_aprem_entree"] == "13:57"
assert vendredi["cible_aprem"] == "17:00"
# Suivre les cibles ne sur-livre plus de 105 min : reste ≤ 12 min
# (le solde résiduel vient des bornes de plages, pas du calcul).
# Suivre les cibles ne sur-livre plus de 105 min : solde exactement 0.
suivis = [dict(p) for p in pointages]
suivis[4] = _pointage("2026-07-17", "09:00", vendredi["cible_matin"],
vendredi["cible_aprem_entree"], vendredi["cible_aprem"])
apres = compute_week(suivis, [], HEURES_JOUR_MIN)
assert 0 <= apres["solde_min"] <= 12
assert apres["solde_min"] == 0
def test_en_cours_sature_aprem_propose_sortie_immediate(monkeypatch):
@ -649,6 +648,30 @@ def test_en_cours_non_sature_reste_inchange(monkeypatch):
assert apres["solde_min"] == 0
def test_reprise_desiree_depassee_ancree_a_maintenant(monkeypatch):
"""La reprise de pause (pause_dejeuner_fin) n'est qu'une heure désirée :
le jour même, quand elle est dépassée et que l'après-midi n'a pas commencé,
la cible doit ancrer la reprise à maintenant — jamais dans le passé — et
la sortie s'ajuste pour que le suivi donne exactement le solde visé."""
_Freezer.freeze(monkeypatch, 14, 12)
pointages = [
_pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-15", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-16", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-17", "09:00", "12:00"), # aujourd'hui 14:12, aprem non repris
]
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert vendredi["cible_aprem_entree"] == "14:12" # maintenant, pas 13:30
assert vendredi["cible_aprem"] == "19:00" # 14:12 + 234 nominal + 54 retard
suivis = [dict(p) for p in pointages]
suivis[4] = _pointage("2026-07-17", "09:00", "12:00",
vendredi["cible_aprem_entree"], vendredi["cible_aprem"])
apres = compute_week(suivis, [], HEURES_JOUR_MIN)
assert apres["solde_min"] == 0
def test_entree_apres_fin_matin_le_surplus_ne_double_compte_pas(monkeypatch):
"""Régression : entrée du matin pointée à 12:30 (après la fin de Présence
matin), sans sortie, il est 13:45. Aucun créneau matin n'existe (capacité

View File

@ -113,3 +113,40 @@ def test_forfait_bonifie_journee_affiche_dans_le_recap(monkeypatch):
assert "forfait 1h00 → 1h30 pondérées" in r.text # ligne du jour (bloc AS)
assert "forfaits 1h00 → 1h30 pondérés" in r.text # totaux du récapitulatif
assert "considéré réalisé en journée" in r.text # explication au survol
def test_refresh_regenerere_les_cibles_en_fonction_de_lheure(monkeypatch):
"""Le poll /refresh (toutes les 30 s, et au retour sur l'onglet) régénère
les cibles OOB avec l'heure courante : à 14:12, l'après-midi non commencé
du vendredi ancre sa reprise à maintenant — jamais à l'heure désirée 13:30
passée — et la sortie s'ajuste au reste à faire de la semaine."""
class _Frozen14(dt_module.datetime):
@classmethod
def today(cls):
return cls(2026, 7, 17, 14, 12)
monkeypatch.setattr(main, "datetime", _Frozen14)
import calcul
monkeypatch.setattr(calcul, "datetime", _Frozen14)
cookies = _login("u-ui-refresh")
# Lundi-jeudi complets, vendredi matin seulement : reste 4h42,
# seul créneau ouvert = vendredi aprem, reprise ancrée à 14:12.
for d in ("2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"):
client.post(f"/pointage/{d}", cookies=cookies,
data={"matin_entree": "09:00", "matin_sortie": "12:24",
"aprem_entree": "13:30", "aprem_sortie": "17:54"},
headers={"HX-Request": "true"}, follow_redirects=False)
client.post(f"/pointage/{TODAY}", cookies=cookies,
data={"matin_entree": "09:00", "matin_sortie": "12:00"},
headers={"HX-Request": "true"}, follow_redirects=False)
r = client.get(f"/refresh/{TODAY}", cookies=cookies, follow_redirects=False)
assert r.status_code == 200
# Cible du jour régénérée, ancrée à maintenant (14:12), pas à 13:30
assert f'id="calc-{TODAY}-cible"' in r.text
assert "A 14:12 → 19:00" in r.text
# Toutes les cibles de la semaine suivent (swaps OOB), pas seulement le jour
for d in ("2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"):
assert f'id="calc-{d}-cible"' in r.text
# Le panneau aujourd'hui est rafraîchi avec la même sortie cible
assert "today-panel" in r.text
assert "19:00" in r.text