# ESTAIR Connect – Spécification Frontend Complète
> Document de référence pour Claude Code · Nuxt 3 + Vuexy Vue.js
> Version consolidée de toutes les spécifications de session

---

## Table des matières

1. [Contexte & Objectif](#1-contexte--objectif)
2. [Stack Technique](#2-stack-technique)
3. [Structure du Projet](#3-structure-du-projet)
4. [Responsive Design](#4-responsive-design)
5. [Authentification & Sécurité (Keycloak)](#5-authentification--sécurité-keycloak)
6. [Layout & Navigation](#6-layout--navigation)
7. [Design System & Palette](#7-design-system--palette)
8. [Pages d'Authentification](#8-pages-dauthentification)
9. [Dashboard](#9-dashboard)
10. [Vue Liste Générique (CRUD)](#10-vue-liste-générique-crud)
11. [Panneau Droit – Détail / Filtre / Create-Update](#11-panneau-droit--détail--filtre--create-update)
12. [Interface Utilisateurs & Rôles](#12-interface-utilisateurs--rôles)
13. [Cards Advance](#13-cards-advance)
14. [Form Wizard](#14-form-wizard)
15. [Contrats d'Interface Backend → Frontend](#15-contrats-dinterface-backend--frontend)
16. [Permissions Keycloak](#16-permissions-keycloak)
17. [Patterns UI Globaux](#17-patterns-ui-globaux)
18. [Gestion d'État (Pinia)](#18-gestion-détat-pinia)
19. [Internationalisation](#19-internationalisation)
20. [Points en attente / Manquements identifiés](#20-points-en-attente--manquements-identifiés)

---

## 1. Contexte & Objectif

ESTAIR Connect est un système de gestion de tickets aériens intégré avec un GDS (Global Distribution System) via Amadeus/Smartflow. Il gère les PNR, l'émission de tickets, les EMDs, la tarification présale et les remboursements.

**Objectif de la refonte :** Migrer l'interface frontend existante (ChapDesign) vers un nouveau projet **Nuxt 3** en utilisant le template UI **Vuexy (Vue.js)**. ChapDesign reste la référence fonctionnelle pour les features existantes.

**Principe directeur :** Le frontend est le plus possible **piloté par les métadonnées du backend**. Il n'encode aucune logique métier : colonnes, blocs, permissions, KPIs, types de composants — tout vient de l'API.

---

## 2. Stack Technique

| Couche | Technologie |
|--------|-------------|
| Framework | Nuxt 3 (SSR ou SPA selon config) |
| UI Framework | Vuexy Vue.js Admin Template (licence achetée) |
| Composants | Vuetify 3 (base Vuexy) |
| Auth | Keycloak 23.0.4 – OIDC Authorization Code Flow |
| État | Pinia |
| HTTP | `$fetch` / `useFetch` Nuxt + intercepteurs JWT |
| Formulaires | Validation via vuelidate ou yup |
| Charts | ApexCharts (inclus Vuexy) |
| Icônes | Tabler Icons (inclus Vuexy) |
| i18n | `@nuxtjs/i18n` – langue par défaut : français |
| CSS | Vuetify utility classes + SCSS custom minimal |

### Variables d'environnement requises

```env
KEYCLOAK_URL=https://auth.estair.yourdomain.com
KEYCLOAK_REALM=estair
KEYCLOAK_CLIENT_ID=estair-app
APP_URL=https://estair.yourdomain.com
API_BASE_URL=https://api.estair.yourdomain.com
```

---

## 3. Structure du Projet

```
estair-frontend/
├── assets/
│   ├── images/
│   │   └── logo-estair.png          ← Logo ESTAIR (bleu #29ABE2 + vert #7AB648)
│   └── styles/
│       └── estair.scss              ← Surcharges CSS minimales
├── components/
│   ├── common/
│   │   ├── AppPeriodSelector.vue    ← Sélecteur période global (dashboard + listes)
│   │   ├── AppConfirmModal.vue      ← Modale confirmation actions destructives
│   │   ├── AppToast.vue             ← Système de notifications
│   │   ├── AppSkeletonTable.vue     ← Skeleton loader tableau
│   │   └── AppSkeletonKpi.vue       ← Skeleton loader KPI bande
│   ├── dashboard/
│   │   ├── WidgetRenderer.vue       ← Interprète le type de widget et charge le bon composant
│   │   ├── widgets/
│   │   │   ├── WidgetHero.vue
│   │   │   ├── WidgetKpi.vue
│   │   │   ├── WidgetBarChart.vue
│   │   │   ├── WidgetGauge.vue
│   │   │   ├── WidgetList.vue
│   │   │   └── WidgetTable.vue
│   │   └── PeriodSelector.vue
│   ├── crud/
│   │   ├── ModuleListView.vue       ← Vue liste générique (pilotée par metadata)
│   │   ├── KpiBand.vue              ← Bande KPI au-dessus du tableau
│   │   ├── DataTable.vue            ← Tableau dynamique
│   │   ├── RightPanel.vue           ← Panneau droit multi-mode
│   │   ├── DetailPanel.vue          ← Mode détail avec grow tabs
│   │   ├── FilterPanel.vue          ← Mode filtre (panneau droit)
│   │   ├── WizardPanel.vue          ← Mode create/update (form wizard)
│   │   ├── ContextSection.vue       ← Section objets contextuels (grow tabs)
│   │   ├── ContextListView.vue      ← Vue liste contextuelle (inline filtre)
│   │   └── ExportModal.vue          ← Modale export
│   ├── users/
│   │   ├── RoleCard.vue
│   │   ├── UserTable.vue
│   │   └── UserDetailView.vue
│   └── cards/
│       ├── AdvanceCardA.vue         ← Icône colorée + valeur + %
│       ├── AdvanceCardB.vue         ← Logo + progress bar
│       ├── AdvanceCardC.vue         ← Icône neutre + badge %
│       └── AdvanceCardD.vue         ← Avatar + montant + variation
├── layouts/
│   ├── default.vue                  ← Layout principal avec sidebar + topbar
│   └── auth.vue                     ← Layout split-screen (pages auth)
├── middleware/
│   ├── auth.ts                      ← Vérifie token Keycloak, redirige si expiré
│   └── permissions.ts               ← Vérifie permissions du rôle sur la route
├── pages/
│   ├── index.vue                    ← Redirige vers /dashboard
│   ├── login.vue
│   ├── forgot-password.vue
│   ├── reset-password.vue
│   ├── verify-email.vue
│   ├── two-steps.vue
│   ├── auth/
│   │   └── callback.vue             ← Handler OAuth2 code exchange
│   ├── dashboard/
│   │   └── [...slug].vue            ← Rendu dynamique des rapports via menu
│   ├── [module]/
│   │   └── index.vue                ← Vue liste générique — route: /accounts, /emds, etc.
│   ├── users/
│   │   ├── index.vue
│   │   └── [id].vue
│   └── setup/
│       └── [...slug].vue            ← Pages de configuration (type "setup")
├── plugins/
│   ├── keycloak.client.ts
│   └── api.ts                       ← Intercepteurs HTTP (refresh token, erreurs)
├── stores/
│   ├── auth.ts                      ← Token, user info, rôles Keycloak
│   ├── menu.ts                      ← Structure menu chargée depuis le backend
│   ├── metadata.ts                  ← Cache des ModuleMetadata par module
│   ├── ui.ts                        ← État panneau droit, toasts, modales
│   └── period.ts                    ← Période globale sélectionnée
├── composables/
│   ├── useModuleData.ts             ← Fetch + pagination + sort + search
│   ├── useModuleMetadata.ts         ← Chargement/cache des métadonnées
│   ├── usePermissions.ts            ← Helpers de vérification permissions
│   ├── usePeriod.ts                 ← Gestion période globale
│   └── useToast.ts                  ← Composable toast
└── nuxt.config.ts
```

---

## 4. Responsive Design

**L'application doit être entièrement responsive.** Tous les composants s'adaptent aux breakpoints Vuetify/Vuexy.

### Breakpoints

| Nom | Largeur | Comportement |
|-----|---------|--------------|
| `xs` | < 600px | Mobile — sidebar masquée, navigation bottom bar |
| `sm` | 600–960px | Petite tablette — sidebar en overlay |
| `md` | 960–1280px | Tablette/laptop — sidebar réduite (icônes seules) |
| `lg` | 1280–1920px | Desktop — sidebar pleine largeur |
| `xl` | > 1920px | Grand écran — layout étendu |

### Règles par composant

#### Sidebar / Navigation
- `lg+` : sidebar fixe 260px à gauche
- `md` : sidebar réduite 68px (icônes seules, tooltip au hover)
- `sm-` : sidebar masquée, icône hamburger dans la topbar ouvre un drawer overlay

#### Vue Liste
- `lg+` : toutes les colonnes visibles
- `md` : colonnes secondaires masquées (masquer via `hide-on-md` dans ColumnDef)
- `sm-` : tableau scrollable horizontalement OU basculer en **vue cartes** (1 ligne = 1 card)
- La bande KPI : grille 2×2 sur mobile au lieu de 4 colonnes

#### Panneau Droit
- `md+` : panneau overlay 480px (largeur fixe)
- `sm-` : **panneau plein écran** (`width: 100%`, `height: 100%`) avec bouton retour en haut

#### Dashboard
- `lg+` : grille 3 colonnes
- `md` : grille 2 colonnes
- `sm-` : grille 1 colonne, widgets empilés verticalement

#### Form Wizard
- `md+` : stepper horizontal avec numéros + labels
- `sm-` : stepper vertical (numéros seuls, labels sous le numéro)
- Champs : toujours 1 colonne sur mobile (ignorer `colSpan`)

#### Tableau contextuel
- `sm-` : vue cartes compactes au lieu du tableau classique

### Implémentation
```vue
<!-- Exemple responsive dans DataTable.vue -->
<v-data-table
  :headers="visibleHeaders"
  :items="items"
  class="elevation-0"
>
  <template #[`item.actions`]="{ item }">
    <!-- Sur mobile, actions dans un bottom sheet plutôt qu'un menu ⋮ -->
    <template v-if="$vuetify.display.smAndDown">
      <v-btn icon @click="openBottomSheet(item)">
        <v-icon>tabler-dots-vertical</v-icon>
      </v-btn>
    </template>
  </template>
</v-data-table>

// Dans useModuleMetadata.ts
const visibleHeaders = computed(() =>
  columns.value.filter(col =>
    $vuetify.display.mdAndUp ? true : !col.hideOnMobile
  )
)
```

---

## 5. Authentification & Sécurité (Keycloak)

### Flow OIDC (Authorization Code Flow — recommandé)

```
1. Utilisateur visite /login
2. Clic "Se connecter" → redirect vers Keycloak
   GET {KEYCLOAK_URL}/realms/estair/protocol/openid-connect/auth
     ?client_id=estair-app
     &redirect_uri={APP_URL}/auth/callback
     &response_type=code
     &scope=openid profile email roles
3. Keycloak authentifie → redirect vers /auth/callback?code=XXX
4. /auth/callback échange le code :
   POST {KEYCLOAK_URL}/realms/estair/protocol/openid-connect/token
     { code, client_id, redirect_uri, grant_type: 'authorization_code' }
5. Réception access_token (JWT) + refresh_token
6. Stockage en Pinia store (auth.ts) + cookie httpOnly pour SSR
7. Redirect vers /dashboard
```

### Refresh automatique du token

```typescript
// plugins/api.ts
$fetch.create({
  onRequest({ options }) {
    options.headers.Authorization = `Bearer ${authStore.accessToken}`
  },
  async onResponseError({ response }) {
    if (response.status === 401) {
      await authStore.refreshToken()
      // retry request
    }
  }
})
```

### Middleware auth.ts

Toutes les routes sauf `/login`, `/forgot-password`, `/reset-password`, `/verify-email`, `/two-steps`, `/auth/callback` sont protégées.

### Pages auth complémentaires déclenchées par Keycloak

| Condition Keycloak | Page affichée |
|-------------------|---------------|
| `email_verified: false` | `/verify-email` |
| `required_actions: ['CONFIGURE_TOTP']` | `/two-steps` |

### Token JWT — champs utilisés

```typescript
interface KeycloakTokenPayload {
  sub: string                          // user ID
  name: string                         // Nom complet
  email: string
  picture?: string                     // URL avatar
  realm_access: { roles: string[] }    // Rôles ESTAIR
  preferred_username: string
  email_verified: boolean
}
```

---

## 6. Layout & Navigation

### Layout Principal (`layouts/default.vue`)

```
┌─ Topbar (hauteur 64px, sticky) ─────────────────────────────┐
│  [☰]  [Logo ESTAIR]              [🔍] [🌙] [🔔] [Avatar]   │
└─────────────────────────────────────────────────────────────┘
┌─ Sidebar (260px) ─┬─ Contenu principal ─────────────────────┐
│                   │                                          │
│  DASHBOARDS ▼     │  <NuxtPage />                            │
│    Analytics      │                                          │
│    Contrôle       │                                          │
│                   │                                          │
│  MODULES          │                                          │
│    Accounts       │                                          │
│    Tickets        │                                          │
│    EMDs           │                                          │
│    ...            │                                          │
│                   │                                          │
│  ADMIN            │                                          │
│    Utilisateurs   │                                          │
│    Setup          │                                          │
└───────────────────┴──────────────────────────────────────────┘
```

### Menu dynamique depuis le backend (Chap)

Le menu est **entièrement produit par le backend Chap**. Le frontend consomme une route dédiée et l'interprète selon le type de page.

```typescript
// GET /api/menu → retourne la structure de navigation
interface MenuItem {
  id: string
  label: string
  icon: string           // nom icône Tabler
  type: 'crud' | 'dashboard' | 'setup' | 'group'
  route?: string         // ex: "/accounts"
  moduleId?: string      // ex: "accounts" → charge ModuleMetadata
  children?: MenuItem[]
  roles: string[]        // rôles autorisés à voir ce menu item
}
```

### Types de pages supportés

| Type | Description | Route pattern |
|------|-------------|---------------|
| `crud` | Vue liste + détail + create/update | `/{moduleId}` |
| `dashboard` | Rapport/dashboard dynamique | `/dashboard/{slug}` |
| `setup` | Configuration applicative | `/setup/{slug}` |

### Logo ESTAIR

- Remplacer le logo Vuexy par le logo ESTAIR (`assets/images/logo-estair.png`)
- Palette : bleu `#29ABE2` + vert `#7AB648` + blanc `#FFFFFF`
- Version réduite (sidebar icon-only) : icône carrée bleue avec le "E" blanc

---

## 7. Design System & Palette

### Couleurs principales

| Rôle | Hex | Usage |
|------|-----|-------|
| Primary | `#29ABE2` | CTA principaux, liens, onglet actif |
| Secondary | `#7AB648` | Accents verts, confirmations |
| Success | `#28c76f` | Badges "Actif", "Payé", variations positives |
| Warning | `#ff9f43` | Badges "En attente", alertes |
| Error | `#ea5455` | Badges "Erreur", suppression, variations négatives |
| Info | `#00cfe8` | Badges informatifs |
| Secondary neutral | `#82868b` | Badges "Inactif", textes secondaires |

### Surcharge Vuexy

```scss
// assets/styles/estair.scss
:root {
  --v-theme-primary: 41, 171, 226;      // #29ABE2
  --v-theme-secondary: 122, 182, 72;    // #7AB648
}
```

### Composants Vuexy sélectionnés

| Composant | Variante | Usage |
|-----------|----------|-------|
| Alert | Tonal | Banners d'avertissement |
| Avatar | Tonal | Photos utilisateurs, initiales |
| Button | Tonal | CTAs secondaires |
| Chip / Badge | Color | Statuts, rôles |
| Expansion Panel | Basic | (remplacé par Tabs dans détail) |
| Progress Linear | Indeterminate | Chargements |
| **Tabs** | **Grow** | **Blocs détail + objets contextuels** |
| Timeline | Outlined | Historique activité |
| Tooltip | Delay On Hover | Icônes d'action |

### Formulaires — variante Underlined

Tous les champs de formulaire utilisent la variante **Underlined** (pas Outlined) :
Autocomplete, Combobox, Date/Time, File Input, Select, Textarea, Textfield.

Exceptions : Radio (Colors), Checkbox (Basic), Switch (Basic).

---

## 8. Pages d'Authentification

Toutes les pages auth utilisent le **layout split-screen** (`layouts/auth.vue`) :
- Gauche (2/3) : illustration 3D Vuexy + éléments décoratifs, fond `#f4f5fa`
- Droite (1/3) : formulaire, fond blanc
- Logo ESTAIR en haut à gauche

**Sur mobile (`sm-`) :** seul le panneau droit (formulaire) est visible.

### 8.1 Login (`/login`)
> Fichier source Vuexy : `pages/authentication/login-v2.vue`

| Élément | Vuexy | ESTAIR |
|---------|-------|--------|
| Titre | "Welcome to Vuexy! 👋" | "Bienvenue sur ESTAIR 👋" |
| Sous-titre | "Please sign-in…" | "Connectez-vous à votre espace de gestion" |
| Champ 1 | Email or Username | Identifiant |
| Champ 2 | Password | Mot de passe |
| Checkbox | Remember me | Se souvenir de moi |
| Lien | Forgot Password? | Mot de passe oublié ? |
| Bouton | Login | Se connecter |
| Lien bas | Create an account | **Supprimer** |
| Social login | Facebook/Twitter/GitHub/Google | **Supprimer** |

**Action du bouton :** Déclenche le redirect Keycloak OIDC (pas de POST direct).

### 8.2 Mot de passe oublié (`/forgot-password`)
> Fichier source Vuexy : `pages/authentication/forgot-password-v2.vue`

Champ Email + bouton "Envoyer le lien de réinitialisation" + lien "‹ Retour".
Action : `POST /api/auth/forgot-password { email }` → backend appelle Keycloak Admin API.

### 8.3 Réinitialisation (`/reset-password`)
> Fichier source Vuexy : `pages/authentication/reset-password-v2.vue`

Champs Nouveau MDP + Confirmation + bouton "Définir le nouveau mot de passe".
Token Keycloak passé en query string. Action → POST endpoint Keycloak.

### 8.4 Vérification email (`/verify-email`)
> Fichier source Vuexy : `pages/authentication/verify-email-v2.vue`

Texte dynamique avec `{email_keycloak}` en gras. Bouton "Passer pour l'instant" + lien "Renvoyer".
Affiché si `email_verified: false` dans le token.

### 8.5 Double validation (`/two-steps`)
> Fichier source Vuexy : `pages/authentication/two-steps-v2.vue`

6 inputs OTP individuels avec focus automatique, paste intelligent.
Numéro masqué dynamique `******{4 derniers chiffres}` depuis le token.

---

## 9. Dashboard

### Principe d'architecture

Les dashboards ne sont **pas codés en dur**. Ils sont produits par le backend (Chap) et chargés via le menu. Le frontend reçoit un tableau de widgets et les rend via `WidgetRenderer.vue`.

```
GET /api/dashboard/{slug}?start=YYYY-MM-DD&end=YYYY-MM-DD&agency_id=XXX
→ { period, widgets: WidgetResponse[] }
```

**Route frontend :** `/dashboard/[...slug]`

### Sélecteur de période (composant partagé `AppPeriodSelector`)

Affiché en haut à droite de chaque dashboard et des listes avec `hasPeriod: true`.

| Label | Valeur par défaut dashboard | Valeur par défaut liste |
|-------|-----------------------------|------------------------|
| Aujourd'hui | | |
| Hier | | |
| 7 derniers jours | ✓ | |
| Ce mois | | |
| Mois dernier | | |
| 30 jours glissants | | ✓ |
| Personnalisé | | |

État persisté dans le store Pinia `period.ts`, partagé entre tous les composants de la page.

### Dashboard Analytics (W1-W10)

Référence visuelle : `demo-1/dashboards/analytics` — repliquer l'identité (palette, types de diagrammes, style KPI).

Filtrage par rôle : les rôles `agency` et `agent` voient uniquement les données de leur agence (`agency_id` issu du JWT, appliqué côté backend).

| Widget | Type | Rôles | Source modules |
|--------|------|-------|----------------|
| W1 – Hero carousel (3 slides) | HERO | tous | `etats-de-ventes`, `transaction`, `helpdesk` |
| W2 – CA moyen journalier | KPI + sparkline | tous | `etats-de-ventes` |
| W3 – Comparatif Émissions/Remb. | KPI + barre bicolore | tous | `etats-de-ventes`, `remboursement` |
| W4 – Rapport d'Émissions | BAR_CHART + sous-KPIs | tous | `etats-de-ventes`, `emds`, `remboursement` |
| W5 – Suivi Remboursements | GAUGE | admin, finance, support | `remboursement` |
| W6 – Activité par Agence | LIST (Type D) | admin, support, sale, finance | `accounts`, `etats-de-ventes` |
| W7 – Bilan Financier | BAR_CHART grouped | tous | `transaction`, `remboursement` |
| W8 – Répartition BSP | LIST (Type A) | admin, support, sale, finance | `bsps`, `bsp-details` |
| W9 – Top Compagnies | LIST (Type C) | tous | `compagnies`, `etats-de-ventes` |
| W10 – Derniers États de Vente | TABLE | tous | `etats-de-ventes` |

### Dashboard Contrôle d'Activité

Réplication du rapport ChapDesign : appel service STR, 5 contrôles.
Rôles : `admin`, `finance`, `support` uniquement.

---

## 10. Vue Liste Générique (CRUD)

> Référence visuelle : `apps/ecommerce/order/list`

Composant : `ModuleListView.vue` — piloté intégralement par `ModuleMetadata`.
Route : `/{moduleId}` (ex: `/accounts`, `/etats-de-ventes`)

### Structure complète

```
┌─ BANDE KPI (2 à 4 indicateurs) ─────────────────────────────────┐
│  [🟣 icône] 1 245        [🟢 icône] 142 300 €  [🟠 icône] 56   │
│  Tickets émis            CA total               En attente        │
├─ TOOLBAR ────────────────────────────────────────────────────────┤
│  [🔍 Rechercher…]  [⚙ Filtres [N]]  [+ Nouveau]  [10▼] [Export]│
├─ TABLEAU ────────────────────────────────────────────────────────┤
│  ☐  Col1 (lien)  Col2  Col3 (avatar)  Statut  Montant  ⋮        │
│  ☐  …                                                            │
├─ PAGINATION ─────────────────────────────────────────────────────┤
│  Affichage de 1 à 10 sur 100 entrées   «‹ 1 2 3 … 10 ›»        │
└──────────────────────────────────────────────────────────────────┘
```

### Bande KPI

Fond blanc, séparateurs verticaux, icône arrondie à droite de chaque KPI.
Données : `GET /api/{module}/kpis?start=…&end=…` — skeleton loader pendant le chargement.

### Toolbar

| Élément | Visibilité | Comportement |
|---------|-----------|--------------|
| `🔍 Rechercher` | Toujours | Debounce 300ms → `?search=` |
| `⚙ Filtres [N]` | Toujours | Ouvre panneau droit mode Filtre. N = nb filtres actifs. |
| `+ Nouveau` | Si `permission.create` | Ouvre panneau droit mode Create |
| `[10▼]` | Toujours | 10/25/50/100. Persisté localStorage. |
| `↑ Export` | Si `permission.read` | Ouvre modale Export |

### Tableau

- Colonnes définies par `ModuleMetadata.columns`
- Colonne primaire (`primary: true`) → lien violet, clic ouvre le détail
- Tri : clic sur en-tête de colonne si `sortable: true`
- Ligne sélectionnée : fond `#f3f0ff`
- Clic ligne (hors checkbox/⋮) → ouvre panneau droit mode Détail, animation slide-in 250ms ease-out
- **Sélection multiple** (checkbox) → barre d'actions bulk apparaît en bas :
  ```
  [N sélectionné(s)]  [Supprimer la sélection]  [Exporter la sélection]  [Désélectionner]
  ```

### Types de colonnes

| Type | Rendu |
|------|-------|
| `text` | Texte simple |
| `link` | Violet, cliquable → ouvre détail |
| `date` / `datetime` | Formaté locale française |
| `currency` | Montant + devise (rouge si négatif) |
| `number` | Nombre formaté |
| `badge` | Chip coloré selon `BadgeDef` |
| `dot_status` | Pastille colorée + texte |
| `avatar_text` | Avatar initiales + nom + sous-texte |
| `avatar_image` | Photo + nom + sous-texte |
| `code` | Monospace, fond gris clair |
| `boolean` | ✅ / ❌ |
| `actions` | Menu ⋮ (filtré par permissions) |

### Menu ⋮ (actions par ligne)

| Action | Permission | Style |
|--------|-----------|-------|
| Voir le détail | `read` | Normal |
| Modifier | `update` | Normal |
| Dupliquer | `create` | Normal |
| Actions métier | définies dans metadata | Normal |
| Supprimer | `delete` | Rouge |

### États vides

Quand le tableau n'a aucun résultat :
- Illustration Vuexy "no data" centrée
- Message : "Aucun résultat pour cette recherche" (si filtre actif) ou "Aucun {label_module} pour le moment" (si vide)
- Bouton `+ Nouveau` si `permission.create`

### Pagination

```
Affichage de 1 à 10 sur 100 entrées    «  ‹  1  2  3  4  …  10  ›  »
```

### Bugs connus à corriger (depuis ChapDesign)

| Bug | Description |
|-----|-------------|
| Recherche | Paramètre `search` non pris en compte dans la requête API |
| Filtre | Filtres du panneau ignorés — la requête retourne toujours le total |
| Bouton Nouveau | Absent sur certains modules (ex: PNR) |
| Picklists | Affichent "objet" au lieu de la valeur sélectionnée |
| Autocomplete | Ne fonctionne pas correctement dans certains modules |

---

## 11. Panneau Droit – Détail / Filtre / Create-Update

Panneau overlay unique, multi-mode, 480px sur desktop / 100% sur mobile.

```
Animation entrée : translateX(100%) → translateX(0), 250ms ease-out
Animation sortie : translateX(0) → translateX(100%), 200ms ease-in
Fond overlay : rgba(0,0,0,0.35)
Z-index : 200
```

Header et tabs restent **sticky** lors du scroll :
```css
.panel-header      { position: sticky; top: 0;     z-index: 10; }
.detail-tabs-bar   { position: sticky; top: 64px;  z-index: 9;  }
.context-tabs-bar  { position: sticky; top: 112px; z-index: 8;  }
```

---

### Mode 1 – Détail

**Trigger :** clic sur une ligne du tableau.

#### Structure verticale complète

```
┌─ Header ────────────────────────────────────────┐  sticky
│  👁  {label_primaire}                      ✕   │
├─ Barre d'actions ───────────────────────────────┤  sticky
│  [✏ Modifier]  [📋 Dupliquer]  [🗑 Supprimer]  │
│                                                  │
│  ══════════ SECTION DÉTAIL ══════════           │
│                                                  │
├─ Grow Tabs (blocs) ─────────────────────────────┤  sticky
│  [ Bloc A ]  [ Bloc B ]  [ Bloc C ]  [ Bloc D ] │
├─ Contenu tab actif (max-height 45vh + scroll) ──┤
│  Label      Valeur    Label     Valeur           │
│  Label      Valeur    Label     Valeur           │
│  Label long           Valeur (pleine largeur)    │
│  ...                                             │
│                                                  │
│  ══════════ SECTION CONTEXTES ══════════        │
│                                                  │
├─ Grow Tabs contextuels ─────────────────────────┤  sticky
│  [Tickets [3]] [EMDs [0]] [Factures [2]] [...]  │
├─ Vue liste contextuelle ────────────────────────┤
│  [+ Nouveau] [⚙ Filtres]  [🔍 …]  [5▼] [Export]│
│  ─────────────────────────────────────────────  │
│  ☐  Col1  Col2  Col3  Statut  ⋮                │
│  ──────────────────────────────────────────     │
│  Affichage 1-5 sur 10      «‹ 1 2 ›»           │
└─────────────────────────────────────────────────┘
```

#### Grow Tabs — blocs détail

Composant Vuexy : `<v-tabs grow>` — onglets à largeur égale, underline violet sur actif.
Chaque `BlockDef` des métadonnées = 1 tab. Labels courts (≤15 chars).
Sur mobile : tabs scrollables si overflow.

#### Contenu du tab

Grille 2 colonnes (paires label + valeur). Sur mobile : 1 colonne.
Types de valeurs : texte, badge coloré, lien, montant, date, booléen.

#### Grow Tabs — objets contextuels

Chaque objet contextuel = 1 tab avec badge count.
Badge violet si count > 0, gris si = 0. Mis à jour en temps réel.

#### Vue liste contextuelle

Identique à la vue liste principale **mais :**
- `+ Nouveau` pré-remplit le champ de liaison parent automatiquement
- Page size par défaut : **5**
- Filtre contextuel : expansion **inline** sous le toolbar (pas de panneau imbriqué)
- Colonnes : même définition que la vue principale, sauf colonne de liaison parent masquée

```
[+ Nouveau]  [⚙ Filtres [N] ▲]    [🔍 …]  [5▼]  [Export]
┌── Filtre inline (expansion) ───────────────────────────┐
│  Opérateur :  ⦿ ET   ○ OU                              │
│  [Champ ▼] [opérateur ▼] [valeur     ]  🔵 ET   ✕    │
│  [+ Ajouter]                    [Annuler] [Appliquer]  │
└────────────────────────────────────────────────────────┘
```

---

### Mode 2 – Filtre

**Trigger :** clic sur `⚙ Filtres` dans la toolbar.

```
┌─ Header ───────────────────────────────────────┐
│  ⚙  Filtres            [Réinitialiser tout] ✕ │
├─ Corps ────────────────────────────────────────┤
│  Opérateur global :  ⦿ ET   ○ OU              │
│                                                │
│  🔵 [Champ ▼]  [opérateur ▼]  [valeur    ]  ✕│
│  🔵 [Champ ▼]  [opérateur ▼]  [valeur    ]  ✕│
│                                                │
│  [+ Ajouter un filtre]                         │
├─ Footer ───────────────────────────────────────┤
│              [Annuler]        [Appliquer]       │
└────────────────────────────────────────────────┘
```

Code couleur : badge 🔵 bleu pour ET, 🟠 orange pour OU.

Opérateurs par type : voir section 15.

---

### Mode 3 – Create / Update (Form Wizard)

**Trigger :** `+ Nouveau` (create) ou `✏ Modifier` (update).

Le Form Wizard remplace les blocs collapsibles dans le panneau create/update.
Chaque `BlockDef` des métadonnées = 1 étape du wizard.

```
┌─ Header ──────────────────────────────────────────┐
│  ✏  Nouveau {label}  /  Modifier · {id}      ✕   │
├─ Stepper horizontal ──────────────────────────────┤
│  [✓1] Bloc A  >  [●2] Bloc B  >  [○3] Bloc C     │
├─ Corps étape active ──────────────────────────────┤
│  {label bloc}                                     │
│  {description bloc}                               │
│                                                   │
│  [Champ 1         ]  [Champ 2         ]           │
│  [Champ 3 (long)               ]                  │
│  [Champ conditionnel — si champ2 = X]             │
├─ Footer ──────────────────────────────────────────┤
│  [← Précédent]              [Suivant →]           │
│                     (dernière étape) [Enregistrer]│
└───────────────────────────────────────────────────┘
```

Stepper :
- Étape active : carré violet + titre bold
- Étape validée : carré avec ✓ vert
- Étape future : carré gris outline
- Étape optionnelle : label "optionnel" en gris + bouton "Ignorer"
- Sur mobile : stepper vertical

Validation avant passage à l'étape suivante. Sur `Enregistrer` : validation globale de toutes les étapes.
Sur succès : fermeture panneau + rafraîchissement silencieux tableau + toast succès.

---

### Modale Export

```
┌─ Export – {label_module} ──────────────────────┐
│  Format :  ⦿ Excel (.xlsx)   ○ CSV   ○ PDF     │
│                                                 │
│  Colonnes :  [Tout sélectionner] [Tout effacer] │
│  ☑ Champ visible dans tableau                  │
│  ☑ Champ visible dans tableau                  │
│  ☐ Champ supplémentaire                        │
│  ...                                            │
│                                                 │
│  Périmètre : ⦿ Page courante  ○ Tout exporter  │
│                                                 │
│              [Annuler]       [Exporter]          │
└─────────────────────────────────────────────────┘
```

---

### Modale Confirmation (actions destructives)

Pour : Supprimer, Suspendre, Annuler un ticket, etc.

```
┌─ Confirmer la suppression ─────────────────────┐
│                                                 │
│  Êtes-vous sûr de vouloir supprimer             │
│  {label_primaire} ?                             │
│  Cette action est irréversible.                 │
│                                                 │
│              [Annuler]   [Supprimer]            │
└─────────────────────────────────────────────────┘
```

---

## 12. Interface Utilisateurs & Rôles

> Route : `/users`  
> Référence Vuexy : `apps/roles` + `apps/user/view/{id}`

### 12.1 Vue Liste Utilisateurs (`/users`)

#### Zone haute — Cards de rôles (grille 3 colonnes)

6 cards (1 par rôle ESTAIR) + 1 card "Ajouter un rôle" (visible `admin` uniquement).

| Rôle | Couleur badge |
|------|-------------|
| Admin | Violet |
| Support | Bleu |
| Sale | Vert |
| Finance | Orange |
| Agency | Cyan |
| Agent | Gris |

Structure d'une card : Total X utilisateurs + avatars empilés `+N` + nom du rôle + lien "Modifier le rôle" + icône dupliquer.
Clic "Modifier le rôle" → modale de gestion des permissions (matrice CRUD par module).

#### Zone basse — Tableau utilisateurs

Colonnes : ☐ USER (photo + nom + email) · RÔLE (icône + label) · AGENCE (si rôle agency/agent) · STATUT (badge) · ACTIONS.

> Colonne AGENCE absente dans Vuexy original → ajout ESTAIR.
> Colonnes PLAN et BILLING de Vuexy → supprimées.

Photos : attribut `picture` du JWT Keycloak. Fallback : initiales dans cercle coloré.

Toolbar : `[Show 10▼]  [🔍 Rechercher…]  [Sélectionner un rôle ▼]`

Badges statut : Actif (vert) · Inactif (gris) · En attente (orange) · Suspendu (rouge).

### 12.2 Vue Détail Utilisateur (`/users/{id}`)

Layout 2 colonnes (réf : `apps/user/view/50`) :

**Colonne gauche (card fixe ~400px) :**
- Photo (80px, border-radius) + Nom + Badge rôle coloré
- KPI 1 : Tickets émis (count `etats-de-ventes`)
- KPI 2 : EMDs actifs (count `emds`)
- Section "Détails" : Identifiant · Email · Statut · Rôle · Code agence · Téléphone · Langue · Pays
- Boutons `Modifier` (violet) + `Suspendre` (rouge outline)

**Colonne droite (tabs) :**

| Tab | Contenu |
|-----|---------|
| Compte | Timeline activité récente (connexions, émissions, modifications) |
| Sécurité | Sessions actives Keycloak + historique connexions + reset MDP + 2FA |
| Agence | Infos agence liée (si rôle agency/agent) + solde + crédit limit |
| Permissions | Matrice lecture seule des permissions par module |
| Historique | Audit trail : log de toutes les actions |

---

## 13. Cards Advance

> Référence : `pages/cards/card-advance`

### 4 types disponibles

**Type A — Icône colorée + valeur + % variation**
Icône arrondie fond coloré + label + valeur + % vert/rouge.
Usage ESTAIR : KPI bande liste, blocs détail finances/statuts/helpdesk.

**Type B — Logo + progress bar + %**
Logo carré arrondi + titre + sous-titre + barre progression colorée.
Usage ESTAIR : BSP vs limite crédit, modules actifs par agence.

**Type C — Icône neutre + badge % coloré**
Icône gris + label + sous-label + valeur + badge coloré.
Usage ESTAIR : répartition par compagnie, canaux de distribution.

**Type D — Avatar + montant + flèche variation**
Avatar circulaire + montant + label + flèche ↑↓ + %.
Usage ESTAIR : classement agences, top compagnies, bénéficiaires.

### Carte de déploiement dans ESTAIR

| Module | Bloc contextuel | Type | Données |
|--------|----------------|------|---------|
| `accounts` | Activité financière | A | Solde · Crédit utilisé · Transactions · Remb. en attente |
| `accounts` | BSP par agence | B | Chaque BSP + barre progression vs limite |
| `etats-de-ventes` | Répartition compagnies | D | Logo + tickets + variation |
| `helpdesk` | État des tickets | A | Nouveau · En cours · Résolu · Escaladé |
| `remboursement` | Pipeline | A | Initié · En cours · Validé · Rejeté |
| `emds` | Répartition par service | C | Type service + montant + badge statut |
| `bsps` | Par agence | D | Avatar agence + montant + variation |

---

## 14. Form Wizard

> Référence : `forms/form-wizard-numbered` → variante **Validation**

Utilisé dans le panneau droit **modes Create et Update** pour tous les modules CRUD.

### Principe

Chaque `BlockDef` dans `ModuleMetadata.formBlocks` = 1 étape du wizard, dans l'ordre défini par `BlockDef.order`.

### Types de champs (`FormFieldType`)

`text` · `email` · `tel` · `number` · `password` · `textarea` · `select` · `multiselect` · `autocomplete` · `date` · `datetime` · `radio` · `checkbox` · `switch` · `file` · `currency` · `readonly`

Tous les champs utilisent la variante **Underlined** de Vuetify.

### Layout des champs

- `colSpan: 1` (défaut) → 2 champs par ligne
- `colSpan: 2` → pleine largeur
- Sur mobile : toujours 1 colonne, ignorer `colSpan`

Champs toujours pleine largeur : `textarea`, `multiselect > 4 options`, tout champ avec `dependsOn`.

### Champs conditionnels

```typescript
// FieldDef.dependsOn
dependsOn?: { field: string; value: unknown }
// Le champ n'est affiché que si la valeur de 'field' == 'value'
```

### Étapes optionnelles

`BlockDef.required = false` → bouton "Ignorer" à droite de "Suivant".
Étape ignorée : état gris dans le stepper (ni ✓ ni erreur).

---

## 15. Contrats d'Interface Backend → Frontend

### ModuleMetadata

```typescript
interface ModuleMetadata {
  moduleId: string
  label: string
  labelSingular: string
  icon: string
  permissions: { read: boolean; create: boolean; update: boolean; delete: boolean }
  kpis: ModuleKpi[]
  columns: ColumnDef[]
  filters: FilterDef[]
  actions: ActionDef[]
  detailBlocks: BlockDef[]
  formBlocks: BlockDef[]
  contextObjects: ContextObjectDef[]
  exportFields: FieldDef[]
  hasPeriod: boolean
  defaultSort: { field: string; direction: 'asc' | 'desc' }
  hideOnMobile?: string[]        // clés de colonnes à masquer sur mobile
}

interface BlockDef {
  id: string
  label: string              // ≤15 chars pour les tabs
  description?: string
  icon?: string
  order: number
  required: boolean          // false → étape optionnelle dans le wizard
  fields: FieldDef[]
  advanceCard?: AdvanceCardDef
}

interface ContextObjectDef {
  moduleId: string
  label: string
  parentField: string        // Champ de liaison, ex: "account_id"
  columns: ColumnDef[]
  defaultPageSize: 5
  actions: ActionDef[]
}

interface FieldDef {
  key: string
  label: string
  type: FormFieldType
  placeholder?: string
  required: boolean
  readonly?: boolean
  colSpan?: 1 | 2
  options?: SelectOption[]
  validation?: ValidationRule[]
  dependsOn?: { field: string; value: unknown }
  hideOnMobile?: boolean
}

interface ColumnDef {
  key: string
  label: string
  type: ColumnType
  sortable: boolean
  width?: number
  primary?: boolean          // true → lien violet
  hideOnMobile?: boolean
}

interface ModuleKpi {
  value: number | string
  label: string
  icon: string
  iconBgColor: string
  iconColor: string
  format?: 'number' | 'currency' | 'percent'
  currency?: string
}

interface ActionDef {
  id: string
  label: string
  icon: string
  permission: 'read' | 'create' | 'update' | 'delete' | string
  style?: 'default' | 'danger'
  confirm?: boolean          // true → modale de confirmation avant exécution
  confirmMessage?: string
}

interface ValidationRule {
  type: 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'email' | 'pattern' | 'custom'
  value?: number | string | RegExp
  message: string
}
```

### WidgetResponse (Dashboard)

```typescript
interface WidgetResponse {
  widgetId: string
  widgetType: 'HERO' | 'KPI' | 'BAR_CHART' | 'GAUGE' | 'LIST' | 'TABLE'
  title: string
  subtitle?: string
  period: { start: string; end: string }
  data: BarChartData | GaugeData | ListData | TableData | KpiData | HeroData
  variation?: { value: number; direction: 'up' | 'down' | 'neutral'; vsLabel: string }
}
```

### Opérateurs de filtre par type

| Type champ | Opérateurs |
|------------|-----------|
| `text` | contient · ne contient pas · égal · commence par · se termine par |
| `number` / `currency` | = · ≠ · > · ≥ · < · ≤ · entre |
| `date` | = · avant · après · entre · dans les X jours |
| `boolean` | est vrai · est faux |
| `select` / `badge` | est · n'est pas · dans la liste |

---

## 16. Permissions Keycloak

6 rôles, 26 modules, 201 règles d'autorisation.

### Rôles

| Rôle | Description |
|------|-------------|
| `admin` | 36 permissions sur 21 modules |
| `support` | 40 permissions sur 25 modules |
| `sale` | 37 permissions sur 22 modules |
| `finance` | 37 permissions sur 26 modules |
| `agency` | 16 permissions sur 11 modules (données agence uniquement) |
| `agent` | 15 permissions sur 10 modules (données agence uniquement) |

### Modules (26)

`accounts` · `banques` · `beneficiaires` · `bmp-connexions` · `bmps` · `bsp-details` · `bsps` · `compagnies` · `comptes-bancaires` · `contacts` · `credit-limit` · `document-folders` · `documents` · `emd-coupons` · `emds` · `etats-de-ventes` · `factures` · `helpdesk` · `mapping` · `mod-comments` · `remboursement` · `services` · `soldes` · `soldes-bmp` · `transaction` · `vendors`

### Règles clés

- `mod-comments` : accès CRUD pour **tous** les rôles
- `beneficiaires` : accès lecture seule pour `finance` uniquement
- `factures` : CRUD pour `admin`, lecture seule pour les autres
- `helpdesk` : CRUD pour `admin`, CRU pour `support/sale/agency/agent`, lecture pour `finance`
- `admin` : moins de permissions que `support` sur certains modules (voir matrice Excel)

### Application dans le frontend

```typescript
// composables/usePermissions.ts
const { hasPermission } = usePermissions()

// Vérification depuis le token JWT
hasPermission('accounts', 'read')    // → true/false
hasPermission('factures', 'delete')  // → true si admin
```

Le filtre `agency_id` sur les données des rôles `agency`/`agent` est appliqué **côté backend** à partir du claim JWT — jamais côté frontend.

---

## 17. Patterns UI Globaux

### Toasts / Notifications

Composant `AppToast.vue` — position haut-droite, auto-dismiss 4s.

| Type | Couleur | Usage |
|------|---------|-------|
| `success` | Vert | Enregistrement, création, suppression réussie |
| `error` | Rouge | Erreur API, validation échouée |
| `warning` | Orange | Avertissement (ex: session bientôt expirée) |
| `info` | Bleu | Information neutre |

```typescript
// composables/useToast.ts
const { showToast } = useToast()
showToast({ type: 'success', message: 'Enregistrement effectué' })
```

### Skeleton Loaders

Affichés pendant tous les appels API :
- `AppSkeletonKpi` : 4 blocs rectangles animés pour la bande KPI
- `AppSkeletonTable` : lignes grises animées pour le tableau
- `AppSkeletonPanel` : blocs dans le panneau droit

### Loading states

- Bouton `Enregistrer` : spinner inline + désactivé pendant la soumission
- Bouton `Exporter` : spinner + "Export en cours…"
- Pagination : désactivée pendant le chargement de la page suivante

### Pages d'erreur

| Route | Page |
|-------|------|
| 404 | "Page introuvable" + bouton retour dashboard |
| 403 | "Accès refusé" + message rôle insuffisant |
| 500 | "Erreur serveur" + bouton réessayer |

### Mode sombre

Vuexy supporte nativement le dark mode. Disponible via le toggle 🌙 dans la topbar.
Persisté dans localStorage. Respecter les variables CSS Vuexy — pas de couleurs hardcodées.

---

## 18. Gestion d'État (Pinia)

### `stores/auth.ts`

```typescript
{ accessToken, refreshToken, user, roles, agencyId, isAuthenticated }
actions: login(), logout(), refreshToken(), hasRole(), hasPermission()
```

### `stores/menu.ts`

```typescript
{ items: MenuItem[], loaded: boolean }
actions: fetchMenu()  // GET /api/menu — chargé au mount du layout principal
```

### `stores/metadata.ts`

Cache des `ModuleMetadata` par `moduleId`. Invalidé manuellement ou TTL 5min.

```typescript
{ cache: Map<string, ModuleMetadata> }
actions: getMetadata(moduleId)  // renvoie cache ou fetch
```

### `stores/ui.ts`

```typescript
{
  rightPanel: { open: boolean; mode: 'detail'|'filter'|'create'|'update'; data: any },
  activeDetailTab: string,
  activeContextTab: string,
  toasts: Toast[]
}
```

### `stores/period.ts`

```typescript
{ start: string; end: string; preset: string }
actions: setPeriod(start, end), setPreset(label)
// Partagé entre dashboard et toutes les vues liste avec hasPeriod: true
```

---

## 19. Internationalisation

- Langue par défaut et unique : **Français**
- Module : `@nuxtjs/i18n`
- Fichier `locales/fr.json` avec toutes les chaînes UI (libellés génériques, messages d'erreur, confirmations)
- Les libellés métier (labels de colonnes, blocs, KPIs) viennent du backend via `ModuleMetadata` — pas traduits côté frontend

---

## 20. Points en Attente / Manquements Identifiés

### À fournir par l'équipe ESTAIR

- [ ] **Liste des 26 modules avec KPIs** : définir les 3-4 indicateurs clés par module (type, champ source, label)
- [ ] **Modules avec période vs sans période** : quels modules nécessitent un sélecteur de période dans la bande KPI
- [ ] **CTAs manquants** : inventaire exhaustif des boutons d'action par module dans ChapDesign
- [ ] **Actions métier spécifiques** : liste des actions non-CRUD par module (ex: "Émettre ticket", "Valider BSP", "Rembourser")
- [ ] **Structure de menu Chap** : format exact de la réponse `GET /api/menu`
- [ ] **Pages Setup** : quels paramètres applicatifs sont configurables par l'utilisateur (actuellement dans `.env`)
- [ ] **Logo ESTAIR final** : version définitive PNG/SVG sur fond transparent

### Décisions techniques à prendre

- [ ] **SSR vs SPA** : Nuxt 3 en mode SSR (SEO) ou SPA (plus simple pour auth Keycloak) ?
- [ ] **Stockage tokens** : httpOnly cookie (SSR) vs Pinia uniquement (SPA) ?
- [ ] **Dark mode** : activé par défaut ou opt-in utilisateur ?
- [ ] **ChapDesign API** : l'API backend est-elle déjà RESTful compatible avec les contrats définis ici, ou nécessite des adaptations ?
- [ ] **Formats de dates** : format API (`ISO 8601`) et format affiché (`DD/MM/YYYY` ou `DD MMM YYYY`) à confirmer

### Points non couverts dans cette spec (à implémenter)

- [ ] **Pages Setup** : interface de configuration des paramètres applicatifs — structure à définir
- [ ] **Notifications in-app** : cloche 🔔 dans la topbar — liste des notifications, marquage lu/non-lu
- [ ] **Profil utilisateur** : page de modification du profil propre de l'utilisateur connecté (`/profile`) — référence Vuexy : `pages/account-settings/account`
- [ ] **Gestion des sessions** : déconnexion automatique à expiration, avertissement 5min avant
- [ ] **Bulk actions avancées** : au-delà de supprimer/exporter, des actions métier en masse (ex: valider plusieurs BSPs)
- [ ] **Raccourcis clavier** : `⌘K` pour la recherche globale (topbar), `Escape` pour fermer le panneau droit
- [ ] **Impression** : media query `@print` pour les exports visuels (états de vente, factures)
- [ ] **Service Worker / PWA** : l'application doit-elle fonctionner offline ou en mode PWA ?

### Corrections prioritaires (bugs ChapDesign)

1. Paramètre `search` non transmis à l'API → correction urgente dans `ModuleListView.vue`
2. Filtres du panneau non appliqués → vérifier la construction des query params `filters`
3. Bouton Nouveau absent sur PNR et autres modules → s'assurer que `permission.create` est bien lu
4. Picklists affichent "objet" → corriger le binding valeur/label dans les composants `v-select`
5. Champs autocomplete non fonctionnels → vérifier l'intégration avec l'API de recherche

---

## Annexe — Fichiers Vuexy à copier tels quels

```
pages/authentication/login-v2.vue          → /pages/login.vue
pages/authentication/forgot-password-v2.vue → /pages/forgot-password.vue
pages/authentication/reset-password-v2.vue  → /pages/reset-password.vue
pages/authentication/verify-email-v2.vue    → /pages/verify-email.vue
pages/authentication/two-steps-v2.vue       → /pages/two-steps.vue
apps/roles/index.vue                        → base pour /pages/users/index.vue
apps/user/view/[id].vue                     → base pour /pages/users/[id].vue
```

## Annexe — URLs de référence Vuexy

| Page | URL Demo |
|------|----------|
| Dashboard Analytics | `demo-1/dashboards/analytics` |
| Order List | `demo-1/apps/ecommerce/order/list` |
| Roles & Users | `demo-1/apps/roles` |
| User View | `demo-1/apps/user/view/50` |
| Login v2 | `demo-1/pages/authentication/login-v2` |
| Forgot Password v2 | `demo-1/pages/authentication/forgot-password-v2` |
| Reset Password v2 | `demo-1/pages/authentication/reset-password-v2` |
| Verify Email v2 | `demo-1/pages/authentication/verify-email-v2` |
| Two Steps v2 | `demo-1/pages/authentication/two-steps-v2` |
| Cards Advance | `demo-1/pages/cards/card-advance` |
| Tabs (Grow) | `demo-1/components/tabs` |
| Form Wizard | `demo-1/forms/form-wizard-numbered` |
