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,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useQuery } from 'react-query';
|
|
3
|
+
import * as FaIcons from 'react-icons/fa'; // Importer toutes les icônes Fa
|
|
4
|
+
import * as Fa6Icons from 'react-icons/fa6'; // Importer Fa6
|
|
5
|
+
import { FaInfo, FaTimes } from 'react-icons/fa';
|
|
6
|
+
import { isLightColor } from "data-primals-engine/core"; // Pour le bouton supprimer
|
|
7
|
+
import { Tooltip } from 'react-tooltip';
|
|
8
|
+
|
|
9
|
+
// Fonction pour récupérer la valeur calculée d'un KPI (MODIFIÉE)
|
|
10
|
+
const fetchKpiValue = async (kpiId) => {
|
|
11
|
+
if (!kpiId) return null;
|
|
12
|
+
const response = await fetch(`/api/kpis/calculate/${kpiId}`);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const errorData = await response.json().catch(() => ({}));
|
|
15
|
+
throw new Error(errorData.error || `Network response was not ok (${response.status})`);
|
|
16
|
+
}
|
|
17
|
+
const data = await response.json();
|
|
18
|
+
if (!data.success) {
|
|
19
|
+
throw new Error(data.error || 'Failed to calculate KPI value');
|
|
20
|
+
}
|
|
21
|
+
// Retourner l'objet complet { value, totalCount }
|
|
22
|
+
return { value: data.value, totalCount: data.totalCount };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Fonction pour obtenir le composant icône par son nom
|
|
26
|
+
const getIconComponent = (iconName) => {
|
|
27
|
+
if (!iconName) return null;
|
|
28
|
+
const IconComponent = FaIcons[iconName] || Fa6Icons[iconName];
|
|
29
|
+
return IconComponent ? <IconComponent /> : null; // Retourne l'élément React ou null
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const KPIWidget = ({ kpiDefinition, onRemove }) => {
|
|
34
|
+
// Destructurer les nouvelles propriétés de kpiDefinition
|
|
35
|
+
const {
|
|
36
|
+
_id,
|
|
37
|
+
name,
|
|
38
|
+
unit,
|
|
39
|
+
description,
|
|
40
|
+
icon: iconName,
|
|
41
|
+
color,
|
|
42
|
+
showTotal, // Nouveau
|
|
43
|
+
showPercentTotal // Nouveau
|
|
44
|
+
} = kpiDefinition;
|
|
45
|
+
|
|
46
|
+
// Utiliser useQuery pour récupérer l'objet { value, totalCount }
|
|
47
|
+
const { data: kpiData, isLoading, error } = useQuery(
|
|
48
|
+
['kpiValue', _id], // Clé de requête unique par ID de KPI
|
|
49
|
+
() => fetchKpiValue(_id),
|
|
50
|
+
{
|
|
51
|
+
staleTime: 5 * 60 * 1000, // Garder la valeur fraîche pendant 5 minutes
|
|
52
|
+
refetchInterval: 5 * 60 * 1000, // Rafraîchir toutes les 5 minutes
|
|
53
|
+
enabled: !!_id, // N'exécuter que si _id est défini
|
|
54
|
+
refetchOnWindowFocus: false
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let displayValue = '...';
|
|
59
|
+
if (isLoading) {
|
|
60
|
+
displayValue = <span className="kpi-loading">Chargement...</span>;
|
|
61
|
+
} else if (error) {
|
|
62
|
+
displayValue = <span className="kpi-error" title={error.message}>Erreur</span>; // Ajout title pour debug
|
|
63
|
+
console.error(`KPI Error (${name || _id}):`, error);
|
|
64
|
+
} else if (kpiData !== undefined && kpiData !== null) {
|
|
65
|
+
const value = kpiData.value;
|
|
66
|
+
const totalCount = kpiData.totalCount;
|
|
67
|
+
const valueExists = value !== undefined && value !== null;
|
|
68
|
+
const totalExists = totalCount !== undefined && totalCount !== null;
|
|
69
|
+
|
|
70
|
+
// Formatage de la valeur principale (si elle existe)
|
|
71
|
+
const formattedValue = valueExists
|
|
72
|
+
? (typeof value === 'number' ? value.toLocaleString() : value)
|
|
73
|
+
: '-';
|
|
74
|
+
|
|
75
|
+
// 1. Priorité au pourcentage si demandé et possible
|
|
76
|
+
if (showTotal && valueExists && totalExists) {
|
|
77
|
+
const formattedTotal = typeof totalCount === 'number' ? totalCount.toLocaleString() : totalCount;
|
|
78
|
+
displayValue = (
|
|
79
|
+
<span className="kpi-value-with-total">
|
|
80
|
+
<span className="kpi-main-value">{formattedValue}</span>
|
|
81
|
+
<span className="kpi-separator"> / </span>
|
|
82
|
+
<span className="kpi-total-value">{formattedTotal}</span>
|
|
83
|
+
{/* On peut ajouter l'unitci si pertinent */}
|
|
84
|
+
{unit ? <span className="kpi-unit-suffix"> {unit}</span> : ''}
|
|
85
|
+
{showPercentTotal && totalCount !== 0 && (<span className="kpi-percentage">
|
|
86
|
+
({((value / totalCount) * 100).toFixed(1)}%)
|
|
87
|
+
</span>)}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
} else if (valueExists) {
|
|
91
|
+
displayValue = (
|
|
92
|
+
<span className="kpi-value-simple">
|
|
93
|
+
<span className="kpi-value-number">{formattedValue}</span>
|
|
94
|
+
{unit ? <span className="kpi-unit-suffix"> {unit}</span> : ''}
|
|
95
|
+
{showPercentTotal && totalCount !== 0 && (<span className="kpi-percentage">
|
|
96
|
+
({((value / totalCount) * 100).toFixed(1)}%)
|
|
97
|
+
</span>)}
|
|
98
|
+
</span>
|
|
99
|
+
);
|
|
100
|
+
// 4. Cas où la valeur n'existe pas (mais pas d'erreur/chargement)
|
|
101
|
+
} else {
|
|
102
|
+
displayValue = <span className="kpi-value-nodata">-</span>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
} else {
|
|
106
|
+
displayValue = <span className="kpi-value-nodata">-</span>; // Cas où kpiData est null/undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
const IconComponent = getIconComponent(iconName);
|
|
111
|
+
|
|
112
|
+
// Style pour la couleur de fond
|
|
113
|
+
const widgetStyle = color ? { backgroundColor: color, color: isLightColor(color) ? 'black' : 'white' } : {};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className={`kpi-widget ${isLoading ? 'loading' : ''} ${error ? 'error' : ''}`} style={widgetStyle}>
|
|
117
|
+
<button className="kpi-remove-button" onClick={onRemove} title="Retirer ce KPI">
|
|
118
|
+
<FaTimes />
|
|
119
|
+
</button>
|
|
120
|
+
|
|
121
|
+
{IconComponent && <div className="kpi-icon">{IconComponent}</div>}
|
|
122
|
+
<div className="kpi-content">
|
|
123
|
+
<div className="kpi-title">
|
|
124
|
+
<div className={"kpi-name"}>{name.value || 'KPI Title'}</div>
|
|
125
|
+
{description && ( // Condition pour afficher l'icône info seulement si description existe
|
|
126
|
+
<div className="kpi-info" data-tooltip-id={`tooltip-desc-${_id}`} data-tooltip-content={description.value}>
|
|
127
|
+
<FaInfo />
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
<div className="kpi-value">{displayValue}</div>
|
|
132
|
+
</div>
|
|
133
|
+
{/* Le Tooltip doit être unique par widget si plusieurs descriptions existent */}
|
|
134
|
+
{description && <Tooltip id={`tooltip-desc-${_id}`} />}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export default KPIWidget;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// client/src/KanbanCard.jsx
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {useTranslation} from "react-i18next";
|
|
5
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
6
|
+
import {getDataAsString} from "data-primals-engine/data";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const KanbanCard = ({ card, model, subItemsField }) => {
|
|
10
|
+
|
|
11
|
+
const {models} = useModelContext()
|
|
12
|
+
const subItems = subItemsField && card[subItemsField] ? card[subItemsField] : [];
|
|
13
|
+
const tr = useTranslation();
|
|
14
|
+
// Logique du Drag & Drop natif pour la carte
|
|
15
|
+
|
|
16
|
+
const lang = (tr.i18n.resolvedLanguage || tr.i18n.language).split(/[-_]/)?.[0];
|
|
17
|
+
|
|
18
|
+
const getfield = (model, data) => {
|
|
19
|
+
const v = getDataAsString(model, data, tr, models);
|
|
20
|
+
if(!v){
|
|
21
|
+
return model.fields.map((field) => {
|
|
22
|
+
if( field.type === 'array' && field.itemsType === 'file'){
|
|
23
|
+
return card[field.name].map(it => {
|
|
24
|
+
const r = `
|
|
25
|
+
<strong>filename</strong> : ${it.filename}<br />
|
|
26
|
+
<strong>guid</strong> : ${it.guid}<br />
|
|
27
|
+
<strong>type</strong> : ${it.mimeType}<br />
|
|
28
|
+
<strong>size</strong> : ${it.size} bytes<br />
|
|
29
|
+
<strong>timestamp</strong> : ${it.timestamp ? new Date(it.timestamp).toLocaleString(lang) : ''}
|
|
30
|
+
`;
|
|
31
|
+
return <a key={it.guid} href={`/resources/${it.guid}`} target="_blank"
|
|
32
|
+
data-tooltip-id={"tooltipFile"} data-tooltip-html={r}
|
|
33
|
+
rel="noopener noreferrer" onClick={(e) => click(e, i)}><img
|
|
34
|
+
className="image" src={`/resources/${it.guid}`}
|
|
35
|
+
alt={`${it.name} (${it.guid})`}/></a>
|
|
36
|
+
});
|
|
37
|
+
} else if(field.type === 'file'){
|
|
38
|
+
const it = card[field.name];
|
|
39
|
+
const r = `
|
|
40
|
+
<strong>filename</strong> : ${it.filename}<br />
|
|
41
|
+
<strong>guid</strong> : ${it.guid}<br />
|
|
42
|
+
<strong>type</strong> : ${it.mimeType}<br />
|
|
43
|
+
<strong>size</strong> : ${it.size} bytes<br />
|
|
44
|
+
<strong>timestamp</strong> : ${it.timestamp ? new Date(it.timestamp).toLocaleString(lang) : ''}
|
|
45
|
+
`;
|
|
46
|
+
return <a key={it.guid} href={`/resources/${it.guid}`} target="_blank"
|
|
47
|
+
data-tooltip-id={"tooltipFile"} data-tooltip-html={r}
|
|
48
|
+
rel="noopener noreferrer" onClick={(e) => click(e, i)}><img
|
|
49
|
+
className="image" src={`/resources/${it.guid}`}
|
|
50
|
+
alt={`${it.name} (${it.guid})`}/></a>
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}).filter(Boolean)[0];
|
|
54
|
+
}
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className="kanban-card"
|
|
61
|
+
>
|
|
62
|
+
<div className="kanban-card-title">
|
|
63
|
+
{getfield(model, card)}
|
|
64
|
+
</div>
|
|
65
|
+
{subItems.length > 0 && (
|
|
66
|
+
<div className="kanban-card-subitems">
|
|
67
|
+
{subItems.map((subItem, index) => {
|
|
68
|
+
const it = subItem;
|
|
69
|
+
const v = getDataAsString(model, subItem, tr, models);
|
|
70
|
+
if (!v) {
|
|
71
|
+
const r = `
|
|
72
|
+
<strong>filename</strong> : ${it.filename}<br />
|
|
73
|
+
<strong>guid</strong> : ${it.guid}<br />
|
|
74
|
+
<strong>type</strong> : ${it.mimeType}<br />
|
|
75
|
+
<strong>size</strong> : ${it.size} bytes<br />
|
|
76
|
+
<strong>timestamp</strong> : ${it.timestamp ? new Date(it.timestamp).toLocaleString(lang) : ''}
|
|
77
|
+
`;
|
|
78
|
+
return <a key={it.guid} href={`/resources/${it.guid}`} target="_blank"
|
|
79
|
+
data-tooltip-id={"tooltipFile"} data-tooltip-html={r}
|
|
80
|
+
rel="noopener noreferrer" onClick={(e) => click(e, i)}><img
|
|
81
|
+
className="image" src={`/resources/${it.guid}`}
|
|
82
|
+
alt={`${it.name} (${it.guid})`}/></a>
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default KanbanCard;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// client/src/KanbanColumn.jsx
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import KanbanCard from './KanbanCard.jsx';
|
|
5
|
+
|
|
6
|
+
const KanbanColumn = ({ columnId, column, model, subItemsField, handleDrop }) => {
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="kanban-column">
|
|
10
|
+
<h3 className="kanban-column-title">
|
|
11
|
+
{column.title} ({column.items.length})
|
|
12
|
+
</h3>
|
|
13
|
+
<div
|
|
14
|
+
className={`kanban-column-content`}
|
|
15
|
+
>
|
|
16
|
+
{column.items.map((item, index) => (
|
|
17
|
+
<KanbanCard
|
|
18
|
+
key={item._id}
|
|
19
|
+
card={item}
|
|
20
|
+
model={model}
|
|
21
|
+
subItemsField={subItemsField}
|
|
22
|
+
/>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default KanbanColumn;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// client/src/KanbanConfigModal.jsx
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
5
|
+
import { Dialog } from './Dialog.jsx';
|
|
6
|
+
import Button from './Button.jsx';
|
|
7
|
+
import { SelectField } from './Field.jsx';
|
|
8
|
+
|
|
9
|
+
const KanbanConfigModal = ({ isOpen, onClose, onSave, model, initialSettings }) => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
|
|
12
|
+
// --- MODIFICATION 1 : Simplifier l'initialisation de l'--
|
|
13
|
+
// On initialise les états à vide. Le useEffect s'occupera de tout.
|
|
14
|
+
const [groupByField, setGroupByField] = useState('');
|
|
15
|
+
const [subItemsField, setSubItemsField] = useState('');
|
|
16
|
+
|
|
17
|
+
const modelFields = model?.fields || [];
|
|
18
|
+
|
|
19
|
+
const groupableFields = useMemo(() => {
|
|
20
|
+
// ... (pas de changement ici)
|
|
21
|
+
return modelFields
|
|
22
|
+
.filter(field => ['select', 'string', 'string_t'].includes(field.type))
|
|
23
|
+
.map(field => ({
|
|
24
|
+
label: t(`field_${model.name}_${field.name}`, field.name),
|
|
25
|
+
value: field.name,
|
|
26
|
+
}));
|
|
27
|
+
}, [modelFields, t]);
|
|
28
|
+
|
|
29
|
+
const subItemFields = useMemo(() => {
|
|
30
|
+
// ... (pas de changement ici)
|
|
31
|
+
return modelFields
|
|
32
|
+
.filter(field => field.type === 'relation' && field.multiple)
|
|
33
|
+
.map(field => ({
|
|
34
|
+
label: t(`field_${field.name}`, { defaultValue: field.name }),
|
|
35
|
+
value: field.name,
|
|
36
|
+
}));
|
|
37
|
+
}, [modelFields, t]);
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// --- MODIFICATION 2 : La logique d'initialisation est centralisée ici ---
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// Cet effet s'exécute chaque fois que la modale est ouverte.
|
|
43
|
+
if (isOpen) {
|
|
44
|
+
// S'il y a une configuration initiale (mode édition), on charge ses valeurs.
|
|
45
|
+
if (initialSettings && initialSettings.groupByField) {
|
|
46
|
+
setGroupByField(initialSettings.groupByField);
|
|
47
|
+
setSubItemsField(initialSettings.subItemsField || '');
|
|
48
|
+
} else {
|
|
49
|
+
// Sinon (nouvelle configuration), on initialise avec les valeurs par défaut.
|
|
50
|
+
// Cela corrige aussi le bug du premier chargement.
|
|
51
|
+
setGroupByField(groupableFields[0]?.value || '');
|
|
52
|
+
setSubItemsField('');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, [isOpen, initialSettings, groupableFields]); // On ajoute groupableFields aux dépendances
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const handleSave = () => {
|
|
59
|
+
if (!groupByField) {
|
|
60
|
+
alert(t('kanban.config.groupByRequired', 'Le champ de regroupement est obligatoire.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Pour être sûr de ne pas envoyer une valeur vide si isClearable a été utilisé
|
|
64
|
+
onSave({ groupByField, subItemsField: subItemsField || '' });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// --- MODIFICATION 3 : Supprimer le useEffect fautif ---
|
|
68
|
+
// useEffect(() => {
|
|
69
|
+
// setGroupByField(groupableFields[0]?.value || '');
|
|
70
|
+
// }, []); // <--- CE BLOC EST SUPPRIMÉ
|
|
71
|
+
|
|
72
|
+
if (!isOpen) return null;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog isModal={true} isClosable={true} onClose={onClose}>
|
|
76
|
+
<div className="kanban-config-modal p-4">
|
|
77
|
+
<h2><Trans i18nKey="kanban.config.title">Configurer la vue Kanban</Trans></h2>
|
|
78
|
+
<p><Trans i18nKey="kanban.config.description">Choisissez le champ qui servira à créer les colonnes du tableau Kanban.</Trans></p>
|
|
79
|
+
|
|
80
|
+
<div className="my-4">
|
|
81
|
+
<label htmlFor="groupByField" className="block text-sm font-medium text-gray-700"><Trans i18nKey="kanban.config.groupByLabel">Regrouper par</Trans></label>
|
|
82
|
+
<SelectField
|
|
83
|
+
id="groupByField"
|
|
84
|
+
name="groupByField"
|
|
85
|
+
value={groupByField}
|
|
86
|
+
onChange={e => setGroupByField(e?.value || '')} // Gérer le cas où on efface la sélection
|
|
87
|
+
items={groupableFields}
|
|
88
|
+
placeholder={t('kanban.config.selectField', 'Sélectionner un champ...')}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="my-4">
|
|
93
|
+
<label htmlFor="subItemsField" className="block text-sm font-medium text-gray-700"><Trans i18nKey="kanban.config.subItemsLabel">Champ des sous-éléments (optionnel)</Trans></label>
|
|
94
|
+
<SelectField
|
|
95
|
+
id="subItemsField"
|
|
96
|
+
name="subItemsField"
|
|
97
|
+
isClearable={true}
|
|
98
|
+
value={subItemsField}
|
|
99
|
+
onChange={e => setSubItemsField(e?.value || '')} // Gérer le cas où on efface la sélection
|
|
100
|
+
items={subItemFields}
|
|
101
|
+
placeholder={t('kanban.config.selectSubItemsField', 'Sélectionner un champ...')}
|
|
102
|
+
/>
|
|
103
|
+
<p className="text-xs text-gray-500 mt-1"><Trans i18nKey="kanban.config.subItemsHint">Sélectionnez un champ de type relation multiple pour afficher des sous-éléments dans les cartes.</Trans></p>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="flex justify-end gap-2 mt-6">
|
|
107
|
+
<Button onClick={onClose} className="btn-secondary"><Trans i18nKey="btns.cancel">Annuler</Trans></Button>
|
|
108
|
+
<Button onClick={handleSave} className="btn-primary"><Trans i18nKey="btns.save">Enregistrer</Trans></Button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</Dialog>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default KanbanConfigModal;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// client/src/KanbanView.jsx
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
|
5
|
+
import KanbanColumn from './KanbanColumn.jsx';
|
|
6
|
+
import { useNotificationContext } from "./NotificationProvider.jsx";
|
|
7
|
+
import "./KanbanView.scss";
|
|
8
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx"; // Assurez-vous d'importer le style
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Traite les données brutes pour les structurer pour le tableau Kanban.
|
|
12
|
+
* @param {Array<Object>} data - La liste des éléments du modèle.
|
|
13
|
+
* @param {string} groupByField - Le nom du champ utilisé pour grouper en colonnes.
|
|
14
|
+
* @param {Object} model - Le modèle de données complet pour accéder aux options des champs.
|
|
15
|
+
* @returns {Object} Un objet où les clés sont les valeurs du champ de regroupement.
|
|
16
|
+
*/
|
|
17
|
+
const processDataForKanban = (data, groupByField, model) => {
|
|
18
|
+
if (!data || !groupByField || !model) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 1. Grouper les items existants
|
|
23
|
+
const grouped = data.reduce((acc, item) => {
|
|
24
|
+
const columnKey = item[groupByField]?.value || item[groupByField] || 'Non classé';
|
|
25
|
+
|
|
26
|
+
if (!acc[columnKey]) {
|
|
27
|
+
acc[columnKey] = {
|
|
28
|
+
title: columnKey,
|
|
29
|
+
items: []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
acc[columnKey].items.push(item);
|
|
33
|
+
return acc;
|
|
34
|
+
}, {});
|
|
35
|
+
|
|
36
|
+
// 2. S'assurer que toutes les colonnes possibles existent, même si elles sont vides.
|
|
37
|
+
const fieldSchema = model.fields.find(f => f.name === groupByField);
|
|
38
|
+
if (fieldSchema && fieldSchema.type === 'select' && Array.isArray(fieldSchema.options)) {
|
|
39
|
+
fieldSchema.options.forEach(option => {
|
|
40
|
+
const optionValue = typeof option === 'object' ? option.value : option;
|
|
41
|
+
if (!grouped[optionValue]) {
|
|
42
|
+
grouped[optionValue] = { title: optionValue, items: [] };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!grouped['Non classé']) {
|
|
48
|
+
grouped['Non classé'] = { title: 'Non classé', items: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return grouped;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const KanbanView = ({ settings, model }) => {
|
|
55
|
+
const queryClient = useQueryClient();
|
|
56
|
+
const { addNotification } = useNotificationContext();
|
|
57
|
+
const { me } = useAuthContext();
|
|
58
|
+
const [columns, setColumns] = useState({});
|
|
59
|
+
|
|
60
|
+
// Requête pour récupérer toutes les données du modèle nécessaires pour le Kanban
|
|
61
|
+
const { data: kanbanData, isLoading, isError } = useQuery(
|
|
62
|
+
['kanbanData', model.name], // Clé de requête unique pour ce modèle
|
|
63
|
+
() => fetch(`/api/data/search?_user=${me.username}&depth=2`, { // La fonction qui appelle l'API
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
// On demande toutes les données pour le modèle spécifié (limit: 0 pour "tout")
|
|
67
|
+
body: JSON.stringify({ model: model.name, page: 1 })
|
|
68
|
+
}).then(res => res.json()),
|
|
69
|
+
{
|
|
70
|
+
// N'exécuter la requête que si le modèle et le champ de regroupement sont définis
|
|
71
|
+
enabled: !!model?.name && !!settings.groupByField,
|
|
72
|
+
// Extraire le tableau de données de la réponse de l'API
|
|
73
|
+
select: (data) => data.data || [],
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (kanbanData) {
|
|
79
|
+
const processedColumns = processDataForKanban(kanbanData, settings.groupByField, model);
|
|
80
|
+
setColumns(processedColumns);
|
|
81
|
+
}
|
|
82
|
+
}, [kanbanData, settings.groupByField, model]);
|
|
83
|
+
|
|
84
|
+
// Helper pour trouver la carte et sa colonne d'origine dans notre état local
|
|
85
|
+
const findCardAndSourceColumn = (cardId) => {
|
|
86
|
+
for (const [columnId, column] of Object.entries(columns)) {
|
|
87
|
+
const card = column.items.find(item => item._id.toString() === cardId);
|
|
88
|
+
if (card) {
|
|
89
|
+
return { card, sourceColumnId: columnId };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { card: null, sourceColumnId: null };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (isLoading) return <div>Chargement du Kanban...</div>;
|
|
96
|
+
if (isError) return <div>Erreur lors du chargement des données.</div>;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="kanban-board">
|
|
100
|
+
{Object.entries(columns).map(([columnId, column]) => (
|
|
101
|
+
<KanbanColumn
|
|
102
|
+
key={columnId}
|
|
103
|
+
columnId={columnId}
|
|
104
|
+
column={column}
|
|
105
|
+
model={model}
|
|
106
|
+
subItemsField={settings.subItemsField}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default KanbanView;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// C:/Dev/hackersonline-engine/client/src/KanbanView.scss
|
|
2
|
+
|
|
3
|
+
// --- Variables pour une personnalisation facile ---
|
|
4
|
+
$board-bg: #f4f5f7; // Fond gris clair pour le tableau
|
|
5
|
+
$column-bg: #ebecf0; // Fond légèrement plus foncé pour les colonnes
|
|
6
|
+
$card-bg: #ffffff; // Fond blanc pour les cartes
|
|
7
|
+
$subitems-bg: #f9f9f9; // Fond très clair pour la liste des sous-éléments
|
|
8
|
+
$text-color-dark: #172b4d; // Couleur de texte principale
|
|
9
|
+
$text-color-light: #5e6c84; // Couleur de texte secondaire
|
|
10
|
+
$border-color: #dcdfe4; // Couleur de bordure subtile
|
|
11
|
+
$primary-color: #007bff; // Couleur d'accentuation (pour le survol)
|
|
12
|
+
$border-radius: 5px;
|
|
13
|
+
$column-width: 300px;
|
|
14
|
+
$spacing-unit: 8px;
|
|
15
|
+
|
|
16
|
+
// --- Styles globaux du tableau Kanban ---
|
|
17
|
+
.kanban-board {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: flex-start; // Aligne les colonnes en haut
|
|
20
|
+
gap: $spacing-unit * 2;
|
|
21
|
+
padding: $spacing-unit * 2;
|
|
22
|
+
background-color: $board-bg;
|
|
23
|
+
height: calc(100vh - 100px); // Ajustez cette valeur selon la hauteur de votre barre de navigation
|
|
24
|
+
overflow-x: auto; // Permet le défilement horizontal si les colonnes débordent
|
|
25
|
+
overflow-y: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Style pour une Colonne ---
|
|
29
|
+
.kanban-column {
|
|
30
|
+
flex: 0 0 $column-width; // Empêche la colonne de s'étirer/rétrécir
|
|
31
|
+
width: $column-width;
|
|
32
|
+
max-height: 100%;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
background-color: $column-bg;
|
|
36
|
+
border-radius: $border-radius;
|
|
37
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
38
|
+
|
|
39
|
+
.kanban-column-title {
|
|
40
|
+
padding: $spacing-unit * 1.5;
|
|
41
|
+
font-size: 1rem;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
color: $text-color-dark;
|
|
44
|
+
border-bottom: 1px solid $border-color;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.kanban-column-content {
|
|
48
|
+
padding: $spacing-unit;
|
|
49
|
+
overflow-y: auto; // Défilement vertical pour les cartes
|
|
50
|
+
flex-grow: 1;
|
|
51
|
+
|
|
52
|
+
// Stylisation de la barre de défilement
|
|
53
|
+
&::-webkit-scrollbar {
|
|
54
|
+
width: 6px;
|
|
55
|
+
}
|
|
56
|
+
&::-webkit-scrollbar-thumb {
|
|
57
|
+
background-color: #c4c9d2;
|
|
58
|
+
border-radius: 3px;
|
|
59
|
+
}
|
|
60
|
+
&::-webkit-scrollbar-track {
|
|
61
|
+
background: transparent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Style pour une Carte ---
|
|
67
|
+
.kanban-card {
|
|
68
|
+
background-color: $card-bg;
|
|
69
|
+
border-radius: $border-radius;
|
|
70
|
+
box-shadow: 0 1px 3px rgba(9, 30, 66, 0.15), 0 0 1px rgba(9, 30, 66, 0.2);
|
|
71
|
+
padding: $spacing-unit * 1.5;
|
|
72
|
+
margin-bottom: $spacing-unit;
|
|
73
|
+
color: $text-color-dark;
|
|
74
|
+
font-size: 0.9rem;
|
|
75
|
+
border-left: 4px solid transparent; // Pour une future indication de couleur/priorité
|
|
76
|
+
transition: box-shadow 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
|
77
|
+
|
|
78
|
+
&:hover {
|
|
79
|
+
box-shadow: 0 4px 8px rgba(9, 30, 66, 0.2), 0 0 1px rgba(9, 30, 66, 0.25);
|
|
80
|
+
border-left-color: $primary-color; // Couleur bleue au survol
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.kanban-card-title {
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
line-height: 1.3;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Style pour la liste des Sous-Cartes ---
|
|
89
|
+
.kanban-card-subitems {
|
|
90
|
+
margin-top: $spacing-unit * 1.5;
|
|
91
|
+
padding: $spacing-unit;
|
|
92
|
+
background-color: $subitems-bg;
|
|
93
|
+
border: 1px solid #e9e9e9;
|
|
94
|
+
border-radius: $border-radius;
|
|
95
|
+
|
|
96
|
+
.kanban-card-subitem {
|
|
97
|
+
padding: $spacing-unit / 2;
|
|
98
|
+
font-size: 0.85rem;
|
|
99
|
+
color: $text-color-light;
|
|
100
|
+
border-bottom: 1px solid #e9e9e9;
|
|
101
|
+
|
|
102
|
+
&:last-child {
|
|
103
|
+
border-bottom: none;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/components/MessageRotator.jsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import './MessageRotator.scss'; // Nous allons créer ce fichier CSS pour l'effet de fondu
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Affiche une série de messages les uns après les autres avec un effet de fondu.
|
|
8
|
+
*
|
|
9
|
+
* @param {object[]} messages - Tableau d'objets messages. Chaque objet peut contenir:
|
|
10
|
+
* - text: (string) Le texte du message.
|
|
11
|
+
* - duration: (number, optionnel) Durée d'affichage en millisecondes pour ce message spécifique.
|
|
12
|
+
* @param {number} defaultDelay - Délai d'affichage par défaut en millisecondes si `duration` n'est pas spécifié pour un message.
|
|
13
|
+
* @param {number} fadeDuration - Durée de l'effet de fondu en millisecondes.
|
|
14
|
+
* @param {string} className - Classe CSS optionnelle pour le conteneur principal.
|
|
15
|
+
*/
|
|
16
|
+
const MessageRotator = ({ messages, defaultDelay = 3000, fadeDuration = 500, className = '' }) => {
|
|
17
|
+
const [currentIndex, setCurrentIndex] = React.useState(0);
|
|
18
|
+
const [isVisible, setIsVisible] = React.useState(true);
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (!messages || messages.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Déclenche le fondu entrant
|
|
26
|
+
setIsVisible(true);
|
|
27
|
+
|
|
28
|
+
const currentMessage = messages[currentIndex];
|
|
29
|
+
const displayDuration = currentMessage.duration || defaultDelay;
|
|
30
|
+
|
|
31
|
+
// Timer pour le fondu sortant
|
|
32
|
+
const fadeOutTimer = setTimeout(() => {
|
|
33
|
+
setIsVisible(false);
|
|
34
|
+
}, displayDuration);
|
|
35
|
+
|
|
36
|
+
// Timer pour passer au message suivant (après la durée du fondu sortant)
|
|
37
|
+
const nextMessageTimer = setTimeout(() => {
|
|
38
|
+
setCurrentIndex((prevIndex) => (prevIndex + 1) % messages.length);
|
|
39
|
+
}, displayDuration + fadeDuration);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
clearTimeout(fadeOutTimer);
|
|
43
|
+
clearTimeout(nextMessageTimer);
|
|
44
|
+
};
|
|
45
|
+
}, [currentIndex, messages, defaultDelay, fadeDuration]);
|
|
46
|
+
|
|
47
|
+
if (!messages || messages.length === 0) {
|
|
48
|
+
return null; // Ne rien afficher si pas de messages
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const messageStyle = {
|
|
52
|
+
transition: `opacity ${fadeDuration}ms ease-in-out`,
|
|
53
|
+
opacity: isVisible ? 1 : 0,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={`message-rotator-container ${className}`}>
|
|
58
|
+
<div style={messageStyle} className="message-rotator-text">
|
|
59
|
+
{messages[currentIndex].text}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
MessageRotator.propTypes = {
|
|
66
|
+
messages: PropTypes.arrayOf(
|
|
67
|
+
PropTypes.shape({
|
|
68
|
+
text: PropTypes.string.isRequired,
|
|
69
|
+
duration: PropTypes.number,
|
|
70
|
+
})
|
|
71
|
+
).isRequired,
|
|
72
|
+
defaultDelay: PropTypes.number,
|
|
73
|
+
fadeDuration: PropTypes.number,
|
|
74
|
+
className: PropTypes.string,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default MessageRotator;
|