From 08971544609222a0e5c011d0c8429d2c5385f743 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 14 Apr 2026 15:08:45 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20ajout=20d'un=20=C3=A9cran=20pour=20l?= =?UTF-8?q?e=20r=C3=A9cap=20cong=C3=A9s=20et=20RTT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 11 ++ README.md | 2 + doc/functional-rules.md | 18 +++ doc/leave-recap-screen.md | 73 +++++++++ frontend/data/documentation-content.ts | 11 ++ frontend/i18n/locales/fr.json | 3 + frontend/layouts/default.vue | 21 +++ frontend/middleware/leave-recap-access.ts | 11 ++ frontend/pages/leave-recap.vue | 121 ++++++++++++++ frontend/pages/users.vue | 25 ++- frontend/services/dto/leave-recap.ts | 14 ++ frontend/services/dto/user-data.ts | 1 + frontend/services/dto/user.ts | 1 + frontend/services/leave-recap.ts | 12 ++ frontend/services/users.ts | 8 +- migrations/Version20260414100000.php | 26 +++ src/ApiResource/EmployeeLeaveRecap.php | 35 ++++ src/Entity/User.php | 19 +++ src/Service/Leave/LeaveRecapRowBuilder.php | 180 +++++++++++++++++++++ src/State/EmployeeLeaveRecapProvider.php | 114 +++++++++++++ src/State/EmployeeLeaveSummaryProvider.php | 25 +-- src/State/LeaveRecapPrintProvider.php | 150 +---------------- src/Util/LeaveRecapCutoff.php | 23 +++ 23 files changed, 743 insertions(+), 161 deletions(-) create mode 100644 doc/leave-recap-screen.md create mode 100644 frontend/middleware/leave-recap-access.ts create mode 100644 frontend/pages/leave-recap.vue create mode 100644 frontend/services/dto/leave-recap.ts create mode 100644 frontend/services/leave-recap.ts create mode 100644 migrations/Version20260414100000.php create mode 100644 src/ApiResource/EmployeeLeaveRecap.php create mode 100644 src/Service/Leave/LeaveRecapRowBuilder.php create mode 100644 src/State/EmployeeLeaveRecapProvider.php create mode 100644 src/Util/LeaveRecapCutoff.php 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 @@

Employés

+ + +

Récap. congés

+
Utilisateurs

+ + +

Récap. congés

+
auth.user?.roles?.includes('ROLE_ADMIN') ?? false) const isSuperAdmin = computed(() => auth.user?.roles?.includes('ROLE_SUPER_ADMIN') ?? false) const isDriver = computed(() => auth.user?.isDriver ?? false) +const hasLeaveRecapAccess = computed(() => auth.user?.hasLeaveRecapAccess ?? false) const route = useRoute() diff --git a/frontend/middleware/leave-recap-access.ts b/frontend/middleware/leave-recap-access.ts new file mode 100644 index 0000000..db61bc5 --- /dev/null +++ b/frontend/middleware/leave-recap-access.ts @@ -0,0 +1,11 @@ +export default defineNuxtRouteMiddleware(async () => { + const auth = useAuthStore() + + if (!auth.checked) { + await auth.ensureSession() + } + + if (!auth.user?.hasLeaveRecapAccess) { + return navigateTo('/') + } +}) diff --git a/frontend/pages/leave-recap.vue b/frontend/pages/leave-recap.vue new file mode 100644 index 0000000..bbaae3a --- /dev/null +++ b/frontend/pages/leave-recap.vue @@ -0,0 +1,121 @@ + + + diff --git a/frontend/pages/users.vue b/frontend/pages/users.vue index e459c72..2757209 100644 --- a/frontend/pages/users.vue +++ b/frontend/pages/users.vue @@ -189,6 +189,20 @@

+
+ +

+ Affiche l'onglet dans la sidebar et donne accès au tableau récap. +

+
+