Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [ ] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #15 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
144 lines
12 KiB
Markdown
144 lines
12 KiB
Markdown
# 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
|