diff --git a/app/main.py b/app/main.py index 04122bd..4b59a1b 100644 --- a/app/main.py +++ b/app/main.py @@ -18,10 +18,11 @@ from models import ( toggle_conge, list_all_weeks, load_notif_config, save_notif_config, find_user_by_token, save_presence, load_auth, save_auth, find_user_by_reset_token, - load_plages, save_plages, + load_plages, save_plages, heures_jour_min, ) from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues from stats import compute_all_stats +from export import export_semaine_ods, export_annee_ods from notifications import reminder_loop logging.basicConfig( @@ -94,10 +95,6 @@ def week_dates(year: int, week: int) -> list[str]: return [(first + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(5)] -def heures_jour_min() -> int: - return int(float(load_config().get("heures_jour", 7.8)) * 60) - - def _weeks_by_year(all_weeks: list) -> dict: result = {} for yr, wks in groupby(sorted(all_weeks, reverse=True), key=lambda x: x[0]): @@ -518,6 +515,39 @@ def stats_page(request: Request): }) +ODS_MEDIA_TYPE = "application/vnd.oasis.opendocument.spreadsheet" + + +@app.get("/export/semaine/{year}/{week}") +def export_semaine(request: Request, year: int, week: int): + user_id = _get_user(request) + if not user_id: + return RedirectResponse("/login", status_code=302) + + content = export_semaine_ods(year, week, user_id) + filename = f"pointages-{user_id}-{year}-S{week:02d}.ods" + logger.info("export ODS semaine: user_id=%r year=%d week=%d", user_id, year, week) + return Response( + content=content, media_type=ODS_MEDIA_TYPE, + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + +@app.get("/export/annee/{year}") +def export_annee(request: Request, year: int): + user_id = _get_user(request) + if not user_id: + return RedirectResponse("/login", status_code=302) + + content = export_annee_ods(year, user_id) + filename = f"pointages-{user_id}-{year}.ods" + logger.info("export ODS année: user_id=%r year=%d", user_id, year) + return Response( + content=content, media_type=ODS_MEDIA_TYPE, + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + @app.get("/settings", response_class=HTMLResponse) def settings_page(request: Request): user_id = _get_user(request) diff --git a/app/models.py b/app/models.py index 79e2b39..b9abf74 100644 --- a/app/models.py +++ b/app/models.py @@ -68,6 +68,10 @@ def save_config(cfg: dict): CONFIG_FILE.write_text(json.dumps(cfg, indent=2, ensure_ascii=False)) +def heures_jour_min() -> int: + return int(float(load_config().get("heures_jour", 7.8)) * 60) + + # ── Pointages ───────────────────────────────────────────────────────────────── def _week_file(user_id: str, year: int, week: int) -> Path: diff --git a/app/templates/base.html b/app/templates/base.html index 144d9df..b00946c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -127,6 +127,19 @@ } .week-id .wk { color: var(--muted); font-size: 1rem; font-weight: 300; } .week-id .slash { color: var(--rule); margin: 0 .2em; } + .week-header-spacer { flex: 1; } + .export-btn { + font-family: var(--mono); + font-size: 11px; + letter-spacing: .04em; + color: var(--muted); + text-decoration: none; + border: 1.5px solid var(--rule); + padding: .4rem .75rem; + transition: border-color .15s, color .15s; + white-space: nowrap; + } + .export-btn:hover { border-color: var(--accent); color: var(--accent); } /* ── Soldes ── */ .soldes { @@ -384,6 +397,8 @@ } .chip:hover { border-color: var(--text); color: var(--text); } .chip.here { background: var(--text); border-color: var(--text); color: var(--ground); } + .chip.chip-export { color: var(--accent); border-color: rgba(47,95,160,.35); margin-left: .3rem; } + .chip.chip-export:hover { border-color: var(--accent); background: rgba(47,95,160,.06); } /* ── Calendrier sélecteur ── */ .cal-trigger-wrap { position: relative; } diff --git a/app/templates/semaine.html b/app/templates/semaine.html index e20329f..478e7b3 100644 --- a/app/templates/semaine.html +++ b/app/templates/semaine.html @@ -32,6 +32,11 @@ + + + + ⇩ Exporter + @@ -80,6 +85,9 @@ S{{ '%02d' % yw[1] }} {% endfor %} + + ⇩ {{ yr }} + {% endfor %}