Skip to content

Commit 1a75676

Browse files
authored
feat(audit): separate quick and advanced diagnosis (#48)
* feat(audit): separate quick diagnosis from advanced diagnosis (#46) Add generic preliminary assessment support to the evaluation system. EROOM quick diagnosis now has its own score card, intro modal, and result modal with recommendation. Scoring formula aligned with the official EROOM Excel template. * fix(audit): hide advanced summary when only quick diagnosis answered Advanced score, context and radar chart now stay hidden until at least one category 1-6 question has an answer. Prevents step 0 alone from displaying a misleading score of 0 ("Mature") and an empty radar. Addresses remaining acceptance criteria of #46: "no detailed summary for step 0 alone" and "synthesis progressively available during advanced diagnosis".
1 parent 918244c commit 1a75676

10 files changed

Lines changed: 888 additions & 379 deletions

File tree

plans/eroom-evolution.md

Lines changed: 0 additions & 347 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Plan : Issue #46 — Séparer diagnostic rapide et diagnostic avancé
2+
3+
## Contexte
4+
5+
L'audit EROOM a 2 niveaux : catégorie 0 (Quick Diagnosis, 1-5) et catégories 1-6 (Advanced Diagnosis, qualitatif). Actuellement, seul le score avancé est stocké et affiché. `calculateQuickDiagnosisScore()` existe déjà mais n'est jamais appelé dans le flux de sauvegarde.
6+
7+
**Objectif** : afficher les deux scores séparément sur la page projet, avec un message de recommandation pour le diagnostic rapide. L'approche doit être **générique** pour supporter d'autres types d'évaluation avec étape préliminaire à l'avenir.
8+
9+
## Architecture : étape préliminaire générique
10+
11+
### Configuration dans `EVALUATION_TYPES` (`src/types/evaluation.ts`)
12+
13+
Chaque type d'évaluation peut déclarer une étape préliminaire via sa config :
14+
15+
```typescript
16+
preliminary?: {
17+
label: string; // "Quick Diagnosis" pour EROOM
18+
blocking: boolean; // true = score minimal requis pour continuer
19+
minScore?: number; // seuil minimal (utilisé si blocking: true)
20+
thresholds: { low: number; high: number };
21+
recommendations: {
22+
low: string; // score ≤ low
23+
medium: string; // low < score ≤ high
24+
high: string; // score > high
25+
};
26+
}
27+
```
28+
29+
`blocking: false` → l'utilisateur peut toujours passer aux étapes avancées. Si un futur type d'évaluation a `blocking: true` avec `minScore: 40`, l'UI empêchera de continuer si le score préliminaire est en dessous.
30+
31+
## Ce qui a été fait
32+
33+
### 1. `src/types/evaluation.ts` — Types et config ✅
34+
35+
- Interface `PreliminaryConfig` avec `label`, `blocking`, `minScore?`, `thresholds`, `recommendations`
36+
- `preliminary?: PreliminaryConfig` dans `EvaluationTypeInfo`
37+
- `preliminaryScore?: number | null` dans `Evaluation`
38+
- Config EROOM : `blocking: false`, seuils 25/50
39+
40+
### 2. `src/services/evaluation-service.ts` — Score préliminaire ✅
41+
42+
- `computePreliminaryScore()` : trouve la catégorie 0 et calcule le score
43+
- `saveEroomPreliminaryScore()` : sauvegarde explicite du score préliminaire (appelée uniquement depuis "Quick Evaluate", pas depuis l'auto-save)
44+
- `finalizeEroomEvaluation()` : sauvegarde aussi le `preliminaryScore`
45+
46+
### 3. `src/services/project-view-service.ts` — Données d'affichage ✅
47+
48+
- Interface `PreliminaryDisplayData` avec score, details, label, recommendation, blocking, blocked
49+
- `buildPreliminaryDisplayData()` : lit la config du type d'évaluation (générique)
50+
- `getPreliminaryRecommendation()` : génère la recommandation selon les seuils
51+
- `getEvaluationDisplayData()` retourne `preliminary: PreliminaryDisplayData | null`
52+
- `scoreTitle` adapté : "Advanced Diagnosis Score" quand une étape préliminaire existe
53+
54+
### 4. `src/pages/projects/view.astro` — Affichage dual ✅
55+
56+
- Carte Quick Diagnosis au-dessus de la carte Advanced (cercle w-24, `role="region"`, `aria-labelledby`)
57+
- Carte masquée par défaut, visible si `preliminaryScore !== null`
58+
- Titre du radar chart adapté dynamiquement
59+
60+
### 5. `src/pages/audit-eroom.astro` — UX du flux d'audit ✅
61+
62+
- **Modale d'intro** au chargement : explique l'échelle 1-5, bouton "Start Quick Diagnosis"
63+
- **Bouton "Quick Evaluate"** au lieu de "Next" au step 0
64+
- **Modale de résultat** : score en cercle coloré, recommandation, choix "Back to Project" / "Continue Advanced Diagnosis"
65+
- Modale ne réapparaît plus si `preliminaryScore` déjà sauvegardé (démarre au step 1)
66+
67+
### 6. `src/utils/eroom-scoring.ts` — Formule corrigée ✅
68+
69+
- Formule alignée avec l'Excel EROOM officiel : `sum(5 - answer) / (totalQuestions × 4) × 100`
70+
- Correspond à `(1 - (answer-1)/4) × weight` de l'Excel (tous poids à 1.0)
71+
- Unanswered = 0 potentiel (comme l'Excel)
72+
73+
### 7. Tests mis à jour ✅
74+
75+
- Tests de `calculateQuickDiagnosisScore` adaptés à la nouvelle formule
76+
- 2 tests ajoutés : score max (tout à 1 → 100%) et score min (tout à 5 → 0%)
77+
- 167 tests passent
78+
79+
## Ce qui ne change PAS
80+
81+
- `src/components/RadarChart.astro` — Reçoit ses données, pas concerné
82+
- Dashboard (`index.astro`) — Le score préliminaire n'est pas affiché sur les cartes
83+
- API Green Score — Pas de `preliminary` dans sa config, rien ne change
84+
85+
## Ce qui reste à faire
86+
87+
- [ ] Tests manuels approfondis (différents scénarios de scoring)
88+
- [ ] Vérifier le rendu responsive des modales et de la carte Quick Diagnosis
89+
- [ ] Vérifier le comportement avec des évaluations existantes en localStorage (rétrocompatibilité)

src/pages/audit-eroom.astro

Lines changed: 163 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,66 @@ const auditSteps: AuditStep[] = categories.map((category) => ({
6666
<AuditNavigation/>
6767
</form>
6868
</div>
69+
70+
<!-- Quick Diagnosis: Intro Modal -->
71+
<div id="introModal"
72+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
73+
role="dialog" aria-modal="true" aria-labelledby="introModalTitle">
74+
<div class="glass-card max-w-lg w-full mx-4 p-6" role="document">
75+
<h2 id="introModalTitle" class="text-xl font-bold mb-3">Quick Diagnosis</h2>
76+
<p class="text-muted mb-2">
77+
Before diving into the detailed evaluation, start with the <strong>quick diagnosis</strong>.
78+
</p>
79+
<p class="text-muted mb-2">
80+
Rate each question from 1 to 5 based on available information:
81+
</p>
82+
<ul class="text-sm text-muted mb-4 list-disc pl-5 space-y-1">
83+
<li><strong>1</strong> = not mastered / unknown</li>
84+
<li><strong>5</strong> = mastered, measured, optimized</li>
85+
</ul>
86+
<p class="text-sm text-muted mb-6">
87+
If the quick score is very low, a detailed evaluation may not be necessary.
88+
</p>
89+
<div class="flex justify-end">
90+
<button class="btn btn-primary" id="introModalClose">Start Quick Diagnosis</button>
91+
</div>
92+
</div>
93+
</div>
94+
95+
<!-- Quick Diagnosis: Result Modal -->
96+
<div id="quickResultModal"
97+
class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 backdrop-blur-sm"
98+
role="dialog" aria-modal="true" aria-labelledby="quickResultTitle">
99+
<div class="glass-card max-w-md w-full mx-4 p-6 text-center" role="document">
100+
<h2 id="quickResultTitle" class="text-xl font-bold mb-4">Quick Diagnosis Result</h2>
101+
<div class="flex justify-center mb-4">
102+
<div class="w-24 h-24 rounded-full border-6 flex items-center justify-center bg-white/5"
103+
id="quickResultCircle">
104+
<span class="text-4xl font-extrabold" id="quickResultScore">--</span>
105+
</div>
106+
</div>
107+
<p class="text-sm text-muted mb-1" id="quickResultLabel">Level</p>
108+
<p class="text-sm text-muted mt-3 leading-relaxed" id="quickResultRecommendation"></p>
109+
<div class="flex justify-center gap-3 mt-6">
110+
<a id="quickResultBackLink" href="/" class="btn btn-secondary">Back to Project</a>
111+
<button class="btn btn-primary" id="quickResultContinue">Continue Advanced Diagnosis</button>
112+
</div>
113+
</div>
114+
</div>
69115
</Layout>
70116

71117
<script>
72-
import {EvaluationType} from "../types/evaluation";
73-
import {saveEroomProgressiveScore, finalizeEroomEvaluation} from "../services/evaluation-service";
118+
import {EvaluationType, EVALUATION_TYPES} from "../types/evaluation";
119+
import {saveEroomProgressiveScore, saveEroomPreliminaryScore, finalizeEroomEvaluation, getScoreDetailsByType} from "../services/evaluation-service";
74120
import {getProject} from "../services/project-service";
75-
import {validateEroomCompletion} from "../utils/eroom-scoring";
121+
import {validateEroomCompletion, calculateQuickDiagnosisScore} from "../utils/eroom-scoring";
76122
import type {EroomCategory, EroomAnswerValue} from "../types/eroom";
77123
import eroomData from "../data/eroom-data-model-1-1.json";
78124
import {showToast} from "../utils/toast";
79125

80126
const categories = eroomData.categories as EroomCategory[];
127+
const preliminaryConfig = EVALUATION_TYPES[EvaluationType.EROOM].preliminary!;
128+
const preliminaryCategory = categories.find(c => !c.includeInScore)!;
81129
const progressCounter = document.getElementById("progressCounter");
82130

83131
let currentStep = 0;
@@ -143,15 +191,108 @@ const auditSteps: AuditStep[] = categories.map((category) => ({
143191
if (nextBtn) nextBtn.style.display = "none";
144192
if (submitBtn) submitBtn.style.display = "block";
145193
} else {
146-
if (nextBtn) nextBtn.style.display = "block";
194+
if (nextBtn) {
195+
nextBtn.style.display = "block";
196+
// Step 0 = quick diagnosis → special label
197+
nextBtn.textContent = currentStep === 0 ? 'Quick Evaluate' : 'Next';
198+
}
147199
if (submitBtn) submitBtn.style.display = "none";
148200
}
149201

150202
// Scroll to top
151203
window.scrollTo({top: 0, behavior: "smooth"});
152204
}
153205

206+
// ========================================================================
207+
// QUICK DIAGNOSIS MODALS
208+
// ========================================================================
209+
210+
const introModal = document.getElementById("introModal");
211+
const introModalClose = document.getElementById("introModalClose");
212+
const quickResultModal = document.getElementById("quickResultModal");
213+
const quickResultContinue = document.getElementById("quickResultContinue");
214+
const quickResultBackLink = document.getElementById("quickResultBackLink") as HTMLAnchorElement | null;
215+
216+
// Set back link to project view
217+
const urlParamsClient = new URLSearchParams(window.location.search);
218+
const projectIdParam = urlParamsClient.get("projectId");
219+
if (quickResultBackLink && projectIdParam) {
220+
quickResultBackLink.href = `/projects/view?id=${projectIdParam}&evaluationType=eroom`;
221+
}
222+
223+
/**
224+
* Show the quick diagnosis result modal with score and recommendation
225+
*/
226+
function showQuickResultModal() {
227+
const answers = collectAnswers();
228+
const { score } = calculateQuickDiagnosisScore(answers, preliminaryCategory.questions);
229+
const details = getScoreDetailsByType(score, EvaluationType.EROOM);
230+
231+
// Determine recommendation from config thresholds
232+
let recommendation: string;
233+
if (score <= preliminaryConfig.thresholds.low) {
234+
recommendation = preliminaryConfig.recommendations.low;
235+
} else if (score <= preliminaryConfig.thresholds.high) {
236+
recommendation = preliminaryConfig.recommendations.medium;
237+
} else {
238+
recommendation = preliminaryConfig.recommendations.high;
239+
}
240+
241+
// Update modal content
242+
const scoreEl = document.getElementById("quickResultScore");
243+
const circleEl = document.getElementById("quickResultCircle");
244+
const labelEl = document.getElementById("quickResultLabel");
245+
const recoEl = document.getElementById("quickResultRecommendation");
246+
247+
if (scoreEl) scoreEl.textContent = score.toString();
248+
if (labelEl) labelEl.textContent = details.text;
249+
if (circleEl) {
250+
circleEl.style.borderColor = details.color;
251+
circleEl.style.boxShadow = `0 0 16px ${details.shadow}`;
252+
circleEl.style.backgroundColor = details.shadow;
253+
}
254+
if (recoEl) recoEl.textContent = recommendation;
255+
256+
// Show modal
257+
if (quickResultModal) {
258+
quickResultModal.classList.remove("hidden");
259+
quickResultModal.classList.add("flex");
260+
quickResultContinue?.focus();
261+
}
262+
}
263+
264+
// Intro modal: close and focus on first question
265+
introModalClose?.addEventListener("click", () => {
266+
if (introModal) {
267+
introModal.classList.add("hidden");
268+
introModal.classList.remove("flex");
269+
}
270+
// Focus the first interactive element in the audit form
271+
const firstInput = document.querySelector('.step-content[data-step="0"] input') as HTMLElement | null;
272+
firstInput?.focus();
273+
});
274+
275+
// Quick result modal: continue to advanced diagnosis
276+
quickResultContinue?.addEventListener("click", () => {
277+
if (quickResultModal) {
278+
quickResultModal.classList.add("hidden");
279+
quickResultModal.classList.remove("flex");
280+
}
281+
currentStep = 1;
282+
updateUI();
283+
});
284+
285+
// Next button: intercept at step 0 to show quick result modal
154286
nextBtn?.addEventListener("click", () => {
287+
if (currentStep === 0) {
288+
// Save answers + preliminary score, then show result
289+
saveAnswers();
290+
if (projectId) {
291+
saveEroomPreliminaryScore(projectId, collectAnswers(), categories);
292+
}
293+
showQuickResultModal();
294+
return;
295+
}
155296
if (currentStep < totalSteps - 1) {
156297
currentStep++;
157298
updateUI();
@@ -301,6 +442,24 @@ const auditSteps: AuditStep[] = categories.map((category) => ({
301442
// Load answers on init
302443
loadAnswers();
303444

445+
// Check if quick diagnosis was already completed
446+
const hasPreliminaryScore = (() => {
447+
if (!projectId) return false;
448+
const project = getProject(projectId);
449+
const evaluation = project?.evaluations[evaluationType];
450+
return evaluation?.preliminaryScore !== undefined && evaluation?.preliminaryScore !== null;
451+
})();
452+
453+
if (hasPreliminaryScore) {
454+
// Skip intro modal and start at step 1 (advanced diagnosis)
455+
if (introModal) {
456+
introModal.classList.add("hidden");
457+
introModal.classList.remove("flex");
458+
}
459+
currentStep = 1;
460+
updateUI();
461+
}
462+
304463
// Initialize progress counter
305464
updateProgress(collectAnswers());
306465

src/pages/projects/view.astro

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,24 @@ const evaluationTypes = getAvailableEvaluationTypes()
7777
</div>
7878
</div>
7979

80-
<div class="md:col-span-1">
81-
<div class="glass-card text-center">
80+
<div class="md:col-span-1 space-y-6">
81+
<!-- Preliminary Assessment Card (hidden by default) -->
82+
<div class="glass-card text-center hidden" id="preliminaryCard" role="region" aria-labelledby="preliminaryTitle">
83+
<h3 class="text-base font-medium text-muted mb-4" id="preliminaryTitle">Quick Diagnosis</h3>
84+
<div class="flex justify-center mb-3">
85+
<div
86+
class="w-24 h-24 rounded-full border-6 flex items-center justify-center bg-white/5 shadow-[0_0_16px_rgba(0,0,0,0.1)]"
87+
id="preliminaryCircle"
88+
>
89+
<span class="text-4xl font-extrabold" id="preliminaryScore">--</span>
90+
</div>
91+
</div>
92+
<p class="text-sm text-muted" id="preliminaryLabel">Level</p>
93+
<p class="text-xs text-muted mt-3 leading-relaxed" id="preliminaryRecommendation"></p>
94+
</div>
95+
96+
<!-- Advanced / Main Score Card -->
97+
<div class="glass-card text-center" id="advancedCard" role="region" aria-labelledby="scoreTitle">
8298
<h3 class="text-base font-medium text-muted mb-4" id="scoreTitle">Current Score</h3>
8399
<div class="flex justify-center mb-4">
84100
<div
@@ -161,6 +177,15 @@ const evaluationTypes = getAvailableEvaluationTypes()
161177
const scoreCircleEl = document.getElementById('scoreCircle')
162178
const scoreContextEl = document.getElementById('scoreContext')
163179
const chartCard = document.getElementById('chartCard')
180+
const chartTitleEl = chartCard?.querySelector('h2')
181+
182+
// Preliminary card elements
183+
const preliminaryCard = document.getElementById('preliminaryCard')
184+
const preliminaryTitleEl = document.getElementById('preliminaryTitle')
185+
const preliminaryScoreEl = document.getElementById('preliminaryScore')
186+
const preliminaryCircleEl = document.getElementById('preliminaryCircle')
187+
const preliminaryLabelEl = document.getElementById('preliminaryLabel')
188+
const preliminaryRecommendationEl = document.getElementById('preliminaryRecommendation')
164189
const deleteBtn = document.getElementById('deleteBtn')
165190
const deleteModal = document.getElementById('deleteModal')
166191
const deleteProjectNameEl = document.getElementById('deleteProjectName')
@@ -249,6 +274,32 @@ const evaluationTypes = getAvailableEvaluationTypes()
249274
}
250275
}
251276

277+
// Preliminary Assessment Card
278+
if (preliminaryCard) {
279+
if (displayData.preliminary) {
280+
const { score: pScore, details, label, recommendation } = displayData.preliminary
281+
preliminaryCard.classList.remove('hidden')
282+
if (preliminaryTitleEl) preliminaryTitleEl.textContent = label
283+
if (preliminaryScoreEl) preliminaryScoreEl.textContent = pScore.toString()
284+
if (preliminaryLabelEl) preliminaryLabelEl.textContent = details.text
285+
if (preliminaryCircleEl) {
286+
preliminaryCircleEl.style.borderColor = details.color
287+
preliminaryCircleEl.style.boxShadow = `0 0 16px ${details.shadow}`
288+
preliminaryCircleEl.style.backgroundColor = details.shadow
289+
}
290+
if (preliminaryRecommendationEl) preliminaryRecommendationEl.textContent = recommendation
291+
} else {
292+
preliminaryCard.classList.add('hidden')
293+
}
294+
}
295+
296+
// Chart title adapts when a preliminary step exists
297+
if (chartTitleEl) {
298+
chartTitleEl.textContent = displayData.preliminary
299+
? 'Advanced Diagnosis - Score by Section'
300+
: 'Score by Section'
301+
}
302+
252303
// Radar Chart
253304
if (chartCard) {
254305
if (displayData.chartData.length > 0) {

0 commit comments

Comments
 (0)