diff --git a/CLAUDE.md b/CLAUDE.md
index 53ac590..07d1184 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,6 +56,17 @@
- Driver contracts: RTT uses `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` instead of morning/afternoon/evening time ranges
- FORFAIT weekend/holiday bonus: each weekend or public holiday day worked gives bonus leave (full day if morning+afternoon, 0.5 if only one). Added to acquired days, no cap. PRESENCE mode only.
+## Récap. congés (écran)
+- Accès via sidebar `Récap. congés`, conditionné au flag `User.hasLeaveRecapAccess` (défaut `false`) — activé au create/edit user. Le flag s'applique à tous les profils, y compris admin.
+- Scope : `ROLE_ADMIN` → tous les employés, `ROLE_USER` (chef de site) → employés de ses sites, `ROLE_SELF` → sa ligne
+- Cutoff temporel : fin de la semaine S-2 (dimanche 23:59:59). Formule : `dimanche(lundi_semaine_courante − 14j)`. Pas de gate `isValid`.
+- Helper : `App\Util\LeaveRecapCutoff::resolveCutoff()`
+- Colonnes : Nom, Prénom, Contrat, CP N-1 restant, CP N, Samedis, RTT — identiques au PDF
+- Service partagé : `LeaveRecapRowBuilder` consommé par `LeaveRecapPrintProvider` (as-of today) et `EmployeeLeaveRecapProvider` (as-of cutoff)
+- `EmployeeLeaveSummaryProvider::computeYearSummary()` accepte un `?DateTimeImmutable $asOfDate` qui cappe l'accrual et les absences sur l'année cible (`null` = comportement live inchangé)
+- Pas d'export PDF depuis cet écran
+- Doc détaillée : `doc/leave-recap-screen.md`
+
## Frais (MileageAllowance)
- Onglet "Frais" (anciennement "Frais Kms") sur la fiche employé
- Validation: mois obligatoire + au moins `kilometers > 0` ou `amount > 0`
diff --git a/README.md b/README.md
index 5c7f7c9..2e9ab55 100644
--- a/README.md
+++ b/README.md
@@ -23,3 +23,5 @@ docker compose exec -T db psql -U root -d sirh < sirh.sql
```sql
UPDATE users SET roles = '["ROLE_ADMIN","ROLE_SUPER_ADMIN"]' WHERE username = 'emilie';
```
+sudo -u postgres pg_dump --no-owner --no-privileges --clean --if-exists sirh_prod > /tmp/sirh_prod_$(date +%F).sql
+scp user@:/tmp/sirh_prod_2026-04-14.dump ~/workspace/
diff --git a/doc/functional-rules.md b/doc/functional-rules.md
index e9bdf66..49df2bf 100644
--- a/doc/functional-rules.md
+++ b/doc/functional-rules.md
@@ -330,6 +330,24 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
| CP N | Forfait: jours acquis année civile. Non-forfait: en cours d'acquisition |
| RTT | Minutes disponibles (report N-1 + acquis N - payés). Format `X h Y m`. Forfait et INTERIM: `-` |
+## 10bis) Écran Récap. congés (tableau)
+
+- Complément de l'export PDF : même logique de calcul, mais accessible aux employés et chefs de site
+- Endpoint: `GET /api/leave-recap`
+- Accès conditionné au flag `User.hasLeaveRecapAccess` (défaut `false`, activé au create/edit user)
+- Le flag s'applique à tous les profils, y compris admin (pas de bypass)
+- Scoping :
+ - `ROLE_ADMIN` : tous les employés
+ - `ROLE_USER` (chef de site) : employés des sites autorisés (`UserSiteRole`)
+ - `ROLE_SELF` : uniquement son employé lié
+- **Cutoff temporel** : le récap est figé à la fin de la semaine S-2 (dimanche 23:59:59)
+ - Formule : `cutoffDate = dimanche(lundi_semaine_courante − 14 jours)`
+ - Exemple : mardi 14/04/2026 (S16) → dimanche 05/04/2026 (fin S14)
+ - `isValid` n'entre PAS en compte : cutoff purement temporel
+ - Les heures et absences postérieures au cutoff sont ignorées dans les calculs
+- Colonnes identiques au PDF (voir §10)
+- Détails techniques : voir `doc/leave-recap-screen.md`
+
## 11) Récapitulatif Salaire (PDF mensuel)
- Accessible depuis la page Employés via le bouton "Récap. Salaire" (réservé `ROLE_ADMIN`)
diff --git a/doc/leave-recap-screen.md b/doc/leave-recap-screen.md
new file mode 100644
index 0000000..24ee7ab
--- /dev/null
+++ b/doc/leave-recap-screen.md
@@ -0,0 +1,73 @@
+# Écran Récap. congés
+
+## Objet
+
+Vue tableau des soldes de congés par employé, figée à un cutoff temporel (fin de semaine S-2).
+Complémentaire à l'export PDF admin : mêmes colonnes, accès étendu aux employés et chefs de site.
+
+## Cutoff
+
+La formule est : `cutoffDate = dimanche de (lundi de la semaine courante − 14 jours)`.
+
+Exemple : mardi 14/04/2026 (S16) → **dimanche 05/04/2026 23:59:59** (fin S14).
+
+Le cutoff est purement temporel : l'état `isValid` des heures n'entre pas en compte. Les heures
+et absences postérieures au cutoff sont ignorées dans le calcul des soldes.
+
+Implémentation : `App\Util\LeaveRecapCutoff::resolveCutoff()` côté backend, helper `parseYmd` +
+`getIsoWeekNumber` côté frontend pour l'affichage du badge.
+
+## Colonnes
+
+Identiques au PDF :
+
+- Nom
+- Prénom
+- Contrat
+- CP N-1 restant
+- CP N
+- Samedis acquis
+- RTT
+
+Pour les admins et chefs de site, une colonne **Site** est ajoutée à gauche.
+
+## Scoping
+
+| Profil | Données visibles |
+|---------------|-----------------------------------------|
+| `ROLE_ADMIN` | Tous les employés actifs, tous sites |
+| `ROLE_USER` (chef de site) | Employés actifs des sites autorisés via `UserSiteRole` |
+| `ROLE_SELF` | Uniquement l'employé lié à son compte |
+
+## Flag d'accès
+
+Le champ `User.hasLeaveRecapAccess` (boolean, défaut `false`) conditionne :
+
+- L'affichage de l'entrée "Récap. congés" dans la sidebar
+- L'accès à la route `/leave-recap` (middleware `leave-recap-access.ts`)
+- L'endpoint API `GET /api/leave-recap` (le provider renvoie `403` si le flag est faux)
+
+Le flag s'applique même aux admins : un admin sans le flag ne voit pas l'écran. Il se configure
+dans le drawer de création/édition d'un utilisateur.
+
+## Service partagé
+
+`App\Service\Leave\LeaveRecapRowBuilder::build(Employee $employee, DateTimeImmutable $asOfDate)`
+construit une ligne de récap. Il est utilisé par :
+
+- `LeaveRecapPrintProvider` (PDF admin) avec `$asOfDate = today`
+- `EmployeeLeaveRecapProvider` (écran) avec `$asOfDate = cutoff`
+
+## Propagation du cutoff dans les calculs
+
+`EmployeeLeaveSummaryProvider::computeYearSummary()` accepte un `?DateTimeImmutable $asOfDate`.
+Lorsqu'il est fourni et appliqué à l'année cible, il remplace "today" dans :
+
+- `resolveAccrualCalculationEndDate()` — la borne d'accrual devient le dernier jour du mois
+ précédant `asOfDate` (au lieu du mois précédent today).
+- `resolveTakenCalculationEndDate()` — les absences postérieures à `asOfDate` sont ignorées.
+
+Pour les années antérieures (carry forward), le comportement reste inchangé (pas de cap).
+
+Le RTT est capé via `RttRecoveryComputationService::computeTotalRecoveryForExercise(..., $limitDate)`
+qui existait déjà, en passant `cutoff` comme date de référence.
diff --git a/frontend/data/documentation-content.ts b/frontend/data/documentation-content.ts
index bbfb83a..559b7ac 100644
--- a/frontend/data/documentation-content.ts
+++ b/frontend/data/documentation-content.ts
@@ -442,6 +442,17 @@ export const documentationSections: DocSection[] = [
{ type: 'paragraph', content: 'Compteurs visibles sur l\'onglet Congé de la fiche employé : acquis, en cours d\'acquisition, pris, restant.' },
],
},
+ {
+ id: 'ecran-recap-conges',
+ title: 'Écran Récap. congés',
+ requiredLevel: 'employee',
+ blocks: [
+ { type: 'paragraph', content: 'L\'écran "Récap. congés" affiche un tableau figé des soldes de congés et RTT par employé. Il est accessible via la sidebar lorsque l\'accès a été activé sur le compte utilisateur.' },
+ { type: 'list', content: 'Employé : voit uniquement sa propre ligne\nChef de site : voit les employés des sites qui lui sont rattachés\nAdmin : voit tous les employés, groupés par site' },
+ { type: 'note', content: 'Le récap est arrêté à la fin de la semaine S-2 (dimanche). Exemple : un mardi en S16, les soldes sont calculés jusqu\'au dimanche de la S14 inclus. Les heures et absences postérieures ne sont pas comptées.' },
+ { type: 'paragraph', content: 'Les colonnes affichées sont identiques à l\'export PDF admin (Nom, Prénom, Contrat, CP N-1 restant, CP N, Samedis, RTT). L\'accès à cet écran est géré par un flag sur l\'utilisateur, activé depuis le drawer de création/édition d\'un utilisateur par un admin.' },
+ ],
+ },
],
},
{
diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json
index 04162d4..75abe0e 100644
--- a/frontend/i18n/locales/fr.json
+++ b/frontend/i18n/locales/fr.json
@@ -56,6 +56,9 @@
"create": "Impossible de créer l'observation.",
"update": "Impossible de mettre à jour l'observation.",
"delete": "Impossible de supprimer l'observation."
+ },
+ "leaveRecap": {
+ "load": "Impossible de charger le récap des congés."
}
},
"success": {
diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue
index c5d37df..8747e0f 100644
--- a/frontend/layouts/default.vue
+++ b/frontend/layouts/default.vue
@@ -53,6 +53,17 @@