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 %}