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,362 @@
1
+ // C:/Dev/hackersonline-engine/client/src/DashboardChart.jsx
2
+ import React, {useEffect, useMemo, useState} from 'react';
3
+ import { fr } from 'date-fns/locale'
4
+ import {
5
+ Chart as ChartJS,
6
+ CategoryScale, LinearScale, BarElement, LineElement,
7
+ PointElement, ArcElement, Title, Tooltip, Legend, TimeScale, Filler,
8
+ } from 'chart.js';
9
+ import { Bar, Line, Pie, Doughnut } from 'react-chartjs-2';
10
+ import { useQuery } from 'react-query';
11
+ import { useTranslation } from 'react-i18next';
12
+ import { FaSpinner, FaExclamationTriangle } from 'react-icons/fa';
13
+ import i18n from 'i18next';
14
+
15
+ import 'chartjs-adapter-date-fns';
16
+ import {useModelContext} from "./contexts/ModelContext.jsx";
17
+ import {isDate} from "data-primals-engine/core";
18
+ import useWindowSize from "./hooks/useWindowSize.js";
19
+ import useDebounce from "./hooks/useDebounce.js";
20
+
21
+ ChartJS.register(
22
+ Filler,
23
+ TimeScale, CategoryScale, LinearScale, BarElement, LineElement,
24
+ PointElement, ArcElement, Title, Tooltip, Legend
25
+ );
26
+
27
+ const fetchChartData = async (chartConfig) => {
28
+ console.log(`[fetchChartData] Fetching aggregated data with config:`, chartConfig); // chartConfig inclura maintenant potentiellement chartConfig.filter
29
+ try {
30
+ const response = await fetch(`/api/charts/aggregate`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify(chartConfig) // Le filtre sera envoyil est dans chartConfig
34
+ });
35
+ const responseClone = response.clone();
36
+ if (!response.ok) {
37
+ let errorText = `API Error ${response.status}`;
38
+ try {
39
+ const errorData = await response.json();
40
+ errorText = errorData.error || JSON.stringify(errorData);
41
+ } catch (e) {
42
+ try { errorText = await responseClone.text(); } catch (e2) { /* ignore */ }
43
+ }
44
+ console.error(`[fetchChartData] API Error: ${errorText}`);
45
+ throw new Error(errorText);
46
+ }
47
+ const data = await response.json();
48
+ console.log(`[fetchChartData] Aggregated data received:`, data);
49
+ return data;
50
+ } catch (error) {
51
+ console.error(`[fetchChartData] Error during fetch:`, error);
52
+ throw error;
53
+ }
54
+ };
55
+
56
+ // formatDateLabel, parsePotentiallyFormattedDate, processDataForChart restent inchangées
57
+ const formatDateLabel = (label, locale = 'fr-FR') => {
58
+ if (typeof label === 'string' && (label.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) || label.match(/^\d{4}-\d{2}-\d{2}$/))) {
59
+ const date = new Date(label);
60
+ if (!isNaN(date.getTime())) {
61
+ if (label.includes('T') && (date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0 || date.getUTCSeconds() !== 0)) {
62
+ return date.toLocaleString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
63
+ } else {
64
+ return date.toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit' });
65
+ }
66
+ }
67
+ }
68
+ if (typeof label === 'boolean') {
69
+ return label ? i18n.t('boolean.true', 'Vrai') : i18n.t('boolean.false', 'Faux');
70
+ }
71
+ if (label === null || label === undefined) {
72
+ return i18n.t('charts.labelNull', '(Non défini)');
73
+ }
74
+ return String(label);
75
+ };
76
+
77
+ const parsePotentiallyFormattedDate = (str) => {
78
+ if (typeof str !== 'string') return null;
79
+ let match = str.match(/^(\d{2})\/(\d{2})\/(\d{4})(?: (\d{2}):(\d{2}))?$/);
80
+ if (match) {
81
+ const [, day, month, year, hour, minute] = match;
82
+ const isoStr = `${year}-${month}-${day}${hour ? `T${hour}:${minute || '00'}:00` : 'T00:00:00'}`;
83
+ const dt = new Date(isoStr);
84
+ return !isNaN(dt) && dt.getFullYear() === parseInt(year) && dt.getMonth() === parseInt(month) - 1 && dt.getDate() === parseInt(day) ? dt : null;
85
+ }
86
+ const dt = new Date(str);
87
+ return !isNaN(dt) ? dt : null;
88
+ };
89
+
90
+ const processDataForChart = (inputData, config, t, timed, lang) => {
91
+ const { type, title, model, groupBy, xAxis, yAxis, aggregationType = 'count' } = config;
92
+ const currentLocale = i18n.language || 'fr-FR';
93
+
94
+ if (!inputData || !Array.isArray(inputData) || inputData.length === 0 || !('label' in inputData[0]) || !('value' in inputData[0])) {
95
+ return { labels: [], datasets: [] };
96
+ }
97
+ let processedItems = [...inputData];
98
+ processedItems.sort((itemA, itemB) => {
99
+ const labelA = itemA.label;
100
+ const labelB = itemB.label;
101
+ const dateA = parsePotentiallyFormattedDate(labelA);
102
+ const dateB = parsePotentiallyFormattedDate(labelB);
103
+ if (dateA && dateB) {
104
+ return dateA.getTime() - dateB.getTime();
105
+ }
106
+ const numA = parseFloat(labelA);
107
+ const numB = parseFloat(labelB);
108
+ if (!isNaN(numA) && !isNaN(numB)) {
109
+ return numA - numB;
110
+ }
111
+ return String(labelA).localeCompare(String(labelB), currentLocale, { sensitivity: 'base' });
112
+ });
113
+
114
+ const labels = processedItems.filter(item => {
115
+ if( !timed )
116
+ return true;
117
+ const d = new Date(item.label);
118
+ return isDate(d);
119
+ }).map(item => {
120
+ if( !timed)
121
+ return item.label;
122
+ return new Date(item.label);
123
+ });
124
+ const dataValues = processedItems.map(item => parseFloat(item.value) || 0);
125
+
126
+ let datasetLabel = title || 'Data';
127
+ const isGroupingChart = ['pie', 'doughnut'].includes(type);
128
+ const aggregationLabel = t('aggregation.' + aggregationType, aggregationType);
129
+ if (isGroupingChart) {
130
+ datasetLabel = title || t(`field_${model}_${groupBy}`, groupBy);
131
+ if (yAxis && aggregationType !== 'count') {
132
+ datasetLabel += ` (${aggregationLabel} ${t(`field_${model}_${yAxis}`, yAxis)})`;
133
+ } else if (aggregationType === 'count') {
134
+ datasetLabel += ` (${aggregationLabel})`;
135
+ }
136
+ } else {
137
+ if (yAxis && aggregationType !== 'count') {
138
+ datasetLabel = `${title || t(`field_${model}_${yAxis}`, yAxis)} (${aggregationLabel} ${t(`field_${model}_${yAxis}`, yAxis)})`;
139
+ } else {
140
+ datasetLabel = `${title || t(`field_${model}_${xAxis}`, xAxis)} (${aggregationLabel})`;
141
+ }
142
+ }
143
+
144
+ const chartJsData = {
145
+ labels: labels,
146
+ datasets: [
147
+ {
148
+ label: datasetLabel,
149
+ data: dataValues,
150
+ barThickness: config.type === 'bar' ? (timed ? 5 : 3500/(20+(datasetLabel.length))) : undefined,
151
+ barPercentage: timed && config.type === 'bar' ? 0.9 : undefined,
152
+ categoryPercentage: timed && config.type === 'bar' ? 0.8 : undefined,
153
+ backgroundColor: (type === 'pie' || type === 'doughnut')
154
+ ? ['rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 206, 86, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(153, 102, 255, 0.7)', 'rgba(255, 159, 64, 0.7)', 'rgba(199, 199, 199, 0.7)', 'rgba(83, 102, 89, 0.7)']
155
+ : config.chartBackgroundColor,
156
+ borderColor: (type === 'pie' || type === 'doughnut')
157
+ ? '#fff'
158
+ : '#666',
159
+ borderWidth: 1,
160
+ fill: type === 'line' ? true : undefined,
161
+ tension: type === 'line' ? 0.1 : undefined,
162
+ },
163
+ ],
164
+ };
165
+ return chartJsData;
166
+ };
167
+
168
+
169
+ const DashboardChart = ({ config }) => { // config peut maintenant contenir config.filter
170
+ const { i18n, t } = useTranslation();
171
+ const { models } = useModelContext();
172
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
173
+
174
+
175
+ // --- NOUVEAU: Logique de gestion du redimensionnement ---
176
+ const windowSize = useWindowSize();
177
+ // On "debounce" la taille de la fenêtre. La requête API et le rendu final n'auront lieu qu'après 300ms sans mouvement.
178
+ const debouncedSize = useDebounce(windowSize, 300);
179
+ // On utilise un état pour savoir si l'utilisateur est EN TRAIN de redimensionner.
180
+ const [isResizing, setIsResizing] = useState(false);
181
+ useEffect(() => {
182
+ // 1. Au début de chaque redimensionnement, on active le flag.
183
+ setIsResizing(true);
184
+
185
+ // 2. On utilise un timer pour désactiver le flag 300ms après le DERNIER événement de redimensionnement.
186
+ // C'est une forme de "debouncing" directement appliquée à notre état.
187
+ const resizeTimer = setTimeout(() => {
188
+ setIsResizing(false);
189
+ }, 100); // Un délai raisonnable
190
+
191
+ // 3. On nettoie le timer à chaque nouvel événement pour qu'il ne se déclenche qu'à la fin.
192
+ return () => clearTimeout(resizeTimer);
193
+ }, [windowSize]); // Cet effet se déclenche à chaque micro-mouvement.
194
+
195
+ const modelDefinition = models?.find(f => f.name === config.model);
196
+ const fieldDefinition = modelDefinition?.fields.find(f => f.name === config.xAxis);
197
+ const timed = fieldDefinition && ['datetime', 'date'].includes(fieldDefinition.type);
198
+
199
+ const isGroupingChart = config && ['pie', 'doughnut'].includes(config.type);
200
+ const requiresYAxisForValidation = config && config.aggregationType && config.aggregationType !== 'count';
201
+ const isValidConfig = config && config.model && config.type && config.title && config.aggregationType &&
202
+ (isGroupingChart
203
+ ? (config.groupBy && (requiresYAxisForValidation ? !!config.yAxis : true))
204
+ : (config.xAxis && (requiresYAxisForValidation ? !!config.yAxis : true))
205
+ );
206
+
207
+
208
+
209
+ const queryKey = useMemo(() => {
210
+ const filterKey = JSON.stringify(config.filter || {});
211
+ return [
212
+ 'chartData',
213
+ config.model,
214
+ config.type,
215
+ config.aggregationType,
216
+ isGroupingChart ? config.groupBy : config.xAxis,
217
+ requiresYAxisForValidation ? config.yAxis : null,
218
+ isGroupingChart && config.groupByLabelField ? config.groupByLabelField : null,
219
+ filterKey
220
+ ];
221
+ }, [config, isGroupingChart, requiresYAxisForValidation, debouncedSize.width]);
222
+
223
+
224
+ const queryFn = () => fetchChartData(config); // config est passé en entier, incluant config.filter
225
+ const isQueryEnabled = isValidConfig;
226
+
227
+ const { data: queryData, isLoading, isError, error } = useQuery(
228
+ queryKey,
229
+ queryFn,
230
+ {
231
+ enabled: isQueryEnabled,
232
+ staleTime: 5 * 60 * 1000, // 5 minutes
233
+ refetchOnWindowFocus: false
234
+ }
235
+ );
236
+
237
+ const chartData = useMemo(() => {
238
+ if (!queryData) {
239
+ return { labels: [], datasets: [] };
240
+ }
241
+ return processDataForChart(queryData, config, t, timed, lang);
242
+ }, [queryData, config, t, timed, lang]);
243
+
244
+ const renderChart = useMemo(() => {
245
+
246
+ const baseOptions = {
247
+ responsive: true,
248
+ maintainAspectRatio: false,
249
+ locale: 'fr-FR',
250
+ adapters: {
251
+ date: {
252
+ locale: fr
253
+ }
254
+ },
255
+ plugins: {
256
+ legend: {
257
+ position: (config.type === 'pie' || config.type === 'doughnut') ? 'right' : 'top',
258
+ },
259
+ title: {
260
+ display: !!config.title,
261
+ text: config.title || '',
262
+ }
263
+ },
264
+ };
265
+
266
+ const axisOptions = {
267
+ ...baseOptions,
268
+ scales: {
269
+ x: {
270
+ type: timed ? 'time' : 'category',
271
+ time: timed ? {
272
+ tooltipFormat: 'dd/MM/yyyy HH:mm',
273
+ unit: 'minute',
274
+ } : {},
275
+ ticks: timed ? {
276
+ autoSkip: true,
277
+ maxTicksLimit: 20,
278
+ callback: function (value) {
279
+ const d = new Date();
280
+ d.setTime(value);
281
+ return d.toLocaleDateString(lang, {
282
+ minute: '2-digit',
283
+ hour: '2-digit'
284
+ });
285
+ }
286
+ } : {}
287
+ },
288
+ y: {
289
+ beginAtZero: true
290
+ }
291
+ },
292
+ };
293
+ const noAxisOptions = { ...baseOptions, scales: undefined };
294
+
295
+ const data = !isResizing ? chartData : { labels: [], datasets: [] };
296
+ try {
297
+ switch (config.type) {
298
+ case 'line':
299
+ return <Line options={axisOptions} data={data} />;
300
+ case 'pie':
301
+ return <Pie options={noAxisOptions} data={data} />;
302
+ case 'doughnut':
303
+ return <Doughnut options={noAxisOptions} data={data} />;
304
+ case 'bar':
305
+ default:
306
+ return <Bar options={axisOptions} data={data} />;
307
+ }
308
+ } catch (renderError) {
309
+ return (
310
+ <div className="chart-error" style={{ color: 'red', padding: '10px' }}>
311
+ {t('charts.renderError', 'Erreur interne lors du rendu du graphique.')}
312
+ </div>
313
+ );
314
+ }
315
+ }, [config, t, chartData, isResizing]);
316
+
317
+ if (!isValidConfig) {
318
+ return (
319
+ <div className="chart-error" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'red', padding: '10px', textAlign: 'center' }}>
320
+ <FaExclamationTriangle />&nbsp; {t('charts.invalidConfig', 'Configuration invalide ou incomplète')}
321
+ </div>
322
+ );
323
+ }
324
+
325
+ if (isLoading) {
326
+ return (
327
+ <div className="chart-loading" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
328
+ <FaSpinner className="spin" />&nbsp;{t('loading', 'Chargement...')}
329
+ </div>
330
+ );
331
+ }
332
+ if (isError) {
333
+ return (
334
+ <div className="chart-error" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'red', padding: '10px', textAlign: 'center' }}>
335
+ <FaExclamationTriangle size="2em" />
336
+ <p>{t('errorLoadingData', 'Erreur de chargement des données.')}</p>
337
+ {error && <p style={{ fontSize: '0.8em', marginTop: '5px' }}>{error.message}</p>}
338
+ </div>
339
+ );
340
+ }
341
+ if (!chartData || !chartData.datasets || chartData.datasets.length === 0 || (chartData.datasets[0].data && chartData.datasets[0].data.length === 0)) {
342
+ return (
343
+ <div className="chart-no-data" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.7 }}>
344
+ {t('charts.noData', 'Aucune donnée à afficher.')}
345
+ </div>
346
+ );
347
+ }
348
+
349
+ const containerStyle = {
350
+ position: 'relative',
351
+ height: '300px',
352
+ width: '100%'
353
+ };
354
+
355
+ return (
356
+ <div className="dashboard-chart-container" style={containerStyle}>
357
+ {renderChart}
358
+ </div>
359
+ );
360
+ };
361
+
362
+ export default DashboardChart;
@@ -0,0 +1,129 @@
1
+
2
+ // --- Component to fetch data and render a single FlexView ---
3
+ import {useTranslation} from "react-i18next";
4
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
5
+ import React, {useEffect, useMemo, useRef} from "react";
6
+ import {useQuery} from "react-query";
7
+ import {conditionToApiSearchFilter} from "data-primals-engine/data";
8
+ import {FaSpinner} from "react-icons/fa";
9
+ import {DisplayFlexNodeRenderer} from "./DisplayFlexNodeRenderer.jsx";
10
+
11
+ const DashboardFlexViewItem = ({ flexViewConfig, allModels }) => {
12
+ const { t, i18n } = useTranslation();
13
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
14
+ const { me } = useAuthContext();
15
+
16
+ // MODIFICATION: Extraire dataLimit et s'assurer qu'il est entre 1 et 8
17
+ const { selectedModelName, dataFilter, flexStructure } = flexViewConfig;
18
+ const effectiveDataLimit = Math.max(1, Math.min(flexViewConfig.dataLimit || 1, 8));
19
+
20
+ const queryKey = useMemo(() => ['flexViewData', selectedModelName, dataFilter, effectiveDataLimit, me?.username, lang],
21
+ [selectedModelName, dataFilter, effectiveDataLimit, me?.username, lang]);
22
+
23
+ const { data: queryResult, isLoading, error, isFetching } = useQuery(
24
+ queryKey,
25
+ async () => {
26
+ if (!selectedModelName || !me?.username) return { data: [] };
27
+
28
+ const payload = {
29
+ model: selectedModelName,
30
+ filter: dataFilter || {},
31
+ limit: effectiveDataLimit, // Utiliser la limite effective
32
+ lang: lang,
33
+ };
34
+
35
+ // MODIFICATION: Construire l'URL dynamiquement avec effectiveDataLimit
36
+ const apiUrl = `/api/data/search?model=${selectedModelName}&depth=1&limit=${effectiveDataLimit}&_user=${me.username}`;
37
+
38
+ const response = await fetch(apiUrl, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify(payload)
42
+ });
43
+
44
+ if (!response.ok) {
45
+ const resError = await response.json();
46
+ throw new Error(resError.error || t('dashboards.error.fetchFlexData', 'Erreur de chargement des données pour la vue Flex'));
47
+ }
48
+ const result = await response.json();
49
+ return result; // Expects { data: [...] }
50
+ },
51
+ {
52
+ enabled: !!selectedModelName && !!me?.username,
53
+ keepPreviousData: true,
54
+ refetchOnWindowFocus: false, // Désactive le rafraîchissement au focus de la fenêtre
55
+ refetchOnMount: true, // Ou false si 'enabled' gère déjà le premier chargement et que vous ne voulez pas de refetch au remontage
56
+ staleTime: 5 * 60 * 1000, // Considérer les données fraîches pendant 5 minutes (exemple)
57
+ refetchInterval: flexViewConfig.refreshInterval || 60 * 1000, // Utiliser l'intervalle configur
58
+ }
59
+ );
60
+
61
+ const dataToRender = useMemo(() => queryResult?.data || [], [queryResult]);
62
+ const dataIndexRef = useRef(0);
63
+
64
+ const currentModel = useMemo(() => {
65
+ if (!selectedModelName || !allModels) return null;
66
+ return allModels.find(m => m.name === selectedModelName);
67
+ }, [selectedModelName, allModels]);
68
+
69
+ const currentModelFields = useMemo(() => currentModel?.fields || [], [currentModel]);
70
+
71
+ useEffect(() => {
72
+ dataIndexRef.current = 0;
73
+ }, [dataToRender]);
74
+
75
+ if (isLoading && !queryResult) {
76
+ return <div className="flex-view-placeholder"><FaSpinner className="spin" /> {t('loading', 'Chargement...')}</div>;
77
+ }
78
+ if (isFetching && !queryResult) {
79
+ return <div className="flex-view-placeholder"><FaSpinner className="spin" /> {t('loading', 'Chargement...')}</div>;
80
+ }
81
+
82
+ if (error) {
83
+ return <div className="flex-view-placeholder error" title={error.message}>{t('dashboards.error.displayFlexData', 'Erreur d\'affichage')}</div>;
84
+ }
85
+
86
+ console.log(flexStructure)
87
+ if (!flexStructure || (!selectedModelName && !flexStructure.htmlContent)) {
88
+ return <div className="flex-view-placeholder">{t('dashboards.flexViewNotConfigured', 'Vue Flex non configurée ou modèle manquant.')}</div>;
89
+ }
90
+
91
+ if (dataToRender.length === 0 && !isLoading && !isFetching) {
92
+ // Afficher la structure avec des données vides pour montrer les placeholders
93
+ const dummyDataIndexRef = { current: 0 }; // Référence séparée pour l'affichage des placeholders
94
+ return (
95
+ <div className="flex-view-content-wrapper is-empty">
96
+ <DisplayFlexNodeRenderer
97
+ key={currentModel?.name}
98
+ model={currentModel}
99
+ node={flexStructure}
100
+ allModels={allModels}
101
+ baseModelFields={currentModelFields}
102
+ data={[{}]} // Passer un objet factice pour que les placeholders s'affichent
103
+ dataIndexRef={dummyDataIndexRef}
104
+ />
105
+ <p className="empty-data-message">{t('dashboards.flexViewNoData', 'Aucune donnée à afficher pour la configuration actuelle.')}</p>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <div className="flex-view-content-wrapper">
112
+ {isFetching && <div className="loading-overlay-flexview"><FaSpinner className="spin"/></div>}
113
+ {dataToRender.map((item, index) => {
114
+ const singleItemDataIndexRef = { current: 0 }; // Chaque item commence son propre cycle d'index
115
+ return <DisplayFlexNodeRenderer
116
+ key={item._id || `flex-item-${index}`} // AMÉLIORATION : Clé plus stable
117
+ node={flexStructure}
118
+ model={selectedModelName}
119
+ data={[item]} // Passer un seul item à la fois
120
+ allModels={allModels}
121
+ dataIndexRef={singleItemDataIndexRef}
122
+ baseModelFields={currentModelFields}
123
+ />
124
+ })}
125
+ </div>
126
+ );
127
+ };
128
+
129
+ export { DashboardFlexViewItem };