data-primals-engine 1.0.10 → 1.1.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.
- package/client/README.md +8 -0
- package/client/index.html +31 -0
- package/client/index.js +25 -0
- package/client/package-lock.json +11664 -0
- package/client/package.json +70 -0
- package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
- package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
- package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
- package/client/public/doc/API.postman_collection.json +214 -0
- package/client/public/github.svg +1 -0
- package/client/public/profilCompany.jpg +0 -0
- package/client/public/profilDeveloper.jpg +0 -0
- package/client/public/profilEngineer.jpg +0 -0
- package/client/public/profilPersonal.jpg +0 -0
- package/client/public/profils .psd +0 -0
- package/client/public/react.svg +9 -0
- package/client/src/APIInfo.jsx +328 -0
- package/client/src/AddWidgetTypeModal.jsx +44 -0
- package/client/src/AddWidgetTypeModal.scss +87 -0
- package/client/src/App.css +42 -0
- package/client/src/App.jsx +586 -0
- package/client/src/App.scss +1466 -0
- package/client/src/AssistantChat.jsx +280 -0
- package/client/src/AssistantChat.scss +229 -0
- package/client/src/Button.jsx +12 -0
- package/client/src/Button.scss +197 -0
- package/client/src/CalculationBuilder.jsx +338 -0
- package/client/src/CalculationStepRow.jsx +199 -0
- package/client/src/CalendarConfigModal.jsx +49 -0
- package/client/src/ChartConfigModal.jsx +402 -0
- package/client/src/ConditionBuilder.jsx +622 -0
- package/client/src/ConditionBuilder.scss +672 -0
- package/client/src/ConditionBuilder2.jsx +757 -0
- package/client/src/ContentView.jsx +274 -0
- package/client/src/CronBuilder.jsx +126 -0
- package/client/src/CronBuilder.scss +128 -0
- package/client/src/CronPartBuilder.jsx +150 -0
- package/client/src/Dashboard.jsx +210 -0
- package/client/src/Dashboard.scss +445 -0
- package/client/src/DashboardChart.jsx +362 -0
- package/client/src/DashboardFlexViewItem.jsx +129 -0
- package/client/src/DashboardView.jsx +554 -0
- package/client/src/DataChart.jsx +349 -0
- package/client/src/DataEditor.jsx +556 -0
- package/client/src/DataLayout.jsx +709 -0
- package/client/src/DataTable.jsx +996 -0
- package/client/src/DataTable.scss +73 -0
- package/client/src/Dialog.jsx +90 -0
- package/client/src/Dialog.scss +117 -0
- package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
- package/client/src/DocumentationPageLayout.jsx +93 -0
- package/client/src/DocumentationPageLayout.scss +262 -0
- package/client/src/Draggable.jsx +222 -0
- package/client/src/Draggable.scss +49 -0
- package/client/src/ExportDialog.jsx +172 -0
- package/client/src/Field.jsx +1558 -0
- package/client/src/FlexBuilder.jsx +269 -0
- package/client/src/FlexBuilder.scss +226 -0
- package/client/src/FlexBuilderControls.jsx +170 -0
- package/client/src/FlexBuilderModal.jsx +75 -0
- package/client/src/FlexBuilderModal.scss +109 -0
- package/client/src/FlexBuilderPreview.jsx +48 -0
- package/client/src/FlexBuilderUtils.js +84 -0
- package/client/src/FlexDataRenderer.jsx +170 -0
- package/client/src/FlexDataRenderer.scss +80 -0
- package/client/src/FlexNode.jsx +290 -0
- package/client/src/FlexTreeUtils.js +129 -0
- package/client/src/HiddenableCell.jsx +46 -0
- package/client/src/HiddenableCell.scss +36 -0
- package/client/src/KPIDialog.jsx +39 -0
- package/client/src/KPIWidget.jsx +139 -0
- package/client/src/KanbanCard.jsx +92 -0
- package/client/src/KanbanColumn.jsx +29 -0
- package/client/src/KanbanConfigModal.jsx +115 -0
- package/client/src/KanbanView.jsx +113 -0
- package/client/src/KanbanView.scss +107 -0
- package/client/src/MessageRotator.jsx +77 -0
- package/client/src/MessageRotator.scss +14 -0
- package/client/src/ModelCreator.jsx +640 -0
- package/client/src/ModelCreator.scss +154 -0
- package/client/src/ModelCreatorField.jsx +810 -0
- package/client/src/ModelImporter.jsx +120 -0
- package/client/src/ModelList.jsx +205 -0
- package/client/src/Notification.jsx +137 -0
- package/client/src/Notification.scss +126 -0
- package/client/src/NotificationProvider.jsx +73 -0
- package/client/src/PackGallery.jsx +211 -0
- package/client/src/PackGallery.scss +156 -0
- package/client/src/Pagination.jsx +141 -0
- package/client/src/RTE.jsx +415 -0
- package/client/src/RTETrans.jsx +103 -0
- package/client/src/RelationField.jsx +247 -0
- package/client/src/RelationValue.jsx +230 -0
- package/client/src/RestoreConfirmationModal.jsx +183 -0
- package/client/src/RestoreDialog.jsx +134 -0
- package/client/src/RestoreDialog.scss +67 -0
- package/client/src/RichTextEditorModal.jsx +34 -0
- package/client/src/RichTextEditorModal.scss +54 -0
- package/client/src/TourSpotlight.jsx +234 -0
- package/client/src/TourSpotlight.scss +100 -0
- package/client/src/TutorialsMenu.jsx +83 -0
- package/client/src/TutorialsMenu.scss +217 -0
- package/client/src/TutorialsRewardModal.jsx +65 -0
- package/client/src/Utils.jsx +78 -0
- package/client/src/ViewSwitcher.jsx +54 -0
- package/client/src/ViewSwitcher.scss +74 -0
- package/client/src/Webpage.jsx +69 -0
- package/client/src/_variables.scss +21 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/constants.js +68 -0
- package/client/src/contexts/AuthContext.jsx +28 -0
- package/client/src/contexts/ModelContext.jsx +328 -0
- package/client/src/contexts/UIContext.jsx +52 -0
- package/client/src/core/data.js +35 -0
- package/client/src/entry-client.jsx +15 -0
- package/client/src/entry-server.jsx +13 -0
- package/client/src/filter.js +222 -0
- package/client/src/hooks/data.js +101 -0
- package/client/src/hooks/useDebounce.js +20 -0
- package/client/src/hooks/useDragAndDrop.js +59 -0
- package/client/src/hooks/useLocalStorage.js +100 -0
- package/client/src/hooks/useMount.js +42 -0
- package/client/src/hooks/useTutorials.jsx +227 -0
- package/client/src/hooks/useWindowSize.js +24 -0
- package/client/src/i18n.js +47 -0
- package/client/src/index.css +90 -0
- package/client/src/index.js +10 -0
- package/client/src/main.jsx +11 -0
- package/client/src/translations.js +15940 -0
- package/client/src/tutorials.js +112 -0
- package/client/vite.config.js +30 -0
- package/eslint.config.js +50 -0
- package/package.json +48 -27
- package/server.js +9 -0
- package/src/constants.js +12 -23
- package/src/core.js +9 -8
- package/src/data.js +81 -80
- package/src/defaultModels.js +114 -115
- package/src/email.js +8 -9
- package/src/engine.js +5 -5
- package/src/events.js +1 -2
- package/src/i18n.js +5390 -211
- package/src/middlewares/middleware-mongodb.js +94 -93
- package/src/migrate.js +24 -24
- package/src/modules/assistant.js +55 -55
- package/src/modules/bucket.js +10 -9
- package/src/modules/data.js +387 -357
- package/src/modules/file.js +3 -6
- package/src/modules/mongodb.js +6 -7
- package/src/modules/user.js +24 -47
- package/src/modules/workflow.js +68 -67
- package/src/openai.jobs.js +2 -2
- package/src/packs.js +30 -31
- package/src/providers.js +101 -3
- package/src/setenv.js +1 -1
- package/src/user.js +0 -17
- package/src/workers/crypto-worker.js +21 -20
- package/src/workers/import-export-worker.js +18 -18
- package/test/data.backup.integration.test.js +7 -12
- package/test/data.integration.test.js +27 -25
- package/test/globalSetup.js +2 -2
- package/test/import_export.integration.test.js +6 -6
- package/test/model.integration.test.js +21 -22
- package/test/workflow.integration.test.js +2 -5
- package/test/workflow.robustness.test.js +6 -13
- package/vitest.config.js +2 -2
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/DashboardChart.jsx
|
|
2
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
3
|
+
import { fr } from 'date-fns/locale'
|
|
4
|
+
import {
|
|
5
|
+
Chart as ChartJS,
|
|
6
|
+
CategoryScale, LinearScale, BarElement, LineElement,
|
|
7
|
+
PointElement, ArcElement, Title, Tooltip, Legend, TimeScale, Filler,
|
|
8
|
+
} from 'chart.js';
|
|
9
|
+
import { Bar, Line, Pie, Doughnut } from 'react-chartjs-2';
|
|
10
|
+
import { useQuery } from 'react-query';
|
|
11
|
+
import { useTranslation } from 'react-i18next';
|
|
12
|
+
import { FaSpinner, FaExclamationTriangle } from 'react-icons/fa';
|
|
13
|
+
import i18n from 'i18next';
|
|
14
|
+
|
|
15
|
+
import 'chartjs-adapter-date-fns';
|
|
16
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
17
|
+
import {isDate} from "data-primals-engine/core";
|
|
18
|
+
import useWindowSize from "./hooks/useWindowSize.js";
|
|
19
|
+
import useDebounce from "./hooks/useDebounce.js";
|
|
20
|
+
|
|
21
|
+
ChartJS.register(
|
|
22
|
+
Filler,
|
|
23
|
+
TimeScale, CategoryScale, LinearScale, BarElement, LineElement,
|
|
24
|
+
PointElement, ArcElement, Title, Tooltip, Legend
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const fetchChartData = async (chartConfig) => {
|
|
28
|
+
console.log(`[fetchChartData] Fetching aggregated data with config:`, chartConfig); // chartConfig inclura maintenant potentiellement chartConfig.filter
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(`/api/charts/aggregate`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify(chartConfig) // Le filtre sera envoyil est dans chartConfig
|
|
34
|
+
});
|
|
35
|
+
const responseClone = response.clone();
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
let errorText = `API Error ${response.status}`;
|
|
38
|
+
try {
|
|
39
|
+
const errorData = await response.json();
|
|
40
|
+
errorText = errorData.error || JSON.stringify(errorData);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
try { errorText = await responseClone.text(); } catch (e2) { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
console.error(`[fetchChartData] API Error: ${errorText}`);
|
|
45
|
+
throw new Error(errorText);
|
|
46
|
+
}
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
console.log(`[fetchChartData] Aggregated data received:`, data);
|
|
49
|
+
return data;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[fetchChartData] Error during fetch:`, error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// formatDateLabel, parsePotentiallyFormattedDate, processDataForChart restent inchangées
|
|
57
|
+
const formatDateLabel = (label, locale = 'fr-FR') => {
|
|
58
|
+
if (typeof label === 'string' && (label.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) || label.match(/^\d{4}-\d{2}-\d{2}$/))) {
|
|
59
|
+
const date = new Date(label);
|
|
60
|
+
if (!isNaN(date.getTime())) {
|
|
61
|
+
if (label.includes('T') && (date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0 || date.getUTCSeconds() !== 0)) {
|
|
62
|
+
return date.toLocaleString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
63
|
+
} else {
|
|
64
|
+
return date.toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (typeof label === 'boolean') {
|
|
69
|
+
return label ? i18n.t('boolean.true', 'Vrai') : i18n.t('boolean.false', 'Faux');
|
|
70
|
+
}
|
|
71
|
+
if (label === null || label === undefined) {
|
|
72
|
+
return i18n.t('charts.labelNull', '(Non défini)');
|
|
73
|
+
}
|
|
74
|
+
return String(label);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const parsePotentiallyFormattedDate = (str) => {
|
|
78
|
+
if (typeof str !== 'string') return null;
|
|
79
|
+
let match = str.match(/^(\d{2})\/(\d{2})\/(\d{4})(?: (\d{2}):(\d{2}))?$/);
|
|
80
|
+
if (match) {
|
|
81
|
+
const [, day, month, year, hour, minute] = match;
|
|
82
|
+
const isoStr = `${year}-${month}-${day}${hour ? `T${hour}:${minute || '00'}:00` : 'T00:00:00'}`;
|
|
83
|
+
const dt = new Date(isoStr);
|
|
84
|
+
return !isNaN(dt) && dt.getFullYear() === parseInt(year) && dt.getMonth() === parseInt(month) - 1 && dt.getDate() === parseInt(day) ? dt : null;
|
|
85
|
+
}
|
|
86
|
+
const dt = new Date(str);
|
|
87
|
+
return !isNaN(dt) ? dt : null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const processDataForChart = (inputData, config, t, timed, lang) => {
|
|
91
|
+
const { type, title, model, groupBy, xAxis, yAxis, aggregationType = 'count' } = config;
|
|
92
|
+
const currentLocale = i18n.language || 'fr-FR';
|
|
93
|
+
|
|
94
|
+
if (!inputData || !Array.isArray(inputData) || inputData.length === 0 || !('label' in inputData[0]) || !('value' in inputData[0])) {
|
|
95
|
+
return { labels: [], datasets: [] };
|
|
96
|
+
}
|
|
97
|
+
let processedItems = [...inputData];
|
|
98
|
+
processedItems.sort((itemA, itemB) => {
|
|
99
|
+
const labelA = itemA.label;
|
|
100
|
+
const labelB = itemB.label;
|
|
101
|
+
const dateA = parsePotentiallyFormattedDate(labelA);
|
|
102
|
+
const dateB = parsePotentiallyFormattedDate(labelB);
|
|
103
|
+
if (dateA && dateB) {
|
|
104
|
+
return dateA.getTime() - dateB.getTime();
|
|
105
|
+
}
|
|
106
|
+
const numA = parseFloat(labelA);
|
|
107
|
+
const numB = parseFloat(labelB);
|
|
108
|
+
if (!isNaN(numA) && !isNaN(numB)) {
|
|
109
|
+
return numA - numB;
|
|
110
|
+
}
|
|
111
|
+
return String(labelA).localeCompare(String(labelB), currentLocale, { sensitivity: 'base' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const labels = processedItems.filter(item => {
|
|
115
|
+
if( !timed )
|
|
116
|
+
return true;
|
|
117
|
+
const d = new Date(item.label);
|
|
118
|
+
return isDate(d);
|
|
119
|
+
}).map(item => {
|
|
120
|
+
if( !timed)
|
|
121
|
+
return item.label;
|
|
122
|
+
return new Date(item.label);
|
|
123
|
+
});
|
|
124
|
+
const dataValues = processedItems.map(item => parseFloat(item.value) || 0);
|
|
125
|
+
|
|
126
|
+
let datasetLabel = title || 'Data';
|
|
127
|
+
const isGroupingChart = ['pie', 'doughnut'].includes(type);
|
|
128
|
+
const aggregationLabel = t('aggregation.' + aggregationType, aggregationType);
|
|
129
|
+
if (isGroupingChart) {
|
|
130
|
+
datasetLabel = title || t(`field_${model}_${groupBy}`, groupBy);
|
|
131
|
+
if (yAxis && aggregationType !== 'count') {
|
|
132
|
+
datasetLabel += ` (${aggregationLabel} ${t(`field_${model}_${yAxis}`, yAxis)})`;
|
|
133
|
+
} else if (aggregationType === 'count') {
|
|
134
|
+
datasetLabel += ` (${aggregationLabel})`;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
if (yAxis && aggregationType !== 'count') {
|
|
138
|
+
datasetLabel = `${title || t(`field_${model}_${yAxis}`, yAxis)} (${aggregationLabel} ${t(`field_${model}_${yAxis}`, yAxis)})`;
|
|
139
|
+
} else {
|
|
140
|
+
datasetLabel = `${title || t(`field_${model}_${xAxis}`, xAxis)} (${aggregationLabel})`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const chartJsData = {
|
|
145
|
+
labels: labels,
|
|
146
|
+
datasets: [
|
|
147
|
+
{
|
|
148
|
+
label: datasetLabel,
|
|
149
|
+
data: dataValues,
|
|
150
|
+
barThickness: config.type === 'bar' ? (timed ? 5 : 3500/(20+(datasetLabel.length))) : undefined,
|
|
151
|
+
barPercentage: timed && config.type === 'bar' ? 0.9 : undefined,
|
|
152
|
+
categoryPercentage: timed && config.type === 'bar' ? 0.8 : undefined,
|
|
153
|
+
backgroundColor: (type === 'pie' || type === 'doughnut')
|
|
154
|
+
? ['rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 206, 86, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(153, 102, 255, 0.7)', 'rgba(255, 159, 64, 0.7)', 'rgba(199, 199, 199, 0.7)', 'rgba(83, 102, 89, 0.7)']
|
|
155
|
+
: config.chartBackgroundColor,
|
|
156
|
+
borderColor: (type === 'pie' || type === 'doughnut')
|
|
157
|
+
? '#fff'
|
|
158
|
+
: '#666',
|
|
159
|
+
borderWidth: 1,
|
|
160
|
+
fill: type === 'line' ? true : undefined,
|
|
161
|
+
tension: type === 'line' ? 0.1 : undefined,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
return chartJsData;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
const DashboardChart = ({ config }) => { // config peut maintenant contenir config.filter
|
|
170
|
+
const { i18n, t } = useTranslation();
|
|
171
|
+
const { models } = useModelContext();
|
|
172
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// --- NOUVEAU: Logique de gestion du redimensionnement ---
|
|
176
|
+
const windowSize = useWindowSize();
|
|
177
|
+
// On "debounce" la taille de la fenêtre. La requête API et le rendu final n'auront lieu qu'après 300ms sans mouvement.
|
|
178
|
+
const debouncedSize = useDebounce(windowSize, 300);
|
|
179
|
+
// On utilise un état pour savoir si l'utilisateur est EN TRAIN de redimensionner.
|
|
180
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
// 1. Au début de chaque redimensionnement, on active le flag.
|
|
183
|
+
setIsResizing(true);
|
|
184
|
+
|
|
185
|
+
// 2. On utilise un timer pour désactiver le flag 300ms après le DERNIER événement de redimensionnement.
|
|
186
|
+
// C'est une forme de "debouncing" directement appliquée à notre état.
|
|
187
|
+
const resizeTimer = setTimeout(() => {
|
|
188
|
+
setIsResizing(false);
|
|
189
|
+
}, 100); // Un délai raisonnable
|
|
190
|
+
|
|
191
|
+
// 3. On nettoie le timer à chaque nouvel événement pour qu'il ne se déclenche qu'à la fin.
|
|
192
|
+
return () => clearTimeout(resizeTimer);
|
|
193
|
+
}, [windowSize]); // Cet effet se déclenche à chaque micro-mouvement.
|
|
194
|
+
|
|
195
|
+
const modelDefinition = models?.find(f => f.name === config.model);
|
|
196
|
+
const fieldDefinition = modelDefinition?.fields.find(f => f.name === config.xAxis);
|
|
197
|
+
const timed = fieldDefinition && ['datetime', 'date'].includes(fieldDefinition.type);
|
|
198
|
+
|
|
199
|
+
const isGroupingChart = config && ['pie', 'doughnut'].includes(config.type);
|
|
200
|
+
const requiresYAxisForValidation = config && config.aggregationType && config.aggregationType !== 'count';
|
|
201
|
+
const isValidConfig = config && config.model && config.type && config.title && config.aggregationType &&
|
|
202
|
+
(isGroupingChart
|
|
203
|
+
? (config.groupBy && (requiresYAxisForValidation ? !!config.yAxis : true))
|
|
204
|
+
: (config.xAxis && (requiresYAxisForValidation ? !!config.yAxis : true))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
const queryKey = useMemo(() => {
|
|
210
|
+
const filterKey = JSON.stringify(config.filter || {});
|
|
211
|
+
return [
|
|
212
|
+
'chartData',
|
|
213
|
+
config.model,
|
|
214
|
+
config.type,
|
|
215
|
+
config.aggregationType,
|
|
216
|
+
isGroupingChart ? config.groupBy : config.xAxis,
|
|
217
|
+
requiresYAxisForValidation ? config.yAxis : null,
|
|
218
|
+
isGroupingChart && config.groupByLabelField ? config.groupByLabelField : null,
|
|
219
|
+
filterKey
|
|
220
|
+
];
|
|
221
|
+
}, [config, isGroupingChart, requiresYAxisForValidation, debouncedSize.width]);
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
const queryFn = () => fetchChartData(config); // config est passé en entier, incluant config.filter
|
|
225
|
+
const isQueryEnabled = isValidConfig;
|
|
226
|
+
|
|
227
|
+
const { data: queryData, isLoading, isError, error } = useQuery(
|
|
228
|
+
queryKey,
|
|
229
|
+
queryFn,
|
|
230
|
+
{
|
|
231
|
+
enabled: isQueryEnabled,
|
|
232
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
233
|
+
refetchOnWindowFocus: false
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const chartData = useMemo(() => {
|
|
238
|
+
if (!queryData) {
|
|
239
|
+
return { labels: [], datasets: [] };
|
|
240
|
+
}
|
|
241
|
+
return processDataForChart(queryData, config, t, timed, lang);
|
|
242
|
+
}, [queryData, config, t, timed, lang]);
|
|
243
|
+
|
|
244
|
+
const renderChart = useMemo(() => {
|
|
245
|
+
|
|
246
|
+
const baseOptions = {
|
|
247
|
+
responsive: true,
|
|
248
|
+
maintainAspectRatio: false,
|
|
249
|
+
locale: 'fr-FR',
|
|
250
|
+
adapters: {
|
|
251
|
+
date: {
|
|
252
|
+
locale: fr
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
plugins: {
|
|
256
|
+
legend: {
|
|
257
|
+
position: (config.type === 'pie' || config.type === 'doughnut') ? 'right' : 'top',
|
|
258
|
+
},
|
|
259
|
+
title: {
|
|
260
|
+
display: !!config.title,
|
|
261
|
+
text: config.title || '',
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const axisOptions = {
|
|
267
|
+
...baseOptions,
|
|
268
|
+
scales: {
|
|
269
|
+
x: {
|
|
270
|
+
type: timed ? 'time' : 'category',
|
|
271
|
+
time: timed ? {
|
|
272
|
+
tooltipFormat: 'dd/MM/yyyy HH:mm',
|
|
273
|
+
unit: 'minute',
|
|
274
|
+
} : {},
|
|
275
|
+
ticks: timed ? {
|
|
276
|
+
autoSkip: true,
|
|
277
|
+
maxTicksLimit: 20,
|
|
278
|
+
callback: function (value) {
|
|
279
|
+
const d = new Date();
|
|
280
|
+
d.setTime(value);
|
|
281
|
+
return d.toLocaleDateString(lang, {
|
|
282
|
+
minute: '2-digit',
|
|
283
|
+
hour: '2-digit'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} : {}
|
|
287
|
+
},
|
|
288
|
+
y: {
|
|
289
|
+
beginAtZero: true
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
const noAxisOptions = { ...baseOptions, scales: undefined };
|
|
294
|
+
|
|
295
|
+
const data = !isResizing ? chartData : { labels: [], datasets: [] };
|
|
296
|
+
try {
|
|
297
|
+
switch (config.type) {
|
|
298
|
+
case 'line':
|
|
299
|
+
return <Line options={axisOptions} data={data} />;
|
|
300
|
+
case 'pie':
|
|
301
|
+
return <Pie options={noAxisOptions} data={data} />;
|
|
302
|
+
case 'doughnut':
|
|
303
|
+
return <Doughnut options={noAxisOptions} data={data} />;
|
|
304
|
+
case 'bar':
|
|
305
|
+
default:
|
|
306
|
+
return <Bar options={axisOptions} data={data} />;
|
|
307
|
+
}
|
|
308
|
+
} catch (renderError) {
|
|
309
|
+
return (
|
|
310
|
+
<div className="chart-error" style={{ color: 'red', padding: '10px' }}>
|
|
311
|
+
{t('charts.renderError', 'Erreur interne lors du rendu du graphique.')}
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}, [config, t, chartData, isResizing]);
|
|
316
|
+
|
|
317
|
+
if (!isValidConfig) {
|
|
318
|
+
return (
|
|
319
|
+
<div className="chart-error" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'red', padding: '10px', textAlign: 'center' }}>
|
|
320
|
+
<FaExclamationTriangle /> {t('charts.invalidConfig', 'Configuration invalide ou incomplète')}
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (isLoading) {
|
|
326
|
+
return (
|
|
327
|
+
<div className="chart-loading" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
328
|
+
<FaSpinner className="spin" /> {t('loading', 'Chargement...')}
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (isError) {
|
|
333
|
+
return (
|
|
334
|
+
<div className="chart-error" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'red', padding: '10px', textAlign: 'center' }}>
|
|
335
|
+
<FaExclamationTriangle size="2em" />
|
|
336
|
+
<p>{t('errorLoadingData', 'Erreur de chargement des données.')}</p>
|
|
337
|
+
{error && <p style={{ fontSize: '0.8em', marginTop: '5px' }}>{error.message}</p>}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!chartData || !chartData.datasets || chartData.datasets.length === 0 || (chartData.datasets[0].data && chartData.datasets[0].data.length === 0)) {
|
|
342
|
+
return (
|
|
343
|
+
<div className="chart-no-data" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.7 }}>
|
|
344
|
+
{t('charts.noData', 'Aucune donnée à afficher.')}
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const containerStyle = {
|
|
350
|
+
position: 'relative',
|
|
351
|
+
height: '300px',
|
|
352
|
+
width: '100%'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<div className="dashboard-chart-container" style={containerStyle}>
|
|
357
|
+
{renderChart}
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export default DashboardChart;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
|
|
2
|
+
// --- Component to fetch data and render a single FlexView ---
|
|
3
|
+
import {useTranslation} from "react-i18next";
|
|
4
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
5
|
+
import React, {useEffect, useMemo, useRef} from "react";
|
|
6
|
+
import {useQuery} from "react-query";
|
|
7
|
+
import {conditionToApiSearchFilter} from "data-primals-engine/data";
|
|
8
|
+
import {FaSpinner} from "react-icons/fa";
|
|
9
|
+
import {DisplayFlexNodeRenderer} from "./DisplayFlexNodeRenderer.jsx";
|
|
10
|
+
|
|
11
|
+
const DashboardFlexViewItem = ({ flexViewConfig, allModels }) => {
|
|
12
|
+
const { t, i18n } = useTranslation();
|
|
13
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
14
|
+
const { me } = useAuthContext();
|
|
15
|
+
|
|
16
|
+
// MODIFICATION: Extraire dataLimit et s'assurer qu'il est entre 1 et 8
|
|
17
|
+
const { selectedModelName, dataFilter, flexStructure } = flexViewConfig;
|
|
18
|
+
const effectiveDataLimit = Math.max(1, Math.min(flexViewConfig.dataLimit || 1, 8));
|
|
19
|
+
|
|
20
|
+
const queryKey = useMemo(() => ['flexViewData', selectedModelName, dataFilter, effectiveDataLimit, me?.username, lang],
|
|
21
|
+
[selectedModelName, dataFilter, effectiveDataLimit, me?.username, lang]);
|
|
22
|
+
|
|
23
|
+
const { data: queryResult, isLoading, error, isFetching } = useQuery(
|
|
24
|
+
queryKey,
|
|
25
|
+
async () => {
|
|
26
|
+
if (!selectedModelName || !me?.username) return { data: [] };
|
|
27
|
+
|
|
28
|
+
const payload = {
|
|
29
|
+
model: selectedModelName,
|
|
30
|
+
filter: dataFilter || {},
|
|
31
|
+
limit: effectiveDataLimit, // Utiliser la limite effective
|
|
32
|
+
lang: lang,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// MODIFICATION: Construire l'URL dynamiquement avec effectiveDataLimit
|
|
36
|
+
const apiUrl = `/api/data/search?model=${selectedModelName}&depth=1&limit=${effectiveDataLimit}&_user=${me.username}`;
|
|
37
|
+
|
|
38
|
+
const response = await fetch(apiUrl, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(payload)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const resError = await response.json();
|
|
46
|
+
throw new Error(resError.error || t('dashboards.error.fetchFlexData', 'Erreur de chargement des données pour la vue Flex'));
|
|
47
|
+
}
|
|
48
|
+
const result = await response.json();
|
|
49
|
+
return result; // Expects { data: [...] }
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
enabled: !!selectedModelName && !!me?.username,
|
|
53
|
+
keepPreviousData: true,
|
|
54
|
+
refetchOnWindowFocus: false, // Désactive le rafraîchissement au focus de la fenêtre
|
|
55
|
+
refetchOnMount: true, // Ou false si 'enabled' gère déjà le premier chargement et que vous ne voulez pas de refetch au remontage
|
|
56
|
+
staleTime: 5 * 60 * 1000, // Considérer les données fraîches pendant 5 minutes (exemple)
|
|
57
|
+
refetchInterval: flexViewConfig.refreshInterval || 60 * 1000, // Utiliser l'intervalle configur
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const dataToRender = useMemo(() => queryResult?.data || [], [queryResult]);
|
|
62
|
+
const dataIndexRef = useRef(0);
|
|
63
|
+
|
|
64
|
+
const currentModel = useMemo(() => {
|
|
65
|
+
if (!selectedModelName || !allModels) return null;
|
|
66
|
+
return allModels.find(m => m.name === selectedModelName);
|
|
67
|
+
}, [selectedModelName, allModels]);
|
|
68
|
+
|
|
69
|
+
const currentModelFields = useMemo(() => currentModel?.fields || [], [currentModel]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
dataIndexRef.current = 0;
|
|
73
|
+
}, [dataToRender]);
|
|
74
|
+
|
|
75
|
+
if (isLoading && !queryResult) {
|
|
76
|
+
return <div className="flex-view-placeholder"><FaSpinner className="spin" /> {t('loading', 'Chargement...')}</div>;
|
|
77
|
+
}
|
|
78
|
+
if (isFetching && !queryResult) {
|
|
79
|
+
return <div className="flex-view-placeholder"><FaSpinner className="spin" /> {t('loading', 'Chargement...')}</div>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (error) {
|
|
83
|
+
return <div className="flex-view-placeholder error" title={error.message}>{t('dashboards.error.displayFlexData', 'Erreur d\'affichage')}</div>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(flexStructure)
|
|
87
|
+
if (!flexStructure || (!selectedModelName && !flexStructure.htmlContent)) {
|
|
88
|
+
return <div className="flex-view-placeholder">{t('dashboards.flexViewNotConfigured', 'Vue Flex non configurée ou modèle manquant.')}</div>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (dataToRender.length === 0 && !isLoading && !isFetching) {
|
|
92
|
+
// Afficher la structure avec des données vides pour montrer les placeholders
|
|
93
|
+
const dummyDataIndexRef = { current: 0 }; // Référence séparée pour l'affichage des placeholders
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex-view-content-wrapper is-empty">
|
|
96
|
+
<DisplayFlexNodeRenderer
|
|
97
|
+
key={currentModel?.name}
|
|
98
|
+
model={currentModel}
|
|
99
|
+
node={flexStructure}
|
|
100
|
+
allModels={allModels}
|
|
101
|
+
baseModelFields={currentModelFields}
|
|
102
|
+
data={[{}]} // Passer un objet factice pour que les placeholders s'affichent
|
|
103
|
+
dataIndexRef={dummyDataIndexRef}
|
|
104
|
+
/>
|
|
105
|
+
<p className="empty-data-message">{t('dashboards.flexViewNoData', 'Aucune donnée à afficher pour la configuration actuelle.')}</p>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="flex-view-content-wrapper">
|
|
112
|
+
{isFetching && <div className="loading-overlay-flexview"><FaSpinner className="spin"/></div>}
|
|
113
|
+
{dataToRender.map((item, index) => {
|
|
114
|
+
const singleItemDataIndexRef = { current: 0 }; // Chaque item commence son propre cycle d'index
|
|
115
|
+
return <DisplayFlexNodeRenderer
|
|
116
|
+
key={item._id || `flex-item-${index}`} // AMÉLIORATION : Clé plus stable
|
|
117
|
+
node={flexStructure}
|
|
118
|
+
model={selectedModelName}
|
|
119
|
+
data={[item]} // Passer un seul item à la fois
|
|
120
|
+
allModels={allModels}
|
|
121
|
+
dataIndexRef={singleItemDataIndexRef}
|
|
122
|
+
baseModelFields={currentModelFields}
|
|
123
|
+
/>
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export { DashboardFlexViewItem };
|