Forráskód Böngészése

Merge branch 'release/2.4.2'

Vincent 11 hónapja
szülő
commit
11d9d680a1
63 módosított fájl, 1642 hozzáadás és 467 törlés
  1. 7 1
      .eslintrc.cjs
  2. 1 0
      .gitignore
  3. 1 1
      .gitlab-ci.yml
  4. 6 1
      components/Layout/Header/Menu.vue
  5. 2 2
      components/Layout/Header/Notification.vue
  6. 310 310
      components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue
  7. 1 0
      components/Layout/MainMenu.vue
  8. 5 10
      components/Ui/Button/Delete.vue
  9. 1 1
      components/Ui/Card.vue
  10. 99 44
      components/Ui/DatePicker.vue
  11. 2 2
      components/Ui/Form.vue
  12. 1 1
      components/Ui/Input/Image.vue
  13. 8 0
      config/abilities/pages/basicompta.yaml
  14. 5 0
      config/abilities/pages/myAccount.yaml
  15. 3 3
      env/setupEnv.mjs
  16. 5 0
      lang/fr.json
  17. 8 8
      middleware/routing.global.ts
  18. 6 2
      models/Access/Access.ts
  19. 3 0
      models/Access/MyProfile.ts
  20. 17 0
      models/Access/Preferences.ts
  21. 2 2
      models/Core/Notification.ts
  22. 0 14
      models/models.ts
  23. 7 0
      nuxt.config.ts
  24. 6 2
      package.json
  25. 1 1
      pages/cmf_licence_structure.vue
  26. 45 0
      pages/dev/poc_models_index.vue
  27. 52 0
      pages/dev/poc_persist.vue
  28. 59 0
      pages/my-settings.vue
  29. 0 1
      pages/parameters/attendances.vue
  30. 0 1
      pages/parameters/education_timings/index.vue
  31. 0 1
      pages/parameters/residence_areas/index.vue
  32. 70 0
      prepare/buildIndex.ts
  33. 47 23
      services/data/entityManager.ts
  34. 10 4
      services/layout/menuBuilder/abstractMenuBuilder.ts
  35. 2 0
      services/layout/menuBuilder/accountMenuBuilder.ts
  36. 29 0
      services/layout/menuBuilder/basicomptaMenuBuilder.ts
  37. 2 0
      services/layout/menuBuilder/mainMenuBuilder.ts
  38. 3 0
      stores/accessProfile.ts
  39. 3 2
      stores/sse.ts
  40. 70 26
      tests/units/services/data/entityManager.test.ts
  41. 4 1
      tests/units/services/layout/menuBuilder/abstractMenuBuilder.test.ts
  42. 7 0
      tests/units/services/layout/menuBuilder/accessMenuBuilder.test.ts
  43. 16 1
      tests/units/services/layout/menuBuilder/accountMenuBuilder.test.ts
  44. 7 0
      tests/units/services/layout/menuBuilder/admin2iosMenuBuilder.test.ts
  45. 2 0
      tests/units/services/layout/menuBuilder/agendaMenuBuilder.test.ts
  46. 85 0
      tests/units/services/layout/menuBuilder/basicomptaMenuBuilder.ts
  47. 9 0
      tests/units/services/layout/menuBuilder/billingMenuBuilder.test.ts
  48. 3 0
      tests/units/services/layout/menuBuilder/communicationMenuBuilder.test.ts
  49. 9 0
      tests/units/services/layout/menuBuilder/configurationMenuBuilder.test.ts
  50. 17 0
      tests/units/services/layout/menuBuilder/cotisationsMenuBuilder.test.ts
  51. 1 0
      tests/units/services/layout/menuBuilder/donorsMenuBuilder.test.ts
  52. 6 0
      tests/units/services/layout/menuBuilder/educationalMenuBuilder.test.ts
  53. 1 0
      tests/units/services/layout/menuBuilder/equipmentMenuBuilder.test.ts
  54. 1 1
      tests/units/services/layout/menuBuilder/mainMenuBuilder.test.ts
  55. 3 0
      tests/units/services/layout/menuBuilder/myAccessesMenuBuilder.test.ts
  56. 4 0
      tests/units/services/layout/menuBuilder/myFamilyMenuBuilder.test.ts
  57. 4 0
      tests/units/services/layout/menuBuilder/statsMenuBuilder.test.ts
  58. 1 0
      tests/units/services/layout/menuBuilder/websiteAdminMenuBuilder.test.ts
  59. 7 0
      tests/units/services/layout/menuBuilder/websiteListMenuBuilder.test.ts
  60. 8 0
      types/enum/enums.ts
  61. 1 0
      types/interfaces.d.ts
  62. 4 0
      types/layout.d.ts
  63. 543 1
      yarn.lock

+ 7 - 1
.eslintrc.cjs

@@ -19,7 +19,13 @@ module.exports = {
     'plugin:vue/vue3-recommended',
     'plugin:prettier/recommended',
   ],
-  ignorePatterns: ['.nuxt', 'coverage/*', 'vendor/*', 'dist/*'],
+  ignorePatterns: [
+    '.nuxt',
+    'coverage/*',
+    'vendor/*',
+    'dist/*',
+    'models/models.ts',
+  ],
   plugins: ['vue', '@typescript-eslint'],
   // add your custom rules here
   rules: {

+ 1 - 0
.gitignore

@@ -31,3 +31,4 @@ coverage/
 !.yarn/releases
 !.yarn/sdks
 !.yarn/versions
+models/models.ts

+ 1 - 1
.gitlab-ci.yml

@@ -11,7 +11,7 @@ before_script:
   - corepack enable
   - yarn set version berry
   - yarn install --network-timeout 10000
-  - HOST=ci yarn prepare
+  - HOSTNAME=ci yarn prepare
 
 cache:
   paths:

+ 6 - 1
components/Layout/Header/Menu.vue

@@ -10,10 +10,15 @@ header principal (configuration, paramètres du compte...)
         v-if="menu.icon.avatarId || menu.icon.avatarByDefault"
         size="30"
       >
-        <UiImage
+        <!-- remplacera l'UiImage en dessous lorsque la gestion des images avec liip sera OK
+         <UiImage
           :imageId="menu.icon.avatarId"
           :defaultImage="menu.icon.avatarByDefault"
           :width="30"
+        /> -->
+        <UiImage
+          :defaultImage="menu.icon.avatarByDefault"
+          :width="30"
         />
       </v-avatar>
 

+ 2 - 2
components/Layout/Header/Notification.vue

@@ -233,9 +233,9 @@ const markNotificationAsRead = (notification: Notification) => {
     isRead: true,
   })
 
-  em.persist(NotificationUsers, notificationUsers)
+  em.persist(notificationUsers)
   notification.notificationUsers = ['read']
