data-primals-engine 1.1.7-rc1 → 1.1.7

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.
@@ -55,12 +55,13 @@ import {useCookies, CookiesProvider} from "react-cookie";
55
55
  import { translations as allTranslations} from "../../src/i18n.js";
56
56
  import {getBrowserRandom, getRandom} from "../../src/core.js";
57
57
  import {getUserHash} from "../../src/data.js";
58
- import {seoTitle} from "./constants.js";
58
+ import {langs, seoTitle} from "./constants.js";
59
59
  import {host, useAI} from "../../src/constants.js";
60
60
  import i18next from "i18next";
61
61
  import {websiteTranslations} from "./translations.js";
62
62
 
63
63
  import { Tooltip } from 'react-tooltip';
64
+ import {availableLangs} from "data-primals-engine/constants";
64
65
 
65
66
  let queryClient = new QueryClient();
66
67
 
@@ -84,7 +85,7 @@ function TopBar({header}) {
84
85
  return <>{header}</>;
85
86
  }
86
87
 
87
- function Layout ({header, translationMutation, routes, body, footer}) {
88
+ function Layout ({header, routes, body, footer}) {
88
89
  const [cookies, setCookie, removeCookie] = useCookies(['username']);
89
90
  const { i18n, t } = useTranslation();
90
91
  const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
@@ -157,8 +158,6 @@ function Layout ({header, translationMutation, routes, body, footer}) {
157
158
  );
158
159
 
159
160
 
160
- const [lastLang, setLastLang] = useState(null);
161
-
162
161
  // AJOUT : Liste des suggestions de prompts
163
162
  const suggestedPrompts = [
164
163
  {
@@ -180,37 +179,11 @@ function Layout ({header, translationMutation, routes, body, footer}) {
180
179
  ];
181
180
 
182
181
  const { setCurrentTourSteps, setAllTourSteps, currentTour, setCurrentTour } = useUI();
183
- const [translations, setTranslations] = useState([]);
184
182
 
185
183
 
186
184
  const isProd = import.meta.env.MODE === 'production';
187
185
  const loc = useLocation();
188
186
 
189
- const changeLanguage = (newLang) => {
190
- if (typeof(newLang) === 'string' && newLang) {
191
-
192
- i18n.changeLanguage(newLang, (err)=>{
193
-
194
- i18next.removeResourceBundle(lang, "translation");
195
- i18next.addResourceBundle(newLang, 'translation', {...websiteTranslations[newLang]['translation'], ...translations[newLang]?.['translation']});
196
-
197
- gtag("event", "change_language "+newLang);
198
- });
199
- }
200
- };
201
-
202
- useEffect(() => {
203
- if( me ){
204
- translationMutation?.mutateAsync(lang).then(d => {
205
- setTranslations(d);
206
- setLastLang(lang);
207
- });
208
- }
209
- }, [me, lang])
210
-
211
- useEffect(() => {
212
- changeLanguage(lang);
213
- }, [lang]);
214
187
 
215
188
  const [currentProfile, setCurrentProfile] = useLocalStorage('profile', null);
216
189
 
@@ -225,24 +198,6 @@ function Layout ({header, translationMutation, routes, body, footer}) {
225
198
  }
226
199
  }
227
200
 
228
- useEffect(() => {
229
- if (Array.isArray(translations)) {
230
- var trs= {};
231
- translations.forEach(tr =>{
232
- trs[tr.key] = tr.value;
233
- });
234
- trs= { ...allTranslations[lang]['translation'], ...trs};
235
-
236
- if( lastLang && lang !== lastLang) {
237
- i18n.removeResourceBundle(lastLang, 'translation');
238
- }
239
- i18n.addResourceBundle(lang, 'translation', trs);
240
- }
241
- }, [translations]);
242
-
243
- useEffect(() => {
244
- changeLanguage(lang);
245
- }, []);
246
201
 
247
202
  const [refreshReducer, refreshUI]= useReducer((n) => n+1, 0,() => 0);
248
203
 
@@ -389,7 +344,9 @@ function Layout ({header, translationMutation, routes, body, footer}) {
389
344
 
390
345
  <div className="flex flex-row flex-no-wrap">
391
346
  <div className="flex flex-1 flex-no-gap home-header">
392
- <a href={"https://github.com/anonympins/data-primals-engine"} target={"_blank"} className="link-top"><img src={"/github.svg"} alt={"Github"} /></a>
347
+ <div className="flex flex-self-end">
348
+ <a href={"https://github.com/anonympins/data-primals-engine"} target={"_blank"} className="link-top"><img src={"/github.svg"} alt={"Github"} /></a>
349
+ </div>
393
350
  <div className="flex prior">
394
351
  <img
395
352
  src="https://web.primals.net/PRIOR.png"
@@ -575,10 +532,59 @@ function UserPage({notifs,triggerSignin, onAuthenticated}) {
575
532
  données</p>}</>;
576
533
  }
577
534
 
535
+ const allLangs = Object.keys(langs).map(l => {
536
+ return {value: l, label: langs[l]};
537
+ })
538
+
578
539
  const BaseLayout=()=>{
540
+ const [lastLang, setLastLang] = useState(null);
541
+ const { i18n, t} = useTranslation();
542
+ const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
543
+ const { me, setMe } = useAuthContext();
544
+ const [translations, setTranslations] = useState([]);
545
+
546
+ useEffect(() => {
547
+ if (Array.isArray(translations)) {
548
+ var trs= {};
549
+ translations.forEach(tr =>{
550
+ trs[tr.key] = tr.value;
551
+ });
552
+ trs= { ...allTranslations[lang]['translation'], ...trs};
553
+
554
+ if( lastLang && lang !== lastLang) {
555
+ i18n.removeResourceBundle(lastLang, 'translation');
556
+ }
557
+ i18n.addResourceBundle(lang, 'translation', trs);
558
+ }
559
+ }, [translations]);
560
+
561
+ const changeLanguage = (newLang) => {
562
+ if (typeof(newLang) === 'string' && newLang) {
563
+
564
+ i18n.changeLanguage(newLang, (err)=>{
565
+
566
+ i18next.removeResourceBundle(lang, "dataEngineTranslations");
567
+ const trs = {...websiteTranslations[newLang]?.['translation']} || {};
568
+ i18next.addResourceBundle(newLang, 'dataEngineTranslations', trs);
569
+
570
+ gtag("event", "change_language "+newLang);
571
+ });
572
+ }
573
+ };
574
+
575
+ useEffect(() => {
576
+ changeLanguage(lang);
577
+ }, [lang]);
578
+
579
579
  return <Layout header={<header className={"flex"}>
580
580
  <Tooltip id={"header"}/>
581
581
  <h1 className="flex-1">{seoTitle}</h1>
582
+ <div className="center">
583
+ <SelectField label={<FaLanguage />} items={allLangs} onChange={(e) => {
584
+ console.log(e.value);
585
+ changeLanguage(e.value);
586
+ }} />
587
+ </div>
582
588
  <div className="flex">
583
589
  <FaQuestion data-tooltip-id="header" data-tooltip-content="Documentation" onClick={()=> {
584
590
  window.open("https://data.primals.net/en/documentation/", "_blank");
@@ -45,6 +45,9 @@ header {
45
45
  &.flex-start {
46
46
  align-items: start;
47
47
  }
48
+ &.flex-self-end {
49
+ align-self: end;
50
+ }
48
51
  .flex-left {
49
52
  justify-content: flex-start;
50
53
  }
@@ -513,7 +516,6 @@ label + .field, label + input, label + select {
513
516
 
514
517
  /* website*/
515
518
  #content {
516
- margin-top: 40px;
517
519
  padding: 0;
518
520
  h1 {
519
521
  margin: 24px 8px;
@@ -66,4 +66,23 @@ export const OPERAND_TYPES = {
66
66
 
67
67
  export const getHost = () => {
68
68
  return process.env.HOST || host || 'localhost';
69
- }
69
+ }
70
+
71
+
72
+
73
+ export const langs = {
74
+ fr: "Français",
75
+ en: "English",
76
+ ar: "عربي",
77
+ fa: "فارسی",
78
+ it: "Italiano",
79
+ es: "Español",
80
+ el: "Ελληνικά",
81
+ de: "Deutsch",
82
+ cs: "Čeština",
83
+ sv: "Svenska",
84
+ pt: "Português",
85
+ ja: "日本語",
86
+ zh: "简体中文",
87
+ ru: "Русский"
88
+ };
@@ -258,4 +258,22 @@ export const pagedFilterToMongoConds = (pagedFilters, model) => {
258
258
  });
259
259
  return c;
260
260
  }
261
+ };
262
+
263
+ /**
264
+ * Remplace les placeholders dans un objet filtre.
265
+ * Gère {{userId}}.
266
+ */
267
+ export const processFilterPlaceholders = (filter, user) => {
268
+ const processedFilter = JSON.parse(JSON.stringify(filter));
269
+ for (const key in processedFilter) {
270
+ if (processedFilter[key] === '{{userId}}') {
271
+ // Dans le système Primals, le champ utilisateur est souvent `_user`
272
+ // et contient le nom d'utilisateur, pas l'ID. Adaptez si besoin.
273
+ processedFilter[key] = user.username;
274
+ } else if (typeof processedFilter[key] === 'object' && processedFilter[key] !== null) {
275
+ processedFilter[key] = processFilterPlaceholders(processedFilter[key], user);
276
+ }
277
+ }
278
+ return processedFilter;
261
279
  };
@@ -52,6 +52,8 @@ export const websiteTranslations = {
52
52
  "modelcreator.hiddenable.hint": "Une option sera disponible pour afficher cette donnée ou la masquer (il sera masqué par défaut).",
53
53
 
54
54
  "tourpoint.importDatapack": "Importez des packs de données pré-construits avec notre galerie de packs : Sites internets, e-commerce, CRM, et plus encore !",
55
+ "tourpoint.tutorials": "Laisse-vous guider en suivant nos tutoriels pas à pas !",
56
+
55
57
  "add_language": "Ajouter...",
56
58
  "select_or_add_language": "Sélectionnez ou ajoutez une langue pour commencer.",
57
59
  "field.richtext_t": "Texte enrichi traduit",
@@ -522,7 +524,7 @@ export const websiteTranslations = {
522
524
  "links.deploy_api": "Déployez votre API",
523
525
  "links.documentation": "Documentation",
524
526
  "links.demo": "Essayez maintenant !",
525
- "filterstringfield.placeholder.regex": 'Expression régulière',
527
+ "filterstringfield.placeholder.regex": 'Expression régulière'
526
528
 
527
529
  }
528
530
  },
@@ -594,6 +596,8 @@ export const websiteTranslations = {
594
596
  "api.data.serverStorageFull": "The server has reached its maximum storage capacity. Please try again later.",
595
597
 
596
598
  "tourpoint.importDatapack": "Import pre-built data packs with our pack gallery: Websites, e-commerce, CRM, and more!",
599
+ "tourpoint.tutorials": "Let yourself be guided by following our step-by-step tutorials!",
600
+
597
601
  "add_language": "Add...",
598
602
  "select_or_add_language": "Select or add a language to get started.",
599
603
  "field.richtext_t": "Translated rich text",
@@ -1925,8 +1929,8 @@ export const websiteTranslations = {
1925
1929
  // channel
1926
1930
  field_channel_name: 'Name',
1927
1931
  field_channel_description: 'Description',
1928
- field_channel_type: 'Type',
1929
- },
1932
+ field_channel_type: 'Type'
1933
+ }
1930
1934
  },
1931
1935
  es: {
1932
1936
  translation: {
@@ -1996,6 +2000,8 @@ export const websiteTranslations = {
1996
2000
  "api.data.serverStorageFull": "El servidor ha alcanzado su capacidad máxima de almacenamiento. Inténtelo de nuevo más tarde.",
1997
2001
 
1998
2002
  "tourpoint.importDatapack": "Importa paquetes de datos prediseñados con nuestra galería de paquetes: sitios web, comercio electrónico, CRM y más",
2003
+ "tourpoint.tutorials": "¡Déjate guiar por nuestros tutoriales paso a paso!",
2004
+
1999
2005
  "add_language": "Añadir...",
2000
2006
  "select_or_add_language": "Seleccione o añada un idioma para empezar.",
2001
2007
  "field.richtext_t": "Texto enriquecido traducido",
@@ -3332,8 +3338,8 @@ export const websiteTranslations = {
3332
3338
  // channel
3333
3339
  field_channel_name: 'Nombre',
3334
3340
  field_channel_description: 'Descripción',
3335
- field_channel_type: 'Tipo',
3336
- },
3341
+ field_channel_type: 'Tipo'
3342
+ }
3337
3343
  },
3338
3344
  pt: {
3339
3345
  translation: {
@@ -3403,6 +3409,7 @@ export const websiteTranslations = {
3403
3409
  "api.data.serverStorageFull": "O servidor atingiu a sua capacidade máxima de armazenamento. Tente novamente mais tarde.",
3404
3410
 
3405
3411
  "tourpoint.importDatapack": "Importe pacotes de dados pré-criados com a nossa galeria de pacotes: sites, comércio eletrónico, CRM e muito mais!",
3412
+ "tourpoint.tutorials": "Deixe-se guiar seguindo os nossos tutoriais passo a passo!",
3406
3413
  "add_language": "Adicionar...",
3407
3414
  "select_or_add_language": "Selecione ou adicione um idioma para começar.",
3408
3415
  "field.richtext_t": "Texto rico traduzido",
@@ -4736,8 +4743,8 @@ export const websiteTranslations = {
4736
4743
  // channel
4737
4744
  field_channel_name: 'Nome',
4738
4745
  field_channel_description: 'Descrição',
4739
- field_channel_type: 'Tipo',
4740
- },
4746
+ field_channel_type: 'Tipo'
4747
+ }
4741
4748
  },
4742
4749
  de: {
4743
4750
  translation: {
@@ -4795,6 +4802,8 @@ export const websiteTranslations = {
4795
4802
  "api.data.serverStorageFull": "Der Server hat seine maximale Speicherkapazität erreicht. Bitte versuchen Sie es später erneut.",
4796
4803
 
4797
4804
  "tourpoint.importDatapack": "Vorgefertigte Datenpakete mit unserer Packgalerie importieren: Websites, E-Commerce, CRM und mehr!",
4805
+ "tourpoint.tutorials": "Lassen Sie sich von unseren Schritt-für-Schritt-Tutorials leiten!",
4806
+
4798
4807
  "add_language": "Hinzufügen...",
4799
4808
  "select_or_add_language": "Wählen oder fügen Sie eine Sprache hinzu, um zu beginnen.",
4800
4809
  "field.richtext_t": "Übersetzter Rich Text",
@@ -6122,8 +6131,8 @@ export const websiteTranslations = {
6122
6131
  // channel
6123
6132
  field_channel_name: 'Name',
6124
6133
  field_channel_description: 'Beschreibung',
6125
- field_channel_type: 'Typ',
6126
- },
6134
+ field_channel_type: 'Typ'
6135
+ }
6127
6136
  },
6128
6137
  it: {
6129
6138
  translation: {
@@ -6193,6 +6202,7 @@ export const websiteTranslations = {
6193
6202
  "api.data.serverStorageFull": "Il server ha raggiunto la capacità massima di archiviazione. Riprova più tardi.",
6194
6203
 
6195
6204
  "tourpoint.importDatapack": "Importa pacchetti di dati predefiniti con la nostra galleria di pacchetti: siti web, e-commerce, CRM e altro ancora!",
6205
+ "tourpoint.tutorials": "Lasciati guidare seguendo i nostri tutorial passo dopo passo!",
6196
6206
  "add_language": "Aggiungi...",
6197
6207
  "select_or_add_language": "Seleziona o aggiungi una lingua per iniziare.",
6198
6208
  "field.richtext_t": "Rich text tradotto",
@@ -7522,8 +7532,8 @@ export const websiteTranslations = {
7522
7532
  // channel
7523
7533
  field_channel_name: 'Nome',
7524
7534
  field_channel_description: 'Descrizione',
7525
- field_channel_type: 'Tipo',
7526
- },
7535
+ field_channel_type: 'Tipo'
7536
+ }
7527
7537
  },
7528
7538
  cs: {
7529
7539
  translation: {
@@ -7593,6 +7603,7 @@ export const websiteTranslations = {
7593
7603
  "api.data.serverStorageFull": "Server dosáhl maximální úložné kapacity. Zkuste to prosím znovu později.",
7594
7604
 
7595
7605
  "tourpoint.importDatapack": "Importujte předpřipravené datové balíčky s naší galerií balíčků: Webové stránky, e-commerce, CRM a další!",
7606
+ "tourpoint.tutorials": "Nechte se vést našimi podrobnými tutoriály!",
7596
7607
  "add_language": "Přidat...",
7597
7608
  "select_or_add_language": "Vyberte nebo přidejte jazyk pro zahájení.",
7598
7609
  "field.richtext_t": "Přeložený formátovaný text",
@@ -8917,8 +8928,8 @@ export const websiteTranslations = {
8917
8928
  // channel
8918
8929
  field_channel_name: 'Jméno',
8919
8930
  field_channel_description: 'Popis',
8920
- field_channel_type: 'Typ',
8921
- },
8931
+ field_channel_type: 'Typ'
8932
+ }
8922
8933
  },
8923
8934
  ru: {
8924
8935
  translation: {
@@ -8988,6 +8999,7 @@ export const websiteTranslations = {
8988
8999
  "api.data.serverStorageFull": "Сервер достиг максимального объёма хранилища. Повторите попытку позже.",
8989
9000
 
8990
9001
  "tourpoint.importDatapack": "Импортируйте готовые пакеты данных с помощью нашей галереи пакетов: веб-сайты, электронная коммерция, CRM и многое другое!",
9002
+ "tourpoint.tutorials": "Доверьтесь нашим пошаговым инструкциям!",
8991
9003
  "add_language": "Добавить...",
8992
9004
  "select_or_add_language": "Выберите или добавьте язык для начала работы",
8993
9005
  "field.richtext_t": "Переведенный форматированный текст",
@@ -10322,8 +10334,8 @@ export const websiteTranslations = {
10322
10334
  // channel
10323
10335
  field_channel_name: 'Имя',
10324
10336
  field_channel_description: 'Описание',
10325
- field_channel_type: 'Тип',
10326
- },
10337
+ field_channel_type: 'Тип'
10338
+ }
10327
10339
  },
10328
10340
  ar: {
10329
10341
  translation: {
@@ -10395,6 +10407,8 @@ export const websiteTranslations = {
10395
10407
  "api.data.storageLimitExceeded": "سيتم تجاوز حد التخزين البالغ {{limit}} ميجابايت بهذه الإضافة.",
10396
10408
  "api.data.serverStorageFull": "وصل الخادم إلى الحد الأقصى لسعة التخزين. يُرجى المحاولة مرة أخرى لاحقًا.",
10397
10409
  "tourpoint.importDatapack": "استورد حزم البيانات المعدة مسبقًا باستخدام معرض الحزم لدينا: مواقع الويب، والتجارة الإلكترونية، وإدارة علاقات العملاء، والمزيد!",
10410
+ "tourpoint.tutorials": "دع نفسك تسترشد باتباع دروسنا التعليمية خطوة بخطوة!",
10411
+
10398
10412
  "add_language": "إضافة...",
10399
10413
  "select_or_add_language": "اختر أو أضف لغة للبدء.",
10400
10414
  "field.richtext_t": "نص منسق مترجم",
@@ -11743,7 +11757,7 @@ export const websiteTranslations = {
11743
11757
  // channel
11744
11758
  field_channel_name: 'الاسم',
11745
11759
  field_channel_description: 'الوصف',
11746
- field_channel_type: 'النوع',
11760
+ field_channel_type: 'النوع'
11747
11761
  }
11748
11762
  },
11749
11763
  sv: {
@@ -11814,6 +11828,7 @@ export const websiteTranslations = {
11814
11828
  "api.data.serverStorageFull": "Servern har nått sin maximala lagringskapacitet. Försök igen senare.",
11815
11829
 
11816
11830
  "tourpoint.importDatapack": "Importera färdiga datapaket med vårt paketgalleri: Webbplatser, e-handel, CRM och mer!",
11831
+ "tourpoint.tutorials": "Låt dig vägledas av våra steg-för-steg-handledningar!",
11817
11832
  "add_language": "Lägg till...",
11818
11833
  "select_or_add_language": "Välj eller lägg till ett språk för att komma igång.",
11819
11834
  "field.richtext_t": "Översatt RTF",
@@ -13138,7 +13153,7 @@ export const websiteTranslations = {
13138
13153
  // channel
13139
13154
  field_channel_name: 'Namn',
13140
13155
  field_channel_description: 'Beskrivning',
13141
- field_channel_type: 'Typ',
13156
+ field_channel_type: 'Typ'
13142
13157
  }
13143
13158
  },
13144
13159
  el: {
@@ -13209,6 +13224,7 @@ export const websiteTranslations = {
13209
13224
  "api.data.serverStorageFull": "Ο διακομιστής έχει φτάσει στη μέγιστη χωρητικότητα αποθήκευσης. Παρακαλώ προσπαθήστε ξανά αργότερα.",
13210
13225
 
13211
13226
  "tourpoint.importDatapack": "Εισαγάγετε προκατασκευασμένα πακέτα δεδομένων με τη συλλογή πακέτων μας: Ιστότοποι, ηλεκτρονικό εμπόριο, CRM και πολλά άλλα!",
13227
+ "tourpoint.tutorials": "Ακολουθώντας τα βήμα προς βήμα εκπαιδευτικά μας βίντεο, αφήστε τον εαυτό σας να σας καθοδηγήσει!",
13212
13228
  "add_language": "Προσθήκη...",
13213
13229
  "select_or_add_language": "Επιλέξτε ή προσθέστε μια γλώσσα για να ξεκινήσετε.",
13214
13230
  "field.richtext_t": "Μεταφρασμένο εμπλουτισμένο κείμενο",
@@ -14611,6 +14627,7 @@ export const websiteTranslations = {
14611
14627
  "api.data.serverStorageFull": "سرور به حداکثر ظرفیت ذخیره‌سازی خود رسیده است. لطفاً بعداً دوباره امتحان کنید.",
14612
14628
 
14613
14629
  "tourpoint.importDatapack": "بسته‌های داده از پیش ساخته شده را با گالری بسته‌های ما وارد کنید: وب‌سایت‌ها، تجارت الکترونیک، CRM و موارد دیگر!",
14630
+ "tourpoint.tutorials": "با دنبال کردن آموزش‌های گام به گام ما، خودتان را راهنمایی کنید!",
14614
14631
  "add_language": "اضافه کردن...",
14615
14632
  "select_or_add_language": "برای شروع، یک زبان را انتخاب یا اضافه کنید.",
14616
14633
  "field.richtext_t": "متن غنی ترجمه شده",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-primals-engine",
3
- "version": "1.1.7-rc1",
3
+ "version": "1.1.7",
4
4
  "description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
5
5
  "main": "src/engine.js",
6
6
  "type": "module",
@@ -89,6 +89,8 @@ import {getAllPacks} from "../packs.js";
89
89
  import {throttleMiddleware} from "../middlewares/throttle.js";
90
90
  import {Config} from "../config.js";
91
91
  import {profiles} from "../../client/src/constants.js";
92
+ import {processFilterPlaceholders} from "../../client/src/filter.js";
93
+ import {tutorialsConfig} from "../../client/src/tutorials.js";
92
94
 
93
95
  // Obtenir le chemin du répertoire courant de manière fiable avec ES Modules
94
96
  const __filename = fileURLToPath(import.meta.url);
@@ -1507,6 +1509,128 @@ export async function onInit(defaultEngine) {
1507
1509
  }
1508
1510
  });
1509
1511
 
1512
+ /**
1513
+ * Route pour vérifier si une condition de complétion est remplie.
1514
+ * Utilise la fonction `searchData` existante pour interroger les données.
1515
+ */
1516
+ engine.post('/api/tutorials/check-completion', middlewareAuthenticator, async (req, res) => {
1517
+ try {
1518
+ const { model, filter, limit } = req.fields;
1519
+ const user = req.me;
1520
+
1521
+ if (!model || !filter || limit === undefined) {
1522
+ return res.status(400).json({ error: 'Payload de condition de complétion invalide.' });
1523
+ }
1524
+
1525
+ const processedFilter = processFilterPlaceholders(filter, user);
1526
+
1527
+ // On utilise la fonction de recherche interne de l'application
1528
+ const searchResult = await searchData({
1529
+ query: {
1530
+ model,
1531
+ filter: processedFilter,
1532
+ limit, // Optimisation : pas besoin de plus de résultats
1533
+ page: 1
1534
+ },
1535
+ user: user
1536
+ });
1537
+
1538
+ // searchData devrait renvoyer un `count` total des documents correspondants
1539
+ const isCompleted = searchResult.count >= limit;
1540
+
1541
+ res.json({ isCompleted });
1542
+
1543
+ } catch (error) {
1544
+ console.error('[Tutoriel Check Error]', error);
1545
+ res.status(500).json({ error: 'Erreur lors de la vérification de la complétion.', details: error.message });
1546
+ }
1547
+ });
1548
+
1549
+
1550
+
1551
+ engine.post('/api/tutorials/set-active', middlewareAuthenticator, async (req, res) => {
1552
+ try {
1553
+ const { tutorialState } = req.fields;
1554
+ const user = req.me;
1555
+
1556
+ if (tutorialState !== null && (typeof tutorialState !== 'object' || !tutorialState.id)) {
1557
+ return res.status(400).json({ error: 'Invalid tutorial state payload.' });
1558
+ }
1559
+
1560
+ // Créer une représentation de l'utilisateur mis à jour pour la réponse
1561
+ const updatedData = { activeTutorial: tutorialState };
1562
+
1563
+ await engine.userProvider.updateUser(user, updatedData);
1564
+
1565
+ // --- MODIFICATION ---
1566
+ res.json({
1567
+ success: true,
1568
+ updatedData // Renvoyer l'objet utilisateur complet
1569
+ });
1570
+
1571
+ } catch (error) {
1572
+ console.error('[Tutoriel Set Active Error]', error);
1573
+ res.status(500).json({ error: 'Error setting active tutorial.', details: error.message });
1574
+ }
1575
+ });
1576
+
1577
+ // ...
1578
+
1579
+ engine.post('/api/tutorials/:tutorialId/claim-rewards', middlewareAuthenticator, async (req, res) => {
1580
+ try {
1581
+ const { tutorialId } = req.params;
1582
+ const user = req.me; // L'objet utilisateur est déjà chargé
1583
+ const tutorial = tutorialsConfig.find(t => t.id === tutorialId);
1584
+
1585
+ if (!tutorial) return res.status(404).json({ error: 'Tutoriel non trouvé.' });
1586
+ if (!tutorial.rewards) return res.status(400).json({ error: 'Ce tutoriel n\'a pas de récompenses.' });
1587
+ if (user.completedTutorials?.includes(tutorialId)) {
1588
+ return res.status(400).json({ error: 'Tutoriel déjà terminé.' });
1589
+ }
1590
+
1591
+ const { xpBonus, skill, achievement, notification } = tutorial.rewards;
1592
+
1593
+ // --- LOGIQUE CORRIGÉE ---
1594
+ let newData= {};
1595
+ newData.xp = newData.xp || 0;
1596
+ newData.achievements = newData.achievements || [];
1597
+ newData.skills = newData.skills || [];
1598
+ newData.completedTutorials = newData.completedTutorials || [];
1599
+
1600
+ // Appliquer les récompenses directement sur l'objet newData
1601
+ if (xpBonus) newData.xp += xpBonus;
1602
+ if (achievement && !newData.achievements.includes(achievement)) newData.achievements.push(achievement);
1603
+ if (skill) {
1604
+ const existingSkill = newData.skills.find(s => s.name === skill.name);
1605
+ if (existingSkill) {
1606
+ existingSkill.points += skill.points;
1607
+ } else {
1608
+ newData.skills.push({ name: skill.name, points: skill.points });
1609
+ }
1610
+ }
1611
+
1612
+ newData.completedTutorials.push(tutorialId);
1613
+ newData.activeTutorial = null;
1614
+
1615
+ await engine.userProvider.updateUser(user, newData);
1616
+
1617
+ const translatedNotification = {
1618
+ title: i18n.t(notification.title, notification.title),
1619
+ message: i18n.t(notification.message, notification.message)
1620
+ };
1621
+
1622
+ res.json({
1623
+ success: true,
1624
+ userUpdate: newData, // Renvoyer l'objet utilisateur complet et mis à jour
1625
+ notification: translatedNotification
1626
+ });
1627
+
1628
+ } catch (error) {
1629
+ console.error('[Tutoriel Rewards Error]', error);
1630
+ res.status(500).json({ error: 'Erreur lors de l\'attribution des récompenses.', details: error.message });
1631
+ }
1632
+ });
1633
+
1510
1634
  engine.get('/api/import/progress/:jobId', [middlewareAuthenticator], async (req, res) => {
1511
1635
  const { jobId } = req.params;
1512
1636
  const user = req.me;