data-primals-engine 1.3.4 → 1.4.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.
- package/README.md +72 -2
- package/client/README.md +20 -0
- package/client/package-lock.json +712 -147
- package/client/package.json +38 -37
- package/client/src/App.jsx +9 -10
- package/client/src/App.scss +7 -1
- package/client/src/Dashboard.jsx +349 -208
- package/client/src/DashboardView.jsx +569 -547
- package/client/src/DataEditor.jsx +20 -2
- package/client/src/DataLayout.jsx +24 -12
- package/client/src/DataTable.jsx +807 -760
- package/client/src/DataTable.scss +187 -90
- package/client/src/Field.jsx +1783 -1656
- package/client/src/ModelCreator.jsx +2 -2
- package/client/src/ModelCreatorField.jsx +906 -804
- package/client/src/ModelList.jsx +2 -2
- package/client/src/PackGallery.jsx +391 -290
- package/client/src/PackGallery.scss +231 -210
- package/client/src/constants.js +16 -4
- package/client/src/translations.js +16750 -16392
- package/package.json +14 -11
- package/src/core.js +9 -0
- package/src/defaultModels.js +18 -10
- package/src/email.js +2 -1
- package/src/gameObject.js +1 -1
- package/src/i18n.js +501 -28
- package/src/modules/auth-google/index.js +51 -0
- package/src/modules/auth-microsoft/index.js +82 -0
- package/src/modules/auth-saml/index.js +90 -0
- package/src/modules/data/data.core.js +5 -1
- package/src/modules/data/data.history.js +489 -489
- package/src/modules/data/data.js +86 -12
- package/src/modules/data/data.routes.js +1691 -1663
- package/src/modules/user.js +19 -0
- package/src/modules/workflow.js +2 -12
- package/src/providers.js +110 -1
- package/src/sso.js +194 -0
- package/test/data.integration.test.js +3 -0
- package/test/user.test.js +1 -1
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const newLayoutState = JSON.parse(JSON.stringify(layoutState));
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const newLayoutState =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
</div>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
{/*
|
|
504
|
-
{sectionData.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
}
|