# #03 — Choix du stand en trois temps (surface, angles, gamme)

> Statut : ✅ **fait** — 15/08/2026. Aucune migration : le formulaire n’est pas modifié.

**Formulaire :** `offres_stands_exposants_2027` (id 58) · **Charge :** M
**Dépend de :** [#02](REEX2027-02-grille-stands-2027.md), [#00](REEX2027-00-socle-prix-formidable-js.md)

## Demande client

> Question : Peut-on ajouter des champs conditionnels pour le choix des angles ? Certains stands
> peuvent avoir qu'un angle (4m2 par exemple) et d'autres ont 2 ou 4 angles obligatoirement
> (32 et 64m2 par exemple) ?

## Décision retenue

Parcours en deux temps — **surface d'abord** (avec une fourchette de prix « de XXX € à YYY € »),
**puis nombre d'angles** — mais implémenté **en JavaScript seul, sans toucher à la structure du
formulaire Formidable**.

C'est le meilleur des deux options envisagées initialement : on obtient le parcours court sans
payer le coût du découpage en trois saisies. La saisie `radio_1` et ses 45 variantes restent la
source de vérité ; le prix, le récapitulatif de contrat et l'export CSV sont inchangés.

Conséquence importante : **#02 n'est pas invalidé.** Le risque rétroactif signalé initialement
disparaît, les deux tickets s'enchaînent.

## Combinaisons autorisées par la grille 2027

| Surface | Nu | 1 angle | 2 angles | 4 angles |
| --- | :---: | :---: | :---: | :---: |
| 4 m² | ✅ | ✅ | — | — |
| 8 m² | ✅ | ✅ | ✅ | — |
| 12 m² | ✅ | ✅ | ✅ | — |
| 16 m² | — | ✅ | ✅ | — |
| 24 m² | — | ✅ | ✅ | — |
| 32 m² | — | — | ✅ | ✅ |
| 64 m² | — | — | — | ✅ |

Chaque combinaison existe en 3 gammes : standard, *premium*, *premium +*.

## Ce qui a été fait

### Trois étapes, par-dessus la saisie existante

[stand_selector.js](../squelettes/js/stand_selector.js) masque les 47 options de `radio_1` et
construit au-dessus un dévoilement progressif : **surface → angles → gamme**. Le choix final se
contente de cocher le radio d'origine et de déclencher son événement ; `contract_form.js` fait le
reste. Prix, récapitulatif de contrat et export CSV sont inchangés, et le formulaire Formidable
n'a pas été touché — **ce ticket n'a pas de migration**.

Le masquage est fait en JavaScript, pas en CSS : si le script ne se charge pas, la liste complète
reste visible et utilisable.

### La gamme est bien une troisième étape

Point laissé ouvert par ce ticket, tranché ici : une étape dédiée plutôt que 9 options à l'étape 2.
Chaque étape reste ainsi à 7 choix maximum, et la logique de dévoilement est la même du début à la
fin. Facile à revenir en arrière si l'usage montre que c'est un clic de trop.

### Fourchettes de prix

Calculées à la volée sur les variantes concernées, dans le mode tarifaire de l'exposant — jamais
en dur. L'étape 1 affiche « de 2 160 € à 3 160 € HT » pour un 8 m², l'étape 2 resserre par nombre
d'angles, l'étape 3 donne le prix exact.

En masquant le tableau, on masquait aussi ses en-têtes de colonnes, qui disaient à l'exposant quel
tarif lui est appliqué et jusqu'à quand. Le sélecteur les **reprend** au lieu de les réécrire :
l'information est bilingue, et la date de fin du tarif fidélité y est tenue à jour par la migration
de [#02](REEX2027-02-grille-stands-2027.md).

### Les stands hors schéma se retirent d'eux-mêmes

Le ticket demandait de trancher le cas de `4m2_habille`. Aucune exception n'a été nécessaire : le
parcours ne prend en charge que les stands **visibles** et **conformes au schéma** surface × angles
× gamme. Or `contract_form.js` ne laisse qu'un seul stand visible à une jeune pousse
(`3m2_habille`) ou à un partenaire (`4m2_habille`), et ces deux clés ne se décomposent pas. Le
sélecteur constate qu'il a moins de deux stands à proposer et rend la main à la liste native.

### Lecture des clés plutôt que table de correspondance

Ce ticket recommandait de **ne pas** déduire surface et angles en analysant les clés, jugeant le
procédé fragile. La recommandation est caduque depuis #02 : les clés ne sont plus saisies à la
main, elles sont **engendrées** par cette grammaire même
([reex_stands.php](../plugins/reuseeconomyexpo/inc/reex_stands.php)). Les lire n'est donc pas une
supposition sur leur forme, c'est l'inverse de leur écriture.

Une table de correspondance aurait au contraire réintroduit exactement ce que #00 a supprimé : une
seconde liste, à tenir à jour à la main, dont l'écart avec la première ne se voit pas.

La grammaire existe désormais en deux exemplaires, PHP et JavaScript. C'est assumé, et tenu par un
test qui compare les libellés produits de part et d'autre pour les 47 clés de l'édition.

## Vérification

`node --test tests/` — 28 tests, dont 13 pour ce ticket :

- **concordance PHP ↔ JavaScript** des libellés sur les 47 clés, dans les deux langues ;
- **les combinaisons de la grille** correspondent au tableau de ce ticket (4 → nu/1 angle,
  8 et 12 → nu/1/2, 16 et 24 → 1/2, 32 → 2/4, 64 → 4), chacune en trois gammes ;
- **`composer()` est l'inverse de `decrire()`** sur toutes les clés ;
- **l'ajustement de la sélection**, qui est la seule vraie logique du parcours : conserver ce qui
  reste valide en changeant de surface, oublier ce qui devient impossible, trancher d'office quand
  une surface n'admet qu'un agencement (64 m² n'existe qu'en 4 angles), et repartir de zéro sur une
  surface disparue — le cas d'un dossier repris qui portait encore un 30 m².

Cette logique a été **extraite du fichier d'interface** vers `reex_stands.js` précisément pour
pouvoir être testée : le reste de `stand_selector.js` est du balisage.

Chaîne complète vérifiée en local : les quatre scripts sont servis dans le bon ordre.

**Ce qui n'a pas été vérifié : le rendu et les interactions dans un navigateur.** Le projet n'a pas
d'environnement de test DOM et l'accès au formulaire demande un compte exposant. Restent donc à
contrôler à l'œil : la mise en page des trois étapes, le passage au clavier, et le comportement du
lecteur d'écran sur la zone `aria-live`.

## Deux points traités que le ticket ne mentionnait pas

**Les radios masqués gardaient leur attribut `required`.** Chrome refuse d'envoyer un formulaire
dont un champ requis est invalide *et* invisible, sans rien afficher — l'exposant aurait vu un
bouton sans effet. La contrainte native est retirée et réimplémentée sur l'envoi, où l'on peut
désigner l'étape fautive et y amener le focus.

**L'emplacement des visuels est prévu** — un `<span class="stand-selector-choice-visuel">` vide dans
chaque bouton, replié par le CSS tant qu'il l'est. [#04](REEX2027-04-visuels-stands.md) n'a qu'à le
remplir, et la mise en page reste correcte sans aucune image.

## Critères d'acceptation

- ✅ Aucune combinaison absente de la grille n'est atteignable : les étapes sont construites à
  partir des clés existantes.
- ✅ La fourchette affichée correspond aux tarifs applicables à l'utilisateur — elle est lue dans la
  table, dans son mode tarifaire.
- ✅ Le montant calculé est celui de la variante cochée : le sélecteur ne fait que cocher le radio.
- ✅ Script en erreur ou absent : la liste complète reste visible et fonctionnelle.
- ✅ La reprise d'un dossier rempli restitue les trois étapes (testé sur la chaîne
  valeur → état → clé).
- ✅ Récapitulatif de contrat et export CSV inchangés — rien n'a été modifié de ce côté.
- ⏳ Contrôle visuel, clavier et lecteur d'écran, avec un compte exposant.
