> ## Documentation Index
> Fetch the complete documentation index at: https://docs-kit.lucdidion.lu/llms.txt
> Use this file to discover all available pages before exploring further.

# Quiz Diagnostic V2

> Moteur de scoring pondéré avec recommandations personnalisées, profils et plan d'action

## Vue d'ensemble

Le système de quiz permet aux associations d'évaluer leur maturité numérique via un questionnaire interactif de **7 questions**. Le moteur V2 utilise un **scoring pondéré** pour recommander les outils les plus pertinents, déterminer un **profil utilisateur** (Débutant, Transition, Connecté) et générer un **plan d'action** personnalisé.

**Caractéristiques principales :**

* Moteur de scoring V2 avec pondération par réponse
* 3 profils : Débutant, Transition, Connecté
* Top 6 outils recommandés avec scores et raisons
* Plan d'action en 3 étapes
* Branchement conditionnel entre questions
* Résultats partageables via URL encodée
* Persistence de progression (localStorage, 24h TTL)
* Capture d'email + envoi de rapport par email (Netlify Function + Resend)
* Interface publique + gestion admin complète

***

## Architecture V2 — Scoring pondéré

### Principe

Chaque **réponse** du quiz porte des `scoring_weights` (JSONB) qui attribuent des points à des outils, catégories ou tiers de pricing. En fin de quiz, les points sont agrégés par outil pour produire un classement.

```
Réponse "Email & Newsletter" → scoring_weights:
{
  "tool:uuid-mailchimp": 5,
  "tool:uuid-brevo": 4,
  "category:communication": 2,
  "pricing:gratuit": 1
}
```

**3 types de clés supportés :**

* `tool:<id>` — Points directs pour un outil spécifique
* `category:<name>` — Points pour tous les outils d'une catégorie
* `pricing:<tier>` — Points pour tous les outils d'un tier de pricing

### Flux de scoring

```mermaid theme={null}
graph TD
    A[Réponses utilisateur] --> B[Récupérer scoring_weights par réponse]
    B --> C[Accumuler les points par outil]
    C --> D[Trier par score décroissant]
    D --> E[Top 6 outils avec raisons]

    A --> F[Déterminer le profil]
    F --> G[beginner / transition / connected]

    E --> H[Générer le plan d'action]
    G --> H
    H --> I[QuizReport complet]

    style C fill:#4F46E5
    style G fill:#3B82F6
```

***

## Tables impliquées

Le système quiz utilise **5 tables PostgreSQL** gérées via Hasura GraphQL :

**1. quizzes** — Définitions des quiz

```typescript theme={null}
interface Quiz {
  id: string;
  title: string;
  description: string | null;
  slug: string;         // Pour routing (ex: /quiz/diagnostic-digital)
  is_active: boolean;
  created_at: string;
  updated_at: string;
  questions: QuizQuestion[];
}
```

**2. quiz\_questions** — Questions du quiz

```typescript theme={null}
interface QuizQuestion {
  id: string;
  quiz_id: string;
  question_text: string;
  question_type: 'single' | 'multiple' | 'scale';
  order_index: number;
  is_required: boolean;
  help_text: string | null;
  next_question_rules?: NextQuestionRule[] | null;  // V2 : branchement conditionnel
  answers: QuizAnswer[];
}
```

**3. quiz\_answers** — Options de réponse avec scoring

```typescript theme={null}
interface QuizAnswer {
  id: string;
  question_id: string;
  answer_text: string;
  answer_value: string;
  order_index: number;
  scoring_weights?: Record<string, number>;  // V2 : pondération par réponse
}
```

**4. quiz\_recommendations** — Règles de recommandation (legacy V1)

```typescript theme={null}
interface QuizRecommendation {
  id: string;
  quiz_id: string;
  condition_logic: Record<string, any>;  // JSONB — utilisé par l'ancien moteur V1
  recommended_pack_ids: string[];
  recommended_tool_ids: string[];
  recommendation_text: string | null;
  priority: number;
}
```

<Info>
  La table `quiz_recommendations` et son `condition_logic` sont le système V1 (règles conditionnelles). Le moteur V2 utilise les `scoring_weights` sur `quiz_answers` à la place. Les deux systèmes coexistent dans le code.
</Info>

**5. quiz\_responses** — Soumissions utilisateurs

```typescript theme={null}
interface QuizResponse {
  id: string;
  quiz_id: string;
  answers: Record<string, any>;
  recommended_pack_ids: string[];
  recommended_tool_ids: string[];
  email: string | null;
  created_at: string;
}
```

