data-primals-engine 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,350 +1,397 @@
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
-
17
- // --- DashboardsPage (Reste inchangé) ---
18
- export function DashboardsPage() {
19
- const { setSelectedModel, selectedModel } = useModelContext()
20
- const { t } = useTranslation();
21
- const { me } = useAuthContext();
22
- const [selectedDashboardId, setSelectedDashboardId] = useState(null);
23
-
24
- const [searchParams, setSearchParams] = useSearchParams();
25
- const { hash } = useParams(); // 2. Récupérer le hash de l'URL
26
-
27
- // ... après les autres hooks useState, useQuery, etc.
28
- const queryClient = useQueryClient();
29
- const [newDashboardName, setNewDashboardName] = useState('');
30
- const [isCreating, setIsCreating] = useState(false);
31
- const [isEditing, setIsEditing] = useState(false);
32
- const [dashboardToEdit, setDashboardToEdit] = useState(null);
33
-
34
- const createDashboardMutation = useMutation(
35
- async (dashboardName) => {
36
- const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
37
- const response = await fetch('/api/data', {
38
- method: 'POST',
39
- headers: { 'Content-Type': 'application/json' },
40
- body: JSON.stringify({
41
- model: 'dashboard',
42
- data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
43
- })
44
- });
45
- if (!response.ok) {
46
- const errorData = await response.json();
47
- throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
48
- }
49
- return response.json();
50
- },
51
- {
52
- onSuccess: () => {
53
- // Rafraîchit la liste des dashboards après la création
54
- queryClient.invalidateQueries(['userDashboards', me?.username]);
55
- setNewDashboardName('');
56
- setIsCreating(false);
57
- },
58
- onError: (error) => {
59
- // Idéalement, afficher une notification à l'utilisateur
60
- console.error("Erreur lors de la création du dashboard:", error);
61
- }
62
- }
63
- );
64
-
65
- const updateDashboardMutation = useMutation(
66
- async (dashboardData) => {
67
- const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
68
- const response = await fetch(`/api/data`, {
69
- method: 'PUT',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify({
72
- model: 'dashboard',
73
- _id: _id,
74
- data: data
75
- })
76
- });
77
- if (!response.ok) {
78
- const errorData = await response.json();
79
- throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
80
- }
81
- return response.json();
82
- },
83
- {
84
- onSuccess: () => {
85
- queryClient.invalidateQueries(['userDashboards', me?.username]);
86
- setIsEditing(false);
87
- },
88
- onError: (error) => {
89
- console.error("Erreur lors de la mise à jour du dashboard:", error);
90
- }
91
- }
92
- );
93
-
94
- const handleCreateDashboard = (e) => {
95
- e.preventDefault();
96
- if (newDashboardName.trim()) {
97
- createDashboardMutation.mutate(newDashboardName.trim());
98
- }
99
- };
100
-
101
- const handleUpdateDashboard = (e) => {
102
- e.preventDefault();
103
- if (dashboardToEdit.name.trim()) {
104
- updateDashboardMutation.mutate(dashboardToEdit);
105
- }
106
- };
107
-
108
- const handleEditFieldChange = (e) => {
109
- // e can be a standard event or an object { name, value } from custom fields
110
- const name = e.target ? e.target.name : e.name;
111
- const value = e.target ? e.target.value : e.value;
112
- setDashboardToEdit(prev => ({ ...prev, [name]: value }));
113
- };
114
-
115
- const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
116
- ['userDashboards', me?.username],
117
- async () => {
118
- if (!me?.username) return null;
119
- const response = await fetch(
120
- `/api/data/search?model=dashboard&_user=${me.username}`, {
121
- method: 'POST',
122
- headers: { 'Content-Type': 'application/json' },
123
- // body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
124
- });
125
- if (!response.ok) {
126
- const res = await response.json();
127
- throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
128
- }
129
- return await response.json();
130
- },
131
- {
132
- enabled: !!me?.username
133
- }
134
- );
135
- const [isLoading, setIsLoading] = useState(true);
136
-
137
- const refreshPresets = useMemo(() => [
138
- { label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
139
- { label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
140
- { label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
141
- { label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
142
- { label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
143
- { label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
144
- ], [t]);
145
- useEffect(() => {
146
- // When a new dashboard is selected, close the editing form
147
- setIsEditing(false);
148
- }, [selectedDashboardId]);
149
-
150
- useEffect(() => {
151
- // When the edit form is opened, initialize it with the selected dashboard's data
152
- if (isEditing && selectedDashboard) {
153
- setDashboardToEdit({ ...selectedDashboard });
154
- }
155
- }, [isEditing]);
156
-
157
- // 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
158
- useEffect(() => {
159
- if (dashboardsData?.data.length > 0) {
160
- if( hash ) {
161
- const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
162
- setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
163
- }else{
164
- const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
165
- if( !defaultDashboard) {
166
- setSelectedDashboardId(dashboardsData.data[0]._id);
167
- }else
168
- setSelectedDashboardId(defaultDashboard._id || null);
169
- }
170
- }
171
- setIsLoading(false);
172
- }, [hash,dashboardsData]);
173
- const selectedDashboard = useMemo(() => {
174
- if (!selectedDashboardId || !dashboardsData?.data) return null;
175
- return dashboardsData.data.find(db => db._id === selectedDashboardId);
176
- }, [selectedDashboardId, dashboardsData]);
177
-
178
- const dashboardOptions = useMemo(() => {
179
- return (dashboardsData?.data || []).map(db => ({
180
- label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
181
- value: db._id
182
- }));
183
- }, [dashboardsData, t]);
184
-
185
- const nav = useNavigate();
186
-
187
- useEffect(() => {
188
- setSelectedModel(null)
189
- }, []);
190
-
191
-
192
- if (isLoading) {
193
- return <div>{t('loading', 'Chargement...')}</div>;
194
- }
195
-
196
- if (!selectedDashboardId && hash) {
197
- return (
198
- <div className="dashboard-not-found">
199
- <h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
200
- <p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
201
- </div>
202
- );
203
- }
204
-
205
- const creationForm = (
206
- <form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
207
- <input
208
- type="text"
209
- className="input-fit"
210
- value={newDashboardName}
211
- onChange={(e) => setNewDashboardName(e.target.value)}
212
- placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
213
- disabled={createDashboardMutation.isLoading}
214
- autoFocus
215
- />
216
- <button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
217
- {createDashboardMutation.isLoading
218
- ? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
219
- : <Trans i18nKey="btns.create">Créer</Trans>
220
- }
221
- </button>
222
- {/* Bouton pour annuler quand on a cliqué sur le "+" */}
223
- {isCreating && (
224
- <button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
225
- <Trans i18nKey="btns.cancel">Annuler</Trans>
226
- </button>
227
- )}
228
- </form>
229
- );
230
-
231
- return (
232
- <div className="dashboards-page">
233
- <div className={"flex right actions"}>
234
- <Button onClick={() => {
235
- nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
236
- }}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
237
- </div>
238
- <h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
239
-
240
- {isLoadingDashboards && (
241
- <p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
242
- )}
243
-
244
- {errorDashboards && (
245
- <p className="error">{errorDashboards.message}</p>
246
- )}
247
-
248
- {!isLoadingDashboards && !errorDashboards && (
249
- <>
250
- {dashboardsData?.data && dashboardsData.data.length > 0 ? (
251
- <div className="dashboard-selector-container">
252
- <div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
253
- <SelectField
254
- name="dashboardSelection"
255
- label={t('dashboards.select', 'Choisir un tableau de bord :')}
256
- value={selectedDashboardId}
257
- onChange={(selectedOption) => {
258
- setSelectedDashboardId(selectedOption.value);
259
- const d = dashboardsData.data.find(f => f._id === selectedOption.value);
260
- history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
261
- }}
262
- items={dashboardOptions}
263
- />
264
- <button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
265
- <FaPlus />
266
- </button>
267
- {selectedDashboard && (
268
- <button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
269
- <FaCog />
270
- </button>
271
- )}
272
- </div>
273
- {isCreating && creationForm}
274
- {isEditing && dashboardToEdit && (
275
- <form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
276
- <h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
277
- <TextField
278
- name="name"
279
- label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
280
- value={dashboardToEdit.name}
281
- onChange={handleEditFieldChange}
282
- required
283
- autoFocus
284
- />
285
-
286
- <div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
287
- <div style={{ flexGrow: 1 }}>
288
- <DurationField
289
- name="refreshInterval"
290
- label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
291
- value={dashboardToEdit.refreshInterval}
292
- onChange={handleEditFieldChange}
293
- help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
294
- />
295
- </div>
296
- <SelectField
297
- name="refreshIntervalPreset"
298
- label={t('dashboards.refreshPresets.label', 'Préréglages')}
299
- value={dashboardToEdit.refreshInterval}
300
- onChange={(option) => {
301
- console.log({option});
302
- if (option?.value)
303
- handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
304
- }}
305
- items={refreshPresets}
306
- style={{ minWidth: '150px' }}
307
- />
308
- </div>
309
- <div className="flex mg-t-1" style={{ gap: '8px' }}>
310
- <button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
311
- {updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
312
- </button>
313
- <button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
314
- <Trans i18nKey="btns.cancel">Annuler</Trans>
315
- </button>
316
- </div>
317
- </form>
318
- )}
319
- </div>
320
- ) : (
321
- <>
322
- <p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
323
- {creationForm}
324
- </>
325
- )}
326
-
327
- {/* Affiche la vue du dashboard sélectionné */}
328
- {/* Passe l'objet dashboard complet */}
329
- {/*
330
- TODO in DashboardView.jsx:
331
- Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
332
- Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
333
- doivent être rafraîchies automatiquement à cet intervalle.
334
- Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
335
- ou avec useEffect/setInterval:
336
- useEffect(() => {
337
- if (dashboard?.refreshInterval > 0) {
338
- const intervalId = setInterval(() => {
339
- // logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
340
- }, dashboard.refreshInterval * 1000);
341
- return () => clearInterval(intervalId);
342
- }
343
- }, [dashboard?.refreshInterval, queryClient]);
344
- */}
345
- <DashboardView dashboard={selectedDashboard} />
346
- </>
347
- )}
348
- </div>
349
- );
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
+ );
350
397
  }