-  em.save(Notification, notification)
+  em.save(notification)
 }
 
 /**

+ 310 - 310
components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue

@@ -1,304 +1,307 @@
 <!--
   Contenu de la boite de dialogue "Assistant de création"
--->
 
+  @see https://vuetifyjs.com/en/components/steppers/
+-->
 <template>
-  <!-- Menu Accueil -->
-  <v-container v-if="location === 'home'">
-    <v-row>
-      <!-- Une personne -->
-      <v-col cols="6" v-if="ability.can('manage', 'users')">
-        <LayoutHeaderUniversalCreationCard
-          to="access"
-          title="a_person"
-          text-content="add_new_person_student"
-          icon="fa fa-user"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un évènement -->
-      <v-col
-        cols="6"
-        v-if="
-          ability.can('display', 'agenda_page') &&
-          (ability.can('display', 'course_page') ||
-            ability.can('display', 'exam_page') ||
-            ability.can('display', 'pedagogics_project_page'))
-        "
-      >
-        <LayoutHeaderUniversalCreationCard
-          to="event"
-          title="an_event"
-          text-content="add_an_event_course"
-          icon="fa fa-calendar-alt"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Autre évènement -->
-      <v-col
-        cols="6"
-        v-else-if="
-          ability.can('display', 'agenda_page') &&
-          ability.can('manage', 'events')
-        "
-      >
-        <LayoutHeaderUniversalCreationCard
-          to="event-params"
-          title="other_event"
-          text-content="other_event_text_creation_card"
-          icon="far fa-calendar"
-          href="/calendar/create/events"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Une correspondance -->
-      <v-col
-        cols="6"
-        v-if="
-          ability.can('display', 'message_send_page') &&
-          (ability.can('manage', 'emails') ||
-            ability.can('manage', 'mails') ||
-            ability.can('manage', 'texto'))
-        "
-      >
-        <LayoutHeaderUniversalCreationCard
-          to="message"
-          title="a_correspondence"
-          text-content="send_email_letter"
-          icon="fa fa-comment"
-          type="message"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un matériel (direct link) -->
-      <v-col cols="6" v-if="ability.can('manage', 'equipments')">
-        <LayoutHeaderUniversalCreationCard
-          title="a_materiel"
-          text-content="add_any_type_material"
-          icon="fa fa-cube"
-          href="/list/create/equipment"
-          @click="onCardClick"
-        />
-      </v-col>
-    </v-row>
-  </v-container>
-
-  <!-- Menu "Créer une personne" -->
-  <v-container v-if="location === 'access'">
-    <v-row>
-      <!-- Un adhérent -->
-      <v-col cols="6" v-if="isLaw1901">
-        <LayoutHeaderUniversalCreationCard
-          title="an_adherent"
-          text-content="adherent_text_creation_card"
-          icon="fa fa-user"
-          href="/universal_creation_person/adherent"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un membre du CA -->
-      <v-col cols="6" v-if="isLaw1901">
-        <LayoutHeaderUniversalCreationCard
-          title="a_ca_member"
-          text-content="ca_member_text_creation_card"
-          icon="fa fa-users"
-          href="/universal_creation_person/ca_member"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un élève -->
-      <v-col cols="6">
-        <LayoutHeaderUniversalCreationCard
-          title="a_student"
-          text-content="student_text_creation_card"
-          icon="fa fa-user"
-          href="/universal_creation_person/student"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un tuteur -->
-      <v-col cols="6">
-        <LayoutHeaderUniversalCreationCard
-          title="a_guardian"
-          text-content="guardian_text_creation_card"
-          icon="fa fa-female"
-          href="/universal_creation_person/guardian"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un professeur -->
-      <v-col cols="6">
-        <LayoutHeaderUniversalCreationCard
-          title="a_teacher"
-          text-content="teacher_text_creation_card"
-          icon="fa fa-graduation-cap"
-          href="/universal_creation_person/teacher"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un membre du personnel -->
-      <v-col cols="6">
-        <LayoutHeaderUniversalCreationCard
-          title="a_member_of_staff"
-          text-content="personnel_text_creation_card"
-          icon="fa fa-suitcase"
-          href="/universal_creation_person/personnel"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Une entité légale -->
-      <v-col cols="6">
-        <LayoutHeaderUniversalCreationCard
-          title="a_legal_entity"
-          text-content="moral_text_creation_card"
-          icon="fa fa-building"
-          href="/universal_creation_person/company"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Une inscription en ligne -->
-      <v-col cols="6" v-if="hasOnlineRegistrationModule">
-        <LayoutHeaderUniversalCreationCard
-          title="online_registration"
-          text-content="online_registration_text_creation_card"
-          icon="fa fa-list-alt"
-          href="/online/registration/new_registration"
-          @click="onCardClick"
+  <v-stepper v-model="step">
+    <v-stepper-items>
+      <v-stepper-content step="1">
+        <!-- Menu Accueil -->
+        <v-container v-if="location === 'home'">
+          <v-row>
+            <!-- Une personne -->
+            <v-col cols="6" v-if="ability.can('manage', 'users')">
+              <LayoutHeaderUniversalCreationCard
+                @click="onCardClick('access')"
+                title="a_person"
+                text-content="add_new_person_student"
+                icon="fa fa-user"
+              />
+            </v-col>
+            <v-col
+              cols="6"
+              v-if="
+                ability.can('display', 'agenda_page') &&
+                (ability.can('display', 'course_page') ||
+                  ability.can('display', 'exam_page') ||
+                  ability.can('display', 'pedagogics_project_page'))
+              "
+            >
+              <!-- Un évènement -->
+              <LayoutHeaderUniversalCreationCard
+                @click="onCardClick('event')"
+                title="an_event"
+                text-content="add_an_event_course"
+                icon="fa fa-calendar"
+              />
+            </v-col>
+
+            <!-- Autre évènement -->
+            <v-col
+              cols="6"
+              v-else-if="
+                ability.can('display', 'agenda_page') &&
+                ability.can('manage', 'events')
+              "
+            >
+              <LayoutHeaderUniversalCreationCard
+                to="event-params"
+                title="other_event"
+                text-content="other_event_text_creation_card"
+                icon="far fa-calendar"
+                href="/calendar/create/events"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Une correspondance -->
+            <v-col
+              cols="6"
+              v-if="
+                ability.can('display', 'message_send_page') &&
+                (ability.can('manage', 'emails') ||
+                  ability.can('manage', 'mails') ||
+                  ability.can('manage', 'texto'))
+              "
+            >
+              <LayoutHeaderUniversalCreationCard
+                @click="onCardClick('message')"
+                title="a_correspondence"
+                text-content="send_email_letter"
+                icon="fa fa-envelope"
+              />
+            </v-col>
+
+            <!-- Un matériel (direct link) -->
+            <v-col cols="6" v-if="ability.can('manage', 'equipments')">
+              <LayoutHeaderUniversalCreationCard
+                title="a_materiel"
+                text-content="add_any_type_material"
+                icon="fa fa-laptop"
+                href="/list/create/equipment"
+                @click="onCardClick"
+              />
+            </v-col>
+          </v-row>
+        </v-container>
+      </v-stepper-content>
+
+      <v-stepper-content step="2">
+        <!-- Menu creer une personne -->
+        <v-container v-if="location === 'access'">
+          <v-row>
+            <!-- Un adhérent -->
+            <v-col cols="6" v-if="isLaw1901">
+              <LayoutHeaderUniversalCreationCard
+                title="an_adherent"
+                text-content="adherent_text_creation_card"
+                icon="fa fa-user"
+                href="/universal_creation_person/adherent"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un membre du CA -->
+            <v-col cols="6" v-if="isLaw1901">
+              <LayoutHeaderUniversalCreationCard
+                title="a_ca_member"
+                text-content="ca_member_text_creation_card"
+                icon="fa fa-users"
+                href="/universal_creation_person/ca_member"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un élève -->
+            <v-col cols="6">
+              <LayoutHeaderUniversalCreationCard
+                title="a_student"
+                text-content="student_text_creation_card"
+                icon="fa fa-user"
+                href="/universal_creation_person/student"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un tuteur -->
+            <v-col cols="6">
+              <LayoutHeaderUniversalCreationCard
+                title="a_guardian"
+                text-content="guardian_text_creation_card"
+                icon="fa fa-female"
+                href="/universal_creation_person/guardian"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un professeur -->
+            <v-col cols="6">
+              <LayoutHeaderUniversalCreationCard
+                title="a_teacher"
+                text-content="teacher_text_creation_card"
+                icon="fa fa-graduation-cap"
+                href="/universal_creation_person/teacher"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un membre du personnel -->
+            <v-col cols="6">
+              <LayoutHeaderUniversalCreationCard
+                title="a_member_of_staff"
+                text-content="personnel_text_creation_card"
+                icon="fa fa-suitcase"
+                href="/universal_creation_person/personnel"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Une entité légale -->
+            <v-col cols="6">
+              <LayoutHeaderUniversalCreationCard
+                title="a_legal_entity"
+                text-content="moral_text_creation_card"
+                icon="fa fa-building"
+                href="/universal_creation_person/company"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Une inscription en ligne -->
+            <v-col cols="6" v-if="hasOnlineRegistrationModule">
+              <LayoutHeaderUniversalCreationCard
+                title="online_registration"
+                text-content="online_registration_text_creation_card"
+                icon="fa fa-list-alt"
+                href="/online/registration/new_registration"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un autre type de contact -->
+            <v-col cols="6">
+              <LayoutHeaderUniversalCreationCard
+                title="another_type_of_contact"
+                text-content="other_contact_text_creation_card"
+                icon="fa fa-plus"
+                href="/universal_creation_person/other_contact"
+                @click="onCardClick"
+              />
+            </v-col>
+          </v-row>
+        </v-container>
+
+        <!-- Menu créer un evenement-->
+        <v-container v-if="location === 'event'">
+          <v-row>
+            <!-- Un cours -->
+            <v-col cols="6" v-if="ability.can('display', 'course_page')">
+              <LayoutHeaderUniversalCreationCard
+                title="course"
+                text-content="course_text_creation_card"
+                icon="fa fa-book"
+                href="/universal_creation_event/course"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un examen -->
+            <v-col cols="6" v-if="ability.can('display', 'exam_page')">
+              <LayoutHeaderUniversalCreationCard
+                title="exam"
+                text-content="exam_text_creation_card"
+                icon="fa fa-clipboard"
+                href="/universal_creation_event/exam"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un projet pédagogique -->
+            <v-col
+              cols="6"
+              v-if="ability.can('display', 'pedagogics_project_page')"
+            >
+              <LayoutHeaderUniversalCreationCard
+                title="educational_services"
+                text-content="educational_services_text_creation_card"
+                icon="fa fa-graduation-cap"
+                href="/universal_creation_event/pedagogical_project"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un autre évènement -->
+            <v-col cols="6" v-if="ability.can('manage', 'events')">
+              <LayoutHeaderUniversalCreationCard
+                to="event-params"
+                href="/calendar/create/events"
+                title="other_event"
+                text-content="other_event_text_creation_card"
+                icon="far fa-calendar"
+                @click="onCardClick"
+              />
+            </v-col>
+          </v-row>
+        </v-container>
+
+        <!-- Menu créer une correspondance -->
+        <v-container v-if="location === 'message'">
+          <v-row>
+            <!-- Un email -->
+            <v-col cols="6" v-if="ability.can('manage', 'emails')">
+              <LayoutHeaderUniversalCreationCard
+                title="an_email"
+                text-content="email_text_creation_card"
+                icon="far fa-envelope"
+                href="/list/create/emails"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un courrier -->
+            <v-col cols="6" v-if="ability.can('manage', 'mails')">
+              <LayoutHeaderUniversalCreationCard
+                title="a_letter"
+                text-content="letter_text_creation_card"
+                icon="far fa-file-alt"
+                href="/list/create/mails"
+                @click="onCardClick"
+              />
+            </v-col>
+
+            <!-- Un SMS -->
+            <v-col cols="6" v-if="ability.can('manage', 'texto')">
+              <LayoutHeaderUniversalCreationCard
+                title="a_sms"
+                text-content="sms_text_creation_card"
+                icon="fa fa-mobile-alt"
+                href="/list/create/sms"
+                @click="onCardClick"
+              />
+            </v-col>
+          </v-row>
+        </v-container>
+      </v-stepper-content>
+
+      <v-stepper-content step="3">
+        <!-- Page de pré-paramétrage des évènements -->
+        <LayoutHeaderUniversalCreationEventParams
+          v-if="location === 'event-params'"
+          @params-updated="onEventParamsUpdated"
         />
-      </v-col>
-
-      <!-- Un autre type de contact -->
-      <v-col cols="6">
-        <LayoutHeaderUniversalCreationCard
-          title="another_type_of_contact"
-          text-content="other_contact_text_creation_card"
-          icon="fa fa-plus"
-          href="/universal_creation_person/other_contact"
-          @click="onCardClick"
-        />
-      </v-col>
-    </v-row>
-  </v-container>
-
-  <!-- Menu Évènement -->
-  <v-container v-if="location === 'event'">
-    <v-row>
-      <!-- Un cours -->
-      <v-col cols="6" v-if="ability.can('display', 'course_page')">
-        <LayoutHeaderUniversalCreationCard
-          to="event-params"
-          href="/calendar/create/courses"
-          title="course"
-          text-content="course_text_creation_card"
-          icon="fa fa-users"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un examen -->
-      <v-col cols="6" v-if="ability.can('display', 'exam_page')">
-        <LayoutHeaderUniversalCreationCard
-          to="event-params"
-          href="/calendar/create/examens"
-          title="exam"
-          text-content="exam_text_creation_card"
-          icon="fa fa-graduation-cap"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un projet pédagogique -->
-      <v-col cols="6" v-if="ability.can('display', 'pedagogics_project_page')">
-        <LayoutHeaderUniversalCreationCard
-          to="event-params"
-          href="/calendar/create/educational_projects"
-          title="educational_services"
-          text-content="educational_services_text_creation_card"
-          icon="fa fa-suitcase"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un autre évènement -->
-      <v-col cols="6" v-if="ability.can('manage', 'events')">
-        <LayoutHeaderUniversalCreationCard
-          to="event-params"
-          href="/calendar/create/events"
-          title="other_event"
-          text-content="other_event_text_creation_card"
-          icon="far fa-calendar"
-          @click="onCardClick"
-        />
-      </v-col>
-    </v-row>
-  </v-container>
-
-  <!-- Une correspondance -->
-  <v-container v-if="location === 'message'">
-    <v-row>
-      <!-- Un email -->
-      <v-col cols="6" v-if="ability.can('manage', 'emails')">
-        <LayoutHeaderUniversalCreationCard
-          title="an_email"
-          text-content="email_text_creation_card"
-          icon="far fa-envelope"
-          href="/list/create/emails"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un courrier -->
-      <v-col cols="6" v-if="ability.can('manage', 'mails')">
-        <LayoutHeaderUniversalCreationCard
-          title="a_letter"
-          text-content="letter_text_creation_card"
-          icon="far fa-file-alt"
-          href="/list/create/mails"
-          @click="onCardClick"
-        />
-      </v-col>
-
-      <!-- Un SMS -->
-      <v-col cols="6" v-if="ability.can('manage', 'texto')">
-        <LayoutHeaderUniversalCreationCard
-          title="a_sms"
-          text-content="sms_text_creation_card"
-          icon="fa fa-mobile-alt"
-          href="/list/create/sms"
-          @click="onCardClick"
-        />
-      </v-col>
-    </v-row>
-  </v-container>
-
-  <!-- Page de pré-paramétrage des évènements -->
-  <LayoutHeaderUniversalCreationEventParams
-    v-if="location === 'event-params'"
-    @params-updated="onEventParamsUpdated"
-  />
+      </v-stepper-content>
+    </v-stepper-items>
+  </v-stepper>
 </template>
-
 <script setup lang="ts">
-import type { Ref } from '@vue/reactivity'
+import { ref, computed } from 'vue'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import { useAbility } from '@casl/vue'
-import type { ComputedRef } from 'vue'
-import UrlUtils from '~/services/utils/urlUtils'
 
 const props = defineProps({
   /**
-   * The path that the user followed troughout the wizard
+   * The path that the user followed throughout the wizard
    */
   path: {
     type: Array<string>,
@@ -306,28 +309,19 @@ const props = defineProps({
   },
 })
 
+const step = ref(1)
+
 const location: ComputedRef<string> = computed(() => {
   return props.path.at(-1) ?? 'home'
 })
 
 const ability = useAbility()
-
 const organizationProfile = useOrganizationProfileStore()
-const isLaw1901: ComputedRef<boolean> = organizationProfile.isAssociation
+const isLaw1901 = computed(() => organizationProfile.isAssociation)
 const hasOnlineRegistrationModule: Ref<boolean> = ref(
   organizationProfile.hasModule('IEL'),
 )
 
-const baseUrl: Ref<string | null> = ref(null)
-const query: Ref<Record<string, string>> = ref({})
-
-const url: ComputedRef<string | null> = computed(() => {
-  if (baseUrl.value === null) {
-    return null
-  }
-  return UrlUtils.addQuery(baseUrl.value, query.value)
-})
-
 const emit = defineEmits(['cardClick', 'urlUpdate'])
 
 /**
@@ -335,11 +329,16 @@ const emit = defineEmits(['cardClick', 'urlUpdate'])
  * @param to  Target location in the wizard
  * @param href  Target absolute url
  */
