Quellcode durchsuchen

Merge branch 'release/2.3.beta' into develop

Olivier Massot vor 2 Jahren
Ursprung
Commit
20ca296423

+ 1 - 0
components/Layout/AlertBar.vue

@@ -16,6 +16,7 @@ Contient les différentes barres d'alertes qui s'affichent dans certains cas
 
     <LayoutAlertBarSwitchYear />
     <LayoutAlertBarSuperAdmin />
+    <LayoutAlertBarRegistrationStatus v-if="organizationProfile.hasModule('IEL')" />
   </main>
 </template>
 

+ 11 - 8
components/Layout/AlertBar/Cotisation.vue

@@ -7,7 +7,7 @@ Barre d'alerte qui s'affiche pour donner l'état de la cotisation
 <template>
   <main>
     <UiSystemBar
-        v-if="alert !== null"
+        v-if="alert && alert.text && alert.callback"
         :text="$t(alert.text)"
         icon="fas fa-info-circle"
         :on-click="alert.callback"
@@ -71,16 +71,18 @@ if (!organizationProfile.id) {
 }
 
 const { fetch } = useEntityFetch()
-const { data: cotisation } = await fetch(Cotisation, organizationProfile.id)
+const { data: cotisation, pending } = await fetch(Cotisation, organizationProfile.id)
 
 interface Alert {
   text: string
   callback: () => void
 }
 
