Files
SIRH/CLAUDE.md
tristan 47f9bea57d
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
feat : sélecteur d'exercice sur l'onglet RTT de la fiche employé
Permet de consulter les exercices passés (table hebdomadaire RTT) en
réutilisant le pattern de l'onglet Congés. Plage bornée par
max(début historique contrat, RTT_START_DATE). Bouton + Payer les RTT
verrouillé sur exercices clos. Onglet masqué pour FORFAIT (inchangé).

Backend : rttStartDate désormais toujours exposé sur EmployeeRttSummary
pour que le sélecteur conserve sa borne lors de la navigation vers un
exercice passé. Le masquage existant des lignes Report continue de
fonctionner (comparaison mois-à-mois).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 09:58:50 +02:00

14 KiB
Raw Blame History

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é.

Onglet Congés (fiche employé)

  • Calendrier annuel des congés (frontend/components/employees/LeaveTab.vue) — période = Janvier→Décembre pour FORFAIT, Juin(N-1)→Mai(N) pour les autres contrats. Règle pilotée par le contrat courant (cf. EmployeeLeaveSummaryProvider::resolveYear), même quand on consulte une année passée.
  • Sélecteur d'année en pied de calendrier (zone scrollable, à gauche). Plage : de l'exercice courant jusqu'à max(floor_contrat, floor_data_start_date)floor_contrat = premier exercice avec contrat ouvert (employee.contractHistory[].startDate) ; floor_data_start_date = exercice contenant RTT_START_DATE (env, ex. 2026-02-23 → exercice 2026). Le double plancher empêche de remonter avant la mise en service du logiciel. Format : 2026 pour FORFAIT, Juin 2025 → Mai 2026 sinon.
  • Changement d'année → recharge complète de l'onglet via useEmployeeLeave.setSelectedLeaveYear(year) (reload de getEmployeeLeaveSummary?year=YYYY + listAbsences + listPublicHolidays). Backend : filtre ?year=YYYY validé 2000-2100, et EmployeeLeaveSummary expose dataStartDate (env RTT_START_DATE, injecté via services.yaml).
  • Sur un exercice passé (selectedYear !== currentYear), les boutons crayon Jours fractionnés et Année N-1 payés sont désactivés : pas d'édition rétroactive des stocks de report.
  • Doc : doc/leave-tab.md.

Onglet RTT (fiche employé)

  • Tableau hebdomadaire (frontend/components/employees/RttTab.vue) — exercice fixe Juin(N-1)→Mai(N). Onglet masqué pour les FORFAIT (showRttTab).
  • Sélecteur d'année sous le tableau dans la zone scrollable. Même mécanique que l'onglet Congés (double plancher) : max(floor_contrat, floor_rttStartDate). Format unique : Juin 2025 → Mai 2026.
  • Changement d'année → recharge via useEmployeeRtt.setSelectedRttYear(year) (getEmployeeRttSummary?year=YYYY). EmployeeRttSummary.rttStartDate est déjà exposé (champ existant) — il sert à la fois au floor du sélecteur et au masquage des lignes Report avant la mise en service.
  • Sur un exercice passé, le bouton + Payer les RTT est désactivé (pas de paiement rétroactif).
  • Doc : doc/rtt-tab.md.

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