data-primals-engine 1.4.3 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +913 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/AddWidgetTypeModal.jsx +47 -43
  5. package/client/src/App.jsx +3 -7
  6. package/client/src/App.scss +25 -3
  7. package/client/src/AssistantChat.jsx +363 -323
  8. package/client/src/AssistantChat.scss +30 -12
  9. package/client/src/Dashboard.jsx +480 -396
  10. package/client/src/Dashboard.scss +1 -1
  11. package/client/src/DashboardHtmlViewItem.jsx +147 -0
  12. package/client/src/DashboardView.jsx +104 -19
  13. package/client/src/DataEditor.jsx +12 -5
  14. package/client/src/DataLayout.jsx +805 -762
  15. package/client/src/DataLayout.scss +14 -0
  16. package/client/src/DataTable.jsx +63 -77
  17. package/client/src/Dialog.scss +1 -1
  18. package/client/src/Field.jsx +591 -322
  19. package/client/src/FlexDataRenderer.jsx +2 -0
  20. package/client/src/FlexTreeUtils.js +1 -1
  21. package/client/src/FlexViewCard.jsx +44 -0
  22. package/client/src/HistoryDialog.jsx +47 -14
  23. package/client/src/HtmlViewBuilderModal.jsx +91 -0
  24. package/client/src/HtmlViewBuilderModal.scss +18 -0
  25. package/client/src/HtmlViewCard.jsx +44 -0
  26. package/client/src/HtmlViewCard.scss +35 -0
  27. package/client/src/KPIDialog.jsx +11 -1
  28. package/client/src/KanbanCard.jsx +1 -2
  29. package/client/src/ModelCreator.jsx +6 -6
  30. package/client/src/ModelCreatorField.jsx +74 -31
  31. package/client/src/ModelList.jsx +93 -54
  32. package/client/src/Notification.jsx +136 -136
  33. package/client/src/Notification.scss +0 -18
  34. package/client/src/Pagination.jsx +5 -3
  35. package/client/src/RelationField.jsx +354 -258
  36. package/client/src/RelationSelectorWidget.jsx +173 -0
  37. package/client/src/constants.js +1 -1
  38. package/client/src/contexts/ModelContext.jsx +10 -1
  39. package/client/src/contexts/UIContext.jsx +72 -63
  40. package/client/src/filter.js +262 -212
  41. package/client/src/hooks/useTutorials.jsx +62 -65
  42. package/client/src/hooks/useValidation.js +75 -0
  43. package/client/src/translations.js +26 -24
  44. package/package.json +3 -1
  45. package/perf/README.md +147 -0
  46. package/perf/artillery-hooks.js +37 -0
  47. package/perf/perf-shot-hardwork.yml +84 -0
  48. package/perf/perf-shot-search.yml +45 -0
  49. package/perf/setup.yml +26 -0
  50. package/server.js +1 -1
  51. package/src/constants.js +3 -28
  52. package/src/core.js +15 -1
  53. package/src/data.js +1 -1
  54. package/src/defaultModels.js +63 -7
  55. package/src/email.js +5 -2
  56. package/src/engine.js +5 -3
  57. package/src/filter.js +5 -3
  58. package/src/i18n.js +710 -10
  59. package/src/modules/assistant/assistant.js +151 -19
  60. package/src/modules/bucket.js +14 -16
  61. package/src/modules/data/data.backup.js +11 -8
  62. package/src/modules/data/data.core.js +118 -92
  63. package/src/modules/data/data.history.js +531 -492
  64. package/src/modules/data/data.js +9 -56
  65. package/src/modules/data/data.operations.js +3282 -2999
  66. package/src/modules/data/data.relations.js +686 -686
  67. package/src/modules/data/data.routes.js +118 -24
  68. package/src/modules/data/data.scheduling.js +2 -1
  69. package/src/modules/data/data.validation.js +85 -3
  70. package/src/modules/file.js +247 -236
  71. package/src/modules/user.js +4 -1
  72. package/src/modules/workflow.js +9 -10
  73. package/src/openai.jobs.js +2 -0
  74. package/src/packs.js +5482 -5461
  75. package/src/providers.js +22 -7
  76. package/test/data.integration.test.js +136 -2
  77. package/test/import_export.integration.test.js +1 -1
