data-primals-engine 1.3.4 → 1.4.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.
@@ -1,548 +1,570 @@
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
- const { data: availableKpis, isLoading: isLoadingKpiDefs, error: errorKpiDefs } = useQuery(
142
- ['kpiDefinitions', me?.username, lang],
143
- async () => {
144
- if (!me?.username) return [];
145
- const response = await fetch(
146
- `/api/data/search?model=kpi&lang=${lang}&_user=${me.username}`, {
147
- method: 'POST',
148
- headers: { 'Content-Type': 'application/json' }
149
- });
150
- if (!response.ok) {
151
- const res = await response.json();
152
- throw new Error(res.error || t('dashboards.errorDefs', 'Erreur chargement définitions KPI'));
153
- }
154
- const data = await response.json();
155
- return data.data;
156
- },
157
- { enabled: !!me?.username, refetchOnWindowFocus: false, staleTime: 5 * 60 * 1000, refetchInterval: dashboard?.refetchInterval || 60 * 1000 }
158
- );
159
-
160
- const allKpiIdsInLayout = useMemo(() => layoutState.flatMap(section => section.kpis), [layoutState]);
161
-
162
- const handleOpenAddWidgetTypeModal = (sectionIndex) => {
163
- setAddingToSectionIndex(sectionIndex);
164
- setIsAddWidgetTypeModalOpen(true);
165
- };
166
-
167
- const handleSelectWidgetType = (type) => {
168
- setIsAddWidgetTypeModalOpen(false);
169
- setEditingChartConfig(null);
170
- setEditingFlexViewConfig(null); // Réinitialiser aussi la config FlexView en édition
171
-
172
- if (type === 'KPI') {
173
- setIsAddKpiDialogOpen(true);
174
- } else if (type === 'Chart') {
175
- setIsChartModalOpen(true);
176
- } else if (type === 'FlexView') { // Gérer le type FlexView
177
- setIsFlexBuilderModalOpen(true);
178
- }
179
- };
180
-
181
-
182
- const handleAddKpi = (kpiDefinition) => {
183
- if (addingToSectionIndex === null || !layoutState[addingToSectionIndex]) return;
184
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
185
- if (!newLayoutState[addingToSectionIndex].kpis.includes(t(kpiDefinition.name.value)) && !newLayoutState[addingToSectionIndex].kpis.includes(kpiDefinition.name.value)) {
186
- newLayoutState[addingToSectionIndex].kpis.push(kpiDefinition.name.value);
187
- mutation.mutate(newLayoutState);
188
- // gtag('event', 'add_kpi_to_section');
189
- }
190
- setIsAddKpiDialogOpen(false);
191
- setAddingToSectionIndex(null);
192
- };
193
-
194
- const handleRemoveKpi = (kpiDefinition, sectionIndex) => {
195
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
196
- if (newLayoutState[sectionIndex]) {
197
- newLayoutState[sectionIndex].kpis = newLayoutState[sectionIndex].kpis.filter(id => id !== kpiDefinition.name.value);
198
- mutation.mutate(newLayoutState);
199
- // gtag('event', 'remove_kpi_from_section');
200
- }
201
- };
202
-
203
- const handleOpenEditChartModal = (chartToEdit, sectionIndex) => {
204
- setEditingChartConfig(chartToEdit);
205
- setAddingToSectionIndex(sectionIndex);
206
- setIsChartModalOpen(true);
207
- };
208
-
209
- const handleCloseChartModal = () => {
210
- setIsChartModalOpen(false);
211
- setAddingToSectionIndex(null);
212
- setEditingChartConfig(null);
213
- };
214
-
215
- const handleSaveChartConfig = (config) => {
216
- if (addingToSectionIndex === null) return;
217
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
218
- const targetSection = newLayoutState[addingToSectionIndex];
219
- if (!targetSection) return;
220
- if (!Array.isArray(targetSection.chartConfigs)) targetSection.chartConfigs = [];
221
-
222
- if (config.id) { // Edition
223
- const chartIndex = targetSection.chartConfigs.findIndex(chart => chart.id === config.id);
224
- if (chartIndex !== -1) {
225
- targetSection.chartConfigs[chartIndex] = config;
226
- mutation.mutate(newLayoutState);
227
- // gtag('event', 'edit_chart_in_section');
228
- }
229
- } else { // Ajout
230
- targetSection.chartConfigs.push({
231
- ...config,
232
- id: `chart-${Date.now()}-${Math.random().toString(16).slice(2)}`
233
- });
234
- mutation.mutate(newLayoutState);
235
- // gtag('event', 'add_chart_to_section');
236
- }
237
- handleCloseChartModal();
238
- };
239
-
240
- const handleRemoveChart = (chartId, sectionIndex) => {
241
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
242
- if (newLayoutState[sectionIndex]?.chartConfigs) {
243
- newLayoutState[sectionIndex].chartConfigs = newLayoutState[sectionIndex].chartConfigs.filter(chart => chart.id !== chartId);
244
- mutation.mutate(newLayoutState);
245
- // gtag('event', 'remove_chart_from_section');
246
- }
247
- };
248
-
249
- // --- Fonctions pour FlexView ---
250
- const handleOpenEditFlexViewModal = (flexViewToEdit, sectionIndex) => {
251
- setEditingFlexViewConfig(flexViewToEdit);
252
- setAddingToSectionIndex(sectionIndex);
253
- setIsFlexBuilderModalOpen(true);
254
- };
255
-
256
- const handleCloseFlexBuilderModal = () => {
257
- setIsFlexBuilderModalOpen(false);
258
- setAddingToSectionIndex(null);
259
- setEditingFlexViewConfig(null);
260
- };
261
-
262
- const handleSaveFlexViewConfig = (config) => { // config vient de FlexBuilderModal
263
- if (addingToSectionIndex === null) return;
264
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
265
- const targetSection = newLayoutState[addingToSectionIndex];
266
- if (!targetSection) return;
267
- if (!Array.isArray(targetSection.flexViews)) targetSection.flexViews = [];
268
-
269
- // MODIFICATION: S'assurer que dataLimit est valide (1-8) et a une valeur par défaut
270
- const newFlexViewData = {
271
- ...config,
272
- dataLimit: Math.max(1, Math.min(config.dataLimit || 1, 8)), // Valeur par défaut 1, max 8
273
- };
274
-
275
- if (config.id) { // Edition
276
- const flexViewIndex = targetSection.flexViews.findIndex(fv => fv.id === config.id);
277
- if (flexViewIndex !== -1) {
278
- targetSection.flexViews[flexViewIndex] = { ...newFlexViewData, id: config.id }; // Conserver l'ID existant
279
- mutation.mutate(newLayoutState);
280
- // gtag('event', 'edit_flexview_in_section');
281
- }
282
- } else { // Ajout
283
- targetSection.flexViews.push({
284
- ...newFlexViewData,
285
- id: `flex-${Date.now()}-${Math.random().toString(16).slice(2)}`
286
- });
287
- mutation.mutate(newLayoutState);
288
- // gtag('event', 'add_flexview_to_section');
289
- }
290
- handleCloseFlexBuilderModal();
291
- };
292
-
293
- const handleRemoveFlexView = (flexViewId, sectionIndex) => {
294
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
295
- if (newLayoutState[sectionIndex]?.flexViews) {
296
- newLayoutState[sectionIndex].flexViews = newLayoutState[sectionIndex].flexViews.filter(fv => fv.id !== flexViewId);
297
- mutation.mutate(newLayoutState);
298
- // gtag('event', 'remove_flexview_from_section');
299
- }
300
- };
301
-
302
-
303
- const handleAddSection = () => {
304
- const newLayoutState = [...layoutState, {
305
- name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
306
- kpis: [], // Utiliser kpis au lieu de kpiIds
307
- chartConfigs: [],
308
- flexViews: []
309
- }];
310
- mutation.mutate(newLayoutState);
311
- };
312
-
313
- const handleRemoveSection = (sectionIndex) => {
314
- if (layoutState.length <= 1) return; // Empêcher la suppression de la dernière section
315
- const newLayoutState = layoutState.filter((_, index) => index !== sectionIndex);
316
- mutation.mutate(newLayoutState);
317
- // gtag('event', 'remove_dashboard_section');
318
- };
319
-
320
- const handleUpdateSectionName = (sectionIndex, newName) => {
321
- const trimmedName = newName.trim();
322
- if (!trimmedName || trimmedName === layoutState[sectionIndex].name) {
323
- setEditingSectionIndex(null); // Quitter le mode édition si pas de changement ou nom vide
324
- return;
325
- }
326
- const newLayoutState = JSON.parse(JSON.stringify(layoutState));
327
- newLayoutState[sectionIndex].name = trimmedName;
328
- mutation.mutate(newLayoutState);
329
- setEditingSectionIndex(null);
330
- // gtag('event', 'rename_dashboard_section');
331
- };
332
-
333
- const handleSectionNameBlur = (e, sectionIndex) => {
334
- handleUpdateSectionName(sectionIndex, e.target.innerText);
335
- };
336
-
337
- const handleSectionNameKeyDown = (e, sectionIndex) => {
338
- if (e.key === 'Enter') {
339
- e.preventDefault();
340
- handleUpdateSectionName(sectionIndex, e.target.innerText);
341
- } else if (e.key === 'Escape') {
342
- e.preventDefault();
343
- e.target.innerText = originalSectionName; // Restaurer le nom original
344
- setEditingSectionIndex(null);
345
- }
346
- };
347
-
348
- const startEditingSectionName = (index, currentName) => {
349
- setOriginalSectionName(currentName);
350
- setEditingSectionIndex(index);
351
- // Focus et sélection du contenu après un court délai pour permettre au DOM de se mettre à jour
352
- setTimeout(() => {
353
- const element = document.querySelector(`.dashboard-section:nth-child(${index + 1}) .section-title`);
354
- if (element) {
355
- element.focus();
356
- const range = document.createRange();
357
- const sel = window.getSelection();
358
- range.selectNodeContents(element);
359
- range.collapse(false); // Place le curseur à la fin
360
- sel.removeAllRanges();
361
- sel.addRange(range);
362
- }
363
- }, 0);
364
- };
365
-
366
- const sectionsWithItems = useMemo(() => {
367
- return layoutState.map(section => {
368
- return {
369
- name: section.name, // On garde le nom
370
-
371
- kpis: (section.kpis || [])
372
- .map(id => availableKpis?.find(kpi => kpi.name.value === id))
373
- .filter(Boolean), // On retire les KPIs qui n'auraient pas été trouvés
374
-
375
- chartConfigs: section.chartConfigs || [],
376
-
377
- flexViews: section.flexViews || [],
378
- };
379
- });
380
- }, [layoutState, availableKpis]);
381
-
382
- if (layoutState === null && !dashboard) {
383
- return <p><Trans i18nKey="dashboards.noDashboardSelected">Aucun tableau de bord sélectionné.</Trans></p>;
384
- }
385
- if (layoutState === null && dashboard) { // Devrait être gaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingLayout">Chargement de la disposition...</Trans></p>;
386
- }
387
- if (isLoadingKpiDefs) return <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingDefs">Chargement des définitions de KPI...</Trans></p>;
388
- if (errorKpiDefs) return <p className="error">{errorKpiDefs.message}</p>;
389
-
390
-
391
- return (
392
- <div className="dashboard-view">
393
- <DialogProvider> {/* Assurez-vous que DialogProvider englobe bien tous les modaux */}
394
- {isAddWidgetTypeModalOpen && (<AddWidgetTypeModal
395
- onClose={() => setIsAddWidgetTypeModalOpen(false)}
396
- onSelectType={handleSelectWidgetType}
397
- />)}
398
-
399
- {isAddKpiDialogOpen && (
400
- <KPIDialog
401
- availableKpis={(availableKpis||[]).filter(kpi => !allKpiIdsInLayout.includes(kpi.name.value) && !allKpiIdsInLayout.includes(kpi.name.value))}
402
- onAddKpi={handleAddKpi}
403
- onClose={() => {
404
- setIsAddKpiDialogOpen(false);
405
- setAddingToSectionIndex(null); // Réinitialiser l'index de section
406
- }}
407
- />
408
- )}
409
- {isChartModalOpen && (
410
- <ChartConfigModal
411
- isOpen={isChartModalOpen}
412
- onClose={handleCloseChartModal}
413
- onSave={handleSaveChartConfig}
414
- initialConfig={editingChartConfig}
415
- models={models} // Passer les modèles pour la configuration du graphique
416
- />
417
- )}
418
- </DialogProvider>
419
-
420
- {isFlexBuilderModalOpen && (
421
- <FlexBuilderModal
422
- isOpen={isFlexBuilderModalOpen}
423
- onClose={handleCloseFlexBuilderModal}
424
- onSave={handleSaveFlexViewConfig} // Utiliser le handler pour FlexView
425
- models={models} // Passer les modèles
426
- initialConfig={editingFlexViewConfig} // Passer la config en édition
427
- // data prop for FlexBuilder is for its internal preview,
428
- // not for the data displayed on the dashboard itself.
429
- />
430
- )}
431
- {dashboard && (
432
- <>
433
- <h2>{dashboard.name.value}</h2>
434
- {dashboard.description && <p className="dashboard-description">{dashboard.description}</p>}
435
-
436
- <div className="dashboard-sections">
437
-
438
- {sectionsWithItems.map((sectionData, sectionIndex) => (
439
- <div key={`section-${sectionIndex}-${dashboard._id}`} className="dashboard-section">
440
- <div className="section-header">
441
- <h4
442
- className={`section-title ${editingSectionIndex === sectionIndex ? 'editing' : ''}`}
443
- contentEditable={editingSectionIndex === sectionIndex}
444
- suppressContentEditableWarning={true}
445
- onClick={() => {
446
- if (editingSectionIndex !== sectionIndex) startEditingSectionName(sectionIndex, sectionData.name);
447
- }}
448
- onBlur={(e) => handleSectionNameBlur(e, sectionIndex)}
449
- onKeyDown={(e) => handleSectionNameKeyDown(e, sectionIndex)}
450
- >
451
- {sectionData.name}
452
- </h4>
453
- {/* Bouton d'édition de nom de section (optionnel, car le titre est cliquable) */}
454
- </div>
455
-
456
- <div
457
- className="items-grid flex"> {/* Assurez-vous que cette classe est bien stylée pour flex/grid */}
458
- {sectionData.kpis.map(kpiDef => (
459
- <KPIWidget
460
- key={kpiDef._id}
461
- kpiDefinition={kpiDef}
462
- onRemove={() => handleRemoveKpi(kpiDef, sectionIndex)}
463
- disabled={mutation.isLoading}
464
- />
465
- ))}
466
- {sectionData.chartConfigs.map(chartConfig => (
467
- <div key={chartConfig.id} className="dashboard-item-wrapper chart-wrapper">
468
- <DashboardChart config={chartConfig}/>
469
- <div className="item-actions">
470
- <button className="edit-item-button"
471
- onClick={() => handleOpenEditChartModal(chartConfig, sectionIndex)}
472
- title={t('dashboards.editChartTitle', 'Modifier ce graphique')}
473
- disabled={mutation.isLoading}><FaPencilAlt/></button>
474
- <button className="remove-item-button"
475
- onClick={() => handleRemoveChart(chartConfig.id, sectionIndex)}
476
- title={t('dashboards.removeChartTitle', 'Supprimer ce graphique')}
477
- disabled={mutation.isLoading}><FaTrash/></button>
478
- </div>
479
- </div>
480
- ))}
481
- {/* Affichage des FlexViews */}
482
- {sectionData.flexViews?.map(flexViewConfig => (
483
-
484
- <div key={flexViewConfig.id}
485
- className="dashboard-item-wrapper flex-view-wrapper">
486
- <DashboardFlexViewItem
487
- flexViewConfig={flexViewConfig}
488
- allModels={models} // Passer tous les modisponibles
489
- />
490
- <div className="item-actions">
491
- <button className="edit-item-button"
492
- onClick={() => handleOpenEditFlexViewModal(flexViewConfig, sectionIndex)}
493
- title={t('dashboards.editFlexViewTitle', 'Modifier cette vue Flex')}
494
- disabled={mutation.isLoading}><FaPencilAlt/></button>
495
- <button className="remove-item-button"
496
- onClick={() => handleRemoveFlexView(flexViewConfig.id, sectionIndex)}
497
- title={t('dashboards.removeFlexViewTitle', 'Supprimer cette vue Flex')}
498
- disabled={mutation.isLoading}><FaTrash/></button>
499
- </div>
500
- </div>
501
- ))}
502
-
503
- {/* Message si la section est vide */}
504
- {sectionData.kpis.length === 0 && sectionData.chartConfigs.length === 0 && (sectionData.flexViews === undefined || sectionData.flexViews.length === 0) && (
505
- <p className="empty-section-message"><Trans
506
- i18nKey="dashboards.emptySectionClickPlus">Section vide. Cliquez sur le
507
- bouton '+' ci-dessous pour ajouter des éléments.</Trans></p>
508
- )}
509
- </div>
510
- <div className="add-buttons-inline">
511
- <button
512
- className="add-kpi-button add-kpi-button-inline" /* Renommer la classe si elle est générique */
513
- onClick={() => handleOpenAddWidgetTypeModal(sectionIndex)}
514
- title={t('dashboards.addWidgetToSectionTitle', 'Ajouter un élément à cette section')}
515
- disabled={mutation.isLoading}>
516
- <FaPlus/>
517
- </button>
518
- {layoutState.length > 1 && (
519
- <button
520
- className="remove-section-button"
521
- onClick={() => handleRemoveSection(sectionIndex)}
522
- title={t('dashboards.removeSectionTitle', 'Supprimer cette section')}
523
- disabled={mutation.isLoading}
524
- >
525
- <FaTrash/>
526
- </button>
527
- )}
528
- </div>
529
- </div>
530
- ))}
531
-
532
- {sectionsWithItems.length > 0 && (<div className="flex actions left">
533
- <button onClick={handleAddSection} className="add-section-button"
534
- disabled={mutation.isLoading}>
535
- <FaPlus/> <Trans i18nKey="dashboards.addSection">Ajouter une section</Trans>
536
- </button>
537
- </div>)}
538
-
539
- {layoutState.length === 0 && !isLoadingKpiDefs && (
540
- <p><Trans i18nKey="dashboards.noSections">Ce tableau de bord n'a pas encore de
541
- sections.</Trans></p>
542
- )}
543
- </div>
544
- </>
545
- )}
546
- </div>
547
- );
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
+ );
548
570
  }