-const alert: Ref<Alert | null> = ref(null)
+const alert: ComputedRef<Alert | null> = computed(() => {
+  if (pending.value) {
+    return null
+  }
 
-if (cotisation.value !== null) {
   cotisationYear.value = cotisation.value.cotisationYear
 
   const mapping: Record<ALERT_STATE_COTISATION, Alert> = {
@@ -90,11 +92,12 @@ if (cotisation.value !== null) {
     'ADVERTISINGINSURANCE': { text: 'insurance_cmf_subscription', callback: openCmfSubscriptionPage },
   }
 
-  alert.value = mapping[cotisation.value.alertState as ALERT_STATE_COTISATION]
-}
-
-
+  if (!cotisation.value.alertState) {
+    return null
+  }
 
+  return mapping[cotisation.value.alertState as ALERT_STATE_COTISATION]
+})
 
 </script>
 

+ 41 - 0
components/Layout/AlertBar/OnlineRegistration.vue

@@ -0,0 +1,41 @@
+<!--
+Barre d'alerte sur l'ouverture ou non de l'inscription en ligne
+
+-->
+
+<template>
+  <UiSystemBar
+      v-if="show"
+      :text="$t(message)"
+      icon="fas fa-id-card"
+      class="theme-secondary-alt"
+  />
+</template>
+
+<script setup lang="ts">
+
+import {useEntityFetch} from "~/composables/data/useEntityFetch";
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import RegistrationAvailability from "~/models/OnlineRegistration/RegistrationAvailability";
+import {ComputedRef} from "@vue/reactivity";
+
+const { fetch } = useEntityFetch()
+
+const accessProfile = useAccessProfileStore()
+
+const { data: registrationAvailability, pending } = fetch(RegistrationAvailability, accessProfile.id ?? 0)
+
+const show: ComputedRef<boolean> = computed(() => {
+  return !pending && (registrationAvailability.value as RegistrationAvailability).available
+})
+
+const message: ComputedRef<string> = computed(() => {
+  return (registrationAvailability.value as RegistrationAvailability).message
+})
+
+
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 45 - 0
components/Layout/AlertBar/RegistrationStatus.vue

@@ -0,0 +1,45 @@
+<!--
+Barre d'alerte quand au statut (l'avancement) de l'inscription en ligne de l'utilisateur
+
+-->
+
+<template>
+  <UiSystemBar
+      v-if="!pending && message"
+      :text="$t(message)"
+      icon="fas fa-id-card"
+      class="theme-secondary"
+  />
+</template>
+
+<script setup lang="ts">
+import {useEntityFetch} from "~/composables/data/useEntityFetch";
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import RegistrationStatus from "~/models/OnlineRegistration/RegistrationStatus";
+import {ComputedRef} from "@vue/reactivity";
+
+const { fetch } = useEntityFetch()
+
+const accessProfile = useAccessProfileStore()
+
+const { data: registrationStatus, pending } = fetch(RegistrationStatus, accessProfile.id ?? 0)
+
+const messagesByStatus = {
+  'NEGOTIABLE': "your_application_is_awaiting_processing",
+  'PENDING': "you_have_been_placed_on_the_waiting_list",
+  'ACCEPTED': "your_registration_file_has_been_validated",
+  'DENIED': "your_application_has_been_refused",
+}
+
+const message: ComputedRef<string> = computed(() => {
+  if (!registrationStatus.value) {
+    return ''
+  }
+  const status = (registrationStatus.value as RegistrationStatus).status
+  return status ? messagesByStatus[status] : ''
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 1 - 1
components/Layout/AlertBar/SwitchYear.vue

@@ -6,7 +6,7 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur n'est pas sur l'année couran
 
 <template>
   <!-- TODO : fonctionnement à valider -->
-  <UiSystemBar v-if="show" class="theme-warning">
+  <UiSystemBar v-if="show" class="theme-warning flex-column">
     {{$t('not_current_year')}}
 
     <a @click="resetYear" class="text-decoration-none on-warning" style="cursor: pointer;">

+ 1 - 1
components/Layout/Header.vue

@@ -39,7 +39,7 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
 
     <a
         :href="runtimeConfig.supportUrl"
-        class="text-body pa-3 ml-2 theme-secondary text-decoration-none"
+        class="text-body pa-3 ml-2 theme-secondary text-decoration-none h-100"
         target="_blank"
     >
       <span class="d-none d-sm-none d-md-flex">{{ $t('help_access') }}</span>

+ 4 - 0
components/Layout/Header/Menu.vue

@@ -125,6 +125,10 @@ const btn = ref(null)
     padding: 0;
   }
 
+  .v-list-item {
+    width: 100%;
+  }
+
   .header-menu .v-list .v-list-item:last-child {
     border-bottom: none;
   }

+ 18 - 11
components/Layout/Header/Notification.vue

@@ -17,11 +17,12 @@
     </v-badge>
   </v-btn>
 
-  <v-tooltip :activator="btn" location="bottom">
+  <v-tooltip v-if="btn !== null" :activator="btn" location="bottom">
     <span>{{ $t('notification') }}</span>
   </v-tooltip>
 
   <v-menu
+      v-if="btn !== null"
       :activator="btn"
       v-model="isOpen"
       location="bottom left"
@@ -122,7 +123,6 @@ const query: ComputedRef<AnyJson> = computed(() => {
   return { 'page': page.value }
 })
 
-
 let { data: collection, pending, refresh } = await fetchCollection(Notification, null, query)
 
 /**
@@ -143,10 +143,10 @@ const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
  * Les metadata dépendront de la dernière valeur du GET lancé
  */
 const pagination: ComputedRef<Pagination> = computed(() => {
-  return collection.value !== null ? collection.value.pagination : {}
+  return (!pending.value && collection.value !== null) ? collection.value.pagination : {}
 })
 
-const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, 'notifications/list/')
+const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/notifications/list/')
 
 /**
  * L'utilisateur a fait défiler le menu jusqu'à la dernière notification affichée
@@ -224,7 +224,7 @@ const markNotificationAsRead = (notification: Notification) => {
     throw new Error('Current access id is null')
   }
   const notificationUsers = em.newInstance(NotificationUsers, {
-    access:`/api/accesses/${accessProfileStore.id}`,
+    access:`/api/accesses/${accessProfileStore.switchId ?? accessProfileStore.id}`,
     notification:`/api/notifications/${notification.id}`,
     isRead: true
   })
@@ -251,14 +251,21 @@ const download = (link: string) => {
   if (accessProfileStore.id === null) {
     throw new Error('Current access id is null')
   }
-  const url_parts: Array<string> = link.split('/api');
+  // TODO: passer cette logique dans un service ; tester ; voir si possible de réunir avec composables/utils/useDownloadFile.ts
+
+  const path: string = link.split('/api')[1];
 
-  if(accessProfileStore.originalAccess)
-    url_parts[0] = UrlUtils.join('api', String(accessProfileStore.originalAccess.id), String(accessProfileStore.id))
-  else
-    url_parts[0] = UrlUtils.join('api', String(accessProfileStore.id))
+  // En switch : https://api.test5.opentalent.fr/api/{accessId}/{switchId}/files/{fileId}/download
+  // Sans switch : https://local.api.opentalent.fr/api/{accessId}/files/{fileId}/download
+  const url = UrlUtils.join(
+      runtimeConfig.baseUrlLegacy,
+      'api',
+      String(accessProfileStore.id),
+      String(accessProfileStore.switchId || ''),
+      path
+  )
 
-  window.open(UrlUtils.join(runtimeConfig.baseUrlLegacy, url_parts.join('')));
+  window.open(url);
 }
 
 

+ 8 - 3
components/Layout/MainMenu.vue

@@ -22,15 +22,16 @@ Prend en paramètre une liste de ItemMenu et les met en forme
       <!-- TODO: que se passe-t-il si le menu ne comprend qu'un seul MenuItem? -->
       <div v-for="(item, i) in items" :key="i">
 
-        <!-- Cas 1 : l'item n'a pas d'enfants, c'est un lien -->
+        <!-- Cas 1 : l'item n'a pas d'enfants, c'est un lien (ou le menu est en mode réduit) -->
         <v-list-item
-          v-if="!item.children"
+          v-if="!item.children || isRail"
           :title="$t(item.label)"
           :prepend-icon="item.icon.name"
           :href="!isInternalLink(item) ? item.to : undefined"
           :to="isInternalLink(item) ? item.to : undefined"
           exact
           height="48px"
+          class="menu-item"
         />
 
         <!-- Cas 2 : l'item a des enfants, c'est un groupe -->
@@ -44,7 +45,7 @@ Prend en paramètre une liste de ItemMenu et les met en forme
                 v-bind="props"
                 :prepend-icon="item.icon.name"
                 :title="$t(item.label)"
-                class="theme-secondary"
+                class="theme-secondary menu-item"
                 height="48px"
             />
           </template>
@@ -179,4 +180,8 @@ onUnmounted(() => {
     margin-right: 10px;
   }
 
+  :deep(.menu-item .fa) {
+    text-align: center;
+  }
+
 </style>

+ 4 - 2
components/Layout/SubHeader/DataTiming.vue

@@ -55,7 +55,9 @@ const historicalValue: Ref<Array<string>> = ref(historicalChoices.filter((item)
 const onUpdate = async (newValue: Array<string>) => {
   historicalValue.value = newValue
 
-  if (accessProfileStore.id === null) {
+  const accessId = accessProfileStore.switchId ?? accessProfileStore.id
+
+  if (accessId === null) {
     throw new Error('Invalid profile id')
   }
 
@@ -68,7 +70,7 @@ const onUpdate = async (newValue: Array<string>) => {
   setDirty(false)
   pageStore.loading = true
 
-  await em.patch(Access, accessProfileStore.id, {'historical': accessProfileStore.historical})
+  await em.patch(Access, accessId, {'historical': accessProfileStore.historical})
   if (process.server) {
       // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
       await em.refreshProfile()

+ 5 - 2
components/Layout/SubHeader/DataTimingRange.vue

@@ -34,7 +34,10 @@ const end = accessProfileStore.historical.dateEnd
 const datesRange: Ref<Array<Date> | null> = ref((start && end) ? [new Date(start), new Date(end)] : null)
 
 const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
-  if (accessProfileStore.id === null) {
+
+  const accessId = accessProfileStore.switchId ?? accessProfileStore.id
+
+  if (accessId === null) {
     throw new Error('Invalid profile id')
   }
 
@@ -51,7 +54,7 @@ const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
   setDirty(false)
   pageStore.loading = true
 
-  await em.patch(Access, accessProfileStore.id, {'historical': accessProfileStore.historical})
+  await em.patch(Access, accessId, {'historical': accessProfileStore.historical})
   if (process.server) {
       // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
       await em.refreshProfile()

+ 9 - 5
composables/data/useEntityFetch.ts

@@ -3,6 +3,7 @@ import ApiResource from "~/models/ApiResource";
 import {AssociativeArray, Collection} from "~/types/data";
 import {AsyncData} from "#app";
 import {ComputedRef, Ref} from "vue";
+import {v4 as uuid4} from "uuid";
 
 interface useEntityFetchReturnType {
     fetch: (model: typeof ApiResource, id: number) => AsyncData<ApiResource, ApiResource | true>,
@@ -11,20 +12,23 @@ interface useEntityFetchReturnType {
     getRef: <T extends ApiResource>(model: typeof T, id: Ref<number | null>) => ComputedRef<null | T>
 }
 
-
 // TODO: améliorer le typage des fonctions sur le modèle de getRef
 export const useEntityFetch = (lazy: boolean = false): useEntityFetchReturnType => {
     const { em } = useEntityManager()
 
     const fetch = (model: typeof ApiResource, id: number) => useAsyncData(
-        model.entity + '_' + id,
+        model.entity + '_' + id + '_' + uuid4(),
         () => em.fetch(model, id, true),
         { lazy }
     )
 
-    const fetchCollection = (model: typeof ApiResource, parent: ApiResource | null = null, query: Ref<AssociativeArray> = ref([])) => useAsyncData(
-        model.entity + '_many',
-        () => em.fetchCollection(model, parent, query.value),
+    const fetchCollection = (
+        model: typeof ApiResource,
+        parent: ApiResource | null = null,
+        query: Ref<AssociativeArray | null> = ref(null)
+    ) => useAsyncData(
+        model.entity + '_many_' + uuid4(),
+        () => em.fetchCollection(model, parent, query.value ?? undefined),
         { lazy }
     )
 

+ 0 - 0
composables/layout/useRedirectToLogin.ts


+ 21 - 0
composables/utils/useRedirect.ts

@@ -0,0 +1,21 @@
+import UrlUtils from "~/services/utils/urlUtils";
+
+export const useRedirect = () => {
+    const runtimeConfig = useRuntimeConfig()
+
+    const redirectToLogout = () => {
+        if (!runtimeConfig.baseUrlAdminLegacy) {
+            throw new Error('Configuration error : no redirection target')
+        }
+        navigateTo(UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/logout'), {external: true})
+    }
+
+    const redirectToHome = () => {
+        if (!runtimeConfig.baseUrlAdminLegacy) {
+            throw new Error('Configuration error : no redirection target')
+        }
+        navigateTo(UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/dashboard'), {external: true})
+    }
+
+    return { redirectToLogout, redirectToHome }
+}

+ 0 - 12
composables/utils/useRedirectToLogout.ts

@@ -1,12 +0,0 @@
-import UrlUtils from "~/services/utils/urlUtils";
-
-export const useRedirectToLogout = () => {
-    const runtimeConfig = useRuntimeConfig()
-
-    return () => {
-        if (!runtimeConfig.baseUrlAdminLegacy) {
-            throw new Error('Configuration error : no redirection target')
-        }
-        navigateTo(UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/logout'), {external: true})
-    }
-}

+ 0 - 1
config/abilities/pages/addressBook.yaml

@@ -34,4 +34,3 @@
     conditions:
       - {function: organizationHasAnyModule, parameters: ['NetworkOrganization']}
       - {function: organizationHasChildren, expectedResult: false}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'core'}]}

+ 1 - 1
config/abilities/pages/myAccount.yaml

@@ -68,7 +68,7 @@
     action: 'display'
     conditions:
       - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
-      - { function: accessHasAnyProfile, parameters: ['admin', 'administratifManager', 'pedagogicManager', 'financialManager'] }
+      - { function: accessHasAnyProfile, parameters: ['admin', 'administratifManager', 'pedagogicManager', 'financialManager', 'caMember'] }
 
   my_bills_page:
     action: 'display'

+ 82 - 0
doc/abilities.md

@@ -0,0 +1,82 @@
+# Abilities
+
+### Principe de base
+
+La gestion des droits se base sur la librairie [@casl/ability](https://casl.js.org/v6/en/) et génère un ensemble de 
+droits (abilities), par exemple le droit d'afficher une page.
+
+Ces droits dépendent de deux choses :
+
+- Les modules du logiciel possédés par l'organisation
+- Les rôles de l'utilisateur dans cette organisation (y compris d'éventuels droits personnalisés)
+
+Par la suite, on pourra tester les droits d'un utilisateur à effectuer une action en faisant par exemple : 
+
+      if(!ability.can('display', 'subscription_page')) {
+          throw new Error('Forbidden')
+      }
+
+
+### Fonctionnement
+
+#### Les rôles de l'utilisateur
+
+Les rôles de l'utilisateur sont récupérés auprès de l'API. Ils peuvent être de deux natures :
+
+- rôle en tant que fonction (`ROLE_CA`, `ROLE_MEMBER`, ...)
+- rôle lié à un droit particulier (`ROLE_GENERAL_CONFIG`, `ROLE_USERS`, ...)
+
+#### Les fichiers de configuration
+
+L'ensemble des droits est défini dans les fichiers de configuration `./config/abilities`
+
+Chaque entrée est constituée : 
+
+- d'un nom
+- d'une action associée (`display` ou `manage`)
+- et éventuellement d'une ou plusieurs conditions
+
+Exemple :
+
+    adherent_list_page:
+        action: 'display'
+        conditions:
+        - { function: organizationHasAnyModule, parameters: ['Users'] }
+        - { function: organizationIsShowAdherentList }
+        - { function: accessHasAnyProfile, parameters: ['member'] }
+
+
+#### Le plugin ability.ts
+
+Le plugin `plugins/ability.ts` :
+
+1. récupère les profils de l'access et de l'organisation
+2. instancie le service AbilityBuilder
+3. lui injecte les deux profils récupérés plus haut
+4. puis créé un listener qui appellera la méthode `buildAbilities` de ce service à chaque mise à jour du profil de l'organisation
+
+
+#### Le service AbilityBuilder
+
+Ce service fait appel tour à tour à deux méthodes `buildAbilitiesFromRoles` et `buildAbilitiesFromConfig`
+
+La première fait appel à son tour au service `RoleUtils` pour convertir tous les rôles (hors rôles liés à une fonction,
+par ex "Membre du CA") en droits. Ainsi un rôle `ROLE_EXAMENS` deviendra un droit `{subject: 'examen', action: 'manage'}`
+et un rôle deviendra un droits `{subject: 'billing_administration', action: 'display'}`
+
+La seconde construit les droits à partir des fichiers de configuration qui définissent les conditions d'accès aux 
+différents droits (voir plus haut). Ces conditions sont testées de diverses manières selon leur nature. Par exemple, 
+la condition `accessIsAdminAccount` deviendra un test sur `accessProfile.isAdminAccount`.
+
+Une fois construit, ces droits sont passés au service MongoAbility de la librairie Casl.
+
+#### Utilisation
+
+A partir de là, on pourra tester les droits d'un utilisateur dans une page ou un composable à l'aide de la commande 
+`ability.can(action, subject)`. 
+
+Cette méthode sera ainsi appelée en en-tête de la section script de chaque page, et pour chaque entrée des menus.
+
+
+
+

+ 6 - 2
lang/fr.json

@@ -50,7 +50,7 @@
   "domain_name": "Nom de domaine",
   "dummy_domain_name": "ma-structure.fr",
   "associated_mail_address": "Adresse email associée",
-  "dummy_email_address": "contact[at]ma-structure.fr",
+  "dummy_email_address": "contact{'@'}ma-structure.fr",
   "GUARDIANS": "Tuteurs uniquement",
   "STUDENTS": "Élèves uniquement",
   "STUDENTS_AND_THEIR_GUARDIANS": "Élèves et leurs tuteurs",
@@ -586,5 +586,9 @@
   "ending_date": "Date de fin",
   "please_note_that_this_reservation_start_on_date_anterior_to_now": "Veuillez noter que cette réservation débute à une date antérieure à aujourd'hui",
   "validate": "Valider",
-  "which_date_and_which_hour": "A quelle date et quelle heure ?"
+  "which_date_and_which_hour": "A quelle date et quelle heure ?",
+  "your_application_is_awaiting_processing": "Votre dossier d'inscription est en attente de traitement",
+  "you_have_been_placed_on_the_waiting_list": "Votre dossier d'inscription est en cours de traitement. Vous avez été placé sur liste d'attente pour une ou plusieurs activités",
+  "your_registration_file_has_been_validated": "Votre dossier d'inscription a été validé. Vous pouvez consulter le bloc \"Enseignements en cours et à venir\" sur la page d'accueil pour avoir des détails sur vos activités.",
+  "your_application_has_been_refused": "Votre dossier d'inscription a été refusé / annulé. Veuillez contacter l'administration pour plus d'informations."
 }

+ 0 - 2
models/Core/Notification.ts

@@ -30,6 +30,4 @@ export default class Notification extends ApiModel {
 
   @Attr({})
   declare notificationUsers: Array<string>
-
-
 }

+ 18 - 0
models/OnlineRegistration/RegistrationAvailability.ts

@@ -0,0 +1,18 @@
+import ApiResource from "~/models/ApiResource";
+import {Bool, Str, Uid} from "pinia-orm/dist/decorators";
+
+/**
+ * Disponibilité (ouverture) de l'IEL
+ */
+export default class RegistrationAvailability extends ApiResource {
+    static entity = 'online_registration/availability'
+
+    @Uid()
+    declare id: number | string | null
+
+    @Bool(false)
+    declare available: boolean
+
+    @Str(null)
+    declare message: string
+}