@@ -1,397 +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);
26
-
27
- const { chartToAdd, setChartToAdd } = 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 createDashboardMutation = useMutation(
43
- async (dashboardName) => {
44
- const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
45
- const response = await fetch('/api/data', {
46
- method: 'POST',
47
- headers: { 'Content-Type': 'application/json' },
48
- body: JSON.stringify({
49
- model: 'dashboard',
50
- data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
51
- })
52
- });
53
- if (!response.ok) {
54
- const errorData = await response.json();
55
- throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
56
- }
57
- return response.json();
58
- },
59
- {
60
- onSuccess: () => {
61
- // Rafraîchit la liste des dashboards après la création
62
- queryClient.invalidateQueries(['userDashboards', me?.username]);
63
- setNewDashboardName('');
64
- setIsCreating(false);
65
- },
66
- onError: (error) => {
67
- // Idéalement, afficher une notification à l'utilisateur
68
- console.error("Erreur lors de la création du dashboard:", error);
69
- }
70
- }
71
- );
72
-
73
- const updateDashboardMutation = useMutation(
74
- async (dashboardData) => {
75
- const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
76
- const response = await fetch(`/api/data`, {
77
- method: 'PUT',
78
- headers: { 'Content-Type': 'application/json' },
79
- body: JSON.stringify({
80
- model: 'dashboard',
81
- _id: _id,
82
- data: data
83
- })
84
- });
85
- if (!response.ok) {
86
- const errorData = await response.json();
87
- throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
88
- }
89
- return response.json();
90
- },
91
- {
92
- onSuccess: () => {
93
- queryClient.invalidateQueries(['userDashboards', me?.username]);
94
- setIsEditing(false);
95
- },
96
- onError: (error) => {
97
- console.error("Erreur lors de la mise à jour du dashboard:", error);
98
- }
99
- }
100
- );
101
-
102
- const handleCreateDashboard = (e) => {
103
- e.preventDefault();
104
- if (newDashboardName.trim()) {
105
- createDashboardMutation.mutate(newDashboardName.trim());
106
- }
107
- };
108
-
109
- const handleUpdateDashboard = (e) => {
110
- e.preventDefault();
111
- if (dashboardToEdit.name.trim()) {
112
- updateDashboardMutation.mutate(dashboardToEdit);
113
- }
114
- };
115
-
116
- const handleEditFieldChange = (e) => {
117
- // e can be a standard event or an object { name, value } from custom fields
118
- const name = e.target ? e.target.name : e.name;
119
- const value = e.target ? e.target.value : e.value;
120
- setDashboardToEdit(prev => ({ ...prev, [name]: value }));
121
- };
122
-
123
- const handleSaveChart = (chartConfig) => {
124
- if (!selectedDashboard) return;
125
-
126
- const newLayout = [...(selectedDashboard.layout || [])];
127
-
128
- if (chartConfig.id) { // Mode édition
129
- const index = newLayout.findIndex(c => c.id === chartConfig.id);
130
- if (index > -1) {
131
- newLayout[index] = chartConfig;
132
- }
133
- } else { // Mode ajout
134
- newLayout.push({ ...chartConfig, id: new Date().getTime().toString() });
135
- }
136
-
137
- updateDashboardMutation.mutate({
138
- ...selectedDashboard,
139
- layout: newLayout
140
- });
141
-
142
- setIsChartModalOpen(false);
143
- setChartToConfigure(null);
144
- };
145
- const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
146
- ['userDashboards', me?.username],
147
- async () => {
148
- if (!me?.username) return null;
149
- const response = await fetch(
150
- `/api/data/search?model=dashboard&_user=${me.username}`, {
151
- method: 'POST',
152
- headers: { 'Content-Type': 'application/json' },
153
- // body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
154
- });
155
- if (!response.ok) {
156
- const res = await response.json();
157
- throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
158
- }
159
- return await response.json();
160
- },
161
- {
162
- enabled: !!me?.username
163
- }
164
- );
165
- const [isLoading, setIsLoading] = useState(true);
166
-
167
- const refreshPresets = useMemo(() => [
168
- { label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
169
- { label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
170
- { label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
171
- { label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
172
- { label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
173
- { label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
174
- ], [t]);
175
-
176
- useEffect(() => {
177
- if( chartToAdd ){
178
- // Ouvre le modal de configuration avec les données du graphique de l'IA
179
- setChartToConfigure(chartToAdd);
180
- setIsChartModalOpen(true);
181
- setChartToAdd(null); // Réinitialise le contexte pour éviter de rouvrir le modal
182
- }
183
- }, [chartToAdd, setChartToAdd]);
184
- useEffect(() => {
185
- // When a new dashboard is selected, close the editing form
186
- setIsEditing(false);
187
- }, [selectedDashboardId]);
188
-
189
- useEffect(() => {
190
- // When the edit form is opened, initialize it with the selected dashboard's data
191
- if (isEditing && selectedDashboard) {
192
- setDashboardToEdit({ ...selectedDashboard });
193
- }
194
- }, [isEditing]);
195
-
196
- // 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
197
- useEffect(() => {
198
- if (dashboardsData?.data.length > 0) {
199
- if( hash ) {
200
- const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
201
- setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
202
- }else{
203
- const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
204
- if( !defaultDashboard) {
205
- setSelectedDashboardId(dashboardsData.data[0]._id);
206
- }else
207
- setSelectedDashboardId(defaultDashboard._id || null);
208
- }
209
- }
210
- setIsLoading(false);
211
- }, [hash,dashboardsData]);
212
- const selectedDashboard = useMemo(() => {
213
- if (!selectedDashboardId || !dashboardsData?.data) return null;
214
- return dashboardsData.data.find(db => db._id === selectedDashboardId);
215
- }, [selectedDashboardId, dashboardsData]);
216
-
217
- const dashboardOptions = useMemo(() => {
218
- return (dashboardsData?.data || []).map(db => ({
219
- label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
220
- value: db._id
221
- }));
222
- }, [dashboardsData, t]);
223
-
224
- const nav = useNavigate();
225
-
226
- useEffect(() => {
227
- setSelectedModel(null)
228
- }, []);
229
-
230
-
231
- if (isLoading) {
232
- return <div>{t('loading', 'Chargement...')}</div>;
233
- }
234
-
235
- if (!selectedDashboardId && hash) {
236
- return (
237
- <div className="dashboard-not-found">
238
- <h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
239
- <p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
240
- </div>
241
- );
242
- }
243
-
244
- const creationForm = (
245
- <form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
246
- <input
247
- type="text"
248
- className="input-fit"
249
- value={newDashboardName}
250
- onChange={(e) => setNewDashboardName(e.target.value)}
251
- placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
252
- disabled={createDashboardMutation.isLoading}
253
- autoFocus
254
- />
255
- <button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
256
- {createDashboardMutation.isLoading
257
- ? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
258
- : <Trans i18nKey="btns.create">Créer</Trans>
259
- }
260
- </button>
261
- {/* Bouton pour annuler quand on a cliqué sur le "+" */}
262
- {isCreating && (
263
- <button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
264
- <Trans i18nKey="btns.cancel">Annuler</Trans>
265
- </button>
266
- )}
267
- </form>
268
- );
269
-
270
- return (
271
- <div className="dashboards-page">
272
- <div className={"flex right actions"}>
273
- <Button onClick={() => {
274
- nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
275
- }}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
276
- </div>
277
- <h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
278
-
279
- {isLoadingDashboards && (
280
- <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
281
- )}
282
-
283
- {errorDashboards && (
284
- <p className="error">{errorDashboards.message}</p>
285
- )}
286
-
287
- {!isLoadingDashboards && !errorDashboards && (
288
- <>
289
- {dashboardsData?.data && dashboardsData.data.length > 0 ? (
290
- <div className="dashboard-selector-container">
291
- <div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
292
- <SelectField
293
- name="dashboardSelection"
294
- label={t('dashboards.select', 'Choisir un tableau de bord :')}
295
- value={selectedDashboardId}
296
- onChange={(selectedOption) => {
297
- setSelectedDashboardId(selectedOption.value);
298
- const d = dashboardsData.data.find(f => f._id === selectedOption.value);
299
- history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
300
- }}
301
- items={dashboardOptions}
302
- />
303
- <button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
304
- <FaPlus />
305
- </button>
306
- {selectedDashboard && (
307
- <button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
308
- <FaCog />
309
- </button>
310
- )}
311
- </div>
312
- {isCreating && creationForm}
313
- {isEditing && dashboardToEdit && (
314
- <form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
315
- <h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
316
- <TextField
317
- name="name"
318
- label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
319
- value={dashboardToEdit.name}
320
- onChange={handleEditFieldChange}
321
- required
322
- autoFocus
323
- />
324
-
325
- <div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
326
- <div style={{ flexGrow: 1 }}>
327
- <DurationField
328
- name="refreshInterval"
329
- label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
330
- value={dashboardToEdit.refreshInterval}
331
- onChange={handleEditFieldChange}
332
- help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
333
- />
334
- </div>
335
- <SelectField
336
- name="refreshIntervalPreset"
337
- label={t('dashboards.refreshPresets.label', 'Préréglages')}
338
- value={dashboardToEdit.refreshInterval}
339
- onChange={(option) => {
340
- console.log({option});
341
- if (option?.value)
342
- handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
343
- }}
344
- items={refreshPresets}
345
- style={{ minWidth: '150px' }}
346
- />
347
- </div>
348
- <div className="flex mg-t-1" style={{ gap: '8px' }}>
349
- <button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
350
- {updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
351
- </button>
352
- <button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
353
- <Trans i18nKey="btns.cancel">Annuler</Trans>
354
- </button>
355
- </div>
356
- </form>
357
- )}
358
- </div>
359
- ) : (
360
- <>
361
- <p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
362
- {creationForm}
363
- </>
364
- )}
365
-
366
- {/* Affiche la vue du dashboard sélectionné */}
367
- {/* Passe l'objet dashboard complet */}
368
- {/*
369
- TODO in DashboardView.jsx:
370
- Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
371
- Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
372
- doivent être rafraîchies automatiquement à cet intervalle.
373
- Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
374
- ou avec useEffect/setInterval:
375
- useEffect(() => {
376
- if (dashboard?.refreshInterval > 0) {
377
- const intervalId = setInterval(() => {
378
- // logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
379
- }, dashboard.refreshInterval * 1000);
380
- return () => clearInterval(intervalId);
381
- }
382
- }, [dashboard?.refreshInterval, queryClient]);
383
- */}
384
- <DashboardView dashboard={selectedDashboard} />
385
-
386
- {/* Modal pour ajouter/éditer un graphique */}
387
- <DialogProvider><ChartConfigModal
388
- isOpen={isChartModalOpen}
389
- onClose={() => setIsChartModalOpen(false)}
390
- onSave={handleSaveChart}
391
- initialConfig={chartToConfigure}
392
- /></DialogProvider>
393
- </>
394
- )}
395
- </div>
396
- );
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
+ );
397
481
  }