***

## Moteur de scoring V2

### calculateScores()

**Fichier :** `src/api/quiz/scoring.ts`

```typescript theme={null}
export function calculateScores(
  userAnswers: QuizUserAnswers,
  answersData: AnswerScoringData[],
  tools: ToolForScoring[],
  maxTools: number   // Défaut : 6
): ScoredTool[]
```

**Algorithme :**

1. Initialise un score de 0 pour chaque outil du catalogue
2. Pour chaque réponse de l'utilisateur, trouve les `scoring_weights` correspondants
3. Pour chaque clé de poids :
   * `tool:<id>` → ajoute les points directement à l'outil
   * `category:<name>` → ajoute les points à tous les outils de la catégorie
   * `pricing:<tier>` → ajoute les points à tous les outils du tier
4. Accumule les "raisons" (texte de la question + réponse) pour expliquer le score
5. Filtre les outils avec score > 0, trie par score décroissant
6. Retourne les `maxTools` meilleurs résultats

### determineProfile()

Classifie l'utilisateur en 3 profils basés sur ses réponses :

```typescript theme={null}
export function determineProfile(
  answers: QuizUserAnswers,
  budgetQuestionId: string,
  maturityQuestionId: string,
  prioritiesQuestionId: string
): 'beginner' | 'transition' | 'connected'
```

| Profil         | Conditions                                        |
| -------------- | ------------------------------------------------- |
| **connected**  | Maturité "avancé" ET 3+ priorités                 |
| **beginner**   | Budget "0" ET maturité "aucun" ET 2 priorités max |
| **transition** | Tous les autres cas                               |

### generateSummary()

