Initial commit — décompte horaire

App web de suivi des heures de travail (FastAPI + HTMX + JSON plats).
- Saisie inline par semaine avec bouton ✓ par ligne
- Congés MA/AM/J avec coloration cellule et mise à jour live via OOB HTMX
- Import fichier ODS badgeuse
- Solde semaine et solde global recalculés à la volée
- Design "ledger" écru, DM Mono, zéro border-radius
- Déploiement Docker sur port 8000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
toto
2026-06-24 15:08:32 +02:00
commit 6f6c7f154e
14 changed files with 1398 additions and 0 deletions

80
app/calcul.py Normal file
View File

@ -0,0 +1,80 @@
"""Business logic: compute worked hours and expected hours per day/week."""
from datetime import datetime, timedelta
def hhmm_to_minutes(val: str | None) -> int:
if not val:
return 0
h, m = val.split(":")
return int(h) * 60 + int(m)
def minutes_to_hhmm(minutes: int) -> str:
sign = "-" if minutes < 0 else ""
minutes = abs(minutes)
return f"{sign}{minutes // 60}h{minutes % 60:02d}"
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 matin + aprem
def heures_dues(date_str: str, conges: list[dict], heures_jour_min: int) -> int:
"""Return expected minutes for a date, accounting for vacations."""
d = datetime.strptime(date_str, "%Y-%m-%d").date()
# Weekend = no hours due
if d.weekday() >= 5:
return 0
conges_date = {c["type"] for c in conges if c["date"] == date_str}
if "jour" in conges_date:
return 0
if "matin" in conges_date and "aprem" in conges_date:
return 0
half = heures_jour_min // 2
if "matin" in conges_date or "aprem" in conges_date:
return half
return heures_jour_min
def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int) -> dict:
"""Compute totals for a list of pointages (one week)."""
total_travaille = 0
total_du = 0
jours = []
for p in pointages:
travaille = heures_travaillees(p)
du = heures_dues(p["date"], conges, heures_jour_min)
delta = travaille - du
total_travaille += travaille
total_du += du
jours.append({
**p,
"travaille_min": travaille,
"du_min": du,
"delta_min": delta,
"travaille": minutes_to_hhmm(travaille),
"du": minutes_to_hhmm(du),
"delta": minutes_to_hhmm(delta),
"conge": {c["type"] for c in conges if c["date"] == p["date"]},
})
return {
"jours": jours,
"total_travaille": minutes_to_hhmm(total_travaille),
"total_du": minutes_to_hhmm(total_du),
"solde": minutes_to_hhmm(total_travaille - total_du),
"solde_min": total_travaille - total_du,
}