# CSTM Enquetes

Application de collecte de donnees terrain pour la CSTM (Compagnie Senegalaise
de Transformation des Materiaux). Elle permet a des agents terrain de visiter
des boutiques, clients existants ou prospects, et de soumettre des reponses a
des questionnaires dynamiques crees par un administrateur.

Chaque soumission correspond a une visite terrain geolocalisee.

## Stack Technique

- Backend API : Laravel 13
- Authentification API : Laravel Sanctum
- Frontend SPA : Vue.js avec Vite
- Base de donnees locale : MySQL, selon `.env`
- PHP : 8.3 via `.envrc`
- Node.js / npm : 22.18.0

Le projet Laravel et le projet Vue sont deja installes. Il ne faut pas les
reinstaller pour les evolutions courantes.

## Environnement Local

Le fichier `.envrc` ajoute PHP 8.3 au `PATH` :

```sh
export PATH="/usr/local/opt/php@8.3/bin:$PATH"
```

Apres entree dans le dossier du projet, verifier que l'environnement est bien
charge :

```sh
php -v
node -v
npm -v
```

Si `direnv` est utilise, autoriser le fichier une premiere fois :

```sh
direnv allow
```

## Commandes Utiles

Installer les dependances PHP :

```sh
composer install
```

Installer les dependances JavaScript :

```sh
npm install
```

Lancer les migrations :

```sh
php artisan migrate
```

Lancer le frontend en developpement :

```sh
npm run dev
```

Verifier le formatage PHP :

```sh
php ./vendor/bin/pint --test
```

Corriger le formatage PHP :

```sh
php ./vendor/bin/pint
```

Creer ou mettre a jour l'administrateur initial :

```sh
php artisan db:seed --class=AdminUserSeeder
```

## Layout Administrateur

Toutes les routes protegees sont rendues dans `AdminLayout.vue`. Le layout
contient :

- une sidebar avec les menus filtres selon le role de l'utilisateur ;
- une navbar avec le titre de la page, le theme, le profil et la deconnexion ;
- un bouton de navigation pour reduire la sidebar aux icones sur grand ecran ;
- un tiroir de navigation avec voile et fermeture par `Echap` sur petit ecran.

L'etat de la sidebar et du theme est gere par le store Pinia `ui` et persiste
dans le navigateur. Le layout est monte comme parent des pages authentifiees et
les pages enfants sont rendues dans son `RouterView`.

Les routes de navigation actuellement disponibles sont :

- `/` : tableau de bord ;
- `/visites` : visites terrain ;
- `/points` : points de vente ;
- `/questionnaires` : liste des questionnaires, reserve aux administrateurs ;
- `/questionnaires/nouveau` : constructeur, creation ;
- `/questionnaires/:id/modifier` : constructeur, reedition ;
- `/statistiques` : statistiques ;
- `/agents` : agents, reserve aux administrateurs ;
- `/parametres` : parametres, reserve aux administrateurs.

Les routes reservees a certains roles declarent `meta.roles` et sont filtrees
par la garde `router.beforeEach`. La navigation laterale applique le meme
filtrage via `navigationPourRole()` dans `resources/js/config/navigation.js`.

Les modules `visites`, `points`, `statistiques`, `agents` et `parametres`
utilisent provisoirement la page `EnConstruction.vue` en attendant leur
branchement sur l'API.

## Domaine Fonctionnel

L'application repose sur trois blocs principaux :

- Administration des questionnaires dynamiques.
- Collecte terrain par les agents.
- Stockage des reponses avec position GPS obligatoire.

Un questionnaire est compose de groupes de questions. Chaque groupe contient
des questions ordonnees. Selon le type de reponse, une question peut accepter du
texte, un booleen, une option unique ou plusieurs options.

Une reponse represente une visite terrain complete d'un agent dans une boutique
ou un point prospect.

## Enums

Les enums sont des backed enums PHP natifs dans `app/Enums`.

- `RoleEnum` : `admin`, `agent`
- `TypeReponseEnum` : `choix_unique`, `choix_multiple`, `texte_court`,
  `texte_long`, `oui_non`
- `TypePointEnum` : `client`, `prospect`

Les migrations utilisent des colonnes `string` pour ces valeurs. La contrainte
est applicative et les models Eloquent declarent les casts enum correspondants.

`TypeReponseEnum` expose en plus :

- `aBesoinDOptions()` : vrai pour `choix_unique` et `choix_multiple` ;
- `valeursAvecOptions()` : les valeurs concernees, utilisees en validation ;
- `libelle()` : libelle affichable.

## Authentification API

L'authentification API utilise Laravel Sanctum en mode token Bearer.

Endpoints disponibles :

- `POST /api/auth/login`
- `GET /api/auth/me`
- `POST /api/auth/logout`