Génère un résumé personnalisé basé sur les réponses (taille d'équipe, budget, priorités).

### Types de retour

```typescript theme={null}
interface QuizReport {
  profile: 'beginner' | 'transition' | 'connected';
  profileLabel: string;      // Ex: "Explorateur Digital"
  profileDescription: string;
  summary: string;           // Résumé personnalisé
  scoredTools: ScoredTool[]; // Top 6 outils avec scores
  actionPlan: ActionStep[];  // 3 étapes concrètes
  answers: QuizUserAnswers;
  quiz_id: string;
}

interface ScoredTool {
  tool: {
    id: string;
    name: string;
    description: string | null;
    pricing_tier: string;
    logo_url: string | null;
    website_url: string | null;
    category_name: string;
  };
  score: number;
  reasons: string[];  // Max 2 raisons uniques
}

interface ActionStep {
  step: number;        // 1, 2, 3
  toolName: string;
  toolId: string;
  action: string;      // Instruction concrète
  pricingInfo: string;
}
```

***

## Branchement conditionnel

### Principe

Les questions peuvent rediriger vers d'autres questions selon la réponse choisie, via le champ `next_question_rules` :

```json theme={null}
[
  { "if_answer_value": "aucun",  "go_to_question_id": "uuid-question-3" },
  { "if_answer_value": "avance", "go_to_question_id": "uuid-question-5" }
]
```

Si aucune règle ne correspond ou si `next_question_rules` est `null`, le quiz avance séquentiellement (par `order_index`).

### Navigation

**Fichier :** `src/features/quiz/hooks/useQuiz.ts`

```typescript theme={null}
// Détermine la prochaine question selon les règles de branchement
function getNextQuestionIndex(
  currentQuestion: QuizQuestion,
  answer: string | string[] | undefined,
  allQuestions: QuizQuestion[]
): number | 'end'

// Calcule le chemin complet pour la barre de progression
function calculateQuestionPath(
  questions: QuizQuestion[],
  answers: QuizUserAnswers,
  startIndex: number
): string[]
```

**Retour arrière :** Le hook maintient un `questionHistory` (pile d'indices) pour naviguer correctement en arrière même avec le branchement.

***

## Résultats partageables

Les résultats du quiz sont encodés en base64 dans l'URL pour permettre le partage :

```
https://kit.lucdidion.lu/quiz/diagnostic-digital?r=eyJxdWl6SWQiOiJ...
```

```typescript theme={null}
// Encoder les réponses dans l'URL
function encodeAnswersToUrl(quizId: string, answers: QuizUserAnswers): string {
  const data = { quizId, answers };
  return btoa(encodeURIComponent(JSON.stringify(data)));
}

// Décoder depuis l'URL
function decodeAnswersFromUrl(encoded: string): { quizId: string; answers: QuizUserAnswers } | null
```

Quand un lien partagé est ouvert, le quiz charge directement les résultats sans repasser par les questions.

***

## Persistence de progression

La progression du quiz est sauvegardée dans localStorage pour permettre la reprise :

```typescript theme={null}
const QUIZ_STORAGE_KEY = 'kitasso_quiz_progress';
const QUIZ_PROGRESS_MAX_AGE = 24 * 60 * 60 * 1000; // 24h

// Données sauvegardées
{ slug, answers, currentStep, timestamp }
```

* Sauvegarde automatique à chaque réponse et changement d'étape
* Restauration au rechargement de la page (même slug, \< 24h)
* Nettoyage automatique après complétion du quiz ou expiration

***

## Composants UI

### Architecture des composants

```
src/features/quiz/
├── pages/
│   └── Quiz.tsx                  # Route wrapper (/quiz/:slug?)
├── components/
│   ├── QuizContainer.tsx         # Orchestrateur principal
│   ├── QuizStart.tsx             # Ecran d'introduction
│   ├── QuizQuestion.tsx          # Rendu de question (3 types)
│   ├── QuizProgress.tsx          # Barre de progression dynamique
│   ├── QuizResults.tsx           # Affichage des résultats
│   └── QuizResultsSkeleton.tsx   # Skeleton pendant le calcul
├── hooks/
│   ├── useQuiz.ts                # Hook principal (440 lignes)
│   └── useQuizAnalytics.ts      # Analytics (placeholder)
├── admin/                        # CRUD admin (6 composants)
│   ├── QuizList.tsx
│   ├── QuizForm.tsx
│   ├── QuizQuestionManager.tsx
│   ├── QuestionForm.tsx          # Editeur avec scoring weights
│   ├── QuizRecommendationManager.tsx
│   └── RecommendationForm.tsx
└── constants.ts
```

### QuizContainer — Orchestrateur

Gère le flux : **Start** -> **Questions** -> **Results**

```typescript theme={null}
// Rendu conditionnel basé sur l'état du quiz
if (isLoading) return <QuizSkeleton />;
if (isSubmitting) return <QuizResultsSkeleton />;
if (report) return <QuizResults report={report} />;
if (isIntroScreen) return <QuizStart onStart={startQuiz} />;
return <QuizQuestion question={currentQuestion} onAnswer={answerQuestion} />;
```

### QuizResults — Affichage des résultats

Affiche le rapport complet :

* Badge du profil (Débutant / Transition / Connecté)
* Résumé personnalisé
* Cartes des outils recommandés (logo, pricing, catégorie, raisons)
* Plan d'action en 3 étapes
* Formulaire de capture d'email
* Bouton de partage (copie l'URL)
* Bouton de réinitialisation

***

## Hook useQuiz

**Fichier :** `src/features/quiz/hooks/useQuiz.ts` (440 lignes)

Hook principal qui orchestre toute la logique du quiz :

```typescript theme={null}
const {
  quiz,              // Données du quiz chargé
  currentStep,       // Index de la question courante (-1 = intro)
  currentQuestion,   // Question courante ou null
  answers,           // Réponses de l'utilisateur
  isLoading,         // Chargement initial
  error,             // Message d'erreur
  report,            // QuizReport (null tant que non terminé)
  isSubmitting,      // Calcul des résultats en cours
  email,             // Email capturé
  setEmail,          // Setter pour l'email
  progress,          // 0-1, progression dynamique avec branchement
  isIntroScreen,     // true si step === -1
  isLastStep,        // true si dernière question du chemin
  shareableUrl,      // URL de partage (disponible après complétion)
  isFromSharedLink,  // true si résultats chargés depuis un lien partagé
  // Actions
  loadQuiz,          // Recharger le quiz
  answerQuestion,    // (questionId, answer) => void
  startQuiz,         // Passer de l'intro aux questions
  nextStep,          // Question suivante (avec branchement)
  prevStep,          // Question précédente (via historique)
  resetQuiz,         // Réinitialiser tout
} = useQuiz({ slug: 'diagnostic-digital' });
```

**Flux de completeQuiz() :**

1. Appelle `quizApi.getRecommendationsByScoring()` avec les réponses
2. Stocke le `QuizReport` dans le state
3. Met à jour l'URL avec le lien partageable
4. Soumet la réponse pour analytics via `quizApi.submitResponse()`

***

## Envoi de rapport par email

### Netlify Function

**Fichier :** `netlify/functions/send-report.ts`

Une Netlify Function envoie le rapport par email via l'API **Resend** :

* **Endpoint :** `POST /.netlify/functions/send-report`
* **Rate limiting :** 3 emails par IP par heure
* **Validation :** Email requis et valide, outils requis
* **CORS :** Restreint à `https://kit.lucdidion.lu`
* **Template :** Email HTML avec profil, résumé, outils recommandés, plan d'action, lien vers résultats

```typescript theme={null}
// Données envoyées à la fonction
interface ReportData {
  email: string;
  profileLabel: string;
  profileDescription: string;
  summary: string;
  tools: ReportTool[];
  actionPlan: ActionStep[];
  resultsUrl: string;
}
```

<Warning>
  La variable d'environnement `RESEND_API_KEY` doit être configurée dans les settings Netlify pour que l'envoi d'email fonctionne.
</Warning>

***

## API Layer (GraphQL)

**Fichier :** `src/api/quiz/index.ts`

```typescript theme={null}
export const quizApi = {
  // Public
  getActiveQuiz(slug: string): Promise<Quiz>,
  submitResponse(data: QuizResponseInsert): Promise<void>,

  // V2 Scoring
  getRecommendationsByScoring(
    quiz: Quiz,
    answers: QuizUserAnswers,
    questionIdMap: { teamSize, budget, maturity, priorities }
  ): Promise<QuizReport>,

  // V1 Legacy
  getRecommendations(quizId: string, answers: QuizUserAnswers): Promise<QuizRecommendation[]>,

  // Admin CRUD
  createQuiz(data: QuizFormData): Promise<Quiz>,
  updateQuiz(id: string, data: Partial<QuizFormData>): Promise<Quiz>,
  deleteQuiz(id: string): Promise<void>,

  // Questions
  createQuestion(quizId: string, data: QuestionFormData): Promise<void>,
  updateQuestion(id: string, data: QuestionFormData): Promise<void>,
  deleteQuestion(id: string): Promise<void>,

  // Recommendations (V1)
  createRecommendation(quizId: string, data: RecommendationFormData): Promise<void>,
  updateRecommendation(id: string, data: RecommendationFormData): Promise<void>,
  deleteRecommendation(id: string): Promise<void>,

  // Analytics
  getQuizResponses(quizId: string): Promise<QuizResponse[]>,
  getResponseStats(quizId: string): Promise<QuizResponseStats>,
};
```

**Query GraphQL pour charger un quiz :**

```graphql theme={null}
query GetQuizBySlug($slug: String!) {
  quizzes(where: { slug: { _eq: $slug }, is_active: { _eq: true } }) {
    id title description slug
    quiz_questions(order_by: { order_index: asc }) {
      id question_text question_type is_required help_text
      order_index next_question_rules
      quiz_answers(order_by: { order_index: asc }) {
        id answer_text answer_value scoring_weights
      }
    }
  }
}
```

***

## Permissions Hasura

* **public** : SELECT sur `quizzes` (actifs), `quiz_questions`, `quiz_answers`
* **public** : INSERT sur `quiz_responses` (soumission des réponses)
* **admin** : CRUD complet sur toutes les tables quiz

***

## Bonnes pratiques

### A faire

* Toujours utiliser `getRecommendationsByScoring()` (V2) pour les nouveaux quiz
* Configurer les `scoring_weights` sur chaque réponse dans l'admin
* Tester le branchement conditionnel en vérifiant que tous les chemins mènent à une fin
* Valider les questions requises avant de passer à la suivante
* Charger uniquement le quiz demandé par slug

### A éviter

* Utiliser l'ancien moteur V1 (`getRecommendations`) pour de nouveaux quiz
* Stocker des données sensibles dans l'URL partageable (les réponses y sont encodées)
* Oublier de configurer `RESEND_API_KEY` en production pour l'envoi d'email

***

## Ressources

<CardGroup cols={2}>
  <Card title="Admin Dashboard" icon="shield" href="/features/admin">
    Gestion complète des quiz et scoring weights
  </Card>

  <Card title="Database Schema" icon="database" href="/database/schema">
    Structure des tables quiz avec scoring\_weights
  </Card>

  <Card title="API Layer" icon="code" href="/development/api-layer">
    Utilisation de l'API GraphQL
  </Card>

  <Card title="Custom Hooks" icon="hook" href="/development/hooks">
    Documentation du hook useQuiz
  </Card>
</CardGroup>
