# 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

Les routes de l'espace administrateur 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. Chaque espace est monte comme parent de ses pages enfants,
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 ;
- `/admin/agents` : gestion des agents, reservee 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` et `parametres` utilisent
provisoirement la page `EnConstruction.vue` en attendant leur branchement sur
l'API. La gestion des agents dispose de son propre ecran.

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

## Gestion Des Agents

La gestion des comptes agents est reservee aux administrateurs et se trouve
derriere `auth:sanctum` et le middleware `admin`.

Endpoints disponibles :

- `GET /api/agents` : liste des agents, triee du plus recent au plus ancien,
  avec le nombre de reponses soumises ;
- `POST /api/agents` : creation d'un compte actif avec mot de passe temporaire
  genere par `Str::password(12)` ;
- `PATCH /api/agents/{agent}/desactiver` : desactivation du compte ;
- `PATCH /api/agents/{agent}/reactiver` : reactivation du compte ;
- `POST /api/agents/{agent}/reinitialiser-mot-de-passe` : generation et envoi
  d'un nouveau mot de passe temporaire ;
- `DELETE /api/agents/{agent}` : suppression si aucune reponse n'est liee.

La creation et la reinitialisation envoient immediatement les identifiants via
`App\Mail\AgentCredentialsMail`. Le mot de passe temporaire est affiche une
seule fois dans l'email et n'est jamais renvoye dans la reponse JSON. Si
l'envoi echoue, le compte est conserve et la reponse contient
`email_envoye: false`.
Le mailable est volontairement synchrone et n'implemente pas `ShouldQueue`.
L'email contient le nom de l'agent, son email, le mot de passe temporaire, le
lien de connexion construit depuis `config('app.url')` et la recommandation de
modifier le mot de passe apres la premiere connexion.

La desactivation empeche la connexion : `AuthController@login` refuse tout
compte dont `actif` vaut `false`. La suppression d'un agent ayant soumis une
enquete retourne `422` avec la recommandation de le desactiver.

### Interface Frontend

`resources/js/pages/admin/AgentsListe.vue` est accessible via `/admin/agents`
dans le layout admin. Elle propose :

- la liste des agents avec nom, email, telephone, statut et nombre d'enquetes
  soumises ;
- la creation d'un agent depuis une modale avec nom, email et telephone
  optionnel ;
- la desactivation et la reactivation d'un compte ;
- la reinitialisation du mot de passe avec confirmation ;
- la suppression avec confirmation, desactivee visuellement lorsqu'une
  reponse est liee.

Apres creation, l'interface confirme l'envoi des identifiants ou affiche un
avertissement si `email_envoye` vaut `false`. La reinitialisation affiche le
meme type de retour utilisateur.

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

## Espace Agent

L'espace agent est accessible aux utilisateurs authentifies ayant le role
`agent` et utilise `AgentLayout.vue`, distinct du layout administrateur. Il est
mobile-first pour les telephones et tablettes : en-tete compact, gros controles
tactiles, menu mobile et navigation fixe en bas de l'ecran.

Il contient uniquement trois menus : `Nouvelle enquête`, `Mes enquêtes` et
`Paramètres`. Ce dernier permet a l'agent de modifier son mot de passe.

### API Des Reponses

Les routes suivantes sont accessibles a tout utilisateur authentifie via
`auth:sanctum` :

- `GET /api/questionnaire-actif` : questionnaire actif complet, avec groupes,
  questions et options ordonnes ; retourne `404` si aucun questionnaire n'est
  actif ;
- `POST /api/reponses` : enregistre une enquete complete dans une transaction ;
- `GET /api/mes-reponses` : historique de l'agent connecte, trie par date
  decroissante et pagine par 20 ;
- `GET /api/reponses/{reponse}` : detail accessible a l'auteur ou a un admin,
  sinon `403`.

La soumission contient le point visite, son type, les coordonnees GPS et, si
disponible, `precision_metres`. Le questionnaire doit toujours etre celui qui
est actif au moment de la soumission ; un changement intervenu pendant la
saisie retourne `422`. Les reponses sont controlees selon le type de chaque
question et les choix doivent appartenir au questionnaire actif.

Chaque soumission utilise l'utilisateur authentifie comme `agent_id` et fixe
`date_soumission` au moment de l'enregistrement. Les details sont crees avec
les valeurs texte, booleennes ou choix uniques, et la table pivot
`reponse_detail_options` pour les choix multiples.

### Interface Agent

Les pages frontend sont :

- `resources/js/pages/agent/NouvelleEnquete.vue` : formulaire mobile-first,
  etape 1/2 avec informations du point visite, choix client/prospect et
  capture GPS obligatoire ;
- `resources/js/pages/agent/RemplirQuestionnaire.vue` : etape 2/2 avec stepper
  par groupe, controles adaptes aux cinq types de question et soumission ;
- `resources/js/pages/agent/MesEnquetes.vue` : historique mobile-first en
  cartes empilees, pagine par 20 et accessible depuis un bouton flottant ;
- `resources/js/pages/agent/ReponseDetail.vue` : consultation du detail d'une
  enquete soumise.

Routes frontend : `/agent`, `/agent/nouvelle-enquete`,
`/agent/nouvelle-enquete/questionnaire`, `/agent/mes-enquetes/:id` et
`/agent/parametres`. L'etape 2 est inaccessible directement si l'etape 1 n'a
pas ete completee.

`resources/js/composables/useGeolocation.js` encapsule les etats
`inactif`, `en_cours`, `succes` et `erreur`, ainsi que les messages distincts
pour les erreurs de permission, de position indisponible et de delai depasse.
La position est capturee avec `navigator.geolocation.getCurrentPosition` en
haute precision ; l'envoi reste bloque tant qu'une latitude et une longitude
n'ont pas ete obtenues. `resources/js/stores/nouvelleEnquete.js` conserve les
donnees du point et la position entre les deux etapes, uniquement en memoire.

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