data-primals-engine 1.0.10 → 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 +5390 -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,554 @@
1
+ // C:/Dev/hackersonline-engine/client/src/DashboardView.jsx
2
+ import React, {useEffect, useState, useMemo, useRef, useCallback} from 'react';
3
+ import { Trans, useTranslation } from 'react-i18next';
4
+ import KPIWidget from "./KPIWidget.jsx";
5
+ import { FaPencilAlt, FaPlus, FaSpinner, FaTrash } from "react-icons/fa";
6
+ import KPIDialog from "./KPIDialog.jsx";
7
+ import ChartConfigModal from "./ChartConfigModal.jsx";
8
+ import DashboardChart from "./DashboardChart.jsx";
9
+ import AddWidgetTypeModal from './AddWidgetTypeModal.jsx';
10
+ import FlexBuilderModal from './FlexBuilderModal.jsx';
11
+
12
+ import "./Dashboard.scss"
13
+ import { useQuery, useQueryClient, useMutation } from "react-query";
14
+ import { useAuthContext } from "./contexts/AuthContext.jsx";
15
+ import { DialogProvider } from "./Dialog.jsx";
16
+ import {useModelContext} from "./contexts/ModelContext.jsx";
17
+ import FlexDataRenderer from "./FlexDataRenderer.jsx";
18
+ import {conditionToApiSearchFilter} from "data-primals-engine/data";
19
+ // --- MODIFICATION : Import de la fonction cssProps ---
20
+ import { cssProps } from 'data-primals-engine/core';
21
+ import {DashboardFlexViewItem} from "./DashboardFlexViewItem.jsx";
22
+
23
+ // --- updateDashboardLayout (fonction utilitaire, peut rester ici ou être externalisée) ---
24
+ async function updateDashboardLayout(dashboard, newLayoutData, username, t) {
25
+ if (!dashboard || !username) {
26
+ console.error("Dashboard and username are required to update layout.");
27
+ throw new Error(t('dashboards.error.missingId', "ID du tableau de bord ou nom d'utilisateur manquant."));
28
+ }
29
+
30
+ try {
31
+ const response = await fetch(`/api/data/${dashboard._id}?_user=${username}`, {
32
+ method: 'PUT',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({
35
+ model: 'dashboard',
36
+ data: {
37
+ ...dashboard,
38
+ _id: undefined,
39
+ layout: newLayoutData.map(section => ({
40
+ ...section,
41
+ kpis: section.kpis, // Assure la cohérence
42
+ kpiIds: undefined // Supprime l'ancien champ si présent
43
+ }))
44
+ }
45
+ })
46
+ });
47
+
48
+ if (!response.ok) {
49
+ let errorMsg = t('dashboards.error.updateLayoutGeneric', "Erreur lors de la mise à jour de la disposition.");
50
+ try {
51
+ const errorData = await response.json();
52
+ errorMsg = errorData.error || errorMsg;
53
+ } catch (e) { /* Ignore */ }
54
+ throw new Error(errorMsg);
55
+ }
56
+ return await response.json();
57
+ } catch (error) {
58
+ console.error("Failed to update dashboard layout:", error);
59
+ throw error;
60
+ }
61
+ }
62
+
63
+
64
+
65
+ // --- DashboardView ---
66
+ export function DashboardView({ dashboard }) {
67
+ const { t, i18n } = useTranslation();
68
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
69
+ const { me } = useAuthContext();
70
+ const queryClient = useQueryClient();
71
+ const { models } = useModelContext();
72
+
73
+ const [layoutState, setLayoutState] = useState([]);
74
+ const [editingSectionIndex, setEditingSectionIndex] = useState(null);
75
+ const [originalSectionName, setOriginalSectionName] = useState('');
76
+
77
+ const [isAddWidgetTypeModalOpen, setIsAddWidgetTypeModalOpen] = useState(false);
78
+ const [isAddKpiDialogOpen, setIsAddKpiDialogOpen] = useState(false);
79
+ const [isChartModalOpen, setIsChartModalOpen] = useState(false);
80
+ const [isFlexBuilderModalOpen, setIsFlexBuilderModalOpen] = useState(false);
81
+ const [addingToSectionIndex, setAddingToSectionIndex] = useState(null);
82
+ const [editingChartConfig, setEditingChartConfig] = useState(null);
83
+ const [editingFlexViewConfig, setEditingFlexViewConfig] = useState(null);
84
+
85
+ const mutation = useMutation(
86
+ (newLayout) => updateDashboardLayout(dashboard, newLayout, me.username, t),
87
+ {
88
+ onMutate: async (newLayout) => {
89
+ console.log("Optimi stic update:", newLayout);
90
+ const previousLayout = layoutState;
91
+ setLayoutState(newLayout);
92
+ return { previousLayout };
93
+ },
94
+ onSettled: () => {
95
+ queryClient.invalidateQueries(['userDashboards', me?.username]);
96
+ },
97
+ onError: (err, newLayout, context) => {
98
+ console.error("Mutation failed:", err);
99
+ if (context?.previousLayout) {
100
+ console.log("Rolling back to:", context.previousLayout);
101
+ setLayoutState(context.previousLayout);
102
+ }
103
+ },
104
+ onSuccess: (result) => {
105
+ console.log("Mutation succeeded, server response:", result);
106
+ }
107
+ }
108
+ );
109
+ const processedDashboardId = useRef(null);
110
+
111
+ useEffect(() => {
112
+ if (!dashboard) return;
113
+ if (dashboard._id === processedDashboardId.current) return;
114
+
115
+ let parsedLayout = [];
116
+
117
+ if (dashboard?.layout) {
118
+ try {
119
+ parsedLayout = dashboard.layout.map(section => ({
120
+ ...section,
121
+ kpis: section.kpis || section.kpiIds || [], // Normalisation cohérente
122
+ chartConfigs: section.chartConfigs || [],
123
+ flexViews: section.flexViews || []
124
+ }));
125
+ } catch (e) {
126
+ console.error("Failed to parse layout", e);
127
+ parsedLayout = [{
128
+ name: t('dashboards.defaultSectionName'),
129
+ kpis: [],
130
+ chartConfigs: [],
131
+ flexViews: []
132
+ }];
133
+ }
134
+ } else {
135
+ parsedLayout = [{
136
+ name: t('dashboards.defaultSectionName'),
137
+ kpis: [],
138
+ chartConfigs: [],
139
+ flexViews: []
140
+ }];
141
+ }
142
+
143
+ setLayoutState(parsedLayout);
144
+ processedDashboardId.current = dashboard._id;
145
+ }, [dashboard, t]);
146
+
147
+ const { data: availableKpis, isLoading: isLoadingKpiDefs, error: errorKpiDefs } = useQuery(
148
+ ['kpiDefinitions', me?.username, lang],
149
+ async () => {
150
+ if (!me?.username) return [];
151
+ const response = await fetch(
152
+ `/api/data/search?model=kpi&lang=${lang}&_user=${me.username}`, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' }
155
+ });
156
+ if (!response.ok) {
157
+ const res = await response.json();
158
+ throw new Error(res.error || t('dashboards.errorDefs', 'Erreur chargement définitions KPI'));
159
+ }
160
+ const data = await response.json();
161
+ return data.data;
162
+ },
163
+ { enabled: !!me?.username, refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000, refetchInterval: dashboard?.refetchInterval || 60 * 1000 }
164
+ );
165
+
166
+ const allKpiIdsInLayout = useMemo(() => layoutState.flatMap(section => section.kpis), [layoutState]);
167
+
168
+ const handleOpenAddWidgetTypeModal = (sectionIndex) => {
169
+ setAddingToSectionIndex(sectionIndex);
170
+ setIsAddWidgetTypeModalOpen(true);
171
+ };
172
+
173
+ const handleSelectWidgetType = (type) => {
174
+ setIsAddWidgetTypeModalOpen(false);
175
+ setEditingChartConfig(null);
176
+ setEditingFlexViewConfig(null); // Réinitialiser aussi la config FlexView en édition
177
+
178
+ if (type === 'KPI') {
179
+ setIsAddKpiDialogOpen(true);
180
+ } else if (type === 'Chart') {
181
+ setIsChartModalOpen(true);
182
+ } else if (type === 'FlexView') { // Gérer le type FlexView
183
+ setIsFlexBuilderModalOpen(true);
184
+ }
185
+ };
186
+
187
+
188
+ const handleAddKpi = (kpiDefinition) => {
189
+ if (addingToSectionIndex === null || !layoutState[addingToSectionIndex]) return;
190
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
191
+ if (!newLayoutState[addingToSectionIndex].kpis.includes(t(kpiDefinition.name.value)) && !newLayoutState[addingToSectionIndex].kpis.includes(kpiDefinition.name.value)) {
192
+ newLayoutState[addingToSectionIndex].kpis.push(kpiDefinition.name.value);
193
+ mutation.mutate(newLayoutState);
194
+ // gtag('event', 'add_kpi_to_section');
195
+ }
196
+ setIsAddKpiDialogOpen(false);
197
+ setAddingToSectionIndex(null);
198
+ };
199
+
200
+ const handleRemoveKpi = (kpiDefinition, sectionIndex) => {
201
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
202
+ if (newLayoutState[sectionIndex]) {
203
+ newLayoutState[sectionIndex].kpis = newLayoutState[sectionIndex].kpis.filter(id => id !== kpiDefinition.name.value);
204
+ mutation.mutate(newLayoutState);
205
+ // gtag('event', 'remove_kpi_from_section');
206
+ }
207
+ };
208
+
209
+ const handleOpenEditChartModal = (chartToEdit, sectionIndex) => {
210
+ setEditingChartConfig(chartToEdit);
211
+ setAddingToSectionIndex(sectionIndex);
212
+ setIsChartModalOpen(true);
213
+ };
214
+
215
+ const handleCloseChartModal = () => {
216
+ setIsChartModalOpen(false);
217
+ setAddingToSectionIndex(null);
218
+ setEditingChartConfig(null);
219
+ };
220
+
221
+ const handleSaveChartConfig = (config) => {
222
+ if (addingToSectionIndex === null) return;
223
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
224
+ const targetSection = newLayoutState[addingToSectionIndex];
225
+ if (!targetSection) return;
226
+ if (!Array.isArray(targetSection.chartConfigs)) targetSection.chartConfigs = [];
227
+
228
+ if (config.id) { // Edition
229
+ const chartIndex = targetSection.chartConfigs.findIndex(chart => chart.id === config.id);
230
+ if (chartIndex !== -1) {
231
+ targetSection.chartConfigs[chartIndex] = config;
232
+ mutation.mutate(newLayoutState);
233
+ // gtag('event', 'edit_chart_in_section');
234
+ }
235
+ } else { // Ajout
236
+ targetSection.chartConfigs.push({
237
+ ...config,
238
+ id: `chart-${Date.now()}-${Math.random().toString(16).slice(2)}`
239
+ });
240
+ mutation.mutate(newLayoutState);
241
+ // gtag('event', 'add_chart_to_section');
242
+ }
243
+ handleCloseChartModal();
244
+ };
245
+
246
+ const handleRemoveChart = (chartId, sectionIndex) => {
247
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
248
+ if (newLayoutState[sectionIndex]?.chartConfigs) {
249
+ newLayoutState[sectionIndex].chartConfigs = newLayoutState[sectionIndex].chartConfigs.filter(chart => chart.id !== chartId);
250
+ mutation.mutate(newLayoutState);
251
+ // gtag('event', 'remove_chart_from_section');
252
+ }
253
+ };
254
+
255
+ // --- Fonctions pour FlexView ---
256
+ const handleOpenEditFlexViewModal = (flexViewToEdit, sectionIndex) => {
257
+ setEditingFlexViewConfig(flexViewToEdit);
258
+ setAddingToSectionIndex(sectionIndex);
259
+ setIsFlexBuilderModalOpen(true);
260
+ };
261
+
262
+ const handleCloseFlexBuilderModal = () => {
263
+ setIsFlexBuilderModalOpen(false);
264
+ setAddingToSectionIndex(null);
265
+ setEditingFlexViewConfig(null);
266
+ };
267
+
268
+ const handleSaveFlexViewConfig = (config) => { // config vient de FlexBuilderModal
269
+ if (addingToSectionIndex === null) return;
270
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
271
+ const targetSection = newLayoutState[addingToSectionIndex];
272
+ if (!targetSection) return;
273
+ if (!Array.isArray(targetSection.flexViews)) targetSection.flexViews = [];
274
+
275
+ // MODIFICATION: S'assurer que dataLimit est valide (1-8) et a une valeur par défaut
276
+ const newFlexViewData = {
277
+ ...config,
278
+ dataLimit: Math.max(1, Math.min(config.dataLimit || 1, 8)), // Valeur par défaut 1, max 8
279
+ };
280
+
281
+ if (config.id) { // Edition
282
+ const flexViewIndex = targetSection.flexViews.findIndex(fv => fv.id === config.id);
283
+ if (flexViewIndex !== -1) {
284
+ targetSection.flexViews[flexViewIndex] = { ...newFlexViewData, id: config.id }; // Conserver l'ID existant
285
+ mutation.mutate(newLayoutState);
286
+ // gtag('event', 'edit_flexview_in_section');
287
+ }
288
+ } else { // Ajout
289
+ targetSection.flexViews.push({
290
+ ...newFlexViewData,
291
+ id: `flex-${Date.now()}-${Math.random().toString(16).slice(2)}`
292
+ });
293
+ mutation.mutate(newLayoutState);
294
+ // gtag('event', 'add_flexview_to_section');
295
+ }
296
+ handleCloseFlexBuilderModal();
297
+ };
298
+
299
+ const handleRemoveFlexView = (flexViewId, sectionIndex) => {
300
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
301
+ if (newLayoutState[sectionIndex]?.flexViews) {
302
+ newLayoutState[sectionIndex].flexViews = newLayoutState[sectionIndex].flexViews.filter(fv => fv.id !== flexViewId);
303
+ mutation.mutate(newLayoutState);
304
+ // gtag('event', 'remove_flexview_from_section');
305
+ }
306
+ };
307
+
308
+
309
+ const handleAddSection = () => {
310
+ const newLayoutState = [...layoutState, {
311
+ name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
312
+ kpis: [], // Utiliser kpis au lieu de kpiIds
313
+ chartConfigs: [],
314
+ flexViews: []
315
+ }];
316
+ mutation.mutate(newLayoutState);
317
+ };
318
+
319
+ const handleRemoveSection = (sectionIndex) => {
320
+ if (layoutState.length <= 1) return; // Empêcher la suppression de la dernière section
321
+ const newLayoutState = layoutState.filter((_, index) => index !== sectionIndex);
322
+ mutation.mutate(newLayoutState);
323
+ // gtag('event', 'remove_dashboard_section');
324
+ };
325
+
326
+ const handleUpdateSectionName = (sectionIndex, newName) => {
327
+ const trimmedName = newName.trim();
328
+ if (!trimmedName || trimmedName === layoutState[sectionIndex].name) {
329
+ setEditingSectionIndex(null); // Quitter le mode édition si pas de changement ou nom vide
330
+ return;
331
+ }
332
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
333
+ newLayoutState[sectionIndex].name = trimmedName;
334
+ mutation.mutate(newLayoutState);
335
+ setEditingSectionIndex(null);
336
+ // gtag('event', 'rename_dashboard_section');
337
+ };
338
+
339
+ const handleSectionNameBlur = (e, sectionIndex) => {
340
+ handleUpdateSectionName(sectionIndex, e.target.innerText);
341
+ };
342
+
343
+ const handleSectionNameKeyDown = (e, sectionIndex) => {
344
+ if (e.key === 'Enter') {
345
+ e.preventDefault();
346
+ handleUpdateSectionName(sectionIndex, e.target.innerText);
347
+ } else if (e.key === 'Escape') {
348
+ e.preventDefault();
349
+ e.target.innerText = originalSectionName; // Restaurer le nom original
350
+ setEditingSectionIndex(null);
351
+ }
352
+ };
353
+
354
+ const startEditingSectionName = (index, currentName) => {
355
+ setOriginalSectionName(currentName);
356
+ setEditingSectionIndex(index);
357
+ // Focus et sélection du contenu après un court délai pour permettre au DOM de se mettre à jour
358
+ setTimeout(() => {
359
+ const element = document.querySelector(`.dashboard-section:nth-child(${index + 1}) .section-title`);
360
+ if (element) {
361
+ element.focus();
362
+ const range = document.createRange();
363
+ const sel = window.getSelection();
364
+ range.selectNodeContents(element);
365
+ range.collapse(false); // Place le curseur à la fin
366
+ sel.removeAllRanges();
367
+ sel.addRange(range);
368
+ }
369
+ }, 0);
370
+ };
371
+
372
+ const sectionsWithItems = useMemo(() => {
373
+ return layoutState.map(section => {
374
+ return {
375
+ name: section.name, // On garde le nom
376
+
377
+ kpis: (section.kpis || [])
378
+ .map(id => availableKpis?.find(kpi => kpi.name.value === id))
379
+ .filter(Boolean), // On retire les KPIs qui n'auraient pas été trouvés
380
+
381
+ chartConfigs: section.chartConfigs || [],
382
+
383
+ flexViews: section.flexViews || [],
384
+ };
385
+ });
386
+ }, [layoutState, availableKpis]);
387
+
388
+ if (layoutState === null && !dashboard) {
389
+ return <p><Trans i18nKey="dashboards.noDashboardSelected">Aucun tableau de bord sélectionné.</Trans></p>;
390
+ }
391
+ if (layoutState === null && dashboard) { // Devrait être gaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingLayout">Chargement de la disposition...</Trans></p>;
392
+ }
393
+ if (isLoadingKpiDefs) return <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingDefs">Chargement des définitions de KPI...</Trans></p>;
394
+ if (errorKpiDefs) return <p className="error">{errorKpiDefs.message}</p>;
395
+
396
+
397
+ return (
398
+ <div className="dashboard-view">
399
+ <DialogProvider> {/* Assurez-vous que DialogProvider englobe bien tous les modaux */}
400
+ {isAddWidgetTypeModalOpen && (<AddWidgetTypeModal
401
+ onClose={() => setIsAddWidgetTypeModalOpen(false)}
402
+ onSelectType={handleSelectWidgetType}
403
+ />)}
404
+
405
+ {isAddKpiDialogOpen && (
406
+ <KPIDialog
407
+ availableKpis={(availableKpis||[]).filter(kpi => !allKpiIdsInLayout.includes(kpi.name.value) && !allKpiIdsInLayout.includes(kpi.name.value))}
408
+ onAddKpi={handleAddKpi}
409
+ onClose={() => {
410
+ setIsAddKpiDialogOpen(false);
411
+ setAddingToSectionIndex(null); // Réinitialiser l'index de section
412
+ }}
413
+ />
414
+ )}
415
+ {isChartModalOpen && (
416
+ <ChartConfigModal
417
+ isOpen={isChartModalOpen}
418
+ onClose={handleCloseChartModal}
419
+ onSave={handleSaveChartConfig}
420
+ initialConfig={editingChartConfig}
421
+ models={models} // Passer les modèles pour la configuration du graphique
422
+ />
423
+ )}
424
+ </DialogProvider>
425
+
426
+ {isFlexBuilderModalOpen && (
427
+ <FlexBuilderModal
428
+ isOpen={isFlexBuilderModalOpen}
429
+ onClose={handleCloseFlexBuilderModal}
430
+ onSave={handleSaveFlexViewConfig} // Utiliser le handler pour FlexView
431
+ models={models} // Passer les modèles
432
+ initialConfig={editingFlexViewConfig} // Passer la config en édition
433
+ // data prop for FlexBuilder is for its internal preview,
434
+ // not for the data displayed on the dashboard itself.
435
+ />
436
+ )}
437
+ {dashboard && (
438
+ <>
439
+ <h2>{dashboard.name.value}</h2>
440
+ {dashboard.description && <p className="dashboard-description">{dashboard.description}</p>}
441
+
442
+ <div className="dashboard-sections">
443
+
444
+ {sectionsWithItems.map((sectionData, sectionIndex) => (
445
+ <div key={`section-${sectionIndex}-${dashboard._id}`} className="dashboard-section">
446
+ <div className="section-header">
447
+ <h4
448
+ className={`section-title ${editingSectionIndex === sectionIndex ? 'editing' : ''}`}
449
+ contentEditable={editingSectionIndex === sectionIndex}
450
+ suppressContentEditableWarning={true}
451
+ onClick={() => {
452
+ if (editingSectionIndex !== sectionIndex) startEditingSectionName(sectionIndex, sectionData.name);
453
+ }}
454
+ onBlur={(e) => handleSectionNameBlur(e, sectionIndex)}
455
+ onKeyDown={(e) => handleSectionNameKeyDown(e, sectionIndex)}
456
+ >
457
+ {sectionData.name}
458
+ </h4>
459
+ {/* Bouton d'édition de nom de section (optionnel, car le titre est cliquable) */}
460
+ </div>
461
+
462
+ <div
463
+ className="items-grid flex"> {/* Assurez-vous que cette classe est bien stylée pour flex/grid */}
464
+ {sectionData.kpis.map(kpiDef => (
465
+ <KPIWidget
466
+ key={kpiDef._id}
467
+ kpiDefinition={kpiDef}
468
+ onRemove={() => handleRemoveKpi(kpiDef, sectionIndex)}
469
+ disabled={mutation.isLoading}
470
+ />
471
+ ))}
472
+ {sectionData.chartConfigs.map(chartConfig => (
473
+ <div key={chartConfig.id} className="dashboard-item-wrapper chart-wrapper">
474
+ <DashboardChart config={chartConfig}/>
475
+ <div className="item-actions">
476
+ <button className="edit-item-button"
477
+ onClick={() => handleOpenEditChartModal(chartConfig, sectionIndex)}
478
+ title={t('dashboards.editChartTitle', 'Modifier ce graphique')}
479
+ disabled={mutation.isLoading}><FaPencilAlt/></button>
480
+ <button className="remove-item-button"
481
+ onClick={() => handleRemoveChart(chartConfig.id, sectionIndex)}
482
+ title={t('dashboards.removeChartTitle', 'Supprimer ce graphique')}
483
+ disabled={mutation.isLoading}><FaTrash/></button>
484
+ </div>
485
+ </div>
486
+ ))}
487
+ {/* Affichage des FlexViews */}
488
+ {sectionData.flexViews?.map(flexViewConfig => (
489
+
490
+ <div key={flexViewConfig.id}
491
+ className="dashboard-item-wrapper flex-view-wrapper">
492
+ <DashboardFlexViewItem
493
+ flexViewConfig={flexViewConfig}
494
+ allModels={models} // Passer tous les modisponibles
495
+ />
496
+ <div className="item-actions">
497
+ <button className="edit-item-button"
498
+ onClick={() => handleOpenEditFlexViewModal(flexViewConfig, sectionIndex)}
499
+ title={t('dashboards.editFlexViewTitle', 'Modifier cette vue Flex')}
500
+ disabled={mutation.isLoading}><FaPencilAlt/></button>
501
+ <button className="remove-item-button"
502
+ onClick={() => handleRemoveFlexView(flexViewConfig.id, sectionIndex)}
503
+ title={t('dashboards.removeFlexViewTitle', 'Supprimer cette vue Flex')}
504
+ disabled={mutation.isLoading}><FaTrash/></button>
505
+ </div>
506
+ </div>
507
+ ))}
508
+
509
+ {/* Message si la section est vide */}
510
+ {sectionData.kpis.length === 0 && sectionData.chartConfigs.length === 0 && (sectionData.flexViews === undefined || sectionData.flexViews.length === 0) && (
511
+ <p className="empty-section-message"><Trans
512
+ i18nKey="dashboards.emptySectionClickPlus">Section vide. Cliquez sur le
513
+ bouton '+' ci-dessous pour ajouter des éléments.</Trans></p>
514
+ )}
515
+ </div>
516
+ <div className="add-buttons-inline">
517
+ <button
518
+ className="add-kpi-button add-kpi-button-inline" /* Renommer la classe si elle est générique */
519
+ onClick={() => handleOpenAddWidgetTypeModal(sectionIndex)}
520
+ title={t('dashboards.addWidgetToSectionTitle', 'Ajouter un élément à cette section')}
521
+ disabled={mutation.isLoading}>
522
+ <FaPlus/>
523
+ </button>
524
+ {layoutState.length > 1 && (
525
+ <button
526
+ className="remove-section-button"
527
+ onClick={() => handleRemoveSection(sectionIndex)}
528
+ title={t('dashboards.removeSectionTitle', 'Supprimer cette section')}
529
+ disabled={mutation.isLoading}
530
+ >
531
+ <FaTrash/>
532
+ </button>
533
+ )}
534
+ </div>
535
+ </div>
536
+ ))}
537
+
538
+ {sectionsWithItems.length > 0 && (<div className="flex actions left">
539
+ <button onClick={handleAddSection} className="add-section-button"
540
+ disabled={mutation.isLoading}>
541
+ <FaPlus/> <Trans i18nKey="dashboards.addSection">Ajouter une section</Trans>
542
+ </button>
543
+ </div>)}
544
+
545
+ {layoutState.length === 0 && !isLoadingKpiDefs && (
546
+ <p><Trans i18nKey="dashboards.noSections">Ce tableau de bord n'a pas encore de
547
+ sections.</Trans></p>
548
+ )}
549
+ </div>
550
+ </>
551
+ )}
552
+ </div>
553
+ );
554
+ }