Projette les cibles en temps réel et corrige le fuseau horaire
Calcule les heures travaillées et cibles d'entrée/sortie même quand une demi-journée est en cours (pas encore de pointage de sortie), au lieu d'attendre la fin de journée. Ajoute un endpoint /refresh pour rafraîchir l'affichage sans repointer. Désambiguïse les libellés de semaine sur plusieurs années et force TZ=Europe/Paris dans le conteneur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
"""Business logic: compute worked hours and expected hours per day/week."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
APREM_DEBUT_MIN = 13 * 60 + 30 # 13:30 — pause déjeuner de base 12h00-13h30
|
||||
|
||||
|
||||
def hhmm_to_minutes(val: str | None) -> int:
|
||||
if not val:
|
||||
@ -16,14 +18,26 @@ def minutes_to_hhmm(minutes: int) -> str:
|
||||
|
||||
|
||||
def heures_travaillees(p: dict) -> int:
|
||||
"""Return worked minutes for a pointage row."""
|
||||
matin = max(0, hhmm_to_minutes(p["matin_sortie"]) - hhmm_to_minutes(p["matin_entree"]))
|
||||
aprem = max(0, hhmm_to_minutes(p["aprem_sortie"]) - hhmm_to_minutes(p["aprem_entree"]))
|
||||
# only count if entry AND exit are present
|
||||
if not (p["matin_entree"] and p["matin_sortie"]):
|
||||
matin = 0
|
||||
if not (p["aprem_entree"] and p["aprem_sortie"]):
|
||||
aprem = 0
|
||||
"""Return worked minutes for a pointage row.
|
||||
|
||||
If a half-day has started but has no exit time yet, count elapsed time up to now
|
||||
(today only — past dates with missing exits are treated as 0).
|
||||
"""
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
now_min = datetime.today().hour * 60 + datetime.today().minute
|
||||
|
||||
matin = 0
|
||||
if p["matin_entree"] and p["matin_sortie"]:
|
||||
matin = max(0, hhmm_to_minutes(p["matin_sortie"]) - hhmm_to_minutes(p["matin_entree"]))
|
||||
elif p["matin_entree"] and p.get("date") == today:
|
||||
matin = max(0, now_min - hhmm_to_minutes(p["matin_entree"]))
|
||||
|
||||
aprem = 0
|
||||
if p["aprem_entree"] and p["aprem_sortie"]:
|
||||
aprem = max(0, hhmm_to_minutes(p["aprem_sortie"]) - hhmm_to_minutes(p["aprem_entree"]))
|
||||
elif p["aprem_entree"] and p.get("date") == today:
|
||||
aprem = max(0, now_min - hhmm_to_minutes(p["aprem_entree"]))
|
||||
|
||||
return matin + aprem
|
||||
|
||||
|
||||
@ -91,11 +105,29 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
||||
jour["cumul_visible"] = jour["is_complete"] and cumul_chain_ok
|
||||
|
||||
# Sortie cible: optimal aprem exit so the week totals exactly the target hours
|
||||
today = datetime.today().strftime("%Y-%m-%d")
|
||||
now_min = datetime.today().hour * 60 + datetime.today().minute
|
||||
worked_before = 0
|
||||
for i, jour in enumerate(jours):
|
||||
cg = jour["conge"]
|
||||
# Jour futur pas encore entamé : pas de cible, le rattrapage n'est pas figé sur ce jour précis
|
||||
is_future_untouched = jour["date"] > today and not jour.get("matin_entree") and not jour.get("aprem_entree")
|
||||
if is_future_untouched:
|
||||
jour["sortie_cible"] = None
|
||||
jour["entree_cible"] = None
|
||||
worked_before += jour["travaille_min"]
|
||||
continue
|
||||
if jour["du_min"] == 0 or "aprem" in cg or "jour" in cg:
|
||||
jour["sortie_cible"] = None
|
||||
jour["entree_cible"] = None
|
||||
if "aprem" in cg and "matin" not in cg and jour["du_min"] > 0:
|
||||
du_remaining_mat = sum(j["du_min"] for j in jours[i + 1:])
|
||||
if du_remaining_mat == 0 and not jour.get("matin_entree"):
|
||||
needed_today_mat = max(0, total_du - worked_before - du_remaining_mat)
|
||||
if needed_today_mat > 3 * 60: # plus que le créneau 9h-12h obligatoire
|
||||
entree_min = 12 * 60 - needed_today_mat
|
||||
if entree_min >= 6 * 60: # avant 9h obligatoire, pas avant 6h
|
||||
jour["entree_cible"] = f"{entree_min // 60:02d}:{entree_min % 60:02d}"
|
||||
worked_before += jour["travaille_min"]
|
||||
continue
|
||||
|
||||
@ -105,15 +137,29 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
||||
worked_morning = 0
|
||||
if jour.get("matin_entree") and jour.get("matin_sortie"):
|
||||
worked_morning = max(0, hhmm_to_minutes(jour["matin_sortie"]) - hhmm_to_minutes(jour["matin_entree"]))
|
||||
elif jour.get("matin_entree") and jour["date"] == today:
|
||||
# Pas de sortie pointée le matin : projeter une matinée normale jusqu'à midi
|
||||
# (sinon la cible dumperait tout le quota du jour sur l'après-midi dès l'arrivée).
|
||||
entree_min = hhmm_to_minutes(jour["matin_entree"])
|
||||
elapsed = max(0, now_min - entree_min)
|
||||
projected_to_noon = max(0, 12 * 60 - entree_min)
|
||||
worked_morning = max(elapsed, projected_to_noon)
|
||||
|
||||
needed_aprem = max(0, needed_today - worked_morning)
|
||||
aprem_start = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else 14 * 60
|
||||
aprem_start = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else APREM_DEBUT_MIN
|
||||
sortie_min = max(aprem_start + needed_aprem, 17 * 60)
|
||||
|
||||
if sortie_min > 23 * 60 + 59:
|
||||
jour["sortie_cible"] = None # deficit too large to recover in one afternoon
|
||||
else:
|
||||
jour["sortie_cible"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}"
|
||||
|
||||
entree_cible = None
|
||||
if du_remaining == 0 and not jour.get("aprem_entree") and needed_aprem > (17 * 60 - APREM_DEBUT_MIN):
|
||||
entree_min = 17 * 60 - needed_aprem
|
||||
if 12 * 60 < entree_min < APREM_DEBUT_MIN: # avant l'heure de reprise par défaut, après le déjeuner
|
||||
entree_cible = f"{entree_min // 60:02d}:{entree_min % 60:02d}"
|
||||
jour["entree_cible"] = entree_cible
|
||||
worked_before += jour["travaille_min"]
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user