+ 18 - 0
models/OnlineRegistration/RegistrationStatus.ts

@@ -0,0 +1,18 @@
+import ApiModel from "~/models/ApiModel";
+import {Num, Str, Uid} from "pinia-orm/dist/decorators";
+import ApiResource from "~/models/ApiResource";
+
+/**
+ * AP2i Model : File
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/Entity/Core/File.php
+ */
+export default class RegistrationStatus extends ApiResource {
+  static entity = 'online_registration/status'
+
+  @Uid()
+  declare id: number | string | null
+
+  @Str('')
+  declare status: 'PENDING' | 'NEGOTIABLE' | 'ACCEPTED' | 'DENIED' | null
+}

+ 10 - 10
pages/subscription.vue

@@ -91,7 +91,7 @@ Page 'Mon abonnement'
                 <v-row>
                   {{ $t('PRODUCT_ARTIST_PREMIUM') }}
                 </v-row>
-                <v-row>
+                <v-row class="align-end">
                   <nuxt-img src="/images/Artist-Square.jpg" />
                 </v-row>
                 <v-row>
@@ -144,7 +144,7 @@ Page 'Mon abonnement'
                 <v-row>
                   {{ $t('PRODUCT_SCHOOL') }}
                 </v-row>
-                <v-row>
+                <v-row class="align-end">
                   <nuxt-img src="/images/School-Square.jpg" />
                 </v-row>
                 <v-row>
