Rend les plages horaires et les rappels ntfy configurables, supprime le

classement, ajoute une suite de tests pytest

- calcul.py: matin_debut/fin, aprem_debut/fin et pause_dejeuner_fin sont
  paramétrables via config.json > plages (moteur de calcul de l'heure de
  sortie optimale inclus, pas seulement l'affichage). Défauts identiques
  à l'ancien comportement, validé par la suite de tests.
- stats.py: les seuils de conformité utilisent la même config.
- main.py/templates: warn de saisie et suffixes de cible dynamiques.
- Rappels ntfy: serveur, topic et plage horaire (rappel_debut_h/fin_h)
  configurables par utilisateur depuis /settings, plus en dur dans le code.
- Page classement supprimée (pas de compétition entre utilisateurs).
- Nouvelle suite pytest (app/tests/), à lancer via
  `docker compose run --rm pointeuse pytest -v`.
This commit is contained in:
Antoine
2026-07-17 22:40:55 +02:00
parent 0b47e4a625
commit c2d7d0e3ac
19 changed files with 419 additions and 415 deletions

View File

@ -3,7 +3,7 @@ import statistics as _st
from datetime import datetime, timedelta, date as _date
from models import load_week_pointages, load_conges, list_all_weeks, load_config
from calcul import hhmm_to_minutes, minutes_to_hhmm, heures_travaillees, heures_dues
from calcul import hhmm_to_minutes, minutes_to_hhmm, heures_travaillees, heures_dues, DEFAULT_PLAGES
MOIS_FR = [
"janvier", "février", "mars", "avril", "mai", "juin",
@ -21,7 +21,7 @@ def _to_hhmm(minutes_float: float) -> str:
return f"{m // 60:02d}:{m % 60:02d}"
def _day_compliant(p: dict, mc: bool, ac: bool) -> bool | None:
def _day_compliant(p: dict, mc: bool, ac: bool, plages: dict) -> bool | None:
if mc and ac:
return None
matin_ok = mc or (bool(p.get("matin_entree")) and bool(p.get("matin_sortie")))
@ -29,24 +29,24 @@ def _day_compliant(p: dict, mc: bool, ac: bool) -> bool | None:
if not (matin_ok and aprem_ok):
return None
if not mc:
if p.get("matin_entree") and hhmm_to_minutes(p["matin_entree"]) > 9 * 60:
if p.get("matin_entree") and hhmm_to_minutes(p["matin_entree"]) > hhmm_to_minutes(plages["matin_debut"]):
return False
if p.get("matin_sortie") and hhmm_to_minutes(p["matin_sortie"]) < 12 * 60:
if p.get("matin_sortie") and hhmm_to_minutes(p["matin_sortie"]) < hhmm_to_minutes(plages["matin_fin"]):
return False
if not ac:
if p.get("aprem_entree") and hhmm_to_minutes(p["aprem_entree"]) > 14 * 60:
if p.get("aprem_entree") and hhmm_to_minutes(p["aprem_entree"]) > hhmm_to_minutes(plages["aprem_debut"]):
return False
if p.get("aprem_sortie") and hhmm_to_minutes(p["aprem_sortie"]) < 17 * 60:
if p.get("aprem_sortie") and hhmm_to_minutes(p["aprem_sortie"]) < hhmm_to_minutes(plages["aprem_fin"]):
return False
return True
def _compliance_over(all_days: list[tuple], cg_set: set) -> dict:
def _compliance_over(all_days: list[tuple], cg_set: set, plages: dict) -> dict:
n = n_ok = 0
for date, p in all_days:
mc = (date, "matin") in cg_set or (date, "jour") in cg_set
ac = (date, "aprem") in cg_set or (date, "jour") in cg_set
result = _day_compliant(p, mc, ac)
result = _day_compliant(p, mc, ac, plages)
if result is None:
continue
n += 1
@ -58,7 +58,9 @@ def _compliance_over(all_days: list[tuple], cg_set: set) -> dict:
def compute_all_stats(user_id: str = "") -> dict:
all_weeks = sorted(list_all_weeks(user_id))
conges = load_conges(user_id)
h_jour = int(float(load_config().get("heures_jour", 7.8)) * 60)
cfg = load_config()
h_jour = int(float(cfg.get("heures_jour", 7.8)) * 60)
plages = {**DEFAULT_PLAGES, **cfg.get("plages", {})}
cg_set = {(c["date"], c["type"]) for c in conges}
today = _date.today()
@ -73,10 +75,10 @@ def compute_all_stats(user_id: str = "") -> dict:
comp_ok = {s: 0 for s in SLOTS}
TARGETS = {
"matin_entree": ("le", 9 * 60),
"matin_sortie": ("ge", 12 * 60),
"aprem_entree": ("le", 14 * 60),
"aprem_sortie": ("ge", 17 * 60),
"matin_entree": ("le", hhmm_to_minutes(plages["matin_debut"])),
"matin_sortie": ("ge", hhmm_to_minutes(plages["matin_fin"])),
"aprem_entree": ("le", hhmm_to_minutes(plages["aprem_debut"])),
"aprem_sortie": ("ge", hhmm_to_minutes(plages["aprem_fin"])),
}
weekly_balances = []
@ -177,13 +179,14 @@ def compute_all_stats(user_id: str = "") -> dict:
deltas = [b["delta_min"] for b in weekly_balances]
avg_delta_min = round(_st.mean(deltas)) if deltas else 0
comp_globale = _compliance_over(all_days, cg_set)
comp_mois = _compliance_over(month_days, cg_set)
comp_sem_n1 = _compliance_over(prev_week_day_list, cg_set)
comp_globale = _compliance_over(all_days, cg_set, plages)
comp_mois = _compliance_over(month_days, cg_set, plages)
comp_sem_n1 = _compliance_over(prev_week_day_list, cg_set, plages)
return {
"time_stats": time_stats,
"compliance": compliance,
"plages": plages,
"weekly_balances": weekly_balances,
"total_weeks": len(weekly_balances),
"n_jours": n_jours,