@rsuci/shared-form-components 1.0.15 → 1.0.17

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.
@@ -0,0 +1,371 @@
1
+ /**
2
+ * FormTree - Arbre virtuel du formulaire
3
+ * RSU v2 - Gestion centralisée de l'état de visibilité et des jumps
4
+ *
5
+ * L'arbre virtuel (FormTree) est une représentation en mémoire de l'état du formulaire qui:
6
+ * 1. Connaît l'ordre de toutes les variables
7
+ * 2. Centralise l'évaluation des conditions
8
+ * 3. Stocke l'état de visibilité calculé
9
+ * 4. Gère nativement les jumps comme des "plages masquées"
10
+ */
11
+ import { ConditionEngine } from './condition-engine';
12
+ export class FormTree {
13
+ constructor(responses = {}, options = {}) {
14
+ this.groupNodes = new Map();
15
+ this.variableNodes = new Map();
16
+ this.orderedVariables = [];
17
+ this.jumpRanges = [];
18
+ this.jumpErrors = [];
19
+ this.responses = responses;
20
+ this.conditionEngine = new ConditionEngine(responses);
21
+ this.onJumpError = options.onJumpError;
22
+ this.debug = options.debug ?? false;
23
+ }
24
+ // ============ LOGGING ============
25
+ log(message, ...args) {
26
+ if (this.debug) {
27
+ console.log(`🌳 [FormTree] ${message}`, ...args);
28
+ }
29
+ }
30
+ // ============ CONSTRUCTION ============
31
+ buildFromFormulaire(groupes) {
32
+ this.log('Building tree from', groupes.length, 'groups');
33
+ this.groupNodes.clear();
34
+ this.variableNodes.clear();
35
+ this.orderedVariables = [];
36
+ // Trier les groupes par ordre
37
+ const sortedGroupes = [...groupes].sort((a, b) => a.ordre - b.ordre);
38
+ for (const groupe of sortedGroupes) {
39
+ // Trier les variables par ordre dans le groupe
40
+ const sortedVariables = [...groupe.variables].sort((a, b) => a.ordre - b.ordre);
41
+ const variableStates = sortedVariables.map(variable => {
42
+ const state = {
43
+ variable,
44
+ isVisible: variable.estVisible,
45
+ isJumpedOver: false,
46
+ isConditionMet: true,
47
+ isValid: true,
48
+ skipValidation: false,
49
+ currentValue: this.responses[variable.code]?.valeur ?? null,
50
+ shouldClearOnSave: false
51
+ };
52
+ this.variableNodes.set(variable.code, state);
53
+ this.orderedVariables.push(variable);
54
+ return state;
55
+ });
56
+ const groupState = {
57
+ groupe,
58
+ variables: variableStates,
59
+ isVisible: true,
60
+ isComplete: false,
61
+ validationErrors: [],
62
+ iterations: new Map()
63
+ };
64
+ this.groupNodes.set(groupe.code, groupState);
65
+ this.log(`Group ${groupe.code} built with ${variableStates.length} variables`);
66
+ }
67
+ // Évaluer toutes les conditions initiales
68
+ this.evaluateAll();
69
+ }
70
+ // ============ MISE À JOUR ============
71
+ updateResponses(responses) {
72
+ this.log('Updating responses');
73
+ this.responses = responses;
74
+ this.conditionEngine.updateContext(responses);
75
+ // Mettre à jour les valeurs dans les noeuds
76
+ for (const [code, state] of this.variableNodes) {
77
+ state.currentValue = responses[code]?.valeur ?? null;
78
+ }
79
+ // Réévaluer toutes les conditions
80
+ this.evaluateAll();
81
+ }
82
+ evaluateAll() {
83
+ this.log('Evaluating all conditions');
84
+ // Reset des états
85
+ this.jumpRanges = [];
86
+ this.jumpErrors = [];
87
+ for (const state of this.variableNodes.values()) {
88
+ state.isJumpedOver = false;
89
+ state.isConditionMet = true;
90
+ state.skipValidation = false;
91
+ state.shouldClearOnSave = false;
92
+ }
93
+ // Premier passage: évaluer les conditions et détecter les jumps
94
+ for (const groupNode of this.groupNodes.values()) {
95
+ this.evaluateGroupConditions(groupNode);
96
+ }
97
+ // Deuxième passage: appliquer les jumps
98
+ this.applyJumpRanges();
99
+ // Troisième passage: calculer la visibilité finale
100
+ this.computeFinalVisibility();
101
+ this.log('Evaluation complete:', {
102
+ activeJumps: this.jumpRanges.filter(j => j.isActive).length,
103
+ jumpErrors: this.jumpErrors.length
104
+ });
105
+ }
106
+ evaluateGroupConditions(groupNode) {
107
+ const { groupe } = groupNode;
108
+ this.log(`Evaluating group ${groupe.code}`);
109
+ // Évaluer la condition du groupe
110
+ if (groupe.conditionsAffichage) {
111
+ groupNode.isVisible = this.conditionEngine.evaluate(groupe.conditionsAffichage);
112
+ this.log(`Group ${groupe.code} visibility:`, groupNode.isVisible);
113
+ }
114
+ if (!groupNode.isVisible)
115
+ return;
116
+ // Évaluer les conditions de chaque variable
117
+ for (const varState of groupNode.variables) {
118
+ const condition = varState.variable.conditionsAffichage;
119
+ if (!condition) {
120
+ varState.isConditionMet = true;
121
+ continue;
122
+ }
123
+ // Détecter et traiter les jumps
124
+ if (this.containsJump(condition)) {
125
+ this.processJumpCondition(varState, groupe.code, condition);
126
+ // La variable source du jump reste visible (sa condition propre est évaluée séparément)
127
+ varState.isConditionMet = this.evaluateNonJumpPart(condition);
128
+ }
129
+ else {
130
+ varState.isConditionMet = this.conditionEngine.evaluate(condition);
131
+ }
132
+ this.log(`Variable ${varState.variable.code} conditionMet:`, varState.isConditionMet);
133
+ }
134
+ }
135
+ containsJump(condition) {
136
+ return /jump\s*\(/.test(condition);
137
+ }
138
+ /**
139
+ * Évalue la partie non-jump d'une condition mixte
140
+ * Ex: "showMe(${A} == '1') || jump(${B} == '2', ${Q10})" -> évalue seulement showMe(${A} == '1')
141
+ */
142
+ evaluateNonJumpPart(condition) {
143
+ // Retirer les appels jump() de la condition
144
+ const withoutJumps = condition
145
+ .replace(/jump\s*\([^)]*\)/g, 'true')
146
+ .replace(/\|\|\s*true/g, '')
147
+ .replace(/true\s*\|\|/g, '')
148
+ .replace(/&&\s*true/g, '')
149
+ .replace(/true\s*&&/g, '')
150
+ .trim();
151
+ if (!withoutJumps || withoutJumps === 'true') {
152
+ return true;
153
+ }
154
+ return this.conditionEngine.evaluate(withoutJumps);
155
+ }
156
+ processJumpCondition(sourceState, groupeCode, condition) {
157
+ this.log(`Processing jump condition for ${sourceState.variable.code}:`, condition);
158
+ // Parser tous les jumps dans la condition
159
+ // Pattern: jump(condition, ${VARIABLE_CIBLE})
160
+ const jumpPattern = /jump\s*\(\s*(.+?)\s*,\s*\$\{([A-Z_][A-Z0-9_]*)\}\s*\)/g;
161
+ let match;
162
+ while ((match = jumpPattern.exec(condition)) !== null) {
163
+ const innerCondition = match[1].trim();
164
+ const targetCode = match[2];
165
+ this.log(`Found jump: condition="${innerCondition}", target="${targetCode}"`);
166
+ const result = this.evaluateJump(sourceState, groupeCode, innerCondition, targetCode);
167
+ if (result.error) {
168
+ this.jumpErrors.push(result.error);
169
+ this.onJumpError?.(result.error);
170
+ continue;
171
+ }
172
+ if (result.canExecute && result.targetCode) {
173
+ const targetState = this.variableNodes.get(result.targetCode);
174
+ const jumpRange = {
175
+ sourceCode: sourceState.variable.code,
176
+ sourceOrdre: sourceState.variable.ordre,
177
+ targetCode: result.targetCode,
178
+ targetOrdre: targetState.variable.ordre,
179
+ groupeCode,
180
+ condition: innerCondition,
181
+ isActive: result.shouldActivate
182
+ };
183
+ this.jumpRanges.push(jumpRange);
184
+ this.log(`Jump range created:`, jumpRange);
185
+ }
186
+ }
187
+ }
188
+ evaluateJump(sourceState, groupeCode, innerCondition, targetCode) {
189
+ const result = {
190
+ isValidSyntax: true,
191
+ canExecute: false,
192
+ shouldActivate: false,
193
+ targetCode: null
194
+ };
195
+ // Validation: cible existe?
196
+ const targetState = this.variableNodes.get(targetCode);
197
+ if (!targetState) {
198
+ result.error = {
199
+ type: 'invalid_target',
200
+ sourceVariable: sourceState.variable.code,
201
+ targetVariable: targetCode,
202
+ message: `Variable cible "${targetCode}" non trouvée pour le jump depuis "${sourceState.variable.code}"`
203
+ };
204
+ return result;
205
+ }
206
+ // Validation: même groupe?
207
+ if (targetState.variable.groupeCode !== groupeCode) {
208
+ result.error = {
209
+ type: 'cross_group_jump',
210
+ sourceVariable: sourceState.variable.code,
211
+ targetVariable: targetCode,
212
+ message: `Jump inter-groupe non autorisé: "${sourceState.variable.code}" (${groupeCode}) → "${targetCode}" (${targetState.variable.groupeCode})`
213
+ };
214
+ return result;
215
+ }
216
+ // Validation: direction avant?
217
+ if (targetState.variable.ordre <= sourceState.variable.ordre) {
218
+ result.error = {
219
+ type: 'backward_jump',
220
+ sourceVariable: sourceState.variable.code,
221
+ targetVariable: targetCode,
222
+ message: `Jump arrière non autorisé: "${sourceState.variable.code}" (ordre ${sourceState.variable.ordre}) → "${targetCode}" (ordre ${targetState.variable.ordre})`
223
+ };
224
+ return result;
225
+ }
226
+ // Le jump est valide
227
+ result.canExecute = true;
228
+ result.targetCode = targetCode;
229
+ // Évaluer la condition du jump
230
+ try {
231
+ result.shouldActivate = this.conditionEngine.evaluate(innerCondition);
232
+ this.log(`Jump condition "${innerCondition}" evaluated to:`, result.shouldActivate);
233
+ }
234
+ catch (e) {
235
+ this.log(`Error evaluating jump condition:`, e);
236
+ result.shouldActivate = false;
237
+ }
238
+ return result;
239
+ }
240
+ applyJumpRanges() {
241
+ for (const jump of this.jumpRanges) {
242
+ if (!jump.isActive)
243
+ continue;
244
+ this.log(`Applying jump from ${jump.sourceCode} to ${jump.targetCode}`);
245
+ // Marquer toutes les variables entre source et cible comme "jumped over"
246
+ for (const [code, state] of this.variableNodes) {
247
+ if (state.variable.groupeCode !== jump.groupeCode)
248
+ continue;
249
+ const ordre = state.variable.ordre;
250
+ // Variables strictement entre source et cible (exclusif aux deux bornes)
251
+ if (ordre > jump.sourceOrdre && ordre < jump.targetOrdre) {
252
+ state.isJumpedOver = true;
253
+ state.skipValidation = true;
254
+ state.shouldClearOnSave = true;
255
+ this.log(`Variable ${code} marked as jumped over`);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ computeFinalVisibility() {
261
+ for (const state of this.variableNodes.values()) {
262
+ // Visible = base visible ET condition satisfaite ET pas sauté
263
+ state.isVisible =
264
+ state.variable.estVisible &&
265
+ state.isConditionMet &&
266
+ !state.isJumpedOver;
267
+ }
268
+ }
269
+ // ============ REQUÊTES ============
270
+ getVisibleVariables(groupeCode) {
271
+ const groupNode = this.groupNodes.get(groupeCode);
272
+ if (!groupNode || !groupNode.isVisible)
273
+ return [];
274
+ return groupNode.variables
275
+ .filter(state => state.isVisible)
276
+ .map(state => state.variable);
277
+ }
278
+ getGroupState(groupeCode) {
279
+ return this.groupNodes.get(groupeCode);
280
+ }
281
+ getVariableState(variableCode) {
282
+ return this.variableNodes.get(variableCode);
283
+ }
284
+ getJumpedVariableCodes(groupeCode) {
285
+ const codes = new Set();
286
+ for (const [code, state] of this.variableNodes) {
287
+ if (state.variable.groupeCode === groupeCode && state.isJumpedOver) {
288
+ codes.add(code);
289
+ }
290
+ }
291
+ return codes;
292
+ }
293
+ getActiveJumps() {
294
+ return this.jumpRanges.filter(j => j.isActive);
295
+ }
296
+ getJumpErrors() {
297
+ return [...this.jumpErrors];
298
+ }
299
+ // ============ VALIDATION ============
300
+ validateGroup(groupeCode) {
301
+ const groupNode = this.groupNodes.get(groupeCode);
302
+ if (!groupNode)
303
+ return { isValid: true, errors: [] };
304
+ const errors = [];
305
+ for (const state of groupNode.variables) {
306
+ // Ignorer les variables masquées ou sautées
307
+ if (!state.isVisible || state.skipValidation)
308
+ continue;
309
+ // Vérifier les champs obligatoires
310
+ if (state.variable.estObligatoire) {
311
+ const isEmpty = this.isValueEmpty(state.currentValue);
312
+ if (isEmpty) {
313
+ errors.push(`${state.variable.designation} est obligatoire`);
314
+ state.isValid = false;
315
+ state.validationError = 'Champ obligatoire';
316
+ }
317
+ else {
318
+ state.isValid = true;
319
+ state.validationError = undefined;
320
+ }
321
+ }
322
+ }
323
+ groupNode.validationErrors = errors;
324
+ groupNode.isComplete = errors.length === 0;
325
+ return { isValid: errors.length === 0, errors };
326
+ }
327
+ isValueEmpty(value) {
328
+ return value === null ||
329
+ value === undefined ||
330
+ value === '' ||
331
+ (Array.isArray(value) && value.length === 0);
332
+ }
333
+ getVariablesToClearOnSave() {
334
+ const codes = [];
335
+ for (const [code, state] of this.variableNodes) {
336
+ if (state.shouldClearOnSave && !this.isValueEmpty(state.currentValue)) {
337
+ codes.push(code);
338
+ }
339
+ }
340
+ return codes;
341
+ }
342
+ // ============ ACCÈS AU CONDITION ENGINE ============
343
+ /**
344
+ * Expose le ConditionEngine pour les fonctionnalités existantes
345
+ * (showMe, hideMe, setValeur, etc.)
346
+ */
347
+ getConditionEngine() {
348
+ return this.conditionEngine;
349
+ }
350
+ // ============ DEBUG ============
351
+ /**
352
+ * Retourne un snapshot de l'état complet pour debug
353
+ */
354
+ getDebugSnapshot() {
355
+ return {
356
+ groups: Array.from(this.groupNodes.values()).map(g => ({
357
+ code: g.groupe.code,
358
+ isVisible: g.isVisible,
359
+ variableCount: g.variables.length
360
+ })),
361
+ variables: Array.from(this.variableNodes.values()).map(v => ({
362
+ code: v.variable.code,
363
+ isVisible: v.isVisible,
364
+ isJumpedOver: v.isJumpedOver,
365
+ value: v.currentValue
366
+ })),
367
+ activeJumps: this.getActiveJumps(),
368
+ errors: this.jumpErrors
369
+ };
370
+ }
371
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Moteur d'évaluation des conditions scopées pour les variables ROSTER
3
+ * RSU v2 - Moteur de Rendu des Formulaires d'Enquête
4
+ *
5
+ * Ce moteur est ISOLÉ du ConditionEngine principal.
6
+ * Les conditions sont évaluées PAR LIGNE (par option de roster) indépendamment.
7
+ * Le scope est limité aux variables du même ROSTER uniquement.
8
+ */
9
+ export type RosterConditionErrorType = 'variable_not_in_scope' | 'jump_out_of_scope' | 'backward_jump' | 'self_reference' | 'syntax_error';
10
+ export interface RosterConditionError {
11
+ type: RosterConditionErrorType;
12
+ variableCode: string;
13
+ message: string;
14
+ }
15
+ export interface RosterConditionValidationResult {
16
+ isValid: boolean;
17
+ errors: RosterConditionError[];
18
+ }
19
+ export interface RosterVariableRef {
20
+ code: string;
21
+ ordre: number;
22
+ }
23
+ /**
24
+ * Moteur de conditions scopées pour les ROSTER
25
+ * Toutes les méthodes sont statiques pour une utilisation sans état
26
+ */
27
+ export declare class RosterConditionEngine {
28
+ /**
29
+ * Valide une condition au moment du design (admin)
30
+ * Vérifie que les références sont dans le scope et que les jumps sont valides
31
+ */
32
+ static validateCondition(condition: string, availableVariables: RosterVariableRef[], currentVariableCode: string, currentVariableOrdre: number): RosterConditionValidationResult;
33
+ /**
34
+ * Évalue une condition au runtime (rendu)
35
+ * Utilise les valeurs de la ligne courante du roster
36
+ */
37
+ static evaluate(condition: string, lineValues: Record<string, any>): boolean;
38
+ /**
39
+ * Extrait toutes les références de variables d'une condition
40
+ * Format: ${VAR_CODE}
41
+ */
42
+ static extractVariableReferences(condition: string): string[];
43
+ /**
44
+ * Extrait la cible d'un jump s'il existe dans la condition
45
+ * Format: jump(condition, ${TARGET})
46
+ */
47
+ static extractJumpTarget(condition: string): string | null;
48
+ /**
49
+ * Calcule les variables qui doivent être masquées à cause des jumps actifs
50
+ */
51
+ static computeJumpedVariables(sortedVariables: RosterVariableRef[], lineValues: Record<string, any>): Set<string>;
52
+ /**
53
+ * Valide la syntaxe de base d'une condition
54
+ */
55
+ private static validateSyntax;
56
+ }
57
+ //# sourceMappingURL=roster-condition-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roster-condition-engine.d.ts","sourceRoot":"","sources":["../../src/lib/roster-condition-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,wBAAwB,GAChC,uBAAuB,GACvB,mBAAmB,GACnB,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAC;AAEnB,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,qBAAa,qBAAqB;IAEhC;;;OAGG;IACH,MAAM,CAAC,iBAAiB,CACtB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,iBAAiB,EAAE,EACvC,mBAAmB,EAAE,MAAM,EAC3B,oBAAoB,EAAE,MAAM,GAC3B,+BAA+B;IAgElC;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO;IAyB5E;;;OAGG;IACH,MAAM,CAAC,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE;IAO7D;;;OAGG;IACH,MAAM,CAAC,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAO1D;;OAEG;IACH,MAAM,CAAC,sBAAsB,CAC3B,eAAe,EAAE,iBAAiB,EAAE,EACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC9B,GAAG,CAAC,MAAM,CAAC;IA2Bd;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;CA0B9B"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Moteur d'évaluation des conditions scopées pour les variables ROSTER
3
+ * RSU v2 - Moteur de Rendu des Formulaires d'Enquête
4
+ *
5
+ * Ce moteur est ISOLÉ du ConditionEngine principal.
6
+ * Les conditions sont évaluées PAR LIGNE (par option de roster) indépendamment.
7
+ * Le scope est limité aux variables du même ROSTER uniquement.
8
+ */
9
+ import { ConditionEngine } from './condition-engine';
10
+ /**
11
+ * Moteur de conditions scopées pour les ROSTER
12
+ * Toutes les méthodes sont statiques pour une utilisation sans état
13
+ */
14
+ export class RosterConditionEngine {
15
+ /**
16
+ * Valide une condition au moment du design (admin)
17
+ * Vérifie que les références sont dans le scope et que les jumps sont valides
18
+ */
19
+ static validateCondition(condition, availableVariables, currentVariableCode, currentVariableOrdre) {
20
+ const errors = [];
21
+ if (!condition?.trim()) {
22
+ return { isValid: true, errors: [] };
23
+ }
24
+ const availableCodes = new Set(availableVariables.map(v => v.code));
25
+ // 1. Vérifier les références de variables
26
+ const refs = this.extractVariableReferences(condition);
27
+ for (const ref of refs) {
28
+ if (ref === currentVariableCode) {
29
+ errors.push({
30
+ type: 'self_reference',
31
+ variableCode: ref,
32
+ message: 'Une variable ne peut pas se référencer elle-même'
33
+ });
34
+ }
35
+ else if (!availableCodes.has(ref)) {
36
+ errors.push({
37
+ type: 'variable_not_in_scope',
38
+ variableCode: ref,
39
+ message: `La variable \${${ref}} n'existe pas dans ce Roster`
40
+ });
41
+ }
42
+ }
43
+ // 2. Vérifier le jump s'il existe
44
+ const jumpTarget = this.extractJumpTarget(condition);
45
+ if (jumpTarget) {
46
+ if (!availableCodes.has(jumpTarget)) {
47
+ errors.push({
48
+ type: 'jump_out_of_scope',
49
+ variableCode: jumpTarget,
50
+ message: `La variable cible \${${jumpTarget}} n'existe pas dans ce Roster`
51
+ });
52
+ }
53
+ else {
54
+ const targetVar = availableVariables.find(v => v.code === jumpTarget);
55
+ if (targetVar && targetVar.ordre <= currentVariableOrdre) {
56
+ errors.push({
57
+ type: 'backward_jump',
58
+ variableCode: jumpTarget,
59
+ message: 'Le jump ne peut pas cibler une variable précédente'
60
+ });
61
+ }
62
+ }
63
+ }
64
+ // 3. Valider la syntaxe générale
65
+ const syntaxError = this.validateSyntax(condition);
66
+ if (syntaxError) {
67
+ errors.push({
68
+ type: 'syntax_error',
69
+ variableCode: '',
70
+ message: syntaxError
71
+ });
72
+ }
73
+ return {
74
+ isValid: errors.length === 0,
75
+ errors
76
+ };
77
+ }
78
+ /**
79
+ * Évalue une condition au runtime (rendu)
80
+ * Utilise les valeurs de la ligne courante du roster
81
+ */
82
+ static evaluate(condition, lineValues) {
83
+ if (!condition?.trim()) {
84
+ return true;
85
+ }
86
+ try {
87
+ // Construire un contexte isolé pour cette ligne du roster
88
+ const responses = {};
89
+ for (const [code, value] of Object.entries(lineValues)) {
90
+ responses[code] = {
91
+ variableCode: code,
92
+ valeur: value,
93
+ dateModification: new Date()
94
+ };
95
+ }
96
+ // Utiliser le ConditionEngine en composition (pas héritage)
97
+ const engine = new ConditionEngine(responses);
98
+ return engine.evaluate(condition);
99
+ }
100
+ catch (error) {
101
+ console.warn('[RosterConditionEngine] Erreur évaluation:', condition, error);
102
+ return true; // En cas d'erreur, afficher par défaut
103
+ }
104
+ }
105
+ /**
106
+ * Extrait toutes les références de variables d'une condition
107
+ * Format: ${VAR_CODE}
108
+ */
109
+ static extractVariableReferences(condition) {
110
+ if (!condition)
111
+ return [];
112
+ const matches = condition.match(/\$\{([A-Z_][A-Z0-9_]*)\}/g) || [];
113
+ return matches.map(m => m.slice(2, -1));
114
+ }
115
+ /**
116
+ * Extrait la cible d'un jump s'il existe dans la condition
117
+ * Format: jump(condition, ${TARGET})
118
+ */
119
+ static extractJumpTarget(condition) {
120
+ if (!condition)
121
+ return null;
122
+ const match = condition.match(/jump\s*\([^,]+,\s*\$\{([A-Z_][A-Z0-9_]*)\}\s*\)/);
123
+ return match ? match[1] : null;
124
+ }
125
+ /**
126
+ * Calcule les variables qui doivent être masquées à cause des jumps actifs
127
+ */
128
+ static computeJumpedVariables(sortedVariables, lineValues) {
129
+ const jumped = new Set();
130
+ for (const rosterVar of sortedVariables) {
131
+ const condition = rosterVar.conditionsAffichage;
132
+ if (!condition)
133
+ continue;
134
+ const jumpTarget = this.extractJumpTarget(condition);
135
+ if (!jumpTarget)
136
+ continue;
137
+ // Évaluer si le jump doit s'activer
138
+ if (this.evaluate(condition, lineValues)) {
139
+ const targetVar = sortedVariables.find(v => v.code === jumpTarget);
140
+ if (targetVar) {
141
+ // Masquer toutes les variables entre la source et la cible
142
+ for (const v of sortedVariables) {
143
+ if (v.ordre > rosterVar.ordre && v.ordre < targetVar.ordre) {
144
+ jumped.add(v.code);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return jumped;
151
+ }
152
+ /**
153
+ * Valide la syntaxe de base d'une condition
154
+ */
155
+ static validateSyntax(condition) {
156
+ try {
157
+ // Vérifier les parenthèses équilibrées
158
+ let depth = 0;
159
+ for (const char of condition) {
160
+ if (char === '(')
161
+ depth++;
162
+ if (char === ')')
163
+ depth--;
164
+ if (depth < 0) {
165
+ return 'Parenthèses non équilibrées';
166
+ }
167
+ }
168
+ if (depth !== 0) {
169
+ return 'Parenthèses non équilibrées';
170
+ }
171
+ // Vérifier les fonctions reconnues
172
+ const functionMatch = condition.match(/^(showMe|hideMe|jump)\s*\(/);
173
+ if (!functionMatch && !condition.includes('${')) {
174
+ return 'Condition invalide: utilisez showMe(), hideMe() ou jump()';
175
+ }
176
+ return null;
177
+ }
178
+ catch (error) {
179
+ return `Erreur de syntaxe: ${error instanceof Error ? error.message : 'Erreur inconnue'}`;
180
+ }
181
+ }
182
+ }