feat(absences) : avancement module absences + suppression du portail client

Deux lots regroupés sur la branche feat/absence-management.

Suppression complète du portail client :
- retire ROLE_CLIENT (security.yaml) ; User::getRoles() ajoute toujours ROLE_USER
- supprime l'entité ClientTicket (+ repo, states, relations), User.client et
  User.allowedProjects, NotificationService, ProjectAllowedExtension, le bloc
  ROLE_CLIENT de MailAccessChecker
- front : pages /portal, layout portal, composants client-ticket/,
  AdminClientTicketTab, services/dto/i18n/docs associés
- fixtures : retire les users client-liot / client-acme
- migration Version20260522110000 (drop client_ticket, user_allowed_projects,
  colonnes liées ; task_document.task_id -> NOT NULL)
- tests : retire les cas obsolètes testant le blocage des clients sur le mail

Module gestion des absences (WIP) :
- entités / migrations (Version20260521160000, Version20260522090000)
- pages absences.vue / team-absences.vue, composants frontend/components/absence/
- services front, AccrueLeaveCommand, PublicHolidayController

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-22 11:31:31 +02:00
parent de98924fd3
commit 2a0b202d32
109 changed files with 3918 additions and 3656 deletions

View File

@@ -0,0 +1,119 @@
# Refonte UX du formulaire « Nouvelle demande d'absence »
Date : 2026-05-21
Composant : `frontend/components/absence/AbsenceRequestDrawer.vue`
Branche : `feat/absence-management`
## Contexte & problème
Le formulaire actuel fonctionne mais est inutilisable côté UX :
- `VueDatePicker` brut **non thématisé** (couleur violette par défaut, calendrier au rendu anglo-saxon / semaine au dimanche) → aspect « cassé », rien à voir avec PayFit.
- `<label>` et `<input type="file">` bruts au lieu des composants Malio.
- **Aucune erreur explicite** : le bouton « Valider » est simplement grisé via `canSubmit` quand un champ manque, sans dire pourquoi.
- `preview` qui échoue en silence (`catch { preview.value = null }`).
- Solde insuffisant signalé par un simple warning ambre.
Cible : reproduire l'ergonomie PayFit (référence fournie par l'utilisateur) — apparition progressive des champs, deux champs date saisissables + popup, pills de demi-journée, lignes de solde, erreurs explicites.
## Objectifs
1. Look & ergonomie alignés sur PayFit, cohérents avec le design system Malio.
2. Apparition **progressive** des champs au fil des choix.
3. **Erreurs explicites** par champ, affichées au clic « Valider », qui se vident dès correction.
4. Champs de date en français `JJ / MM / AAAA`, calendrier FR / semaine au lundi.
5. Justificatif via `MalioInputUpload`, affiché seulement si le type l'exige.
## Hors périmètre
- Aucun changement backend : payload (`AbsenceRequestWrite`), endpoints `create` / `preview` / `uploadJustification` inchangés.
- Aucune modification des autres composants du module (traités séparément dans les points #2 à #7 de la revue).
## Layout cible (drawer `max-w-xl`, 1 colonne, apparition progressive)
**Étape 1 — toujours visible**
- `Type d'absence` : `MalioSelect` (options = policies actives). Erreur inline si vide au submit.
**Étape 2 — apparaît dès qu'un type est choisi**
- `Date de début` : champ texte `JJ / MM / AAAA` saisissable + icône calendrier (popup) + bouton effacer. `VueDatePicker` mono-date, thématisé (voir ci-dessous).
- Pills demi-journée début : `Journée entière` | `Matin` | `Après-midi` (défaut : Journée entière).
- Ligne `Solde au <date début> : X jours` (valeur = `preview.available`, alignée à droite, **non repliable**).
**Étape 3 — apparaît dès que la date de début est renseignée**
- `Date de fin` : même composant que la date de début.
- Pills demi-journée fin : `Journée entière` | `Matin` (défaut : Journée entière).
- Ligne `Durée de la demande : N jours` (valeur = `preview.countedDays`).
- Ligne `Solde après validation : Y jours` (valeur = `preview.projectedAvailable`, non repliable). Si `< 0` → bandeau ambre **non bloquant** « solde après = … j, demande soumise pour validation ».
**Étape 4**
- `Justificatif` : `MalioInputUpload`, affiché **uniquement si** `selectedPolicy.justificationRequired`. Label avec `*`. Erreur inline si requis et absent.
- `Commentaire` (optionnel) : `MalioInputTextArea`, placeholder « Écrire un commentaire… ».
**Footer**
- `[ Annuler (tertiary) ] [ Valider (primary) ]`, bouton **toujours actif**.
## Datepicker — thématisation
Reprendre le pattern de `frontend/components/ui/DateFilter.vue` :
- `VueDatePicker` mono-date, `:enable-time-picker="false"`, `auto-apply`, `text-input` activé (saisie clavier), `format``JJ/MM/AAAA`.
- `locale="fr"` (chaîne) + semaine au lundi (`week-start: 1`).
- Variables CSS `--dp-primary-color: #222783`, `--dp-border-color`, `--dp-hover-color`, `--dp-font-size`, `font-family: inherit` (scopées au composant).
- `:min-date` sur la date de fin = date de début ; `:max-date` sur la date de début = date de fin.
## Pills demi-journée → payload
Segment de boutons (style pill, bordure + fond bleu primaire quand sélectionné).
| Pill début | `startHalfDay` | Pill fin | `endHalfDay` |
|------------|----------------|----------|--------------|
| Journée entière | `null` | Journée entière | `null` |
| Matin | `matin` | Matin | `matin` |
| Après-midi | `apres_midi` | — | — |
Cas particulier — **demande sur une seule journée** (`startDate == endDate`) : seules les pills de début sont pertinentes (`Journée entière` / `Matin` / `Après-midi`) ; les pills de fin sont masquées et `endHalfDay` recopie `startHalfDay`. Le décompte reste calculé par `preview` côté backend.
## Validation
État réactif `errors: { type?: string; startDate?: string; endDate?: string; justification?: string }`.
`validate()` (au clic « Valider ») remplit `errors` :
- `type` manquant → `absences.form.errors.typeRequired`
- `startDate` manquante → `absences.form.errors.startRequired`
- `endDate` manquante → `absences.form.errors.endRequired`
- `endDate < startDate``absences.form.errors.endBeforeStart`
- `preview.countedDays <= 0``absences.form.errors.zeroDays`
- justificatif requis et absent → `absences.form.errors.justificationRequired`
Si `errors` non vide → on stoppe la soumission. Des `watch` vident chaque message dès que le champ correspondant redevient valide.
Le solde insuffisant **n'est pas** une erreur bloquante (seul le bandeau ambre).
### Erreurs serveur
- `service.create` / `uploadJustification` : le toast d'erreur de `useApi` reste ; en plus, un bandeau d'erreur en tête de formulaire affiche le message renvoyé (422 de validation).
- `preview` : conserver le `catch` pour les coupures réseau, mais ne plus masquer une erreur de validation de période (afficher le message si l'API en renvoie un).
## Calcul live (preview)
Inchangé sur le principe : `watch` debouncé (300 ms) sur `[type, startDate, endDate, startHalfDay, endHalfDay]``service.preview(payload)`. Les lignes de solde et de durée se mettent à jour à partir du résultat (`available`, `countedDays`, `projectedAvailable`).
## i18n
Nouvelles clés sous `absences.form.errors.*` dans `frontend/i18n/locales/fr.json` :
`typeRequired`, `startRequired`, `endRequired`, `endBeforeStart`, `zeroDays`, `justificationRequired`, plus `absences.form.balanceAt` (« Solde au {date} ») et `absences.form.duration` (« Durée de la demande »).
## Découpage du composant
`AbsenceRequestDrawer.vue` orchestre l'état du formulaire. Pour garder le fichier focalisé, extraire :
- `AbsenceDateField.vue` : champ date thématisé + pills demi-journée (props : `modelValue`, `halfValue`, `halfOptions`, `label`, `error`, `min`/`max`).
Le reste (lignes de solde, bandeau, footer) reste inline dans le drawer.
## Critères d'acceptation
- Au chargement, seul « Type d'absence » est visible ; les sections suivantes apparaissent au fur et à mesure.
- Les dates s'affichent et se saisissent en `JJ / MM / AAAA`, calendrier en français, semaine au lundi, thème bleu primaire.
- Cliquer « Valider » sans remplir → messages d'erreur explicites sous les champs concernés ; ils disparaissent dès correction.
- Un type à justificatif obligatoire affiche le champ d'upload et bloque la soumission tant qu'aucun fichier n'est fourni.
- Une période dépassant le solde affiche le bandeau ambre mais reste soumissible.
- Le payload envoyé au backend est identique à l'actuel.

View File

@@ -0,0 +1,96 @@
# Réorganisation de la gestion des employés (module Absences)
Date : 2026-05-22
Branche : `feat/absence-management`
Statut : design approuvé, prêt pour plan d'implémentation
## Contexte
Aujourd'hui, le `UserDrawer` (admin → utilisateurs) porte deux responsabilités mélangées :
1. l'administration du compte (nom, mot de passe, rôles, client, projets) ;
2. **tout le détail RH/employé** : case « Employé » + un bloc de champs (date d'embauche, date de sortie, type de contrat, situation familiale, temps de travail, CP annuels, début de période de référence, solde CP initial, nombre d'enfants).
Ces champs existent déjà sur l'entité `User` (backend) et dans le DTO `UserData`/`UserWrite` (frontend). La persistance se fait via l'API user (`PATCH /api/users/{id}`).
On veut séparer ces deux préoccupations : le `UserDrawer` ne décide plus que **si un utilisateur est un employé** ; l'édition des informations RH se fait dans un espace dédié, dans le module Absences.
## Objectifs
- `UserDrawer` : ne conserver que la case à cocher « Employé ».
- `team-absences` : ajouter un onglet « Employés » (la page est déjà `middleware: ["admin"]`, donc admin-only).
- L'onglet liste les utilisateurs marqués `isEmployee` avec leurs soldes de congés.
- Un drawer dédié permet d'éditer les informations RH d'un employé.
Hors périmètre : création d'utilisateur (reste dans l'admin), modification du flag `isEmployee` ailleurs que dans le `UserDrawer`, backend (déjà en place).
## Composants
### 1. `frontend/components/user/UserDrawer.vue`
- Supprimer le bloc détaillé employé (`hireDate`, `endDate`, `contractType`, `familySituation`, `workTimeRatio`, `annualLeaveDays`, `referencePeriodStart`, `initialLeaveBalance`, `nbChildren`).
- Conserver **uniquement** la case `isEmployee` (« Employé (soumis à la gestion des absences) »).
- Le payload de sauvegarde du user **n'envoie plus** les champs détaillés, pour ne pas les écraser. Il continue d'envoyer `isEmployee` et les champs de compte existants.
- Nettoyer l'état du formulaire et les imports devenus inutiles (options contrat / situation familiale si elles ne servent plus que là).
### 2. `frontend/pages/team-absences.vue` — onglet « Employés »
- Ajouter un 4ᵉ onglet dans `tabs` : `{ key: 'employees', label: t('absences.admin.tabs.employees'), icon: 'mdi:account-group' }`.
- Slot `#employees` : `MalioDataTable` avec les colonnes **Nom · Contrat · CP pris · CP restants**.
- Clic sur une ligne → ouvre `EmployeeDrawer` avec l'utilisateur sélectionné (cohérent avec l'onglet Demandes qui ouvre le détail au clic ligne).
- Chargement des données :
- `usersService.getAll()` filtré sur `isEmployee === true` ;
- `absenceService.getBalances({ type: 'cp' })` → map par `user.id` pour récupérer `taken` (CP pris) et `available` (CP restants) ;
- fusion en lignes de tableau (`taken`/`available` à `—` si pas de solde CP pour l'employé).
- Recharger la liste après `saved` du drawer.
### 3. `frontend/components/absence/EmployeeDrawer.vue` (nouveau)
- Props : `modelValue: boolean`, `user: UserData | null`.
- Events : `update:modelValue`, `saved`.
- Formulaire en **composants Malio** :
- `MalioDate` : `hireDate`, `endDate` (valeurs ISO `YYYY-MM-DD`) ;
- `MalioSelect` : `contractType` (CDI/CDD/Stage/Alternance/Autre), `familySituation` (Célibataire/Marié/Pacsé/Divorcé/Veuf) ;
- `MalioInputText` : `workTimeRatio`, `annualLeaveDays`, `referencePeriodStart` (MM-DD), `initialLeaveBalance`, `nbChildren`.
- À l'ouverture, initialiser le formulaire depuis `props.user` ; remettre à jour si `user` change.
- Sauvegarde : `usersService.update(user.id, { …champs employé })` ; à la réussite, émettre `saved` et fermer.
- En-tête : nom de l'employé.
### 4. i18n (`frontend/i18n/locales/fr.json`)
Nouvelles clés regroupées sous `absences.admin.employees.*` (et l'onglet sous `absences.admin.tabs.employees`) :
- onglet : `absences.admin.tabs.employees` = « Employés » ;
- colonnes liste : `absences.admin.employees.columns.{name,contract,cpTaken,cpRemaining}` ;
- drawer : `absences.admin.employees.drawer.title` + libellés de champs sous `absences.admin.employees.fields.*`.
Les libellés de contrat et de situation familiale (CDI/CDD/…, Célibataire/Marié/…) actuellement codés en dur dans `UserDrawer` sont déplacés avec leur drawer ; on les passe en clés i18n à cette occasion.
## Flux de données
```
UserDrawer --PATCH isEmployee--> User (backend)
|
team-absences (onglet Employés) |
getAll() ∩ isEmployee <-----------+
getBalances({type:'cp'}) --> map user.id -> {taken, available}
=> lignes tableau (nom, contrat, CP pris, CP restants)
|
clic ligne
v
EmployeeDrawer(user) --usersService.update--> User (backend)
|
@saved --> recharge liste
```
## Découpage / responsabilités
- `UserDrawer` : compte + flag employé. Ne connaît plus le détail RH.
- Onglet Employés : vue dérivée en lecture (jointure users ⋈ soldes), aiguille vers le drawer.
- `EmployeeDrawer` : seule unité qui édite les champs RH ; interface claire (`user` en entrée, `saved` en sortie), testable isolément.
## Vérification
- `UserDrawer` : ouvrir un user, vérifier qu'il ne reste que la case « Employé », cocher/décocher et sauvegarder sans perte des autres champs RH (qui ne sont plus envoyés).
- Onglet Employés : la liste affiche les users `isEmployee` avec CP pris/restants cohérents avec l'onglet Soldes.
- `EmployeeDrawer` : éditer un employé (dates en JJ/MM/AAAA, selects FR), sauvegarder, vérifier la persistance (recharge) et l'absence d'erreurs console.
- Vérification navigateur via Chrome DevTools sur les trois écrans.