Payload de connexion :

```json
{
  "email": "admin@cstm.sn",
  "password": "password",
  "device_name": "web"
}
```

Reponse de connexion :

```json
{
  "token": "...",
  "token_type": "Bearer",
  "user": {}
}
```

Les requetes protegees doivent envoyer le token :

```http
Authorization: Bearer <token>
```

L'administrateur initial est cree par `AdminUserSeeder`. Les valeurs peuvent
etre surchargees via l'environnement :

- `CSTM_ADMIN_NAME`, defaut `Administrateur CSTM`
- `CSTM_ADMIN_EMAIL`, defaut `admin@cstm.sn`
- `CSTM_ADMIN_PASSWORD`, defaut `password`
- `CSTM_ADMIN_TELEPHONE`, defaut `null`

Changer le mot de passe par defaut avant une mise en production.

## Administration Des Questionnaires

Toutes les routes sont placees derriere `auth:sanctum` puis le middleware
`admin` (`App\Http\Middleware\EnsureUserIsAdmin`, aliase dans
`bootstrap/app.php`). Un compte `agent` recoit un `403`, une requete sans token
un `401`.

Endpoints :

- `GET /api/questionnaires` : liste triee par `created_at` descendant.
- `POST /api/questionnaires` : creation complete en une requete.
- `GET /api/questionnaires/{questionnaire}` : arbre complet, pour reedition.
- `PUT /api/questionnaires/{questionnaire}` : remplacement de l'arbre.
- `DELETE /api/questionnaires/{questionnaire}` : suppression.
- `POST /api/questionnaires/{questionnaire}/activate` : activation exclusive.
- `POST /api/questionnaires/{questionnaire}/duplicate` : copie profonde inactive.

### Payload Imbrique

Groupes, questions et options sont enregistres en une seule requete, en arbre.
Il n'y a volontairement pas de controller dedie a ces niveaux.

```json
{
  "titre": "Enquete boutiques Dakar",
  "description": "Releve mensuel",
  "actif": false,
  "groupes": [
    {
      "id": "uuid ou absent",
      "titre": "Identification",
      "description": null,
      "ordre": 0,
      "questions": [
        {
          "id": "uuid ou absent",
          "libelle": "Quels produits sont en rayon ?",
          "type_reponse": "choix_multiple",
          "obligatoire": true,
          "ordre": 0,
          "options": [
            { "id": "uuid ou absent", "libelle": "Peintures", "ordre": 0 },
            { "id": "uuid ou absent", "libelle": "Colles", "ordre": 1 }
          ]
        }
      ]
    }
  ]
}
```

Synchronisation appliquee par `update`, dans une transaction :

- element avec `id` connu : mis a jour ;
- element sans `id` : cree ;
- element absent du payload : supprime, avec cascade sur ses enfants.

`ordre` est derive de la position dans le tableau par le frontend. En son
absence, le service retombe sur l'index.

### Validation

`App\Http\Requests\QuestionnaireRequest` est partagee par `store` et `update` :

- `titre` : requis, string, max 255 ;
- `description` : facultatif, max 2000 ;
- `groupes` : requis, tableau, au moins un element ;
- `groupes.*.titre` : requis ;
- `groupes.*.questions` : requis, tableau, au moins un element ;
- `groupes.*.questions.*.libelle` : requis ;
- `groupes.*.questions.*.type_reponse` : requis, valeur de `TypeReponseEnum` ;
- `groupes.*.questions.*.options` : requis et au moins deux elements si le type
  est `choix_unique` ou `choix_multiple` ;
- `groupes.*.questions.*.options.*.libelle` : requis.

`prepareForValidation` retire les options des questions dont le type n'en
attend pas, au lieu de les rejeter : le constructeur frontend peut conserver
des options residuelles apres un changement de type.

### Regles Metier

Un seul questionnaire actif dans toute l'application. `activate` desactive tous
les autres dans une transaction, et `update` avec `actif: true` applique la
meme exclusivite. Un questionnaire cree par `store` est toujours `actif = false`,
de meme qu'une copie produite par `duplicate`.

Structure verrouillee apres collecte. Des qu'une reponse existe pour un
questionnaire, le service compare une empreinte de la structure enregistree
(libelles, types, `obligatoire`, `ordre`, options) a celle du payload :

- empreintes differentes : `422` sur la cle `groupes`, avec le message de
  `QuestionnaireService::MESSAGE_VERROU` :
  `Impossible de modifier les questions d’un questionnaire ayant déjà des réponses — dupliquez-le plutôt.` ;
- empreintes identiques : `titre`, `description` et `actif` sont enregistres et
  la synchronisation de l'arbre est entierement sautee, pour ne pas recreer des
  questions referencees par `reponse_details`.

