data-primals-engine 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +924 -915
  2. package/client/README.md +136 -136
  3. package/client/index.js +0 -1
  4. package/client/public/doc/API.postman_collection.json +213 -213
  5. package/client/public/react.svg +9 -9
  6. package/client/src/AddWidgetTypeModal.jsx +47 -47
  7. package/client/src/App.css +42 -42
  8. package/client/src/App.jsx +92 -150
  9. package/client/src/App.scss +6 -0
  10. package/client/src/AssistantChat.jsx +362 -363
  11. package/client/src/Button.jsx +12 -12
  12. package/client/src/Dashboard.jsx +480 -480
  13. package/client/src/DashboardHtmlViewItem.jsx +146 -146
  14. package/client/src/DashboardView.jsx +654 -654
  15. package/client/src/DataEditor.jsx +383 -383
  16. package/client/src/DataLayout.jsx +779 -808
  17. package/client/src/DataTable.jsx +782 -822
  18. package/client/src/DataTable.scss +186 -186
  19. package/client/src/GeolocationField.jsx +93 -93
  20. package/client/src/HistoryDialog.jsx +307 -307
  21. package/client/src/HistoryDialog.scss +319 -319
  22. package/client/src/HtmlViewBuilderModal.jsx +90 -90
  23. package/client/src/HtmlViewBuilderModal.scss +17 -17
  24. package/client/src/HtmlViewCard.jsx +43 -43
  25. package/client/src/ModelCreatorField.jsx +950 -950
  26. package/client/src/ModelList.jsx +280 -280
  27. package/client/src/Notification.jsx +136 -136
  28. package/client/src/PackGallery.jsx +391 -391
  29. package/client/src/PackGallery.scss +231 -231
  30. package/client/src/Pagination.jsx +143 -143
  31. package/client/src/RelationField.jsx +354 -354
  32. package/client/src/RelationSelectorWidget.jsx +172 -172
  33. package/client/src/RelationValue.jsx +2 -1
  34. package/client/src/contexts/CommandContext.jsx +260 -0
  35. package/client/src/contexts/ModelContext.jsx +4 -1
  36. package/client/src/contexts/UIContext.jsx +72 -72
  37. package/client/src/filter.js +263 -263
  38. package/client/src/index.css +90 -90
  39. package/package.json +6 -5
  40. package/perf/artillery-hooks.js +37 -37
  41. package/perf/setup.yml +25 -25
  42. package/src/constants.js +4 -0
  43. package/src/engine.js +335 -335
  44. package/src/events.js +232 -137
  45. package/src/filter.js +274 -274
  46. package/src/modules/assistant/assistant.js +225 -83
  47. package/src/modules/auth-google/index.js +50 -50
  48. package/src/modules/auth-microsoft/index.js +81 -81
  49. package/src/modules/data/data.core.js +118 -118
  50. package/src/modules/data/data.history.js +555 -555
  51. package/src/modules/data/data.operations.js +3401 -3381
  52. package/src/modules/data/data.relations.js +686 -686
  53. package/src/modules/data/data.routes.js +1879 -1879
  54. package/src/modules/data/data.validation.js +2 -2
  55. package/src/modules/file.js +247 -247
  56. package/src/packs.js +2 -2
  57. package/src/providers.js +2 -2
  58. package/src/services/stripe.js +196 -196
  59. package/src/sso.js +193 -193
  60. package/test/data.history.integration.test.js +264 -264
  61. package/test/data.integration.test.js +1206 -1206
  62. package/test/events.test.js +108 -10
  63. package/test/model.integration.test.js +377 -377