@@ -191,8 +191,8 @@ Page 'Mon abonnement'
                 <v-row>
                   {{ $t('sms') }}
                 </v-row>
-                <v-row>
-                  <nuxt-img src="/images/sms_big.png" :height="100" :width="100"/>
+                <v-row class="align-end">
+                  <nuxt-img src="/images/sms.png" :height="140" :width="100"/>
                 </v-row>
                 <v-row>
                   <p><b>{{ $t('send_sms') }} {{ $t('to_your_members_from_app') }}</b></p>
@@ -229,8 +229,8 @@ Page 'Mon abonnement'
                 <v-row>
                   {{ $t('website') }}
                 </v-row>
-                <v-row>
-                  <nuxt-img src="/images/nom-de-domaine.jpg" />
+                <v-row class="align-end">
+                  <nuxt-img src="/images/nom_de_domaine.png" :height="160" />
                 </v-row>
                 <v-row>
                     <v-col>
@@ -283,13 +283,13 @@ Page 'Mon abonnement'
   import MobytUserStatus from "~/models/Organization/MobytUserStatus";
   import {Ref} from "@vue/reactivity";
   import {useDisplay} from "vuetify";
+  import {useRedirect} from "~/composables/utils/useRedirect";
 
   const ability = useAbility()
 
-  onBeforeMount(() => {
-    if(!ability.can('display', 'subscription_page'))
-      return navigateTo('/error')
-  })
+  if(!ability.can('display', 'subscription_page')) {
+    throw new Error('Forbidden')
+  }
 
   const showDolibarrPanel = computed(() => !dolibarrPending.value && dolibarrAccount.value && dolibarrAccount.value.bills.length > 0)
 

