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>
110 lines
3.2 KiB
Python
110 lines
3.2 KiB
Python
"""Flat-file storage: JSON files per week for pointages, one file for congés."""
|
|
import json
|
|
from pathlib import Path
|
|
|
|
DATA_DIR = Path("/data")
|
|
POINTAGES_DIR = DATA_DIR / "pointages"
|
|
CONGES_FILE = DATA_DIR / "conges.json"
|
|
CONFIG_FILE = DATA_DIR / "config.json"
|
|
|
|
DEFAULT_CONFIG = {
|
|
"heures_jour": 7.8, # 7h48 = 7.8 h
|
|
}
|
|
|
|
|
|
def _ensure_dirs():
|
|
POINTAGES_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# ---------- config ----------
|
|
|
|
def load_config() -> dict:
|
|
_ensure_dirs()
|
|
if CONFIG_FILE.exists():
|
|
return json.loads(CONFIG_FILE.read_text())
|
|
CONFIG_FILE.write_text(json.dumps(DEFAULT_CONFIG, indent=2))
|
|
return DEFAULT_CONFIG.copy()
|
|
|
|
|
|
def save_config(cfg: dict):
|
|
_ensure_dirs()
|
|
CONFIG_FILE.write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
|
|
|
|
|
# ---------- pointages ----------
|
|
|
|
def _week_file(year: int, week: int) -> Path:
|
|
return POINTAGES_DIR / f"{year}-W{week:02d}.json"
|
|
|
|
|
|
def load_week_pointages(year: int, week: int) -> list[dict]:
|
|
f = _week_file(year, week)
|
|
if f.exists():
|
|
return json.loads(f.read_text())
|
|
return []
|
|
|
|
|
|
def save_week_pointages(year: int, week: int, pointages: list[dict]):
|
|
_ensure_dirs()
|
|
_week_file(year, week).write_text(json.dumps(pointages, indent=2, ensure_ascii=False))
|
|
|
|
|
|
def upsert_pointages(records: list[dict]):
|
|
"""Insert or update pointage records, grouped by ISO week."""
|
|
from datetime import datetime
|
|
by_week: dict[tuple, dict[str, dict]] = {}
|
|
for r in records:
|
|
d = datetime.strptime(r["date"], "%Y-%m-%d").date()
|
|
iso = d.isocalendar()
|
|
key = (iso.year, iso.week)
|
|
by_week.setdefault(key, {})
|
|
by_week[key][r["date"]] = r
|
|
|
|
for (year, week), new_by_date in by_week.items():
|
|
existing = {p["date"]: p for p in load_week_pointages(year, week)}
|
|
existing.update(new_by_date)
|
|
save_week_pointages(year, week, sorted(existing.values(), key=lambda p: p["date"]))
|
|
|
|
|
|
def list_all_weeks() -> list[tuple[int, int]]:
|
|
"""Return sorted list of (year, week) for which we have data."""
|
|
_ensure_dirs()
|
|
weeks = []
|
|
for f in POINTAGES_DIR.glob("????-W??.json"):
|
|
parts = f.stem.split("-W")
|
|
if len(parts) == 2:
|
|
weeks.append((int(parts[0]), int(parts[1])))
|
|
return sorted(weeks)
|
|
|
|
|
|
# ---------- congés ----------
|
|
|
|
def load_conges() -> list[dict]:
|
|
if CONGES_FILE.exists():
|
|
return json.loads(CONGES_FILE.read_text())
|
|
return []
|
|
|
|
|
|
def save_conges(conges: list[dict]):
|
|
_ensure_dirs()
|
|
CONGES_FILE.write_text(json.dumps(conges, indent=2, ensure_ascii=False))
|
|
|
|
|
|
def toggle_conge(date: str, type_conge: str):
|
|
assert type_conge in ("jour", "matin", "aprem")
|
|
conges = load_conges()
|
|
already = any(c["date"] == date and c["type"] == type_conge for c in conges)
|
|
|
|
if already:
|
|
conges = [c for c in conges if not (c["date"] == date and c["type"] == type_conge)]
|
|
else:
|
|
# Adding: remove conflicting entries first
|
|
if type_conge == "jour":
|
|
conges = [c for c in conges if c["date"] != date]
|
|
else:
|
|
conges = [c for c in conges if not (c["date"] == date and c["type"] == "jour")]
|
|
conges.append({"date": date, "type": type_conge})
|
|
|
|
conges.sort(key=lambda c: (c["date"], c["type"]))
|
|
save_conges(conges)
|