@@ -1,481 +1,481 @@
1
- import React, {useState, useMemo, useEffect} from 'react';
2
- import { Trans, useTranslation } from 'react-i18next';
3
- import {FaPlus, FaSpinner, FaCog} from "react-icons/fa"; // Ajout FaTrash
4
-
5
- import "./Dashboard.scss"
6
- import {useMutation, useQuery, useQueryClient} from "react-query";
7
- import { useAuthContext } from "./contexts/AuthContext.jsx";
8
- import { SelectField, DurationField, TextField } from "./Field.jsx";
9
- import {DashboardView} from "./DashboardView.jsx";
10
- import {useModelContext} from "./contexts/ModelContext.jsx";
11
- import {useNavigate, useParams, useSearchParams} from "react-router-dom";
12
- import {getObjectHash} from "../../src/core.js";
13
- import {getUserHash, getUserId} from "../../src/data.js";
14
- import Button from "./Button.jsx";
15
- import {FaNoteSticky} from "react-icons/fa6";
16
- import ChartConfigModal from "./ChartConfigModal.jsx";
17
- import {useUI} from "./contexts/UIContext.jsx";
18
- import {DialogProvider} from "./Dialog.jsx";
19
-
20
- // --- DashboardsPage (Reste inchangé) ---
21
- export function DashboardsPage() {
22
- const { setSelectedModel, selectedModel } = useModelContext()
23
- const { t } = useTranslation();
24
- const { me } = useAuthContext();
25
- const [selectedDashboardId, setSelectedDashboardId] = useState(null); // This seems to be the main state
26
-
27
- const { chartToAdd, setChartToAdd, flexViewToAdd, setFlexViewToAdd, htmlViewToAdd, setHtmlViewToAdd } = useUI();
28
- const [searchParams, setSearchParams] = useSearchParams();
29
- const { hash } = useParams(); // 2. Récupérer le hash de l'URL
30
-
31
- // ... après les autres hooks useState, useQuery, etc.
32
- const queryClient = useQueryClient();
33
- const [newDashboardName, setNewDashboardName] = useState('');
34
- const [isCreating, setIsCreating] = useState(false);
35
- const [isEditing, setIsEditing] = useState(false);
36
- const [dashboardToEdit, setDashboardToEdit] = useState(null);
37
-
38
- // États pour le modal de configuration des graphiques
39
- const [isChartModalOpen, setIsChartModalOpen] = useState(false);
40
- const [chartToConfigure, setChartToConfigure] = useState(null);
41
-
42
- const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
43
- ['userDashboards', me?.username],
44
- async () => {
45
- if (!me?.username) return null;
46
- const response = await fetch(
47
- `/api/data/search?model=dashboard&_user=${me.username}`, {
48
- method: 'POST',
49
- headers: { 'Content-Type': 'application/json' },
50
- // body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
51
- });
52
- if (!response.ok) {
53
- const res = await response.json();
54
- throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
55
- }
56
- return await response.json();
57
- },
58
- {
59
- enabled: !!me?.username
60
- }
61
- );
62
- const selectedDashboard = useMemo(() => {
63
- if (!selectedDashboardId || !dashboardsData?.data) return null;
64
- return dashboardsData.data.find(db => db._id === selectedDashboardId);
65
- }, [selectedDashboardId, dashboardsData]);
66
-
67
- const createDashboardMutation = useMutation(
68
- async (dashboardName) => {
69
- const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
70
- const response = await fetch('/api/data', {
71
- method: 'POST',
72
- headers: { 'Content-Type': 'application/json' },
73
- body: JSON.stringify({
74
- model: 'dashboard',
75
- data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
76
- })
77
- });
78
- if (!response.ok) {
79
- const errorData = await response.json();
80
- throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
81
- }
82
- return response.json();
83
- },
84
- {
85
- onSuccess: () => {
86
- // Rafraîchit la liste des dashboards après la création
87
- queryClient.invalidateQueries(['userDashboards', me?.username]);
88
- setNewDashboardName('');
89
- setIsCreating(false);
90
- },
91
- onError: (error) => {
92
- // Idéalement, afficher une notification à l'utilisateur
93
- console.error("Erreur lors de la création du dashboard:", error);
94
- }
95
- }
96
- );
97
-
98
- const updateDashboardMutation = useMutation(
99
- async (dashboardData) => {
100
- const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
101
- const response = await fetch(`/api/data`, {
102
- method: 'PUT',
103
- headers: { 'Content-Type': 'application/json' },
104
- body: JSON.stringify({
105
- model: 'dashboard',
106
- _id: _id,
107
- data: data
108
- })
109
- });
110
- if (!response.ok) {
111
- const errorData = await response.json();
112
- throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
113
- }
114
- return response.json();
115
- },
116
- {
117
- onSuccess: () => {
118
- queryClient.invalidateQueries(['userDashboards', me?.username]);
119
- setIsEditing(false);
120
- },
121
- onError: (error) => {
122
- console.error("Erreur lors de la mise à jour du dashboard:", error);
123
- }
124
- }
125
- );
126
-
127
- const handleCreateDashboard = (e) => {
128
- e.preventDefault();
129
- if (newDashboardName.trim()) {
130
- createDashboardMutation.mutate(newDashboardName.trim());
131
- }
132
- };
133
-
134
- const handleUpdateDashboard = (e) => {
135
- e.preventDefault();
136
- if (dashboardToEdit.name.trim()) {
137
- updateDashboardMutation.mutate(dashboardToEdit);
138
- }
139
- };
140
-
141
- const handleEditFieldChange = (e) => {
142
- // e can be a standard event or an object { name, value } from custom fields
143
- const name = e.target ? e.target.name : e.name;
144
- const value = e.target ? e.target.value : e.value;
145
- setDashboardToEdit(prev => ({ ...prev, [name]: value }));
146
- };
147
-
148
- const handleSaveChart = (chartConfig) => {
149
- if (!selectedDashboard) return;
150
-
151
- const newLayout = [...(selectedDashboard.layout || [])];
152
-
153
- if (chartConfig.id) { // Mode édition
154
- const index = newLayout.findIndex(c => c.id === chartConfig.id);
155
- if (index > -1) {
156
- newLayout[index] = chartConfig;
157
- }
158
- } else { // Mode ajout
159
- newLayout.push({ ...chartConfig, id: new Date().getTime().toString() });
160
- }
161
-
162
- updateDashboardMutation.mutate({
163
- ...selectedDashboard,
164
- layout: newLayout
165
- });
166
-
167
- setIsChartModalOpen(false);
168
- setChartToConfigure(null);
169
- };
170
-
171
- const handleSaveFlexView = (flexViewConfig) => {
172
- if (!selectedDashboard) return;
173
-
174
- // Safely get the layout as a mutable array, creating a default section if needed.
175
- const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
176
-
177
- if (newLayout.length === 0) {
178
- newLayout.push({
179
- name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
180
- kpis: [],
181
- chartConfigs: [],
182
- flexViews: [],
183
- htmlViews: []
184
- });
185
- }
186
-
187
- // Add the widget to the first section
188
- const targetSection = newLayout[0];
189
- if (!targetSection.flexViews) {
190
- targetSection.flexViews = [];
191
- }
192
-
193
- targetSection.flexViews.push({
194
- ...flexViewConfig,
195
- type: 'flexView', // Add a type to distinguish from charts
196
- id: new Date().getTime().toString()
197
- });
198
-
199
- updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
200
- };
201
-
202
- const handleSaveHtmlView = (htmlViewConfig) => {
203
- if (!selectedDashboard) return;
204
-
205
- // Safely get the layout as a mutable array, creating a default section if needed.
206
- const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
207
-
208
- if (newLayout.length === 0) {
209
- newLayout.push({
210
- name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
211
- kpis: [],
212
- chartConfigs: [],
213
- flexViews: [],
214
- htmlViews: []
215
- });
216
- }
217
-
218
- // Add the widget to the first section
219
- const targetSection = newLayout[0];
220
- if (!targetSection.htmlViews) {
221
- targetSection.htmlViews = [];
222
- }
223
-
224
- // We only need to save the template and data-fetching info, not the data itself.
225
- const { data, ...configToSave } = htmlViewConfig;
226
-
227
- targetSection.htmlViews.push({
228
- ...configToSave,
229
- type: 'htmlView', // Add a type to distinguish
230
- id: `html-${Date.now()}`
231
- });
232
-
233
- updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
234
- };
235
-
236
- const [isLoading, setIsLoading] = useState(true);
237
-
238
- const refreshPresets = useMemo(() => [
239
- { label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
240
- { label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
241
- { label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
242
- { label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
243
- { label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
244
- { label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
245
- ], [t]);
246
-
247
- useEffect(() => {
248
- if( chartToAdd ){
249
- // Ouvre le modal de configuration avec les données du graphique de l'IA
250
- setChartToConfigure(chartToAdd);
251
- setIsChartModalOpen(true);
252
- setChartToAdd(null); // Réinitialise le contexte pour éviter de rouvrir le modal
253
- }
254
- }, [chartToAdd, setChartToAdd, selectedDashboard]);
255
-
256
- // NOUVEAU: Gérer l'ajout d'une vue flexible depuis le contexte UI
257
- useEffect(() => {
258
- if (flexViewToAdd && selectedDashboard) {
259
- handleSaveFlexView(flexViewToAdd);
260
- setFlexViewToAdd(null); // Réinitialiser le contexte
261
- }
262
- }, [flexViewToAdd, setFlexViewToAdd, selectedDashboard]);
263
-
264
- // Gérer l'ajout d'une vue HTML depuis le contexte UI
265
- useEffect(() => {
266
- if (htmlViewToAdd && selectedDashboard) {
267
- handleSaveHtmlView(htmlViewToAdd);
268
- setHtmlViewToAdd(null); // Réinitialiser le contexte
269
- }
270
- }, [htmlViewToAdd, setHtmlViewToAdd, selectedDashboard]);
271
-
272
- useEffect(() => {
273
- // When a new dashboard is selected, close the editing form
274
- setIsEditing(false);
275
- }, [selectedDashboardId]);
276
-
277
- useEffect(() => {
278
- // When the edit form is opened, initialize it with the selected dashboard's data
279
- if (isEditing && selectedDashboard) {
280
- setDashboardToEdit({ ...selectedDashboard });
281
- }
282
- }, [isEditing]);
283
-
284
- // 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
285
- useEffect(() => {
286
- if (dashboardsData?.data.length > 0) {
287
- if( hash ) {
288
- const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
289
- setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
290
- }else{
291
- const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
292
- if( !defaultDashboard) {
293
- setSelectedDashboardId(dashboardsData.data[0]._id);
294
- }else
295
- setSelectedDashboardId(defaultDashboard._id || null);
296
- }
297
- }
298
- setIsLoading(false);
299
- }, [hash,dashboardsData]);
300
-
301
- const dashboardOptions = useMemo(() => {
302
- return (dashboardsData?.data || []).map(db => ({
303
- label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
304
- value: db._id
305
- }));
306
- }, [dashboardsData, t]);
307
-
308
- const nav = useNavigate();
309
-
310
- useEffect(() => {
311
- setSelectedModel(null)
312
- }, []);
313
-
314
-
315
- if (isLoading) {
316
- return <div>{t('loading', 'Chargement...')}</div>;
317
- }
318
-
319
- if (!selectedDashboardId && hash) {
320
- return (
321
- <div className="dashboard-not-found">
322
- <h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
323
- <p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
324
- </div>
325
- );
326
- }
327
-
328
- const creationForm = (
329
- <form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
330
- <input
331
- type="text"
332
- className="input-fit"
333
- value={newDashboardName}
334
- onChange={(e) => setNewDashboardName(e.target.value)}
335
- placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
336
- disabled={createDashboardMutation.isLoading}
337
- autoFocus
338
- />
339
- <button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
340
- {createDashboardMutation.isLoading
341
- ? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
342
- : <Trans i18nKey="btns.create">Créer</Trans>
343
- }
344
- </button>
345
- {/* Bouton pour annuler quand on a cliqué sur le "+" */}
346
- {isCreating && (
347
- <button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
348
- <Trans i18nKey="btns.cancel">Annuler</Trans>
349
- </button>
350
- )}
351
- </form>
352
- );
353
-
354
- return (
355
- <div className="dashboards-page">
356
- <div className={"flex right actions"}>
357
- <Button onClick={() => {
358
- nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
359
- }}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
360
- </div>
361
- <h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
362
-
363
- {isLoadingDashboards && (
364
- <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
365
- )}
366
-
367
- {errorDashboards && (
368
- <p className="error">{errorDashboards.message}</p>
369
- )}
370
-
371
- {!isLoadingDashboards && !errorDashboards && (
372
- <>
373
- {dashboardsData?.data && dashboardsData.data.length > 0 ? (
374
- <div className="dashboard-selector-container">
375
- <div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
376
- <SelectField
377
- name="dashboardSelection"
378
- label={t('dashboards.select', 'Choisir un tableau de bord :')}
379
- value={selectedDashboardId}
380
- onChange={(selectedOption) => {
381
- setSelectedDashboardId(selectedOption.value);
382
- const d = dashboardsData.data.find(f => f._id === selectedOption.value);
383
- history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
384
- }}
385
- items={dashboardOptions}
386
- />
387
- <button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
388
- <FaPlus />
389
- </button>
390
- {selectedDashboard && (
391
- <button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
392
- <FaCog />
393
- </button>
394
- )}
395
- </div>
396
- {isCreating && creationForm}
397
- {isEditing && dashboardToEdit && (
398
- <form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
399
- <h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
400
- <TextField
401
- name="name"
402
- label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
403
- value={dashboardToEdit.name}
404
- onChange={handleEditFieldChange}
405
- required
406
- autoFocus
407
- />
408
-
409
- <div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
410
- <div style={{ flexGrow: 1 }}>
411
- <DurationField
412
- name="refreshInterval"
413
- label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
414
- value={dashboardToEdit.refreshInterval}
415
- onChange={handleEditFieldChange}
416
- help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
417
- />
418
- </div>
419
- <SelectField
420
- name="refreshIntervalPreset"
421
- label={t('dashboards.refreshPresets.label', 'Préréglages')}
422
- value={dashboardToEdit.refreshInterval}
423
- onChange={(option) => {
424
- console.log({option});
425
- if (option?.value)
426
- handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
427
- }}
428
- items={refreshPresets}
429
- style={{ minWidth: '150px' }}
430
- />
431
- </div>
432
- <div className="flex mg-t-1" style={{ gap: '8px' }}>
433
- <button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
434
- {updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
435
- </button>
436
- <button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
437
- <Trans i18nKey="btns.cancel">Annuler</Trans>
438
- </button>
439
- </div>
440
- </form>
441
- )}
442
- </div>
443
- ) : (
444
- <>
445
- <p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
446
- {creationForm}
447
- </>
448
- )}
449
-
450
- {/* Affiche la vue du dashboard sélectionné */}
451
- {/* Passe l'objet dashboard complet */}
452
- {/*
453
- TODO in DashboardView.jsx:
454
- Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
455
- Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
456
- doivent être rafraîchies automatiquement à cet intervalle.
457
- Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
458
- ou avec useEffect/setInterval:
459
- useEffect(() => {
460
- if (dashboard?.refreshInterval > 0) {
461
- const intervalId = setInterval(() => {
462
- // logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
463
- }, dashboard.refreshInterval * 1000);
464
- return () => clearInterval(intervalId);
465
- }
466
- }, [dashboard?.refreshInterval, queryClient]);
467
- */}
468
- <DashboardView dashboard={selectedDashboard} />
469
-
470
- {/* Modal pour ajouter/éditer un graphique */}
471
- <DialogProvider><ChartConfigModal
472
- isOpen={isChartModalOpen}
473
- onClose={() => setIsChartModalOpen(false)}
474
- onSave={handleSaveChart}
475
- initialConfig={chartToConfigure}
476
- /></DialogProvider>
477
- </>
478
- )}
479
- </div>
480
- );
1
+ import React, {useState, useMemo, useEffect} from 'react';
2
+ import { Trans, useTranslation } from 'react-i18next';
3
+ import {FaPlus, FaSpinner, FaCog} from "react-icons/fa"; // Ajout FaTrash
4
+
5
+ import "./Dashboard.scss"
6
+ import {useMutation, useQuery, useQueryClient} from "react-query";
7
+ import { useAuthContext } from "./contexts/AuthContext.jsx";
8
+ import { SelectField, DurationField, TextField } from "./Field.jsx";
9
+ import {DashboardView} from "./DashboardView.jsx";
10
+ import {useModelContext} from "./contexts/ModelContext.jsx";
11
+ import {useNavigate, useParams, useSearchParams} from "react-router-dom";
12
+ import {getObjectHash} from "../../src/core.js";
13
+ import {getUserHash, getUserId} from "../../src/data.js";
14
+ import Button from "./Button.jsx";
15
+ import {FaNoteSticky} from "react-icons/fa6";
16
+ import ChartConfigModal from "./ChartConfigModal.jsx";
17
+ import {useUI} from "./contexts/UIContext.jsx";
18
+ import {DialogProvider} from "./Dialog.jsx";
19
+
20
+ // --- DashboardsPage (Reste inchangé) ---
21
+ export function DashboardsPage() {
22
+ const { setSelectedModel, selectedModel } = useModelContext()
23
+ const { t } = useTranslation();
24
+ const { me } = useAuthContext();
25
+ const [selectedDashboardId, setSelectedDashboardId] = useState(null); // This seems to be the main state
26
+
27
+ const { chartToAdd, setChartToAdd, flexViewToAdd, setFlexViewToAdd, htmlViewToAdd, setHtmlViewToAdd } = useUI();
28
+ const [searchParams, setSearchParams] = useSearchParams();
29
+ const { hash } = useParams(); // 2. Récupérer le hash de l'URL
30
+
31
+ // ... après les autres hooks useState, useQuery, etc.
32
+ const queryClient = useQueryClient();
33
+ const [newDashboardName, setNewDashboardName] = useState('');
34
+ const [isCreating, setIsCreating] = useState(false);
35
+ const [isEditing, setIsEditing] = useState(false);
36
+ const [dashboardToEdit, setDashboardToEdit] = useState(null);
37
+
38
+ // États pour le modal de configuration des graphiques
39
+ const [isChartModalOpen, setIsChartModalOpen] = useState(false);
40
+ const [chartToConfigure, setChartToConfigure] = useState(null);
41
+
42
+ const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
43
+ ['userDashboards', me?.username],
44
+ async () => {
45
+ if (!me?.username) return null;
46
+ const response = await fetch(
47
+ `/api/data/search?model=dashboard&_user=${me.username}`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ // body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
51
+ });
52
+ if (!response.ok) {
53
+ const res = await response.json();
54
+ throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
55
+ }
56
+ return await response.json();
57
+ },
58
+ {
59
+ enabled: !!me?.username
60
+ }
61
+ );
62
+ const selectedDashboard = useMemo(() => {
63
+ if (!selectedDashboardId || !dashboardsData?.data) return null;
64
+ return dashboardsData.data.find(db => db._id === selectedDashboardId);
65
+ }, [selectedDashboardId, dashboardsData]);
66
+
67
+ const createDashboardMutation = useMutation(
68
+ async (dashboardName) => {
69
+ const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
70
+ const response = await fetch('/api/data', {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({
74
+ model: 'dashboard',
75
+ data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
76
+ })
77
+ });
78
+ if (!response.ok) {
79
+ const errorData = await response.json();
80
+ throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
81
+ }
82
+ return response.json();
83
+ },
84
+ {
85
+ onSuccess: () => {
86
+ // Rafraîchit la liste des dashboards après la création
87
+ queryClient.invalidateQueries(['userDashboards', me?.username]);
88
+ setNewDashboardName('');
89
+ setIsCreating(false);
90
+ },
91
+ onError: (error) => {
92
+ // Idéalement, afficher une notification à l'utilisateur
93
+ console.error("Erreur lors de la création du dashboard:", error);
94
+ }
95
+ }
96
+ );
97
+
98
+ const updateDashboardMutation = useMutation(
99
+ async (dashboardData) => {
100
+ const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
101
+ const response = await fetch(`/api/data`, {
102
+ method: 'PUT',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({
105
+ model: 'dashboard',
106
+ _id: _id,
107
+ data: data
108
+ })
109
+ });
110
+ if (!response.ok) {
111
+ const errorData = await response.json();
112
+ throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
113
+ }
114
+ return response.json();
115
+ },
116
+ {
117
+ onSuccess: () => {
118
+ queryClient.invalidateQueries(['userDashboards', me?.username]);
119
+ setIsEditing(false);
120
+ },
121
+ onError: (error) => {
122
+ console.error("Erreur lors de la mise à jour du dashboard:", error);
123
+ }
124
+ }
125
+ );
126
+
127
+ const handleCreateDashboard = (e) => {
128
+ e.preventDefault();
129
+ if (newDashboardName.trim()) {
130
+ createDashboardMutation.mutate(newDashboardName.trim());
131
+ }
132
+ };
133
+
134
+ const handleUpdateDashboard = (e) => {
135
+ e.preventDefault();
136
+ if (dashboardToEdit.name.trim()) {
137
+ updateDashboardMutation.mutate(dashboardToEdit);
138
+ }
139
+ };
140
+
141
+ const handleEditFieldChange = (e) => {
142
+ // e can be a standard event or an object { name, value } from custom fields
143
+ const name = e.target ? e.target.name : e.name;
144
+ const value = e.target ? e.target.value : e.value;
145
+ setDashboardToEdit(prev => ({ ...prev, [name]: value }));
146
+ };
147
+
148
+ const handleSaveChart = (chartConfig) => {
149
+ if (!selectedDashboard) return;
150
+
151
+ const newLayout = [...(selectedDashboard.layout || [])];
152
+
153
+ if (chartConfig.id) { // Mode édition
154
+ const index = newLayout.findIndex(c => c.id === chartConfig.id);
155
+ if (index > -1) {
156
+ newLayout[index] = chartConfig;
157
+ }
158
+ } else { // Mode ajout
159
+ newLayout.push({ ...chartConfig, id: new Date().getTime().toString() });
160
+ }
161
+
162
+ updateDashboardMutation.mutate({
163
+ ...selectedDashboard,
164
+ layout: newLayout
165
+ });
166
+
167
+ setIsChartModalOpen(false);
168
+ setChartToConfigure(null);
169
+ };
170
+
171
+ const handleSaveFlexView = (flexViewConfig) => {
172
+ if (!selectedDashboard) return;
173
+
174
+ // Safely get the layout as a mutable array, creating a default section if needed.
175
+ const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
176
+
177
+ if (newLayout.length === 0) {
178
+ newLayout.push({
179
+ name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
180
+ kpis: [],
181
+ chartConfigs: [],
182
+ flexViews: [],
183
+ htmlViews: []
184
+ });
185
+ }
186
+
187
+ // Add the widget to the first section
188
+ const targetSection = newLayout[0];
189
+ if (!targetSection.flexViews) {
190
+ targetSection.flexViews = [];
191
+ }
192
+
193
+ targetSection.flexViews.push({
194
+ ...flexViewConfig,
195
+ type: 'flexView', // Add a type to distinguish from charts
196
+ id: new Date().getTime().toString()
197
+ });
198
+
199
+ updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
200
+ };
201
+
202
+ const handleSaveHtmlView = (htmlViewConfig) => {
203
+ if (!selectedDashboard) return;
204
+
205
+ // Safely get the layout as a mutable array, creating a default section if needed.
206
+ const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
207
+
208
+ if (newLayout.length === 0) {
209
+ newLayout.push({
210
+ name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
211
+ kpis: [],
212
+ chartConfigs: [],
213
+ flexViews: [],
214
+ htmlViews: []
215
+ });
216
+ }
217
+
218
+ // Add the widget to the first section
219
+ const targetSection = newLayout[0];
220
+ if (!targetSection.htmlViews) {
221
+ targetSection.htmlViews = [];
222
+ }
223
+
224
+ // We only need to save the template and data-fetching info, not the data itself.
225
+ const { data, ...configToSave } = htmlViewConfig;
226
+
227
+ targetSection.htmlViews.push({
228
+ ...configToSave,
229
+ type: 'htmlView', // Add a type to distinguish
230
+ id: `html-${Date.now()}`
231
+ });
232
+
233
+ updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
234
+ };
235
+
236
+ const [isLoading, setIsLoading] = useState(true);
237
+
238
+ const refreshPresets = useMemo(() => [
239
+ { label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
240
+ { label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
241
+ { label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
242
+ { label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
243
+ { label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
244
+ { label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
245
+ ], [t]);
246
+
247
+ useEffect(() => {
248
+ if( chartToAdd ){
249
+ // Ouvre le modal de configuration avec les données du graphique de l'IA
250
+ setChartToConfigure(chartToAdd);
251
+ setIsChartModalOpen(true);
252
+ setChartToAdd(null); // Réinitialise le contexte pour éviter de rouvrir le modal
253
+ }
254
+ }, [chartToAdd, setChartToAdd, selectedDashboard]);
255
+
256
+ // NOUVEAU: Gérer l'ajout d'une vue flexible depuis le contexte UI
257
+ useEffect(() => {
258
+ if (flexViewToAdd && selectedDashboard) {
259
+ handleSaveFlexView(flexViewToAdd);
260
+ setFlexViewToAdd(null); // Réinitialiser le contexte
261
+ }
262
+ }, [flexViewToAdd, setFlexViewToAdd, selectedDashboard]);
263
+
264
+ // Gérer l'ajout d'une vue HTML depuis le contexte UI
265
+ useEffect(() => {
266
+ if (htmlViewToAdd && selectedDashboard) {
267
+ handleSaveHtmlView(htmlViewToAdd);
268
+ setHtmlViewToAdd(null); // Réinitialiser le contexte
269
+ }
270
+ }, [htmlViewToAdd, setHtmlViewToAdd, selectedDashboard]);
271
+
272
+ useEffect(() => {
273
+ // When a new dashboard is selected, close the editing form
274
+ setIsEditing(false);
275
+ }, [selectedDashboardId]);
276
+
277
+ useEffect(() => {
278
+ // When the edit form is opened, initialize it with the selected dashboard's data
279
+ if (isEditing && selectedDashboard) {
280
+ setDashboardToEdit({ ...selectedDashboard });
281
+ }
282
+ }, [isEditing]);
283
+
284
+ // 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
285
+ useEffect(() => {
286
+ if (dashboardsData?.data.length > 0) {
287
+ if( hash ) {
288
+ const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
289
+ setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
290
+ }else{
291
+ const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
292
+ if( !defaultDashboard) {
293
+ setSelectedDashboardId(dashboardsData.data[0]._id);
294
+ }else
295
+ setSelectedDashboardId(defaultDashboard._id || null);
296
+ }
297
+ }
298
+ setIsLoading(false);
299
+ }, [hash,dashboardsData]);
300
+
301
+ const dashboardOptions = useMemo(() => {
302
+ return (dashboardsData?.data || []).map(db => ({
303
+ label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
304
+ value: db._id
305
+ }));
306
+ }, [dashboardsData, t]);
307
+
308
+ const nav = useNavigate();
309
+
310
+ useEffect(() => {
311
+ setSelectedModel(null)
312
+ }, []);
313
+
314
+
315
+ if (isLoading) {
316
+ return <div>{t('loading', 'Chargement...')}</div>;
317
+ }
318
+
319
+ if (!selectedDashboardId && hash) {
320
+ return (
321
+ <div className="dashboard-not-found">
322
+ <h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
323
+ <p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
324
+ </div>
325
+ );
326
+ }
327
+
328
+ const creationForm = (
329
+ <form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
330
+ <input
331
+ type="text"
332
+ className="input-fit"
333
+ value={newDashboardName}
334
+ onChange={(e) => setNewDashboardName(e.target.value)}
335
+ placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
336
+ disabled={createDashboardMutation.isLoading}
337
+ autoFocus
338
+ />
339
+ <button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
340
+ {createDashboardMutation.isLoading
341
+ ? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
342
+ : <Trans i18nKey="btns.create">Créer</Trans>
343
+ }
344
+ </button>
345
+ {/* Bouton pour annuler quand on a cliqué sur le "+" */}
346
+ {isCreating && (
347
+ <button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
348
+ <Trans i18nKey="btns.cancel">Annuler</Trans>
349
+ </button>
350
+ )}
351
+ </form>
352
+ );
353
+
354
+ return (
355
+ <div className="dashboards-page">
356
+ <div className={"flex right actions"}>
357
+ <Button onClick={() => {
358
+ nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
359
+ }}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
360
+ </div>
361
+ <h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
362
+
363
+ {isLoadingDashboards && (
364
+ <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
365
+ )}
366
+
367
+ {errorDashboards && (
368
+ <p className="error">{errorDashboards.message}</p>
369
+ )}
370
+
371
+ {!isLoadingDashboards && !errorDashboards && (
372
+ <>
373
+ {dashboardsData?.data && dashboardsData.data.length > 0 ? (
374
+ <div className="dashboard-selector-container">
375
+ <div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
376
+ <SelectField
377
+ name="dashboardSelection"
378
+ label={t('dashboards.select', 'Choisir un tableau de bord :')}
379
+ value={selectedDashboardId}
380
+ onChange={(selectedOption) => {
381
+ setSelectedDashboardId(selectedOption.value);
382
+ const d = dashboardsData.data.find(f => f._id === selectedOption.value);
383
+ history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
384
+ }}
385
+ items={dashboardOptions}
386
+ />
387
+ <button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
388
+ <FaPlus />
389
+ </button>
390
+ {selectedDashboard && (
391
+ <button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
392
+ <FaCog />
393
+ </button>
394
+ )}
395
+ </div>
396
+ {isCreating && creationForm}
397
+ {isEditing && dashboardToEdit && (
398
+ <form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
399
+ <h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
400
+ <TextField
401
+ name="name"
402
+ label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
403
+ value={dashboardToEdit.name}
404
+ onChange={handleEditFieldChange}
405
+ required
406
+ autoFocus
407
+ />
408
+
409
+ <div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
410
+ <div style={{ flexGrow: 1 }}>
411
+ <DurationField
412
+ name="refreshInterval"
413
+ label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
414
+ value={dashboardToEdit.refreshInterval}
415
+ onChange={handleEditFieldChange}
416
+ help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
417
+ />
418
+ </div>
419
+ <SelectField
420
+ name="refreshIntervalPreset"
421
+ label={t('dashboards.refreshPresets.label', 'Préréglages')}
422
+ value={dashboardToEdit.refreshInterval}
423
+ onChange={(option) => {
424
+ console.log({option});
425
+ if (option?.value)
426
+ handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
427
+ }}
428
+ items={refreshPresets}
429
+ style={{ minWidth: '150px' }}
430
+ />
431
+ </div>
432
+ <div className="flex mg-t-1" style={{ gap: '8px' }}>
433
+ <button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
434
+ {updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
435
+ </button>
436
+ <button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
437
+ <Trans i18nKey="btns.cancel">Annuler</Trans>
438
+ </button>
439
+ </div>
440
+ </form>
441
+ )}
442
+ </div>
443
+ ) : (
444
+ <>
445
+ <p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
446
+ {creationForm}
447
+ </>
448
+ )}
449
+
450
+ {/* Affiche la vue du dashboard sélectionné */}
451
+ {/* Passe l'objet dashboard complet */}
452
+ {/*
453
+ TODO in DashboardView.jsx:
454
+ Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
455
+ Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
456
+ doivent être rafraîchies automatiquement à cet intervalle.
457
+ Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
458
+ ou avec useEffect/setInterval:
459
+ useEffect(() => {
460
+ if (dashboard?.refreshInterval > 0) {
461
+ const intervalId = setInterval(() => {
462
+ // logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
463
+ }, dashboard.refreshInterval * 1000);
464
+ return () => clearInterval(intervalId);
465
+ }
466
+ }, [dashboard?.refreshInterval, queryClient]);
467
+ */}
468
+ <DashboardView dashboard={selectedDashboard} />
469
+
470
+ {/* Modal pour ajouter/éditer un graphique */}
471
+ <DialogProvider><ChartConfigModal
472
+ isOpen={isChartModalOpen}
473
+ onClose={() => setIsChartModalOpen(false)}
474
+ onSave={handleSaveChart}
475
+ initialConfig={chartToConfigure}
476
+ /></DialogProvider>
477
+ </>
478
+ )}
479
+ </div>
480
+ );
481
481
  }