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.
Files changed (166) hide show
  1. package/client/README.md +8 -0
  2. package/client/index.html +31 -0
  3. package/client/index.js +25 -0
  4. package/client/package-lock.json +11664 -0
  5. package/client/package.json +70 -0
  6. package/client/public/demo/26899917-d1ba-4df4-bb33-48d09a8778da.jpg +0 -0
  7. package/client/public/demo/4b9894c7-12cd-466d-8fd3-a2757b09ec96.jpg +0 -0
  8. package/client/public/demo/7ed369ac-a1a2-4b45-b0cd-4fd5bcf5a75a.jpg +0 -0
  9. package/client/public/doc/API.postman_collection.json +214 -0
  10. package/client/public/github.svg +1 -0
  11. package/client/public/profilCompany.jpg +0 -0
  12. package/client/public/profilDeveloper.jpg +0 -0
  13. package/client/public/profilEngineer.jpg +0 -0
  14. package/client/public/profilPersonal.jpg +0 -0
  15. package/client/public/profils .psd +0 -0
  16. package/client/public/react.svg +9 -0
  17. package/client/src/APIInfo.jsx +328 -0
  18. package/client/src/AddWidgetTypeModal.jsx +44 -0
  19. package/client/src/AddWidgetTypeModal.scss +87 -0
  20. package/client/src/App.css +42 -0
  21. package/client/src/App.jsx +586 -0
  22. package/client/src/App.scss +1466 -0
  23. package/client/src/AssistantChat.jsx +280 -0
  24. package/client/src/AssistantChat.scss +229 -0
  25. package/client/src/Button.jsx +12 -0
  26. package/client/src/Button.scss +197 -0
  27. package/client/src/CalculationBuilder.jsx +338 -0
  28. package/client/src/CalculationStepRow.jsx +199 -0
  29. package/client/src/CalendarConfigModal.jsx +49 -0
  30. package/client/src/ChartConfigModal.jsx +402 -0
  31. package/client/src/ConditionBuilder.jsx +622 -0
  32. package/client/src/ConditionBuilder.scss +672 -0
  33. package/client/src/ConditionBuilder2.jsx +757 -0
  34. package/client/src/ContentView.jsx +274 -0
  35. package/client/src/CronBuilder.jsx +126 -0
  36. package/client/src/CronBuilder.scss +128 -0
  37. package/client/src/CronPartBuilder.jsx +150 -0
  38. package/client/src/Dashboard.jsx +210 -0
  39. package/client/src/Dashboard.scss +445 -0
  40. package/client/src/DashboardChart.jsx +362 -0
  41. package/client/src/DashboardFlexViewItem.jsx +129 -0
  42. package/client/src/DashboardView.jsx +554 -0
  43. package/client/src/DataChart.jsx +349 -0
  44. package/client/src/DataEditor.jsx +556 -0
  45. package/client/src/DataLayout.jsx +709 -0
  46. package/client/src/DataTable.jsx +996 -0
  47. package/client/src/DataTable.scss +73 -0
  48. package/client/src/Dialog.jsx +90 -0
  49. package/client/src/Dialog.scss +117 -0
  50. package/client/src/DisplayFlexNodeRenderer.jsx +115 -0
  51. package/client/src/DocumentationPageLayout.jsx +93 -0
  52. package/client/src/DocumentationPageLayout.scss +262 -0
  53. package/client/src/Draggable.jsx +222 -0
  54. package/client/src/Draggable.scss +49 -0
  55. package/client/src/ExportDialog.jsx +172 -0
  56. package/client/src/Field.jsx +1558 -0
  57. package/client/src/FlexBuilder.jsx +269 -0
  58. package/client/src/FlexBuilder.scss +226 -0
  59. package/client/src/FlexBuilderControls.jsx +170 -0
  60. package/client/src/FlexBuilderModal.jsx +75 -0
  61. package/client/src/FlexBuilderModal.scss +109 -0
  62. package/client/src/FlexBuilderPreview.jsx +48 -0
  63. package/client/src/FlexBuilderUtils.js +84 -0
  64. package/client/src/FlexDataRenderer.jsx +170 -0
  65. package/client/src/FlexDataRenderer.scss +80 -0
  66. package/client/src/FlexNode.jsx +290 -0
  67. package/client/src/FlexTreeUtils.js +129 -0
  68. package/client/src/HiddenableCell.jsx +46 -0
  69. package/client/src/HiddenableCell.scss +36 -0
  70. package/client/src/KPIDialog.jsx +39 -0
  71. package/client/src/KPIWidget.jsx +139 -0
  72. package/client/src/KanbanCard.jsx +92 -0
  73. package/client/src/KanbanColumn.jsx +29 -0
  74. package/client/src/KanbanConfigModal.jsx +115 -0
  75. package/client/src/KanbanView.jsx +113 -0
  76. package/client/src/KanbanView.scss +107 -0
  77. package/client/src/MessageRotator.jsx +77 -0
  78. package/client/src/MessageRotator.scss +14 -0
  79. package/client/src/ModelCreator.jsx +640 -0
  80. package/client/src/ModelCreator.scss +154 -0
  81. package/client/src/ModelCreatorField.jsx +810 -0
  82. package/client/src/ModelImporter.jsx +120 -0
  83. package/client/src/ModelList.jsx +205 -0
  84. package/client/src/Notification.jsx +137 -0
  85. package/client/src/Notification.scss +126 -0
  86. package/client/src/NotificationProvider.jsx +73 -0
  87. package/client/src/PackGallery.jsx +211 -0
  88. package/client/src/PackGallery.scss +156 -0
  89. package/client/src/Pagination.jsx +141 -0
  90. package/client/src/RTE.jsx +415 -0
  91. package/client/src/RTETrans.jsx +103 -0
  92. package/client/src/RelationField.jsx +247 -0
  93. package/client/src/RelationValue.jsx +230 -0
  94. package/client/src/RestoreConfirmationModal.jsx +183 -0
  95. package/client/src/RestoreDialog.jsx +134 -0
  96. package/client/src/RestoreDialog.scss +67 -0
  97. package/client/src/RichTextEditorModal.jsx +34 -0
  98. package/client/src/RichTextEditorModal.scss +54 -0
  99. package/client/src/TourSpotlight.jsx +234 -0
  100. package/client/src/TourSpotlight.scss +100 -0
  101. package/client/src/TutorialsMenu.jsx +83 -0
  102. package/client/src/TutorialsMenu.scss +217 -0
  103. package/client/src/TutorialsRewardModal.jsx +65 -0
  104. package/client/src/Utils.jsx +78 -0
  105. package/client/src/ViewSwitcher.jsx +54 -0
  106. package/client/src/ViewSwitcher.scss +74 -0
  107. package/client/src/Webpage.jsx +69 -0
  108. package/client/src/_variables.scss +21 -0
  109. package/client/src/assets/react.svg +1 -0
  110. package/client/src/constants.js +68 -0
  111. package/client/src/contexts/AuthContext.jsx +28 -0
  112. package/client/src/contexts/ModelContext.jsx +328 -0
  113. package/client/src/contexts/UIContext.jsx +52 -0
  114. package/client/src/core/data.js +35 -0
  115. package/client/src/entry-client.jsx +15 -0
  116. package/client/src/entry-server.jsx +13 -0
  117. package/client/src/filter.js +222 -0
  118. package/client/src/hooks/data.js +101 -0
  119. package/client/src/hooks/useDebounce.js +20 -0
  120. package/client/src/hooks/useDragAndDrop.js +59 -0
  121. package/client/src/hooks/useLocalStorage.js +100 -0
  122. package/client/src/hooks/useMount.js +42 -0
  123. package/client/src/hooks/useTutorials.jsx +227 -0
  124. package/client/src/hooks/useWindowSize.js +24 -0
  125. package/client/src/i18n.js +47 -0
  126. package/client/src/index.css +90 -0
  127. package/client/src/index.js +10 -0
  128. package/client/src/main.jsx +11 -0
  129. package/client/src/translations.js +15940 -0
  130. package/client/src/tutorials.js +112 -0
  131. package/client/vite.config.js +30 -0
  132. package/eslint.config.js +50 -0
  133. package/package.json +48 -27
  134. package/server.js +9 -0
  135. package/src/constants.js +12 -23
  136. package/src/core.js +9 -8
  137. package/src/data.js +81 -80
  138. package/src/defaultModels.js +114 -115
  139. package/src/email.js +8 -9
  140. package/src/engine.js +5 -5
  141. package/src/events.js +1 -2
  142. package/src/i18n.js +212 -211
  143. package/src/middlewares/middleware-mongodb.js +94 -93
  144. package/src/migrate.js +24 -24
  145. package/src/modules/assistant.js +55 -55
  146. package/src/modules/bucket.js +10 -9
  147. package/src/modules/data.js +387 -357
  148. package/src/modules/file.js +3 -6
  149. package/src/modules/mongodb.js +6 -7
  150. package/src/modules/user.js +24 -47
  151. package/src/modules/workflow.js +68 -67
  152. package/src/openai.jobs.js +2 -2
  153. package/src/packs.js +30 -31
  154. package/src/providers.js +101 -3
  155. package/src/setenv.js +1 -1
  156. package/src/user.js +0 -17
  157. package/src/workers/crypto-worker.js +21 -20
  158. package/src/workers/import-export-worker.js +18 -18
  159. package/test/data.backup.integration.test.js +7 -12
  160. package/test/data.integration.test.js +27 -25
  161. package/test/globalSetup.js +2 -2
  162. package/test/import_export.integration.test.js +6 -6
  163. package/test/model.integration.test.js +21 -22
  164. package/test/workflow.integration.test.js +2 -5
  165. package/test/workflow.robustness.test.js +6 -13
  166. 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">&nbsp;
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">&nbsp;
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;