@fidusia/question-engine-dynamic 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/ai-questions-loader.d.ts +43 -0
- package/dist/ai-questions-loader.d.ts.map +1 -0
- package/dist/ai-questions-loader.js +131 -0
- package/dist/config-loader.d.ts +134 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +80 -0
- package/dist/engine.d.ts +81 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1243 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +98 -0
- package/package.json +29 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
// =====================================================
|
|
2
|
+
// QUESTION ENGINE - MOTEUR PRINCIPAL
|
|
3
|
+
// Gère la queue, la navigation et les triggers
|
|
4
|
+
// Version avec configuration dynamique complète
|
|
5
|
+
// =====================================================
|
|
6
|
+
import { loadAllAIQuestionsForSector, convertAIQuestionToModuleQuestion, checkConditions } from "./ai-questions-loader";
|
|
7
|
+
import { getPriorityMappings, getBookedProviderMappings, getPriorityBoosts, getSystemIds, getPriorityToBookedProviders, getPriorityCalculation, getSpecialBehaviors } from './config-loader';
|
|
8
|
+
// Import conditionnel pour la version serveur (si disponible)
|
|
9
|
+
// Utiliser une fonction helper pour charger la version serveur
|
|
10
|
+
// Note: Les fichiers *-server.ts doivent être fournis par le projet consommateur
|
|
11
|
+
async function getServerLoader() {
|
|
12
|
+
// Vérifier si on est côté client (browser)
|
|
13
|
+
// Utiliser une vérification qui fonctionne à la fois en TypeScript et à l'exécution
|
|
14
|
+
const isClient = typeof globalThis !== 'undefined' && 'window' in globalThis;
|
|
15
|
+
if (isClient) {
|
|
16
|
+
// Côté client, ne pas essayer d'importer le module serveur
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// Utiliser une chaîne construite dynamiquement pour éviter l'analyse statique de Next.js
|
|
21
|
+
// Cela empêche Next.js d'essayer de résoudre le module au moment du bundling
|
|
22
|
+
// En construisant le chemin avec des variables, Next.js ne peut pas l'analyser statiquement
|
|
23
|
+
const parts = ['./ai-questions-loader', '-server'];
|
|
24
|
+
const serverModulePath = parts.join('');
|
|
25
|
+
// Utiliser eval pour créer un import vraiment dynamique
|
|
26
|
+
// qui ne peut pas être analysé statiquement par Next.js
|
|
27
|
+
// eslint-disable-next-line no-eval
|
|
28
|
+
const serverModule = await eval(`import('${serverModulePath}')`).catch(() => null);
|
|
29
|
+
if (serverModule && typeof serverModule.loadAllAIQuestionsForSectorServer === 'function') {
|
|
30
|
+
return serverModule.loadAllAIQuestionsForSectorServer;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
// Version serveur non disponible, retourner null
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class QuestionEngine {
|
|
40
|
+
constructor(i18n) {
|
|
41
|
+
this.modules = new Map();
|
|
42
|
+
this.listeners = new Set();
|
|
43
|
+
this.advancedByTrigger = false; // Flag pour éviter double avancement
|
|
44
|
+
this.aiQuestionsCache = new Map(); // Cache de toutes les questions IA chargées
|
|
45
|
+
this.config = {}; // Configuration du chatbot
|
|
46
|
+
this.chatbotId = undefined; // ID du chatbot pour filtrer les questions IA
|
|
47
|
+
this.i18n = i18n;
|
|
48
|
+
this.state = this.createInitialState();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Définit l'ID du chatbot pour filtrer les questions IA
|
|
52
|
+
*/
|
|
53
|
+
setChatbotId(chatbotId) {
|
|
54
|
+
this.chatbotId = chatbotId;
|
|
55
|
+
}
|
|
56
|
+
// =====================================================
|
|
57
|
+
// INITIALISATION
|
|
58
|
+
// =====================================================
|
|
59
|
+
createInitialState() {
|
|
60
|
+
return {
|
|
61
|
+
queue: [],
|
|
62
|
+
currentModuleId: null,
|
|
63
|
+
currentQuestionId: null,
|
|
64
|
+
completedModules: [],
|
|
65
|
+
hiddenModules: [],
|
|
66
|
+
skippedModules: [],
|
|
67
|
+
formData: {},
|
|
68
|
+
answeredQuestions: [],
|
|
69
|
+
isComplete: false,
|
|
70
|
+
transitionInfo: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
registerModule(module) {
|
|
74
|
+
this.modules.set(module.id, module);
|
|
75
|
+
}
|
|
76
|
+
registerModules(modules) {
|
|
77
|
+
modules.forEach((m) => this.registerModule(m));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Définit la configuration du chatbot
|
|
81
|
+
* Doit être appelée avant initialize() pour que la configuration soit utilisée
|
|
82
|
+
*/
|
|
83
|
+
setConfig(config) {
|
|
84
|
+
this.config = config;
|
|
85
|
+
}
|
|
86
|
+
async initialize() {
|
|
87
|
+
this.state = this.createInitialState();
|
|
88
|
+
this.buildQueue();
|
|
89
|
+
// Charger toutes les questions IA une seule fois pour tous les secteurs
|
|
90
|
+
await this.loadAllAIQuestions();
|
|
91
|
+
// Injecter les questions IA qui correspondent aux conditions initiales
|
|
92
|
+
this.injectAIQuestionsFromCache();
|
|
93
|
+
// Activer la première question
|
|
94
|
+
this.activateFirstQuestion();
|
|
95
|
+
this.notifyListeners();
|
|
96
|
+
}
|
|
97
|
+
reset() {
|
|
98
|
+
this.initialize();
|
|
99
|
+
}
|
|
100
|
+
// Restaurer l'état depuis localStorage (pour la persistance de session)
|
|
101
|
+
async restoreState(savedState) {
|
|
102
|
+
// Sauvegarder la question actuelle AVANT la restauration
|
|
103
|
+
const savedCurrentQuestionId = savedState.currentQuestionId;
|
|
104
|
+
const savedCurrentModuleId = savedState.currentModuleId;
|
|
105
|
+
this.state = Object.assign({}, savedState);
|
|
106
|
+
// Reconstruire les références des modules dans la queue
|
|
107
|
+
this.state.queue.forEach((queuedModule) => {
|
|
108
|
+
const module = this.modules.get(queuedModule.moduleId);
|
|
109
|
+
if (module) {
|
|
110
|
+
// S'assurer que les questions du module sont synchronisées
|
|
111
|
+
queuedModule.questions.forEach((q) => {
|
|
112
|
+
const moduleQuestion = module.questions.find((mq) => mq.id === q.questionId);
|
|
113
|
+
if (moduleQuestion) {
|
|
114
|
+
q.text = moduleQuestion.text;
|
|
115
|
+
q.type = moduleQuestion.type;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// IMPORTANT: Recharger les questions IA et les réinjecter dans la queue
|
|
121
|
+
// car elles peuvent avoir changé ou de nouvelles conditions peuvent être remplies
|
|
122
|
+
await this.loadAllAIQuestions();
|
|
123
|
+
this.injectAIQuestionsFromCache();
|
|
124
|
+
// IMPORTANT: Restaurer la question actuelle après l'injection des questions IA
|
|
125
|
+
// pour éviter que l'injection ne change la question actuelle
|
|
126
|
+
if (savedCurrentQuestionId && savedCurrentModuleId) {
|
|
127
|
+
// Vérifier que la question existe toujours dans la queue
|
|
128
|
+
const queuedModule = this.state.queue.find(m => m.moduleId === savedCurrentModuleId);
|
|
129
|
+
if (queuedModule) {
|
|
130
|
+
const queuedQuestion = queuedModule.questions.find(q => q.questionId === savedCurrentQuestionId);
|
|
131
|
+
if (queuedQuestion) {
|
|
132
|
+
// La question existe toujours, restaurer la question actuelle
|
|
133
|
+
this.state.currentQuestionId = savedCurrentQuestionId;
|
|
134
|
+
this.state.currentModuleId = savedCurrentModuleId;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.notifyListeners();
|
|
139
|
+
}
|
|
140
|
+
// =====================================================
|
|
141
|
+
// CONSTRUCTION DE LA QUEUE
|
|
142
|
+
// =====================================================
|
|
143
|
+
buildQueue() {
|
|
144
|
+
const queuedModules = [];
|
|
145
|
+
this.modules.forEach((module) => {
|
|
146
|
+
// Vérifier les triggers au niveau module
|
|
147
|
+
if (this.shouldHideModule(module)) {
|
|
148
|
+
this.state.hiddenModules.push(module.id);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const queuedModule = {
|
|
152
|
+
moduleId: module.id,
|
|
153
|
+
moduleName: module.name,
|
|
154
|
+
effectivePriority: module.basePriority,
|
|
155
|
+
status: "pending",
|
|
156
|
+
questions: this.buildModuleQuestions(module),
|
|
157
|
+
};
|
|
158
|
+
queuedModules.push(queuedModule);
|
|
159
|
+
});
|
|
160
|
+
// Trier par priorité effective
|
|
161
|
+
queuedModules.sort((a, b) => a.effectivePriority - b.effectivePriority);
|
|
162
|
+
this.state.queue = queuedModules;
|
|
163
|
+
}
|
|
164
|
+
buildModuleQuestions(module) {
|
|
165
|
+
// Exclure les questions IA lors de la construction initiale de la queue
|
|
166
|
+
// Elles seront ré-injectées dynamiquement depuis les métadonnées
|
|
167
|
+
return module.questions
|
|
168
|
+
.filter((q) => !q.aiQuestionMetadata) // Exclure les questions IA
|
|
169
|
+
.map((q) => ({
|
|
170
|
+
questionId: q.id,
|
|
171
|
+
moduleId: module.id,
|
|
172
|
+
text: q.text,
|
|
173
|
+
type: q.type,
|
|
174
|
+
status: "pending",
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Charge toutes les questions IA pour tous les secteurs une seule fois
|
|
179
|
+
* Utilise la version serveur si disponible (API routes), sinon la version fetch (client)
|
|
180
|
+
* OU extrait depuis les métadonnées des modules si disponibles (pour JSON)
|
|
181
|
+
*/
|
|
182
|
+
async loadAllAIQuestions() {
|
|
183
|
+
// D'abord, essayer d'extraire les questions IA depuis les métadonnées des modules (pour JSON)
|
|
184
|
+
const aiQuestionsFromMetadata = this.extractAIQuestionsFromModules();
|
|
185
|
+
if (aiQuestionsFromMetadata.size > 0) {
|
|
186
|
+
// Utiliser les questions IA extraites depuis les métadonnées
|
|
187
|
+
aiQuestionsFromMetadata.forEach((questions, sectorId) => {
|
|
188
|
+
this.aiQuestionsCache.set(sectorId, questions);
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Sinon, charger depuis l'API (comportement normal)
|
|
193
|
+
// Essayer de charger la version serveur (disponible uniquement côté serveur)
|
|
194
|
+
const serverLoader = await getServerLoader();
|
|
195
|
+
// Parcourir tous les modules dans la queue pour charger leurs questions IA
|
|
196
|
+
const loadPromises = this.state.queue.map(async (queuedModule) => {
|
|
197
|
+
// Utiliser la version serveur si disponible (côté serveur), sinon la version fetch (côté client)
|
|
198
|
+
const loader = serverLoader || loadAllAIQuestionsForSector;
|
|
199
|
+
// Passer le chatbot_id au loader
|
|
200
|
+
const aiQuestions = await loader(queuedModule.moduleId, this.chatbotId);
|
|
201
|
+
this.aiQuestionsCache.set(queuedModule.moduleId, aiQuestions);
|
|
202
|
+
});
|
|
203
|
+
await Promise.all(loadPromises);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Extrait les questions IA depuis les métadonnées des modules (pour import JSON)
|
|
207
|
+
* Retourne une Map<sectorId, AIQuestion[]>
|
|
208
|
+
*/
|
|
209
|
+
extractAIQuestionsFromModules() {
|
|
210
|
+
const aiQuestionsMap = new Map();
|
|
211
|
+
// Parcourir tous les modules enregistrés
|
|
212
|
+
for (const [moduleId, module] of this.modules.entries()) {
|
|
213
|
+
const aiQuestions = [];
|
|
214
|
+
// Parcourir toutes les questions du module
|
|
215
|
+
for (const question of module.questions) {
|
|
216
|
+
// Si la question a des métadonnées IA, l'extraire
|
|
217
|
+
if (question.aiQuestionMetadata) {
|
|
218
|
+
const metadata = question.aiQuestionMetadata;
|
|
219
|
+
aiQuestions.push({
|
|
220
|
+
id: question.id, // Utiliser l'ID de la question comme ID
|
|
221
|
+
sectorId: metadata.sectorId || moduleId,
|
|
222
|
+
questionId: question.id,
|
|
223
|
+
questionText: question.text,
|
|
224
|
+
questionType: question.type,
|
|
225
|
+
options: question.options || null,
|
|
226
|
+
placeholder: question.placeholder || null,
|
|
227
|
+
minValue: question.min || null,
|
|
228
|
+
maxValue: question.max || null,
|
|
229
|
+
conditions: metadata.conditions || null,
|
|
230
|
+
isActive: metadata.isActive !== false, // Par défaut true
|
|
231
|
+
parentQuestionId: metadata.parentQuestionId || null,
|
|
232
|
+
nextQuestionId: metadata.nextQuestionId || null,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (aiQuestions.length > 0) {
|
|
237
|
+
aiQuestionsMap.set(moduleId, aiQuestions);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return aiQuestionsMap;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Injecte les questions IA depuis le cache selon les conditions actuelles
|
|
244
|
+
* Évite les doublons en vérifiant si la question est déjà dans la queue
|
|
245
|
+
* Gère l'injection récursive des questions IA qui dépendent d'autres questions IA
|
|
246
|
+
*/
|
|
247
|
+
injectAIQuestionsFromCache() {
|
|
248
|
+
// Parcourir tous les modules dans la queue
|
|
249
|
+
for (const queuedModule of this.state.queue) {
|
|
250
|
+
const module = this.modules.get(queuedModule.moduleId);
|
|
251
|
+
if (!module)
|
|
252
|
+
continue;
|
|
253
|
+
// Récupérer les questions IA depuis le cache pour ce secteur
|
|
254
|
+
const allAIQuestions = this.aiQuestionsCache.get(queuedModule.moduleId) || [];
|
|
255
|
+
// Filtrer selon les conditions actuelles
|
|
256
|
+
const aiQuestions = allAIQuestions.filter(q => checkConditions(q.conditions, this.state.formData));
|
|
257
|
+
if (aiQuestions.length === 0)
|
|
258
|
+
continue;
|
|
259
|
+
// Récupérer les IDs des questions déjà dans la queue pour éviter les doublons
|
|
260
|
+
const existingQuestionIds = new Set(queuedModule.questions.map(q => q.questionId));
|
|
261
|
+
// Récupérer les IDs des questions déjà dans le module
|
|
262
|
+
const existingModuleQuestionIds = new Set(module.questions.map(q => q.id));
|
|
263
|
+
// Fonction récursive pour injecter une question IA et ses dépendances
|
|
264
|
+
const injectQuestionRecursive = (aiQuestion, processedIds) => {
|
|
265
|
+
// Éviter les cycles et les doublons
|
|
266
|
+
if (processedIds.has(aiQuestion.questionId) || existingQuestionIds.has(aiQuestion.questionId)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Si la question a un parent qui est aussi une question IA, injecter d'abord le parent
|
|
270
|
+
if (aiQuestion.parentQuestionId && !existingQuestionIds.has(aiQuestion.parentQuestionId)) {
|
|
271
|
+
const parentAIQuestion = allAIQuestions.find(q => q.questionId === aiQuestion.parentQuestionId);
|
|
272
|
+
if (parentAIQuestion && checkConditions(parentAIQuestion.conditions, this.state.formData)) {
|
|
273
|
+
// Injecter récursivement le parent d'abord
|
|
274
|
+
injectQuestionRecursive(parentAIQuestion, processedIds);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Maintenant injecter cette question
|
|
278
|
+
processedIds.add(aiQuestion.questionId);
|
|
279
|
+
// Convertir en ModuleQuestion
|
|
280
|
+
const moduleQuestion = convertAIQuestionToModuleQuestion(aiQuestion);
|
|
281
|
+
// Vérifier si la question n'est pas déjà dans le module
|
|
282
|
+
if (existingModuleQuestionIds.has(aiQuestion.questionId)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Ajouter au module
|
|
286
|
+
module.questions.push(moduleQuestion);
|
|
287
|
+
existingModuleQuestionIds.add(aiQuestion.questionId);
|
|
288
|
+
// Créer la question dans la queue
|
|
289
|
+
const queuedQuestion = {
|
|
290
|
+
questionId: aiQuestion.questionId,
|
|
291
|
+
moduleId: queuedModule.moduleId,
|
|
292
|
+
text: aiQuestion.questionText,
|
|
293
|
+
type: aiQuestion.questionType,
|
|
294
|
+
status: "pending",
|
|
295
|
+
};
|
|
296
|
+
// Si parentQuestionId est défini, injecter après cette question
|
|
297
|
+
if (aiQuestion.parentQuestionId) {
|
|
298
|
+
// Chercher dans la queue ET dans le module (pour les questions statiques)
|
|
299
|
+
const queuedParentIndex = queuedModule.questions.findIndex(q => q.questionId === aiQuestion.parentQuestionId);
|
|
300
|
+
// Si pas trouvé dans la queue, chercher dans le module (question statique)
|
|
301
|
+
if (queuedParentIndex < 0) {
|
|
302
|
+
const parentInModule = module.questions.find(q => q.id === aiQuestion.parentQuestionId);
|
|
303
|
+
if (parentInModule) {
|
|
304
|
+
// Trouver où insérer après la question statique dans la queue
|
|
305
|
+
// Chercher la dernière question qui correspond à une question du module avant ou égale au parent
|
|
306
|
+
let insertIndex = -1;
|
|
307
|
+
for (let i = queuedModule.questions.length - 1; i >= 0; i--) {
|
|
308
|
+
const q = queuedModule.questions[i];
|
|
309
|
+
if (q.questionId === aiQuestion.parentQuestionId) {
|
|
310
|
+
insertIndex = i + 1;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
// Si on trouve une question qui vient avant le parent dans le module, insérer après
|
|
314
|
+
const qInModule = module.questions.find(mq => mq.id === q.questionId);
|
|
315
|
+
if (qInModule) {
|
|
316
|
+
// Vérifier si cette question vient avant le parent dans l'ordre du module
|
|
317
|
+
const parentIndex = module.questions.findIndex(mq => mq.id === aiQuestion.parentQuestionId);
|
|
318
|
+
const currentIndex = module.questions.findIndex(mq => mq.id === q.questionId);
|
|
319
|
+
if (currentIndex < parentIndex) {
|
|
320
|
+
insertIndex = i + 1;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (insertIndex >= 0) {
|
|
326
|
+
queuedModule.questions.splice(insertIndex, 0, queuedQuestion);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
// Si on ne trouve pas où insérer, chercher la position après la question statique
|
|
330
|
+
// en suivant le nextInModule
|
|
331
|
+
const findInsertPosition = (parentId) => {
|
|
332
|
+
const parentQ = module.questions.find(q => q.id === parentId);
|
|
333
|
+
if (!parentQ || !parentQ.nextInModule) {
|
|
334
|
+
return queuedModule.questions.length;
|
|
335
|
+
}
|
|
336
|
+
// Chercher si nextInModule est déjà dans la queue
|
|
337
|
+
const nextIndex = queuedModule.questions.findIndex(q => q.questionId === parentQ.nextInModule);
|
|
338
|
+
if (nextIndex >= 0) {
|
|
339
|
+
return nextIndex;
|
|
340
|
+
}
|
|
341
|
+
// Sinon, chercher récursivement
|
|
342
|
+
return findInsertPosition(parentQ.nextInModule);
|
|
343
|
+
};
|
|
344
|
+
const insertPos = findInsertPosition(aiQuestion.parentQuestionId);
|
|
345
|
+
queuedModule.questions.splice(insertPos, 0, queuedQuestion);
|
|
346
|
+
}
|
|
347
|
+
// Modifier le nextInModule de la question parente pour pointer vers la question IA
|
|
348
|
+
if (parentInModule.nextInModule !== aiQuestion.questionId) {
|
|
349
|
+
const originalNextInModule = parentInModule.nextInModule;
|
|
350
|
+
parentInModule.nextInModule = aiQuestion.questionId;
|
|
351
|
+
moduleQuestion.nextInModule = originalNextInModule;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Parent non trouvé (ni dans la queue, ni dans le module)
|
|
356
|
+
// Essayer de trouver une position logique en cherchant la dernière question injectée
|
|
357
|
+
// ou en cherchant une question statique qui pourrait être un parent logique
|
|
358
|
+
// Si le parent est une question IA qui n'existe pas, chercher la dernière question IA injectée
|
|
359
|
+
// ou la dernière question statique du module
|
|
360
|
+
let insertIndex = queuedModule.questions.length;
|
|
361
|
+
// Chercher la dernière question statique du module dans la queue
|
|
362
|
+
for (let i = queuedModule.questions.length - 1; i >= 0; i--) {
|
|
363
|
+
const q = queuedModule.questions[i];
|
|
364
|
+
const qInModule = module.questions.find(mq => mq.id === q.questionId);
|
|
365
|
+
if (qInModule && !allAIQuestions.find(aq => aq.questionId === q.questionId)) {
|
|
366
|
+
// C'est une question statique
|
|
367
|
+
insertIndex = i + 1;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Si on n'a pas trouvé de question statique, chercher la dernière question IA injectée
|
|
372
|
+
if (insertIndex === queuedModule.questions.length) {
|
|
373
|
+
for (let i = queuedModule.questions.length - 1; i >= 0; i--) {
|
|
374
|
+
const q = queuedModule.questions[i];
|
|
375
|
+
if (allAIQuestions.find(aq => aq.questionId === q.questionId)) {
|
|
376
|
+
insertIndex = i + 1;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
queuedModule.questions.splice(insertIndex, 0, queuedQuestion);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Parent trouvé dans la queue, insérer après
|
|
386
|
+
queuedModule.questions.splice(queuedParentIndex + 1, 0, queuedQuestion);
|
|
387
|
+
// Modifier le nextInModule de la question parente
|
|
388
|
+
const parentQuestionInModule = module.questions.find(q => q.id === aiQuestion.parentQuestionId);
|
|
389
|
+
if (parentQuestionInModule && parentQuestionInModule.nextInModule !== aiQuestion.questionId) {
|
|
390
|
+
const originalNextInModule = parentQuestionInModule.nextInModule;
|
|
391
|
+
parentQuestionInModule.nextInModule = aiQuestion.questionId;
|
|
392
|
+
moduleQuestion.nextInModule = originalNextInModule;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Si pas de parent, ajouter à la fin du module
|
|
398
|
+
queuedModule.questions.push(queuedQuestion);
|
|
399
|
+
}
|
|
400
|
+
// Ajouter à la liste des questions existantes pour éviter les doublons
|
|
401
|
+
existingQuestionIds.add(aiQuestion.questionId);
|
|
402
|
+
};
|
|
403
|
+
// Traiter toutes les questions IA en respectant les dépendances
|
|
404
|
+
const processedIds = new Set();
|
|
405
|
+
for (const aiQuestion of aiQuestions) {
|
|
406
|
+
injectQuestionRecursive(aiQuestion, processedIds);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
this.notifyListeners();
|
|
410
|
+
}
|
|
411
|
+
shouldHideModule(module) {
|
|
412
|
+
if (!module.triggers || module.triggers.length === 0)
|
|
413
|
+
return false;
|
|
414
|
+
for (const trigger of module.triggers) {
|
|
415
|
+
if (trigger.action.type === "hide_module" && this.evaluateModuleTrigger(trigger)) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
evaluateModuleTrigger(trigger) {
|
|
422
|
+
const { condition } = trigger;
|
|
423
|
+
const value = this.state.formData[condition.questionId];
|
|
424
|
+
switch (condition.type) {
|
|
425
|
+
case "form_data_equals":
|
|
426
|
+
return value === condition.value;
|
|
427
|
+
case "form_data_contains":
|
|
428
|
+
return typeof value === "string" && value.includes(condition.value);
|
|
429
|
+
case "form_data_exists":
|
|
430
|
+
return value !== undefined && value !== null;
|
|
431
|
+
case "form_data_includes_any":
|
|
432
|
+
if (Array.isArray(value) && Array.isArray(condition.value)) {
|
|
433
|
+
return condition.value.some((v) => value.includes(v));
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
default:
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// =====================================================
|
|
441
|
+
// NAVIGATION
|
|
442
|
+
// =====================================================
|
|
443
|
+
activateFirstQuestion() {
|
|
444
|
+
const firstModule = this.state.queue.find((m) => m.status === "pending");
|
|
445
|
+
if (!firstModule) {
|
|
446
|
+
this.state.isComplete = true;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
firstModule.status = "active";
|
|
450
|
+
this.state.currentModuleId = firstModule.moduleId;
|
|
451
|
+
const module = this.modules.get(firstModule.moduleId);
|
|
452
|
+
if (module) {
|
|
453
|
+
this.state.currentQuestionId = module.entryQuestionId;
|
|
454
|
+
const queuedQuestion = firstModule.questions.find((q) => q.questionId === module.entryQuestionId);
|
|
455
|
+
if (queuedQuestion) {
|
|
456
|
+
queuedQuestion.status = "active";
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
getCurrentQuestion() {
|
|
461
|
+
if (!this.state.currentModuleId || !this.state.currentQuestionId) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const module = this.modules.get(this.state.currentModuleId);
|
|
465
|
+
if (!module)
|
|
466
|
+
return null;
|
|
467
|
+
const question = module.questions.find((q) => q.id === this.state.currentQuestionId) || null;
|
|
468
|
+
if (!question)
|
|
469
|
+
return null;
|
|
470
|
+
// Récupérer les IDs système et comportements depuis la config
|
|
471
|
+
const systemIds = getSystemIds(this.config);
|
|
472
|
+
const behaviors = getSpecialBehaviors(this.config);
|
|
473
|
+
// Pour la question budget_priority, filtrer les options en fonction des prestataires déjà réservés
|
|
474
|
+
if (question.id === systemIds.budgetPriorityQuestionId && behaviors.budgetPriority.enabled) {
|
|
475
|
+
// Les options du module sont maintenant des clés i18n (ex: "intro_options.budget_priority_venue")
|
|
476
|
+
const loadedOptions = question.options && question.options.length > 0
|
|
477
|
+
? question.options
|
|
478
|
+
: this.i18n.getQuestionOptions(question.id, 'fr');
|
|
479
|
+
// Traduire les options pour le filtrage
|
|
480
|
+
const translatedOptions = loadedOptions.map(opt => {
|
|
481
|
+
if (opt.includes('.') && !opt.includes(' ')) {
|
|
482
|
+
return this.i18n.t(opt, undefined, 'fr');
|
|
483
|
+
}
|
|
484
|
+
return opt;
|
|
485
|
+
});
|
|
486
|
+
const bookedProviders = this.state.formData.booked_providers;
|
|
487
|
+
if (bookedProviders && bookedProviders.length > 0 && translatedOptions.length > 0 && behaviors.budgetPriority.filterOptionsByBookedProviders) {
|
|
488
|
+
// Récupérer le mapping priorité -> prestataires depuis la config
|
|
489
|
+
const priorityToBookedProviders = getPriorityToBookedProviders(this.config);
|
|
490
|
+
// Filtrer les options traduites
|
|
491
|
+
const filteredTranslatedOptions = translatedOptions.filter((translatedOpt, index) => {
|
|
492
|
+
// Récupérer les prestataires correspondants à cette priorité (utilise les valeurs traduites)
|
|
493
|
+
const relatedProviders = priorityToBookedProviders[translatedOpt] || [];
|
|
494
|
+
// Vérifier si AU MOINS UN prestataire de cette catégorie est déjà réservé
|
|
495
|
+
if (relatedProviders.length === 0) {
|
|
496
|
+
// Pas de prestataire associé (ex: "Les tenues"), on garde l'option
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
// Si au moins un prestataire de cette catégorie est réservé, on exclut l'option
|
|
500
|
+
const hasBookedProvider = relatedProviders.some(provider => bookedProviders.includes(provider));
|
|
501
|
+
return !hasBookedProvider;
|
|
502
|
+
});
|
|
503
|
+
// Retourner les clés i18n correspondantes aux options traduites filtrées
|
|
504
|
+
const filteredOptions = filteredTranslatedOptions.map(translatedOpt => {
|
|
505
|
+
// Trouver la clé i18n correspondante dans loadedOptions
|
|
506
|
+
const originalKeyIndex = translatedOptions.findIndex(opt => opt === translatedOpt);
|
|
507
|
+
return originalKeyIndex >= 0 ? loadedOptions[originalKeyIndex] : translatedOpt;
|
|
508
|
+
});
|
|
509
|
+
// Retourner les options filtrées (clés i18n) - elles seront traduites dans enrichQuestionWithTranslations
|
|
510
|
+
return Object.assign(Object.assign({}, question), { options: filteredOptions });
|
|
511
|
+
}
|
|
512
|
+
else if (loadedOptions.length > 0) {
|
|
513
|
+
// Si pas de prestataires réservés, retourner avec les options chargées (clés i18n)
|
|
514
|
+
return Object.assign(Object.assign({}, question), { options: loadedOptions });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Pour la question contact_correction, déterminer dynamiquement le type (email ou phone)
|
|
518
|
+
if (question.id === systemIds.contactCorrectionQuestionId && behaviors.contactCorrection.enabled) {
|
|
519
|
+
const contactIntro = this.state.formData.contact_intro;
|
|
520
|
+
const contactEmail = this.state.formData.contact_email;
|
|
521
|
+
const detectEmailFrom = behaviors.contactCorrection.detectEmailFrom;
|
|
522
|
+
const isEmail = detectEmailFrom.some(key => contactIntro === key) || contactEmail !== undefined;
|
|
523
|
+
// Modifier le type de question selon le mode de contact
|
|
524
|
+
return Object.assign(Object.assign({}, question), { type: isEmail ? "email" : "phone" });
|
|
525
|
+
}
|
|
526
|
+
return question;
|
|
527
|
+
}
|
|
528
|
+
getCurrentModule() {
|
|
529
|
+
if (!this.state.currentModuleId)
|
|
530
|
+
return null;
|
|
531
|
+
return this.modules.get(this.state.currentModuleId) || null;
|
|
532
|
+
}
|
|
533
|
+
getModuleById(moduleId) {
|
|
534
|
+
return this.modules.get(moduleId) || null;
|
|
535
|
+
}
|
|
536
|
+
// Vérifie si une question has_* doit être sautée (on a déjà l'info depuis l'intro)
|
|
537
|
+
// Retourne: { shouldSkip: boolean, isBooked: boolean }
|
|
538
|
+
checkHasQuestionSkip(questionId, moduleId) {
|
|
539
|
+
// Vérifier si on a posé la question booked_providers
|
|
540
|
+
const bookedProviders = this.state.formData.booked_providers;
|
|
541
|
+
// Si booked_providers n'a pas encore été répondu, ne pas sauter
|
|
542
|
+
if (bookedProviders === undefined) {
|
|
543
|
+
return { shouldSkip: false, isBooked: false };
|
|
544
|
+
}
|
|
545
|
+
// Chercher si ce module/question correspond à un prestataire dans le mapping
|
|
546
|
+
const bookedProviderMappings = getBookedProviderMappings(this.config);
|
|
547
|
+
for (const [providerName, mapping] of Object.entries(bookedProviderMappings)) {
|
|
548
|
+
if (mapping.moduleId === moduleId && mapping.skipQuestion === questionId) {
|
|
549
|
+
// On a trouvé le mapping, donc on doit sauter cette question
|
|
550
|
+
// isBooked = true si le prestataire est dans la liste des réservés
|
|
551
|
+
// (et n'est pas "Aucun pour l'instant")
|
|
552
|
+
const isBooked = bookedProviders.includes(providerName) && !bookedProviders.includes("Aucun pour l'instant");
|
|
553
|
+
return { shouldSkip: true, isBooked };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return { shouldSkip: false, isBooked: false };
|
|
557
|
+
}
|
|
558
|
+
// Récupère l'ID de la question suivante selon la branche choisie
|
|
559
|
+
getSkipDestination(question, branch) {
|
|
560
|
+
// Si la question a une navigation conditionnelle, aller vers la branche appropriée
|
|
561
|
+
if (question.conditionalNextInModule && question.conditionalNextInModule[branch]) {
|
|
562
|
+
return question.conditionalNextInModule[branch];
|
|
563
|
+
}
|
|
564
|
+
return question.nextInModule || null;
|
|
565
|
+
}
|
|
566
|
+
// =====================================================
|
|
567
|
+
// SOUMISSION DE RÉPONSE
|
|
568
|
+
// =====================================================
|
|
569
|
+
async submitAnswer(answer) {
|
|
570
|
+
const currentQuestion = this.getCurrentQuestion();
|
|
571
|
+
const currentModule = this.getCurrentModule();
|
|
572
|
+
if (!currentQuestion || !currentModule) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
// Reset le flag de double avancement
|
|
576
|
+
this.advancedByTrigger = false;
|
|
577
|
+
// Enregistrer la réponse
|
|
578
|
+
this.state.formData[currentQuestion.id] = answer;
|
|
579
|
+
// Récupérer les IDs système et comportements depuis la config
|
|
580
|
+
const systemIds = getSystemIds(this.config);
|
|
581
|
+
const behaviors = getSpecialBehaviors(this.config);
|
|
582
|
+
// Si c'est la question contact_correction, mettre à jour contact_email ou contact_phone selon le mode
|
|
583
|
+
if (currentQuestion.id === systemIds.contactCorrectionQuestionId && behaviors.contactCorrection.enabled) {
|
|
584
|
+
const contactIntro = this.state.formData.contact_intro;
|
|
585
|
+
const contactEmail = this.state.formData.contact_email;
|
|
586
|
+
const detectEmailFrom = behaviors.contactCorrection.detectEmailFrom;
|
|
587
|
+
const isEmail = detectEmailFrom.some(key => contactIntro === key) || contactEmail !== undefined;
|
|
588
|
+
if (isEmail) {
|
|
589
|
+
// Mettre à jour l'email
|
|
590
|
+
this.state.formData.contact_email = answer;
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// Mettre à jour le téléphone
|
|
594
|
+
this.state.formData.contact_phone = answer;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Après avoir enregistré la réponse, injecter les questions IA depuis le cache
|
|
598
|
+
// qui pourraient maintenant correspondre aux nouvelles conditions
|
|
599
|
+
this.injectAIQuestionsFromCache();
|
|
600
|
+
// Marquer la question comme complétée dans la queue
|
|
601
|
+
const queuedModule = this.state.queue.find((m) => m.moduleId === currentModule.id);
|
|
602
|
+
if (queuedModule) {
|
|
603
|
+
const queuedQuestion = queuedModule.questions.find((q) => q.questionId === currentQuestion.id);
|
|
604
|
+
if (queuedQuestion) {
|
|
605
|
+
queuedQuestion.status = "completed";
|
|
606
|
+
queuedQuestion.answer = answer;
|
|
607
|
+
queuedQuestion.answeredAt = new Date();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Ajouter aux questions répondues
|
|
611
|
+
this.state.answeredQuestions.push({
|
|
612
|
+
questionId: currentQuestion.id,
|
|
613
|
+
moduleId: currentModule.id,
|
|
614
|
+
moduleName: currentModule.name,
|
|
615
|
+
text: currentQuestion.text,
|
|
616
|
+
answer,
|
|
617
|
+
answeredAt: new Date(),
|
|
618
|
+
});
|
|
619
|
+
// Exécuter les triggers de la question
|
|
620
|
+
if (currentQuestion.onAnswer) {
|
|
621
|
+
this.executeQuestionTriggers(currentQuestion.onAnswer, answer);
|
|
622
|
+
}
|
|
623
|
+
// Si la question a des sous-questions à générer (multiselect avec prestataires multiples)
|
|
624
|
+
let firstSubQuestionId = null;
|
|
625
|
+
if (currentQuestion.generateSubQuestionsFor && Array.isArray(answer) && answer.length > 0) {
|
|
626
|
+
firstSubQuestionId = this.generateDynamicSubQuestions(currentModule, currentQuestion, answer, queuedModule);
|
|
627
|
+
}
|
|
628
|
+
// Si la question est de type "info" avec generateSubQuestionsFromFormData,
|
|
629
|
+
// générer les sous-questions à partir des données du formulaire
|
|
630
|
+
if (currentQuestion.type === "info" &&
|
|
631
|
+
currentQuestion.generateSubQuestionsFromFormData &&
|
|
632
|
+
currentQuestion.subQuestionTemplates) {
|
|
633
|
+
const sourceData = this.state.formData[currentQuestion.generateSubQuestionsFromFormData];
|
|
634
|
+
if (Array.isArray(sourceData) && sourceData.length > 0) {
|
|
635
|
+
// Filtrer "Aucun pour l'instant" si présent
|
|
636
|
+
const filteredItems = sourceData.filter((item) => item !== "Aucun pour l'instant");
|
|
637
|
+
if (filteredItems.length > 0) {
|
|
638
|
+
firstSubQuestionId = this.generateSubQuestionsFromFormData(currentModule, currentQuestion, filteredItems, queuedModule);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Si des sous-questions ont été générées, naviguer vers la première
|
|
643
|
+
if (firstSubQuestionId) {
|
|
644
|
+
this.state.currentQuestionId = firstSubQuestionId;
|
|
645
|
+
const nextQueuedQuestion = queuedModule === null || queuedModule === void 0 ? void 0 : queuedModule.questions.find((q) => q.questionId === firstSubQuestionId);
|
|
646
|
+
if (nextQueuedQuestion) {
|
|
647
|
+
nextQueuedQuestion.status = "active";
|
|
648
|
+
}
|
|
649
|
+
this.notifyListeners();
|
|
650
|
+
return this.getCurrentQuestion();
|
|
651
|
+
}
|
|
652
|
+
// Sinon, déterminer la prochaine question normalement
|
|
653
|
+
const nextQuestionId = this.getNextQuestionId(currentQuestion, answer);
|
|
654
|
+
if (nextQuestionId === null) {
|
|
655
|
+
// Fin du module - Si c'est la question de priorité, appliquer la réorganisation
|
|
656
|
+
if (currentQuestion.id === systemIds.budgetPriorityQuestionId &&
|
|
657
|
+
Array.isArray(answer) &&
|
|
658
|
+
behaviors.budgetPriority.enabled) {
|
|
659
|
+
this.applyPriorityOrder(answer);
|
|
660
|
+
// Marquer le module intro comme complété
|
|
661
|
+
if (queuedModule) {
|
|
662
|
+
queuedModule.status = "completed";
|
|
663
|
+
}
|
|
664
|
+
// Notifier pour mettre à jour la visualisation
|
|
665
|
+
this.notifyListeners();
|
|
666
|
+
}
|
|
667
|
+
// Si c'est la question continue_other_sectors_question et que la réponse est "non"
|
|
668
|
+
// Sauter directement au module final
|
|
669
|
+
if (currentQuestion.id === systemIds.continueOtherSectorsQuestionId &&
|
|
670
|
+
behaviors.continueOtherSectors.enabled &&
|
|
671
|
+
behaviors.continueOtherSectors.skipToFinalOnNo) {
|
|
672
|
+
// Détecter la réponse "non" : boolean false, string "non" ou "false"
|
|
673
|
+
const isNo = answer === false ||
|
|
674
|
+
(typeof answer === "string" && (answer.toLowerCase() === "non" || answer.toLowerCase() === "false"));
|
|
675
|
+
if (isNo) {
|
|
676
|
+
// Trouver le module final
|
|
677
|
+
const finalModule = this.state.queue.find((m) => m.moduleId === systemIds.finalModuleId);
|
|
678
|
+
if (finalModule) {
|
|
679
|
+
// Marquer continue_other_sectors comme complété
|
|
680
|
+
if (queuedModule) {
|
|
681
|
+
queuedModule.status = "completed";
|
|
682
|
+
this.state.completedModules.push(queuedModule.moduleId);
|
|
683
|
+
}
|
|
684
|
+
// Marquer tous les modules non prioritaires comme skippés
|
|
685
|
+
const skipPriority = behaviors.continueOtherSectors.skipModulesWithPriority;
|
|
686
|
+
this.state.queue.forEach((m) => {
|
|
687
|
+
if (m.effectivePriority >= skipPriority &&
|
|
688
|
+
m.moduleId !== systemIds.finalModuleId &&
|
|
689
|
+
m.moduleId !== systemIds.continueOtherSectorsModuleId) {
|
|
690
|
+
m.status = "skipped";
|
|
691
|
+
this.state.skippedModules.push(m.moduleId);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
// Activer le module final
|
|
695
|
+
finalModule.status = "active";
|
|
696
|
+
this.state.currentModuleId = finalModule.moduleId;
|
|
697
|
+
const finalModuleDef = this.modules.get(systemIds.finalModuleId);
|
|
698
|
+
if (finalModuleDef) {
|
|
699
|
+
this.state.currentQuestionId = finalModuleDef.entryQuestionId;
|
|
700
|
+
const queuedQuestion = finalModule.questions.find((q) => q.questionId === finalModuleDef.entryQuestionId);
|
|
701
|
+
if (queuedQuestion) {
|
|
702
|
+
queuedQuestion.status = "active";
|
|
703
|
+
}
|
|
704
|
+
// S'assurer que le module est bien dans la queue et que la question existe
|
|
705
|
+
this.notifyListeners();
|
|
706
|
+
const currentQuestion = this.getCurrentQuestion();
|
|
707
|
+
if (currentQuestion) {
|
|
708
|
+
return currentQuestion;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Si on arrive ici, il y a un problème - essayer d'avancer normalement
|
|
712
|
+
return this.advanceToNextModule();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Passer au suivant (qui sera maintenant le module priorisé)
|
|
717
|
+
// Mais seulement si un trigger n'a pas déjà fait avancer
|
|
718
|
+
if (this.advancedByTrigger) {
|
|
719
|
+
this.notifyListeners();
|
|
720
|
+
return this.getCurrentQuestion();
|
|
721
|
+
}
|
|
722
|
+
return this.advanceToNextModule();
|
|
723
|
+
}
|
|
724
|
+
// Naviguer vers la prochaine question dans le module
|
|
725
|
+
this.state.currentQuestionId = nextQuestionId;
|
|
726
|
+
const nextQueuedQuestion = queuedModule === null || queuedModule === void 0 ? void 0 : queuedModule.questions.find((q) => q.questionId === nextQuestionId);
|
|
727
|
+
if (nextQueuedQuestion) {
|
|
728
|
+
nextQueuedQuestion.status = "active";
|
|
729
|
+
}
|
|
730
|
+
this.notifyListeners();
|
|
731
|
+
return this.getCurrentQuestion();
|
|
732
|
+
}
|
|
733
|
+
generateDynamicSubQuestions(module, parentQuestion, selectedItems, queuedModule) {
|
|
734
|
+
if (!parentQuestion.generateSubQuestionsFor || !queuedModule)
|
|
735
|
+
return null;
|
|
736
|
+
// Trouver l'index de la question parente dans la queue
|
|
737
|
+
const parentIndex = queuedModule.questions.findIndex((q) => q.questionId === parentQuestion.id);
|
|
738
|
+
if (parentIndex === -1)
|
|
739
|
+
return null;
|
|
740
|
+
const subQuestions = [];
|
|
741
|
+
const dynamicModuleQuestions = [];
|
|
742
|
+
// Vérifier si l'utilisateur veut la coordination
|
|
743
|
+
const wantsCoordination = this.state.formData.aimi_coordination === "oui";
|
|
744
|
+
const hasExistingProviders = this.state.formData.has_existing_providers === "oui";
|
|
745
|
+
const shouldAskContact = wantsCoordination && hasExistingProviders;
|
|
746
|
+
// Filtrer les templates : exclure provider_contact si pas de coordination
|
|
747
|
+
const filteredTemplates = parentQuestion.generateSubQuestionsFor.filter(template => {
|
|
748
|
+
if (template.id === "provider_contact" && !shouldAskContact) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
return true;
|
|
752
|
+
});
|
|
753
|
+
// Pour chaque élément sélectionné, générer les sous-questions
|
|
754
|
+
selectedItems.forEach((item, itemIndex) => {
|
|
755
|
+
filteredTemplates.forEach((template, templateIndex) => {
|
|
756
|
+
// ID unique pour la sous-question générée
|
|
757
|
+
const subQuestionId = `${parentQuestion.id}_${template.id}_${itemIndex}`;
|
|
758
|
+
// Traduire le template puis remplacer {item}
|
|
759
|
+
// Le template peut être une clé i18n (ex: "intro_templates.provider_name")
|
|
760
|
+
let translatedTemplate = template.textTemplate;
|
|
761
|
+
if (template.textTemplate.includes('.') && !template.textTemplate.includes(' ')) {
|
|
762
|
+
// C'est une clé i18n, on la traduit d'abord
|
|
763
|
+
translatedTemplate = this.i18n.t(template.textTemplate, undefined, 'fr');
|
|
764
|
+
}
|
|
765
|
+
// Remplacer {item} dans le template traduit
|
|
766
|
+
const text = translatedTemplate.replace("{item}", item);
|
|
767
|
+
// Traduire le placeholder si c'est une clé i18n
|
|
768
|
+
let translatedPlaceholder = template.placeholder;
|
|
769
|
+
if (template.placeholder && template.placeholder.includes('.') && !template.placeholder.includes(' ')) {
|
|
770
|
+
translatedPlaceholder = this.i18n.t(template.placeholder, undefined, 'fr');
|
|
771
|
+
}
|
|
772
|
+
// Déterminer la prochaine question
|
|
773
|
+
let nextInModule = null;
|
|
774
|
+
const nextItemIndex = itemIndex + 1;
|
|
775
|
+
const hasNextItem = nextItemIndex < selectedItems.length;
|
|
776
|
+
// Récupérer la destination après les sous-questions (depuis la question parente)
|
|
777
|
+
const finalDestination = parentQuestion.nextInModule || null;
|
|
778
|
+
if (template.id === "provider_name") {
|
|
779
|
+
// Après le nom, aller vers contact (si coordination) ou prochain prestataire
|
|
780
|
+
if (shouldAskContact) {
|
|
781
|
+
nextInModule = `${parentQuestion.id}_provider_contact_${itemIndex}`;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
nextInModule = hasNextItem
|
|
785
|
+
? `${parentQuestion.id}_provider_name_${nextItemIndex}`
|
|
786
|
+
: finalDestination;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else if (template.id === "provider_contact") {
|
|
790
|
+
// Après le contact, passer au prochain prestataire ou destination finale
|
|
791
|
+
nextInModule = hasNextItem
|
|
792
|
+
? `${parentQuestion.id}_provider_name_${nextItemIndex}`
|
|
793
|
+
: finalDestination;
|
|
794
|
+
}
|
|
795
|
+
// Créer la question dynamique
|
|
796
|
+
const dynamicQuestion = {
|
|
797
|
+
id: subQuestionId,
|
|
798
|
+
text,
|
|
799
|
+
type: template.type,
|
|
800
|
+
placeholder: translatedPlaceholder,
|
|
801
|
+
isRequired: template.isRequired,
|
|
802
|
+
nextInModule,
|
|
803
|
+
};
|
|
804
|
+
// Ajouter à la liste des questions du module
|
|
805
|
+
dynamicModuleQuestions.push(dynamicQuestion);
|
|
806
|
+
// Ajouter à la queue
|
|
807
|
+
subQuestions.push({
|
|
808
|
+
questionId: subQuestionId,
|
|
809
|
+
moduleId: module.id,
|
|
810
|
+
text,
|
|
811
|
+
type: template.type,
|
|
812
|
+
status: "pending",
|
|
813
|
+
isDynamic: true,
|
|
814
|
+
parentQuestionId: parentQuestion.id,
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
// Ajouter les questions dynamiques au module
|
|
819
|
+
module.questions.push(...dynamicModuleQuestions);
|
|
820
|
+
// Insérer les sous-questions après la question parente
|
|
821
|
+
queuedModule.questions.splice(parentIndex + 1, 0, ...subQuestions);
|
|
822
|
+
// Retourner l'ID de la première sous-question générée
|
|
823
|
+
return subQuestions.length > 0 ? subQuestions[0].questionId : null;
|
|
824
|
+
}
|
|
825
|
+
// Génère des sous-questions à partir des données du formulaire (pour les questions "info")
|
|
826
|
+
generateSubQuestionsFromFormData(module, parentQuestion, selectedItems, queuedModule) {
|
|
827
|
+
if (!parentQuestion.subQuestionTemplates || !queuedModule)
|
|
828
|
+
return null;
|
|
829
|
+
// Trouver l'index de la question parente dans la queue
|
|
830
|
+
const parentIndex = queuedModule.questions.findIndex((q) => q.questionId === parentQuestion.id);
|
|
831
|
+
if (parentIndex === -1)
|
|
832
|
+
return null;
|
|
833
|
+
const subQuestions = [];
|
|
834
|
+
const dynamicModuleQuestions = [];
|
|
835
|
+
// Récupérer la destination après les sous-questions
|
|
836
|
+
const finalDestination = parentQuestion.nextInModule || null;
|
|
837
|
+
// Pour chaque élément sélectionné, générer les sous-questions
|
|
838
|
+
selectedItems.forEach((item, itemIndex) => {
|
|
839
|
+
parentQuestion.subQuestionTemplates.forEach((template, templateIndex) => {
|
|
840
|
+
// ID unique pour la sous-question générée
|
|
841
|
+
const subQuestionId = `${parentQuestion.id}_${template.id}_${itemIndex}`;
|
|
842
|
+
// Traduire le template puis remplacer {item}
|
|
843
|
+
// Le template peut être une clé i18n (ex: "intro_templates.provider_name")
|
|
844
|
+
let translatedTemplate = template.textTemplate;
|
|
845
|
+
if (template.textTemplate.includes('.') && !template.textTemplate.includes(' ')) {
|
|
846
|
+
// C'est une clé i18n, on la traduit d'abord
|
|
847
|
+
translatedTemplate = this.i18n.t(template.textTemplate, undefined, 'fr');
|
|
848
|
+
}
|
|
849
|
+
// Remplacer {item} dans le template traduit (item est déjà traduit depuis booked_providers)
|
|
850
|
+
const text = translatedTemplate.replace("{item}", item.toLowerCase());
|
|
851
|
+
// Traduire le placeholder si c'est une clé i18n
|
|
852
|
+
let translatedPlaceholder = template.placeholder;
|
|
853
|
+
if (template.placeholder && template.placeholder.includes('.') && !template.placeholder.includes(' ')) {
|
|
854
|
+
translatedPlaceholder = this.i18n.t(template.placeholder, undefined, 'fr');
|
|
855
|
+
}
|
|
856
|
+
// Déterminer la prochaine question
|
|
857
|
+
let nextInModule = null;
|
|
858
|
+
const nextItemIndex = itemIndex + 1;
|
|
859
|
+
const hasNextItem = nextItemIndex < selectedItems.length;
|
|
860
|
+
const nextTemplateIndex = templateIndex + 1;
|
|
861
|
+
const hasNextTemplate = nextTemplateIndex < parentQuestion.subQuestionTemplates.length;
|
|
862
|
+
if (hasNextTemplate) {
|
|
863
|
+
// Aller vers le prochain template pour le même item
|
|
864
|
+
nextInModule = `${parentQuestion.id}_${parentQuestion.subQuestionTemplates[nextTemplateIndex].id}_${itemIndex}`;
|
|
865
|
+
}
|
|
866
|
+
else if (hasNextItem) {
|
|
867
|
+
// Aller vers le premier template du prochain item
|
|
868
|
+
nextInModule = `${parentQuestion.id}_${parentQuestion.subQuestionTemplates[0].id}_${nextItemIndex}`;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
// Fin des sous-questions, aller vers la destination finale
|
|
872
|
+
nextInModule = finalDestination;
|
|
873
|
+
}
|
|
874
|
+
// Créer la question dynamique
|
|
875
|
+
const dynamicQuestion = {
|
|
876
|
+
id: subQuestionId,
|
|
877
|
+
text,
|
|
878
|
+
type: template.type,
|
|
879
|
+
placeholder: translatedPlaceholder,
|
|
880
|
+
isRequired: template.isRequired,
|
|
881
|
+
nextInModule,
|
|
882
|
+
};
|
|
883
|
+
// Ajouter à la liste des questions du module
|
|
884
|
+
dynamicModuleQuestions.push(dynamicQuestion);
|
|
885
|
+
// Ajouter à la queue
|
|
886
|
+
subQuestions.push({
|
|
887
|
+
questionId: subQuestionId,
|
|
888
|
+
moduleId: module.id,
|
|
889
|
+
text,
|
|
890
|
+
type: template.type,
|
|
891
|
+
status: "pending",
|
|
892
|
+
isDynamic: true,
|
|
893
|
+
parentQuestionId: parentQuestion.id,
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
// Ajouter les questions dynamiques au module
|
|
898
|
+
module.questions.push(...dynamicModuleQuestions);
|
|
899
|
+
// Insérer les sous-questions après la question parente
|
|
900
|
+
queuedModule.questions.splice(parentIndex + 1, 0, ...subQuestions);
|
|
901
|
+
// Retourner l'ID de la première sous-question générée
|
|
902
|
+
return subQuestions.length > 0 ? subQuestions[0].questionId : null;
|
|
903
|
+
}
|
|
904
|
+
getNextQuestionId(question, answer) {
|
|
905
|
+
// Vérifier la navigation conditionnelle
|
|
906
|
+
if (question.conditionalNextInModule) {
|
|
907
|
+
const answerKey = typeof answer === "boolean"
|
|
908
|
+
? (answer ? "oui" : "non")
|
|
909
|
+
: String(answer).toLowerCase();
|
|
910
|
+
if (answerKey in question.conditionalNextInModule) {
|
|
911
|
+
return question.conditionalNextInModule[answerKey];
|
|
912
|
+
}
|
|
913
|
+
// Essayer avec la valeur exacte
|
|
914
|
+
if (String(answer) in question.conditionalNextInModule) {
|
|
915
|
+
return question.conditionalNextInModule[String(answer)];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Navigation par défaut
|
|
919
|
+
let nextId = question.nextInModule !== undefined ? question.nextInModule : null;
|
|
920
|
+
// Si la prochaine question est une question de contact (*_contact),
|
|
921
|
+
// vérifier si l'utilisateur a accepté la coordination
|
|
922
|
+
if (nextId && nextId.endsWith("_contact")) {
|
|
923
|
+
const wantsCoordination = this.state.formData.aimi_coordination === "oui";
|
|
924
|
+
const hasExistingProviders = this.state.formData.has_existing_providers === "oui";
|
|
925
|
+
// Si l'utilisateur n'a pas accepté la coordination ou n'a pas de prestataires existants,
|
|
926
|
+
// on saute la question de contact (fin du module ou branche)
|
|
927
|
+
if (!wantsCoordination || !hasExistingProviders) {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return nextId;
|
|
932
|
+
}
|
|
933
|
+
advanceToNextModule() {
|
|
934
|
+
var _a;
|
|
935
|
+
// Récupérer le module actuel avant de le marquer comme complété
|
|
936
|
+
const currentQueuedModule = this.state.queue.find((m) => m.moduleId === this.state.currentModuleId);
|
|
937
|
+
// Stocker l'ID du module qui se termine pour les messages de transition
|
|
938
|
+
const completedModuleId = (currentQueuedModule === null || currentQueuedModule === void 0 ? void 0 : currentQueuedModule.moduleId) || null;
|
|
939
|
+
// Marquer le module actuel comme complété
|
|
940
|
+
if (currentQueuedModule) {
|
|
941
|
+
currentQueuedModule.status = "completed";
|
|
942
|
+
this.state.completedModules.push(currentQueuedModule.moduleId);
|
|
943
|
+
}
|
|
944
|
+
// Recalculer la queue (pour appliquer les changements de priorité)
|
|
945
|
+
this.recalculateQueue();
|
|
946
|
+
// Détecter si on passe des modules prioritaires aux modules non prioritaires
|
|
947
|
+
// Les modules prioritaires ont une priorité négative (< 0), les autres ont une priorité positive (>= 0)
|
|
948
|
+
const currentModule = currentQueuedModule ? this.modules.get(currentQueuedModule.moduleId) : null;
|
|
949
|
+
const currentPriority = (_a = currentQueuedModule === null || currentQueuedModule === void 0 ? void 0 : currentQueuedModule.effectivePriority) !== null && _a !== void 0 ? _a : 0;
|
|
950
|
+
// Récupérer les IDs système et comportements depuis la config
|
|
951
|
+
const systemIds = getSystemIds(this.config);
|
|
952
|
+
const behaviors = getSpecialBehaviors(this.config);
|
|
953
|
+
// Trouver le prochain module en attente
|
|
954
|
+
const nextModule = this.state.queue.find((m) => m.status === "pending" && !this.state.hiddenModules.includes(m.moduleId));
|
|
955
|
+
// Si on passe d'un module prioritaire (priorité < 0) à un module non prioritaire (priorité >= 0)
|
|
956
|
+
// ET que le module continue_other_sectors n'a pas encore été complété
|
|
957
|
+
// Alors insérer le module continue_other_sectors
|
|
958
|
+
if (nextModule &&
|
|
959
|
+
currentPriority < 0 &&
|
|
960
|
+
nextModule.effectivePriority >= 0 &&
|
|
961
|
+
behaviors.continueOtherSectors.enabled &&
|
|
962
|
+
behaviors.continueOtherSectors.insertBetweenPriorityAndNonPriority) {
|
|
963
|
+
const continueModule = this.state.queue.find((m) => m.moduleId === systemIds.continueOtherSectorsModuleId &&
|
|
964
|
+
!this.state.completedModules.includes(systemIds.continueOtherSectorsModuleId));
|
|
965
|
+
if (continueModule && continueModule.status === "pending") {
|
|
966
|
+
// Insérer le module continue_other_sectors avant les modules non prioritaires
|
|
967
|
+
nextModule.status = "pending"; // Remettre en pending
|
|
968
|
+
continueModule.status = "active";
|
|
969
|
+
this.state.currentModuleId = continueModule.moduleId;
|
|
970
|
+
const continueModuleDef = this.modules.get(continueModule.moduleId);
|
|
971
|
+
if (continueModuleDef) {
|
|
972
|
+
this.state.currentQuestionId = continueModuleDef.entryQuestionId;
|
|
973
|
+
const queuedQuestion = continueModule.questions.find((q) => q.questionId === continueModuleDef.entryQuestionId);
|
|
974
|
+
if (queuedQuestion) {
|
|
975
|
+
queuedQuestion.status = "active";
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
this.notifyListeners();
|
|
979
|
+
return this.getCurrentQuestion();
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!nextModule) {
|
|
983
|
+
this.state.isComplete = true;
|
|
984
|
+
this.state.currentModuleId = null;
|
|
985
|
+
this.state.currentQuestionId = null;
|
|
986
|
+
this.notifyListeners();
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
// Activer le prochain module
|
|
990
|
+
nextModule.status = "active";
|
|
991
|
+
this.state.currentModuleId = nextModule.moduleId;
|
|
992
|
+
const module = this.modules.get(nextModule.moduleId);
|
|
993
|
+
if (module) {
|
|
994
|
+
let entryQuestionId = module.entryQuestionId;
|
|
995
|
+
const entryQuestion = module.questions.find(q => q.id === entryQuestionId);
|
|
996
|
+
// Vérifier si le module doit être skippé (prestataire déjà réservé)
|
|
997
|
+
const { isBooked } = this.checkHasQuestionSkip(entryQuestionId, module.id);
|
|
998
|
+
// Si le prestataire est déjà réservé, on skip TOUT le module
|
|
999
|
+
// car les coordonnées ont déjà été collectées dans l'intro (booked_providers_contact_intro)
|
|
1000
|
+
if (isBooked) {
|
|
1001
|
+
// Marquer le module comme skippé
|
|
1002
|
+
nextModule.status = "skipped";
|
|
1003
|
+
this.state.skippedModules.push(nextModule.moduleId);
|
|
1004
|
+
// Marquer toutes les questions comme skippées
|
|
1005
|
+
nextModule.questions.forEach(q => {
|
|
1006
|
+
q.status = "skipped";
|
|
1007
|
+
});
|
|
1008
|
+
// Passer au module suivant
|
|
1009
|
+
return this.advanceToNextModule();
|
|
1010
|
+
}
|
|
1011
|
+
this.state.currentQuestionId = entryQuestionId;
|
|
1012
|
+
const queuedQuestion = nextModule.questions.find((q) => q.questionId === entryQuestionId);
|
|
1013
|
+
if (queuedQuestion) {
|
|
1014
|
+
queuedQuestion.status = "active";
|
|
1015
|
+
}
|
|
1016
|
+
// Stocker les informations de transition pour les messages
|
|
1017
|
+
// Ces informations seront utilisées dans le hook pour afficher les messages de transition
|
|
1018
|
+
// Ne pas afficher de transition pour les modules exclus
|
|
1019
|
+
// IMPORTANT: transitionInfo est réinitialisé à null après utilisation dans le hook
|
|
1020
|
+
const excludeModules = behaviors.transitions.excludeModules;
|
|
1021
|
+
if (completedModuleId &&
|
|
1022
|
+
!excludeModules.includes(completedModuleId) &&
|
|
1023
|
+
!excludeModules.includes(nextModule.moduleId)) {
|
|
1024
|
+
// Ne créer transitionInfo que s'il n'existe pas déjà (éviter les duplications)
|
|
1025
|
+
if (!this.state.transitionInfo) {
|
|
1026
|
+
this.state.transitionInfo = {
|
|
1027
|
+
completedModuleId,
|
|
1028
|
+
startingModuleId: nextModule.moduleId
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
this.state.transitionInfo = null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
this.notifyListeners();
|
|
1037
|
+
return this.getCurrentQuestion();
|
|
1038
|
+
}
|
|
1039
|
+
// =====================================================
|
|
1040
|
+
// TRIGGERS
|
|
1041
|
+
// =====================================================
|
|
1042
|
+
executeQuestionTriggers(triggers, answer) {
|
|
1043
|
+
for (const trigger of triggers) {
|
|
1044
|
+
if (this.evaluateQuestionTrigger(trigger, answer)) {
|
|
1045
|
+
this.executeAction(trigger.action);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
evaluateQuestionTrigger(trigger, answer) {
|
|
1050
|
+
const { condition } = trigger;
|
|
1051
|
+
switch (condition.type) {
|
|
1052
|
+
case "answer_equals":
|
|
1053
|
+
return answer === condition.value ||
|
|
1054
|
+
(typeof answer === "boolean" &&
|
|
1055
|
+
((answer && condition.value === "oui") ||
|
|
1056
|
+
(!answer && condition.value === "non")));
|
|
1057
|
+
case "answer_not_equals":
|
|
1058
|
+
return answer !== condition.value;
|
|
1059
|
+
case "answer_contains":
|
|
1060
|
+
return typeof answer === "string" &&
|
|
1061
|
+
answer.toLowerCase().includes(condition.value.toLowerCase());
|
|
1062
|
+
case "answer_exists":
|
|
1063
|
+
return answer !== undefined && answer !== null && answer !== "";
|
|
1064
|
+
case "answer_includes_any":
|
|
1065
|
+
if (Array.isArray(answer) && Array.isArray(condition.value)) {
|
|
1066
|
+
return condition.value.some((v) => answer.map(a => a.toLowerCase()).includes(v.toLowerCase()));
|
|
1067
|
+
}
|
|
1068
|
+
return false;
|
|
1069
|
+
default:
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
executeAction(action) {
|
|
1074
|
+
switch (action.type) {
|
|
1075
|
+
case "hide_module":
|
|
1076
|
+
if (action.targetModuleId && !this.state.hiddenModules.includes(action.targetModuleId)) {
|
|
1077
|
+
this.state.hiddenModules.push(action.targetModuleId);
|
|
1078
|
+
// Mettre à jour le statut dans la queue
|
|
1079
|
+
const queuedModule = this.state.queue.find(m => m.moduleId === action.targetModuleId);
|
|
1080
|
+
if (queuedModule && queuedModule.status === "pending") {
|
|
1081
|
+
queuedModule.status = "hidden";
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
break;
|
|
1085
|
+
case "show_module":
|
|
1086
|
+
if (action.targetModuleId) {
|
|
1087
|
+
this.state.hiddenModules = this.state.hiddenModules.filter((id) => id !== action.targetModuleId);
|
|
1088
|
+
const queuedModule = this.state.queue.find(m => m.moduleId === action.targetModuleId);
|
|
1089
|
+
if (queuedModule && queuedModule.status === "hidden") {
|
|
1090
|
+
queuedModule.status = "pending";
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
break;
|
|
1094
|
+
case "set_priority":
|
|
1095
|
+
if (action.targetModuleId && action.newPriority !== undefined) {
|
|
1096
|
+
const queuedModule = this.state.queue.find((m) => m.moduleId === action.targetModuleId);
|
|
1097
|
+
if (queuedModule) {
|
|
1098
|
+
queuedModule.effectivePriority = action.newPriority;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
case "skip_to_end":
|
|
1103
|
+
// Marquer toutes les questions restantes du module comme skipped
|
|
1104
|
+
const currentQueuedModule = this.state.queue.find((m) => m.moduleId === this.state.currentModuleId);
|
|
1105
|
+
if (currentQueuedModule) {
|
|
1106
|
+
currentQueuedModule.questions.forEach((q) => {
|
|
1107
|
+
if (q.status === "pending") {
|
|
1108
|
+
q.status = "skipped";
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
// Avancer au module suivant et marquer qu'on l'a déjà fait
|
|
1113
|
+
this.advancedByTrigger = true;
|
|
1114
|
+
this.advanceToNextModule();
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// =====================================================
|
|
1119
|
+
// PRIORITÉ UTILISATEUR
|
|
1120
|
+
// =====================================================
|
|
1121
|
+
applyUserPriority(priority) {
|
|
1122
|
+
const priorityBoosts = getPriorityBoosts(this.config);
|
|
1123
|
+
const boosts = priorityBoosts[priority];
|
|
1124
|
+
if (!boosts)
|
|
1125
|
+
return;
|
|
1126
|
+
for (const boost of boosts) {
|
|
1127
|
+
const queuedModule = this.state.queue.find((m) => m.moduleId === boost.moduleId);
|
|
1128
|
+
if (queuedModule && queuedModule.status === "pending") {
|
|
1129
|
+
const module = this.modules.get(boost.moduleId);
|
|
1130
|
+
if (module && !module.isFixed) {
|
|
1131
|
+
queuedModule.effectivePriority = module.basePriority + boost.boost;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
this.recalculateQueue();
|
|
1136
|
+
this.notifyListeners();
|
|
1137
|
+
}
|
|
1138
|
+
recalculateQueue() {
|
|
1139
|
+
// Réévaluer les modules cachés
|
|
1140
|
+
this.modules.forEach((module) => {
|
|
1141
|
+
if (module.triggers) {
|
|
1142
|
+
for (const trigger of module.triggers) {
|
|
1143
|
+
if (this.evaluateModuleTrigger(trigger)) {
|
|
1144
|
+
this.executeAction(trigger.action);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
// Trier par priorité effective (ne pas toucher aux modules complétés/actifs)
|
|
1150
|
+
const completed = this.state.queue.filter((m) => m.status === "completed" || m.status === "active");
|
|
1151
|
+
const pending = this.state.queue.filter((m) => m.status === "pending" || m.status === "hidden");
|
|
1152
|
+
pending.sort((a, b) => a.effectivePriority - b.effectivePriority);
|
|
1153
|
+
this.state.queue = [...completed, ...pending];
|
|
1154
|
+
}
|
|
1155
|
+
// =====================================================
|
|
1156
|
+
// GETTERS
|
|
1157
|
+
// =====================================================
|
|
1158
|
+
getState() {
|
|
1159
|
+
return Object.assign({}, this.state);
|
|
1160
|
+
}
|
|
1161
|
+
getFormData() {
|
|
1162
|
+
return Object.assign({}, this.state.formData);
|
|
1163
|
+
}
|
|
1164
|
+
getAnsweredQuestions() {
|
|
1165
|
+
return [...this.state.answeredQuestions];
|
|
1166
|
+
}
|
|
1167
|
+
getProgress() {
|
|
1168
|
+
const totalQuestions = this.state.queue.reduce((acc, m) => acc + (m.status !== "hidden" ? m.questions.length : 0), 0);
|
|
1169
|
+
const answeredCount = this.state.answeredQuestions.length;
|
|
1170
|
+
return {
|
|
1171
|
+
current: answeredCount,
|
|
1172
|
+
total: totalQuestions,
|
|
1173
|
+
percent: totalQuestions > 0 ? Math.round((answeredCount / totalQuestions) * 100) : 0,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
getCurrentModuleName() {
|
|
1177
|
+
const module = this.getCurrentModule();
|
|
1178
|
+
return (module === null || module === void 0 ? void 0 : module.name) || "En cours...";
|
|
1179
|
+
}
|
|
1180
|
+
isComplete() {
|
|
1181
|
+
return this.state.isComplete;
|
|
1182
|
+
}
|
|
1183
|
+
// =====================================================
|
|
1184
|
+
// APPLICATION DES PRIORITÉS
|
|
1185
|
+
// =====================================================
|
|
1186
|
+
applyPriorityOrder(priorityOrder) {
|
|
1187
|
+
// Créer un mapping de priorités: plus haut dans la liste = priorité plus élevée
|
|
1188
|
+
// On utilise des valeurs négatives où plus négatif = plus prioritaire
|
|
1189
|
+
const priorityMappings = getPriorityMappings(this.config);
|
|
1190
|
+
const calc = getPriorityCalculation(this.config);
|
|
1191
|
+
priorityOrder.forEach((priority, index) => {
|
|
1192
|
+
const moduleIds = priorityMappings[priority];
|
|
1193
|
+
if (!moduleIds)
|
|
1194
|
+
return;
|
|
1195
|
+
// Calculer la priorité selon la config
|
|
1196
|
+
const newPriority = calc.base + (index * calc.step);
|
|
1197
|
+
// Appliquer la nouvelle priorité à tous les modules concernés
|
|
1198
|
+
moduleIds.forEach((moduleId) => {
|
|
1199
|
+
const queuedModule = this.state.queue.find((m) => m.moduleId === moduleId);
|
|
1200
|
+
if (queuedModule) {
|
|
1201
|
+
queuedModule.effectivePriority = newPriority;
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
// Retrier la queue selon les nouvelles priorités
|
|
1206
|
+
this.state.queue.sort((a, b) => {
|
|
1207
|
+
// Les modules fixes restent en place
|
|
1208
|
+
const moduleA = this.modules.get(a.moduleId);
|
|
1209
|
+
const moduleB = this.modules.get(b.moduleId);
|
|
1210
|
+
if ((moduleA === null || moduleA === void 0 ? void 0 : moduleA.isFixed) && !(moduleB === null || moduleB === void 0 ? void 0 : moduleB.isFixed))
|
|
1211
|
+
return -1;
|
|
1212
|
+
if (!(moduleA === null || moduleA === void 0 ? void 0 : moduleA.isFixed) && (moduleB === null || moduleB === void 0 ? void 0 : moduleB.isFixed))
|
|
1213
|
+
return 1;
|
|
1214
|
+
if ((moduleA === null || moduleA === void 0 ? void 0 : moduleA.isFixed) && (moduleB === null || moduleB === void 0 ? void 0 : moduleB.isFixed)) {
|
|
1215
|
+
return a.effectivePriority - b.effectivePriority;
|
|
1216
|
+
}
|
|
1217
|
+
return a.effectivePriority - b.effectivePriority;
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
// =====================================================
|
|
1221
|
+
// LISTENERS (pour la réactivité)
|
|
1222
|
+
// =====================================================
|
|
1223
|
+
subscribe(listener) {
|
|
1224
|
+
this.listeners.add(listener);
|
|
1225
|
+
return () => this.listeners.delete(listener);
|
|
1226
|
+
}
|
|
1227
|
+
notifyListeners() {
|
|
1228
|
+
const state = this.getState();
|
|
1229
|
+
this.listeners.forEach((listener) => listener(state));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Instance singleton
|
|
1233
|
+
let engineInstance = null;
|
|
1234
|
+
export function getQuestionEngine(i18n) {
|
|
1235
|
+
if (!engineInstance) {
|
|
1236
|
+
engineInstance = new QuestionEngine(i18n);
|
|
1237
|
+
}
|
|
1238
|
+
return engineInstance;
|
|
1239
|
+
}
|
|
1240
|
+
export function resetQuestionEngine(i18n) {
|
|
1241
|
+
engineInstance = new QuestionEngine(i18n);
|
|
1242
|
+
return engineInstance;
|
|
1243
|
+
}
|