Les titres et descriptions de groupes restent modifiables apres collecte : ce ne
sont pas des questions et ils n'invalident aucune reponse.

Suppression refusee tant que des reponses existent : `422` sur la cle
`questionnaire`, avec le message de `QuestionnaireService::MESSAGE_SUPPRESSION` :
`Impossible de supprimer un questionnaire ayant déjà des réponses.`
Pour faire evoluer un questionnaire deja utilise, il faut le dupliquer.

### Reponses De L'API

`show`, `store` et `update` renvoient l'arbre complet, mis en forme par
`QuestionnaireResource` : `id`, `titre`, `description`, `actif`, `cree_par`,
`reponses_count`, `verrouille`, `groupes[]`, horodatages. Chaque groupe porte
`questions[]`, chaque question porte `type_reponse`, `type_reponse_libelle`,
`obligatoire`, `ordre` et `options[]`.

`index`, `activate` et `duplicate` renvoient la forme liste,
`QuestionnaireListeResource` : `id`, `titre`, `description`, `actif`,
`groupes_count`, `questions_count`, `reponses_count`, `verrouille`,
`created_at`.

Le champ `verrouille` vaut vrai des que `reponses_count` est superieur a zero.
Le frontend s'en sert pour desactiver l'edition de la structure.

### Service

`App\Services\QuestionnaireService` porte toute l'ecriture et garde le
controller mince :

- `creer(array $donnees, User $auteur)`
- `mettreAJour(Questionnaire $questionnaire, array $donnees)`
- `activer(Questionnaire $questionnaire)`
- `dupliquer(Questionnaire $questionnaire, User $auteur)`
- `supprimer(Questionnaire $questionnaire)`
- `arbre(Questionnaire $questionnaire)` : chargement ordonne des trois niveaux

## Constructeur De Questionnaires

Interface d'administration, dans l'espace authentifie.

Pages :

- `resources/js/pages/questionnaires/Index.vue` : liste en tableau, carte du
  questionnaire actif, menu d'actions par ligne (modifier, activer, dupliquer,
  supprimer), confirmation de suppression, etat vide et squelettes de
  chargement.
- `resources/js/pages/questionnaires/Builder.vue` : constructeur, utilise pour
  la creation et la reedition selon la presence du parametre de route `id`.

Composants :

- `resources/js/components/questionnaires/GroupeEditor.vue` : une section, son
  titre, sa description et ses questions.
- `resources/js/components/questionnaires/QuestionEditor.vue` : une question,
  son type, son caractere obligatoire et ses options.

Le reordonnancement des sections et des questions se fait par boutons flechees,
sans dependance de glisser-deposer.

`resources/js/stores/questionnaires.js` expose l'etat et les appels API. Les
actions renvoient la donnee produite ou `null`, et publient le detail de
l'erreur dans `message` et `erreurs`, comme le store d'authentification.
`erreurPour('groupes.0.questions.1.libelle')` permet d'afficher une erreur de
validation Laravel sous le champ concerne.

`resources/js/lib/questionnaire.js` centralise :

- `TYPES_REPONSE` : les cinq types, avec libelle, aide et icone ;
- `aBesoinDOptions()` : pendant frontend du helper de l'enum PHP ;
- `nouveauGroupe()`, `nouvelleQuestion()`, `nouvelleOption()` : fabriques avec
  cle locale pour les `v-for`, les elements non enregistres ayant `id = null` ;
- `versFormulaire()` : arbre API vers modele de formulaire ;
- `versPayload()` : modele de formulaire vers payload API, `ordre` derive de la
  position et options omises pour les types qui n'en attendent pas.

Les retours utilisateur passent par `vue-sonner`, monte une fois dans `App.vue`.

## Modele De Donnees

Toutes les cles primaires applicatives sont des UUID. Les models Eloquent
utilisent le trait `HasUuids`.

### users

Utilisateurs de l'application.

- `id` : UUID, cle primaire
- `name`
- `email`
- `email_verified_at`
- `password`
- `role` : `RoleEnum`, defaut `agent`
- `telephone`, nullable
- `actif`, defaut `true`
- `remember_token`
- timestamps

Relations principales :

- `questionnaires()` : questionnaires crees par l'utilisateur
- `reponses()` : visites soumises par l'agent

### questionnaires

Questionnaires dynamiques crees par l'administration.

- `id` : UUID
- `titre`
- `description`, nullable
- `actif`, defaut `false`
- `cree_par` : utilisateur createur, nullable
- timestamps

Regle metier : un seul questionnaire actif a la fois dans toute l'application.
La contrainte n'est pas dans la migration, elle est tenue par
`App\Services\QuestionnaireService` (voir « Administration Des Questionnaires »).

Relations principales :

