Files
Lesstime/docs/superpowers/specs/2026-05-21-absence-request-form-redesign-design.md
Matthieu 2a0b202d32 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>
2026-05-22 11:31:31 +02:00

7.1 KiB

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), formatJJ/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 < startDateabsences.form.errors.endBeforeStart
  • preview.countedDays <= 0absences.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.