Astreintes : fenêtre 18h-18h, week-end affiché, forfait réglable
Un jour d'astreinte déclaré couvre désormais 18:00 → 18:00 le lendemain :
les interventions hors fenêtre ne comptent ni au décompte ni pour les
11h de repos, et deux astreintes consécutives se recollent sans trou.
La fin de la dernière intervention est arrondie au quart d'heure entamé
avant de poser le retour. Samedi et dimanche apparaissent dans la
semaine (heures dues nulles) pour pouvoir y déclarer des astreintes,
dans les exports aussi. Un réglage par utilisateur ajoute une durée
forfaitaire au temps travaillé de chaque jour d'astreinte déclaré.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
45
app/main.py
45
app/main.py
@ -32,7 +32,7 @@ from models import (
|
||||
from calcul import (
|
||||
compute_week, minutes_to_hhmm, minutes_to_time, hhmm_to_minutes, heures_dues,
|
||||
heures_travaillees, jour_astreinte_qualifie, exclusion_retour,
|
||||
nominaux_du_jour, retour_apres_repos, decompte_astreinte,
|
||||
nominaux_du_jour, retour_apres_repos, decompte_astreinte, fenetre_astreinte_jour,
|
||||
)
|
||||
from stats import compute_all_stats
|
||||
from export import export_semaine_ods, export_annee_ods
|
||||
@ -125,7 +125,7 @@ templates = Jinja2Templates(directory="templates")
|
||||
templates.env.filters["zip"] = zip
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
JOURS_FR = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]
|
||||
JOURS_FR = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"]
|
||||
_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_.\-]{1,64}$")
|
||||
_EMAIL_RE = re.compile(r"^([a-zA-Z0-9_.\-]{1,64})@([a-zA-Z0-9.\-]{1,255})$")
|
||||
_HHMM_RE = re.compile(r"^([01]\d|2[0-3]):[0-5]\d$")
|
||||
@ -288,8 +288,10 @@ def _require_user(request: Request):
|
||||
# ── Shared helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def week_dates(year: int, week: int) -> list[str]:
|
||||
"""Les 7 jours ISO de la semaine : le week-end est affiché (heures dues nulles)
|
||||
pour pouvoir y déclarer des astreintes et y saisir les interventions."""
|
||||
first = datetime.strptime(f"{year}-W{week:02d}-1", "%G-W%V-%u").date()
|
||||
return [(first + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(5)]
|
||||
return [(first + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(7)]
|
||||
|
||||
|
||||
def _weeks_by_year(all_weeks: list) -> dict:
|
||||
@ -327,7 +329,9 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
|
||||
# activité d'astreinte (saisie sur le jour ou la veille qualifiés)
|
||||
retour_min = retour_apres_repos(date, p_by_date, plages, astreintes)
|
||||
matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages)
|
||||
trav = heures_travaillees(p, retour_min)
|
||||
forfait = (hhmm_to_minutes(plages["astreinte_forfait"]) if plages.get("astreinte_forfait") else 0) \
|
||||
if date in astreintes else 0
|
||||
trav = heures_travaillees(p, retour_min) + forfait
|
||||
du = heures_dues(date, conges, h_jour, plages, retour_min)
|
||||
du_full = du + matin_ex + aprem_ex
|
||||
matin_nom, aprem_nom = nominaux_du_jour(conges_date, du_full, matin_ex, aprem_ex)
|
||||
@ -335,7 +339,7 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
|
||||
# Décompte astreinte du jour (compteur séparé, hors heures de semaine) :
|
||||
# seulement si des événements sont stockés et que le jour est qualifié.
|
||||
events = p.get("astreinte_events") or []
|
||||
dec = (decompte_astreinte(date, events, load_facteurs_astreinte())
|
||||
dec = (decompte_astreinte(date, events, load_facteurs_astreinte(), fenetre_astreinte_jour(date, astreintes))
|
||||
if events and jour_astreinte_qualifie(date, astreintes) else None)
|
||||
return {
|
||||
**p,
|
||||
@ -344,6 +348,8 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
|
||||
"du": minutes_to_hhmm(du),
|
||||
"delta": minutes_to_hhmm(delta),
|
||||
"conge": conges_date,
|
||||
"forfait_min": forfait,
|
||||
"forfait_h": minutes_to_hhmm(forfait) if forfait else "",
|
||||
"du_full_min": du_full,
|
||||
"matin_excl_min": matin_ex, "aprem_excl_min": aprem_ex,
|
||||
"matin_nominal_min": matin_nom, "aprem_nominal_min": aprem_nom,
|
||||
@ -476,8 +482,11 @@ def _as_block_html(date, jour):
|
||||
rows = "".join(_as_event_html(date, ev) for ev in events) or _as_event_html(date, {})
|
||||
hint = ""
|
||||
if jour.get("retour_min") is not None:
|
||||
hint = (f'<span class="as-hint" title="11h de repos consécutif après la dernière activité">'
|
||||
hint = (f'<span class="as-hint" title="11h de repos consécutif après la dernière activité, fin arrondie au quart d\'heure entamé">'
|
||||
f'retour ≥ {_e(jour["retour"])}</span>')
|
||||
if jour.get("forfait_min"):
|
||||
hint += (f'<span class="as-hint" title="Forfait réglé dans Réglages, ajouté au temps travaillé">'
|
||||
f'forfait + {_e(jour["forfait_h"])}</span>')
|
||||
total = ""
|
||||
if jour.get("astreinte_min"):
|
||||
total = (f'<span class="as-total" title="Compteur séparé, hors heures de semaine">'
|
||||
@ -941,9 +950,12 @@ def settings_page(request: Request):
|
||||
"arrivee_url": f"{base_url}/presence/{notif['token']}/arrivee",
|
||||
"depart_url": f"{base_url}/presence/{notif['token']}/depart",
|
||||
"plages": plages,
|
||||
"facteurs": load_facteurs_astreinte(),
|
||||
"saved": request.query_params.get("saved") == "1",
|
||||
"plages_saved": request.query_params.get("plages_saved") == "1",
|
||||
"plages_error": request.query_params.get("plages_error") == "1",
|
||||
"forfait_saved": request.query_params.get("forfait_saved") == "1",
|
||||
"forfait_error": request.query_params.get("forfait_error") == "1",
|
||||
})
|
||||
|
||||
|
||||
@ -1085,6 +1097,27 @@ def settings_plages_save(
|
||||
return RedirectResponse("/settings?plages_saved=1", status_code=303)
|
||||
|
||||
|
||||
@app.post("/settings/astreinte-forfait", response_class=HTMLResponse)
|
||||
def settings_astreinte_forfait(request: Request, astreinte_forfait: str = Form("")):
|
||||
"""Durée forfaitaire ajoutée au temps travaillé de chaque jour d'astreinte
|
||||
déclaré (réglage par utilisateur, stocké avec ses plages)."""
|
||||
user_id = _get_user(request)
|
||||
if not user_id:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
val = astreinte_forfait.strip()
|
||||
if val and (not _HHMM_RE.match(val) or hhmm_to_minutes(val) > 12 * 60):
|
||||
logger.warning("réglages forfait astreinte rejetés (invalide): user_id=%r valeur=%r", user_id, val)
|
||||
return RedirectResponse("/settings?forfait_error=1", status_code=303)
|
||||
|
||||
plages = load_plages(user_id)
|
||||
plages["astreinte_forfait"] = val
|
||||
save_plages(user_id, plages)
|
||||
logger.info("réglages forfait astreinte mis à jour: user_id=%r durée=%r", user_id, val or "(aucune)")
|
||||
append_log(user_id, "settings", "Forfait astreinte", f"durée={val or '(aucune)'}")
|
||||
return RedirectResponse("/settings?forfait_saved=1", status_code=303)
|
||||
|
||||
|
||||
@app.post("/presence/{token}/arrivee")
|
||||
def presence_arrivee(request: Request, token: str):
|
||||
user_id = find_user_by_token(token)
|
||||
|
||||
Reference in New Issue
Block a user