data-primals-engine 1.0.11 → 1.1.0

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.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +212 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. package/vitest.config.js +2 -2
@@ -0,0 +1,199 @@
1
+ import React, {useEffect} from 'react';
2
+ import { FaTrash, FaEquals } from 'react-icons/fa';
3
+ import {getFieldDefinitionFromPath, isAtomicDateOperator} from "./core/data.js";
4
+ import {MONGO_OPERATORS, OPERAND_TYPES} from "./constants.js";
5
+
6
+ // --- Sub-component for a single calculation step ---
7
+ const CalculationStepRow = ({
8
+ step,
9
+ onUpdateStep,
10
+ onRemoveStep,
11
+ allFlattenedFields,
12
+ previousStepAliases,
13
+ isLastStep,
14
+ currentModelName,
15
+ models,
16
+ }) => {
17
+ const handleOperandChange = (operandKey, part, value) => {
18
+ const newOperand = { ...step[operandKey], [part]: value };
19
+ if (part === 'type') {
20
+ if (value === OPERAND_TYPES.CONSTANT) {
21
+ newOperand.value = (step.operator === MONGO_OPERATORS.CONCAT.mongo) ? '' : 0;
22
+ } else {
23
+ newOperand.value = '';
24
+ }
25
+ }
26
+ onUpdateStep(step.id, { ...step, [operandKey]: newOperand });
27
+ };
28
+
29
+ const handleOperatorChange = (e) => {
30
+ const newOperator = e.target.value;
31
+ const updatedStep = { ...step, operator: newOperator };
32
+
33
+ const adjustConstantOperand = (operand) => {
34
+ if (operand.type === OPERAND_TYPES.CONSTANT) {
35
+ if (newOperator === MONGO_OPERATORS.CONCAT.mongo) {
36
+ operand.value = String(operand.value);
37
+ } else {
38
+ const num = parseFloat(operand.value);
39
+ operand.value = isNaN(num) ? 0 : num;
40
+ }
41
+ }
42
+ return operand;
43
+ };
44
+
45
+ updatedStep.operand1 = adjustConstantOperand({...updatedStep.operand1});
46
+ if (!isAtomicDateOperator(newOperator)) {
47
+ updatedStep.operand2 = adjustConstantOperand({...updatedStep.operand2});
48
+ } else {
49
+ delete updatedStep.operand2;
50
+ }
51
+ onUpdateStep(step.id, updatedStep);
52
+ };
53
+
54
+ const handleAliasChange = (e) => {
55
+ onUpdateStep(step.id, { ...step, outputAlias: e.target.value.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_]/g, '') });
56
+ };
57
+
58
+ const renderOperandInput = (operandKey) => {
59
+ const operand = step[operandKey];
60
+ const isDateOperator = Object.values(MONGO_OPERATORS).filter(op => op.isDate).map(op => op.mongo).includes(step.operator);
61
+ const isAtomic = isAtomicDateOperator(step.operator);
62
+ const isDisabled = isAtomic && isDateOperator;
63
+ // Filtrage des champs pour le type d'opérande, appliqué au mapping des options
64
+ const filteredFields = allFlattenedFields.filter(field => {
65
+ if (step.operator === MONGO_OPERATORS.CONCAT.mongo) return true;
66
+ const targetFieldDef = getFieldDefinitionFromPath(field.value, currentModelName, models);
67
+ return targetFieldDef && (isDateOperator ? ['date', 'datetime'].includes(targetFieldDef.type) : targetFieldDef.type === 'number');
68
+ });
69
+
70
+ // Group fields by model for the dropdown
71
+ const groupedFields = filteredFields.reduce((acc, field) => {
72
+ const modelLabel = field.modelLabel || 'Current Model';
73
+ if (!acc[modelLabel]) {
74
+ acc[modelLabel] = [];
75
+ }
76
+ acc[modelLabel].push(field);
77
+ return acc;
78
+ }, {});
79
+
80
+ return (
81
+ <div className="flex">
82
+ <select
83
+ value={operand.type}
84
+ onChange={(e) => handleOperandChange(operandKey, 'type', e.target.value)}
85
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
86
+ disabled={isDisabled}
87
+ >
88
+ <option value={OPERAND_TYPES.FIELD}>Field</option>
89
+ {!isDateOperator && !isAtomic && <option value={OPERAND_TYPES.CONSTANT}>Constant</option>}
90
+ {previousStepAliases.length > 0 && (
91
+ <option value={OPERAND_TYPES.PREVIOUS_STEP}>Previous Step Result</option>
92
+ )}
93
+ </select>
94
+
95
+ {operand.type === OPERAND_TYPES.FIELD && (
96
+ <select
97
+ value={operand.value}
98
+ onChange={(e) => handleOperandChange(operandKey, 'value', e.target.value)}
99
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc', minWidth: '150px' }}
100
+ >
101
+ <option value="">Select Field</option>
102
+ {Object.entries(groupedFields).map(([modelLabel, fieldsInGroup]) => (
103
+ <optgroup label={modelLabel} key={modelLabel}>
104
+ {fieldsInGroup.map((field) => (
105
+ <option key={field.value} value={field.value}>
106
+ {field.label}
107
+ </option>
108
+ ))}
109
+ </optgroup>
110
+ ))}
111
+ </select>
112
+ )}
113
+
114
+ {operand.type === OPERAND_TYPES.CONSTANT && (
115
+ (step.operator === MONGO_OPERATORS.CONCAT.mongo) ? (
116
+ <input
117
+ type="text"
118
+ value={operand.value}
119
+ onChange={(e) => handleOperandChange(operandKey, 'value', e.target.value)}
120
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc', width: '100px' }}
121
+ placeholder="Enter text"
122
+ />
123
+ ) : (
124
+ <input
125
+ type="number"
126
+ value={operand.value}
127
+ onChange={(e) => handleOperandChange(operandKey, 'value', e.target.value === '' ? '' : parseFloat(e.target.value))}
128
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc', width: '100px' }}
129
+ />
130
+ )
131
+ )}
132
+
133
+ {operand.type === OPERAND_TYPES.PREVIOUS_STEP && (
134
+ <select
135
+ value={operand.value}
136
+ onChange={(e) => handleOperandChange(operandKey, 'value', e.target.value)}
137
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc', minWidth: '120px' }}
138
+ >
139
+ <option value="">Select Step</option>
140
+ {previousStepAliases.map((alias) => (
141
+ <option key={alias} value={alias}>
142
+ {alias}
143
+ </option>
144
+ ))}
145
+ </select>
146
+ )}
147
+ </div>
148
+ );
149
+ };
150
+
151
+
152
+ return (
153
+ <div style={{
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ gap: '10px',
157
+ padding: '10px',
158
+ border: '1px solid #eee',
159
+ borderRadius: '4px',
160
+ marginBottom: '10px',
161
+ flexWrap: 'wrap',
162
+ backgroundColor: isLastStep ? '#f0f8ff' : '#fff'
163
+ }}>
164
+ <input
165
+ type="text"
166
+ placeholder="Output Alias (no spaces)"
167
+ value={step.outputAlias}
168
+ onChange={handleAliasChange}
169
+ style={{ maxWidth: "120px", padding: '8px', borderRadius: '4px', border: '1px solid #ccc', fontWeight: 'bold' }}
170
+ title="Name for the result of this calculation step (e.g., my_calculated_value)"
171
+ />
172
+ <FaEquals color="#555" />
173
+ {renderOperandInput('operand1')}
174
+ <select
175
+ value={step.operator}
176
+ onChange={handleOperatorChange}
177
+ style={{ padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
178
+ >
179
+ {Object.entries(MONGO_OPERATORS).map(([key, op]) => (
180
+ <option key={key} value={op.mongo}>
181
+ {op.label}
182
+ </option>
183
+ ))}
184
+ </select>
185
+ {/* Suppression du second operande si l'opérateur est atomique */}
186
+ {!isAtomicDateOperator(step.operator) && renderOperandInput('operand2')}
187
+ <button
188
+ type="button"
189
+ onClick={() => onRemoveStep(step.id)}
190
+ style={{ background: 'none', border: 'none', color: 'red', cursor: 'pointer', fontSize: '1.2em' }}
191
+ title="Remove this step"
192
+ >
193
+ <FaTrash />
194
+ </button>
195
+ </div>
196
+ );
197
+ };
198
+
199
+ export default CalculationStepRow;
@@ -0,0 +1,49 @@
1
+ // src/components/dataview/CalendarConfigModal.jsx
2
+ import React, { useState, useMemo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {Dialog} from "./Dialog.jsx";
5
+
6
+ // Remplacez par vos propres composants Modal, Select, Button
7
+ const Modal = ({ isOpen, onClose, title, children }) => isOpen ? <div style={{...modalStyles}}>{title}{children}{onClose}</div> : null;
8
+ const Select = ({ children, ...props }) => <select {...props}>{children}</select>;
9
+ const Button = (props) => <button {...props} />;
10
+
11
+ export default function CalendarConfigModal({ isOpen, onClose, onSave, modelFields }) {
12
+ const { t } = useTranslation();
13
+ const [selectedField, setSelectedField] = useState('');
14
+
15
+ const dateFields = useMemo(() =>
16
+ modelFields.filter(f => f.type === 'date' || f.type === 'datetime'),
17
+ [modelFields]
18
+ );
19
+
20
+ const handleSave = () => {
21
+ if (selectedField) {
22
+ onSave({ dateField: selectedField });
23
+ }
24
+ };
25
+
26
+ if (!isOpen) return null;
27
+
28
+ return (
29
+ // Utilisez votre propre composant Modal ici
30
+ <Dialog isOpen={isOpen} onClose={onClose}>
31
+ <h2>{t('dataview.calendar.configureTitle')}</h2>
32
+ <p>{t('dataview.calendar.notConfigured')}</p>
33
+
34
+ <div style={{ margin: '20px 0' }}>
35
+ <label htmlFor="dateField">{t('dataview.calendar.dateFieldLabel')}</label>
36
+ <br />
37
+ <Select id="dateField" value={selectedField} onChange={e => setSelectedField(e.target.value)}>
38
+ <option value="">{t('dataview.calendar.dateFieldPlaceholder')}</option>
39
+ {dateFields.map(field => (
40
+ <option key={field.name} value={field.name}>{field.name}</option>
41
+ ))}
42
+ </Select>
43
+ </div>
44
+
45
+ <Button onClick={handleSave} disabled={!selectedField}>{t('btns.saveConfig')}</Button>
46
+ <Button onClick={onClose} style={{ marginLeft: '10px' }}>{t('btns.cancel')}</Button>
47
+ </Dialog>
48
+ );
49
+ }
@@ -0,0 +1,402 @@
1
+ // C:/Dev/hackersonline-engine/client/src/ChartConfigModal.jsx
2
+ import React, { useState, useEffect } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useModelContext } from './contexts/ModelContext.jsx';
5
+ import {ColorField, SelectField, TextField} from './Field.jsx';
6
+ import Button from './Button.jsx';
7
+ import { useAuthContext } from "./contexts/AuthContext.jsx";
8
+ import { Dialog } from "./Dialog.jsx";
9
+ import ConditionBuilder from "./ConditionBuilder.jsx";
10
+
11
+ // Styles (supposés définis ailleurs ou inline pour l'exemple)
12
+ const modalStyles = {
13
+ overlay: {
14
+ position: 'fixed',
15
+ top: 0,
16
+ left: 0,
17
+ right: 0,
18
+ bottom: 0,
19
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ zIndex: 1000, // Assurez-vous qu'il est au-dessus des autres éléments
24
+ },
25
+ content: {
26
+ backgroundColor: '#fff',
27
+ padding: '20px',
28
+ borderRadius: '8px',
29
+ boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
30
+ minWidth: '320px',
31
+ maxWidth: '90vw',
32
+ maxHeight: '90vh',
33
+ overflowY: 'auto',
34
+ display: 'flex',
35
+ flexDirection: 'column',
36
+ gap: '15px', // Espace entre les champs
37
+ }
38
+ };
39
+
40
+ // *** AJOUT: Prop initialConfig avec valeur par défaut null ***
41
+ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) => {
42
+ const { t } = useTranslation();
43
+ const { models } = useModelContext();
44
+ const { me } = useAuthContext();
45
+
46
+ // États internes (inchangés)
47
+ const [selectedModel, setSelectedModel] = useState('');
48
+ const [filter, setFilter] = useState({});
49
+ const [modelFields, setModelFields] = useState([]);
50
+ const [xAxisField, setXAxisField] = useState('');
51
+ const [yAxisField, setYAxisField] = useState('');
52
+ const [groupByField, setGroupByField] = useState('');
53
+ const [colorField, setColorField] = useState('#4BC0C0');
54
+ const [groupByLabelField, setGroupByLabelField] = useState('');
55
+ const [relatedModelFields, setRelatedModelFields] = useState([]);
56
+ const [chartType, setChartType] = useState('bar');
57
+ const [chartAggregationType, setChartAggregationType] = useState('count');
58
+ const [chartTitle, setChartTitle] = useState('');
59
+
60
+ const currentModel = models.find(f => f.name === selectedModel && f._user === me.username);
61
+
62
+ // *** AJOUT: useEffect pour initialiser ou réinitialiser l'état basé sur initialConfig ***
63
+ useEffect(() => {
64
+ // Ne s'exécute que si le modal est ouvert
65
+ if (isOpen) {
66
+ if (initialConfig) {
67
+ // Mode édition: Pré-remplir les états
68
+ setSelectedModel(initialConfig.model || '');
69
+ setFilter(initialConfig.filter);
70
+ setChartTitle(initialConfig.title || '');
71
+ setChartType(initialConfig.type || 'bar');
72
+ setXAxisField(initialConfig.xAxis || '');
73
+ setYAxisField(initialConfig.yAxis || '');
74
+ setGroupByField(initialConfig.groupBy || '');
75
+ setColorField(initialConfig.chartBackgroundColor || null);
76
+ setGroupByLabelField(initialConfig.groupByLabelField || '');
77
+ setChartAggregationType(initialConfig.aggregationType || 'count');
78
+ // Note: modelFields et relatedModelFields seront chargés par les autres useEffects
79
+ // déclenchés par le changement de selectedModel et groupByField.
80
+ } else {
81
+ // Mode ajout: Réinitialiser les états (sécurité, même si déjà fait au changement de modèle)
82
+ setSelectedModel('');
83
+ setFilter(null);
84
+ setChartTitle('');
85
+ setChartType('bar');
86
+ setXAxisField('');
87
+ setYAxisField('');
88
+ setGroupByField('');
89
+ setGroupByLabelField('');
90
+ setChartAggregationType('count');
91
+ setModelFields([]);
92
+ setRelatedModelFields([]);
93
+ }
94
+ }
95
+ // De son absence)
96
+ }, [isOpen, initialConfig]);
97
+
98
+
99
+ // useEffect pour charger les champs du modèle (inchangé)
100
+ useEffect(() => {
101
+ if (selectedModel && models) {
102
+ const modelDef = models.find(m => m.name === selectedModel);
103
+ const fields = modelDef ? modelDef.fields : [];
104
+ setModelFields(fields);
105
+ // Réinitialiser les champs dépendants SEULEMENT si ce n'est pas le chargement initial en mode édition
106
+ if (!initialConfig || selectedModel !== initialConfig.model) {
107
+ setXAxisField('');
108
+ setYAxisField('');
109
+ setFilter(null);
110
+ setGroupByField('');
111
+ setGroupByLabelField('');
112
+ setRelatedModelFields([]);
113
+ setChartAggregationType('count');
114
+ // Ne pas réinitialiser le titre ici, car il peut être défini avant le modèle
115
+ }
116
+ } else {
117
+ setModelFields([]);
118
+ }
119
+ }, [selectedModel, models, initialConfig]); // Ajouter initialConfig aux dépendances
120
+
121
+ // useEffect pour charger les champs liés
122
+ useEffect(() => {
123
+ if (groupByField && models && modelFields.length > 0) {
124
+ const mainModelField = modelFields.find(f => f.name === groupByField);
125
+ if (mainModelField?.type === 'relation') {
126
+ const relatedModelName = mainModelField.relation;
127
+ const relatedModelDef = models.find(m => m.name === relatedModelName);
128
+ const fields = relatedModelDef ? relatedModelDef.fields : [];
129
+ setRelatedModelFields(fields);
130
+ // Pré-remplir groupByLabelField si en mode édition, sinon chercher le défaut
131
+ const labelToSet = (initialConfig && groupByField === initialConfig.groupBy && initialConfig.groupByLabelField)
132
+ ? initialConfig.groupByLabelField
133
+ : (fields.find(f => f.asMain && ['string', 'string_t', 'enum'].includes(f.type))?.name ||
134
+ fields.find(f => f.name === 'name' && ['string', 'string_t', 'enum'].includes(f.type))?.name ||
135
+ fields.find(f => f.name === 'title' && ['string', 'string_t', 'enum'].includes(f.type))?.name || '');
136
+ setGroupByLabelField(labelToSet);
137
+ } else {
138
+ setRelatedModelFields([]);
139
+ setGroupByLabelField('');
140
+ }
141
+ } else {
142
+ setRelatedModelFields([]);
143
+ setGroupByLabelField('');
144
+ }
145
+ }, [groupByField, modelFields, models, initialConfig]); // Ajouter initialConfig aux dépendances
146
+
147
+ // useEffect pour réinitialiser l'agrégation si l'axe Y est enlevé (inchangé)
148
+ useEffect(() => {
149
+ if (!yAxisField && chartAggregationType !== 'count') {
150
+ setChartAggregationType('count');
151
+ }
152
+ }, [yAxisField, chartAggregationType]);
153
+
154
+ // Logique de validation et options (inchangée)
155
+ const isGroupingChart = ['pie', 'doughnut'].includes(chartType);
156
+ const isRelationGroupBy = isGroupingChart && modelFields.find(f => f.name === groupByField)?.type === 'relation';
157
+ const isYAxisRequiredForValidation = chartAggregationType && !['count'].includes(chartAggregationType);
158
+
159
+ // handleSave (inchangé - envoie l'état actuel)
160
+ const handleSave = () => {
161
+ const isValid = selectedModel && chartType && chartTitle &&
162
+ (isGroupingChart
163
+ ? (groupByField && (!isRelationGroupBy || groupByLabelField) && (isYAxisRequiredForValidation ? !!yAxisField : true))
164
+ : (xAxisField && chartAggregationType && (isYAxisRequiredForValidation ? !!yAxisField : true))
165
+ );
166
+
167
+ if (isValid) {
168
+ // L'objet envoyé contient l'état actuel des champs
169
+ onSave({
170
+ // *** AJOUT: Inclure l'ID si on est en mode édition ***
171
+ id: initialConfig?.id, // Sera undefined en mode ajout
172
+ title: chartTitle,
173
+ model: selectedModel,
174
+ xAxis: !isGroupingChart ? xAxisField : undefined,
175
+ yAxis: yAxisField || undefined,
176
+ groupBy: isGroupingChart ? groupByField : undefined,
177
+ groupByLabelField: isRelationGroupBy ? groupByLabelField : undefined,
178
+ type: chartType,
179
+ aggregationType: chartAggregationType,
180
+ chartBackgroundColor: colorField,
181
+ filter,
182
+ });
183
+ } else {
184
+ let errorMsg = t('chartConfigModal.fillFields', "Veuillez remplir tous les champs requis.");
185
+ if (isYAxisRequiredForValidation && !yAxisField) {
186
+ errorMsg += ` ${t('chartConfigModal.yAxisRequiredForAggregation', "L'axe Y est requis pour l'agrégation '{aggregation}'.", { aggregation: t('aggregation.'+chartAggregationType, chartAggregationType) })}`;
187
+ }
188
+ alert(errorMsg);
189
+ }
190
+ };
191
+
192
+ if (!isOpen) return null;
193
+
194
+ // Options pour les SelectFields (inchangées)
195
+ const modelOptions = models
196
+ ? models.filter(f => f._user === me?.username).map(m => ({ label: t(`model_${m.name}`, m.name), value: m.name }))
197
+ : [];
198
+ const xAxisOptions = modelFields
199
+ .filter(f => ['string', 'string_t', 'enum', 'date', 'datetime', 'number', 'boolean'].includes(f.type))
200
+ .map(f => ({ label: t(`field_${selectedModel}_${f.name}`, f.name), value: f.name }));
201
+ const yAxisOptions = modelFields
202
+ .filter(f => ['number'].includes(f.type))
203
+ .map(f => ({ label: t(`field_${selectedModel}_${f.name}`, f.name), value: f.name }));
204
+ const groupByOptions = modelFields
205
+ .filter(f => f.type === 'enum' || f.type === 'relation')
206
+ .map(f => ({ label: `${t(`field_${selectedModel}_${f.name}`, f.name)} (${f.type})`, value: f.name }));
207
+ const groupByLabelOptions = relatedModelFields
208
+ .filter(f => ['string', 'string_t', 'enum'].includes(f.type))
209
+ .map(f => ({ label: t(`field_${relatedModelFields.find(rm=>rm.name === f.name)?.relation || selectedModel}_${f.name}`, f.name), value: f.name }));
210
+ const chartTypeOptions = [
211
+ { label: t('chartType.bar', 'Barres'), value: 'bar' },
212
+ { label: t('chartType.line', 'Ligne'), value: 'line' },
213
+ { label: t('chartType.pie', 'Secteurs'), value: 'pie' },
214
+ { label: t('chartType.doughnut', 'Donut'), value: 'doughnut' },
215
+ ];
216
+ // Options d'agrégation de base - AJOUT DE 'value'
217
+ const baseAggregationOptions = [
218
+ { label: t('aggregation.count', 'Count'), value: 'count' },
219
+ { label: t('aggregation.value', 'Value'), value: 'value' }, // Option pour afficher la valeur brute
220
+ { label: t('aggregation.sum', 'Sum'), value: 'sum' },
221
+ { label: t('aggregation.avg', 'Average'), value: 'avg' },
222
+ { label: t('aggregation.median', 'Median'), value: 'median' }, // Attention: median peut être coûteux en perf
223
+ { label: t('aggregation.min', 'Minimum'), value: 'min' },
224
+ { label: t('aggregation.max', 'Maximum'), value: 'max' }
225
+ ];
226
+
227
+ // Filtrer les options d'agrégation disponibles
228
+ // 'value', 'sum', 'avg', etc. ne sont dispo que si yAxisField est sélectionné
229
+ const availableAggregationOptions = yAxisField
230
+ ? baseAggregationOptions
231
+ : baseAggregationOptions.filter(opt => opt.value === 'count');
232
+
233
+ // Validation pour le bouton Enregistrer (inchangée)
234
+ const isSaveDisabled = !selectedModel || !chartTitle || !chartType ||
235
+ (isGroupingChart
236
+ ? (!groupByField || (isRelationGroupBy && !groupByLabelField) || (isYAxisRequiredForValidation && !yAxisField))
237
+ : (!xAxisField || !chartAggregationType || (isYAxisRequiredForValidation && !yAxisField))
238
+ );
239
+
240
+ return (
241
+ <Dialog isOpen={isOpen} onClose={onClose} isClosable>
242
+ <form onSubmit={(e) => {
243
+ e.preventDefault();
244
+ handleSave();
245
+ return false;
246
+ }}>
247
+ {/* *** MODIFICATION: Titre dynamique *** */}
248
+ <h2>{initialConfig
249
+ ? t('chartConfigModal.editTitle', 'Modifier le Graphique')
250
+ : t('chartConfigModal.addTitle', 'Configurer un Nouveau Graphique')}
251
+ </h2>
252
+
253
+ {/* Champs TextField et SelectField (inchangés dans leur structure) */}
254
+ <TextField
255
+ label={t('chartConfigModal.chartTitle', 'Titre du Graphique')}
256
+ value={chartTitle}
257
+ onChange={(e) => setChartTitle(e.target.value)}
258
+ required
259
+ />
260
+ <SelectField
261
+ label={t('chartConfigModal.model', 'Source de Données (Modèle)')}
262
+ value={selectedModel}
263
+ onChange={(item) => setSelectedModel(item.value)}
264
+ items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...modelOptions]}
265
+ required
266
+ // *** AJOUT: Désactiver le modèle en mode édition pour éviter les incohérences ***
267
+ hint={initialConfig ? t('chartConfigModal.modelCantBeChanged', 'Le modèle ne peut pas être changé en mode édition.') : ''}
268
+ />
269
+ {/* Section pour le ConditionBuilder */}
270
+ {currentModel?.fields.length > 0 && (
271
+ <div className="flex"><label>{t('modelcreator.relationFilter', 'Filtre sur les données')}</label>
272
+ <ConditionBuilder
273
+ modelFields={currentModel.fields}
274
+ selectableModels={false}
275
+ model={currentModel.name} // Nom du modèle actuel du graphique
276
+ models={models} // Liste de tous les modèles (pour les relations potentielles dans le filtre)
277
+ initialValue={filter} // Le filtre actuel du graphique
278
+ onChange={(e) => setFilter(e)} // Fonction pour mettre à jour chartConfig.filter
279
+ /></div>
280
+ )}
281
+ <SelectField
282
+ label={t('chartConfigModal.chartType', 'Type de Graphique')}
283
+ value={chartType}
284
+ onChange={(item) => setChartType(item.value)}
285
+ items={chartTypeOptions}
286
+ required
287
+ />
288
+
289
+ {selectedModel && (
290
+ <>
291
+
292
+
293
+
294
+ {/* Affichage GroupBy pour Pie/Doughnut, XAxis pour les autres */}
295
+ {isGroupingChart ? (
296
+ <>
297
+ <SelectField
298
+ label={t('chartConfigModal.groupBy', 'Répartir par (Enum/Relation)')}
299
+ value={groupByField}
300
+ onChange={(item) => setGroupByField(item.value)}
301
+ items={[{
302
+ label: t('selectPlaceholder', 'Choisir...'),
303
+ value: ''
304
+ }, ...groupByOptions]}
305
+ required={isGroupingChart}
306
+ disabled={groupByOptions.length === 0}
307
+ hint={groupByOptions.length === 0 ? t('chartConfigModal.noGroupableFields', 'Aucun champ groupable disponible') : ''}
308
+ />
309
+ {isRelationGroupBy && (
310
+ <SelectField
311
+ label={t('chartConfigModal.groupByLabel', 'Champ Label (Relation)')}
312
+ value={groupByLabelField}
313
+ onChange={(item) => setGroupByLabelField(item.value)}
314
+ items={[{
315
+ label: t('selectPlaceholder', 'Choisir...'),
316
+ value: ''
317
+ }, ...groupByLabelOptions]}
318
+ required={isRelationGroupBy}
319
+ disabled={relatedModelFields.length === 0}
320
+ hint={relatedModelFields.length === 0 && groupByField ? t('chartConfigModal.loadingRelated', 'Chargement...') : ''}
321
+ />
322
+ )}
323
+ </>
324
+ ) : (
325
+ <SelectField
326
+ label={t('chartConfigModal.xAxis', 'Axe X')}
327
+ value={xAxisField}
328
+ onChange={(item) => setXAxisField(item.value)}
329
+ items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...xAxisOptions]}
330
+ required={!isGroupingChart}
331
+ disabled={xAxisOptions.length === 0}
332
+ hint={xAxisOptions.length === 0 ? t('chartConfigModal.noXAxisFields', 'Aucun champ utilisable pour l\'axe X') : ''}
333
+ />
334
+ )}
335
+
336
+ {/* Axe Y (inchangé) */}
337
+ <SelectField
338
+ label={t('chartConfigModal.yAxis', 'Axe Y (Numérique)')}
339
+ value={yAxisField}
340
+ onChange={(item) => setYAxisField(item.value)}
341
+ items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...yAxisOptions]}
342
+ required={isYAxisRequiredForValidation}
343
+ disabled={yAxisOptions.length === 0}
344
+ // *** MODIFICATION: Hint mis our refléter la condition de l'axe Y ***
345
+ hint={yAxisOptions.length === 0
346
+ ? t('chartConfigModal.noNumericFields', 'Aucun champ numérique disponible')
347
+ : (chartAggregationType === 'count'
348
+ ? t('chartConfigModal.yAxisOptionalForCount', 'Optionnel si Agrégation = Count')
349
+ : t('chartConfigModal.yAxisRequiredForAggregation', "Requis pour l'agrégation '{aggregation}'", {aggregation: t('aggregation.' + chartAggregationType, chartAggregationType)})
350
+ )
351
+ }
352
+ />
353
+
354
+ {/* Type d'Agrégation (inchangé) */}
355
+ <SelectField
356
+ label={t('chartConfigModal.aggregationType', 'Agrégation')}
357
+ value={chartAggregationType}
358
+ onChange={(item) => setChartAggregationType(item.value)}
359
+ items={availableAggregationOptions} // Utilise les options filtrées
360
+ required={true} // Toujours requis conceptuellement
361
+ disabled={false} // Le filtrage des options suffit
362
+ // *** MODIFICATION: Hint mis à jour pour expliquer 'value' ***
363
+ hint={
364
+ chartAggregationType === 'value'
365
+ ? t('chartConfigModal.aggregationHintValue', 'Affiche la valeur brute du champ de l\'axe Y. Note: le backend peut retourner des données non agrégées ou la première/dernière valeur par groupe.')
366
+ : (!isGroupingChart
367
+ ? t('chartConfigModal.aggregationHintBarLine', 'D comment agréger les valeurs sur l\'axe Y (ou compter les éléments si l\'axe Y n\'est pas défini).')
368
+ : t('chartConfigModal.aggregationHintPie', 'Définit comment agréger les valeurs pour chaque segment (si l\'axe Y est défini).')
369
+ )
370
+ }
371
+ />
372
+
373
+ </>
374
+ )}
375
+
376
+ <div className="flex">
377
+ <label
378
+ className="flex-1"
379
+ htmlFor="chartBackgroundColor">{t('charts.backgroundColor', 'Couleur de fond du graphique')}</label>
380
+ <ColorField // Ou votre composant de sélection de couleur
381
+ name="chartBackgroundColor"
382
+ className={"flex-1"}
383
+ value={colorField || '#FFFFFF'}
384
+ onChange={(event) => {
385
+ setColorField(event.value);
386
+ }}
387
+ />
388
+ </div>
389
+
390
+ {/* Boutons Annuler / Enregistrer (inchangés) */}
391
+ <div className="flex flex-end flex-gap actions"
392
+ style={{marginTop: 'auto', paddingTop: '15px', borderTop: '1px solid #eee'}}>
393
+ <Button onClick={onClose} type="button" className="btn">{t('btns.cancel', 'Annuler')}</Button>
394
+ <Button onClick={handleSave} type="button" className="btn btn-primary"
395
+ disabled={isSaveDisabled}>{t('btns.save', 'Enregistrer')}</Button>
396
+ </div>
397
+ </form>
398
+ </Dialog>
399
+ );
400
+ }
401
+
402
+ export default ChartConfigModal;