- `creePar()`
- `groupesQuestions()`
- `questions()` : `hasManyThrough` vers toutes les questions, tous groupes
  confondus, utilise pour le compteur de la liste
- `reponses()`

### groupes_questions

Groupes ordonnes de questions dans un questionnaire.

- `id` : UUID
- `questionnaire_id`
- `titre`
- `description`, nullable
- `ordre`, defaut `0`
- timestamps

Relations principales :

- `questionnaire()`
- `questions()`

### questions

Questions rattachees a un groupe.

- `id` : UUID
- `groupe_id`
- `libelle`
- `type_reponse` : `TypeReponseEnum`
- `obligatoire`, defaut `true`
- `ordre`, defaut `0`
- timestamps

Relations principales :

- `groupe()`
- `optionsQuestion()`
- `reponseDetails()`

### options_question

Options proposees pour les questions a choix.

- `id` : UUID
- `question_id`
- `libelle`
- `ordre`, defaut `0`
- timestamps

Relations principales :

- `question()`
- `reponseDetails()`
- `selectionneeDans()`

### reponses

Soumissions terrain. Une ligne correspond a une visite d'un agent dans un point
client ou prospect.

- `id` : UUID
- `questionnaire_id`
- `agent_id`
- `nom_point`
- `type_point` : `TypePointEnum`
- `quartier`, nullable
- `commune`, nullable
- `latitude` : decimal `10,7`
- `longitude` : decimal `10,7`
- `date_soumission`
- timestamps

Index :

- `agent_id`
- `questionnaire_id`
- `type_point`

Relations principales :

- `questionnaire()`
- `agent()`
- `reponseDetails()`

### reponse_details

Valeur saisie pour une question dans une soumission.

- `id` : UUID
- `reponse_id`
- `question_id`
- `valeur_texte`, nullable
- `valeur_bool`, nullable
- `option_id`, nullable
- timestamps

Contrainte :

- unique `reponse_id`, `question_id`

Relations principales :

- `reponse()`
- `question()`
- `option()`
- `optionsSelectionnees()`

### reponse_detail_options

Table pivot pour les questions a choix multiple.

- `id` : UUID
- `reponse_detail_id`
- `option_id`
- timestamps

Contrainte :

- unique `reponse_detail_id`, `option_id`

## Fichiers De L'Administration Des Questionnaires

Les responsabilites de l'administration des questionnaires sont reparties de
la maniere suivante :

- `app/Http/Controllers/Api/QuestionnaireController.php` : sept actions API,
  avec un controller volontairement mince ;
- `app/Http/Requests/QuestionnaireRequest.php` : validation commune de la
  creation et de la mise a jour ;
- `app/Services/QuestionnaireService.php` : ecriture transactionnelle,
  synchronisation de l'arbre, exclusivite, verrouillage et duplication ;
- `app/Http/Resources/QuestionnaireResource.php` : representation complete de
  l'arbre ;
- `app/Http/Resources/QuestionnaireListeResource.php` : representation legere
  pour les listes et les actions ;
- `resources/js/stores/questionnaires.js` : etat frontend et appels API ;
- `resources/js/lib/questionnaire.js` : types, fabriques, conversion formulaire
  et construction du payload.

## Verification De La Fonctionnalite

La fonctionnalite a ete verifiee sur les parcours backend et frontend :

- backend : 38 assertions via `curl`/`fetch`, sans echec ;
- frontend : 18 assertions dans Chrome headless via CDP, sans echec ni erreur
  console ;
- `php ./vendor/bin/pint --test` : reussi ;
- `npm run build` : reussi.

Les tests backend couvrent notamment le CRUD, l'arbre imbrique, la
synchronisation a trois niveaux, la validation, l'exclusivite du questionnaire
actif, le verrouillage apres reponse, la duplication, la suppression refusee,
ainsi que les reponses `401` et `403`.

Les tests navigateur couvrent la connexion, la liste, la creation et la
reedition, le changement de type avec generation d'options, les erreurs serveur,
le mode verrouille et le theme sombre.
## Conventions De Developpement

- Ne pas ajouter de controllers, routes, vues ou seeders sans besoin explicite.
- Garder les UUID partout pour les cles primaires applicatives.
- Utiliser des backed enums PHP natifs pour les valeurs enumerees.
- Declarer les casts Eloquent pour les enums, booleens, dates et decimals.
- Garder des migrations separees et ordonnees selon les dependances de cles
  etrangeres.
- Enregistrer les structures imbriquees en une seule requete plutot qu'avec un
  controller par niveau, et placer la logique d'ecriture dans un service.
- Restreindre les routes par middleware aliase plutot que par des controles
  repetes dans chaque action.
- Ce fichier est redige en francais sans accents ; les textes affiches a
  l'utilisateur, eux, sont accentues.