-const onCardClick = (to: string | null, href: string | null) => {
-  if (href !== null) {
-    baseUrl.value = href
+
+const onCardClick = (to: string | null, href: string | null = null) => {
+  if (href) {
+    // If a href is provided, redirect to it directly
+    window.location.href = href
+  } else if (to) {
+    // If only 'to' is provided, navigate to the next step
+    step.value++
+    emit('cardClick', to)
   }
-  emit('cardClick', to, url.value)
 }
 
 /**
@@ -347,13 +346,14 @@ const onCardClick = (to: string | null, href: string | null) => {
  * @param event
  */
 const onEventParamsUpdated = (event: { start: string; end: string }) => {
-  query.value = event
+  emit('urlUpdate', {
+    start: event.start,
+    end: event.end,
+  })
 }
 
-const unwatch = watch(url, (newUrl: string | null) => {
-  emit('urlUpdate', newUrl)
-})
 onUnmounted(() => {
-  unwatch()
+  // Reset the step when the component is unmounted
+  step.value = 1
 })
 </script>

+ 1 - 0
components/Layout/MainMenu.vue

@@ -24,6 +24,7 @@ Prend en paramètre une liste de ItemMenu et les met en forme
           :prepend-icon="item.icon.name"
           :href="!isInternalLink(item) ? item.to : undefined"
           :to="isInternalLink(item) ? item.to : undefined"
+          :target="item.target"
           exact
           height="48px"
           class="menu-item"

+ 5 - 10
components/Ui/Button/Delete.vue

@@ -29,20 +29,15 @@ Bouton Delete avec modale de confirmation de la suppression
 </template>
 
 <script setup lang="ts">
+import type { Ref, PropType } from 'vue'
 import { TYPE_ALERT } from '~/types/enum/enums'
-import type { Ref } from '@vue/reactivity'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 import ApiResource from '~/models/ApiResource'
 import { usePageStore } from '~/stores/page'
-import ApiModel from '~/models/ApiModel'
 
 const props = defineProps({
-  model: {
-    type: Function as any as () => typeof ApiModel,
-    required: true,
-  },
   entity: {
-    type: Object as () => ApiResource,
+    type: Object as PropType<ApiResource>,
     required: true,
   },
   flat: {
@@ -58,10 +53,10 @@ const { em } = useEntityManager()
 
 const deleteItem = async () => {
   try {
-    //@ts-ignore
-    await em.delete(props.model, props.entity)
+    await em.delete(props.entity)
     usePageStore().addAlert(TYPE_ALERT.SUCCESS, ['deleteSuccess'])
-  } catch (error: any) {
+  } catch (error) {
+    // @ts-expect-error error is supposed to have a message prop
     usePageStore().addAlert(TYPE_ALERT.ALERT, [error.message])
     throw error
   }

+ 1 - 1
components/Ui/Card.vue

@@ -24,7 +24,7 @@ Container de type Card
         </NuxtLink>
       </v-btn>
 
-      <UiButtonDelete v-if="withDeleteAction" :model="model" :entity="entity" />
+      <UiButtonDelete v-if="withDeleteAction" :entity="entity" />
 
       <slot name="card.action" />
     </v-card-actions>

+ 99 - 44
components/Ui/DatePicker.vue

@@ -1,79 +1,134 @@
 <!--
-Sélecteur de dates
+Sélecteur de dates avec Vuetify
 
-@see https://vue3datepicker.com/
+@see https://vuetifyjs.com/en/components/date-pickers/
 -->
 
 <template>
-  <main>
-    <VueDatePicker
-      :model-value="modelValue"
-      :locale="i18n.locale.value"
-      :format-locale="fnsLocale"
-      :format="dateFormat"
-      :enable-time-picker="withTime"
-      :teleport="true"
-      text-input
-      :auto-apply="true"
-      :select-text="$t('select')"
-      :cancel-text="$t('cancel')"
-      :disabled="readonly"
-      :position="position"
-      @update:model-value="onUpdate"
-    />
-  </main>
+  <v-layout row wrap>
+    <v-menu
+      ref="menu"
+      v-model="menu"
+      :close-on-content-click="false"
+      :nudge-right="40"
+      :return-value.sync="modelValue"
+      lazy
+      transition="scale-transition"
+      offset-y
+      :max-width="290"
+      :min-width="290"
+      :position-x="positionX"
+      :position-y="positionY"
+    >
+      <template v-slot:activator="{ on, attrs }">
+        <v-text-field
+          v-model="displayDate"
+          :label="label"
+          :readonly="readOnly"
+          v-bind="attrs"
+          v-on="on"
+          @blur="menu = false"
+        ></v-text-field>
+      </template>
+      <v-date-picker
+        v-if="!withTime"
+        :model-value="modelValue"
+        :locale="i18n.locale.value"
+        @update:model-value="updateDate"
+        no-title
+        scrollable
+      >
+      </v-date-picker>
+      <v-date-picker
+        v-else
+        :model-value="modelValue"
+        :locale="i18n.locale.value"
+        @update:model-value="updateDate"
+        no-title
+        scrollable
+        type="datetime"
+      >
+      </v-date-picker>
+    </v-menu>
+  </v-layout>
 </template>
 
 <script setup lang="ts">
-import DateUtils, { supportedLocales } from '~/services/utils/dateUtils'
-import type { PropType } from '@vue/runtime-core'
-
-const i18n = useI18n()
-
-const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
+import { ref, computed, nextTick, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
 
 const props = defineProps({
-  modelValue: {
-    type: Object as PropType<Date>,
-    required: false,
-    default: null,
-  },
-  readonly: {
+  modelValue: Date,
+  label: String,
+  readOnly: {
     type: Boolean,
-    required: false,
     default: false,
   },
   format: {
     type: String,
-    required: false,
     default: null,
   },
   withTime: {
     type: Boolean,
-    required: false,
     default: false,
   },
   /**
-   * @see https://vue3datepicker.com/props/positioning/#position
+   * Position du date-picker
+   * @see https://vuetifyjs.com/en/api/v-menu/#props-position
    */
   position: {
     type: String as PropType<'left' | 'center' | 'right'>,
-    required: false,
     default: 'center',
   },
 })
 
-const defaultFormatPattern = props.withTime
-  ? DateUtils.getFormatPattern(i18n.locale.value as supportedLocales)
-  : DateUtils.getShortFormatPattern(i18n.locale.value as supportedLocales)
+const emit = defineEmits(['update:modelValue'])
 
-const dateFormat: Ref<string> = ref(props.format ?? defaultFormatPattern)
+const i18n = useI18n()
+const menu = ref(false)
+const positionX = ref(0)
+const positionY = ref(0)
 
-const emit = defineEmits(['update:model-value'])
+const displayDate = computed({
+  get: () => {
+    if (!props.modelValue) return ''
+    if (props.format) {
+      return new Intl.DateTimeFormat(i18n.locale.value, {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: props.withTime ? '2-digit' : undefined,
+        minute: props.withTime ? '2-digit' : undefined,
+      }).format(props.modelValue)
+    }
+    return props.modelValue.toLocaleDateString(i18n.locale.value)
+  },
+  set: () => {},
+})
+
+function updateDate(value) {
+  emit('update:modelValue', value)
+  menu.value = false
+}
 
-const onUpdate = (event: Date) => {
-  emit('update:model-value', event)
+function updatePosition() {
+  nextTick(() => {
+    const activator = document.querySelector('.v-menu__activator')
+    if (activator) {
+      const rect = activator.getBoundingClientRect()
+      positionX.value = rect.left
+      positionY.value = rect.bottom
+    }
+  })
 }
+
+watch(menu, (val) => {
+  if (val) updatePosition()
+})
 </script>
 
-<style scoped></style>
+<style scoped>
+.v-menu__content {
+  position: absolute !important;
+}
+</style>

+ 2 - 2
components/Ui/Form.vue

@@ -232,7 +232,7 @@ const submit = async (next: string | null = null) => {
     usePageStore().loading = true
 
     // TODO: est-ce qu'il faut re-fetch l'entité après le persist?
-    const updatedEntity = await em.persist(props.model, props.entity)
+    const updatedEntity = await em.persist(props.entity)
 
     if (props.refreshProfile) {
       await refreshProfile()
@@ -362,7 +362,7 @@ const actions = computed(() => {
  */
 const onFormChange = async () => {
   if (isValid.value) {
-    em.save(props.model, props.entity)
+    em.save(props.entity)
     setIsDirty(true)
 
     if (props.onChanged) {

+ 1 - 1
components/Ui/Input/Image.vue

@@ -383,7 +383,7 @@ const saveExistingImage = async () => {
     width: cropperConfig.value.width,
   })
 
-  await em.persist(File, file.value)
+  await em.persist(file.value)
 }
 
 /**

+ 8 - 0
config/abilities/pages/basicompta.yaml

@@ -0,0 +1,8 @@
+basicompta_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Basicompta'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'basicompta' }],
+      }

+ 5 - 0
config/abilities/pages/myAccount.yaml

@@ -99,3 +99,8 @@ cmf_licence_person_page:
   conditions:
     - { function: organizationIsCmf }
     - { function: accessIsAdminAccount, expectedResult: false }
+
+my_settings_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }

+ 3 - 3
env/setupEnv.mjs

@@ -2,9 +2,9 @@
  * Post install script: create or replace the symlink .env
  * to the .env file matching the current environment
  *
- * To force an hostname, define an env variable named HOST :
+ * To force a hostname, define an env variable named HOSTNAME :
  *
- *     HOST=ci node ./env/setupEnv.mjs
+ *     HOSTNAME=ci node ./env/setupEnv.mjs
  *
  */
 import os from 'os'
@@ -14,7 +14,7 @@ import path from 'path'
 
 const projectDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
 
-const hostname = process.env.HOST ?? os.hostname()
+const hostname = process.env.HOSTNAME || os.hostname()
 
 const environments = {
   app: '.env.docker',

+ 5 - 0
lang/fr.json

@@ -1,4 +1,8 @@
 {
+  "my_settings_page": "Mes paramètres",
+  "allow_report_message": "Je souhaite recevoir les rapports d'envoi des emails que j'envoie",
+  "my-settings_breadcrumbs": "Mes paramètres",
+  "message_settings": "Paramètres des messages",
   "rewards_list": "Configuration des distinctions",
   "access_rewards_list": "Gestion des distinctions",
   "access_rewards_command": "Commande de distinctions",
@@ -420,6 +424,7 @@
   "attendances": "Absences",
   "attendances_breadcrumbs": "Absences",
   "equipment": "Parc matériel",
+  "basicompta_admin": "Comptabilité BasiCompta",
   "education_state": "Suivi pédagogique",
   "criteria_notations": "Critères d'évaluation",
   "education_notation_configs": "Grilles d'évaluation",

+ 8 - 8
middleware/routing.global.ts

@@ -16,14 +16,14 @@ export default defineNuxtRouteMiddleware((to, _) => {
     const name: string = routeName?.toString() ?? ''
 
     // <<- TODO: remove after 2.5 release
-    const runtimeConfig = useRuntimeConfig()
-    if (
-      runtimeConfig.public.env === 'production' &&
-      (name === 'cmf_licence_page' || name === 'parameters_page')
-    ) {
-      const { redirectToHome } = useRedirect()
-      redirectToHome()
-    }
+    // const runtimeConfig = useRuntimeConfig()
+    // if (
+    //   runtimeConfig.public.env === 'production' &&
+    //   (name === 'cmf_licence_page' || name === 'parameters_page')
+    // ) {
+    //   const { redirectToHome } = useRedirect()
+    //   redirectToHome()
+    // }
     // ->>
 
     if (

+ 6 - 2
models/Access/Access.ts

@@ -1,4 +1,4 @@
-import { HasOne, Num, Uid, Attr } from 'pinia-orm/dist/decorators'
+import { Num, Uid, Attr, Str } from 'pinia-orm/dist/decorators'
 import type { Historical } from '~/types/interfaces'
 import Person from '~/models/Person/Person'
 import ApiModel from '~/models/ApiModel'
@@ -16,7 +16,8 @@ export default class Access extends ApiModel {
   @Uid()
   declare id: number | string
 
-  @HasOne(() => Person, 'accessId')
+  @Attr(null)
+  @IriEncoded(Person)
   declare person: Person | null
 
   @Num(0)
@@ -28,4 +29,7 @@ export default class Access extends ApiModel {
   @Attr(null)
   @IriEncoded(Organization)
   declare organization: number | null
+
+  @Str('')
+  declare updateDate: string
 }

+ 3 - 0
models/Access/MyProfile.ts

@@ -59,4 +59,7 @@ export default class MyProfile extends ApiResource {
 
   @Attr({})
   declare originalAccess: Access | null
+
+  @Num(null)
+  declare preferencesId: number
 }

+ 17 - 0
models/Access/Preferences.ts

@@ -0,0 +1,17 @@
+import { Uid, Bool } from 'pinia-orm/dist/decorators'
+import ApiModel from '~/models/ApiModel'
+
+/**
+ * Ap2i Model : Preferences
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/Entity/Access/Preferences.php
+ */
+export default class Preferences extends ApiModel {
+  static entity = 'preferences'
+
+  @Uid()
+  declare id: number | string | null
+
+  @Bool(true)
+  declare messageReport: boolean
+}

+ 2 - 2
models/Core/Notification.ts

@@ -19,8 +19,8 @@ export default class Notification extends ApiModel {
   @Attr({})
   declare message: NotificationMessage | null
 
-  @Str('')
-  declare createDate: string
+  @Str(null)
+  declare createDate: string | null
 
   @Str(null)
   declare type: string | null

+ 0 - 14
models/models.ts

@@ -1,14 +0,0 @@
-import ApiResource from '~/models/ApiResource'
-
-const modules = import.meta.glob('~/models/*/*.ts')
-
-const models: Record<string, typeof ApiResource> = {}
-
-for (const path in modules) {
-  modules[path]().then((mod) => {
-    // @ts-expect-error On est dépendant du retour de import.meta.glob pour le type
-    models[mod.default.entity] = mod.default
-  })
-}
-
-export default models

+ 7 - 0
nuxt.config.ts

@@ -46,6 +46,7 @@ export default defineNuxtConfig({
     baseUrlMercure: '',
     fileStorageBaseUrl: '',
     supportUrl: '',
+    basicomptaUrl: 'https://app.basicompta.fr/',
     // Config within public will be also exposed to the client
     public: {
       env: '',
@@ -56,6 +57,7 @@ export default defineNuxtConfig({
       baseUrlMercure: '',
       fileStorageBaseUrl: '',
       supportUrl: '',
+      basicomptaUrl: 'https://app.basicompta.fr/',
     },
   },
   hooks: {
@@ -160,6 +162,8 @@ export default defineNuxtConfig({
     '@nuxtjs/i18n',
     '@nuxt/devtools',
     '@nuxt/image',
+    'nuxt-prepare',
+    'nuxt-vitalizer',
   ],
   vite: {
     esbuild: {
@@ -219,4 +223,7 @@ export default defineNuxtConfig({
     transpile,
   },
   ignore: [process.env.NUXT_ENV === 'prod' ? 'pages/dev/*' : ''],
+  prepare: {
+    scripts: ['prepare/buildIndex.ts'],
+  },
 })

+ 6 - 2
package.json

@@ -1,5 +1,6 @@
 {
   "name": "app",
+  "type": "module",
   "description": "SPA Opentalent",
   "repository": "https://gitlab.2iopenservice.com/opentalent/app",
   "private": true,
@@ -9,9 +10,9 @@
   },
   "scripts": {
     "setupenv": "node ./env/setupEnv.mjs",
-    "dev": "rm -rf /tmp/nitro && yarn setupenv && nuxt dev",
-    "build": "yarn setupenv && nuxt build && cp -r config/ .output/config",
     "prepare": "yarn setupenv && nuxt prepare",
+    "dev": "yarn setupenv && rm -rf /tmp/nitro && nuxt dev",
+    "build": "yarn setupenv && nuxt build && cp -r config/ .output/config",
     "start": "nuxt start",
     "deploy": "git pull && yarn install & yarn build & sudo supervisorctl restart app:app_00",
     "test": "vitest run",
@@ -37,11 +38,14 @@
     "eslint-import-resolver-typescript": "^3.6.1",
     "event-source-polyfill": "^1.0.31",
     "file-saver": "^2.0.5",
+    "glob": "^10.4.2",
     "js-yaml": "^4.1.0",
     "libphonenumber-js": "1.10.51",
     "lodash": "^4.17.21",
     "lodash-es": "^4.17.21",
     "nuxt": "^3.11.2",
+    "nuxt-prepare": "^2.1.0",
+    "nuxt-vitalizer": "^0.10.0",
     "pinia": "^2.1.7",
     "pinia-orm": "^1.7.2",
     "sass": "^1.69.5",

+ 1 - 1
pages/cmf_licence_structure.vue

@@ -87,7 +87,7 @@ const submit = async () => {
 
   try {
     // Send the export request and get the receipt
-    const receipt = await em.persist(LicenceCmfOrganizationER, exportRequest)
+    const receipt = await em.persist(exportRequest)
     if (receipt.fileId === null) {
       throw new Error("Missing file's id, abort")
     }

+ 45 - 0
pages/dev/poc_models_index.vue

@@ -0,0 +1,45 @@
+<!--
+Permet de tester l'index des modèles, l'idée étant de n'importer que les modèles utilisés,
+dans ce cas ci : Organization (importé dans le setup), Access (importé dynamiquement via
+la méthode `getModelFor` de l'entity manager), et Person (importée depuis la classe Access).
+
+On pourra vérifier que les fichiers suivants sont bien fetchés : Organization.ts, Access.ts
+Ainsi que les classes liées importées depuis celles ci : Person.ts
+
+
+Mais que les autres ne sont pas importés, par ex. : Country.ts ou File.ts
+-->
+<template>
+  <div>
+    <h1>POC Models index</h1>
+    <span>check result in console</span>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import Organization from '~/models/Organization/Organization'
+
+definePageMeta({
+  layout: false,
+})
+
+const { em } = useEntityManager()
+
+const accessModel = await em.getModelFor('accesses')
+
+console.log(
+  'Classe importée directement : ' +
+    Organization.name +
+    ' (entity: ' +
+    Organization.entity +
+    ')',
+)
+console.log(
+  'Classe importée dynamiquement : ' +
+    accessModel.name +
+    ' (entity: ' +
+    accessModel.entity +
+    ')',
+)
+</script>

+ 52 - 0
pages/dev/poc_persist.vue

@@ -0,0 +1,52 @@
+<template>
+  <div>
+    <h1>POC Persist</h1>
+
+    <v-btn v-if="!pending" @click="onUpdateClick">Update access</v-btn>
+
+    <v-btn @click="onCreateClick">Create Notification</v-btn>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import Access from '~/models/Access/Access'
+import Notification from '~/models/Core/Notification'
+
+definePageMeta({
+  layout: false,
+})
+
+const { em } = useEntityManager()
+
+const accessProfile = useAccessProfileStore()
+
+const accessId = accessProfile.currentAccessId
+
+const { fetch } = useEntityFetch()
+
+const { data: access, pending } = await fetch(Access, accessId)
+
+const onUpdateClick = () => {
+  if (access.value === null) {
+    throw new Error('access is null')
+  }
+
+  access.value.updateDate = new Date().toISOString()
+  console.log(access.value.id, access.value.updateDate)
+
+  em.persist(access.value)
+}
+
+const onCreateClick = async () => {
+  const notif = em.newInstance(Notification, { name: 'foo', message: ['bar'] })
+
+  // const notif = new Notification({ name: 'foo', message: ['bar'] })
+
+  const createdNotif = await em.persist(notif)
+
+  console.log(createdNotif)
+}
+</script>

+ 59 - 0
pages/my-settings.vue

@@ -0,0 +1,59 @@
+<!--
+Page 'Mes préférences'
+-->
+<template>
+  <LayoutContainer>
+    <v-col cols="12" sm="12" md="12">
+      <v-expansion-panels v-model="openedPanels" :multiple="true">
+        <UiExpansionPanel title="message_settings" icon="fas fa-inbox">
+          <v-container fluid class="container">
+            <v-row>
+              <UiLoadingPanel v-if="pending" />
+              <UiForm
+                v-else
+                :model="Preferences"
+                :entity="preferences"
+                action-position="bottom"
+              >
+                <v-row>
+                  <v-col cols="12">
+                    <UiInputCheckbox
+                      v-model="preferences.messageReport"
+                      field="messageReport"
+                      label="allow_report_message"
+                    />
+                  </v-col>
+                </v-row>
+              </UiForm>
+            </v-row>
+          </v-container>
+        </UiExpansionPanel>
+      </v-expansion-panels>
+    </v-col>
+  </LayoutContainer>
+</template>
+
+<script setup lang="ts">
+import type { Ref } from 'vue'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import Preferences from '~/models/Access/Preferences'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+
+definePageMeta({
+  name: 'my_settings_page',
+})
+
+const accessProfileStore = useAccessProfileStore()
+if (accessProfileStore.preferencesId === null) {
+  throw new Error("Missing access preference's id")
+}
+
+const { fetch } = useEntityFetch()
+const openedPanels: Ref<Array<number>> = ref([0])
+const { data: preferences, pending } = await fetch(
+  Preferences,
+  accessProfileStore.preferencesId,
+)
+</script>
+
+<style scoped lang="scss"></style>

+ 0 - 1
pages/parameters/attendances.vue

@@ -63,7 +63,6 @@
                 @click="goToEditPage(reason.id as number)"
               />
               <UiButtonDelete
-                :model="AttendanceBookingReason"
                 :entity="reason"
                 :flat="true"
                 class="cycle-edit-icon"

+ 0 - 1
pages/parameters/education_timings/index.vue

@@ -22,7 +22,6 @@
                 @click="goToEditPage(timing.id as number)"
               />
               <UiButtonDelete
-                :model="EducationTiming"
                 :entity="timing"
                 :flat="true"
                 class="cycle-edit-icon"

+ 0 - 1
pages/parameters/residence_areas/index.vue

@@ -25,7 +25,6 @@
                 @click="goToEditPage(residenceArea.id as number)"
               />
               <UiButtonDelete
-                :model="ResidenceArea"
                 :entity="residenceArea"
                 :flat="true"
                 class="cycle-edit-icon"

+ 70 - 0
prepare/buildIndex.ts

@@ -0,0 +1,70 @@
+/**
+ * Build an index of models in models/models.ts
+ *
+ * The index is of the form :
+ *
+ *     {
+ *       [entityName]: async () => [entityClass],
+ *       ...
+ *     }
+ */
+import fs from 'fs'
+import { glob } from 'glob'
+
+console.log('Build entity index')
+
+const modules: Array<{ entity: string; path: string }> = []
+
+const files = await glob('./models/*/*.ts')
+
+files.forEach((file) => {
+  const data = fs.readFileSync(file, 'utf8')
+  const lines = data.split('\n')
+  let entity = null
+
+  for (const line of lines) {
+    const match = line.match(/static entity = ['"]([\w-/]+)['"]/)
+    if (match) {
+      // afficher le groupe capturant
+      entity = match[1]
+      break
+    }
+  }
+
+  if (entity) {
+    modules.push({ entity, path: file })
+  } else {
+    console.warn('No match found for entity name in ' + file)
+  }
+})
+
+const code = []
+code.push('/**')
+code.push(' * /!\\ Auto-generated file : do not modify directly /!\\')
+code.push(' *')
+code.push(
+  ' * > This file is generated by the script prepare/buildIndex.ts when running `nuxt prepare`',
+)
+code.push('*/')
+code.push("import type ApiResource from '~/models/ApiResource'")
+code.push('')
+
+// noinspection JSAnnotator
+code.push(
+  'const modelsIndex: Record<string, () => Promise<typeof ApiResource>> = {',
+)
+
+for (const module of modules) {
+  code.push("  '" + module.entity + "': async () => {")
+  code.push(
+    "    const module = await import('~/" + module.path.slice(0, -3) + "')",
+  )
+  code.push('    return module.default')
+  code.push('  },')
+}
+
+code.push('}')
+code.push('')
+code.push('export default modelsIndex')
+
+fs.writeFileSync('models/models.ts', code.join('\n'))

+ 47 - 23
services/data/entityManager.ts

@@ -8,7 +8,7 @@ import UrlUtils from '~/services/utils/urlUtils'
 import ApiModel from '~/models/ApiModel'
 import ApiResource from '~/models/ApiResource'
 import type { AnyJson, AssociativeArray, Collection } from '~/types/data.d'
-import models from '~/models/models'
+import modelsIndex from '~/models/models'
 import HydraNormalizer from '~/services/data/normalizer/hydraNormalizer'
 import ObjectUtils from '~/services/utils/objectUtils'
 import Query from '~/services/data/Query'
@@ -87,8 +87,11 @@ class EntityManager {
    *
    * @param entityName
    */
-  public getModelFor(entityName: string): typeof ApiResource {
-    return models[entityName]
+  public async getModelFor(entityName: string): Promise<typeof ApiResource> {
+    if (!Object.prototype.hasOwnProperty.call(modelsIndex, entityName)) {
+      throw new Error("No model found for entity name '" + entityName + "'")
+    }
+    return await modelsIndex[entityName]()
   }
 
   /**
@@ -96,12 +99,12 @@ class EntityManager {
    *
    * @param iri An IRI of the form .../api/<entity>/...
    */
-  public getModelFromIri(iri: string): typeof ApiResource {
+  public async getModelFromIri(iri: string): Promise<typeof ApiResource> {
     const matches = iri.match(/^\/api\/(\w+)\/.*/)
     if (!matches || !matches[1]) {
       throw new Error('cannot parse the IRI')
     }
-    return this.getModelFor(matches[1])
+    return await this.getModelFor(matches[1])
   }
 
   /**
@@ -118,10 +121,6 @@ class EntityManager {
 
     const instance = repository.make(properties)
 
-    // Keep track of the model
-    // TODO : attendre de voir si utile ou non
-    // instance.setModel(model)
-
     if (
       !Object.prototype.hasOwnProperty.call(properties, 'id') ||
       // @ts-expect-error Si la première condition passe, on sait que id existe
@@ -131,23 +130,20 @@ class EntityManager {
       instance.id = 'tmp' + uuid4()
     }
 
-    return this.save(model, instance, true)
+    return this.save(instance, true)
   }
 
   /**
    * Save the model instance into the store
    *
-   * @param model
    * @param instance
    * @param permanent Is the change already persisted in the datasource? If this is the case, the initial state of this
    *                  record is also updated.
    */
-  public save(
-    model: typeof ApiResource,
-    instance: ApiResource,
-    permanent: boolean = false,
-  ): ApiResource {
-    instance = this.cast(model, instance)
+  public save(instance: ApiResource, permanent: boolean = false): ApiResource {
+    const model = instance.constructor as typeof ApiResource
+
+    this.validateEntity(instance)
 
     if (permanent) {
       this.saveInitialState(model, instance)
@@ -267,17 +263,16 @@ class EntityManager {
   /**
    * Persist the model instance as it is in the store into the data source via the API
    *
-   * @param model
    * @param instance
    */
-  public async persist(model: typeof ApiModel, instance: ApiModel) {
-    // Recast in case class definition has been "lost"
-    // TODO: attendre de voir si cette ligne est nécessaire
-    instance = this.cast(model, instance)
+  public async persist(instance: ApiModel) {
+    const model = instance.constructor as typeof ApiModel
 
     let url = UrlUtils.join('api', model.entity)
     let response
 
+    this.validateEntity(instance)
+
     const data: AnyJson = HydraNormalizer.normalizeEntity(instance)
 
     const headers = { profileHash: await this.makeProfileHash() }
@@ -330,9 +325,15 @@ class EntityManager {
    * @param model
    * @param instance
    */
-  public async delete(model: typeof ApiModel, instance: ApiResource) {
+  public async delete(instance: ApiResource) {
+    const model = instance.constructor as typeof ApiModel
+
+    this.validateEntity(instance)
+
     const repository = this.getRepository(model)
 
+    this.validateEntity(instance)
+
     // If object has been persisted to the datasource, send a delete request
     if (!instance.isNew()) {
       const url = UrlUtils.join('api', model.entity, String(instance.id))
@@ -472,6 +473,29 @@ class EntityManager {
     const mask = this._getProfileMask()
     return await ObjectUtils.hash(mask)
   }
+
+  /**
+   * Validate the entity, and throw an error if it's not correctly defined.
+   * @param instance
+   * @protected
+   */
+  protected validateEntity(instance: unknown): void {
+    if (Object.prototype.hasOwnProperty.call(instance, 'id')) {
+      // @ts-expect-error At this point, we're sure there is an id property
+      const id = instance.id
+
+      if (
+        !(typeof id === 'number') &&
+        !(typeof id === 'string' && id.startsWith('tmp'))
+      ) {
+        // The id is a pinia orm Uid, the entity has been created using the `new` keyword (not supported for now)
+        throw new Error(
+          'Definition error for the entity, did you use the entityManager.newInstance(...) method?\n' +
+            JSON.stringify(instance),
+        )
+      }
+    }
+  }
 }
 
 export default EntityManager

+ 10 - 4
services/layout/menuBuilder/abstractMenuBuilder.ts

@@ -11,6 +11,7 @@ import type {
 import { MENU_LINK_TYPE } from '~/types/enum/layout'
 import UrlUtils from '~/services/utils/urlUtils'
 import type { AccessProfile, organizationState } from '~/types/interfaces'
+import { LINK_TARGET } from '~/types/enum/enums'
 
 /**
  * Classe de base des menus et sous-menus.
@@ -92,15 +93,14 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
     icon?: IconItem,
     to: string = '',
     type: MENU_LINK_TYPE = MENU_LINK_TYPE.INTERNAL,
+    newTab: boolean = false,
   ): MenuItem {
     let url: string
-
     if (type === MENU_LINK_TYPE.INTERNAL) {
       console.warn(
         "'createItem()' should not be used for internal links, use 'addChildItemIfAllowed()'",
       )
     }
-
     switch (type) {
       case MENU_LINK_TYPE.V1:
         // eslint-disable-next-line no-case-declarations
@@ -115,8 +115,14 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
       default:
         url = to
     }
-
-    return { icon, label, to: url, type, active: false }
+    return {
+      icon,
+      label,
+      to: url,
+      type,
+      active: false,
+      target: newTab ? LINK_TARGET.BLANK : LINK_TARGET.SELF,
+    }
   }
 
   protected buildSubmenu(menuBuilder: typeof AbstractMenuBuilder) {

+ 2 - 0
services/layout/menuBuilder/accountMenuBuilder.ts

@@ -160,6 +160,8 @@ export default class AccountMenuBuilder extends AbstractMenuBuilder {
       )
     }
 
+    children.push(...this.makeChildren([{ pageName: 'my_settings_page' }]))
+
     actions.push(
       this.createItem('logout', undefined, `/logout`, MENU_LINK_TYPE.V1),
     )

+ 29 - 0
services/layout/menuBuilder/basicomptaMenuBuilder.ts

@@ -0,0 +1,29 @@
+import type { MenuItem } from '~/types/layout'
+import { MENU_LINK_TYPE } from '~/types/enum/layout'
+import AbstractMenuBuilder from '~/services/layout/menuBuilder/abstractMenuBuilder'
+
+/**
+ * Menu Basicompta
+ */
+export default class BasicomptaMenuBuilder extends AbstractMenuBuilder {
+  static readonly menuName = 'Basicompta'
+
+  build(): MenuItem | null {
+    // cf droit : https://ressources-opentalent.atlassian.net/wiki/spaces/SPEC/pages/32637034/Acc+s+basi+compta+pour+les+structures+de+la+CMF#Acces-a-Basicompta-pour-les-administrateurs
+    if (
+      this.accessProfile.isAdminAccess ||
+      this.accessProfile.isAdministratifManager ||
+      this.accessProfile.isFinancialManager
+    ) {
+      return this.createItem(
+        'basicompta_admin',
+        { name: 'fas fa-suitcase' },
+        this.runtimeConfig.public.basicomptaUrl,
+        MENU_LINK_TYPE.EXTERNAL,
+        true,
+      )
+    }
+
+    return null
+  }
+}

+ 2 - 0
services/layout/menuBuilder/mainMenuBuilder.ts

@@ -12,6 +12,7 @@ import WebsiteAdminMenuBuilder from '~/services/layout/menuBuilder/websiteAdminM
 import CotisationsMenuBuilder from '~/services/layout/menuBuilder/cotisationsMenuBuilder'
 import StatsMenuBuilder from '~/services/layout/menuBuilder/statsMenuBuilder'
 import Admin2iosMenuBuilder from '~/services/layout/menuBuilder/admin2iosMenuBuilder'
+import BasicomptaMenuBuilder from '~/services/layout/menuBuilder/basicomptaMenuBuilder'
 
 /**
  * Menu principal (ou menu lateral)
@@ -36,6 +37,7 @@ export default class MainMenuBuilder extends AbstractMenuBuilder {
       this.buildSubmenu(CotisationsMenuBuilder),
       this.buildSubmenu(StatsMenuBuilder),
       this.buildSubmenu(Admin2iosMenuBuilder),
+      this.buildSubmenu(BasicomptaMenuBuilder),
     ].filter((m: MenuItem | MenuGroup | null) => m !== null)
 
     if (children.length > 1) {

+ 3 - 0
stores/accessProfile.ts

@@ -46,6 +46,7 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
   const multiAccesses: Ref<Array<BaseOrganizationProfile>> = ref([])
   const familyAccesses: Ref<Array<BaseAccessProfile>> = ref([])
   const originalAccess: Ref<OrignalAccessProfile | null> = ref(null)
+  const preferencesId: Ref<number | null> = ref(null)
 
   // Getters
   /**
@@ -134,6 +135,7 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     isAdminAccess.value = profile.isAdminAccess
     isGuardian.value = profile.isGuardian
     isPayer.value = profile.isPayer
+    preferencesId.value = profile.preferencesId
 
     // Add the original Access (switch User case)
     if (profile.originalAccess !== null) {
@@ -213,5 +215,6 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     setProfile,
     setHistorical,
     setHistoricalRange,
+    preferencesId,
   }
 })

+ 3 - 2
stores/sse.ts

@@ -11,12 +11,13 @@ export const useSseStore = defineStore('sse', () => {
   const addEvent = async (event: MercureEntityUpdate) => {
     const { em } = useEntityManager()
 
-    const model = em.getModelFromIri(event.iri)
+    const model = await em.getModelFromIri(event.iri)
+    const instance = em.newInstance(model, JSON.parse(event.data))
 
     switch (event.operation) {
       case 'update':
       case 'create':
-        await em.save(model, JSON.parse(event.data), true)
+        em.save(instance, true)
         break
 
       case 'delete':

+ 70 - 26
tests/units/services/data/entityManager.test.ts

@@ -40,6 +40,10 @@ class TestableEntityManager extends EntityManager {
   public makeProfileHash() {
     return super.makeProfileHash()
   }
+
+  public validateEntity(instance: unknown): void {
+    return super.validateEntity(instance)
+  }
 }
 
 const _console: any = {
@@ -50,13 +54,13 @@ const _console: any = {
 
 vi.mock('~/models/models', async () => {
   class MyModel {
-    static entity = 'myModel'
+    static entity = 'my-model'
   }
 
-  const models: Record<string, any> = { myModel: MyModel }
+  const modelsIndex: Record<string, any> = { 'my-model': () => MyModel }
 
   return {
-    default: models,
+    default: modelsIndex,
   }
 })
 
@@ -134,27 +138,35 @@ describe('cast', () => {
 })
 
 describe('getModelFor', () => {
-  test('simple call', () => {
-    expect(entityManager.getModelFor('myModel').entity).toEqual('myModel')
+  test('simple call', async () => {
+    const model = await entityManager.getModelFor('my-model')
+    expect(model!.entity).toEqual('my-model')
+  })
+  test('non existing model', async () => {
+    expect(
+      async () => await entityManager.getModelFor('non-existing-model'),
+    ).rejects.toThrowError(
+      "No model found for entity name 'non-existing-model'",
+    )
   })
 })
 
 describe('getModelFromIri', () => {
-  test('simple call', () => {
+  test('simple call', async () => {
     // @ts-ignore
-    entityManager.getModelFor = vi.fn((entityName: string) =>
-      entityName === 'dummy' ? DummyApiResource : null,
+    entityManager.getModelFor = vi.fn(
+      async (entityName: string) => DummyApiResource,
     )
 
     // @ts-ignore
-    const result = entityManager.getModelFromIri('/api/dummy/123')
-
+    const result = await entityManager.getModelFromIri('/api/dummy/123')
+    console.log(result)
     expect(result).toEqual(DummyApiResource)
   })
   test('invalide Iri', () => {
-    expect(() => entityManager.getModelFromIri('/invalid')).toThrowError(
-      'cannot parse the IRI',
-    )
+    expect(
+      async () => await entityManager.getModelFromIri('/invalid'),
+    ).rejects.toThrowError('cannot parse the IRI')
   })
 })
 
@@ -179,18 +191,13 @@ describe('newInstance', () => {
 
     // @ts-ignore
     entityManager.save = vi.fn(
-      (model: typeof ApiResource, entity: ApiResource, permanent: boolean) =>
-        entity,
+      (entity: ApiResource, permanent: boolean) => entity,
     )
 
     const result = entityManager.newInstance(DummyApiResource, properties)
 
     expect(repo.make).toHaveBeenCalledWith(properties)
-    expect(entityManager.save).toHaveBeenCalledWith(
-      DummyApiResource,
-      entity,
-      true,
-    )
+    expect(entityManager.save).toHaveBeenCalledWith(entity, true)
 
     expect(result.id).toEqual(properties.id)
   })
@@ -243,12 +250,15 @@ describe('save', () => {
       return model === DummyApiResource ? repo : null
     })
 
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
     // @ts-ignore
     repo.save = vi.fn((record: Element) => entity)
 
-    const entity = new DummyApiResource()
-    entityManager.save(DummyApiResource, entity)
+    const entity = new DummyApiResource({ id: 1 })
+    entityManager.save(entity)
 
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(entity)
     expect(repo.save).toHaveBeenCalledWith(entity)
   })
 })
@@ -416,6 +426,12 @@ describe('fetchCollection', () => {
       next: undefined,
       previous: undefined,
     })
+
+    // @ts-expect-error Needed to avoid 'Cannot stringify non POJO' occasional bugs
+
+    expect(result.toJSON()).toEqual(
+      'Computed result from fetchCollection at : api/dummyResource',
+    )
   })
 
   test('with a parent', async () => {
@@ -509,6 +525,8 @@ describe('persist', () => {
       (model: typeof ApiResource, entity: ApiResource): ApiResource => entity,
     )
 
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
     const response = { id: 1, name: 'bob' }
     // @ts-ignore
     apiRequestService.post = vi.fn((url, data) => response)
@@ -528,7 +546,7 @@ describe('persist', () => {
     entityManager.removeTempAfterPersist = vi.fn()
     entityManager.makeProfileHash = vi.fn(async () => await 'azerty')
 
-    const result = await entityManager.persist(DummyApiModel, instance)
+    const result = await entityManager.persist(instance)
 
     // temp id should have been purged from the posted data
     expect(apiRequestService.post).toHaveBeenCalledWith(
@@ -553,6 +571,8 @@ describe('persist', () => {
 
     expect(result.id).toEqual(1)
     expect(result.name).toEqual('bob')
+
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(instance)
   })
 
   test('existing entity (PUT)', async () => {
@@ -585,7 +605,9 @@ describe('persist', () => {
     entityManager.removeTempAfterPersist = vi.fn()
     entityManager.makeProfileHash = vi.fn(async () => await 'azerty')
 
-    const result = await entityManager.persist(DummyApiModel, entity)
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
+    const result = await entityManager.persist(entity)
 
     expect(apiRequestService.put).toHaveBeenCalledWith(
       'api/dummyModel/1',
@@ -604,6 +626,8 @@ describe('persist', () => {
 
     expect(result.id).toEqual(1)
     expect(result.name).toEqual('bob')
+
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(entity)
   })
 })
 
@@ -657,16 +681,20 @@ describe('delete', () => {
       return model === DummyApiModel ? repo : null
     })
 
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
     apiRequestService.delete = vi.fn()
 
     // @ts-ignore
     repo.destroy = vi.fn((id: number) => null)
 
-    entityManager.delete(DummyApiModel, entity)
+    entityManager.delete(entity)
 
     expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
     expect(apiRequestService.delete).toHaveBeenCalledTimes(0)
     expect(repo.destroy).toHaveBeenCalledWith('tmp123')
+
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(entity)
   })
 
   test('delete persisted entity', async () => {
@@ -688,7 +716,7 @@ describe('delete', () => {
     // @ts-ignore
     repo.destroy = vi.fn((id: number) => null)
 
-    await entityManager.delete(DummyApiModel, entity)
+    await entityManager.delete(entity)
 
     expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
     expect(apiRequestService.delete).toHaveBeenCalledWith('api/dummyModel/1')
@@ -1000,3 +1028,19 @@ describe('makeProfileHash', () => {
     )
   })
 })
+
+describe('validateEntity', () => {
+  test('instance with numeric id', async () => {
+    entityManager.validateEntity({ id: 123 })
+  })
+
+  test('instance with temp id', async () => {
+    entityManager.validateEntity({ id: 'tmpazerty' })
+  })
+
+  test('invalid entity', async () => {
+    expect(() => entityManager.validateEntity({ id: 'azerty' })).toThrowError(
+      'Definition error for the entity, did you use the entityManager.newInstance(...) method?\n{"id":"azerty"}',
+    )
+  })
+})

+ 4 - 1
tests/units/services/layout/menuBuilder/abstractMenuBuilder.test.ts

@@ -6,6 +6,7 @@ import AbstractMenuBuilder from '~/services/layout/menuBuilder/abstractMenuBuild
 import type { IconItem, MenuGroup, MenuItem, MenuItems } from '~/types/layout'
 import { MENU_LINK_TYPE } from '~/types/enum/layout'
 import type { AccessProfile, organizationState } from '~/types/interfaces'
+import { LINK_TARGET } from '~/types/enum/enums'
 
 class TestableAbstractMenuBuilder extends AbstractMenuBuilder {
   static readonly menuName = 'TestableMenu'
@@ -104,11 +105,12 @@ describe('createItem', () => {
     const label = 'my_menu'
     const icon = { name: 'my_icon' }
     const to = 'https://domain.com/foo/bar'
+    const target = LINK_TARGET.SELF
     const type = MENU_LINK_TYPE.EXTERNAL
 
     const result = menuBuilder.createItem(label, icon, to, type)
 
-    expect(result).toEqual({ icon, label, to, type, active: false })
+    expect(result).toEqual({ icon, label, to, target, type, active: false })
   })
 
   test('default values', () => {
@@ -118,6 +120,7 @@ describe('createItem', () => {
       label: 'my_menu',
       icon: undefined,
       to: '',
+      target: '_self',
       type: MENU_LINK_TYPE.INTERNAL,
       active: false,
     })

+ 7 - 0
tests/units/services/layout/menuBuilder/accessMenuBuilder.test.ts

@@ -71,6 +71,7 @@ describe('build', () => {
       label: 'person',
       icon: { name: 'fas fa-user' },
       to: 'https://mydomain.com/#/students/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'person',
       icon: { name: 'fas fa-user' },
       to: 'https://mydomain.com/#/adherent/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -97,6 +99,7 @@ describe('build', () => {
       label: 'family_view',
       icon: { name: 'fas fa-users' },
       to: 'https://mydomain.com/#/student_registration/new',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -112,6 +115,7 @@ describe('build', () => {
       label: 'education_student_next_year',
       icon: { name: 'fas fa-list-alt' },
       to: 'https://mydomain.com/#/education_student_next_year/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -127,6 +131,7 @@ describe('build', () => {
       label: 'commissions',
       icon: { name: 'fas fa-street-view' },
       to: 'https://mydomain.com/#/commissions/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -142,6 +147,7 @@ describe('build', () => {
       label: 'network',
       icon: { name: 'fas fa-sitemap' },
       to: 'https://mydomain.com/#/networks/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -157,6 +163,7 @@ describe('build', () => {
       label: 'my_network',
       icon: { name: 'fas fa-sitemap' },
       to: 'https://mydomain.com/#/network_artist_schools/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 16 - 1
tests/units/services/layout/menuBuilder/accountMenuBuilder.test.ts

@@ -55,7 +55,7 @@ describe('build', () => {
     expect(result.label).toEqual('my_account')
 
     // @ts-ignore
-    expect(result.children.length).toEqual(14)
+    expect(result.children.length).toEqual(15)
     // @ts-ignore
     expect(result.actions.length).toEqual(1)
 
@@ -66,6 +66,7 @@ describe('build', () => {
         label: 'logout',
         icon: undefined,
         to: 'https://mydomain.com/#/logout',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -112,6 +113,7 @@ describe('build', () => {
         label: 'logout',
         icon: undefined,
         to: 'https://mydomain.com/#/logout',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -129,6 +131,7 @@ describe('build', () => {
       label: 'my_schedule_page',
       icon: undefined,
       to: 'https://mydomain.com/#/my_calendar',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -145,6 +148,7 @@ describe('build', () => {
       label: 'attendance_bookings_menu',
       icon: undefined,
       to: 'https://mydomain.com/#/own_attendance',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -161,6 +165,7 @@ describe('build', () => {
       label: 'my_attendance',
       icon: undefined,
       to: 'https://mydomain.com/#/my_attendances/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -177,6 +182,7 @@ describe('build', () => {
       label: 'my_invitation',
       icon: undefined,
       to: 'https://mydomain.com/#/my_invitations/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -193,6 +199,7 @@ describe('build', () => {
       label: 'my_students',
       icon: undefined,
       to: 'https://mydomain.com/#/my_students/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -210,6 +217,7 @@ describe('build', () => {
       label: 'my_students_education_students',
       icon: undefined,
       to: 'https://mydomain.com/#/my_students_education_students/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -226,6 +234,7 @@ describe('build', () => {
       label: 'my_education_students',
       icon: undefined,
       to: 'https://mydomain.com/#/main/my_profile/123/dashboard/my_education_students/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -242,6 +251,7 @@ describe('build', () => {
       label: 'send_an_email',
       icon: undefined,
       to: 'https://mydomain.com/#/list/create/emails',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -258,6 +268,7 @@ describe('build', () => {
       label: 'my_documents',
       icon: undefined,
       to: 'https://mydomain.com/#/main/my_profile/123/dashboard/show/my_access_file',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -274,6 +285,7 @@ describe('build', () => {
       label: 'my_profile',
       icon: undefined,
       to: 'https://mydomain.com/#/main/my_profile/123/dashboard',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -290,6 +302,7 @@ describe('build', () => {
       label: 'adherent_list',
       icon: undefined,
       to: 'https://mydomain.com/#/adherent_contacts/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -328,6 +341,7 @@ describe('build', () => {
       label: 'my_bills',
       icon: undefined,
       to: 'https://mydomain.com/#/main/my_profile/123/dashboard/show/my_bills',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -344,6 +358,7 @@ describe('build', () => {
       label: 'print_my_licence',
       icon: undefined,
       to: 'https://mydomain.com/#/licence_cmf/user',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 7 - 0
tests/units/services/layout/menuBuilder/admin2iosMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'all_accesses',
       icon: { name: 'fas fa-users' },
       to: 'https://mydomain.com/#/all_accesses/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'all_organizations',
       icon: { name: 'fas fa-building' },
       to: 'https://mydomain.com/#/organization_params/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -97,6 +99,7 @@ describe('build', () => {
       label: 'tips',
       icon: { name: 'fas fa-info-circle' },
       to: 'https://mydomain.com/#/tips/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -112,6 +115,7 @@ describe('build', () => {
       label: 'dgv',
       icon: { name: 'fas fa-house-damage' },
       to: 'https://mydomain.com/#/admin2ios/dgv',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -127,6 +131,7 @@ describe('build', () => {
       label: 'cmf_cotisation',
       icon: { name: 'fas fa-info-circle' },
       to: 'https://mydomain.com/#/admin2ios/cotisationcmf',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -142,6 +147,7 @@ describe('build', () => {
       label: 'right_menu',
       icon: { name: 'fas fa-balance-scale-right' },
       to: 'https://mydomain.com/#/admin2ios/right',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -157,6 +163,7 @@ describe('build', () => {
       label: 'tree_menu',
       icon: { name: 'fas fa-sitemap' },
       to: 'https://mydomain.com/#/admin2ios/tree',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 2 - 0
tests/units/services/layout/menuBuilder/agendaMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'schedule',
       icon: { name: 'fas fa-calendar-alt' },
       to: 'https://mydomain.com/#/calendar',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'attendances',
       icon: { name: 'fas fa-calendar-check' },
       to: 'https://mydomain.com/#/attendances/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 85 - 0
tests/units/services/layout/menuBuilder/basicomptaMenuBuilder.ts

@@ -0,0 +1,85 @@
+import { describe, test, expect, vi, beforeEach } from 'vitest'
+import type { RuntimeConfig } from '@nuxt/schema'
+import type { AnyAbility } from '@casl/ability'
+import type { Router } from 'vue-router'
+import type { AccessProfile, organizationState } from '~/types/interfaces'
+import BasicomptaMenuBuilder from '~/services/layout/menuBuilder/basicomptaMenuBuilder'
+import { MENU_LINK_TYPE } from '~/types/enum/layout'
+
+let runtimeConfig: RuntimeConfig
+let ability: AnyAbility
+let organizationProfile: organizationState
+let accessProfile: AccessProfile
+let menuBuilder: BasicomptaMenuBuilder
+let router: Router
+
+beforeEach(() => {
+  runtimeConfig = vi.fn() as any as RuntimeConfig
+  ability = vi.fn() as any as AnyAbility
+  organizationProfile = vi.fn() as any as organizationState
+  accessProfile = vi.fn() as any as AccessProfile
+  // @ts-ignore
+  router = vi.fn() as Router
+
+  menuBuilder = new BasicomptaMenuBuilder(
+    runtimeConfig,
+    ability,
+    organizationProfile,
+    accessProfile,
+    router,
+  )
+})
+
+describe('getMenuName', () => {
+  test('validate name', () => {
+    expect(menuBuilder.getMenuName()).toEqual('Basicompta')
+  })
+})
+
+describe('build', () => {
+  test('without admin, administratif, or financial manager access', () => {
+    accessProfile.isAdminAccess = false
+    accessProfile.isAdministratifManager = false
+    accessProfile.isFinancialManager = false
+    expect(menuBuilder.build()).toEqual(null)
+  })
+
+  test('with admin access', () => {
+    accessProfile.isAdminAccess = true
+    expect(menuBuilder.build()).toEqual({
+      label: 'basicompta_admin',
+      icon: { name: 'fas fa-suitcase' },
+      to: 'https://app.basicompta.fr/',
+      type: MENU_LINK_TYPE.EXTERNAL,
+      active: false,
+      target: '_blank',
+    })
+  })
+
+  test('with administratif manager access', () => {
+    accessProfile.isAdminAccess = false
+    accessProfile.isAdministratifManager = true
+    expect(menuBuilder.build()).toEqual({
+      label: 'basicompta_admin',
+      icon: { name: 'fas fa-suitcase' },
+      to: 'https://app.basicompta.fr/',
+      type: MENU_LINK_TYPE.EXTERNAL,
+      active: false,
+      target: '_blank',
+    })
+  })
+
+  test('with financial manager access', () => {
+    accessProfile.isAdminAccess = false
+    accessProfile.isAdministratifManager = false
+    accessProfile.isFinancialManager = true
+    expect(menuBuilder.build()).toEqual({
+      label: 'basicompta_admin',
+      icon: { name: 'fas fa-suitcase' },
+      to: 'https://app.basicompta.fr/',
+      type: MENU_LINK_TYPE.EXTERNAL,
+      active: false,
+      target: '_blank',
+    })
+  })
+})

+ 9 - 0
tests/units/services/layout/menuBuilder/billingMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'billing_product',
       icon: { name: 'fas fa-cube' },
       to: 'https://mydomain.com/#/intangibles/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'billing_products_by_student',
       icon: { name: 'fas fa-cubes' },
       to: 'https://mydomain.com/#/access_intangibles/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -97,6 +99,7 @@ describe('build', () => {
       label: 'billing_edition',
       icon: { name: 'fas fa-copy' },
       to: 'https://mydomain.com/#/billing_edition',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -112,6 +115,7 @@ describe('build', () => {
       label: 'billing_accounting',
       icon: { name: 'fas fa-file-alt' },
       to: 'https://mydomain.com/#/bill_accountings/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -127,6 +131,7 @@ describe('build', () => {
       label: 'billing_payment_list',
       icon: { name: 'fas fa-credit-card' },
       to: 'https://mydomain.com/#/bill_payments_list/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -142,6 +147,7 @@ describe('build', () => {
       label: 'pes_export',
       icon: { name: 'fas fa-align-justify' },
       to: 'https://mydomain.com/#/pes/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -157,6 +163,7 @@ describe('build', () => {
       label: 'berger_levrault_export',
       icon: { name: 'fas fa-align-justify' },
       to: 'https://mydomain.com/#/berger_levraults/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -172,6 +179,7 @@ describe('build', () => {
       label: 'jvs_export',
       icon: { name: 'fas fa-align-justify' },
       to: 'https://mydomain.com/#/jvs/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -187,6 +195,7 @@ describe('build', () => {
       label: 'afi_export',
       icon: { name: 'fas fa-align-justify' },
       to: 'https://mydomain.com/#/afis/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 3 - 0
tests/units/services/layout/menuBuilder/communicationMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'inbox',
       icon: { name: 'fas fa-inbox' },
       to: 'https://mydomain.com/#/messages/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'message_send',
       icon: { name: 'fas fa-paper-plane' },
       to: 'https://mydomain.com/#/messagessends/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -97,6 +99,7 @@ describe('build', () => {
       label: 'message_templates',
       icon: { name: 'fas fa-edit' },
       to: 'https://mydomain.com/#/templates/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 9 - 0
tests/units/services/layout/menuBuilder/configurationMenuBuilder.test.ts

@@ -68,6 +68,7 @@ describe('build', () => {
       label: 'organization_page',
       icon: undefined,
       to: 'https://mydomain.com/#/main/organizations/123/dashboard',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -83,6 +84,7 @@ describe('build', () => {
       label: 'cmf_licence_generate',
       icon: undefined,
       to: 'https://mydomain.com/#/licence_cmf/organization',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -108,6 +110,7 @@ describe('build', () => {
       label: 'parameters',
       icon: undefined,
       to: 'https://mydomain.com/#/main/edit/parameters/123',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -131,6 +134,7 @@ describe('build', () => {
       label: 'places',
       icon: undefined,
       to: 'https://mydomain.com/#/places/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -146,6 +150,7 @@ describe('build', () => {
       label: 'education',
       icon: undefined,
       to: 'https://mydomain.com/#/educations/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -161,6 +166,7 @@ describe('build', () => {
       label: 'tags',
       icon: undefined,
       to: 'https://mydomain.com/#/taggs/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -176,6 +182,7 @@ describe('build', () => {
       label: 'activities',
       icon: undefined,
       to: 'https://mydomain.com/#/activities/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -191,6 +198,7 @@ describe('build', () => {
       label: 'course_duplication',
       icon: undefined,
       to: 'https://mydomain.com/#/duplicate_courses',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -206,6 +214,7 @@ describe('build', () => {
       label: 'import',
       icon: undefined,
       to: 'https://mydomain.com/#/import/all',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 17 - 0
tests/units/services/layout/menuBuilder/cotisationsMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'rate_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/rate',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'parameters_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/parameter',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -97,6 +99,7 @@ describe('build', () => {
       label: 'send_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/send',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -112,6 +115,7 @@ describe('build', () => {
       label: 'state_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/state',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -127,6 +131,7 @@ describe('build', () => {
       label: 'pay_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/pay',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -142,6 +147,7 @@ describe('build', () => {
       label: 'check_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/check',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -157,6 +163,7 @@ describe('build', () => {
       label: 'ledger_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/ledger',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -172,6 +179,7 @@ describe('build', () => {
       label: 'magazine_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/magazine',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -187,6 +195,7 @@ describe('build', () => {
       label: 'ventilated_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/ventilated',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -202,6 +211,7 @@ describe('build', () => {
       label: 'pay_erase_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/payerase',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -217,6 +227,7 @@ describe('build', () => {
       label: 'resume_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/resume',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -232,6 +243,7 @@ describe('build', () => {
       label: 'history_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/history',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -247,6 +259,7 @@ describe('build', () => {
       label: 'call_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/call',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -262,6 +275,7 @@ describe('build', () => {
       label: 'history_structure_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/historystructure',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -277,6 +291,7 @@ describe('build', () => {
       label: 'insurance_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/insurance',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -292,6 +307,7 @@ describe('build', () => {
       label: 'resume_all_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/resumeall',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -307,6 +323,7 @@ describe('build', () => {
       label: 'resume_pay_cotisation',
       icon: { name: 'fas fa-euro-sign' },
       to: 'https://mydomain.com/#/cotisation/resumepay',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 1 - 0
tests/units/services/layout/menuBuilder/donorsMenuBuilder.test.ts

@@ -50,6 +50,7 @@ describe('build', () => {
       label: 'donors',
       icon: { name: 'fas fa-handshake' },
       to: 'https://mydomain.com/#/donors/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 6 - 0
tests/units/services/layout/menuBuilder/educationalMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'criteria_notations',
       icon: { name: 'fas fa-bars' },
       to: 'https://mydomain.com/#/criteria_notations/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -82,6 +83,7 @@ describe('build', () => {
       label: 'seizure_period',
       icon: { name: 'fas fa-calendar-alt' },
       to: 'https://mydomain.com/#/education_teachers/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -97,6 +99,7 @@ describe('build', () => {
       label: 'test_seizure',
       icon: { name: 'fas fa-pencil-alt' },
       to: 'https://mydomain.com/#/education_input/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -112,6 +115,7 @@ describe('build', () => {
       label: 'test_validation',
       icon: { name: 'fas fa-check' },
       to: 'https://mydomain.com/#/education_notations/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -127,6 +131,7 @@ describe('build', () => {
       label: 'examen_results',
       icon: { name: 'fas fa-graduation-cap' },
       to: 'https://mydomain.com/#/examen_convocations/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -143,6 +148,7 @@ describe('build', () => {
       label: 'education_by_student_validation',
       icon: { name: 'fas fa-check-square' },
       to: 'https://mydomain.com/#/education_by_student/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 1 - 0
tests/units/services/layout/menuBuilder/equipmentMenuBuilder.test.ts

@@ -50,6 +50,7 @@ describe('build', () => {
       label: 'equipment',
       icon: { name: 'fas fa-cube' },
       to: 'https://mydomain.com/#/equipment/list/',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 1 - 1
tests/units/services/layout/menuBuilder/mainMenuBuilder.test.ts

@@ -90,7 +90,7 @@ describe('build', () => {
 
     expect(result.label).toEqual('main')
     expect(result.icon).toEqual(undefined)
-    expect((result.children ?? []).length).toEqual(12)
+    expect((result.children ?? []).length).toEqual(13)
   })
 
   test('return a single group', () => {

+ 3 - 0
tests/units/services/layout/menuBuilder/myAccessesMenuBuilder.test.ts

@@ -62,6 +62,7 @@ describe('build', () => {
         label: 'Bob',
         icon: undefined,
         to: 'https://mydomain.com/#/switch/1',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -69,6 +70,7 @@ describe('build', () => {
         label: 'Séraphin',
         icon: undefined,
         to: 'https://mydomain.com/#/switch/2',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -76,6 +78,7 @@ describe('build', () => {
         label: 'Lilou',
         icon: undefined,
         to: 'https://mydomain.com/#/switch/3',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },

+ 4 - 0
tests/units/services/layout/menuBuilder/myFamilyMenuBuilder.test.ts

@@ -93,6 +93,7 @@ describe('build', () => {
         label: 'Dupont Bob',
         icon: { avatarId: 1, avatarByDefault: '/images/default/men-1.png' },
         to: 'https://mydomain.com/#/switch_user/100/1/1',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -100,6 +101,7 @@ describe('build', () => {
         label: 'Dupuis Séraphin',
         icon: { avatarId: 2, avatarByDefault: '/images/default/men-1.png' },
         to: 'https://mydomain.com/#/switch_user/100/1/2',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -107,6 +109,7 @@ describe('build', () => {
         label: 'Dubois Lilou',
         icon: { avatarId: 3, avatarByDefault: '/images/default/women-1.png' },
         to: 'https://mydomain.com/#/switch_user/100/1/3',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -114,6 +117,7 @@ describe('build', () => {
         label: 'Soprano Tony',
         icon: undefined,
         to: 'https://mydomain.com/#/switch_user/100/4/exit',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },

+ 4 - 0
tests/units/services/layout/menuBuilder/statsMenuBuilder.test.ts

@@ -67,6 +67,7 @@ describe('build', () => {
       label: 'report_activity',
       icon: { name: 'fas fa-chart-bar' },
       to: 'https://mydomain.com/#/report_activity',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })
@@ -85,6 +86,7 @@ describe('build', () => {
         label: 'educations_quotas_by_education',
         icon: { name: 'fas fa-user-circle' },
         to: 'https://mydomain.com/#/educations_quotas_by_education_year/list/',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -92,6 +94,7 @@ describe('build', () => {
         label: 'accesses_quotas_courses_hebdos',
         icon: { name: 'fas fa-user-circle' },
         to: 'https://mydomain.com/#/accesses_quotas_courses_hebdos/list/',
+        target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
       },
@@ -108,6 +111,7 @@ describe('build', () => {
       label: 'fede_stats',
       icon: { name: 'fas fa-chart-bar' },
       to: 'https://mydomain.com/#/statistic/membersfedeonly',
+      target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
     })

+ 1 - 0
tests/units/services/layout/menuBuilder/websiteAdminMenuBuilder.test.ts

@@ -55,6 +55,7 @@ describe('build', () => {
       label: 'advanced_modification',
       icon: { name: 'fas fa-globe-americas' },
       to: 'https://some-website.com/typo3',
+      target: '_self',
       type: MENU_LINK_TYPE.EXTERNAL,
       active: false,
     })

+ 7 - 0
tests/units/services/layout/menuBuilder/websiteListMenuBuilder.test.ts

@@ -58,6 +58,7 @@ describe('build', () => {
         label: 'MyOrganization',
         icon: undefined,
         to: 'https://some-website.com',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },
@@ -80,6 +81,7 @@ describe('build', () => {
         label: 'parent1',
         icon: undefined,
         to: 'https://parent1.net',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },
@@ -87,6 +89,7 @@ describe('build', () => {
         label: 'parent2',
         icon: undefined,
         to: 'https://parent2.net',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },
@@ -94,6 +97,7 @@ describe('build', () => {
         label: 'parent3',
         icon: undefined,
         to: 'https://parent3.net',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },
@@ -118,6 +122,7 @@ describe('build', () => {
         label: 'MyOrganization',
         icon: undefined,
         to: 'https://some-website.com',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },
@@ -125,6 +130,7 @@ describe('build', () => {
         label: 'parent1',
         icon: undefined,
         to: 'https://parent1.net',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },
@@ -132,6 +138,7 @@ describe('build', () => {
         label: 'parent2',
         icon: undefined,
         to: 'https://parent2.net',
+        target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
       },

+ 8 - 0
types/enum/enums.ts

@@ -97,3 +97,11 @@ export const enum FILE_TYPE {
   BILL = 'BILL',
   UPLOADED = 'UPLOADED',
 }
+
+export const enum LINK_TARGET {
+  BLANK = '_blank',
+  SELF = '_self',
+  PARENT = '_parent',
+  TOP = '_top',
+  FRAMENAME = 'framename',
+}

+ 1 - 0
types/interfaces.d.ts

@@ -113,6 +113,7 @@ interface AccessProfile extends BaseAccessProfile {
   originalAccess: OrignalAccessProfile | null
   hasRole: (role: string) => boolean
   isAdminAccount: boolean
+  preferencesId: number | null
 }
 
 // TODO: y'a un problème entre l'interface AccessProfile et celle organizationState, les noms sont construits différemment,

+ 4 - 0
types/layout.d.ts

@@ -1,4 +1,5 @@
 import { MENU_LINK_TYPE } from '~/types/enum/layout'
+import { LINK_TARGET } from '~/types/enum/enums'
 
 interface IconItem {
   name?: string
@@ -18,6 +19,8 @@ interface MenuItem {
   avatar?: number
   /** Correspond à la page actuelle */
   active: boolean
+  /** Définit l'attribut 'target' du lien */
+  target?: LINK_TARGET
 }
 
 /**
@@ -28,6 +31,7 @@ interface MenuGroup {
   icon?: IconItem
   children?: MenuItems
   actions?: MenuItems
+  target?: LINK_TARGET
 }
 
 type MenuItems = Array<MenuItem | MenuGroup>

+ 543 - 1
yarn.lock

@@ -483,6 +483,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/aix-ppc64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/aix-ppc64@npm:0.21.5"
+  conditions: os=aix & cpu=ppc64
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-arm64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/android-arm64@npm:0.20.2"
@@ -490,6 +497,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/android-arm64@npm:0.21.5"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-arm@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/android-arm@npm:0.20.2"
@@ -497,6 +511,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-arm@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/android-arm@npm:0.21.5"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/android-x64@npm:0.20.2"
@@ -504,6 +525,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/android-x64@npm:0.21.5"
+  conditions: os=android & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/darwin-arm64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/darwin-arm64@npm:0.20.2"
@@ -511,6 +539,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/darwin-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/darwin-arm64@npm:0.21.5"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/darwin-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/darwin-x64@npm:0.20.2"
@@ -518,6 +553,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/darwin-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/darwin-x64@npm:0.21.5"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/freebsd-arm64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/freebsd-arm64@npm:0.20.2"
@@ -525,6 +567,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/freebsd-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/freebsd-arm64@npm:0.21.5"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/freebsd-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/freebsd-x64@npm:0.20.2"
@@ -532,6 +581,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/freebsd-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/freebsd-x64@npm:0.21.5"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-arm64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-arm64@npm:0.20.2"
@@ -539,6 +595,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-arm64@npm:0.21.5"
+  conditions: os=linux & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-arm@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-arm@npm:0.20.2"
@@ -546,6 +609,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-arm@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-arm@npm:0.21.5"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-ia32@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-ia32@npm:0.20.2"
@@ -553,6 +623,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-ia32@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-ia32@npm:0.21.5"
+  conditions: os=linux & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-loong64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-loong64@npm:0.20.2"
@@ -560,6 +637,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-loong64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-loong64@npm:0.21.5"
+  conditions: os=linux & cpu=loong64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-mips64el@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-mips64el@npm:0.20.2"
@@ -567,6 +651,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-mips64el@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-mips64el@npm:0.21.5"
+  conditions: os=linux & cpu=mips64el
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-ppc64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-ppc64@npm:0.20.2"
@@ -574,6 +665,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-ppc64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-ppc64@npm:0.21.5"
+  conditions: os=linux & cpu=ppc64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-riscv64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-riscv64@npm:0.20.2"
@@ -581,6 +679,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-riscv64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-riscv64@npm:0.21.5"
+  conditions: os=linux & cpu=riscv64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-s390x@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-s390x@npm:0.20.2"
@@ -588,6 +693,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-s390x@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-s390x@npm:0.21.5"
+  conditions: os=linux & cpu=s390x
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/linux-x64@npm:0.20.2"
@@ -595,6 +707,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/linux-x64@npm:0.21.5"
+  conditions: os=linux & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/netbsd-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/netbsd-x64@npm:0.20.2"
@@ -602,6 +721,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/netbsd-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/netbsd-x64@npm:0.21.5"
+  conditions: os=netbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/openbsd-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/openbsd-x64@npm:0.20.2"
@@ -609,6 +735,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/openbsd-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/openbsd-x64@npm:0.21.5"
+  conditions: os=openbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/sunos-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/sunos-x64@npm:0.20.2"
@@ -616,6 +749,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/sunos-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/sunos-x64@npm:0.21.5"
+  conditions: os=sunos & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-arm64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/win32-arm64@npm:0.20.2"
@@ -623,6 +763,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-arm64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/win32-arm64@npm:0.21.5"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-ia32@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/win32-ia32@npm:0.20.2"
@@ -630,6 +777,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-ia32@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/win32-ia32@npm:0.21.5"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-x64@npm:0.20.2":
   version: 0.20.2
   resolution: "@esbuild/win32-x64@npm:0.20.2"
@@ -637,6 +791,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-x64@npm:0.21.5":
+  version: 0.21.5
+  resolution: "@esbuild/win32-x64@npm:0.21.5"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0":
   version: 4.4.0
   resolution: "@eslint-community/eslint-utils@npm:4.4.0"
@@ -1340,6 +1501,34 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@nuxt/kit@npm:^3.12.2":
+  version: 3.12.2
+  resolution: "@nuxt/kit@npm:3.12.2"
+  dependencies:
+    "@nuxt/schema": "npm:3.12.2"
+    c12: "npm:^1.11.1"
+    consola: "npm:^3.2.3"
+    defu: "npm:^6.1.4"
+    destr: "npm:^2.0.3"
+    globby: "npm:^14.0.1"
+    hash-sum: "npm:^2.0.0"
+    ignore: "npm:^5.3.1"
+    jiti: "npm:^1.21.6"
+    klona: "npm:^2.0.6"
+    knitwork: "npm:^1.1.0"
+    mlly: "npm:^1.7.1"
+    pathe: "npm:^1.1.2"
+    pkg-types: "npm:^1.1.1"
+    scule: "npm:^1.3.0"
+    semver: "npm:^7.6.2"
+    ufo: "npm:^1.5.3"
+    unctx: "npm:^2.3.1"
+    unimport: "npm:^3.7.2"
+    untyped: "npm:^1.4.2"
+  checksum: 10c0/358e15b0e2305f41f21f814f88795eb1c0f4eb81bb3965a6d3da9ead7b7b1104e921006b03394733cff021636a1c0c59d19355b29ab97141650d2064f29466f8
+  languageName: node
+  linkType: hard
+
 "@nuxt/schema@npm:3.11.2, @nuxt/schema@npm:^3.11.1, @nuxt/schema@npm:^3.11.2":
   version: 3.11.2
   resolution: "@nuxt/schema@npm:3.11.2"
@@ -1359,6 +1548,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@nuxt/schema@npm:3.12.2":
+  version: 3.12.2
+  resolution: "@nuxt/schema@npm:3.12.2"
+  dependencies:
+    compatx: "npm:^0.1.8"
+    consola: "npm:^3.2.3"
+    defu: "npm:^6.1.4"
+    hookable: "npm:^5.5.3"
+    pathe: "npm:^1.1.2"
+    pkg-types: "npm:^1.1.1"
+    scule: "npm:^1.3.0"
+    std-env: "npm:^3.7.0"
+    ufo: "npm:^1.5.3"
+    uncrypto: "npm:^0.1.3"
+    unimport: "npm:^3.7.2"
+    untyped: "npm:^1.4.2"
+  checksum: 10c0/faf17d5d97cd601c4805cc1d5394af24ee8080afd80ef7ae1edb9f926904af4aed03cd67d122fa498832bcae7bdb868e39810a1082129fc1b384bc35dd5275bd
+  languageName: node
+  linkType: hard
+
 "@nuxt/schema@npm:@nuxt/schema-edge@3.8.0-28284309.b3d3d7f4":
   version: 3.8.0-28284309.b3d3d7f4
   resolution: "@nuxt/schema-edge@npm:3.8.0-28284309.b3d3d7f4"
@@ -3745,12 +3954,15 @@ __metadata:
     eslint-plugin-vue: "npm:^9.19.2"
     event-source-polyfill: "npm:^1.0.31"
     file-saver: "npm:^2.0.5"
+    glob: "npm:^10.4.2"
     js-yaml: "npm:^4.1.0"
     jsdom: "npm:^23.0.1"
     libphonenumber-js: "npm:1.10.51"
     lodash: "npm:^4.17.21"
     lodash-es: "npm:^4.17.21"
     nuxt: "npm:^3.11.2"
+    nuxt-prepare: "npm:^2.1.0"
+    nuxt-vitalizer: "npm:^0.10.0"
     pinia: "npm:^2.1.7"
     pinia-orm: "npm:^1.7.2"
     prettier: "npm:^3.1.0"
@@ -4360,6 +4572,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"bundle-require@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "bundle-require@npm:5.0.0"
+  dependencies:
+    load-tsconfig: "npm:^0.2.3"
+  peerDependencies:
+    esbuild: ">=0.18"
+  checksum: 10c0/92c46df02586e0ebd66ee4831c9b5775adb3c32a43fe2b2aaf7bc675135c141f751de6a9a26b146d64c607c5b40f9eef5f10dce3c364f602d4bed268444c32c6
+  languageName: node
+  linkType: hard
+
 "c12@npm:^1.10.0, c12@npm:^1.4.2":
   version: 1.10.0
   resolution: "c12@npm:1.10.0"
@@ -4380,6 +4603,31 @@ __metadata:
   languageName: node
   linkType: hard
 
+"c12@npm:^1.11.1":
+  version: 1.11.1
+  resolution: "c12@npm:1.11.1"
+  dependencies:
+    chokidar: "npm:^3.6.0"
+    confbox: "npm:^0.1.7"
+    defu: "npm:^6.1.4"
+    dotenv: "npm:^16.4.5"
+    giget: "npm:^1.2.3"
+    jiti: "npm:^1.21.6"
+    mlly: "npm:^1.7.1"
+    ohash: "npm:^1.1.3"
+    pathe: "npm:^1.1.2"
+    perfect-debounce: "npm:^1.0.0"
+    pkg-types: "npm:^1.1.1"
+    rc9: "npm:^2.1.2"
+  peerDependencies:
+    magicast: ^0.3.4
+  peerDependenciesMeta:
+    magicast:
+      optional: true
+  checksum: 10c0/4711a399b8ce54258982ffa4df15c88a1f12bbb23a806ebdb1d1b9e17134a1a3bf65be4ab0095b648bfc8f21646e3d343f6a8f12b130d459c3c7ef13437f5e92
+  languageName: node
+  linkType: hard
+
 "cac@npm:^6.7.14":
   version: 6.7.14
   resolution: "cac@npm:6.7.14"
@@ -4805,6 +5053,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"compatx@npm:^0.1.8":
+  version: 0.1.8
+  resolution: "compatx@npm:0.1.8"
+  checksum: 10c0/042b8ed40cd3041a843836dab730848c1bcea97ebdac207c9a04b4f8af116259a2147fdda0ce823cf161363b4def76f9b60019a1315cb3ea55f991f54b06c40e
+  languageName: node
+  linkType: hard
+
 "compress-commons@npm:^6.0.2":
   version: 6.0.2
   resolution: "compress-commons@npm:6.0.2"
@@ -5247,6 +5502,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"debug@npm:^4.3.5":
+  version: 4.3.5
+  resolution: "debug@npm:4.3.5"
+  dependencies:
+    ms: "npm:2.1.2"
+  peerDependenciesMeta:
+    supports-color:
+      optional: true
+  checksum: 10c0/082c375a2bdc4f4469c99f325ff458adad62a3fc2c482d59923c260cb08152f34e2659f72b3767db8bb2f21ca81a60a42d1019605a412132d7b9f59363a005cc
+  languageName: node
+  linkType: hard
+
 "decimal.js@npm:^10.4.3":
   version: 10.4.3
   resolution: "decimal.js@npm:10.4.3"
@@ -5876,6 +6143,86 @@ __metadata:
   languageName: node
   linkType: hard
 
+"esbuild@npm:~0.21.4":
+  version: 0.21.5
+  resolution: "esbuild@npm:0.21.5"
+  dependencies:
+    "@esbuild/aix-ppc64": "npm:0.21.5"
+    "@esbuild/android-arm": "npm:0.21.5"
+    "@esbuild/android-arm64": "npm:0.21.5"
+    "@esbuild/android-x64": "npm:0.21.5"
+    "@esbuild/darwin-arm64": "npm:0.21.5"
+    "@esbuild/darwin-x64": "npm:0.21.5"
+    "@esbuild/freebsd-arm64": "npm:0.21.5"
+    "@esbuild/freebsd-x64": "npm:0.21.5"
+    "@esbuild/linux-arm": "npm:0.21.5"
+    "@esbuild/linux-arm64": "npm:0.21.5"
+    "@esbuild/linux-ia32": "npm:0.21.5"
+    "@esbuild/linux-loong64": "npm:0.21.5"
+    "@esbuild/linux-mips64el": "npm:0.21.5"
+    "@esbuild/linux-ppc64": "npm:0.21.5"
+    "@esbuild/linux-riscv64": "npm:0.21.5"
+    "@esbuild/linux-s390x": "npm:0.21.5"
+    "@esbuild/linux-x64": "npm:0.21.5"
+    "@esbuild/netbsd-x64": "npm:0.21.5"
+    "@esbuild/openbsd-x64": "npm:0.21.5"
+    "@esbuild/sunos-x64": "npm:0.21.5"
+    "@esbuild/win32-arm64": "npm:0.21.5"
+    "@esbuild/win32-ia32": "npm:0.21.5"
+    "@esbuild/win32-x64": "npm:0.21.5"
+  dependenciesMeta:
+    "@esbuild/aix-ppc64":
+      optional: true
+    "@esbuild/android-arm":
+      optional: true
+    "@esbuild/android-arm64":
+      optional: true
+    "@esbuild/android-x64":
+      optional: true
+    "@esbuild/darwin-arm64":
+      optional: true
+    "@esbuild/darwin-x64":
+      optional: true
+    "@esbuild/freebsd-arm64":
+      optional: true
+    "@esbuild/freebsd-x64":
+      optional: true
+    "@esbuild/linux-arm":
+      optional: true
+    "@esbuild/linux-arm64":
+      optional: true
+    "@esbuild/linux-ia32":
+      optional: true
+    "@esbuild/linux-loong64":
+      optional: true
+    "@esbuild/linux-mips64el":
+      optional: true
+    "@esbuild/linux-ppc64":
+      optional: true
+    "@esbuild/linux-riscv64":
+      optional: true
+    "@esbuild/linux-s390x":
+      optional: true
+    "@esbuild/linux-x64":
+      optional: true
+    "@esbuild/netbsd-x64":
+      optional: true
+    "@esbuild/openbsd-x64":
+      optional: true
+    "@esbuild/sunos-x64":
+      optional: true
+    "@esbuild/win32-arm64":
+      optional: true
+    "@esbuild/win32-ia32":
+      optional: true
+    "@esbuild/win32-x64":
+      optional: true
+  bin:
+    esbuild: bin/esbuild
+  checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de
+  languageName: node
+  linkType: hard
+
 "escalade@npm:^3.1.1, escalade@npm:^3.1.2":
   version: 3.1.2
   resolution: "escalade@npm:3.1.2"
@@ -6935,7 +7282,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"giget@npm:^1.2.1":
+"get-tsconfig@npm:^4.7.5":
+  version: 4.7.5
+  resolution: "get-tsconfig@npm:4.7.5"
+  dependencies:
+    resolve-pkg-maps: "npm:^1.0.0"
+  checksum: 10c0/a917dff2ba9ee187c41945736bf9bbab65de31ce5bc1effd76267be483a7340915cff232199406379f26517d2d0a4edcdbcda8cca599c2480a0f2cf1e1de3efa
+  languageName: node
+  linkType: hard
+
+"giget@npm:^1.2.1, giget@npm:^1.2.3":
   version: 1.2.3
   resolution: "giget@npm:1.2.3"
   dependencies:
@@ -7019,6 +7375,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"glob@npm:^10.4.2":
+  version: 10.4.2
+  resolution: "glob@npm:10.4.2"
+  dependencies:
+    foreground-child: "npm:^3.1.0"
+    jackspeak: "npm:^3.1.2"
+    minimatch: "npm:^9.0.4"
+    minipass: "npm:^7.1.2"
+    package-json-from-dist: "npm:^1.0.0"
+    path-scurry: "npm:^1.11.1"
+  bin:
+    glob: dist/esm/bin.mjs
+  checksum: 10c0/2c7296695fa75a935f3ad17dc62e4e170a8bb8752cf64d328be8992dd6ad40777939003754e10e9741ff8fbe43aa52fba32d6930d0ffa0e3b74bc3fb5eebaa2f
+  languageName: node
+  linkType: hard
+
 "glob@npm:^7.1.3, glob@npm:^7.1.4":
   version: 7.2.3
   resolution: "glob@npm:7.2.3"
@@ -7444,6 +7816,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"importx@npm:^0.3.1":
+  version: 0.3.7
+  resolution: "importx@npm:0.3.7"
+  dependencies:
+    bundle-require: "npm:^5.0.0"
+    debug: "npm:^4.3.5"
+    esbuild: "npm:^0.20.2"
+    jiti: "npm:^1.21.6"
+    pathe: "npm:^1.1.2"
+    pkg-types: "npm:^1.1.1"
+    tsx: "npm:^4.15.6"
+  checksum: 10c0/967d58940255ff2fd95bf09f037707bc729b1ef0c11167a944291051484b9793477bd3a0cc9c5fba0aa3d71584d9f08daef47d4827c86b7a2efc97303b6d198a
+  languageName: node
+  linkType: hard
+
 "imurmurhash@npm:^0.1.4":
   version: 0.1.4
   resolution: "imurmurhash@npm:0.1.4"
@@ -8006,6 +8393,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"jackspeak@npm:^3.1.2":
+  version: 3.4.0
+  resolution: "jackspeak@npm:3.4.0"
+  dependencies:
+    "@isaacs/cliui": "npm:^8.0.2"
+    "@pkgjs/parseargs": "npm:^0.11.0"
+  dependenciesMeta:
+    "@pkgjs/parseargs":
+      optional: true
+  checksum: 10c0/7e42d1ea411b4d57d43ea8a6afbca9224382804359cb72626d0fc45bb8db1de5ad0248283c3db45fe73e77210750d4fcc7c2b4fe5d24fda94aaa24d658295c5f
+  languageName: node
+  linkType: hard
+
 "jest-diff@npm:^29.7.0":
   version: 29.7.0
   resolution: "jest-diff@npm:29.7.0"
@@ -8089,6 +8489,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"jiti@npm:^1.21.6":
+  version: 1.21.6
+  resolution: "jiti@npm:1.21.6"
+  bin:
+    jiti: bin/jiti.js
+  checksum: 10c0/05b9ed58cd30d0c3ccd3c98209339e74f50abd9a17e716f65db46b6a35812103f6bde6e134be7124d01745586bca8cc5dae1d0d952267c3ebe55171949c32e56
+  languageName: node
+  linkType: hard
+
 "js-beautify@npm:^1.14.9, js-beautify@npm:^1.6.14":
   version: 1.15.1
   resolution: "js-beautify@npm:1.15.1"
@@ -8447,6 +8856,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"load-tsconfig@npm:^0.2.3":
+  version: 0.2.5
+  resolution: "load-tsconfig@npm:0.2.5"
+  checksum: 10c0/bf2823dd26389d3497b6567f07435c5a7a58d9df82e879b0b3892f87d8db26900f84c85bc329ef41c0540c0d6a448d1c23ddc64a80f3ff6838b940f3915a3fcb
+  languageName: node
+  linkType: hard
+
 "local-pkg@npm:^0.4.3":
   version: 0.4.3
   resolution: "local-pkg@npm:0.4.3"
@@ -8922,6 +9338,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"minipass@npm:^7.1.2":
+  version: 7.1.2
+  resolution: "minipass@npm:7.1.2"
+  checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557
+  languageName: node
+  linkType: hard
+
 "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2":
   version: 2.1.2
   resolution: "minizlib@npm:2.1.2"
@@ -8974,6 +9397,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"mlly@npm:^1.7.0, mlly@npm:^1.7.1":
+  version: 1.7.1
+  resolution: "mlly@npm:1.7.1"
+  dependencies:
+    acorn: "npm:^8.11.3"
+    pathe: "npm:^1.1.2"
+    pkg-types: "npm:^1.1.1"
+    ufo: "npm:^1.5.3"
+  checksum: 10c0/d836a7b0adff4d118af41fb93ad4d9e57f80e694a681185280ba220a4607603c19e86c80f9a6c57512b04280567f2599e3386081705c5b5fd74c9ddfd571d0fa
+  languageName: node
+  linkType: hard
+
 "mri@npm:^1.2.0":
   version: 1.2.0
   resolution: "mri@npm:1.2.0"
@@ -9460,6 +9895,32 @@ __metadata:
   languageName: node
   linkType: hard
 
+"nuxt-prepare@npm:^2.1.0":
+  version: 2.1.0
+  resolution: "nuxt-prepare@npm:2.1.0"
+  dependencies:
+    "@nuxt/kit": "npm:^3.11.2"
+    defu: "npm:^6.1.4"
+    importx: "npm:^0.3.1"
+    mlly: "npm:^1.7.0"
+    pathe: "npm:^1.1.2"
+    scule: "npm:^1.3.0"
+    type-fest: "npm:^4.18.2"
+  checksum: 10c0/2af5f8cb456eba8ade0863d94f0274536cd6e3f54695aa8717722f642b726f63965cc4cb7a7108f766ac7fd7717595c890c5c90e248ffade780a7ec2369a4906
+  languageName: node
+  linkType: hard
+
+"nuxt-vitalizer@npm:^0.10.0":
+  version: 0.10.0
+  resolution: "nuxt-vitalizer@npm:0.10.0"
+  dependencies:
+    "@nuxt/kit": "npm:^3.12.2"
+    defu: "npm:^6.1.4"
+    knitwork: "npm:^1.1.0"
+  checksum: 10c0/228ea512010178291837d4f98f8d124cdf22068fba65982a4b47b21cdd4aa1fa46ef74777853723633476ed5b0309468b02af350bb42c0b189c91d4d38c22d1d
+  languageName: node
+  linkType: hard
+
 "nuxt@npm:^3.11.2":
   version: 3.11.2
   resolution: "nuxt@npm:3.11.2"
@@ -9783,6 +10244,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"package-json-from-dist@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "package-json-from-dist@npm:1.0.0"
+  checksum: 10c0/e3ffaf6ac1040ab6082a658230c041ad14e72fabe99076a2081bb1d5d41210f11872403fc09082daf4387fc0baa6577f96c9c0e94c90c394fd57794b66aa4033
+  languageName: node
+  linkType: hard
+
 "pacote@npm:^18.0.0":
   version: 18.0.5
   resolution: "pacote@npm:18.0.5"
@@ -9927,6 +10395,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"path-scurry@npm:^1.11.1":
+  version: 1.11.1
+  resolution: "path-scurry@npm:1.11.1"
+  dependencies:
+    lru-cache: "npm:^10.2.0"
+    minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
+  checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d
+  languageName: node
+  linkType: hard
+
 "path-type@npm:^4.0.0":
   version: 4.0.0
   resolution: "path-type@npm:4.0.0"
@@ -10033,6 +10511,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"pkg-types@npm:^1.1.1":
+  version: 1.1.1
+  resolution: "pkg-types@npm:1.1.1"
+  dependencies:
+    confbox: "npm:^0.1.7"
+    mlly: "npm:^1.7.0"
+    pathe: "npm:^1.1.2"
+  checksum: 10c0/c7d167935de7207479e5829086040d70bea289f31fc1331f17c83e996a4440115c9deba2aa96de839ea66e1676d083c9ca44b33886f87bffa6b49740b67b6fcb
+  languageName: node
+  linkType: hard
+
 "pluralize@npm:^8.0.0":
   version: 8.0.0
   resolution: "pluralize@npm:8.0.0"
@@ -11140,6 +11629,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"semver@npm:^7.6.2":
+  version: 7.6.2
+  resolution: "semver@npm:7.6.2"
+  bin:
+    semver: bin/semver.js
+  checksum: 10c0/97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c
+  languageName: node
+  linkType: hard
+
 "send@npm:0.18.0":
   version: 0.18.0
   resolution: "send@npm:0.18.0"
@@ -12226,6 +12724,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tsx@npm:^4.15.6":
+  version: 4.15.7
+  resolution: "tsx@npm:4.15.7"
+  dependencies:
+    esbuild: "npm:~0.21.4"
+    fsevents: "npm:~2.3.3"
+    get-tsconfig: "npm:^4.7.5"
+  dependenciesMeta:
+    fsevents:
+      optional: true
+  bin:
+    tsx: dist/cli.mjs
+  checksum: 10c0/e960f4ee084b48cd3183e65946725fd9b0de4afae32a0fd9cd47416a41259fb2c72838b7aeba26adaecc2d89d70e976add9722e72ea5c876b3b493f137cbbf12
+  languageName: node
+  linkType: hard
+
 "tuf-js@npm:^2.2.0":
   version: 2.2.1
   resolution: "tuf-js@npm:2.2.1"
@@ -12297,6 +12811,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"type-fest@npm:^4.18.2":
+  version: 4.20.1
+  resolution: "type-fest@npm:4.20.1"
+  checksum: 10c0/c31da16fe170a74c5b7e102e70e764ba8ec6ade128e782a56f842aefa07307df5a6353e6b5601d3b30ff2d856d8b955f89813df639e4758fedcf8e426b2d854e
+  languageName: node
+  linkType: hard
+
 "typed-array-buffer@npm:^1.0.2":
   version: 1.0.2
   resolution: "typed-array-buffer@npm:1.0.2"
@@ -12483,6 +13004,27 @@ __metadata:
   languageName: node
   linkType: hard
 
+"unimport@npm:^3.7.2":
+  version: 3.7.2
+  resolution: "unimport@npm:3.7.2"
+  dependencies:
+    "@rollup/pluginutils": "npm:^5.1.0"
+    acorn: "npm:^8.11.3"
+    escape-string-regexp: "npm:^5.0.0"
+    estree-walker: "npm:^3.0.3"
+    fast-glob: "npm:^3.3.2"
+    local-pkg: "npm:^0.5.0"
+    magic-string: "npm:^0.30.10"
+    mlly: "npm:^1.7.0"
+    pathe: "npm:^1.1.2"
+    pkg-types: "npm:^1.1.1"
+    scule: "npm:^1.3.0"
+    strip-literal: "npm:^2.1.0"
+    unplugin: "npm:^1.10.1"
+  checksum: 10c0/d07f41c210854f1b58364bec985882e7c521fc4e7ee46bd7e98c9c797ce14cf0bdf2ab9c2d1fd618bdd188e470bf005d15053727bf5b08a098ccc309313de40e
+  languageName: node
+  linkType: hard
+
 "unique-filename@npm:^3.0.0":
   version: 3.0.0
   resolution: "unique-filename@npm:3.0.0"