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