data-primals-engine 1.5.0 → 1.5.1

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 (52) hide show
  1. package/README.md +35 -0
  2. package/client/src/AddWidgetTypeModal.jsx +47 -43
  3. package/client/src/App.jsx +2 -6
  4. package/client/src/App.scss +12 -0
  5. package/client/src/AssistantChat.jsx +363 -323
  6. package/client/src/AssistantChat.scss +27 -10
  7. package/client/src/Dashboard.jsx +480 -396
  8. package/client/src/Dashboard.scss +1 -1
  9. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  10. package/client/src/DashboardView.jsx +654 -569
  11. package/client/src/DataEditor.jsx +10 -3
  12. package/client/src/DataLayout.jsx +805 -755
  13. package/client/src/DataLayout.scss +14 -0
  14. package/client/src/DataTable.jsx +39 -75
  15. package/client/src/Dialog.scss +1 -1
  16. package/client/src/Field.jsx +2057 -1825
  17. package/client/src/FlexViewCard.jsx +44 -0
  18. package/client/src/HistoryDialog.jsx +47 -14
  19. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  20. package/client/src/HtmlViewBuilderModal.scss +18 -0
  21. package/client/src/HtmlViewCard.jsx +44 -0
  22. package/client/src/HtmlViewCard.scss +35 -0
  23. package/client/src/KanbanCard.jsx +1 -2
  24. package/client/src/ModelCreator.jsx +5 -4
  25. package/client/src/ModelCreatorField.jsx +51 -4
  26. package/client/src/ModelList.jsx +92 -53
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/Notification.scss +0 -18
  29. package/client/src/Pagination.jsx +5 -3
  30. package/client/src/RelationField.jsx +354 -258
  31. package/client/src/RelationSelectorWidget.jsx +173 -0
  32. package/client/src/contexts/ModelContext.jsx +10 -1
  33. package/client/src/contexts/UIContext.jsx +72 -63
  34. package/client/src/filter.js +262 -212
  35. package/client/src/hooks/useValidation.js +75 -0
  36. package/client/src/translations.js +24 -24
  37. package/package.json +2 -1
  38. package/src/constants.js +1 -1
  39. package/src/defaultModels.js +1596 -1544
  40. package/src/i18n.js +710 -10
  41. package/src/modules/assistant/assistant.js +148 -18
  42. package/src/modules/bucket.js +2 -1
  43. package/src/modules/data/data.core.js +118 -92
  44. package/src/modules/data/data.history.js +531 -492
  45. package/src/modules/data/data.js +3 -53
  46. package/src/modules/data/data.operations.js +77 -26
  47. package/src/modules/data/data.relations.js +686 -686
  48. package/src/modules/data/data.routes.js +1879 -1821
  49. package/src/modules/data/data.validation.js +81 -2
  50. package/src/modules/file.js +247 -238
  51. package/src/packs.js +5482 -5478
  52. package/test/data.integration.test.js +1115 -1060
