# SIRH ## Mandatory Rules - Any functional change MUST update `doc/` in the same intervention - Any functional change MUST update the in-app documentation (`frontend/data/documentation-content.ts`) in the same intervention - At the end of every feature addition or functional modification, update this CLAUDE.md to reflect new patterns, rules, or conventions introduced ## Commands - `make start` — start Docker stack - `make test` — run backend tests (PHPUnit) - `make dev-nuxt` — dev frontend - `cd frontend && npm run build` — build frontend - `php bin/console cache:clear && php bin/console cache:warmup` — clear cache after deploy ## Stack - Backend: Symfony + API Platform + Doctrine ORM - Frontend: Nuxt 4 + Vue 3 + TypeScript + Tailwind CSS - UI library: `@malio/layer-ui` (Nuxt layer, `extends: ['@malio/layer-ui']` dans `nuxt.config.ts`). Composants auto-importés avec préfixe `Malio*` (ex. `MalioSelectCheckbox`, `MalioInputText`…). Doc d'usage dans `node_modules/@malio/layer-ui/COMPONENTS.md`. Tokens Tailwind `m-*` (primary/muted/danger/success/…) et variables CSS `--m-*` fournies par la couche. ## Project Structure - `src/` — Symfony domain, API resources, state providers/processors, services - `frontend/` — Nuxt app (pages, components, composables, services) - `migrations/` — Doctrine migrations (always include working `down()`) - `doc/` — functional rules and business documentation ## Functional Rules - Reference: `doc/functional-rules.md` (mandatory reading before any business logic change) - Complementary: `doc/leave-rollover.md`, `doc/rtt-rollover.md` ## Domain Model - Contracts: `trackingMode` (TIME=hours, PRESENCE=half-days), `weeklyHours` - Contract types: FORFAIT, THIRTY_FIVE_HOURS, THIRTY_NINE_HOURS, INTERIM, CUSTOM - Contract nature (per period): CDI, CDD, INTERIM - **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat. - Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver` - **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). - **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). - **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`. - Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots - Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE) - Driver periods (`isDriver=true` on `EmployeeContractPeriod`): separate screen `/driver-hours`, uses `dayHoursMinutes`/`nightHoursMinutes` + meal/overnight flags on `WorkHour` ## Fériés - Source : API gouv via `PublicHolidayService` (cache 30j) - Exclusions : env `EXCLUDED_PUBLIC_HOLIDAYS` (CSV de libellés), défaut `"Lundi de Pentecôte"`. Le filtre s'applique après le cache, côté service, donc frontend et calculs backend voient la même liste. - Écrans Heures / Heures Conducteurs (vue jour) : le nom du férié est affiché en badge `#b3e5fc` avec icône `mdi:calendar-star` dans la colonne Absence (distinct du pill absence). Bouton "Modifier" absence masqué sur férié (comme pour les formations). - Création/édition d'absence **autorisée** sur un férié (bouton Modifier visible). En présence d'absence, le crédit d'heures suit `absence.type.countAsWorkedHours` (WorkedHoursCreditPolicy), pas le crédit virtuel férié. - Saisie d'heures (ou de jours de présence) autorisée sur un férié - **Crédit automatique des heures contractuelles** sur un férié Lun-Ven pour tout contrat hors Forfait, **uniquement en l'absence d'absence déclarée** : le total journalier = `max(saisie + credited_absence, référence_contractuelle)`. Référence : 35h→7h, 39h→8h Lun-Jeu/7h Ven, CUSTOM→weeklyHours/5, INTERIM→idem 35h/39h/custom selon weeklyHours. Aucune ligne BDD créée (crédit virtuel). Drivers : crédité en `dayHoursMinutes`. Impacte directement le total hebdo RTT (tranches 25%/50%). Dès qu'une absence est posée sur le férié, le crédit virtuel saute — c'est le `countAsWorkedHours` du type d'absence qui pilote. Services : `App\Service\WorkHours\HolidayVirtualHoursResolver` + `DailyReferenceMinutesResolver`. Doc complète : `doc/holiday-virtual-hours.md`. ## Commentaires de semaine - Entité `EmployeeWeekComment` : commentaire libre par employé et semaine ISO (unique `(employee_id, week_start_date)`). `week_start_date` = lundi. - CRUD `/employee_week_comments` `ROLE_ADMIN`. Write processor audite via `AuditLogger`. - Picto bulle vue semaine (HoursWeekView + DriverHoursWeekView) : fond bleu/rouge. Intégré dans `WeeklySummaryRow.comment/commentId`. - Doc : `doc/week-comments.md`. ## Validation Rules - `isValid` (RH): locks line for everyone (admin can only untoggle validation) - `isSiteValid` (site manager): locks for non-admin, admin can still edit - Any real modification resets both `isSiteValid=false` and `isValid=false` - No-op saves preserve existing validations ## Overtime Rules - Contracts <= 35h: +25% from 35h to 43h, +50% beyond - Contracts >= 39h: +25% from 39h to 43h, +50% beyond - CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery), deficit doesn't impact balance - INTERIM: no overtime bonuses, no recovery time - 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. - **FORFAIT — jours de présence et N-1** : les congés posés et imputés sur le stock N-1 ne décrémentent **pas** les jours de présence affichés (`presenceDaysByMonth` et `presenceDaysToToday`). Implémenté dans `EmployeeLeaveSummaryProvider::computePresenceDaysByMonth` via un budget N-1 (= `previousYearTakenDays`) consommé chronologiquement avant comptage des absences. Pour les non-forfait, ce budget vaut toujours 0 → comportement inchangé. ## 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` - Les deux champs km et montant sont optionnels individuellement mais au moins un requis ## Formations - Onglet "Formation" sur la fiche employé (admin uniquement) - Champs : date début, date fin, justificatif PDF optionnel, commentaire - Validation: dates obligatoires, `endDate >= startDate`, fichier PDF uniquement - Justificatif stocké dans `var/uploads/formations/{année}/{mois}/{uuid}.pdf` (année/mois = startDate) - Suppression et remplacement du justificatif nettoient l'ancien fichier disque - Tri tableau par `startDate DESC` - Affichage écran Heures (jour) : pill "Formation" (indigo) dans la colonne Absence. Quand une formation existe, le bouton "Modifier" de la colonne Absence est masqué (lockdown complet du jour pour la gestion d'absence) - Affichage Calendrier : cellule "F" (indigo) si formation seule, ou icône école en coin si formation + absence. Cellules avec formation non cliquables. Légende dédiée. PDF export : code "F" indigo ou astérisque à côté du code d'absence - Le CRUD formation est exclusivement géré depuis la fiche employé > onglet Formation ## Frontend Patterns ### Table styling (standard across all pages) - Header: `grid border border-black bg-tertiary-500 px-6 py-3 text-[20px] font-semibold text-black rounded-t-md sticky top-0 z-10` - Body wrapper: `border-x border-b border-primary-500 rounded-b-md` - Rows: `grid items-center gap-4 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 cursor-pointer hover:bg-tertiary-500` - Page wrapper for scroll: `h-full flex flex-col overflow-hidden`, table container: `min-h-0 overflow-auto rounded-md bg-white` ### Drawer buttons (AppDrawer) - Edit mode: `grid grid-cols-2 gap-3` → Supprimer (red, left) + Modifier (primary, right) - Create mode: centered `+ Ajouter` button, w-[200px] - Exception: Users drawer has NO delete button - All "Ajouter" buttons across the app use "+" prefix ### API Platform (backend) - Custom operations use Processor (write) / Provider (read) - File uploads: `deserialize: false` on Post, access file via RequestStack - Upload dir: `%kernel.project_dir%/var/uploads` ## Audit Logging - All processors that modify entities impacting calculations (heures, absences, contrats, RTT) MUST inject `AuditLogger` and log create/update/delete actions - `AuditLogger::log()` persists without flushing — the processor's `flush()` handles both the data change and the audit entry atomically - Audit logs are accessible only via `ROLE_SUPER_ADMIN` (hidden role, added manually in DB) - Documentation: `doc/audit-logging.md` ## Backend Conventions - Prefer explicit DTOs over associative arrays - Business rules in backend (providers/processors/services), frontend is display/interaction only - Keep backend PHP DTOs aligned with frontend TS DTOs (`frontend/services/dto/*`) - Update unit tests when constructor/service signatures change ## In-App Documentation - Content: `frontend/data/documentation-content.ts` — structured TypeScript data with all user-facing documentation - Types: `frontend/types/documentation.ts` — DocSection, DocArticle, DocBlock - Composable: `frontend/composables/useDocumentation.ts` — role-based filtering (employee < site_manager < admin) - Components: `frontend/components/documentation/` — DocumentationPage, DocumentationSection, DocumentationArticle - Page: `frontend/pages/documentation.vue` - 3 access levels: `employee` (ROLE_SELF), `site_manager` (ROLE_USER), `admin` (ROLE_ADMIN) — cumulative (admin sees everything) - Each section/article has a `requiredLevel` that controls visibility - When adding or modifying a feature, update the corresponding section in `documentation-content.ts` ## Language - UI is in French - User communicates in French - Code (variables, comments) in English