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,197 @@
1
+ @import "_variables.scss";
2
+
3
+ .btn {
4
+ display: inline-flex;
5
+ gap: 6px;
6
+ align-items: center;
7
+ border-radius: 24px;
8
+ padding: 4px 8px;
9
+ @media only screen and (min-width: 480px){
10
+ padding: 0.6em 1.2em;
11
+ }
12
+ font-size: 1em;
13
+ font-weight: 500;
14
+ font-family: inherit;
15
+ background-color: white;
16
+ cursor: pointer;
17
+ transition: border-color 0.25s;
18
+ color: #979797;
19
+ border: 1px solid #B2BBC2;
20
+ span {
21
+ display: inline-block;
22
+ width: 100%;
23
+ margin: 0 auto;
24
+ }
25
+ &.active {
26
+ background-color: #3498db;
27
+ }
28
+ }
29
+
30
+
31
+ .btn {
32
+ &.btn-ellipsis {
33
+ overflow: hidden;
34
+ white-space: nowrap;
35
+ text-overflow: ellipsis;
36
+ display: block;
37
+ }
38
+ &.btn-nav {
39
+ @media only screen and (max-width: 479px){
40
+ padding: 10px 22px;
41
+ margin: 7px;
42
+ }
43
+ }
44
+ }
45
+
46
+
47
+ // --- Styles Généraux pour les Boutons ---
48
+ .btn, button { // Cible les .btn et les <button> au cas où
49
+ box-shadow: 0 3px 6px $shadow-color; // Ombre subtile pour la profondeur
50
+ transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, background-color 0.2s ease-out, filter 0.2s ease-out; // Transitions douces
51
+ will-change: transform, box-shadow; // Optimisation pour les animations
52
+
53
+ &:hover {
54
+ transform: translateY(-3px) scale(1.03); // Effet de soulèvement et léger zoom
55
+ box-shadow: 0 6px 12px $shadow-color-hover; // Ombre plus prononcée
56
+ //background-color: $primary-color-darker; // Assombrir légèrement
57
+ // filter: brightness(110%); // Alternative: éclaircir légèrement
58
+ }
59
+
60
+ &:active {
61
+ transform: translateY(-1px) scale(1.01); // Effet d'appui léger
62
+ box-shadow: 0 2px 4px $shadow-color; // Ombre réduite
63
+ filter: brightness(95%); // Assombrir un peu plus
64
+ }
65
+
66
+ &:disabled {
67
+ opacity: 0.6;
68
+ cursor: not-allowed;
69
+ transform: none;
70
+ box-shadow: none;
71
+ }
72
+
73
+ // Style spécifique pour les boutons primaires (Ex: Démo, Connexion)
74
+ &.btn-primary {
75
+ background-color: #3498db;
76
+ color: #f8f8f8;
77
+ &:hover {
78
+ color: white;
79
+ }
80
+ }
81
+
82
+ &.btn-big {
83
+ padding: 4px 24px;
84
+ font-size: 130%;
85
+ }
86
+
87
+ // Style pour les boutons "danger" (Ex: Supprimer)
88
+ &.btn-danger {
89
+ background-color: #e74c3c; // Rouge
90
+ &:hover {
91
+ background-color: darken(#e74c3c, 10%);
92
+ }
93
+ }
94
+
95
+ // Style pour les boutons secondaires/discrets (si nécessaire)
96
+ &.btn-secondary {
97
+ background-color: $light-gray;
98
+ color: $dark-gray;
99
+ border: 1px solid $medium-gray;
100
+ box-shadow: none;
101
+ &:hover {
102
+ background-color: $medium-gray;
103
+ border-color: darken($medium-gray, 10%);
104
+ transform: translateY(-2px); // Soullus discret
105
+ box-shadow: 0 4px 8px $shadow-color;
106
+ }
107
+ &:active {
108
+ background-color: darken($medium-gray, 5%);
109
+ transform: translateY(0);
110
+ box-shadow: none;
111
+ }
112
+ }
113
+ }
114
+
115
+
116
+ .dark .btn {
117
+ color: #f1f1f1;
118
+ background-color: black;
119
+ }
120
+
121
+ .btn:hover {
122
+ border-color: $primary-color;
123
+ }
124
+
125
+ .btn:focus,
126
+ .btn:focus-visible {
127
+ outline: 4px auto -webkit-focus-ring-color;
128
+ }
129
+
130
+ .btn-disabled:hover {
131
+ border-color: transparent;
132
+ }
133
+
134
+ .btn-disabled:focus,
135
+ .btn-disabled:focus-visible {
136
+ outline: none;
137
+ }
138
+
139
+ .btn-disabled {
140
+ color: #575757;
141
+ background-color: #f9f9f9;
142
+ cursor: not-allowed;
143
+ }
144
+ .dark .btn-disabled {
145
+ background-color:black;
146
+ }
147
+ .btn-group {
148
+ display: flex;
149
+ width: fit-content;
150
+ gap: 8px;
151
+ padding: 4px;
152
+ }
153
+
154
+ .btn {
155
+ &:has(.alignment-left) {
156
+ flex-direction: row;
157
+ }
158
+
159
+ &:has(.alignment-right) {
160
+ flex-direction: row-reverse;
161
+ }
162
+
163
+ &:has(.alignment-top) {
164
+ flex-direction: column;
165
+ }
166
+
167
+ &:has(.alignment-bottom) {
168
+ flex-direction: column-reverse;
169
+ }
170
+
171
+ svg.alignment-left {
172
+ margin-right: 8px;
173
+ }
174
+
175
+ svg.alignment-right {
176
+ margin-left: 8px;
177
+ }
178
+
179
+ svg.alignment-top {
180
+ }
181
+
182
+ svg.alignment-bottom {
183
+ margin-top: 8px;
184
+ }
185
+ }
186
+
187
+
188
+ .btn-form {
189
+ padding: 4px 8px;
190
+ height: auto;
191
+
192
+ &.btn-last {
193
+ border-radius: 0;
194
+ border-top-right-radius: 8px;
195
+ border-bottom-right-radius: 8px;
196
+ }
197
+ }
@@ -0,0 +1,338 @@
1
+ import React, {useState, useEffect, useCallback, useRef} from 'react';
2
+ import { FaPlus, FaTrash, FaEquals } from 'react-icons/fa';
3
+ import {object_equals} from "data-primals-engine/core"; // Assuming this is a deep equality check
4
+ import CalculationStepRow from "./CalculationStepRow.jsx";
5
+ import {getFieldDefinitionFromPath, isAtomicDateOperator} from "./core/data.js";
6
+ import {MONGO_OPERATORS, OPERAND_TYPES} from "../src/constants.js";
7
+
8
+ // --- Helper Functions & Constants ---
9
+ const generateId = () => `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
10
+
11
+
12
+ // --- Main Calculation Builder Component ---
13
+ const CalculationBuilder = ({
14
+ currentModelName, // Name of the model these calculations are for
15
+ availableFields: rootModelFields = [], // Fields of the currentModelName
16
+ models = [], // All model definitions
17
+ initialSteps = [],
18
+ onCalculationChange,
19
+ }) => {
20
+
21
+ const generateFlattenedFieldsRecursive = useCallback((modelName, allModels, maxDepth = 3, currentDepth = 0, pathPrefix = '', labelPrefix = '', visitedModelsInPath = new Set()) => {
22
+ if (currentDepth > maxDepth) return [];
23
+
24
+ const model = allModels.find(m => m.name === modelName);
25
+ if (!model || !model.fields) return [];
26
+
27
+ // Prevent infinite recursion for circular dependencies within the current path
28
+ if (visitedModelsInPath.has(modelName)) {
29
+ return [];
30
+ }
31
+ const newVisitedInPath = new Set(visitedModelsInPath).add(modelName);
32
+
33
+ let options = [];
34
+ const fieldsToProcess = currentDepth === 0 ? rootModelFields : model.fields;
35
+
36
+
37
+ fieldsToProcess.forEach(field => {
38
+ if (!field.name) return;
39
+ const value = `${pathPrefix}${field.name}`;
40
+ const displayLabel = `${labelPrefix}${field.label || field.name}`;
41
+ const modelLabel = currentDepth === 0 ? (model.label || model.name) : labelPrefix.substring(0, labelPrefix.length - 3); // Get the parent model label
42
+
43
+ options.push({
44
+ value,
45
+ label: displayLabel,
46
+ type: field.type, // Original type of this specific field
47
+ modelName: model.name,
48
+ modelLabel: modelLabel,
49
+ });
50
+
51
+ if (field.type === 'relation' && field.relation) {
52
+ const newPathPrefix = `${value}.`;
53
+ const newLabelPrefix = `${displayLabel} > `;
54
+ options = options.concat(
55
+ generateFlattenedFieldsRecursive(field.relation, allModels, maxDepth, currentDepth + 1, newPathPrefix, newLabelPrefix, newVisitedInPath)
56
+ );
57
+ }
58
+ });
59
+ return options;
60
+ }, [rootModelFields]);
61
+
62
+ const [allFlattenedFields, setAllFlattenedFields] = useState([]);
63
+
64
+ useEffect(() => {
65
+ if (currentModelName && models.length > 0) {
66
+ setAllFlattenedFields(generateFlattenedFieldsRecursive(currentModelName, models));
67
+ }
68
+ }, [currentModelName, models, generateFlattenedFieldsRecursive]);
69
+
70
+
71
+ const createDefaultStepArray = useCallback(() => {
72
+ const numericFields = allFlattenedFields.filter(f => {
73
+ const fieldDef = getFieldDefinitionFromPath(f.value, currentModelName, models);
74
+ return fieldDef && fieldDef.type === 'number';
75
+ });
76
+ const defaultOperand1Value = numericFields.length > 0 ? numericFields[0].value : '';
77
+
78
+ return [{
79
+ id: generateId(),
80
+ outputAlias: 'result',
81
+ operand1: { type: OPERAND_TYPES.FIELD, value: defaultOperand1Value },
82
+ operator: MONGO_OPERATORS.ADD.mongo,
83
+ operand2: { type: OPERAND_TYPES.CONSTANT, value: 0 },
84
+ }];
85
+ }, [allFlattenedFields, currentModelName, models]);
86
+
87
+ const [steps, setSteps] = useState(
88
+ initialSteps && initialSteps.length > 0 ? initialSteps : createDefaultStepArray()
89
+ );
90
+
91
+ useEffect(() => {
92
+ //Re-initialize steps if initialSteps change or if allFlattenedFields are populated for the first time
93
+ if (initialSteps && initialSteps.length > 0) {
94
+ setSteps([...initialSteps]);
95
+ } else if (allFlattenedFields.length > 0) { // Ensure fields are ready before creating default
96
+ setSteps(createDefaultStepArray());
97
+ }
98
+ }, [initialSteps, createDefaultStepArray, allFlattenedFields.length]);
99
+
100
+
101
+ const getPreviousStepAliases = useCallback((currentIndex) => {
102
+ return steps
103
+ .slice(0, currentIndex)
104
+ .map((s) => s.outputAlias)
105
+ .filter(Boolean);
106
+ }, [steps]);
107
+ const handleAddStep = () => {
108
+ const newStep = {
109
+ id: generateId(),
110
+ outputAlias: `calc_step_${steps.length + 1}`,
111
+ operand1: { type: OPERAND_TYPES.FIELD, value: '' }, // Changer ici
112
+ operator: MONGO_OPERATORS.ADD.mongo,
113
+ operand2: { type: OPERAND_TYPES.CONSTANT, value: 0 },
114
+ };
115
+ setSteps([...steps, newStep]);
116
+ };
117
+
118
+ const handleRemoveStep = (id) => {
119
+ if (steps.length === 1 && steps[0].id === id) {
120
+ setSteps(createDefaultStepArray());
121
+ } else {
122
+ setSteps(steps.filter((step) => step.id !== id));
123
+ }
124
+ };
125
+
126
+ const handleUpdateStep = (id, updatedStepData) => {
127
+ setSteps(
128
+ steps.map((step) => (step.id === id ? { ...step, ...updatedStepData } : step))
129
+ );
130
+ };
131
+
132
+ const lastEmittedCalculation = useRef(null);
133
+
134
+ useEffect(() => {
135
+ if (!onCalculationChange || !currentModelName || models.length === 0) return;
136
+
137
+ const abstractPipeline = { lookups: [], addFields: {} };
138
+ const definedLookupAsFields = new Set(); // Tracks 'as' names of lookups
139
+ let isValid = true;
140
+ let finalOutputFieldName = null;
141
+
142
+ if (steps.length === 0 || (steps.length === 1 && !steps[0].outputAlias && !steps[0].operand1?.value && steps[0].operand2?.value === 0)) {
143
+ const emptyCalc = { final: null, pipeline: null, steps: JSON.parse(JSON.stringify(steps)) };
144
+ if (!object_equals(emptyCalc, lastEmittedCalculation.current)) {
145
+ lastEmittedCalculation.current = emptyCalc;
146
+ onCalculationChange(emptyCalc);
147
+ }
148
+ return;
149
+ }
150
+
151
+
152
+ for (const [idx, step] of steps.entries()) {
153
+ if (!step.outputAlias || !step.operator) {
154
+ isValid = false; break;
155
+ }
156
+ if (step.outputAlias.startsWith("__calc_lookup_")) { // Prevent conflicts
157
+ console.warn(`Step alias "${step.outputAlias}" is reserved. Please choose another name.`);
158
+ isValid = false; break;
159
+ }
160
+
161
+
162
+ const parseOperandToMongoPath = (operand, currentStepOperator, idx) => {
163
+ if (operand.type === OPERAND_TYPES.CONSTANT) {
164
+ if (currentStepOperator === MONGO_OPERATORS.CONCAT.mongo) {
165
+ if (operand.value === undefined || operand.value === null) { isValid = false; return null; }
166
+ return String(operand.value);
167
+ } else if (isAtomicDateOperator(currentStepOperator) && typeof operand.value === 'string') {
168
+ // Conversion de la chaîne constante en date pour les opérateurs de date
169
+ return { $toDate: operand.value };
170
+ } else {
171
+ if (operand.value === '' || operand.value === null || operand.value === undefined) { isValid = false; return null; }
172
+ const num = parseFloat(operand.value);
173
+ if (isNaN(num)) { isValid = false; return null; }
174
+ return num;
175
+ }
176
+ }
177
+
178
+ if (operand.type === OPERAND_TYPES.PREVIOUS_STEP) {
179
+ if (!operand.value || !getPreviousStepAliases(idx).includes(operand.value)) {
180
+ isValid = false; return null;
181
+ }
182
+ const prevStepPath = `$${operand.value}`;
183
+ if (currentStepOperator === MONGO_OPERATORS.CONCAT.mongo) {
184
+ return { $toString: prevStepPath }; // Conversion en string
185
+ } else if (isAtomicDateOperator(currentStepOperator)) {
186
+ // Conversion en date si on attend une date
187
+ return { $toDate: prevStepPath }; // À adapter en fonction du type réel de prevStepPath
188
+ }
189
+ return prevStepPath;
190
+ }
191
+
192
+ if (operand.type === OPERAND_TYPES.FIELD) {
193
+ if (!operand.value) { isValid = false; return null; }
194
+
195
+ const pathParts = operand.value.split('.');
196
+ let currentModelForPath = models.find(m => m.name === currentModelName);
197
+ let mongoPath = '$'; // Starts from the root document fields
198
+ let lastFieldDef = null;
199
+
200
+ for (let i = 0; i < pathParts.length; i++) {
201
+ const partName = pathParts[i];
202
+ if (!currentModelForPath || !currentModelForPath.fields) { isValid = false; return null; }
203
+ const fieldDef = currentModelForPath.fields.find(f => f.name === partName);
204
+ if (!fieldDef) { isValid = false; return null; }
205
+ lastFieldDef = fieldDef;
206
+
207
+ if (i < pathParts.length - 1) {
208
+ if (fieldDef.type !== 'relation' || !fieldDef.relation) { isValid = false; return null; }
209
+
210
+ const lookupAs = `__calc_lookup_${pathParts.slice(0, i + 1).join('_')}`;
211
+ mongoPath = `$${lookupAs}`;
212
+
213
+ if (!definedLookupAsFields.has(lookupAs)) {
214
+ abstractPipeline.lookups.push({
215
+ fromModel: currentModelForPath.name,
216
+ localField: (i === 0) ? fieldDef.name : `${pathParts.slice(0, i).map(p => `${p}`).join('.')}.${fieldDef.name}`,
217
+ foreignModel: fieldDef.relation,
218
+ as: lookupAs,
219
+ isMultiple: fieldDef.multiple || false
220
+ });
221
+ definedLookupAsFields.add(lookupAs);
222
+ }
223
+ currentModelForPath = models.find(m => m.name === fieldDef.relation);
224
+ } else {
225
+ mongoPath += (mongoPath === '$' ? '' : '.') + partName;
226
+ }
227
+ }
228
+
229
+ if( 'date' === lastFieldDef.type) {
230
+ return {$toDate: mongoPath };
231
+ }else if('datetime' === lastFieldDef.type){
232
+ return {$toDate: mongoPath };
233
+ }
234
+ if (currentStepOperator !== MONGO_OPERATORS.CONCAT.mongo && lastFieldDef && lastFieldDef.type === 'string') {
235
+ return { $toDate: mongoPath };
236
+ }
237
+ return mongoPath;
238
+ }
239
+ isValid = false; return null;
240
+ };
241
+
242
+ const op1Value = parseOperandToMongoPath(step.operand1, step.operator);
243
+ const op2Value = parseOperandToMongoPath(step.operand2, step.operator);
244
+
245
+ if (!isValid) break;
246
+
247
+ if (isAtomicDateOperator(step.operator)) {
248
+ // Pour les opérateurs atomiques de date, passer op1Value directement
249
+ abstractPipeline.addFields[step.outputAlias] = { [step.operator]: op1Value };
250
+ } else {
251
+ // Pour les autres opérateurs, conserver la logique actuelle
252
+ abstractPipeline.addFields[step.outputAlias] = { [step.operator]: [op1Value, op2Value] };
253
+ }
254
+ }
255
+
256
+ if (isValid && steps.length > 0) {
257
+ finalOutputFieldName = steps[steps.length - 1].outputAlias;
258
+ if (!abstractPipeline.addFields[finalOutputFieldName] && steps.some(s => s.outputAlias)) {
259
+ // If the last step's output isn't in addFields, but there were valid steps, it's an issue
260
+ // unless all steps were invalid.
261
+ if (Object.keys(abstractPipeline.addFields).length > 0) isValid = false;
262
+ }
263
+ } else if (steps.length === 0) {
264
+ isValid = true; // No steps is valid, calc will be null
265
+ }
266
+
267
+
268
+ let currentCalculationData;
269
+ if (isValid && finalOutputFieldName && (abstractPipeline.lookups.length > 0 || Object.keys(abstractPipeline.addFields).length > 0)) {
270
+ currentCalculationData = {
271
+ final: finalOutputFieldName,
272
+ pipeline: abstractPipeline, // Send the abstract pipeline
273
+ steps: JSON.parse(JSON.stringify(steps))
274
+ };
275
+ } else {
276
+ currentCalculationData = {
277
+ final: null,
278
+ pipeline: null,
279
+ steps: JSON.parse(JSON.stringify(steps))
280
+ };
281
+ }
282
+
283
+ if (!object_equals(currentCalculationData, lastEmittedCalculation.current)) {
284
+ lastEmittedCalculation.current = currentCalculationData;
285
+ onCalculationChange(currentCalculationData);
286
+ }
287
+
288
+ }, [steps, onCalculationChange, currentModelName, models, getPreviousStepAliases]);
289
+
290
+
291
+ return (
292
+ <div style={{ fontFamily: 'Arial, sans-serif', padding: '15px', border: '1px solid #ddd', borderRadius: '8px', backgroundColor: '#f9f9f9' }}>
293
+ <h3 style={{ marginTop: 0, marginBottom: '15px', color: '#333' }}>Calculation Builder</h3>
294
+ {steps.map((step, index) => (
295
+ <CalculationStepRow
296
+ key={step.id}
297
+ step={step}
298
+ onUpdateStep={handleUpdateStep}
299
+ onRemoveStep={handleRemoveStep}
300
+ allFlattenedFields={allFlattenedFields}
301
+ previousStepAliases={getPreviousStepAliases(index)}
302
+ isLastStep={index === steps.length - 1}
303
+ currentModelName={currentModelName}
304
+ models={models}
305
+ />
306
+ ))}
307
+ <button
308
+ type={"button"}
309
+ onClick={handleAddStep}
310
+ style={{
311
+ padding: '10px 15px',
312
+ backgroundColor: '#28a745',
313
+ color: 'white',
314
+ border: 'none',
315
+ borderRadius: '4px',
316
+ cursor: 'pointer',
317
+ display: 'flex',
318
+ alignItems: 'center',
319
+ gap: '5px'
320
+ }}
321
+ >
322
+ <FaPlus /> Add Calculation Step
323
+ </button>
324
+ {steps.length > 0 && steps[steps.length - 1].outputAlias && (
325
+ <p style={{marginTop: '15px', fontSize: '0.9em', color: '#555'}}>
326
+ Final result will be available in the field: <strong>{steps[steps.length - 1].outputAlias}</strong>
327
+ </p>
328
+ )}
329
+ {steps.length > 0 && !steps[steps.length - 1].outputAlias && steps.some(s => s.operand1?.value || s.operand2?.value) && (
330
+ <p style={{marginTop: '15px', fontSize: '0.9em', color: 'red'}}>
331
+ <strong>Warning:</strong> Define an output alias for the last calculation step.
332
+ </p>
333
+ )}
334
+ </div>
335
+ );
336
+ };
337
+
338
+ export default CalculationBuilder;