@@ -1,570 +1,655 @@
1
- import React, {useEffect, useState, useMemo, useRef, useCallback} from 'react';
2
- import { Trans, useTranslation } from 'react-i18next';
3
- import KPIWidget from "./KPIWidget.jsx";
4
- import { FaPencilAlt, FaPlus, FaSpinner, FaTrash } from "react-icons/fa";
5
- import KPIDialog from "./KPIDialog.jsx";
6
- import ChartConfigModal from "./ChartConfigModal.jsx";
7
- import DashboardChart from "./DashboardChart.jsx";
8
- import AddWidgetTypeModal from './AddWidgetTypeModal.jsx';
9
- import FlexBuilderModal from './FlexBuilderModal.jsx';
10
-
11
- import "./Dashboard.scss"
12
- import { useQuery, useQueryClient, useMutation } from "react-query";
13
- import { useAuthContext } from "./contexts/AuthContext.jsx";
14
- import { DialogProvider } from "./Dialog.jsx";
15
- import {useModelContext} from "./contexts/ModelContext.jsx";
16
- import {DashboardFlexViewItem} from "./DashboardFlexViewItem.jsx";
17
-
18
- // --- updateDashboardLayout (fonction utilitaire, peut rester ici ou être externalisée) ---
19
- async function updateDashboardLayout(dashboard, newLayoutData, username, t) {
20
- if (!dashboard || !username) {
21
- console.error("Dashboard and username are required to update layout.");
22
- throw new Error(t('dashboards.error.missingId', "ID du tableau de bord ou nom d'utilisateur manquant."));
23
- }
24
-
25
- try {
26
- const response = await fetch(`/api/data/${dashboard._id}?_user=${username}`, {
27
- method: 'PUT',
28
- headers: { 'Content-Type': 'application/json' },
29
- body: JSON.stringify({
30
- model: 'dashboard',
31
- data: {
32
- ...dashboard,
33
- _id: undefined,
34
- layout: newLayoutData.map(section => ({
35
- ...section,
36
- kpis: section.kpis, // Assure la cohérence
37
- kpiIds: undefined // Supprime l'ancien champ si présent
38
- }))
39
- }
40
- })
41
- });
42
-
43
- if (!response.ok) {
44
- let errorMsg = t('dashboards.error.updateLayoutGeneric', "Erreur lors de la mise à jour de la disposition.");
45
- try {
46
- const errorData = await response.json();
47
- errorMsg = errorData.error || errorMsg;
48
- } catch (e) { /* Ignore */ }
49
- throw new Error(errorMsg);
50
- }
51
- return await response.json();
52
- } catch (error) {
53
- console.error("Failed to update dashboard layout:", error);
54
- throw error;
55
- }
56
- }
57
-
58
-
59
-
60
- // --- DashboardView ---
61
- export function DashboardView({ dashboard }) {
62
- const { t, i18n } = useTranslation();
63
- const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
64
- const { me } = useAuthContext();
65
- const queryClient = useQueryClient();
66
- const { models } = useModelContext();
67
-
68
- const [layoutState, setLayoutState] = useState([]);
69
- const [editingSectionIndex, setEditingSectionIndex] = useState(null);
70
- const [originalSectionName, setOriginalSectionName] = useState('');
71
-
72
- const [isAddWidgetTypeModalOpen, setIsAddWidgetTypeModalOpen] = useState(false);
73
- const [isAddKpiDialogOpen, setIsAddKpiDialogOpen] = useState(false);
74
- const [isChartModalOpen, setIsChartModalOpen] = useState(false);
75
- const [isFlexBuilderModalOpen, setIsFlexBuilderModalOpen] = useState(false);
76
- const [addingToSectionIndex, setAddingToSectionIndex] = useState(null);
77
- const [editingChartConfig, setEditingChartConfig] = useState(null);
78
- const [editingFlexViewConfig, setEditingFlexViewConfig] = useState(null);
79
-
80
- const mutation = useMutation(
81
- (newLayout) => updateDashboardLayout(dashboard, newLayout, me.username, t),
82
- {
83
- onMutate: async (newLayout) => {
84
- const previousLayout = layoutState;
85
- setLayoutState(newLayout);
86
- return { previousLayout };
87
- },
88
- onSettled: () => {
89
- queryClient.invalidateQueries(['userDashboards', me?.username]);
90
- },
91
- onError: (err, newLayout, context) => {
92
- console.error("Mutation failed:", err);
93
- if (context?.previousLayout) {
94
- console.log("Rolling back to:", context.previousLayout);
95
- setLayoutState(context.previousLayout);
96
- }
97
- },
98
- onSuccess: (result) => {
99
- console.log("Mutation succeeded, server response:", result);
100
- }
101
- }
102
- );
103
- const processedDashboardId = useRef(null);
104
-
105
- useEffect(() => {
106
- if (!dashboard) return;
107
- if (dashboard._id === processedDashboardId.current) return;
108
-
109
- let parsedLayout = [];
110
-
111
- if (dashboard?.layout) {
112
- try {
113
- parsedLayout = dashboard.layout.map(section => ({
114
- ...section,
115
- kpis: section.kpis || section.kpiIds || [], // Normalisation cohérente
116
- chartConfigs: section.chartConfigs || [],
117
- flexViews: section.flexViews || []
118
- }));
119
- } catch (e) {
120
- console.error("Failed to parse layout", e);
121
- parsedLayout = [{
122
- name: t('dashboards.defaultSectionName'),
123
- kpis: [],
124
- chartConfigs: [],
125
- flexViews: []
126
- }];
127
- }
128
- } else {
129
- parsedLayout = [{
130
- name: t('dashboards.defaultSectionName'),
131
- kpis: [],
132
- chartConfigs: [],
133
- flexViews: []
134
- }];
135
- }
136
-
137
- setLayoutState(parsedLayout);
138
- processedDashboardId.current = dashboard._id;
139
- }, [dashboard, t]);
140
-
141
- // Gère le rafraîchissement automatique des données du dashboard.
142
- useEffect(() => {
143
- // S'assure qu'on a un intervalle de rafraîchissement valide (nombre de secondes > 0)
144
- if (dashboard?.refreshInterval && dashboard.refreshInterval > 0) {
145
- const intervalInMs = dashboard.refreshInterval * 1000;
146
-
147
- const intervalId = setInterval(() => {
148
- console.log(`Refreshing dashboard data for ${dashboard.name.value}...`);
149
- // Invalide les requêtes liées aux données des KPIs, graphiques, et contenus Flex.
150
- // Cela suppose que les composants enfants (KPIWidget, etc.) utilisent des clés de requête
151
- // qui peuvent être invalidées par un préfixe commun, par exemple 'kpiData'.
152
- queryClient.invalidateQueries('kpiData');
153
- }, intervalInMs);
154
-
155
- return () => clearInterval(intervalId); // Nettoie l'intervalle lors du démontage ou changement de dashboard
156
- }
157
- }, [dashboard?._id, dashboard?.refreshInterval, queryClient]);
158
-
159
- const { data: availableKpis, isLoading: isLoadingKpiDefs, error: errorKpiDefs } = useQuery(
160
- ['kpiDefinitions', me?.username, lang],
161
- async () => {
162
- if (!me?.username) return [];
163
- const response = await fetch(
164
- `/api/data/search?model=kpi&lang=${lang}&_user=${me.username}`, {
165
- method: 'POST',
166
- headers: { 'Content-Type': 'application/json' }
167
- });
168
- if (!response.ok) {
169
- const res = await response.json();
170
- throw new Error(res.error || t('dashboards.errorDefs', 'Erreur chargement définitions KPI'));
171
- }
172
- const data = await response.json();
173
- return data.data;
174
- },
175
- {
176
- enabled: !!me?.username,
177
- refetchOnWindowFocus: false,
178
- staleTime: 5 * 60 * 1000 // Les définitions de KPI ne changent pas souvent
179
- }
180
- );
181
-
182
- const allKpiIdsInLayout = useMemo(() => layoutState.flatMap(section => section.kpis), [layoutState]);
183
-
184
- const handleOpenAddWidgetTypeModal = (sectionIndex) => {
185
- setAddingToSectionIndex(sectionIndex);
186
- setIsAddWidgetTypeModalOpen(true);
187
- };
188
-
189
- const handleSelectWidgetType = (type) => {
190
- setIsAddWidgetTypeModalOpen(false);
191
- setEditingChartConfig(null);
192
- setEditingFlexViewConfig(null); // Réinitialiser aussi la config FlexView en édition
193
-
194
- if (type === 'KPI') {
195
- setIsAddKpiDialogOpen(true);
196
- } else if (type === 'Chart') {
197
- setIsChartModalOpen(true);
198
- } else if (type === 'FlexView') { // Gérer le type FlexView
199
- setIsFlexBuilderModalOpen(true);
200
- }
201
- };
202
-
203
-
204
- const handleAddKpi = (kpiDefinition) => {
205
- if (addingToSectionIndex === null || !layoutState[addingToSectionIndex]) return;
206
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
207
- if (!newLayoutState[addingToSectionIndex].kpis.includes(t(kpiDefinition.name.value)) && !newLayoutState[addingToSectionIndex].kpis.includes(kpiDefinition.name.value)) {
208
- newLayoutState[addingToSectionIndex].kpis.push(kpiDefinition.name.value);
209
- mutation.mutate(newLayoutState);
210
- // gtag('event', 'add_kpi_to_section');
211
- }
212
- setIsAddKpiDialogOpen(false);
213
- setAddingToSectionIndex(null);
214
- };
215
-
216
- const handleRemoveKpi = (kpiDefinition, sectionIndex) => {
217
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
218
- if (newLayoutState[sectionIndex]) {
219
- newLayoutState[sectionIndex].kpis = newLayoutState[sectionIndex].kpis.filter(id => id !== kpiDefinition.name.value);
220
- mutation.mutate(newLayoutState);
221
- // gtag('event', 'remove_kpi_from_section');
222
- }
223
- };
224
-
225
- const handleOpenEditChartModal = (chartToEdit, sectionIndex) => {
226
- setEditingChartConfig(chartToEdit);
227
- setAddingToSectionIndex(sectionIndex);
228
- setIsChartModalOpen(true);
229
- };
230
-
231
- const handleCloseChartModal = () => {
232
- setIsChartModalOpen(false);
233
- setAddingToSectionIndex(null);
234
- setEditingChartConfig(null);
235
- };
236
-
237
- const handleSaveChartConfig = (config) => {
238
- if (addingToSectionIndex === null) return;
239
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
240
- const targetSection = newLayoutState[addingToSectionIndex];
241
- if (!targetSection) return;
242
- if (!Array.isArray(targetSection.chartConfigs)) targetSection.chartConfigs = [];
243
-
244
- if (config.id) { // Edition
245
- const chartIndex = targetSection.chartConfigs.findIndex(chart => chart.id === config.id);
246
- if (chartIndex !== -1) {
247
- targetSection.chartConfigs[chartIndex] = config;
248
- mutation.mutate(newLayoutState);
249
- // gtag('event', 'edit_chart_in_section');
250
- }
251
- } else { // Ajout
252
- targetSection.chartConfigs.push({
253
- ...config,
254
- id: `chart-${Date.now()}-${Math.random().toString(16).slice(2)}`
255
- });
256
- mutation.mutate(newLayoutState);
257
- // gtag('event', 'add_chart_to_section');
258
- }
259
- handleCloseChartModal();
260
- };
261
-
262
- const handleRemoveChart = (chartId, sectionIndex) => {
263
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
264
- if (newLayoutState[sectionIndex]?.chartConfigs) {
265
- newLayoutState[sectionIndex].chartConfigs = newLayoutState[sectionIndex].chartConfigs.filter(chart => chart.id !== chartId);
266
- mutation.mutate(newLayoutState);
267
- // gtag('event', 'remove_chart_from_section');
268
- }
269
- };
270
-
271
- // --- Fonctions pour FlexView ---
272
- const handleOpenEditFlexViewModal = (flexViewToEdit, sectionIndex) => {
273
- setEditingFlexViewConfig(flexViewToEdit);
274
- setAddingToSectionIndex(sectionIndex);
275
- setIsFlexBuilderModalOpen(true);
276
- };
277
-
278
- const handleCloseFlexBuilderModal = () => {
279
- setIsFlexBuilderModalOpen(false);
280
- setAddingToSectionIndex(null);
281
- setEditingFlexViewConfig(null);
282
- };
283
-
284
- const handleSaveFlexViewConfig = (config) => { // config vient de FlexBuilderModal
285
- if (addingToSectionIndex === null) return;
286
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
287
- const targetSection = newLayoutState[addingToSectionIndex];
288
- if (!targetSection) return;
289
- if (!Array.isArray(targetSection.flexViews)) targetSection.flexViews = [];
290
-
291
- // MODIFICATION: S'assurer que dataLimit est valide (1-8) et a une valeur par défaut
292
- const newFlexViewData = {
293
- ...config,
294
- dataLimit: Math.max(1, Math.min(config.dataLimit || 1, 8)), // Valeur par défaut 1, max 8
295
- };
296
-
297
- if (config.id) { // Edition
298
- const flexViewIndex = targetSection.flexViews.findIndex(fv => fv.id === config.id);
299
- if (flexViewIndex !== -1) {
300
- targetSection.flexViews[flexViewIndex] = { ...newFlexViewData, id: config.id }; // Conserver l'ID existant
301
- mutation.mutate(newLayoutState);
302
- // gtag('event', 'edit_flexview_in_section');
303
- }
304
- } else { // Ajout
305
- targetSection.flexViews.push({
306
- ...newFlexViewData,
307
- id: `flex-${Date.now()}-${Math.random().toString(16).slice(2)}`
308
- });
309
- mutation.mutate(newLayoutState);
310
- // gtag('event', 'add_flexview_to_section');
311
- }
312
- handleCloseFlexBuilderModal();
313
- };
314
-
315
- const handleRemoveFlexView = (flexViewId, sectionIndex) => {
316
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
317
- if (newLayoutState[sectionIndex]?.flexViews) {
318
- newLayoutState[sectionIndex].flexViews = newLayoutState[sectionIndex].flexViews.filter(fv => fv.id !== flexViewId);
319
- mutation.mutate(newLayoutState);
320
- // gtag('event', 'remove_flexview_from_section');
321
- }
322
- };
323
-
324
-
325
- const handleAddSection = () => {
326
- const newLayoutState = [...layoutState, {
327
- name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
328
- kpis: [], // Utiliser kpis au lieu de kpiIds
329
- chartConfigs: [],
330
- flexViews: []
331
- }];
332
- mutation.mutate(newLayoutState);
333
- };
334
-
335
- const handleRemoveSection = (sectionIndex) => {
336
- if (layoutState.length <= 1) return; // Empêcher la suppression de la dernière section
337
- const newLayoutState = layoutState.filter((_, index) => index !== sectionIndex);
338
- mutation.mutate(newLayoutState);
339
- // gtag('event', 'remove_dashboard_section');
340
- };
341
-
342
- const handleUpdateSectionName = (sectionIndex, newName) => {
343
- const trimmedName = newName.trim();
344
- if (!trimmedName || trimmedName === layoutState[sectionIndex].name) {
345
- setEditingSectionIndex(null); // Quitter le mode édition si pas de changement ou nom vide
346
- return;
347
- }
348
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
349
- newLayoutState[sectionIndex].name = trimmedName;
350
- mutation.mutate(newLayoutState);
351
- setEditingSectionIndex(null);
352
- // gtag('event', 'rename_dashboard_section');
353
- };
354
-
355
- const handleSectionNameBlur = (e, sectionIndex) => {
356
- handleUpdateSectionName(sectionIndex, e.target.innerText);
357
- };
358
-
359
- const handleSectionNameKeyDown = (e, sectionIndex) => {
360
- if (e.key === 'Enter') {
361
- e.preventDefault();
362
- handleUpdateSectionName(sectionIndex, e.target.innerText);
363
- } else if (e.key === 'Escape') {
364
- e.preventDefault();
365
- e.target.innerText = originalSectionName; // Restaurer le nom original
366
- setEditingSectionIndex(null);
367
- }
368
- };
369
-
370
- const startEditingSectionName = (index, currentName) => {
371
- setOriginalSectionName(currentName);
372
- setEditingSectionIndex(index);
373
- // Focus et sélection du contenu après un court délai pour permettre au DOM de se mettre à jour
374
- setTimeout(() => {
375
- const element = document.querySelector(`.dashboard-section:nth-child(${index + 1}) .section-title`);
376
- if (element) {
377
- element.focus();
378
- const range = document.createRange();
379
- const sel = window.getSelection();
380
- range.selectNodeContents(element);
381
- range.collapse(false); // Place le curseur à la fin
382
- sel.removeAllRanges();
383
- sel.addRange(range);
384
- }
385
- }, 0);
386
- };
387
-
388
- const sectionsWithItems = useMemo(() => {
389
- return layoutState.map(section => {
390
- return {
391
- name: section.name, // On garde le nom
392
-
393
- kpis: (section.kpis || [])
394
- .map(id => availableKpis?.find(kpi => kpi.name.value === id))
395
- .filter(Boolean), // On retire les KPIs qui n'auraient pas été trouvés
396
-
397
- chartConfigs: section.chartConfigs || [],
398
-
399
- flexViews: section.flexViews || [],
400
- };
401
- });
402
- }, [layoutState, availableKpis]);
403
-
404
- if (layoutState === null && !dashboard) {
405
- return <p><Trans i18nKey="dashboards.noDashboardSelected">Aucun tableau de bord sélectionné.</Trans></p>;
406
- }
407
- if (layoutState === null && dashboard) { // Devrait être gaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingLayout">Chargement de la disposition...</Trans></p>;
408
- }
409
- if (isLoadingKpiDefs) return <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingDefs">Chargement des définitions de KPI...</Trans></p>;
410
- if (errorKpiDefs) return <p className="error">{errorKpiDefs.message}</p>;
411
-
412
-
413
- return (
414
- <div className="dashboard-view">
415
- <DialogProvider> {/* Assurez-vous que DialogProvider englobe bien tous les modaux */}
416
- {isAddWidgetTypeModalOpen && (<AddWidgetTypeModal
417
- onClose={() => setIsAddWidgetTypeModalOpen(false)}
418
- onSelectType={handleSelectWidgetType}
419
- />)}
420
-
421
- {isAddKpiDialogOpen && (
422
- <KPIDialog
423
- availableKpis={(availableKpis||[]).filter(kpi => !allKpiIdsInLayout.includes(kpi.name.value) && !allKpiIdsInLayout.includes(kpi.name.value))}
424
- onAddKpi={handleAddKpi}
425
- onClose={() => {
426
- setIsAddKpiDialogOpen(false);
427
- setAddingToSectionIndex(null); // Réinitialiser l'index de section
428
- }}
429
- />
430
- )}
431
- {isChartModalOpen && (
432
- <ChartConfigModal
433
- isOpen={isChartModalOpen}
434
- onClose={handleCloseChartModal}
435
- onSave={handleSaveChartConfig}
436
- initialConfig={editingChartConfig}
437
- models={models} // Passer les modèles pour la configuration du graphique
438
- />
439
- )}
440
- </DialogProvider>
441
-
442
- {isFlexBuilderModalOpen && (
443
- <FlexBuilderModal
444
- isOpen={isFlexBuilderModalOpen}
445
- onClose={handleCloseFlexBuilderModal}
446
- onSave={handleSaveFlexViewConfig} // Utiliser le handler pour FlexView
447
- models={models} // Passer les modèles
448
- initialConfig={editingFlexViewConfig} // Passer la config en édition
449
- // data prop for FlexBuilder is for its internal preview,
450
- // not for the data displayed on the dashboard itself.
451
- />
452
- )}
453
- {dashboard && (
454
- <>
455
- <h2>{dashboard.name.value}</h2>
456
- {dashboard.description && <p className="dashboard-description">{dashboard.description}</p>}
457
-
458
- <div className="dashboard-sections">
459
-
460
- {sectionsWithItems.map((sectionData, sectionIndex) => (
461
- <div key={`section-${sectionIndex}-${dashboard._id}`} className="dashboard-section">
462
- <div className="section-header">
463
- <h4
464
- className={`section-title ${editingSectionIndex === sectionIndex ? 'editing' : ''}`}
465
- contentEditable={editingSectionIndex === sectionIndex}
466
- suppressContentEditableWarning={true}
467
- onClick={() => {
468
- if (editingSectionIndex !== sectionIndex) startEditingSectionName(sectionIndex, sectionData.name);
469
- }}
470
- onBlur={(e) => handleSectionNameBlur(e, sectionIndex)}
471
- onKeyDown={(e) => handleSectionNameKeyDown(e, sectionIndex)}
472
- >
473
- {sectionData.name}
474
- </h4>
475
- {/* Bouton d'édition de nom de section (optionnel, car le titre est cliquable) */}
476
- </div>
477
-
478
- <div
479
- className="items-grid flex"> {/* Assurez-vous que cette classe est bien stylée pour flex/grid */}
480
- {sectionData.kpis.map(kpiDef => (
481
- <KPIWidget
482
- key={kpiDef._id}
483
- kpiDefinition={kpiDef}
484
- onRemove={() => handleRemoveKpi(kpiDef, sectionIndex)}
485
- disabled={mutation.isLoading}
486
- />
487
- ))}
488
- {sectionData.chartConfigs.map(chartConfig => (
489
- <div key={chartConfig.id} className="dashboard-item-wrapper chart-wrapper">
490
- <DashboardChart config={chartConfig}/>
491
- <div className="item-actions">
492
- <button className="edit-item-button"
493
- onClick={() => handleOpenEditChartModal(chartConfig, sectionIndex)}
494
- title={t('dashboards.editChartTitle', 'Modifier ce graphique')}
495
- disabled={mutation.isLoading}><FaPencilAlt/></button>
496
- <button className="remove-item-button"
497
- onClick={() => handleRemoveChart(chartConfig.id, sectionIndex)}
498
- title={t('dashboards.removeChartTitle', 'Supprimer ce graphique')}
499
- disabled={mutation.isLoading}><FaTrash/></button>
500
- </div>
501
- </div>
502
- ))}
503
- {/* Affichage des FlexViews */}
504
- {sectionData.flexViews?.map(flexViewConfig => (
505
-
506
- <div key={flexViewConfig.id}
507
- className="dashboard-item-wrapper flex-view-wrapper">
508
- <DashboardFlexViewItem
509
- flexViewConfig={flexViewConfig}
510
- allModels={models} // Passer tous les modisponibles
511
- />
512
- <div className="item-actions">
513
- <button className="edit-item-button"
514
- onClick={() => handleOpenEditFlexViewModal(flexViewConfig, sectionIndex)}
515
- title={t('dashboards.editFlexViewTitle', 'Modifier cette vue Flex')}
516
- disabled={mutation.isLoading}><FaPencilAlt/></button>
517
- <button className="remove-item-button"
518
- onClick={() => handleRemoveFlexView(flexViewConfig.id, sectionIndex)}
519
- title={t('dashboards.removeFlexViewTitle', 'Supprimer cette vue Flex')}
520
- disabled={mutation.isLoading}><FaTrash/></button>
521
- </div>
522
- </div>
523
- ))}
524
-
525
- {/* Message si la section est vide */}
526
- {sectionData.kpis.length === 0 && sectionData.chartConfigs.length === 0 && (sectionData.flexViews === undefined || sectionData.flexViews.length === 0) && (
527
- <p className="empty-section-message"><Trans
528
- i18nKey="dashboards.emptySectionClickPlus">Section vide. Cliquez sur le
529
- bouton '+' ci-dessous pour ajouter des éléments.</Trans></p>
530
- )}
531
- </div>
532
- <div className="add-buttons-inline">
533
- <button
534
- className="add-kpi-button add-kpi-button-inline" /* Renommer la classe si elle est générique */
535
- onClick={() => handleOpenAddWidgetTypeModal(sectionIndex)}
536
- title={t('dashboards.addWidgetToSectionTitle', 'Ajouter un élément à cette section')}
537
- disabled={mutation.isLoading}>
538
- <FaPlus/>
539
- </button>
540
- {layoutState.length > 1 && (
541
- <button
542
- className="remove-section-button"
543
- onClick={() => handleRemoveSection(sectionIndex)}
544
- title={t('dashboards.removeSectionTitle', 'Supprimer cette section')}
545
- disabled={mutation.isLoading}
546
- >
547
- <FaTrash/>
548
- </button>
549
- )}
550
- </div>
551
- </div>
552
- ))}
553
-
554
- {sectionsWithItems.length > 0 && (<div className="flex actions left">
555
- <button onClick={handleAddSection} className="add-section-button"
556
- disabled={mutation.isLoading}>
557
- <FaPlus/> <Trans i18nKey="dashboards.addSection">Ajouter une section</Trans>
558
- </button>
559
- </div>)}
560
-
561
- {layoutState.length === 0 && !isLoadingKpiDefs && (
562
- <p><Trans i18nKey="dashboards.noSections">Ce tableau de bord n'a pas encore de
563
- sections.</Trans></p>
564
- )}
565
- </div>
566
- </>
567
- )}
568
- </div>
569
- );
1
+ import React, {useEffect, useState, useMemo, useRef, useCallback} from 'react';
2
+ import { Trans, useTranslation } from 'react-i18next';
3
+ import KPIWidget from "./KPIWidget.jsx";
4
+ import { FaPencilAlt, FaPlus, FaSpinner, FaTrash } from "react-icons/fa";
5
+ import KPIDialog from "./KPIDialog.jsx";
6
+ import ChartConfigModal from "./ChartConfigModal.jsx";
7
+ import DashboardChart from "./DashboardChart.jsx";
8
+ import AddWidgetTypeModal from './AddWidgetTypeModal.jsx';
9
+ import FlexBuilderModal from './FlexBuilderModal.jsx';
10
+
11
+ import "./Dashboard.scss"
12
+ import { useQuery, useQueryClient, useMutation } from "react-query";
13
+ import { useAuthContext } from "./contexts/AuthContext.jsx";
14
+ import { DialogProvider } from "./Dialog.jsx";
15
+ import {useModelContext} from "./contexts/ModelContext.jsx";
16
+ import {DashboardFlexViewItem} from "./DashboardFlexViewItem.jsx";
17
+ import DashboardHtmlViewItem from "./DashboardHtmlViewItem.jsx";
18
+ import HtmlViewBuilderModal from "./HtmlViewBuilderModal.jsx";
19
+
20
+ // --- updateDashboardLayout (fonction utilitaire, peut rester ici ou être externalisée) ---
21
+ async function updateDashboardLayout(dashboard, newLayoutData, username, t) {
22
+ if (!dashboard || !username) {
23
+ console.error("Dashboard and username are required to update layout.");
24
+ throw new Error(t('dashboards.error.missingId', "ID du tableau de bord ou nom d'utilisateur manquant."));
25
+ }
26
+
27
+ try {
28
+ const response = await fetch(`/api/data/${dashboard._id}?_user=${username}`, {
29
+ method: 'PUT',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({
32
+ model: 'dashboard',
33
+ data: {
34
+ ...dashboard,
35
+ _id: undefined,
36
+ layout: newLayoutData.map(section => ({
37
+ ...section,
38
+ kpis: section.kpis, // Assure la cohérence
39
+ kpiIds: undefined // Supprime l'ancien champ si présent
40
+ }))
41
+ }
42
+ })
43
+ });
44
+
45
+ if (!response.ok) {
46
+ let errorMsg = t('dashboards.error.updateLayoutGeneric', "Erreur lors de la mise à jour de la disposition.");
47
+ try {
48
+ const errorData = await response.json();
49
+ errorMsg = errorData.error || errorMsg;
50
+ } catch (e) { /* Ignore */ }
51
+ throw new Error(errorMsg);
52
+ }
53
+ return await response.json();
54
+ } catch (error) {
55
+ console.error("Failed to update dashboard layout:", error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+
61
+
62
+ // --- DashboardView ---
63
+ export function DashboardView({ dashboard }) {
64
+ const { t, i18n } = useTranslation();
65
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
66
+ const { me } = useAuthContext();
67
+ const queryClient = useQueryClient();
68
+ const { models } = useModelContext();
69
+
70
+ const [editingHtmlViewConfig, setEditingHtmlViewConfig] = useState(null);
71
+ const [isHtmlViewBuilderModalOpen, setIsHtmlViewBuilderModalOpen] = useState(false);
72
+ const [layoutState, setLayoutState] = useState([]);
73
+ const [editingSectionIndex, setEditingSectionIndex] = useState(null);
74
+ const [originalSectionName, setOriginalSectionName] = useState('');
75
+
76
+ const [isAddWidgetTypeModalOpen, setIsAddWidgetTypeModalOpen] = useState(false);
77
+ const [isAddKpiDialogOpen, setIsAddKpiDialogOpen] = useState(false);
78
+ const [isChartModalOpen, setIsChartModalOpen] = useState(false);
79
+ const [isFlexBuilderModalOpen, setIsFlexBuilderModalOpen] = useState(false);
80
+ const [addingToSectionIndex, setAddingToSectionIndex] = useState(null);
81
+ const [editingChartConfig, setEditingChartConfig] = useState(null);
82
+ const [editingFlexViewConfig, setEditingFlexViewConfig] = useState(null);
83
+
84
+ const mutation = useMutation(
85
+ (newLayout) => updateDashboardLayout(dashboard, newLayout, me.username, t),
86
+ {
87
+ onMutate: async (newLayout) => {
88
+ const previousLayout = layoutState;
89
+ setLayoutState(newLayout);
90
+ return { previousLayout };
91
+ },
92
+ onSettled: () => {
93
+ queryClient.invalidateQueries(['userDashboards', me?.username]);
94
+ },
95
+ onError: (err, newLayout, context) => {
96
+ console.error("Mutation failed:", err);
97
+ if (context?.previousLayout) {
98
+ console.log("Rolling back to:", context.previousLayout);
99
+ setLayoutState(context.previousLayout);
100
+ }
101
+ },
102
+ onSuccess: (result) => {
103
+ console.log("Mutation succeeded, server response:", result);
104
+ }
105
+ }
106
+ );
107
+ const processedDashboardId = useRef(null);
108
+ const processedLayoutRef = useRef(null); // Pour comparer le contenu du layout
109
+
110
+ useEffect(() => {
111
+ if (!dashboard) return;
112
+
113
+ const layoutString = JSON.stringify(dashboard.layout);
114
+ // On ne met à jour que si l'ID du dashboard ou le contenu de son layout a changé.
115
+ if (dashboard._id === processedDashboardId.current && layoutString === processedLayoutRef.current) return;
116
+
117
+ let parsedLayout = [];
118
+
119
+ // The layout from the DB can be an object (old default) or an array of sections (new format).
120
+ // We need to handle both cases to avoid crashes.
121
+ if (dashboard?.layout && Array.isArray(dashboard.layout)) {
122
+ try {
123
+ parsedLayout = dashboard.layout.map(section => ({
124
+ ...section,
125
+ kpis: section.kpis || section.kpiIds || [], // Normalisation cohérente
126
+ chartConfigs: section.chartConfigs || [],
127
+ flexViews: section.flexViews || [],
128
+ htmlViews: section.htmlViews || []
129
+ }));
130
+ } catch (e) {
131
+ console.error("Failed to parse layout array", e);
132
+ // Fallback to a default section if mapping fails for some reason
133
+ parsedLayout = [{
134
+ name: t('dashboards.defaultSectionName'),
135
+ kpis: [],
136
+ chartConfigs: [],
137
+ flexViews: [],
138
+ htmlViews: []
139
+ }];
140
+ }
141
+ } else {
142
+ parsedLayout = [{
143
+ name: t('dashboards.defaultSectionName'),
144
+ kpis: [],
145
+ chartConfigs: [],
146
+ flexViews: [],
147
+ htmlViews: []
148
+ }];
149
+ }
150
+
151
+ setLayoutState(parsedLayout);
152
+ processedDashboardId.current = dashboard._id;
153
+ processedLayoutRef.current = layoutString;
154
+ }, [dashboard, t]);
155
+
156
+ // Gère le rafraîchissement automatique des données du dashboard.
157
+ useEffect(() => {
158
+ // S'assure qu'on a un intervalle de rafraîchissement valide (nombre de secondes > 0)
159
+ if (dashboard?.refreshInterval && dashboard.refreshInterval > 0) {
160
+ const intervalInMs = dashboard.refreshInterval * 1000;
161
+
162
+ const intervalId = setInterval(() => {
163
+ console.log(`Refreshing dashboard data for ${dashboard.name.value}...`);
164
+ // Invalide les requêtes liées aux données des KPIs, graphiques, et contenus Flex.
165
+ // Cela suppose que les composants enfants (KPIWidget, etc.) utilisent des clés de requête
166
+ // qui peuvent être invalidées par un préfixe commun, par exemple 'kpiData'.
167
+ queryClient.invalidateQueries('kpiData');
168
+ }, intervalInMs);
169
+
170
+ return () => clearInterval(intervalId); // Nettoie l'intervalle lors du démontage ou changement de dashboard
171
+ }
172
+ }, [dashboard?._id, dashboard?.refreshInterval, queryClient]);
173
+
174
+ const { data: availableKpis, isLoading: isLoadingKpiDefs, error: errorKpiDefs } = useQuery(
175
+ ['kpiDefinitions', me?.username, lang],
176
+ async () => {
177
+ if (!me?.username) return [];
178
+ const response = await fetch(
179
+ `/api/data/search?model=kpi&lang=${lang}&_user=${me.username}`, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' }
182
+ });
183
+ if (!response.ok) {
184
+ const res = await response.json();
185
+ throw new Error(res.error || t('dashboards.errorDefs', 'Erreur chargement définitions KPI'));
186
+ }
187
+ const data = await response.json();
188
+ return data.data;
189
+ },
190
+ {
191
+ enabled: !!me?.username,
192
+ refetchOnWindowFocus: false,
193
+ staleTime: 5 * 60 * 1000 // Les définitions de KPI ne changent pas souvent
194
+ }
195
+ );
196
+
197
+ const allKpiIdsInLayout = useMemo(() => layoutState.flatMap(section => section.kpis), [layoutState]);
198
+
199
+ const handleOpenAddWidgetTypeModal = (sectionIndex) => {
200
+ setAddingToSectionIndex(sectionIndex);
201
+ setIsAddWidgetTypeModalOpen(true);
202
+ };
203
+
204
+ const handleSelectWidgetType = (type) => {
205
+ setIsAddWidgetTypeModalOpen(false);
206
+ setEditingChartConfig(null);
207
+ setEditingFlexViewConfig(null); // Réinitialiser aussi la config FlexView en édition
208
+
209
+ if (type === 'KPI') {
210
+ setIsAddKpiDialogOpen(true);
211
+ } else if (type === 'Chart') {
212
+ setIsChartModalOpen(true);
213
+ } else if (type === 'FlexView') { // Gérer le type FlexView
214
+ setIsFlexBuilderModalOpen(true);
215
+ } else if (type === 'HtmlView') {
216
+ setIsHtmlViewBuilderModalOpen(true); // On peut réutiliser le même modal pour l'instant
217
+ }
218
+ };
219
+
220
+ const handleOpenEditHtmlViewModal = (htmlViewToEdit, sectionIndex) => {
221
+ setEditingHtmlViewConfig(htmlViewToEdit);
222
+ setAddingToSectionIndex(sectionIndex);
223
+ setIsHtmlViewBuilderModalOpen(true);
224
+ };
225
+
226
+ const handleSaveHtmlViewConfig = (config) => {
227
+ if (addingToSectionIndex === null) return;
228
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
229
+ const targetSection = newLayoutState[addingToSectionIndex];
230
+ if (!targetSection) return;
231
+ if (!Array.isArray(targetSection.htmlViews)) targetSection.htmlViews = [];
232
+
233
+ if (config.id) { // Edition
234
+ const index = targetSection.htmlViews.findIndex(v => v.id === config.id);
235
+ if (index !== -1) targetSection.htmlViews[index] = config;
236
+ } else { // Ajout
237
+ targetSection.htmlViews.push({ ...config, id: `html-${Date.now()}` });
238
+ }
239
+ mutation.mutate(newLayoutState);
240
+ handleCloseFlexBuilderModal();
241
+ setIsHtmlViewBuilderModalOpen(false);
242
+ setEditingHtmlViewConfig(null);
243
+ };
244
+
245
+ const handleRemoveHtmlView = (htmlViewId, sectionIndex) => {
246
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
247
+ if (newLayoutState[sectionIndex]?.htmlViews) {
248
+ newLayoutState[sectionIndex].htmlViews = newLayoutState[sectionIndex].htmlViews.filter(hv => hv.id !== htmlViewId);
249
+ mutation.mutate(newLayoutState);
250
+ }
251
+ };
252
+
253
+
254
+
255
+ const handleAddKpi = (kpiDefinition) => {
256
+ if (addingToSectionIndex === null || !layoutState[addingToSectionIndex]) return;
257
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
258
+ if (!newLayoutState[addingToSectionIndex].kpis.includes(t(kpiDefinition.name.value)) && !newLayoutState[addingToSectionIndex].kpis.includes(kpiDefinition.name.value)) {
259
+ newLayoutState[addingToSectionIndex].kpis.push(kpiDefinition.name.value);
260
+ mutation.mutate(newLayoutState);
261
+ // gtag('event', 'add_kpi_to_section');
262
+ }
263
+ setIsAddKpiDialogOpen(false);
264
+ setAddingToSectionIndex(null);
265
+ };
266
+
267
+ const handleRemoveKpi = (kpiDefinition, sectionIndex) => {
268
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
269
+ if (newLayoutState[sectionIndex]) {
270
+ newLayoutState[sectionIndex].kpis = newLayoutState[sectionIndex].kpis.filter(id => id !== kpiDefinition.name.value);
271
+ mutation.mutate(newLayoutState);
272
+ // gtag('event', 'remove_kpi_from_section');
273
+ }
274
+ };
275
+
276
+ const handleOpenEditChartModal = (chartToEdit, sectionIndex) => {
277
+ setEditingChartConfig(chartToEdit);
278
+ setAddingToSectionIndex(sectionIndex);
279
+ setIsChartModalOpen(true);
280
+ };
281
+
282
+ const handleCloseChartModal = () => {
283
+ setIsChartModalOpen(false);
284
+ setAddingToSectionIndex(null);
285
+ setEditingChartConfig(null);
286
+ };
287
+
288
+ const handleSaveChartConfig = (config) => {
289
+ if (addingToSectionIndex === null) return;
290
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
291
+ const targetSection = newLayoutState[addingToSectionIndex];
292
+ if (!targetSection) return;
293
+ if (!Array.isArray(targetSection.chartConfigs)) targetSection.chartConfigs = [];
294
+
295
+ if (config.id) { // Edition
296
+ const chartIndex = targetSection.chartConfigs.findIndex(chart => chart.id === config.id);
297
+ if (chartIndex !== -1) {
298
+ targetSection.chartConfigs[chartIndex] = config;
299
+ mutation.mutate(newLayoutState);
300
+ // gtag('event', 'edit_chart_in_section');
301
+ }
302
+ } else { // Ajout
303
+ targetSection.chartConfigs.push({
304
+ ...config,
305
+ id: `chart-${Date.now()}-${Math.random().toString(16).slice(2)}`
306
+ });
307
+ mutation.mutate(newLayoutState);
308
+ // gtag('event', 'add_chart_to_section');
309
+ }
310
+ handleCloseChartModal();
311
+ };
312
+
313
+ const handleRemoveChart = (chartId, sectionIndex) => {
314
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
315
+ if (newLayoutState[sectionIndex]?.chartConfigs) {
316
+ newLayoutState[sectionIndex].chartConfigs = newLayoutState[sectionIndex].chartConfigs.filter(chart => chart.id !== chartId);
317
+ mutation.mutate(newLayoutState);
318
+ // gtag('event', 'remove_chart_from_section');
319
+ }
320
+ };
321
+
322
+ // --- Fonctions pour FlexView ---
323
+ const handleOpenEditFlexViewModal = (flexViewToEdit, sectionIndex) => {
324
+ setEditingFlexViewConfig(flexViewToEdit);
325
+ setAddingToSectionIndex(sectionIndex);
326
+ setIsFlexBuilderModalOpen(true);
327
+ };
328
+
329
+ const handleCloseFlexBuilderModal = () => {
330
+ setIsFlexBuilderModalOpen(false);
331
+ setAddingToSectionIndex(null);
332
+ setEditingFlexViewConfig(null);
333
+ };
334
+
335
+ const handleSaveFlexViewConfig = (config) => { // config vient de FlexBuilderModal
336
+ if (addingToSectionIndex === null) return;
337
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
338
+ const targetSection = newLayoutState[addingToSectionIndex];
339
+ if (!targetSection) return;
340
+ if (!Array.isArray(targetSection.flexViews)) targetSection.flexViews = [];
341
+
342
+ // MODIFICATION: S'assurer que dataLimit est valide (1-8) et a une valeur par défaut
343
+ const newFlexViewData = {
344
+ ...config,
345
+ dataLimit: Math.max(1, Math.min(config.dataLimit || 1, 8)), // Valeur par défaut 1, max 8
346
+ };
347
+
348
+ if (config.id) { // Edition
349
+ const flexViewIndex = targetSection.flexViews.findIndex(fv => fv.id === config.id);
350
+ if (flexViewIndex !== -1) {
351
+ targetSection.flexViews[flexViewIndex] = { ...newFlexViewData, id: config.id }; // Conserver l'ID existant
352
+ mutation.mutate(newLayoutState);
353
+ // gtag('event', 'edit_flexview_in_section');
354
+ }
355
+ } else { // Ajout
356
+ targetSection.flexViews.push({
357
+ ...newFlexViewData,
358
+ id: `flex-${Date.now()}-${Math.random().toString(16).slice(2)}`
359
+ });
360
+ mutation.mutate(newLayoutState);
361
+ // gtag('event', 'add_flexview_to_section');
362
+ }
363
+ handleCloseFlexBuilderModal();
364
+ };
365
+
366
+ const handleRemoveFlexView = (flexViewId, sectionIndex) => {
367
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
368
+ if (newLayoutState[sectionIndex]?.flexViews) {
369
+ newLayoutState[sectionIndex].flexViews = newLayoutState[sectionIndex].flexViews.filter(fv => fv.id !== flexViewId);
370
+ mutation.mutate(newLayoutState);
371
+ // gtag('event', 'remove_flexview_from_section');
372
+ }
373
+ };
374
+
375
+
376
+ const handleAddSection = () => {
377
+ const newLayoutState = [...layoutState, {
378
+ name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
379
+ kpis: [], // Utiliser kpis au lieu de kpiIds
380
+ chartConfigs: [],
381
+ flexViews: []
382
+ }];
383
+ mutation.mutate(newLayoutState);
384
+ };
385
+
386
+ const handleRemoveSection = (sectionIndex) => {
387
+ if (layoutState.length <= 1) return; // Empêcher la suppression de la dernière section
388
+ const newLayoutState = layoutState.filter((_, index) => index !== sectionIndex);
389
+ mutation.mutate(newLayoutState);
390
+ // gtag('event', 'remove_dashboard_section');
391
+ };
392
+
393
+ const handleUpdateSectionName = (sectionIndex, newName) => {
394
+ const trimmedName = newName.trim();
395
+ if (!trimmedName || trimmedName === layoutState[sectionIndex].name) {
396
+ setEditingSectionIndex(null); // Quitter le mode édition si pas de changement ou nom vide
397
+ return;
398
+ }
399
+ const newLayoutState = JSON.parse(JSON.stringify(layoutState));
400
+ newLayoutState[sectionIndex].name = trimmedName;
401
+ mutation.mutate(newLayoutState);
402
+ setEditingSectionIndex(null);
403
+ // gtag('event', 'rename_dashboard_section');
404
+ };
405
+
406
+ const handleSectionNameBlur = (e, sectionIndex) => {
407
+ handleUpdateSectionName(sectionIndex, e.target.innerText);
408
+ };
409
+
410
+ const handleSectionNameKeyDown = (e, sectionIndex) => {
411
+ if (e.key === 'Enter') {
412
+ e.preventDefault();
413
+ handleUpdateSectionName(sectionIndex, e.target.innerText);
414
+ } else if (e.key === 'Escape') {
415
+ e.preventDefault();
416
+ e.target.innerText = originalSectionName; // Restaurer le nom original
417
+ setEditingSectionIndex(null);
418
+ }
419
+ };
420
+
421
+ const startEditingSectionName = (index, currentName) => {
422
+ setOriginalSectionName(currentName);
423
+ setEditingSectionIndex(index);
424
+ // Focus et sélection du contenu après un court délai pour permettre au DOM de se mettre à jour
425
+ setTimeout(() => {
426
+ const element = document.querySelector(`.dashboard-section:nth-child(${index + 1}) .section-title`);
427
+ if (element) {
428
+ element.focus();
429
+ const range = document.createRange();
430
+ const sel = window.getSelection();
431
+ range.selectNodeContents(element);
432
+ range.collapse(false); // Place le curseur à la fin
433
+ sel.removeAllRanges();
434
+ sel.addRange(range);
435
+ }
436
+ }, 0);
437
+ };
438
+
439
+ const sectionsWithItems = useMemo(() => {
440
+ return layoutState.map(section => {
441
+ return {
442
+ name: section.name, // On garde le nom
443
+
444
+ kpis: (section.kpis || [])
445
+ .map(id => availableKpis?.find(kpi => kpi.name.value === id))
446
+ .filter(Boolean), // On retire les KPIs qui n'auraient pas été trouvés
447
+
448
+ chartConfigs: section.chartConfigs || [],
449
+
450
+ htmlViews: section.htmlViews || [],
451
+
452
+ flexViews: section.flexViews || [],
453
+ };
454
+ });
455
+ }, [layoutState, availableKpis]);
456
+
457
+ if (layoutState === null && !dashboard) {
458
+ return <p><Trans i18nKey="dashboards.noDashboardSelected">Aucun tableau de bord sélectionné.</Trans></p>;
459
+ }
460
+ if (layoutState === null && dashboard) { // Devrait être gaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingLayout">Chargement de la disposition...</Trans></p>;
461
+ }
462
+ if (isLoadingKpiDefs) return <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingDefs">Chargement des définitions de KPI...</Trans></p>;
463
+ if (errorKpiDefs) return <p className="error">{errorKpiDefs.message}</p>;
464
+
465
+
466
+ return (
467
+ <div className="dashboard-view">
468
+ <DialogProvider> {/* Assurez-vous que DialogProvider englobe bien tous les modaux */}
469
+ {isAddWidgetTypeModalOpen && (<AddWidgetTypeModal
470
+ onClose={() => setIsAddWidgetTypeModalOpen(false)}
471
+ onSelectType={handleSelectWidgetType}
472
+ />)}
473
+
474
+ {isAddKpiDialogOpen && (
475
+ <KPIDialog
476
+ availableKpis={(availableKpis||[]).filter(kpi => !allKpiIdsInLayout.includes(kpi.name.value) && !allKpiIdsInLayout.includes(kpi.name.value))}
477
+ onAddKpi={handleAddKpi}
478
+ onClose={() => {
479
+ setIsAddKpiDialogOpen(false);
480
+ setAddingToSectionIndex(null); // Réinitialiser l'index de section
481
+ }}
482
+ />
483
+ )}
484
+ {isChartModalOpen && (
485
+ <ChartConfigModal
486
+ isOpen={isChartModalOpen}
487
+ onClose={handleCloseChartModal}
488
+ onSave={handleSaveChartConfig}
489
+ initialConfig={editingChartConfig}
490
+ models={models} // Passer les modèles pour la configuration du graphique
491
+ />
492
+ )}
493
+ {isHtmlViewBuilderModalOpen && (
494
+ <HtmlViewBuilderModal
495
+ isOpen={isHtmlViewBuilderModalOpen}
496
+ onClose={() => {
497
+ setIsHtmlViewBuilderModalOpen(false);
498
+ setEditingHtmlViewConfig(null);
499
+ }}
500
+ onSave={handleSaveHtmlViewConfig}
501
+ models={models}
502
+ initialConfig={editingHtmlViewConfig}
503
+ />
504
+ )}
505
+ </DialogProvider>
506
+
507
+ {isFlexBuilderModalOpen && (
508
+ <FlexBuilderModal
509
+ isOpen={isFlexBuilderModalOpen}
510
+ onClose={handleCloseFlexBuilderModal}
511
+ onSave={handleSaveFlexViewConfig} // Utiliser le handler pour FlexView
512
+ models={models} // Passer les modèles
513
+ initialConfig={editingFlexViewConfig} // Passer la config en édition
514
+ // data prop for FlexBuilder is for its internal preview,
515
+ // not for the data displayed on the dashboard itself.
516
+ />
517
+ )}
518
+
519
+ {dashboard && (
520
+ <>
521
+ <h2>{dashboard.name.value}</h2>
522
+ {dashboard.description && <p className="dashboard-description">{dashboard.description}</p>}
523
+
524
+ <div className="dashboard-sections">
525
+
526
+ {sectionsWithItems.map((sectionData, sectionIndex) => (
527
+ <div key={`section-${sectionIndex}-${dashboard._id}`} className="dashboard-section">
528
+ <div className="section-header">
529
+ <h4
530
+ className={`section-title ${editingSectionIndex === sectionIndex ? 'editing' : ''}`}
531
+ contentEditable={editingSectionIndex === sectionIndex}
532
+ suppressContentEditableWarning={true}
533
+ onClick={() => {
534
+ if (editingSectionIndex !== sectionIndex) startEditingSectionName(sectionIndex, sectionData.name);
535
+ }}
536
+ onBlur={(e) => handleSectionNameBlur(e, sectionIndex)}
537
+ onKeyDown={(e) => handleSectionNameKeyDown(e, sectionIndex)}
538
+ >
539
+ {sectionData.name}
540
+ </h4>
541
+ {/* Bouton d'édition de nom de section (optionnel, car le titre est cliquable) */}
542
+ </div>
543
+
544
+ <div
545
+ className="items-grid flex"> {/* Assurez-vous que cette classe est bien stylée pour flex/grid */}
546
+ {sectionData.kpis.map(kpiDef => (
547
+ <KPIWidget
548
+ key={kpiDef._id}
549
+ kpiDefinition={kpiDef}
550
+ onRemove={() => handleRemoveKpi(kpiDef, sectionIndex)}
551
+ disabled={mutation.isLoading}
552
+ />
553
+ ))}
554
+ {sectionData.chartConfigs.map(chartConfig => (
555
+ <div key={chartConfig.id} className="dashboard-item-wrapper chart-wrapper">
556
+ <DashboardChart config={chartConfig}/>
557
+ <div className="item-actions">
558
+ <button className="edit-item-button"
559
+ onClick={() => handleOpenEditChartModal(chartConfig, sectionIndex)}
560
+ title={t('dashboards.editChartTitle', 'Modifier ce graphique')}
561
+ disabled={mutation.isLoading}><FaPencilAlt/></button>
562
+ <button className="remove-item-button"
563
+ onClick={() => handleRemoveChart(chartConfig.id, sectionIndex)}
564
+ title={t('dashboards.removeChartTitle', 'Supprimer ce graphique')}
565
+ disabled={mutation.isLoading}><FaTrash/></button>
566
+ </div>
567
+ </div>
568
+ ))}
569
+
570
+ {/* NOUVEAU: Affichage des vues HTML */}
571
+ {sectionData.htmlViews?.map(htmlConfig => (
572
+ <div key={htmlConfig.id} className="dashboard-item-wrapper html-view-wrapper">
573
+ <DashboardHtmlViewItem config={htmlConfig} />
574
+ <div className="item-actions">
575
+ {/* TODO: Add edit button for HTML views */}
576
+ {/* <button ...><FaPencilAlt /></button> */}
577
+ <button className="edit-item-button"
578
+ onClick={() => handleOpenEditHtmlViewModal(htmlConfig, sectionIndex)}
579
+ title={t('dashboards.editHtmlViewTitle', 'Modifier cette vue HTML')}
580
+ disabled={mutation.isLoading}><FaPencilAlt/></button>
581
+ <button className="remove-item-button"
582
+ onClick={() => handleRemoveHtmlView(htmlConfig.id, sectionIndex)}
583
+ title={t('dashboards.removeHtmlViewTitle', 'Supprimer cette vue HTML')}
584
+ disabled={mutation.isLoading}><FaTrash/></button>
585
+ </div>
586
+ </div>
587
+ ))}
588
+ {/* Affichage des FlexViews */}
589
+ {sectionData.flexViews?.map(flexViewConfig => (
590
+
591
+ <div key={flexViewConfig.id}
592
+ className="dashboard-item-wrapper flex-view-wrapper">
593
+ <DashboardFlexViewItem
594
+ flexViewConfig={flexViewConfig}
595
+ allModels={models} // Passer tous les modisponibles
596
+ />
597
+ <div className="item-actions">
598
+ <button className="edit-item-button"
599
+ onClick={() => handleOpenEditFlexViewModal(flexViewConfig, sectionIndex)}
600
+ title={t('dashboards.editFlexViewTitle', 'Modifier cette vue Flex')}
601
+ disabled={mutation.isLoading}><FaPencilAlt/></button>
602
+ <button className="remove-item-button"
603
+ onClick={() => handleRemoveFlexView(flexViewConfig.id, sectionIndex)}
604
+ title={t('dashboards.removeFlexViewTitle', 'Supprimer cette vue Flex')}
605
+ disabled={mutation.isLoading}><FaTrash/></button>
606
+ </div>
607
+ </div>
608
+ ))}
609
+
610
+ {/* Message si la section est vide */}
611
+ {sectionData.kpis.length === 0 && sectionData.chartConfigs.length === 0 && (sectionData.flexViews === undefined || sectionData.flexViews.length === 0) && (
612
+ <p className="empty-section-message"><Trans
613
+ i18nKey="dashboards.emptySectionClickPlus">Section vide. Cliquez sur le
614
+ bouton '+' ci-dessous pour ajouter des éléments.</Trans></p>
615
+ )}
616
+ </div>
617
+ <div className="add-buttons-inline">
618
+ <button
619
+ className="add-kpi-button add-kpi-button-inline" /* Renommer la classe si elle est générique */
620
+ onClick={() => handleOpenAddWidgetTypeModal(sectionIndex)}
621
+ title={t('dashboards.addWidgetToSectionTitle', 'Ajouter un élément à cette section')}
622
+ disabled={mutation.isLoading}>
623
+ <FaPlus/>
624
+ </button>
625
+ {layoutState.length > 1 && (
626
+ <button
627
+ className="remove-section-button"
628
+ onClick={() => handleRemoveSection(sectionIndex)}
629
+ title={t('dashboards.removeSectionTitle', 'Supprimer cette section')}
630
+ disabled={mutation.isLoading}
631
+ >
632
+ <FaTrash/>
633
+ </button>
634
+ )}
635
+ </div>
636
+ </div>
637
+ ))}
638
+
639
+ {sectionsWithItems.length > 0 && (<div className="flex actions left">
640
+ <button onClick={handleAddSection} className="add-section-button"
641
+ disabled={mutation.isLoading}>
642
+ <FaPlus/> <Trans i18nKey="dashboards.addSection">Ajouter une section</Trans>
643
+ </button>
644
+ </div>)}
645
+
646
+ {layoutState.length === 0 && !isLoadingKpiDefs && (
647
+ <p><Trans i18nKey="dashboards.noSections">Ce tableau de bord n'a pas encore de
648
+ sections.</Trans></p>
649
+ )}
650
+ </div>
651
+ </>
652
+ )}
653
+ </div>
654
+ );
570
655
  }