+ 2 - 2
plugins/init.server.ts

@@ -1,10 +1,10 @@
 import {useAccessProfileStore} from "~/stores/accessProfile";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 import UnauthorizedError from "~/services/error/UnauthorizedError";
-import {useRedirectToLogout} from "~/composables/utils/useRedirectToLogout";
+import {useRedirect} from "~/composables/utils/useRedirect";
 
 export default defineNuxtPlugin(async () => {
-    const redirectToLogout = useRedirectToLogout()
+    const { redirectToLogout } = useRedirect()
 
     const bearer = useCookie('BEARER')
     let accessCookieId = useCookie('AccessId')

BIN
public/images/nom-de-domaine.jpg


BIN
public/images/nom_de_domaine.png


BIN
public/images/sms.png


BIN
public/images/sms_big.png


+ 0 - 4
services/layout/menuBuilder/statsMenuBuilder.ts

@@ -27,10 +27,6 @@ export default class StatsMenuBuilder extends AbstractMenuBuilder {
       children.push(this.createItem('fede_stats', {name: 'fas fa-chart-bar'}, '/statistic/membersfedeonly', MENU_LINK_TYPE.V1))
     }
 
-    if (this.ability.can('display', 'structure_stats_page')) {
-      children.push(this.createItem('structure_stats', {name: 'fas fa-chart-bar'}, '/statistic/membersfedeassos', MENU_LINK_TYPE.V1))
-    }
-
     if (children.length > 1) {
       // Plusieurs éléments, on retourne un groupe
       return this.createGroup('stats', {name: 'fas fa-chart-bar'}, children)

+ 2 - 1
services/layout/menuBuilder/websiteListMenuBuilder.ts

@@ -2,6 +2,7 @@ import AbstractMenuBuilder from '~/services/layout/menuBuilder/abstractMenuBuild
 import {MenuGroup, MenuItems} from "~/types/layout";
 import {MENU_LINK_TYPE} from "~/types/enum/layout";
 import * as _ from 'lodash-es'
+import UrlUtils from "~/services/utils/urlUtils";
 
 /**
  * Menu : Liste des sites internet de la structure et de ses structures parentes
@@ -17,7 +18,7 @@ export default class WebsiteListMenuBuilder extends AbstractMenuBuilder {
 
     // Add organization website
     if (this.organizationProfile.website) {
-      const url = this.organizationProfile.website + '/typo3'
+      const url = UrlUtils.join(this.organizationProfile.website, '/typo3')
       children.push(this.createItem(this.organizationProfile.name as string, undefined, url, MENU_LINK_TYPE.EXTERNAL))
     }
 

+ 2 - 0
stores/accessProfile.ts

@@ -108,6 +108,8 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     historical.value = profile.historical
     isAdminAccess.value = profile.isAdminAccess
 
+    // TODO: pqoi est-ce qu'on ne conserve pas les roles fonction et qu'on ne fait pas de ces méthodes des computed?
+    //       est-ce que ce ne serait pas plus intuitif? si on fait ça, attention à maj l'abilityBuilder
     isAdmin.value = RoleUtils.isA('ADMIN', profileRoles)
     isAdministratifManager.value = RoleUtils.isA('ADMINISTRATIF_MANAGER', profileRoles)
     isPedagogicManager.value = RoleUtils.isA('PEDAGOGICS_MANAGER', profileRoles)