Explorar o código

restore activity year, setup rewritten, refactor abilities (ongoing)

Olivier Massot %!s(int64=3) %!d(string=hai) anos
pai
achega
3e83cb6fe2

+ 19 - 15
components/Layout/Dialog.vue

@@ -6,17 +6,19 @@
     max-width="800"
     :content-class="contentClass"
   >
-    <v-card  class="d-flex">
-
+    <v-card class="d-flex">
         <div class="dialog-type flex-column justify-center d-none d-sm-flex">
-          <h3 class="d-flex"> <slot name="dialogType"></slot></h3>
+          <h3 class="d-flex">
+            <slot name="dialogType" />
+          </h3>
         </div>
 
         <div class="dialog-container flex-column flex-grow-1">
           <div class="d-flex flex-column">
             <v-card-title class="dialog-title">
-              <slot name="dialogTitle"></slot>
+              <slot name="dialogTitle" />
             </v-card-title>
+
             <div class="dialog-text-container">
               <slot name="dialogText" />
             </div>
@@ -27,9 +29,7 @@
               <slot name="dialogBtn" />
             </v-card-actions>
           </div>
-
         </div>
-
     </v-card>
   </v-dialog>
 </template>
@@ -48,16 +48,18 @@ const props = defineProps({
 </script>
 
 <style lang="scss" scoped>
-  .dialog-title{
+  .dialog-title {
     background: #e6e6e6;
     padding-left: 40px;
     font-weight: normal;
   }
-  .dialog-type{
-    background: var(--v-theme-ot-green, #00AD8E);
+
+  .dialog-type {
+    background: rgb(var(--v-theme-ot-green, #00AD8E));
     color: #fff;
     width: 160px;
-   h3{
+
+   h3 {
      font-size: 25px;
      font-weight: normal;
      writing-mode: tb-lr;
@@ -68,18 +70,20 @@ const props = defineProps({
     }
   }
 
-  .dialog-text-container{
+  .dialog-text-container {
     max-height: 70vh;
     overflow: auto;
   }
-  .modal-level-alert{
+
+  .modal-level-alert {
     .dialog-type{
-      background: var(--v-theme-ot-danger, #f56954);
+      background: rgb(var(--v-theme-ot-danger, #f56954));
     }
   }
-  .modal-level-warning{
+
+  .modal-level-warning {
     .dialog-type{
-      background: var(--v-theme-ot-warning, #f39c12);
+      background: rgb(var(--v-theme-ot-warning, #f39c12));
     }
   }
 </style>

+ 19 - 28
components/Layout/Header.vue

@@ -21,43 +21,33 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
 
     <v-toolbar-title v-text="title" />
 
-    <v-spacer />
+<!--    <v-spacer />-->
 
 <!--    <LayoutHeaderUniversalCreationCreateButton v-if="showUniversalButton" />-->
 
-<!--    <v-tooltip :text="$t('welcome')" location="bottom">-->
-<!--      <template #activator="{ props }">-->
-<!--        <v-btn-->
-<!--            v-bind="props"-->
-<!--            icon="fas fa-home"-->
-<!--            :href="homeUrl"-->
-<!--            class="ml-2 text-ot-white"-->
-<!--            size="small"-->
-<!--        ></v-btn>-->
-<!--      </template>-->
-<!--    </v-tooltip>-->
+    <LayoutHeaderHomeBtn />
 
     <LayoutHeaderMenu name="WebsiteList" />
 
-<!--    <LayoutHeaderMenu name="MyAccesses" />-->
+    <LayoutHeaderMenu name="MyAccesses" />
 
-<!--    <LayoutHeaderMenu name="MyFamily" />-->
+    <LayoutHeaderMenu name="MyFamily" />
 
 <!--    <LayoutHeaderNotification />-->
 
-<!--    <LayoutHeaderMenu name="Configuration" />-->
+    <LayoutHeaderMenu name="Configuration" />
 
-<!--    <LayoutHeaderMenu name="Account" />-->
+    <LayoutHeaderMenu name="Account" />
 
-<!--    <a-->
-<!--        href="https://support.opentalent.fr/"-->
-<!--        class="text-body pa-3 ml-2 bg-ot-dark-grey text-ot-white text-decoration-none"-->
-<!--        target="_blank"-->
-<!--    >-->
-<!--      &lt;!&ndash; TODO: mettre le lien vers le support dans les .env ou dans la conf &ndash;&gt;-->
-<!--      <span class="d-none d-sm-none d-md-flex">{{ $t('help_access') }}</span>-->
-<!--      <v-icon icon="fas fa-question-circle" class="d-sm-flex d-md-none" color="white"></v-icon>-->
-<!--    </a>-->
+    <a
+        href="https://support.opentalent.fr/"
+        class="text-body pa-3 ml-2 bg-ot-dark-grey text-ot-white text-decoration-none"
+        target="_blank"
+    >
+      <!-- TODO: mettre le lien vers le support dans les .env ou dans la conf -->
+      <span class="d-none d-sm-none d-md-flex">{{ $t('help_access') }}</span>
+      <v-icon icon="fas fa-question-circle" class="d-sm-flex d-md-none" color="white"></v-icon>
+    </a>
   </v-app-bar>
 </template>
 
@@ -79,9 +69,6 @@ const hasMainMenu = computed(() => hasMenu('Main'))
 const isMainMenuOpened = computed(() => isMenuOpened('Main'))
 const toggleMainMenu = () => toggleMenu('Main')
 
-const runtimeConfig = useRuntimeConfig()
-const homeUrl = runtimeConfig.baseUrlAdminLegacy
-
 const { can } = useAbility()
 
 const showUniversalButton =
@@ -104,4 +91,8 @@ const showUniversalButton =
     font-size: 14px;
     text-decoration: none;
   }
+
+  :deep(.v-btn) {
+    background: none !important;
+  }
 </style>

+ 26 - 0
components/Layout/Header/HomeBtn.vue

@@ -0,0 +1,26 @@
+<template>
+  <div>
+    <v-btn
+        ref="btn"
+        icon="fas fa-home"
+        size="small"
+        :href="homeUrl"
+        class="ml-2 text-ot-white"
+    />
+    <v-tooltip :activator="btn" :text="$t('welcome')" location="bottom" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import {useRuntimeConfig} from "#app";
+  import {ref} from "@vue/reactivity";
+
+  const runtimeConfig = useRuntimeConfig()
+  const homeUrl = runtimeConfig.baseUrlAdminLegacy
+
+  const btn = ref(null);
+</script>
+
+<style scoped>
+
+</style>

+ 66 - 69
components/Layout/Header/Menu.vue

@@ -4,80 +4,75 @@ header principal (configuration, paramètres du compte...)
 -->
 
 <template>
-  <v-tooltip
-      v-if="displayMenu"
-      :text="$t(menu.label)"
-      location="bottom"
-  >
-    <template #activator="{ props }">
-      <v-btn v-bind="props" icon width="48px">
-        <v-avatar v-if="menu.icon.avatarId || menu.icon.avatarByDefault" size="30">
-          <UiImage :id="menu.icon.avatarId" :defaultImage="menu.icon.avatarByDefault" :width="30"></UiImage>
-        </v-avatar>
-        <v-icon  v-else class="text-ot-white" small>
-          {{ menu.icon.name }}
-        </v-icon>
-
-        <v-menu
-            :model-value="isOpened()"
-            activator="parent"
-            location="start"
-            @update:modelValue="onStateUpdated"
-        >
-          <v-card>
-            <v-card-title class="ot-header-menu text-body-2 font-weight-bold">
-              {{$t(menu.label)}}
-            </v-card-title>
-
-            <v-card-text class="ma-0 pa-0 header-menu">
-              <v-list density="compact" :subheader="true">
-                <template v-for="(child, index) in menu.children" :key="index">
-                  <v-list-item
-
-                      :id="child.label"
-                      :href="!isInternalLink(child) ? child.to : undefined"
-                      :to="isInternalLink(child) ? child.to : undefined"
-                  >
-                    <v-list-item-title>
-                      <span v-if="child.icon">
-                        <v-avatar v-if="menu.icon.avatarId || child.icon.avatarByDefault" size="30">
-                          <UiImage :id="child.icon.avatarId" :defaultImage="child.icon.avatarByDefault" :width="30"></UiImage>
-                        </v-avatar>
-                        <v-icon v-else class="text-ot-white" small>
-                          {{ child.icon.name }}
-                        </v-icon>
-                      </span>
-
-                      <span>{{$t(child.label)}}</span>
-                    </v-list-item-title>
-                  </v-list-item>
-
-                </template>
-              </v-list>
-            </v-card-text>
-
-            <v-card-actions v-if="menu.actions.length > 0" class="ma-0 pa-0 actions">
-              <template v-for="(action, index) in menu.actions" :key="index">
-                <v-list-item
-                    :id="action.label"
-                    :href="!isInternalLink(action) ? action.to : undefined"
-                    :to="isInternalLink(action) ? action.to : undefined"
-                >
-                  <v-list-item-title class="text-body-2" v-text="$t(action.label)"/>
-                </v-list-item>
-              </template>
-            </v-card-actions>
-          </v-card>
-        </v-menu>
-      </v-btn>
-    </template>
-  </v-tooltip>
+  <div v-if="displayMenu">
 
+    <v-btn ref="btn" icon width="48px" size="small">
+      <v-avatar v-if="menu.icon.avatarId || menu.icon.avatarByDefault" size="30">
+        <UiImage :id="menu.icon.avatarId" :defaultImage="menu.icon.avatarByDefault" :width="30"></UiImage>
+      </v-avatar>
+      <v-icon v-else class="text-ot-white">
+        {{ menu.icon.name }}
+      </v-icon>
+    </v-btn>
+
+    <v-tooltip :activator="btn" :text="$t(menu.label)" location="bottom" />
+
+    <v-menu
+        :activator="btn"
+        :model-value="isOpened()"
+        location="start"
+        @update:modelValue="onStateUpdated"
+    >
+      <v-card>
+        <v-card-title class="ot-header-menu text-body-2 font-weight-bold">
+          {{$t(menu.label)}}
+        </v-card-title>
+
+        <v-card-text class="ma-0 pa-0 header-menu">
+          <v-list density="compact" :subheader="true">
+            <template v-for="(child, index) in menu.children" :key="index">
+              <v-list-item
+                  :id="child.label"
+                  :href="!isInternalLink(child) ? child.to : undefined"
+                  :to="isInternalLink(child) ? child.to : undefined"
+              >
+                <v-list-item-title>
+                    <span v-if="child.icon">
+                      <v-avatar v-if="menu.icon.avatarId || child.icon.avatarByDefault" size="30">
+                        <UiImage :id="child.icon.avatarId" :defaultImage="child.icon.avatarByDefault" :width="30"></UiImage>
+                      </v-avatar>
+                      <v-icon v-else class="text-ot-white" size="small">
+                        {{ child.icon.name }}
+                      </v-icon>
+                    </span>
+
+                  <span>{{$t(child.label)}}</span>
+                </v-list-item-title>
+              </v-list-item>
+
+            </template>
+          </v-list>
+        </v-card-text>
+
+        <v-card-actions v-if="menu.actions.length > 0" class="ma-0 pa-0 actions">
+          <template v-for="(action, index) in menu.actions" :key="index">
+            <v-list-item
+                :id="action.label"
+                :href="!isInternalLink(action) ? action.to : undefined"
+                :to="isInternalLink(action) ? action.to : undefined"
+            >
+              <v-list-item-title class="text-body-2" v-text="$t(action.label)"/>
+            </v-list-item>
+          </template>
+        </v-card-actions>
+      </v-card>
+    </v-menu>
+  </div>
 </template>
 
 <script setup lang="ts">
 import {useMenu} from "~/composables/layout/useMenu";
-import {computed} from "@vue/reactivity";
+import {computed, ref} from "@vue/reactivity";
 
 const props = defineProps({
   name: {
@@ -96,6 +91,8 @@ const onStateUpdated = (e: any) => {
   setMenuState(props.name, e)
 }
 
+const btn = ref(null)
+
 </script>
 
 <style scoped lang="scss">

+ 164 - 189
components/Layout/Header/Notification.vue

@@ -1,34 +1,32 @@
 <template>
-  <v-menu offset-y v-model="isOpen">
-    <template #activator="{ on: { click }, attrs }">
-      <v-tooltip bottom>
-        <template #activator="{ on: on_tooltips , attrs: attrs_tooltips }">
-          <v-btn
-            icon
-            v-bind="[attrs, attrs_tooltips]"
-            color=""
-            v-on="on_tooltips"
-            @click="click"
-          >
-            <v-badge
-              color="orange"
-              offset-y="10"
-              :value="unreadNotification.length > 0"
-              :content="unreadNotification.length"
-            >
-              <v-icon class="ot-white--text" small>
-                fa-bell
-              </v-icon>
-            </v-badge>
-          </v-btn>
-        </template>
-        <span>{{ $t('notification') }}</span>
-      </v-tooltip>
-    </template>
+  <v-btn
+      ref="btn"
+      icon
+      color=""
+      @click="click"
+  >
+    <v-badge
+        color="orange"
+        offset-y="10"
+        :value="unreadNotification.length > 0"
+        :content="unreadNotification.length"
+    >
+      <v-icon class="text-ot-white" small>
+        fa-bell
+      </v-icon>
+    </v-badge>
+  </v-btn>
+
+  <v-tooltip bottom :activator="btn">
+    <span>{{ $t('notification') }}</span>
+  </v-tooltip>
+
+  <v-menu :activator="btn" offset-y v-model="isOpen">
     <v-card scrollable max-width="400">
-      <v-card-title class="ot-header_menu text-body-2 font-weight-bold">
+      <v-card-title class="ot-header-menu text-body-2 font-weight-bold">
         {{ $t('notification') }}
       </v-card-title>
+
       <v-card-text class="ma-0 pa-0 header-menu">
         <v-list dense :subheader="true">
           <template v-for="(notification, index) in notifications">
@@ -37,13 +35,15 @@
                 <v-list-item-title class="list_item mt-2 mb-2" v-text="getMessage(notification)"/>
               </v-list-item-content>
               <v-list-item-icon v-if="notification.link" class="pt-4">
-                <v-icon @click="download(notification.link)">mdi-download</v-icon>
+                <v-icon @click="download(notification.link)" icon="mdi-download" />
               </v-list-item-icon>
             </v-list-item>
             <v-divider></v-divider>
           </template>
         </v-list>
+
         <v-card v-intersect="update"></v-card>
+
         <v-row
           v-if="loading"
           class="fill-height mt-3 mb-3"
@@ -56,6 +56,7 @@
           ></v-progress-circular>
         </v-row>
       </v-card-text>
+
       <v-card-actions class="ma-0 pa-0">
         <template>
           <v-list-item
@@ -64,7 +65,7 @@
             :href="notificationUrl"
             router
           >
-            <v-list-item-title class="text-body-2 ot-white--text" v-text="$t('all_notification')"/>
+            <v-list-item-title class="text-body-2 text-ot-white" v-text="$t('all_notification')"/>
           </v-list-item>
         </template>
       </v-card-actions>
@@ -72,185 +73,159 @@
   </v-menu>
 </template>
 
-<script lang="ts">
-import {computed, ComputedRef, defineComponent, onUnmounted, Ref, ref, useContext, useFetch, watch} from '@nuxtjs/composition-api'
+<script setup lang="ts">
 import {NOTIFICATION_TYPE, QUERY_TYPE} from "~/types/enum/enums";
 import {Notification} from "~/models/Core/Notification";
-import {repositoryHelper} from "~/services/store/repository";
-import {ApiResponse, HydraMetadata} from "~/types/interfaces";
-import {queryHelper} from "~/services/store/query";
 import {NotificationUsers} from "~/models/Core/NotificationUsers";
-import {$accessProfile} from "~/services/accessProfile";
-
-export default defineComponent({
-  setup: function () {
-    const {$dataProvider, $dataPersister, $config, store, app: { i18n }} = useContext()
-    const accessProfile = store.state.profile.access
-    $accessProfile.setStore(store)
-    const currentAccessId = $accessProfile.getCurrentAccessId()
-
-    const loading: Ref<Boolean> = ref(true)
-    const isOpen: Ref<Boolean> = ref(false)
-    const page: Ref<number> = ref(1)
-    const data: Ref<ApiResponse> = ref({} as ApiResponse)
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import {ComputedRef, Ref} from "@vue/reactivity";
+import {useEntityFetch} from "~/composables/data/useEntityFetch";
+import {Pagination} from "~/types/data";
 
-    /**
-     * On récupère les notifications via l'API qui seront stockées dans le store
-     */
-    const {fetch, fetchState} = useFetch(async () => {
-      data.value = await $dataProvider.invoke({
-        type: QUERY_TYPE.MODEL,
-        model: Notification,
-        listArgs: {
-          itemsPerPage: 10,
-          page: page.value
-        }
-      })
-      loading.value = false
-    })
+const accessProfileStore = useAccessProfileStore()
+const currentAccessId = accessProfileStore.id
+const loading: Ref<Boolean> = ref(true)
+const isOpen: Ref<Boolean> = ref(false)
+const page: Ref<number> = ref(1)
 
-    /**
-     * On récupère les Notifications via le store
-     */
-    const notifications: ComputedRef = computed(() => {
-      const query = repositoryHelper.getRepository(Notification).with('message').orderBy('id', 'desc')
-      return queryHelper.getCollection(query)
-    })
+const i18n = useI18n()
+const runtimeConfig = useRuntimeConfig()
 
-    /**
-     * on calcul le nombre de notification non lues
-     */
-    const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
-      return notifications.value.filter((notification: Notification) => {
-        return notification.notificationUsers.length === 0
-      })
-    })
+const btn = ref(null)
 
-    /**
-     * Les metadata dépendront de la dernière valeur du GET lancé
-     */
-    const metadata: ComputedRef<HydraMetadata> = computed(() => {
-      return data.value.metadata
-    })
+const { fetchCollection } = useEntityFetch()
 
-    /**
-     * Lorsque l'utilisateur scroll on regarde la nextPage a charger et on le fait que si le pending du fetch est false
-     * (si on a fini de télécharger les éléments précédents)
-     */
-    const update = async () => {
-      if (!fetchState.pending && metadata.value?.nextPage && metadata.value.nextPage > 0) {
-        loading.value = true
-        page.value = metadata.value.nextPage
-        await fetch()
-        //Si des notifications n'avaient pas été marquées comme lues, on le fait immédiatement.
-        markNotificationsAsRead()
-      }
-    }
-
-    /**
-     * On construit le message qui va devoir s'afficher pour une notification
-     * @param notification
-     */
-    const getMessage = (notification:Notification) => {
-      switch (notification.type){
-        case NOTIFICATION_TYPE.FILE :
-         return `${i18n.t('your_file')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be_downloaded')}`
-          break;
-
-         case NOTIFICATION_TYPE.MESSAGE:
-           if(notification.message?.action)
-             return `${i18n.t('your_message')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be')} ${notification.message.action}`
-
-           return `${i18n.t('your_message')} ${notification.message?.about ?? ''} ${i18n.t('has_been_sent')} `
-           break;
-
-        case NOTIFICATION_TYPE.SYSTEM :
-          if(notification.message?.about)
-            return `${i18n.t(notification.message.about)}`
-          break;
-
-        default:
-          return i18n.t(notification.name)
-      }
-    }
+const { data: collection, pending } = await fetchCollection(Notification)
 
-    /**
-     * Dès l'ouverture du menu, on indique que les notifications non lues, le sont.
-     */
-    const unwatch = watch(isOpen, (newValue, oldValue) => {
-      if(newValue){
-        markNotificationsAsRead()
-      }
-    })
-
-    onUnmounted(() => {
-      unwatch()
-    })
+/**
+ * On récupère les Notifications via le store
+ */
+const notifications: ComputedRef = computed(() => {
+  // TODO: revoir pour reprendre le order by et tout
+  return collection.value !== null ? collection.value.items : []
+})
 
-    /**
-     * Marque les notification non lues comme lues
-     */
-    const markNotificationsAsRead = () => {
-      unreadNotification.value.map((notification:Notification)=>{
-        notification.notificationUsers = ['read']
-        repositoryHelper.persist(Notification, notification)
-        createNewNotificationUsers(notification)
-      })
-    }
+/**
+ * Les metadata dépendront de la dernière valeur du GET lancé
+ */
+const pagination: ComputedRef<Pagination> = computed(() => {
+  return collection.value !== null ? collection.value.pagination : {}
+})
 
-    /**
-     * Créer une nouvelle notification users coté back.
-     * @param notification
-     */
-    const createNewNotificationUsers = (notification: Notification) =>{
-      const newNotificationUsers = repositoryHelper.persist(NotificationUsers, new NotificationUsers(
-        {
-          access:`/api/accesses/${currentAccessId}`,
-          notification:`/api/notifications/${notification.id}`,
-          isRead: true
-        }
-      )) as NotificationUsers
+/**
+ * On calcule le nombre de notifications non lues
+ */
+const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
+  return notifications.value.filter((notification: Notification) => {
+    return notification.notificationUsers.length === 0
+  })
+})
 
-      $dataPersister.invoke({
-        type: QUERY_TYPE.MODEL,
-        model: NotificationUsers,
-        idTemp: newNotificationUsers.id,
-        showProgress: false
-      })
-    }
+/**
+ * Lorsque l'utilisateur scroll on regarde la nextPage a charger et on le fait que si le pending du fetch est false
+ * (si on a fini de télécharger les éléments précédents)
+ */
+const update = async () => {
+  if (!fetchState.pending && metadata.value?.nextPage && metadata.value.nextPage > 0) {
+    loading.value = true
+    page.value = metadata.value.nextPage
+    await fetch()
+    //Si des notifications n'avaient pas été marquées comme lues, on le fait immédiatement.
+    markNotificationsAsRead()
+  }
+}
+
+/**
+ * On construit le message qui va devoir s'afficher pour une notification
+ * @param notification
+ */
+const getMessage = (notification:Notification) => {
+  switch (notification.type){
+    case NOTIFICATION_TYPE.FILE :
+     return `${i18n.t('your_file')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be_downloaded')}`
+
+     case NOTIFICATION_TYPE.MESSAGE:
+       if(notification.message?.action)
+         return `${i18n.t('your_message')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be')} ${notification.message.action}`
+
+       return `${i18n.t('your_message')} ${notification.message?.about ?? ''} ${i18n.t('has_been_sent')} `
+
+    case NOTIFICATION_TYPE.SYSTEM :
+      if(notification.message?.about)
+        return `${i18n.t(notification.message.about)}`
+      break;
+
+    default:
+      return i18n.t(notification.name)
+  }
+}
+
+/**
+ * Dès l'ouverture du menu, on indique que les notifications non lues, le sont.
+ */
+const unwatch = watch(isOpen, (newValue, oldValue) => {
+  if(newValue){
+    markNotificationsAsRead()
+  }
+})
 
-    /**
-     * Download le lien
-     * @param link
-     */
-    const download = (link: string) => {
-      const url_parts: Array<string> = link.split('/api');
-      if(accessProfile.originalAccess)
-        url_parts[0] = `api/${accessProfile.originalAccess.id}/${currentAccessId}`
-      else
-        url_parts[0] = `api/${currentAccessId}`
+onUnmounted(() => {
+  unwatch()
+})
 
-      window.open(`${$config.baseURL_Legacy}/${url_parts.join('')}`);
+/**
+ * Marque les notifications non lues comme lues
+ */
+const markNotificationsAsRead = () => {
+  unreadNotification.value.map((notification:Notification)=>{
+    notification.notificationUsers = ['read']
+    repositoryHelper.persist(Notification, notification)
+    createNewNotificationUsers(notification)
+  })
+}
+
+/**
+ * Créer une nouvelle notification users coté back.
+ * @param notification
+ */
+const createNewNotificationUsers = (notification: Notification) =>{
+  const newNotificationUsers = repositoryHelper.persist(NotificationUsers, new NotificationUsers(
+    {
+      access:`/api/accesses/${currentAccessId}`,
+      notification:`/api/notifications/${notification.id}`,
+      isRead: true
     }
+  )) as NotificationUsers
+
+  $dataPersister.invoke({
+    type: QUERY_TYPE.MODEL,
+    model: NotificationUsers,
+    idTemp: newNotificationUsers.id,
+    showProgress: false
+  })
+}
+
+/**
+ * Download le lien
+ * @param link
+ */
+const download = (link: string) => {
+  const url_parts: Array<string> = link.split('/api');
+  if(accessProfileStore.originalAccess)
+    url_parts[0] = `api/${accessProfileStore.originalAccess.id}/${currentAccessId}`
+  else
+    url_parts[0] = `api/${currentAccessId}`
+
+  window.open(`${runtimeConfig.baseUrlLegacy}/${url_parts.join('')}`);
+}
+
+const notificationUrl = `${runtimeConfig.baseURL_adminLegacy}/notifications/list/`
 
-    return {
-      data,
-      getMessage,
-      notificationUrl: `${$config.baseURL_adminLegacy}/notifications/list/`,
-      loading,
-      notifications,
-      update,
-      unreadNotification,
-      isOpen,
-      download
-    }
-  }
-})
 </script>
 
-<style scoped>
+<style scoped lang="scss">
   #all_notifications{
-    background: var(--v-theme-ot-green, white);
+    background: rgb(var(--v-theme-ot-green, white));
     color: white;
   }
   .list_item{

+ 23 - 30
components/Layout/Header/UniversalCreation/CreateButton.vue

@@ -6,66 +6,59 @@ bouton Créer
   <main>
     <v-btn
       elevation="2"
-      color="ot-warning ot-white--text"
+      color="ot-warning text-ot-white"
       @click="showDialog=true"
     >
       {{ $t('create') }}
     </v-btn>
-    <lazy-LayoutDialog
-      :show="showDialog"
-    >
 
+    <LayoutDialog :show="showDialog" >
       <template #dialogType>{{ $t('creative_assistant') }}</template>
+
       <template #dialogTitle>
-        <span v-if="type=='home'">{{ $t('what_do_you_want_to_create') }}</span>
-        <span v-else-if="type=='access'">{{ $t('universal_create_title_access') }}</span>
-        <span v-else-if="type=='event'">{{ $t('universal_create_title_event') }}</span>
-        <span v-else-if="type=='message'">{{ $t('universal_create_title_message') }}</span>
+        <span v-if="type === 'home'">{{ $t('what_do_you_want_to_create') }}</span>
+        <span v-else-if="type === 'access'">{{ $t('universal_create_title_access') }}</span>
+        <span v-else-if="type === 'event'">{{ $t('universal_create_title_event') }}</span>
+        <span v-else-if="type === 'message'">{{ $t('universal_create_title_message') }}</span>
       </template>
+
       <template #dialogText>
         <LayoutHeaderUniversalCreationGenerateCardsSteps :step="step" @updateStep="updateStep" />
       </template>
+
       <template #dialogBtn>
         <div class="text-center">
           <v-btn
-            color="ot-super_light_grey"
-            @click="showDialog=false;step=1;type='home'"
+            color="ot-super-light-grey"
+            @click="showDialog=false; step=1; type='home'"
           >
             {{ $t('cancel') }}
           </v-btn>
+
           <v-btn
             v-if="step > 1"
             color="ot-super_light_grey"
-            @click="step=1;type='home'"
+            @click="step=1; type='home'"
           >
             {{ $t('previous') }}
           </v-btn>
         </div>
       </template>
-    </lazy-LayoutDialog>
+    </LayoutDialog>
   </main>
 </template>
 
-<script lang="ts">
-import {defineComponent, Ref,ref} from '@nuxtjs/composition-api'
+<script setup lang="ts">
+  import {Ref, ref} from "@vue/reactivity";
+
+  const showDialog: Ref<Boolean> = ref(false);
+  const step: Ref<Number> = ref(1);
+  const type: Ref<String> = ref('home');
 
-export default defineComponent({
-  setup () {
-    const showDialog:Ref<Boolean> = ref(false);
-    const step:Ref<Number> = ref(1);
-    const type:Ref<String> = ref('home');
-    const updateStep = ({stepChoice, typeChoice}: any) =>{
-      step.value = stepChoice
-      type.value = typeChoice
-    }
-    return {
-      showDialog,
-      updateStep,
-      step,
-      type
-    }
+  const updateStep = ({stepChoice, typeChoice}: any) =>{
+    step.value = stepChoice
+    type.value = typeChoice
   }
-})
 </script>
 <style scoped>
 

+ 141 - 54
components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue

@@ -3,52 +3,58 @@
 -->
 
 <template>
-  <v-stepper v-model="step"
-  >
+  <v-stepper v-model="step">
     <v-stepper-items>
       <v-stepper-content step="1">
         <div class="row">
+          <!-- TODO: peut-être faire un builder comme pour les menus? -->
           <LayoutHeaderUniversalCreationTypeCard
-            v-if="$can('manage', 'users')"
+            v-if="can('manage', 'users')"
             title="a_person"
             text-content="add_new_person_student"
             icon="fa fa-user"
             type="access"
             @typeClick="onTypeClick"
           />
+
           <LayoutHeaderUniversalCreationTypeCard
-            v-if="$can('display', 'agenda_page')
-                && ($ability.can('display', 'course_page')
-            || $ability.can('display', 'exam_page')
-            || $ability.can('display', 'pedagogics_project_page'))"
+            v-if="can('display', 'agenda_page')
+                  && (can('display', 'course_page')
+                  || can('display', 'exam_page')
+                  || can('display', 'pedagogics_project_page'))"
             title="an_event"
             text-content="add_an_event_course"
             icon="fa fa-calendar-alt"
             type="event"
             @typeClick="onTypeClick"
           />
+
           <LayoutHeaderUniversalCreationTypeCard
-            v-else-if="$can('display', 'agenda_page') && $can('manage', 'events')"
-            title="other_event" text-content="other_event_text_creation_card"
-            icon="far fa-calendar" :link="adminLegacy+ '/calendar/create/events'" />
+            v-else-if="can('display', 'agenda_page') && can('manage', 'events')"
+            title="other_event"
+            text-content="other_event_text_creation_card"
+            icon="far fa-calendar"
+            :link="adminLegacy + '/calendar/create/events'"
+          />
 
           <LayoutHeaderUniversalCreationTypeCard
-            v-if="$can('display', 'message_send_page')
-            && ($ability.can('manage', 'emails')
-            || $ability.can('manage', 'mails')
-            || $ability.can('manage', 'texto'))"
+            v-if="can('display', 'message_send_page')
+                  && (can('manage', 'emails')
+                  || can('manage', 'mails')
+                  || can('manage', 'texto'))"
             title="a_correspondence"
             text-content="sen_email_letter"
             icon="fa fa-comment"
             type="message"
             @typeClick="onTypeClick"
           />
+
           <LayoutHeaderUniversalCreationTypeCard
-            v-if="$can('manage', 'equipments')"
+            v-if="can('manage', 'equipments')"
             title="a_materiel"
             text-content="add_any_type_material"
             icon="fa fa-cube"
-            :link="adminLegacy+ '/list/create/equipment'"
+            :link="adminLegacy + '/list/create/equipment'"
           />
         </div>
       </v-stepper-content>
@@ -56,64 +62,145 @@
       <v-stepper-content step="2">
         <div class="row">
           <div v-if="type === 'access'" class="row">
-            <LayoutHeaderUniversalCreationTypeCard v-if="isLaw1901" title="an_adherent" text-content="adherent_text_creation_card" icon="fa fa-user" :link="adminLegacy+ '/universal_creation_person/adherent'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard v-if="isLaw1901" title="a_ca_member" text-content="ca_member_text_creation_card" icon="fa fa-users" :link="adminLegacy+ '/universal_creation_person/ca_member'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard title="a_student" text-content="student_text_creation_card" icon="fa fa-user" :link="adminLegacy+ '/universal_creation_person/student'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard title="a_guardian" text-content="guardian_text_creation_card" icon="fa fa-female" :link="adminLegacy+ '/universal_creation_person/guardian'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard title="a_teacher" text-content="teacher_text_creation_card" icon="fa fa-graduation-cap" :link="adminLegacy+ '/universal_creation_person/teacher'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard title="a_member_of_staff" text-content="personnel_text_creation_card" icon="fa fa-suitcase" :link="adminLegacy+ '/universal_creation_person/personnel'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard title="a_legal_entity" text-content="moral_text_creation_card" icon="fa fa-building" :link="adminLegacy+ '/universal_creation_person/company'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard title="another_type_of_contact" text-content="other_contact_text_creation_card" icon="fa fa-plus" :link="adminLegacy+ '/universal_creation_person/other_contact'" ></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="isLaw1901"
+                title="an_adherent"
+                text-content="adherent_text_creation_card"
+                icon="fa fa-user"
+                :link="adminLegacy + '/universal_creation_person/adherent'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="isLaw1901"
+                title="a_ca_member"
+                text-content="ca_member_text_creation_card"
+                icon="fa fa-users"
+                :link="adminLegacy + '/universal_creation_person/ca_member'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                title="a_student"
+                text-content="student_text_creation_card"
+                icon="fa fa-user"
+                :link="adminLegacy + '/universal_creation_person/student'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                title="a_guardian"
+                text-content="guardian_text_creation_card"
+                icon="fa fa-female"
+                :link="adminLegacy + '/universal_creation_person/guardian'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                title="a_teacher"
+                text-content="teacher_text_creation_card"
+                icon="fa fa-graduation-cap"
+                :link="adminLegacy + '/universal_creation_person/teacher'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                title="a_member_of_staff"
+                text-content="personnel_text_creation_card"
+                icon="fa fa-suitcase"
+                :link="adminLegacy + '/universal_creation_person/personnel'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                title="a_legal_entity"
+                text-content="moral_text_creation_card"
+                icon="fa fa-building"
+                :link="adminLegacy + '/universal_creation_person/company'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                title="another_type_of_contact"
+                text-content="other_contact_text_creation_card"
+                icon="fa fa-plus"
+                :link="adminLegacy + '/universal_creation_person/other_contact'" />
           </div>
+
           <div v-if="type === 'event'" class="row">
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('display', 'course_page')" title="course" text-content="course_text_creation_card" icon="fa fa-users" :link="adminLegacy+ '/calendar/create/courses'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('display', 'exam_page')" title="exam" text-content="exam_text_creation_card" icon="fa fa-graduation-cap" :link="adminLegacy+ '/calendar/create/examens'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('display', 'pedagogics_project_page')" title="educational_services" text-content="educational_services_text_creation_card" icon="fa fa-suitcase" :link="adminLegacy+ '/calendar/create/educational_projects'" ></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'events')" title="other_event" text-content="other_event_text_creation_card" icon="far fa-calendar" :link="adminLegacy+ '/calendar/create/events'" ></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('display', 'course_page')"
+                title="course"
+                text-content="course_text_creation_card"
+                icon="fa fa-users"
+                :link="adminLegacy + '/calendar/create/courses'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('display', 'exam_page')"
+                title="exam"
+                text-content="exam_text_creation_card"
+                icon="fa fa-graduation-cap"
+                :link="adminLegacy + '/calendar/create/examens'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('display', 'pedagogics_project_page')"
+                title="educational_services"
+                text-content="educational_services_text_creation_card"
+                icon="fa fa-suitcase"
+                :link="adminLegacy + '/calendar/create/educational_projects'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('manage', 'events')"
+                title="other_event"
+                text-content="other_event_text_creation_card"
+                icon="far fa-calendar"
+                :link="adminLegacy + '/calendar/create/events'" />
           </div>
+
           <div v-if="type === 'message'" class="row">
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'emails')" title="an_email" text-content="email_text_creation_card" icon="far fa-envelope" :link="adminLegacy+ '/list/create/emails'"></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'mails')" title="a_letter" text-content="letter_text_creation_card" icon="far fa-file-alt" :link="adminLegacy+ '/list/create/mails'"></LayoutHeaderUniversalCreationTypeCard>
-            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'texto')" title="an_sms" text-content="sms_text_creation_card" icon="fa fa-mobile-alt" :link="adminLegacy+ '/list/create/sms'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('manage', 'emails')"
+                title="an_email"
+                text-content="email_text_creation_card"
+                icon="far fa-envelope"
+                :link="adminLegacy + '/list/create/emails'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('manage', 'mails')"
+                title="a_letter"
+                text-content="letter_text_creation_card"
+                icon="far fa-file-alt"
+                :link="adminLegacy + '/list/create/mails'" />
+
+            <LayoutHeaderUniversalCreationTypeCard
+                v-if="can('manage', 'texto')"
+                title="an_sms"
+                text-content="sms_text_creation_card"
+                icon="fa fa-mobile-alt"
+                :link="adminLegacy + '/list/create/sms'" />
           </div>
         </div>
 
       </v-stepper-content>
-
     </v-stepper-items>
   </v-stepper>
 </template>
 
-<script lang="ts">
-import {defineComponent, ref, Ref, useContext} from '@nuxtjs/composition-api'
-import { $organizationProfile } from '~/services/profile/organizationProfile'
+<script setup lang="ts">
+  import {Ref, ref} from "@vue/reactivity";
+  import {useOrganizationProfileStore} from "~/stores/organizationProfile";
+  import {useAbility} from "@casl/vue";
 
-export default defineComponent({
-  props: {
+  const props = defineProps({
     step: {
       type: Number,
       required: true
     }
-  },
-  setup (_,{emit}) {
-    const { $config, store } = useContext()
+  })
 
-    const onTypeClick = (step:Number,Cardtype:String)=>{
-      type.value = Cardtype;
-      emit('updateStep', {stepChoice: step, typeChoice: Cardtype});
-    }
-    const type:Ref<String> = ref('');
-    const organizationProfile = $organizationProfile(store)
-
-    return {
-      type,
-      onTypeClick,
-      adminLegacy: $config.baseURL_adminLegacy,
-      isLaw1901: organizationProfile.isAssociation()
-    }
+  const emit = defineEmits(['updateStep'])
+
+  const { can } = useAbility()
+
+  const onTypeClick = (step: Number, Cardtype: String) => {
+    type.value = Cardtype;
+    emit('updateStep', { stepChoice: step, typeChoice: Cardtype });
   }
-})
+
+  const type: Ref<String> = ref('');
+  const organizationProfile = useOrganizationProfileStore()
+
+  const runtimeConfig = useRuntimeConfig()
+  const adminLegacy: Ref<string> = ref(runtimeConfig.baseURL_adminLegacy)
+  const isLaw1901: Ref<boolean> = ref(organizationProfile.isAssociation())
 </script>
+
 <style lang="scss" scoped>
   .creation-type-container{
     border: none!important;

+ 19 - 19
components/Layout/Header/UniversalCreation/TypeCard.vue

@@ -9,7 +9,7 @@
       color=""
       :outlined=true
       :href="link"
-      @click="$emit('typeClick',2,type)"
+      @click="$emit('typeClick', 2, type)"
     >
       <div class="row no-gutters" style="height: 100px">
         <div class="flex-grow-0 flex-shrink-0 d-flex justify-center col col-3" style="">
@@ -28,11 +28,8 @@
 
 </template>
 
-<script lang="ts">
-import {defineComponent} from '@nuxtjs/composition-api'
-
-export default defineComponent({
-  props: {
+<script setup lang="ts">
+  const props = defineProps({
     title: {
       type: String,
       required: true
@@ -53,39 +50,42 @@ export default defineComponent({
       type: String,
       required: false
     }
-  }
-})
+  })
 </script>
+
 <style lang="scss" scoped>
-.creation-type-container{
+.creation-type-container {
   border: none!important;
-  .icon{
+
+  .icon {
     i{
       font-size: 50px;
-      color: var(--v-theme-ot-grey, #777777);
+      color: rgb(var(--v-theme-ot-grey, #777777));
     }
   }
-  .infos-container{
+
+  .infos-container {
     padding: 15px 0;
-    h4{
+
+    h4 {
       font-size: 15px;
-      color: var(--v-theme-ot-green, #00AD8E);
+      color: rgb(var(--v-theme-ot-green, #00AD8E));
       font-weight: bold;
       margin-bottom: 6px;
     }
-    p{
+    p {
       font-size: 13px;
       padding: 0;
       margin: 0;
       color: #767676;
     }
   }
-  &>div{
-    &:hover{
+
+  &>div {
+    &:hover {
       cursor: pointer;
-      background: var(--v-theme-ot-light_green, #a9e0d6);
+      background: rgb(var(--v-theme-ot-light_green, #a9e0d6));
     }
   }
-
 }
 </style>

+ 19 - 28
components/Layout/LoadingScreen.vue

@@ -9,35 +9,26 @@
   </v-overlay>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref, Ref } from '@nuxtjs/composition-api'
-
-export default defineComponent({
-  setup () {
-    const loading: Ref<boolean> = ref(false)
-
-    const set = (_num: number) => {
-      loading.value = true
-    }
-    const start = () => {
-      loading.value = true
-    }
-    const finish = () => {
-      loading.value = false
-    }
-    const fail = () => {
-      loading.value = false
-    }
-
-    return {
-      loading,
-      start,
-      finish,
-      fail,
-      set
-    }
+<script setup lang="ts">
+  import {Ref, ref} from "@vue/reactivity";
+
+  const loading: Ref<boolean> = ref(false)
+
+  const set = (_num: number) => {
+    loading.value = true
+  }
+
+  const start = () => {
+    loading.value = true
+  }
+
+  const finish = () => {
+    loading.value = false
+  }
+
+  const fail = () => {
+    loading.value = false
   }
-})
 </script>
 
 <style scoped>

+ 43 - 19
components/Layout/SubHeader/ActivityYear.vue

@@ -2,16 +2,19 @@
   <main class="d-flex">
     <span class="mr-2 ot-dark_grey--text font-weight-bold">{{ $t(label) }} : </span>
     <UiXeditableText
-      class="activity-year-input"
+      class="activity-year-input bg-ot-light-grey"
       type="number"
-      :data="activityYear"
-      @update="updateActivityYear"
+      :data="currentActivityYear"
+      @update="setActivityYear"
     >
       <template #xeditable.read="{inputValue}">
-        <v-icon aria-hidden="false" class="ot-green--text" x-small>
-          fas fa-edit
-        </v-icon>
-        <strong class="ot-green--text"> {{ inputValue }} <span v-if="yearPlusOne">/ {{ parseInt(inputValue) + 1 }}</span></strong>
+        <v-icon aria-hidden="false" size="x-small" class="text-ot-green mr-1" icon="fas fa-edit" />
+        <strong class="text-ot-green">
+          {{ inputValue }}
+          <span v-if="yearPlusOne">
+            / {{ parseInt(inputValue) + 1 }}
+          </span>
+        </strong>
       </template>
     </UiXeditableText>
   </main>
@@ -23,37 +26,58 @@ import {useEntityManager} from "~/composables/data/useEntityManager";
 import {useFormStore} from "~/stores/form";
 import {useOrganizationProfileStore} from "~/stores/organizationProfile";
 import {useAccessProfileStore} from "~/stores/accessProfile";
+import {Access} from "~/models/Access/Access";
+import {Ref, ref} from "@vue/reactivity";
 
 const { em } = useEntityManager()
-const { activityYear } = useAccessProfileStore()
-const { isManagerProduct, isSchool, isArtist } = useOrganizationProfileStore()
-const { setDirty } = useFormStore()
+const accessProfileStore = useAccessProfileStore()
+const organizationProfileStore = useOrganizationProfileStore()
+const formStore = useFormStore()
 
+const currentActivityYear: Ref<number | null> = ref(accessProfileStore.activityYear)
+const yearPlusOne: boolean = !organizationProfileStore.isManagerProduct
+const label: string = organizationProfileStore.isSchool ? 'schooling_year' : organizationProfileStore.isArtist ? 'season_year' : 'cotisation_year'
+
+/**
+ * Persist a new activityYear
+ * @param activityYear
+ */
 const setActivityYear = async (activityYear: number) => {
   if (!(1900 < activityYear) || !(activityYear <= 2100)) {
     throw new Error("Error: 'year' shall be a valid year")
   }
+  if (accessProfileStore.id === null) {
+    throw new Error("Error: invalide access id")
+  }
+  formStore.setDirty(false)
 
-  await em.updateProfile({'activityYear': activityYear})
-}
+  const access = await em.fetch(Access, accessProfileStore.id)
+  access.activityYear = activityYear
+  await em.persist(Access, access)
 
-const yearPlusOne: boolean = !isManagerProduct
-const label: string = isSchool ? 'schooling_year' : isArtist ? 'season_year' : 'cotisation_year'
+  accessProfileStore.$patch({ activityYear: activityYear }) // TODO: est-ce nécessaire, sachant que l'EM met déjà à jour le profil?
 
-const updateActivityYear = async (newDate: number) => {
-  setDirty(false)
-  await setActivityYear(newDate)
-  window.location.reload()
+  window.location.reload() // TODO: est-ce vraiment nécessaire?
 }
 
 </script>
 
 <style lang="scss">
-  .activity-year-input{
+  .activity-year-input {
+    width: 120px;
     max-height: 20px;
+
+    .v-input {
+      min-width: 70px;
+    }
+
     input{
       font-size: 14px;
       width: 55px !important;
+      padding: 0 !important;
+      margin-top: 0 !important;
+      min-height: 24px;
+      height: 24px;
     }
   }
 </style>

+ 0 - 1
components/Layout/SubHeader/Breadcrumbs.vue

@@ -41,7 +41,6 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
     }
   })
 
-  console.log(crumbs)
   return crumbs
 })
 </script>

+ 18 - 26
components/Layout/SubHeader/DataTiming.vue

@@ -25,35 +25,27 @@
 </template>
 
 <script lang="ts">
-import {defineComponent, onUnmounted, ref, watch, Ref, WatchStopHandle, useContext} from '@nuxtjs/composition-api'
-import {useForm} from "~/composables/form/useForm";
-import { useMyProfile } from '~/composables/data/useMyProfile'
+import {useFormStore} from "~/stores/form";
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import {Ref} from "@vue/reactivity";
+import {WatchStopHandle} from "@vue/runtime-core";
 
-export default defineComponent({
-  setup () {
-    const { store, $dataPersister } = useContext()
-    const { markAsNotDirty } = useForm(store)
-    const { updateMyProfile, setHistorical, historical } = useMyProfile($dataPersister, store)
+const { setDirty } = useFormStore()
+const accessProfileStore = useAccessProfileStore()
 
-    const historicalBtn: Ref<Array<number>> = initHistoricalBtn(historical.value as Array<any>)
+const historicalBtn: Ref<Array<number>> = initHistoricalBtn(accessProfileStore.historical as Array<any>)
 
-    const unwatch: WatchStopHandle = watch(historicalBtn, async (newValue) => {
-      const historicalChoice: Array<string> = initHistoricalChoice(newValue)
+const unwatch: WatchStopHandle = watch(historicalBtn, async (newValue) => {
+  const historicalChoice: Array<string> = initHistoricalChoice(newValue)
 
-      setHistorical(historicalChoice)
-      markAsNotDirty()
-      await updateMyProfile()
-      window.location.reload()
-    })
-
-    onUnmounted(() => {
-      unwatch()
-    })
+  accessProfileStore.setHistorical(historicalChoice)
+  setDirty(false)
+  await updateMyProfile()
+  window.location.reload()
+})
 
-    return {
-      historicalBtn
-    }
-  }
+onUnmounted(() => {
+  unwatch()
 })
 
 /**
@@ -74,7 +66,7 @@ function initHistoricalBtn (historical: Array<any>) {
    * Transforme le résultat renvoyé par le component v-btn-toggle pour l'enregistrer coté AccessProfile
    * @param historical
    */
-function initHistoricalChoice (historical:Array<any>) {
+function initHistoricalChoice (historical: Array<any>) {
   const historicalArray:Array<any> = ['past', 'present', 'future']
 
   const historicalChoice:Array<string> = []
@@ -88,6 +80,6 @@ function initHistoricalChoice (historical:Array<any>) {
 <style scoped lang="scss">
   .toggle-btn{
     z-index: 1;
-    border-radius: 4px 0px 0px 4px;
+    border-radius: 4px 0 0 4px;
   }
 </style>

+ 46 - 47
components/Layout/SubHeader/DataTimingRange.vue

@@ -35,72 +35,71 @@
   </main>
 </template>
 
-<script lang="ts">
-import {
-  defineComponent, onUnmounted, ref, watch, computed, ComputedRef, Ref, WatchStopHandle, useContext
-} from '@nuxtjs/composition-api'
-import { useMyProfile } from '~/composables/data/useMyProfile'
-import {useForm} from "~/composables/form/useForm";
-
-export default defineComponent({
-  setup (_, { emit }) {
-    const { store, $dataPersister } = useContext()
-    const { markAsNotDirty } = useForm(store)
-    const { updateMyProfile, setHistoricalRange, historical } = useMyProfile($dataPersister, store)
-
-    const datesRange:ComputedRef<Array<any>> = computed(() => {
-      return [historical.value.dateStart, historical.value.dateEnd]
-    })
-
-    const show:Ref<boolean> = ref(false)
-    if (historical.value.dateStart || historical.value.dateEnd) {
-      show.value = true
-      emit('showDateTimeRange', true)
-    }
+<script setup lang="ts">
+import {ComputedRef, Ref} from "@vue/reactivity";
+    import {useAccessProfileStore} from "~/stores/accessProfile";
+    import {useFormStore} from "~/stores/form";
+import {WatchStopHandle} from "@vue/runtime-core";
 
-    const unwatch:WatchStopHandle = watch(show, (newValue) => {
-      emit('showDateTimeRange', newValue)
-    })
+const emit = defineEmits(['showDateTimeRange'])
 
-    onUnmounted(() => {
-      unwatch()
-    })
+const { setDirty } = useFormStore()
 
-    const updateDateTimeRange = async (dates:Array<string>): Promise<any> => {
-      setHistoricalRange(dates)
-      markAsNotDirty()
-      await updateMyProfile()
-      window.location.reload()
-    }
+const accessProfileStore = useAccessProfileStore()
 
-    return {
-      show,
-      datesRange,
-      updateDateTimeRange
-    }
-  }
+const { updateMyProfile, setHistoricalRange, historical } = useAccessProfileStore()
+
+const datesRange: ComputedRef<Array<any>> = computed(() => {
+  return [accessProfileStore.historical.dateStart, accessProfileStore.historical.dateEnd]
+})
+
+const show: Ref<boolean> = ref(false)
+
+if (accessProfileStore.historical.dateStart || accessProfileStore.historical.dateEnd) {
+  show.value = true
+  emit('showDateTimeRange', true)
+}
+
+const unwatch: WatchStopHandle = watch(show, (newValue) => {
+  emit('showDateTimeRange', newValue)
 })
+
+onUnmounted(() => {
+  unwatch()
+})
+
+const updateDateTimeRange = async (dates:Array<string>): Promise<any> => {
+  setHistoricalRange(dates)
+  setDirty(false)
+  await updateMyProfile()
+  window.location.reload()
+}
 </script>
 
 <style lang="scss">
-  .v-btn--active .v-icon{
+  .v-btn--active .v-icon {
     color: #FFF !important;
   }
-  .time-btn{
-    border-width: 1px 1px 1px 0px;
+
+  .time-btn {
+    border-width: 1px 1px 1px 0;
     border-style: solid;
     border-color: rgba(0, 0, 0, 0.12) !important;
   }
-  .time-range{
+
+  .time-range {
     max-height: 20px;
-    .v-text-field{
+
+    .v-text-field {
       padding-top: 0 !important;
       margin-top: 0 !important;
     }
-    .v-icon{
+
+    .v-icon {
       font-size: 20px;
     }
-    input{
+
+    input {
       font-size: 14px;
       width: 400px !important;
     }

+ 36 - 53
components/Layout/SubHeader/PersonnalizedList.vue

@@ -50,68 +50,51 @@
   </main>
 </template>
 
-<script lang="ts">
-import {
-  computed, defineComponent, useContext, useFetch, ref, Ref, ComputedRef
-} from '@nuxtjs/composition-api'
-import { Collection } from '@vuex-orm/core'
-import { QUERY_TYPE } from '~/types/enum/enums'
-import { PersonalizedList } from '~/models/Access/PersonalizedList'
-import { repositoryHelper } from '~/services/store/repository'
-import { AnyJson } from '~/types/interfaces'
-import {queryHelper} from "~/services/store/query";
+<script setup lang="ts">
+import {PersonalizedList} from '~/models/Access/PersonalizedList'
+import {useEntityFetch} from "~/composables/data/useEntityFetch";
+import {ComputedRef, ref} from "@vue/reactivity";
+import {AnyJson} from "~/types/data";
+import ApiResource from "~/models/ApiResource";
 
-export default defineComponent({
-  fetchOnServer: false,
-  setup () {
-    const { $dataProvider, $config, app:{i18n} } = useContext()
-    const homeUrl:string = $config.baseURL_adminLegacy
+// fetchOnServer: false,
 
-    const {fetchState} = useFetch(async () => {
-      await $dataProvider.invoke({
-        type: QUERY_TYPE.MODEL,
-        model: PersonalizedList
-      })
-    })
+const { fetch, fetchCollection } = useEntityFetch()
 
-    const items:ComputedRef<Array<AnyJson>> = computed(() => {
-      const query = repositoryHelper.getRepository(PersonalizedList).query()
-      const lists = queryHelper.getCollection(query, {'label':'asc'}) as Collection<PersonalizedList>
-      lists.map(item => {
-        const menu: string = i18n.t(item.menuKey) as string
-        item.menuKey = menu
-      })
-      return lists
-    })
+const { data: collection, pending } = await fetchCollection(PersonalizedList)
 
-    const search = ref('');
-    const filteredItems = computed(() => {
-      return items.value.filter( item => {
-          return item.label.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
-            ||item.menuKey.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
-        }
-      )
-    })
+const i18n = useI18n()
 
-    const goOn = (list:PersonalizedList) => {
-      return `${homeUrl}/${list.entity}/list/${list.id}`
-    }
+const items: ComputedRef<Array<AnyJson>> = computed(() => {
+  const lists: Array<ApiResource> = collection.value !== null ? collection.value.items : []
+
+  lists.map(item => {
+    item.menuKey = i18n.t(item.menuKey) as string
+  })
+  return lists
+})
+
+const search = ref('');
 
-    return {
-      items,
-      fetchState,
-      goOn,
-      filteredItems,
-      search,
-      homeUrl
+const filteredItems = computed(() => {
+  return items.value.filter( item => {
+      return item.label.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
+             ||item.menuKey.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
     }
-  }
+  )
 })
+
+const runtimeConfig = useRuntimeConfig()
+const homeUrl: string = runtimeConfig.baseURL_adminLegacy
+
+const goOn = (list: PersonalizedList) => {
+  return `${homeUrl}/${list.entity}/list/${list.id}`
+}
 </script>
 
-<style type="text/css" lang="scss">
-  .header-personnalized{
-    margin-bottom: 0px;
-    padding-bottom: 0px;
+<style lang="scss">
+  .header-personnalized {
+    margin-bottom: 0;
+    padding-bottom: 0;
   }
 </style>

+ 4 - 2
components/Layout/Subheader.vue

@@ -12,7 +12,7 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
       <LayoutSubHeaderBreadcrumbs class="mr-auto d-sm-none d-md-flex d-none d-sm-flex" />
 
       <v-card
-        class="d-md-flex ot-light_grey pt-2 mr-6  align-baseline"
+        class="d-md-flex ot-light_grey pt-2 mr-6 align-baseline"
         flat
         tile
       >
@@ -46,5 +46,7 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 </script>
 
 <style scoped>
-
+.v-card {
+  max-height: 33px;
+}
 </style>

+ 0 - 2
components/Ui/Collection.vue

@@ -55,6 +55,4 @@ const { data: collection, pending } = await fetchCollection(model.value, parent.
 
 const items: ComputedRef<Collection> = computed(() => collection.value ?? { items: [], pagination: {}, totalItems: 0 })
 
-console.log('%%%', items.value)
-
 </script>

+ 9 - 5
components/Ui/Input/Text.vue

@@ -6,9 +6,8 @@ Champs de saisie de texte
 
 <template>
   <v-text-field
-    :value="modelValue"
-    autocomplete="off"
-    :label="$t(label ?? field)"
+    :model-value="modelValue"
+    :label="(label || field) ? $t(label ?? field) : undefined"
     :rules="rules"
     :disabled="readonly"
     :type="(type === 'password' && show) ? 'text' : type"
@@ -16,10 +15,11 @@ Champs de saisie de texte
     :error-messages="errorMessage || (fieldViolations ? $t(fieldViolations) : '')"
     :append-icon="type === 'password' ? (show ? 'mdi-eye' : 'mdi-eye-off') : ''"
     @click:append="show = !show"
-    @input="$emit('update:modelValue', $event.target.value)"
+    @update:modelValue="$emit('update:modelValue', $event.target.value)"
     @change="updateViolationState($event); $emit('change', $event)"
   />
 
+
 <!--  v-cleave="mask"-->
 </template>
 
@@ -35,7 +35,7 @@ const props = defineProps({
    * v-model
    */
   modelValue: {
-    String,
+    type: [String, Number],
     required: false,
     default: null
   },
@@ -117,6 +117,10 @@ const i18n = useI18n()
 const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
 const show = ref(false)
+// const label = computed(() => {
+//   if (props.label)
+// })
+
 </script>
 
 <style scoped>

+ 32 - 19
components/Ui/Xeditable/Text.vue

@@ -3,20 +3,30 @@
 
 <template>
   <main>
-    <div v-if="edit" class="d-flex align-baseline x-editable-input mt-n1">
+    <div v-if="edit" class="d-flex align-center x-editable-input">
       <UiInputText
-          class="mt-0 pt-0 mt-n1"
+          class="ma-0 pa-0"
           :type="type"
-          :data="inputValue"
-          @update="inputValue=$event"
+          :modelValue="inputValue"
+          @update="$emit('update:modelValue', $event.target.value)"
+      />
+
+      <v-icon
+          icon="fas fa-check"
+          aria-hidden="false"
+          class="valid icons text-ot-green"
+          size="small"
+          @click="update"
+      />
+      <v-icon
+          icon="fas fa-times"
+          aria-hidden="false"
+          class="cancel icons text-ot-grey"
+          size="small"
+          @click="close"
       />
-      <v-icon aria-hidden="false" class="valid icons text-ot-green" small @click="update">
-        fas fa-check
-      </v-icon>
-      <v-icon aria-hidden="false" class="cancel icons text-ot-grey" small @click="close">
-        fas fa-times
-      </v-icon>
     </div>
+
     <div v-else class="edit-link" @click="edit=true">
       <slot name="xeditable.read" v-bind="{inputValue}" />
     </div>
@@ -56,16 +66,19 @@ import {ref, Ref} from "@vue/reactivity";
 
 </script>
 
-<style lang="scss">
-.x-editable-input{
-  input{
-    padding: 0 !important;
-  }
-  .icons{
-    padding: 5px;
-  }
+<style scoped lang="scss">
+.v-input {
+  height: 24px;
+}
+
+.v-icon {
+  padding: 2px;
+  height: 24px;
+  width: 24px;
+  margin: 0 2px;
 }
-.edit-link{
+
+.edit-link {
   cursor: pointer;
 }
 </style>

+ 12 - 0
composables/utils/useAbilityUtils.ts

@@ -0,0 +1,12 @@
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import {useOrganizationProfileStore} from "~/stores/organizationProfile";
+import {useAbility} from "@casl/vue";
+import AbilityUtils from "~/services/rights/abilityUtils";
+
+export const useAbilityUtils = () => {
+    const ability = useAbility()
+    const accessProfile = useAccessProfileStore()
+    const organizationProfile = useOrganizationProfileStore()
+
+    return new AbilityUtils(ability, accessProfile, organizationProfile)
+}

+ 1 - 1
layouts/default.vue

@@ -10,7 +10,7 @@
 
       <v-main class="ot-content-color">
 
-      <LayoutSubheader />
+        <LayoutSubheader />
 
 <!--        <LayoutAlertbar class="mt-1"></LayoutAlertbar>-->
 

+ 5 - 5
package.json

@@ -54,7 +54,7 @@
     "@fortawesome/fontawesome-free": "^6.2.1",
     "@mdi/font": "^7.0.96",
     "@nuxt/image": "^0.7.1",
-    "@nuxtjs/i18n": "^8.0.0-beta.4",
+    "@nuxtjs/i18n": "^8.0.0-beta.7",
     "@pinia-orm/nuxt": "^1.1.4",
     "@pinia/nuxt": "^0.4.3",
     "@types/js-yaml": "^4.0.5",
@@ -65,14 +65,14 @@
     "js-yaml": "^4.1.0",
     "libphonenumber-js": "^1.10.14",
     "nuxt": "^3.0.0",
-    "nuxt-lodash": "^2.4.0",
-    "pinia": "^2.0.23",
-    "pinia-orm": "^1.1.4",
+    "nuxt-lodash": "^2.4.1",
+    "pinia": "^2.0.28",
+    "pinia-orm": "^1.3.0",
     "sass": "^1.56.1",
     "uuid": "^9.0.0",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
-    "vuetify": "^3.0.1",
+    "vuetify": "^3.0.4",
     "yaml-import": "^2.0.0"
   }
 }

+ 0 - 1
pages/poc/index.vue

@@ -43,7 +43,6 @@
       'files',
       () => em.fetchCollection(File, null, { page: page.value })
   )
-  console.log(collection.value)
 
   const totalItems: ComputedRef<number | undefined> = computed(() => collection.value?.totalItems)
 

+ 0 - 1
pages/poc/new.vue

@@ -29,7 +29,6 @@ const em = useEntityManager()
 let file: ApiResource = reactive(await em.newInstance(File))
 
 const save = async () => {
-  console.log('save')
   //@ts-ignore
   await em.persist(File, file)
   navigateTo('/poc')

+ 11 - 2
plugins/ability.ts

@@ -1,7 +1,16 @@
 import { createMongoAbility } from '@casl/ability'
-import { $abilitiesUtils } from '~/services/rights/abilitiesUtils'
+import AbilityUtils from '~/services/rights/abilityUtils'
+import {defineNuxtPlugin} from "nuxt/app";
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import {useOrganizationProfileStore} from "~/stores/organizationProfile";
 
 export const ability = createMongoAbility()
+
 export default defineNuxtPlugin(() => {
-    $abilitiesUtils(ability).setAbilities()
+    const accessProfile = useAccessProfileStore()
+    const organizationProfile = useOrganizationProfileStore()
+
+    const abilityUtils = new AbilityUtils(ability, accessProfile, organizationProfile)
+
+    abilityUtils.defineAbilities()
 })

+ 0 - 24
services/data/entityManager.ts

@@ -265,30 +265,6 @@ class EntityManager {
         accessProfileStore.setProfile(profile)
     }
 
-    /**
-     * Update and persist the user profile
-     * @param data
-     */
-    public async updateProfile(data: object) {
-        const myProfile = await this.fetch(MyProfile, 1)
-
-        for (const prop in data) {
-            if (!Object.prototype.hasOwnProperty.call(myProfile, prop)) {
-                throw new Error('Error : MyProfile has no property named `' + prop + '`')
-            }
-
-            // @ts-ignore
-            myProfile[prop] = data[prop]
-        }
-
-        const newProfile = await this.persist(MyProfile, myProfile)
-
-        // On met à jour le store accessProfile
-        // TODO: sortir le use du service, ça devrait être dans un composable
-        const accessProfileStore = useAccessProfileStore()
-        accessProfileStore.setProfile(newProfile)
-    }
-
     /**
      * Delete all records in the repository of the model
      *

+ 0 - 158
services/rights/abilitiesUtils.ts

@@ -1,158 +0,0 @@
-import {$roleUtils} from '~/services/rights/roleUtils'
-import {AbilitiesType} from '~/types/interfaces'
-import YamlDenormalizer from "~/services/data/serializer/denormalizer/yamlDenormalizer";
-import {MongoAbility} from "@casl/ability/dist/types/Ability";
-import {AnyJson} from "~/types/data";
-import {useAccessProfileStore} from "~/stores/accessProfile";
-import {useOrganizationProfileStore} from "~/stores/organizationProfile";
-
-/**
- * Classe permettant de mener des opérations sur les habilités
- */
-class AbilitiesUtils {
-    private readonly $ability: MongoAbility = {} as MongoAbility
-
-    /**
-     * @constructor
-     */
-    constructor(ability: MongoAbility) {
-        this.$ability = ability
-    }
-
-    /**
-     * Définit les abilities de l'utilisateur à chaque fois qu'on met à jour son profile
-     */
-    setAbilities() {
-        const accessProfileStore = useAccessProfileStore()
-        const organizationProfileStore = useOrganizationProfileStore()
-
-        // Nécessaire pour que l'update des habilités soit correcte après la phase SSR
-        this.$ability.update(accessProfileStore.abilities)
-
-        // Au moment où l'on effectue un SetProfile, il faut aller récupérer
-        // les différentes habilités que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
-        const unsubscribe = organizationProfileStore.$onAction(({
-                name, // name of the action
-                store, // store instance, same as `someStore`
-                args, // array of parameters passed to the action
-                after, // hook after the action returns or resolves
-                onError, // hook if the action throws or rejects
-            }) => {
-            after((result)=>{
-                if(name === 'setProfile'){
-                    //On récupère les abilités
-                    const abilities: Array<AbilitiesType> = this.getAbilities();
-
-                    //On les store puis on update le service ability pour le mettre à jour.
-                    accessProfileStore.abilities = abilities
-                    this.$ability.update(abilities)
-                    // Unsubscribe pour éviter les memory leaks
-                    unsubscribe()
-                }
-            })
-        })
-    }
-
-    /**
-     * Récupération de l'ensemble des abilities, qu'elles soient par Roles ou par Config
-     *
-     * @return {Array<AbilitiesType>}
-     */
-    getAbilities(): Array<AbilitiesType> {
-        const accessProfileStore = useAccessProfileStore()
-        const abilitiesByRoles: Array<AbilitiesType> = this.getAbilitiesByRoles(accessProfileStore.roles)
-        this.$ability.update(abilitiesByRoles)
-        return abilitiesByRoles.concat(this.getAbilitiesByConfig('./config/abilities/config.yaml'))
-    }
-
-    /**
-     * Adaptation et transformations des roles en abilities
-     *
-     * @param {Array<string>} roles
-     * @return {Array<AbilitiesType>}
-     */
-    getAbilitiesByRoles(roles: Array<string>): Array<AbilitiesType> {
-        roles = $roleUtils.transformUnderscoreToHyphenBeforeCompleteMigration(roles)
-        return $roleUtils.transformRoleToAbilities(roles)
-    }
-
-    /**
-     * - Parcourt la config d'abilities en Yaml
-     * - filtres la config pour ne garder que les abilities autorisées
-     * - transform la config restante en Object Abilities
-     * @param {string} configPath
-     * @return {Array<AbilitiesType>}
-     */
-    getAbilitiesByConfig(configPath: string): Array<AbilitiesType> {
-        let abilitiesByConfig: Array<AbilitiesType> = []
-        try {
-            const doc = YamlDenormalizer.denormalize({path: configPath})
-            const abilitiesAvailable = doc.abilities
-            const abilitiesFiltered = this.abilitiesAvailableFilter(abilitiesAvailable)
-            abilitiesByConfig = this.transformAbilitiesConfigToAbility(abilitiesFiltered)
-        } catch (e: any) {
-            throw new Error(e.message)
-        }
-        return abilitiesByConfig
-    }
-
-    /**
-     * Filtre toutes les abilities possible suivant si l'utilisateur est autorisé ou non à les posséder
-     *
-     * @param {AnyJson} abilitiesAvailable
-     * @return {AnyJson}
-     */
-    abilitiesAvailableFilter(abilitiesAvailable: AnyJson): AnyJson {
-        return usePickBy(abilitiesAvailable, (ability: any) => {
-            const services = ability.services
-            return this.canHaveTheAbility(services)
-        })
-    }
-
-    /**
-     * Transform une config d'abilities en un tableau d'Abilities
-     *
-     * @param {AnyJson} abilitiesAvailable
-     * @return {Array<AbilitiesType>}
-     */
-    transformAbilitiesConfigToAbility(abilitiesAvailable: AnyJson): Array<AbilitiesType> {
-        const abilitiesByConfig: Array<AbilitiesType> = []
-        useEach(abilitiesAvailable, (ability, subject) => {
-            const myAbility: AbilitiesType = {
-                action: ability.action,
-                subject
-            }
-            abilitiesByConfig.push(myAbility)
-        })
-        return abilitiesByConfig
-    }
-
-    /**
-     * Parcourt les fonctions par services et établit si oui ou non l'habilité est autorisée
-     *
-     * @return {boolean}
-     * @param functionByservices
-     */
-    canHaveTheAbility(functionByservices: AnyJson) {
-        let hasAbility: boolean = true;
-        useEach(functionByservices, (functions, service) => {
-            if (hasAbility) {
-                const nbFunctions: number = functions.length
-                let cmpt: number = 0
-
-                while (hasAbility && nbFunctions > cmpt) {
-                    const f: string = functions[cmpt]['function'];
-                    const parameters: any = functions[cmpt]['parameters'] ?? null;
-                    const result: boolean = functions[cmpt]['result'] ?? true;
-
-                    // TODO : à revoir
-                    hasAbility = result !== null ? this.factory[service].handler()[f](parameters) == result : this.factory[service].handler()[f](parameters)
-                    cmpt++
-                }
-            }
-        })
-        return hasAbility
-    }
-}
-
-export const $abilitiesUtils = (ability: MongoAbility) => new AbilitiesUtils(ability)

+ 244 - 0
services/rights/abilityUtils.ts

@@ -0,0 +1,244 @@
+import RoleUtils from '~/services/rights/roleUtils'
+import {AbilitiesType} from '~/types/interfaces'
+import YamlDenormalizer from "~/services/data/serializer/denormalizer/yamlDenormalizer";
+import {MongoAbility} from "@casl/ability/dist/types/Ability";
+import {AnyJson} from "~/types/data";
+import {useEach} from "#imports";
+import {ABILITIES} from "~/types/enum/enums";
+
+/**
+ * Classe permettant de mener des opérations sur les habilités
+ */
+class AbilityUtils {
+    private readonly ability: MongoAbility = {} as MongoAbility
+    private readonly accessProfile: any
+    private readonly organizationProfile: any
+
+    /**
+     * @constructor
+     */
+    constructor(
+        ability: MongoAbility,
+        accessProfile: any,
+        organizationProfile: any,
+    ) {
+        this.ability = ability
+        this.accessProfile = accessProfile
+        this.organizationProfile = organizationProfile
+    }
+
+    /**
+     * Définit les abilities de l'utilisateur selon son profil
+     */
+    defineAbilities() {
+        // Nécessaire pour que l'update des habilités soit correcte après la phase SSR
+        this.ability.update(this.accessProfile.abilities)
+
+        // Au moment où l'on effectue une action organizationProfileStore.setProfile, il faut aller récupérer
+        // les différentes habilités que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
+        const unsubscribe = this.organizationProfile.$onAction(({
+                                                                    name, // name of the action
+                                                                    store, // store instance, same as `someStore`
+                                                                    args, // array of parameters passed to the action
+                                                                    after, // hook after the action returns or resolves
+                                                                    onError, // hook if the action throws or rejects
+                                                                }: any) => {
+            after((result: any)=>{
+                if(name === 'setProfile'){
+                    //On récupère les habilités
+                    const abilities: Array<AbilitiesType> = this.buildAbilities();
+
+                    //On les store puis on update le service ability pour le mettre à jour.
+                    this.accessProfile.abilities = abilities
+                    this.ability.update(abilities)
+
+                    // Unsubscribe pour éviter les memory leaks
+                    unsubscribe()
+                }
+            })
+        })
+    }
+
+    /**
+     * Récupération de l'ensemble des habilités de l'utilisateur, qu'elles soient par Roles ou par Config
+     *
+     * @return {Array<AbilitiesType>}
+     */
+    buildAbilities(): Array<AbilitiesType> {
+
+        const abilitiesByRoles: Array<AbilitiesType> = this.buildAbilitiesFromRoles(this.accessProfile.roles)
+        this.ability.update(abilitiesByRoles)
+
+        const abilitiesByConfig = this.buildAbilitiesFromConfig('./config/abilities/config.yaml')
+
+        return abilitiesByRoles.concat(abilitiesByConfig)
+    }
+
+    /**
+     * Adaptation et transformations des roles symfony en abilities Casl
+     *
+     * @param {Array<string>} roles
+     * @return {Array<AbilitiesType>}
+     */
+    buildAbilitiesFromRoles(roles: Array<string>): Array<AbilitiesType> {
+        return RoleUtils.rolesToAbilities(roles)
+    }
+
+    /**
+     * Charge les habilités depuis les fichiers de configuration
+     *
+     * @param {string} configPath
+     * @return {Array<AbilitiesType>}
+     */
+    buildAbilitiesFromConfig(configPath: string): Array<AbilitiesType> {
+        const doc = YamlDenormalizer.denormalize({path: configPath})
+        const fromConfig = doc.abilities
+
+        const abilities: Array<AbilitiesType> = []
+
+        useEach(fromConfig, (ability: { action: ABILITIES, services: object }, subject: string) => {
+            const { action, services } = ability
+            if (this.hasConfigAbility(services)) {
+                abilities.push({ action, subject })
+            }
+        })
+
+        return abilities
+    }
+
+    /**
+     * Parcourt les services définis dans la configuration, et établit si oui ou non l'habilité est autorisée
+     *
+     * @return {boolean}
+     * @param services
+     */
+    hasConfigAbility(services: AnyJson) {
+        const handlerMap: any = {
+            hasRole: (parameters: any) => this.hasRoles(parameters),
+            hasAbility: (parameters: any) => this.hasAbilities(parameters),
+            hasProfile: (parameters: any) => this.hasProfileAmong(parameters),
+            isAdminAccount: (parameters: any) => this.accessProfile.isAdminAccount,
+            hasModule: (parameters: any) => this.hasModule(parameters),
+            isSchool: (parameters: any) => this.organizationProfile.isSchool,
+            isArtist: (parameters: any) => this.organizationProfile.isArtist,
+            isManagerProduct: (parameters: any) => this.organizationProfile.isManagerProduct,
+            isOrganizationWithChildren: (parameters: any) => this.organizationProfile.hasChildren,
+            isAssociation: (parameters: any) => this.organizationProfile.isAssociation,
+            isShowAdherentList: (parameters: any) => this.organizationProfile.isShowAdherentList,
+            isCmf: (parameters: any) => this.organizationProfile.isCmf,
+            getWebsite: (parameters: any) => this.organizationProfile.getWebsite,
+
+        }
+
+        useEach(services, (handlers: Array<{ function: string, parameters?: Array<any>, result?: any }>, service: string) => {
+
+            useEach(handlers, (handler: { function: string, parameters?: Array<any>, result?: any }) => {
+                const expectedResult: boolean = handler.result ?? true;
+                const parametersArray = handler.parameters ?? []
+
+                useEach(parametersArray, (parameters: any) => {
+                    const actualResult = handlerMap[handler.function](parameters ?? null)
+
+                    if (actualResult !== expectedResult) {
+                        return false
+                    }
+                })
+            })
+        })
+        return true
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède la ou les habilités
+     *
+     * @param {Array<AbilitiesType>} abilities Habilités à tester
+     * @return {boolean}
+     */
+    hasAbilities(abilities: Array<AbilitiesType>|null): boolean{
+        useEach(abilities ?? [], (ability) => {
+            if (!this.ability.can(ability.action, ability.subject)) {
+                return false
+            }
+        })
+        return true
+    }
+
+
+    /**
+     * Teste le profil d'un utilisateur
+     *
+     * @param {string} profile : profile à tester
+     * @return {boolean}
+     */
+    private testProfile(profile: string): boolean {
+        const factory: {[key: string]: boolean|null} = {
+            'admin': this.accessProfile.isAdmin,
+            'administratifManager': this.accessProfile.isAdministratifManager,
+            'pedagogicManager': this.accessProfile.isPedagogicManager,
+            'financialManager': this.accessProfile.isFinancialManager,
+            'caMember': this.accessProfile.isCaMember,
+            'student': this.accessProfile.isStudent,
+            'teacher': this.accessProfile.isTeacher,
+            'member': this.accessProfile.isMember,
+            'other': this.accessProfile.isOther,
+            'guardian': this.accessProfile.isGuardian,
+            'payor': this.accessProfile.isPayer,
+        }
+        return factory[profile] ?? false
+    }
+
+    /**
+     * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre
+     *
+     * @param {Array<string>} profiles Profils à tester
+     * @return {boolean}
+     */
+    hasProfileAmong (profiles: Array<string>|null): boolean {
+        if (null === profiles)
+            return true;
+
+        useEach(profiles, (profile) => {
+            if (this.testProfile(profile)) {
+                return true
+            }
+        })
+        return false
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède le rôle donné ?
+     *
+     * @return {boolean}
+     * @param role
+     */
+    hasRole(role: string|null): boolean {
+        return role === null || this.accessProfile.roles.includes(role)
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède tous les rôles donnés ?
+     *
+     * @return {boolean}
+     * @param roles
+     */
+    hasRoles(roles: Array<string>): boolean {
+        useEach(roles, (r: string) =>  {
+            if (!this.accessProfile.roles.includes(r)) {
+                return false
+            }
+        })
+        return true
+    }
+
+    /**
+     * Est-ce que l'organisation possède le module donné
+     *
+     * @return {boolean}
+     * @param module
+     */
+    hasModule(module: string): boolean {
+        return this.organizationProfile.modules.includes(module)
+    }
+}
+
+export default AbilityUtils

+ 18 - 6
services/rights/roleUtils.ts

@@ -1,5 +1,6 @@
 import { AbilitiesType } from '~/types/interfaces'
 import {AnyJson} from "~/types/data";
+import {useEach} from "#imports";
 
 // TODO: peut-être passer ces constantes dans la config?
 const rolesByFunction: Array<string> = [
@@ -57,7 +58,7 @@ class RoleUtils {
    * @param {Array<string>} roles
    * @return {boolean}
    */
-  isA (profileName: string, roles: Array<string>): boolean {
+  static isA (profileName: string, roles: Array<string>): boolean {
     profileName = profileName.toUpperCase()
     if (!profileName.match(/[A-Z_]+/)) {
       throw new Error('invalid role name')
@@ -71,7 +72,7 @@ class RoleUtils {
    * @param {Array<string>} roles
    * @return {Array<string>}
    */
-  filterFunctionRoles (roles: Array<string>): Array<string> {
+  static filterFunctionRoles (roles: Array<string>): Array<string> {
     return roles.filter((role) => {
       return !rolesByFunction.includes(role)
     })
@@ -80,10 +81,12 @@ class RoleUtils {
   /**
    * Fix en attendant la migration complète, quelques rôles disposent d'underscore en trop, on corrige cela...
    *
+   * TODO: remove after complete migration
+   *
    * @param {Array<string>} roles
    * @return {Array<string>}
    */
-  transformUnderscoreToHyphenBeforeCompleteMigration (roles: Array<string>): Array<string> {
+  static transformUnderscoreToHyphen (roles: Array<string>): Array<string> {
     const regex = /(ROLE_)([A-Z]*_[A-Z]*)([A-Z_]*)*/i
     let match
     roles = roles.map((role) => {
@@ -103,12 +106,19 @@ class RoleUtils {
   /**
    * On transforme les ROLES Symfony en Abilities
    *
+   * Ex:
+   *
+   *    "ROLE_ORGANIZATION"  =>  { subject: 'organization', action: 'manage'}
+   *    "ROLE_PLACE_VIEW"    =>  { subject: 'place', action: 'read'}
+   *
    * @param {Array<string>} roles
    * @return {Array<AbilitiesType>}
    */
-  transformRoleToAbilities (roles: Array<string>): [] | Array<AbilitiesType> {
+  static rolesToAbilities (roles: Array<string>): [] | Array<AbilitiesType> {
     const abilities:Array<AbilitiesType> = []
 
+    roles = RoleUtils.transformUnderscoreToHyphen(roles)
+
     const regex = /(ROLE_)([A-Z-]*)([_A-Z]*)/i
     let match
 
@@ -116,12 +126,14 @@ class RoleUtils {
       if ((match = regex.exec(role)) !== null) {
         const subject = match[2]
         const action = match[3]
-        if(subject){
+
+        if (subject) {
           abilities.push({
             action: actionMap[action],
             subject: subject.toLowerCase()
           })
         }
+
       }
     })
 
@@ -129,4 +141,4 @@ class RoleUtils {
   }
 }
 
-export const $roleUtils = new RoleUtils()
+export default RoleUtils

+ 8 - 4
services/utils/url.ts

@@ -68,13 +68,17 @@ class Url {
    * Découpe une URI au niveau des '/'
    * Utilisé entre autres pour le breadcrumb
    *
+   * Ex:
+   *
+   *    foo/bar/1   =>  ['foo', 'bar', '1']
+   *    /foo/bar/1   =>  ['foo', 'bar', '1']
+   *    https://domain.com/foo/bar/1   =>  ['https:', 'domain.com', 'foo', 'bar', '1']
+   *
+   *
    * @param uri
    */
   public static split(uri: string) {
-    if (uri.startsWith('/')) {
-      uri = uri.substring(1)
-    }
-    return uri.split('/')
+    return uri.split('/').filter((s) => s.length > 0)
   }
 }
 

+ 12 - 99
stores/accessProfile.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia'
-import {$roleUtils} from "~/services/rights/roleUtils";
 import {
   AbilitiesType,
   baseAccessState,
@@ -11,6 +10,7 @@ import {computed, ref, Ref} from "@vue/reactivity";
 import {useEach} from "#imports";
 import {useAbility} from "@casl/vue";
 import {useOrganizationProfileStore} from "~/stores/organizationProfile";
+import RoleUtils from "~/services/rights/roleUtils";
 
 export const useAccessProfileStore = defineStore('accessProfile', () => {
 
@@ -95,18 +95,18 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     historical.value = profile.historical
     isAdminAccess.value = profile.isAdminAccess
 
-    isAdmin.value = $roleUtils.isA('ADMIN', profileRoles)
-    isAdministratifManager.value = $roleUtils.isA('ADMINISTRATIF_MANAGER', profileRoles)
-    isPedagogicManager.value = $roleUtils.isA('PEDAGOGICS_MANAGER', profileRoles)
-    isFinancialManager.value = $roleUtils.isA('FINANCIAL_MANAGER', profileRoles)
-    isCaMember.value = $roleUtils.isA('CA', profileRoles)
-    isStudent.value = $roleUtils.isA('STUDENT', profileRoles)
-    isTeacher.value = $roleUtils.isA('TEACHER', profileRoles)
-    isMember.value = $roleUtils.isA('MEMBER', profileRoles)
-    isOther.value = $roleUtils.isA('OTHER', profileRoles)
+    isAdmin.value = RoleUtils.isA('ADMIN', profileRoles)
+    isAdministratifManager.value = RoleUtils.isA('ADMINISTRATIF_MANAGER', profileRoles)
+    isPedagogicManager.value = RoleUtils.isA('PEDAGOGICS_MANAGER', profileRoles)
+    isFinancialManager.value = RoleUtils.isA('FINANCIAL_MANAGER', profileRoles)
+    isCaMember.value = RoleUtils.isA('CA', profileRoles)
+    isStudent.value = RoleUtils.isA('STUDENT', profileRoles)
+    isTeacher.value = RoleUtils.isA('TEACHER', profileRoles)
+    isMember.value = RoleUtils.isA('MEMBER', profileRoles)
+    isOther.value = RoleUtils.isA('OTHER', profileRoles)
     isGuardian.value = profile.isGuardian
     isPayer.value = profile.isPayor
-    roles.value = $roleUtils.filterFunctionRoles(profileRoles)
+    roles.value = RoleUtils.filterFunctionRoles(profileRoles)
 
     // Time to add the original Access (switch User case)
     originalAccess.value = profile.originalAccess
@@ -153,90 +153,6 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     }
   }
 
-  /**
-   * Teste le profil d'un utilisateur
-   *
-   * @param {string} profile : profile à tester
-   * @return {boolean}
-   */
-  const testProfile = (profile:string): boolean => {
-    const factory: {[key: string]: boolean|null} = {
-      'admin': isAdmin.value,
-      'administratifManager': isAdministratifManager.value,
-      'pedagogicManager': isPedagogicManager.value,
-      'financialManager': isFinancialManager.value,
-      'caMember': isCaMember.value,
-      'student': isStudent.value,
-      'teacher': isTeacher.value,
-      'member': isMember.value,
-      'other': isOther.value,
-      'guardian': isGuardian.value,
-      'payor': isPayer.value,
-    }
-
-    if (!(profile in factory)) {
-      return false
-    }
-    return factory[profile] ?? false
-  }
-
-  /**
-   * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre
-   *
-   * @param {Array<string>} profiles Profils à tester
-   * @return {boolean}
-   */
-  const hasProfile = (profiles: Array<string>|null): boolean => {
-    if (null === profiles)
-      return true;
-
-    let hasProfile = false;
-    profiles.map(async (profile) => {
-      if (testProfile(profile))
-        hasProfile = true;
-    });
-    return hasProfile;
-  }
-
-  /**
-   * Est-ce que l'utilisateur possède l'habilité
-   *
-   * @param {Array<AbilitiesType>} abilities Habilités à tester
-   * @return {boolean}
-   */
-  const hasAbility = (abilities: Array<AbilitiesType>|null): boolean => {
-    if(abilities === null)
-      return true;
-
-    const { can } = useAbility()
-
-    let hasAbility= false;
-    abilities.map((ability) => {
-      if (can(ability.action, ability.subject))
-        hasAbility = true;
-    });
-    return hasAbility;
-  }
-
-  /**
-   * Est-ce que l'utilisateur possède le ou les rôles donnés ?
-   *
-   * @param {Array<string>} roles Rôles à tester
-   * @return {boolean}
-   */
-  const hasRole = computed((rolesToTest: Array<string>|null): boolean => {
-    if (rolesToTest === null) {
-      return true
-    }
-
-    rolesToTest.map((r) => {
-      if (roles.value.includes(r)) {
-        return true
-      }
-    })
-    return false
-  })
-
   return {
     bearer,
     id,
@@ -271,9 +187,6 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     setFamilyAccesses,
     setProfile,
     refreshProfile,
-    setOriginalAccess,
-    hasProfile,
-    hasAbility,
-    hasRole,
+    setOriginalAccess
   }
 })

+ 0 - 17
stores/organizationProfile.ts

@@ -20,22 +20,6 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
   const parents: Ref<Array<baseOrganizationState>> = ref([])
 
   // Getters
-  /**
-   * Est-ce que l'organisation possède le module donné
-   *
-   * @param {Array<string>} modules Modules à tester
-   * @return {boolean}
-   */
-  const hasModule = computed((): boolean => {
-    let hasModule = false
-    modules.value.map((module) => {
-      if (modules && modules.value.includes(module)) {
-        hasModule = true
-      }
-    })
-    return hasModule
-  })
-
   /**
    * L'organization fait-elle partie du réseau CMF?
    *
@@ -200,7 +184,6 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
     networks,
     website,
     parents,
-    hasModule,
     isCmf,
     isFfec,
     isInsideNetwork,