data-primals-engine 1.0.11 → 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 +212 -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,210 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/Dashboard.jsx
|
|
2
|
+
import React, {useState, useMemo, useEffect} from 'react';
|
|
3
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
4
|
+
import {FaPlus, FaSpinner} from "react-icons/fa"; // Ajout FaTrash
|
|
5
|
+
|
|
6
|
+
import "./Dashboard.scss"
|
|
7
|
+
import {useMutation, useQuery, useQueryClient} from "react-query";
|
|
8
|
+
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
9
|
+
import { SelectField } from "./Field.jsx";
|
|
10
|
+
import {DashboardView} from "./DashboardView.jsx";
|
|
11
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
12
|
+
import {useParams, useSearchParams} from "react-router-dom";
|
|
13
|
+
import {getObjectHash} from "data-primals-engine/core";
|
|
14
|
+
import {getUserHash, getUserId} from "data-primals-engine/data";
|
|
15
|
+
|
|
16
|
+
// --- DashboardsPage (Reste inchangé) ---
|
|
17
|
+
export function DashboardsPage() {
|
|
18
|
+
const { setSelectedModel } = useModelContext()
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const { me } = useAuthContext();
|
|
21
|
+
const [selectedDashboardId, setSelectedDashboardId] = useState(null);
|
|
22
|
+
|
|
23
|
+
const [ searchParams, setSearchParams ] = useSearchParams();
|
|
24
|
+
const { hash } = useParams(); // 2. Récupérer le hash de l'URL
|
|
25
|
+
|
|
26
|
+
// ... après les autres hooks useState, useQuery, etc.
|
|
27
|
+
const queryClient = useQueryClient();
|
|
28
|
+
const [newDashboardName, setNewDashboardName] = useState('');
|
|
29
|
+
const [isCreating, setIsCreating] = useState(false); // <-- AJOUTEZ CETTE LIGNE
|
|
30
|
+
|
|
31
|
+
const createDashboardMutation = useMutation(
|
|
32
|
+
async (dashboardName) => {
|
|
33
|
+
const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
|
|
34
|
+
const response = await fetch('/api/data', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model: 'dashboard',
|
|
39
|
+
data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorData = await response.json();
|
|
44
|
+
throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
|
|
45
|
+
}
|
|
46
|
+
return response.json();
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
onSuccess: () => {
|
|
50
|
+
// Rafraîchit la liste des dashboards après la création
|
|
51
|
+
queryClient.invalidateQueries(['userDashboards', me?.username]);
|
|
52
|
+
setNewDashboardName('');
|
|
53
|
+
setIsCreating(false);
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => {
|
|
56
|
+
// Idéalement, afficher une notification à l'utilisateur
|
|
57
|
+
console.error("Erreur lors de la création du dashboard:", error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handleCreateDashboard = (e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
if (newDashboardName.trim()) {
|
|
65
|
+
createDashboardMutation.mutate(newDashboardName.trim());
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
|
|
69
|
+
['userDashboards', me?.username],
|
|
70
|
+
async () => {
|
|
71
|
+
if (!me?.username) return null;
|
|
72
|
+
const response = await fetch(
|
|
73
|
+
`/api/data/search?model=dashboard&_user=${me.username}`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
// body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const res = await response.json();
|
|
80
|
+
throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
|
|
81
|
+
}
|
|
82
|
+
return await response.json();
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
enabled: !!me?.username
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
89
|
+
|
|
90
|
+
// 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (dashboardsData?.data.length > 0) {
|
|
93
|
+
if( hash ) {
|
|
94
|
+
const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
|
|
95
|
+
setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
|
|
96
|
+
}else{
|
|
97
|
+
const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
|
|
98
|
+
if( !defaultDashboard) {
|
|
99
|
+
setSelectedDashboardId(dashboardsData.data[0]._id);
|
|
100
|
+
}else
|
|
101
|
+
setSelectedDashboardId(defaultDashboard._id || null);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
setIsLoading(false);
|
|
105
|
+
}, [hash,dashboardsData]);
|
|
106
|
+
const selectedDashboard = useMemo(() => {
|
|
107
|
+
if (!selectedDashboardId || !dashboardsData?.data) return null;
|
|
108
|
+
return dashboardsData.data.find(db => db._id === selectedDashboardId);
|
|
109
|
+
}, [selectedDashboardId, dashboardsData]);
|
|
110
|
+
|
|
111
|
+
const dashboardOptions = useMemo(() => {
|
|
112
|
+
return (dashboardsData?.data || []).map(db => ({
|
|
113
|
+
label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
|
|
114
|
+
value: db._id
|
|
115
|
+
}));
|
|
116
|
+
}, [dashboardsData, t]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
setSelectedModel(null)
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if (isLoading) {
|
|
124
|
+
return <div>{t('loading', 'Chargement...')}</div>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!selectedDashboardId && hash) {
|
|
128
|
+
return (
|
|
129
|
+
<div className="dashboard-not-found">
|
|
130
|
+
<h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
|
|
131
|
+
<p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const creationForm = (
|
|
137
|
+
<form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
|
|
138
|
+
<input
|
|
139
|
+
type="text"
|
|
140
|
+
className="input-fit"
|
|
141
|
+
value={newDashboardName}
|
|
142
|
+
onChange={(e) => setNewDashboardName(e.target.value)}
|
|
143
|
+
placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
|
|
144
|
+
disabled={createDashboardMutation.isLoading}
|
|
145
|
+
autoFocus
|
|
146
|
+
/>
|
|
147
|
+
<button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
|
|
148
|
+
{createDashboardMutation.isLoading
|
|
149
|
+
? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
|
|
150
|
+
: <Trans i18nKey="btns.create">Créer</Trans>
|
|
151
|
+
}
|
|
152
|
+
</button>
|
|
153
|
+
{/* Bouton pour annuler quand on a cliqué sur le "+" */}
|
|
154
|
+
{isCreating && (
|
|
155
|
+
<button type="button" className="btn" onClick={() => setIsCreating(false)}>
|
|
156
|
+
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
157
|
+
</button>
|
|
158
|
+
)}
|
|
159
|
+
</form>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="dashboards-page">
|
|
164
|
+
<h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
|
|
165
|
+
|
|
166
|
+
{isLoadingDashboards && (
|
|
167
|
+
<p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{errorDashboards && (
|
|
171
|
+
<p className="error">{errorDashboards.message}</p>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{!isLoadingDashboards && !errorDashboards && (
|
|
175
|
+
<>
|
|
176
|
+
{dashboardsData?.data && dashboardsData.data.length > 0 ? (
|
|
177
|
+
<div className="dashboard-selector-container">
|
|
178
|
+
<div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
|
|
179
|
+
<SelectField
|
|
180
|
+
name="dashboardSelection"
|
|
181
|
+
label={t('dashboards.select', 'Choisir un tableau de bord :')}
|
|
182
|
+
value={selectedDashboardId}
|
|
183
|
+
onChange={(selectedOption) => {
|
|
184
|
+
setSelectedDashboardId(selectedOption.value);
|
|
185
|
+
const d = dashboardsData.data.find(f => f._id === selectedOption.value);
|
|
186
|
+
history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
|
|
187
|
+
}}
|
|
188
|
+
items={dashboardOptions}
|
|
189
|
+
/>
|
|
190
|
+
<button onClick={() => setIsCreating(true)} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
|
|
191
|
+
<FaPlus />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
{isCreating && creationForm}
|
|
195
|
+
</div>
|
|
196
|
+
) : (
|
|
197
|
+
<>
|
|
198
|
+
<p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
|
|
199
|
+
{creationForm}
|
|
200
|
+
</>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Affiche la vue du dashboard sélectionné */}
|
|
204
|
+
{/* Passe l'objet dashboard complet */}
|
|
205
|
+
<DashboardView dashboard={selectedDashboard} />
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
@import "_variables.scss"; // Importer tes variables
|
|
2
|
+
.dashboards-page {
|
|
3
|
+
padding: 16px;
|
|
4
|
+
}
|
|
5
|
+
.dashboard-view {
|
|
6
|
+
.kpi-grid {
|
|
7
|
+
display: grid;
|
|
8
|
+
// Ajuste minmax pour la largeur minimale/maximale des KPIs
|
|
9
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
10
|
+
gap: 20px; // Espace entre les KPIs
|
|
11
|
+
margin-top: 20px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.add-buttons-inline {
|
|
15
|
+
display: flex;
|
|
16
|
+
gap: 8px;
|
|
17
|
+
align-self: center;
|
|
18
|
+
}
|
|
19
|
+
.add-chart-button, .add-kpi-button {
|
|
20
|
+
// Style pour le bouton d'ajout
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
padding: 15px;
|
|
26
|
+
border: 2px dashed $medium-gray; // Style pointillé
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
background-color: $light-gray;
|
|
29
|
+
color: $dark-gray;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
transition: background-color 0.2s, border-color 0.2s;
|
|
32
|
+
|
|
33
|
+
&:hover {
|
|
34
|
+
background-color: darken($light-gray, 5%);
|
|
35
|
+
border-color: $primary-color;
|
|
36
|
+
color: $primary-color;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ... (styles existants du KPIWidget) ...
|
|
42
|
+
|
|
43
|
+
.kpi-widget {
|
|
44
|
+
position: relative; // Nécessaire pour positionner le bouton remove
|
|
45
|
+
text-align: right;
|
|
46
|
+
padding: 24px;
|
|
47
|
+
|
|
48
|
+
min-width: 240px;
|
|
49
|
+
.kpi-title {
|
|
50
|
+
display: flex; /* Aligner nom et icône info */
|
|
51
|
+
align-items: center;
|
|
52
|
+
}
|
|
53
|
+
.kpi-content {
|
|
54
|
+
padding-left: 24px;
|
|
55
|
+
}
|
|
56
|
+
.kpi-title {
|
|
57
|
+
display: flex;
|
|
58
|
+
justify-content: right;
|
|
59
|
+
}
|
|
60
|
+
.kpi-info {
|
|
61
|
+
outline: 1px solid transparent;
|
|
62
|
+
margin-left: 5px;
|
|
63
|
+
cursor: help;
|
|
64
|
+
opacity: 0.7;
|
|
65
|
+
&:hover {
|
|
66
|
+
outline: 1px solid gray;
|
|
67
|
+
}
|
|
68
|
+
width: 24px;
|
|
69
|
+
height: 24px;
|
|
70
|
+
text-align: center;
|
|
71
|
+
}
|
|
72
|
+
.kpi-remove-button {
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: 5px;
|
|
75
|
+
right: 5px;
|
|
76
|
+
background: none;
|
|
77
|
+
border: none;
|
|
78
|
+
color: inherit;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
padding: 3px;
|
|
81
|
+
line-height: 1;
|
|
82
|
+
border-radius: 50%;
|
|
83
|
+
opacity: 0.5; // Moins visible par défaut
|
|
84
|
+
transition: opacity 0.2s, color 0.2s;
|
|
85
|
+
|
|
86
|
+
&:hover {
|
|
87
|
+
color: $danger-color; // Rouge au survol
|
|
88
|
+
opacity: 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&:hover .kpi-remove-button {
|
|
93
|
+
opacity: 1; // Rendre visible au survol du widget
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.kpi-value-number {
|
|
97
|
+
font-size: 240%;
|
|
98
|
+
padding: 4px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.kpi-icon {
|
|
102
|
+
position: absolute;
|
|
103
|
+
font-size: 340%;
|
|
104
|
+
bottom: 6px;
|
|
105
|
+
left: 24px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Dans votre fichier CSS global ou spécifique aux composants */
|
|
109
|
+
|
|
110
|
+
.kpi-value-with-total .kpi-main-value {
|
|
111
|
+
/* Style pour la valeur principale si nécessaire */
|
|
112
|
+
font-weight: bold; /* Exemple */
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.kpi-value-with-total .kpi-separator {
|
|
116
|
+
opacity: 0.8;
|
|
117
|
+
margin: 0 0.2em; /* Un peu d'espace autour du slash */
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.kpi-value-with-total .kpi-total-value {
|
|
121
|
+
font-size: 0.85em; /* Rur le total */
|
|
122
|
+
opacity: 0.9;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.kpi-percentage {
|
|
126
|
+
font-weight: bold; /* Mettre le pourcentage en gras par exemple */
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.kpi-value-simple .kpi-value-number {
|
|
130
|
+
/* Style pour le nombre seul si nécessaire */
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.kpi-unit-suffix {
|
|
134
|
+
margin-left: 0.2em; /* Espace avant l'unité */
|
|
135
|
+
font-size: 1.4em;
|
|
136
|
+
opacity: 0.8;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.kpi-error {
|
|
140
|
+
color: red; /* Ou une couleur adaptée à votre thème */
|
|
141
|
+
font-weight: bold;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.kpi-loading {
|
|
145
|
+
opacity: 0.7;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.kpi-value-nodata {
|
|
149
|
+
opacity: 0.7;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.kpi-widget:hover .kpi-remove-button {
|
|
153
|
+
opacity: 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.kpi-remove-button:hover {
|
|
157
|
+
background: rgba(0, 0, 0, 0.3);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.dashboard-sections {
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
gap: 12px;
|
|
166
|
+
.dashboard-section {
|
|
167
|
+
border: 1px solid #e0e0e0;
|
|
168
|
+
padding: 1rem;
|
|
169
|
+
border-radius: 4px;
|
|
170
|
+
position: relative; // Pour positionner le bouton de suppression
|
|
171
|
+
|
|
172
|
+
.section-header {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 0.5rem;
|
|
176
|
+
margin-bottom: 1rem;
|
|
177
|
+
border-bottom: 1px solid #eee;
|
|
178
|
+
padding-bottom: 0.5rem;
|
|
179
|
+
|
|
180
|
+
.section-title {
|
|
181
|
+
flex-grow: 1;
|
|
182
|
+
margin: 0;
|
|
183
|
+
padding: 0.25rem 0; // Un peu d'espace pour cliquer/éditer
|
|
184
|
+
border-radius: 3px;
|
|
185
|
+
cursor: text; // Indique qu'on peut cliquer
|
|
186
|
+
|
|
187
|
+
&:focus, &.editing {
|
|
188
|
+
outline: 2px solid dodgerblue; // Mise en évidence lors de l'édition
|
|
189
|
+
background-color: #f0f8ff;
|
|
190
|
+
cursor: initial;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.edit-section-name-button {
|
|
195
|
+
background: none;
|
|
196
|
+
border: none;
|
|
197
|
+
color: #666;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
padding: 0.25rem;
|
|
200
|
+
&:hover {
|
|
201
|
+
color: #333;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.kpi-grid {
|
|
207
|
+
// ... (styles existants pour kpi-grid)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.remove-section-button {
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: 0.5rem;
|
|
213
|
+
right: 0.5rem;
|
|
214
|
+
background: none;
|
|
215
|
+
border: none;
|
|
216
|
+
color: #aaa;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
font-size: 0.9em;
|
|
219
|
+
padding: 0.25rem;
|
|
220
|
+
|
|
221
|
+
&:hover {
|
|
222
|
+
color: red;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.empty-section-message {
|
|
227
|
+
color: #888;
|
|
228
|
+
font-style: italic;
|
|
229
|
+
text-align: center;
|
|
230
|
+
padding: 1rem 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// C:/Dev/hackersonline-engine/client/src/Dashboard.scss
|
|
235
|
+
// ... (autres styles existants) ...
|
|
236
|
+
|
|
237
|
+
// Styles pour les badges de données dans FlexView
|
|
238
|
+
.flex-view-content-wrapper {
|
|
239
|
+
// Style pour le conteneur de chaque "carte" répétée
|
|
240
|
+
// Chaque DisplayFlexNodeRenderer généré par le .map() sera un enfant direct ici.
|
|
241
|
+
// Vous pouvez ajouter des styles pour espacer les cartes, leur donner des bordures, etc.
|
|
242
|
+
// Par exemple, pour que chaque carte (chaque instance de flexStructure) soit distincte :
|
|
243
|
+
& > .flex-node.preview-container { // Cible la racine de chaque structure Flex répétée
|
|
244
|
+
border: 1px solid #e0e0e0;
|
|
245
|
+
padding: 15px;
|
|
246
|
+
border-radius: 4px;
|
|
247
|
+
background-color: #fff;
|
|
248
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.preview-item.is-data-badge-item {
|
|
253
|
+
// Styles spécifiques pour les cases qui contiennent un data-badge
|
|
254
|
+
// Par défaut, preview-item a déjà display: flex, align-items: center, justify-content: center
|
|
255
|
+
// On peut vouloir ajuster ça pour les badges
|
|
256
|
+
align-items: flex-start; // Aligner les badges en haut de la case
|
|
257
|
+
justify-content: flex-start; // Aligner les badges à gauche de la case
|
|
258
|
+
padding: 5px; // Réduire un peu le padding interne de la case si besoin
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.data-badge {
|
|
262
|
+
display: inline-flex; // Pour que le label et la valeur soient sur la même ligne par défaut
|
|
263
|
+
align-items: baseline; // Aligner le texte du label et de la valeur
|
|
264
|
+
padding: 4px 8px;
|
|
265
|
+
border: 1px solid #d1d5db; // Gris clair
|
|
266
|
+
border-radius: 4px;
|
|
267
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
268
|
+
background-color: #f9fafb; // Fond très clair
|
|
269
|
+
font-size: 0.9em;
|
|
270
|
+
margin: 2px; // Petit espacement si plusieurs badges sont dans la même case (si la case elle-même est un flex container)
|
|
271
|
+
width: 100%; // Pour que le badge prenne la largeur de la case
|
|
272
|
+
box-sizing: border-box;
|
|
273
|
+
|
|
274
|
+
.data-badge-label {
|
|
275
|
+
font-weight: 600; // Semi-bold pour le libellé
|
|
276
|
+
color: #374151; // Gris foncé
|
|
277
|
+
margin-right: 6px; // Espace entre le libellé et la valeur
|
|
278
|
+
white-space: nowrap; // Empêcher le libellé de passer à la ligne trop vite
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.data-badge-value {
|
|
282
|
+
color: #1f2937; // Gris un peu plus foncé pour la valeur
|
|
283
|
+
word-break: break-word; // Permettre à la valeur de passer à la ligne si elle est longue
|
|
284
|
+
|
|
285
|
+
.data-badge-novalue,
|
|
286
|
+
.data-badge-nodata {
|
|
287
|
+
color: #9ca3af; // Gris plus clair pour les messages d'absence de données
|
|
288
|
+
font-style: italic;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Si vous voulez que les badges s'empilent verticalement dans une case,
|
|
294
|
+
// et que chaque badge prenne toute la largeur :
|
|
295
|
+
.preview-item.is-data-badge-item {
|
|
296
|
+
// Si la case (preview-item) est un conteneur flex pour ses enfants (les data-badges)
|
|
297
|
+
// et que vous voulez que les badges s'empilent verticalement :
|
|
298
|
+
// display: flex; // Déjà défini sur preview-item
|
|
299
|
+
// flex-direction: column; // Empiler verticalement
|
|
300
|
+
// align-items: stretch; // Étirer les badges sur la largeur
|
|
301
|
+
|
|
302
|
+
// Si la case n'est pas un conteneur flex pour ses enfants directs (les data-badges)
|
|
303
|
+
// ou si vous voulez simplement que chaque badge soit sur sa propre ligne :
|
|
304
|
+
.data-badge {
|
|
305
|
+
display: flex; // Pour un meilleur contrôle interne du badge
|
|
306
|
+
// width: 100%; // Déjà défini ci-dessus, assure que le badge prend la largeur
|
|
307
|
+
margin-bottom: 5px; // Espace entre les badges s'ils s'empilent
|
|
308
|
+
&:last-child {
|
|
309
|
+
margin-bottom: 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
.empty-data-message {
|
|
316
|
+
padding: 20px;
|
|
317
|
+
text-align: center;
|
|
318
|
+
color: #6b7280; // Gris moyen
|
|
319
|
+
font-style: italic;
|
|
320
|
+
border: 1px dashed #d1d5db;
|
|
321
|
+
border-radius: 4px;
|
|
322
|
+
margin-top: 10px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.loading-overlay-flexview {
|
|
326
|
+
position: absolute;
|
|
327
|
+
top: 0;
|
|
328
|
+
left: 0;
|
|
329
|
+
right: 0;
|
|
330
|
+
bottom: 0;
|
|
331
|
+
background-color: rgba(255, 255, 255, 0.7);
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
justify-content: center;
|
|
335
|
+
z-index: 10;
|
|
336
|
+
font-size: 1.2em;
|
|
337
|
+
color: #333;
|
|
338
|
+
.spin {
|
|
339
|
+
margin-right: 10px;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.flex-view-placeholder {
|
|
344
|
+
padding: 20px;
|
|
345
|
+
text-align: center;
|
|
346
|
+
color: #6b7280;
|
|
347
|
+
border: 1px dashed #d1d5db;
|
|
348
|
+
border-radius: 4px;
|
|
349
|
+
min-height: 100px;
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
&.error {
|
|
354
|
+
color: #ef4444; // Rouge pour les erreurs
|
|
355
|
+
border-color: #ef4444;
|
|
356
|
+
}
|
|
357
|
+
.spin {
|
|
358
|
+
margin-right: 8px;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.flex-view-content-wrapper{
|
|
363
|
+
display: flex;
|
|
364
|
+
flex-wrap: wrap;
|
|
365
|
+
gap:16px;
|
|
366
|
+
align-items: center;
|
|
367
|
+
justify-content: center;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.flex-node{
|
|
371
|
+
margin: 0;
|
|
372
|
+
max-width: 480px;
|
|
373
|
+
.relation-value{
|
|
374
|
+
padding: 6px 10px;
|
|
375
|
+
border-radius: 20px;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Styles pour les wrappers d'items dans le dashboard
|
|
379
|
+
.dashboard-item-wrapper {
|
|
380
|
+
position: relative; // Pour les boutons d'action
|
|
381
|
+
border: 1px solid #e5e7eb; // Bordure légère
|
|
382
|
+
border-radius: 6px;
|
|
383
|
+
// box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // Ombre subtile
|
|
384
|
+
display: flex; // Pour que le contenu flex s'ende bien
|
|
385
|
+
flex-direction: column; // Empiler le contenu et les actions
|
|
386
|
+
min-width: 280px; // Largeur minimale pour une carte
|
|
387
|
+
|
|
388
|
+
&.flex-view-wrapper {
|
|
389
|
+
// Styles spécifiques si un flex-view a besoin de plus d'espace ou d'un traitement différent
|
|
390
|
+
// Par exemple, si la structure flex interne doit gérer son propre défilement :
|
|
391
|
+
// overflow: auto; // Attention, cela peut cacher les ombres ou les bordures si mal géré
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.flex-view-content-wrapper {
|
|
395
|
+
flex-grow: 1; // Permettre au contenu de la vue flex de prendre l'espace disponible
|
|
396
|
+
// padding: 10px; // Padding autour du contenu de la vue flex si nécessaire
|
|
397
|
+
// Si vous avez déjà un padding sur .flex-node.preview-container, ce n'est peut-as nécessaire ici.
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.item-actions {
|
|
401
|
+
display: flex;
|
|
402
|
+
justify-content: flex-end;
|
|
403
|
+
padding: 4px 8px;
|
|
404
|
+
|
|
405
|
+
button {
|
|
406
|
+
background: none;
|
|
407
|
+
border: none;
|
|
408
|
+
color: #6b7280; // Gris
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
padding: 6px;
|
|
411
|
+
margin-left: 4px;
|
|
412
|
+
border-radius: 4px;
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: center;
|
|
415
|
+
font-size: 0.9rem;
|
|
416
|
+
|
|
417
|
+
&:hover {
|
|
418
|
+
color: #1f2937; // Noir/Gris foncé
|
|
419
|
+
background-color: #e5e7eb; // Fond au survol
|
|
420
|
+
}
|
|
421
|
+
&:disabled {
|
|
422
|
+
color: #d1d5db;
|
|
423
|
+
cursor: not-allowed;
|
|
424
|
+
background-color: transparent;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Assurez-vous que les items flex dans la grille du dashboard s'affichent correctement
|
|
431
|
+
.items-grid.flex {
|
|
432
|
+
display: grid; // Ou flex si vous préférez une disposition en ligne
|
|
433
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); // Exemple de grille responsive
|
|
434
|
+
gap: 1rem; // Espace entre les items
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.preview-item {
|
|
438
|
+
text-align: center;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.dashboard-selector {
|
|
442
|
+
label {
|
|
443
|
+
text-align: right;
|
|
444
|
+
}
|
|
445
|
+
}
|