浏览代码

apply prettier fixes

Olivier Massot 1 年之前
父节点
当前提交
d6ee61954c
共有 100 个文件被更改,包括 3504 次插入3074 次删除
  1. 28 33
      .eslintrc.cjs
  2. 1 1
      .gitlab-ci.yml
  3. 1 0
      .prettierignore
  4. 13 23
      README.md
  5. 2 2
      assets/css/global.scss
  6. 1 1
      assets/css/settings.scss
  7. 0 1
      assets/css/theme.scss
  8. 12 13
      components/Layout/Alert/Container.vue
  9. 9 11
      components/Layout/Alert/Content.vue
  10. 10 6
      components/Layout/AlertBar.vue
  11. 43 24
      components/Layout/AlertBar/Cotisation.vue
  12. 7 7
      components/Layout/AlertBar/Env.vue
  13. 17 16
      components/Layout/AlertBar/OnlineRegistration.vue
  14. 17 16
      components/Layout/AlertBar/RegistrationStatus.vue
  15. 34 26
      components/Layout/AlertBar/SuperAdmin.vue
  16. 11 8
      components/Layout/AlertBar/SwitchUser.vue
  17. 50 43
      components/Layout/AlertBar/SwitchYear.vue
  18. 10 10
      components/Layout/BannerTop.vue
  19. 3 3
      components/Layout/Container.vue
  20. 42 40
      components/Layout/Dialog.vue
  21. 45 44
      components/Layout/Header.vue
  22. 11 13
      components/Layout/Header/HomeBtn.vue
  23. 57 57
      components/Layout/Header/Menu.vue
  24. 90 85
      components/Layout/Header/Notification.vue
  25. 89 89
      components/Layout/Header/UniversalCreation/Card.vue
  26. 153 137
      components/Layout/Header/UniversalCreation/CreateButton.vue
  27. 72 62
      components/Layout/Header/UniversalCreation/EventParams.vue
  28. 190 178
      components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue
  29. 15 18
      components/Layout/LoadingScreen.vue
  30. 74 75
      components/Layout/MainMenu.vue
  31. 101 98
      components/Layout/ParametersMenu.vue
  32. 45 35
      components/Layout/SubHeader/ActivityYear.vue
  33. 12 12
      components/Layout/SubHeader/Breadcrumbs.vue
  34. 44 30
      components/Layout/SubHeader/DataTiming.vue
  35. 25 18
      components/Layout/SubHeader/DataTimingRange.vue
  36. 45 39
      components/Layout/SubHeader/PersonnalizedList.vue
  37. 55 42
      components/Layout/Subheader.vue
  38. 14 14
      components/Layout/ThemeSwitcher.vue
  39. 27 27
      components/Ui/Button/Delete.vue
  40. 24 23
      components/Ui/Button/Submit.vue
  41. 8 16
      components/Ui/Card.vue
  42. 18 16
      components/Ui/Collection.vue
  43. 17 29
      components/Ui/DataTable.vue
  44. 25 28
      components/Ui/DatePicker.vue
  45. 69 66
      components/Ui/DateRangePicker.vue
  46. 30 30
      components/Ui/ExpansionPanel.vue
  47. 52 51
      components/Ui/Form.vue
  48. 13 21
      components/Ui/Form/Creation.vue
  49. 22 28
      components/Ui/Form/Edition.vue
  50. 10 8
      components/Ui/Help.vue
  51. 58 50
      components/Ui/Image.vue
  52. 92 67
      components/Ui/Input/Autocomplete.vue
  53. 62 50
      components/Ui/Input/Autocomplete/Accesses.vue
  54. 44 45
      components/Ui/Input/AutocompleteWithAPI.vue
  55. 37 28
      components/Ui/Input/AutocompleteWithAp2i.vue
  56. 31 28
      components/Ui/Input/AutocompleteWithEnum.vue
  57. 11 13
      components/Ui/Input/Checkbox.vue
  58. 16 19
      components/Ui/Input/Combobox.vue
  59. 20 22
      components/Ui/Input/DatePicker.vue
  60. 14 18
      components/Ui/Input/Email.vue
  61. 20 21
      components/Ui/Input/Enum.vue
  62. 147 127
      components/Ui/Input/Image.vue
  63. 34 21
      components/Ui/Input/Number.vue
  64. 28 24
      components/Ui/Input/Phone.vue
  65. 33 23
      components/Ui/Input/Text.vue
  66. 23 25
      components/Ui/Input/TextArea.vue
  67. 14 17
      components/Ui/ItemFromUri.vue
  68. 4 14
      components/Ui/LoadingPanel.vue
  69. 38 36
      components/Ui/SystemBar.vue
  70. 6 15
      components/Ui/Template/DataTable.vue
  71. 5 5
      components/Ui/Template/Date.vue
  72. 39 44
      components/Ui/Xeditable/Text.vue
  73. 2 3
      config/abilities/config.yaml
  74. 52 31
      config/abilities/pages/addressBook.yaml
  75. 40 31
      config/abilities/pages/admin2ios.yaml
  76. 88 46
      config/abilities/pages/billing.yaml
  77. 27 27
      config/abilities/pages/communication.yaml
  78. 176 101
      config/abilities/pages/cotisations.yaml
  79. 8 5
      config/abilities/pages/donor.yaml
  80. 68 35
      config/abilities/pages/educational.yaml
  81. 8 5
      config/abilities/pages/equipment.yaml
  82. 8 5
      config/abilities/pages/medals.yaml
  83. 87 68
      config/abilities/pages/myAccount.yaml
  84. 194 122
      config/abilities/pages/parameters.yaml
  85. 50 35
      config/abilities/pages/schedule.yaml
  86. 38 20
      config/abilities/pages/stats.yaml
  87. 7 14
      doc/abilities.md
  88. 8 18
      doc/colors.md
  89. 2 6
      doc/unittests.md
  90. 27 33
      env/setupEnv.mjs
  91. 5 5
      lang/fr.json
  92. 1 1
      layouts/.eslintrc.cjs
  93. 5 3
      package.json
  94. 1 1
      pages/.eslintrc.cjs
  95. 5 5
      tests/.eslintrc.cjs
  96. 0 1
      tests/units/readme.md
  97. 2 7
      tsconfig.json
  98. 29 30
      types/data.d.ts
  99. 9 13
      types/interfaces.d.ts
  100. 18 18
      types/layout.d.ts

+ 28 - 33
.eslintrc.cjs

@@ -2,49 +2,44 @@ module.exports = {
   root: true,
   env: {
     browser: true,
-    node: true
+    node: true,
   },
-  parser: "vue-eslint-parser",
+  parser: 'vue-eslint-parser',
   parserOptions: {
-    "ecmaVersion": 2020,
-    "parser": "@typescript-eslint/parser",
-    "sourceType": "module"
+    ecmaVersion: 2020,
+    parser: '@typescript-eslint/parser',
+    sourceType: 'module',
   },
   extends: [
     '@nuxtjs/eslint-config-typescript',
     'plugin:nuxt/recommended',
-    "eslint:recommended",
-    "plugin:@typescript-eslint/recommended",
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
     'plugin:vue/vue3-recommended',
     'plugin:prettier/recommended',
-
-  ],
-  ignorePatterns: [
-    ".nuxt",
-    "coverage/*",
-    "vendor/*",
-    "dist/*",
-  ],
-  plugins: [
-    "vue",
-    "@typescript-eslint"
   ],
+  ignorePatterns: ['.nuxt', 'coverage/*', 'vendor/*', 'dist/*'],
+  plugins: ['vue', '@typescript-eslint'],
   // add your custom rules here
   rules: {
-    "no-console": 0, // on autorise les appels à la console (puisque ceux ci seront de toute façon nettoyés à la compilation)
-    'vue/valid-v-slot': ['error', {
-      allowModifiers: true,
-    }],
+    'prettier/prettier': 'error',
+    'no-console': 0, // on autorise les appels à la console (puisque ceux-ci seront de toute façon nettoyés à la compilation)
+    'vue/valid-v-slot': [
+      'error',
+      {
+        allowModifiers: true,
+      },
+    ],
+  },
+  globals: {
+    useRuntimeConfig: 'readonly',
+    navigateTo: 'readonly',
+    computed: 'readonly',
+    ref: 'readonly',
+    definePageMeta: 'readonly',
+    useRouter: 'readonly',
+    useRoute: 'readonly',
+    useI18n: 'readonly',
+    onMounted: 'readonly',
   },
-  "globals": {
-    "useRuntimeConfig": "readonly",
-    "navigateTo": "readonly",
-    "computed": "readonly",
-    "ref": "readonly",
-    "definePageMeta": "readonly",
-    "useRouter": "readonly",
-    "useRoute": "readonly",
-    "useI18n": "readonly",
-    "onMounted": "readonly",
-  }
 }

+ 1 - 1
.gitlab-ci.yml

@@ -14,7 +14,7 @@ cache:
 
 code_quality:
   script:
-    - yarn eslint . --env=staging
+    - yarn eslint .
 
 code_style:
   script:

+ 1 - 0
.prettierignore

@@ -2,3 +2,4 @@
 coverage
 node_modules
 dist
+assets/css/*.css

+ 13 - 23
README.md

@@ -2,25 +2,23 @@
 
 [![Latest Release](http://gitlab.2iopenservice.com/opentalent/app/-/badges/release.svg)](http://gitlab.2iopenservice.com/opentalent/app_nuxt3/-/releases)
 
-| Branch  | Status                                                                                                                                                                         | Coverage                                                                                                                                                                       |
-|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Branch  | Status                                                                                                                                                             | Coverage                                                                                                                                                                       |
+| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
 | master  | [![pipeline status](http://gitlab.2iopenservice.com/opentalent/app/badges/master/pipeline.svg)](http://gitlab.2iopenservice.com/opentalent/app/-/commits/master)   | [![coverage report](http://gitlab.2iopenservice.com/opentalent/app_nuxt3/badges/master/coverage.svg)](http://gitlab.2iopenservice.com/opentalent/app_nuxt3/-/commits/master)   |
 | develop | [![pipeline status](http://gitlab.2iopenservice.com/opentalent/app/badges/develop/pipeline.svg)](http://gitlab.2iopenservice.com/opentalent/app/-/commits/develop) | [![coverage report](http://gitlab.2iopenservice.com/opentalent/app_nuxt3/badges/develop/coverage.svg)](http://gitlab.2iopenservice.com/opentalent/app_nuxt3/-/commits/develop) |
 
-
 Frontend Opentalent, avec NuxtJs 3
 
 A voir :
 
-* [vuejs.org](https://vuejs.org/guide/introduction.html) : Vue est le framework de base de l'application
-* [nuxtjs.org](https://nuxt.com/docs/getting-started/introduction) : Nuxt est une surcouche à Vue qui automatise et simplifie beaucoup de choses
-* [pinia.vuejs.org](https://pinia.vuejs.org/introduction.html) : Store library that allow you to share a state accross your components / pages
-* [pinia-orm.codedredd.de](https://pinia-orm.codedredd.de/guide/getting-started/quick-start) : Ajoute une gestion par modèles / repos au store Pinia
-* [vuetifyjs.com](https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg) : Composants graphiques préconstruits
-* [typescriptlang.org](https://www.typescriptlang.org/) : Typescript
-* [jestjs.io](https://jestjs.io/docs/getting-started) : Tests unitaires pour JS
-* [cypress.io](https://docs.cypress.io/guides/getting-started/installing-cypress) : Tests end-to-end pour JS
-
+- [vuejs.org](https://vuejs.org/guide/introduction.html) : Vue est le framework de base de l'application
+- [nuxtjs.org](https://nuxt.com/docs/getting-started/introduction) : Nuxt est une surcouche à Vue qui automatise et simplifie beaucoup de choses
+- [pinia.vuejs.org](https://pinia.vuejs.org/introduction.html) : Store library that allow you to share a state accross your components / pages
+- [pinia-orm.codedredd.de](https://pinia-orm.codedredd.de/guide/getting-started/quick-start) : Ajoute une gestion par modèles / repos au store Pinia
+- [vuetifyjs.com](https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg) : Composants graphiques préconstruits
+- [typescriptlang.org](https://www.typescriptlang.org/) : Typescript
+- [jestjs.io](https://jestjs.io/docs/getting-started) : Tests unitaires pour JS
+- [cypress.io](https://docs.cypress.io/guides/getting-started/installing-cypress) : Tests end-to-end pour JS
 
 ## Installation (mode dev)
 
@@ -28,23 +26,19 @@ Cloner le projet :
 
     git clone git@gitlab.2iopenservice.com:opentalent/app.git
 
-
 Installer les dépendances :
 
     yarn install
 
-
 Copier les certificats dans le répertoire `env/` de ce projet :
 
-* local.app.opentalent.fr.crt
-* local.app.opentalent.fr.key
-
+- local.app.opentalent.fr.crt
+- local.app.opentalent.fr.key
 
 Lancer le serveur de développement :
 
     yarn dev -o
 
-
 ## Déploiement en prod
 
 ### Premier déploiement en tant que service
@@ -73,7 +67,6 @@ Attention, sur les environnements de test, il faut utiliser nvm pour exécuter l
 
     nvm exec yarn install
 
-
 ## Autres
 
 ### Lancer les tests
@@ -101,7 +94,6 @@ Sur les environnements où app est servie par supervisor, on peut consulter les
 
     yarn eslint
 
-
 ### Faire fonctionner le HMR
 
 Si le HMR (Hot Module Reload) ne fontionne pas et qu'un message d'erreur est logué en console disant que l'adresse
@@ -113,13 +105,12 @@ n'est pas accessible, alors suivre les étapes suivantes :
 - Clic droit dessus, puis "ouvrir dans un nouvel onglet"
 - Ajouter une exception de sécurité dans le navigateur
 
-
 ## Plus d'infos
 
 ## Structure du projet
 
 | Répertoire     | Rôle                                                                                                |
-|----------------|-----------------------------------------------------------------------------------------------------|
+| -------------- | --------------------------------------------------------------------------------------------------- |
 | `assets`       | Contient les fichiers style et medias                                                               |
 | `components`   | Les différents composants graphiques qui composent l'application                                    |
 | `composables`  | Des fonctions conscientes du contexte applicatif, qui font le lien entre les pages et les services  |
@@ -137,4 +128,3 @@ n'est pas accessible, alors suivre les étapes suivantes :
 | `stores`       | Le store et ses composants servent d'entrepôt de donnés, et s'assurent de la cohérence de celles-ci |
 | `tests`        | Regroupe les tests (unitaires, end-to-end...)                                                       |
 | `types`        | Types Typescript (interfaces, enums...)                                                             |
-

文件差异内容过多而无法显示
+ 2 - 2
assets/css/global.scss


+ 1 - 1
assets/css/settings.scss

@@ -2,4 +2,4 @@
 @forward 'vuetify/settings' with (
   $button-color: green,
   $button-font-weight: 700
-);
+);

+ 0 - 1
assets/css/theme.scss

@@ -1,4 +1,3 @@
-
 .theme-primary {
   background-color: rgb(var(--v-theme-primary)) !important;
   color: rgb(var(--v-theme-on-primary)) !important;

+ 12 - 13
components/Layout/Alert/Container.vue

@@ -17,27 +17,26 @@ Container principal pour l'affichage d'une ou plusieurs alertes
 
 <script setup lang="ts">
 import type { Alert } from '~/types/interfaces'
-import {usePageStore} from "~/stores/page";
-import type {ComputedRef} from "@vue/reactivity";
+import { usePageStore } from '~/stores/page'
+import type { ComputedRef } from '@vue/reactivity'
 
 const pageStore = usePageStore()
 
 const alerts: ComputedRef<Array<Alert>> = computed(() => {
   return pageStore.alerts
 })
-
 </script>
 
 <style scoped>
-  .alertContainer {
-    position: fixed;
-    bottom: 0;
-    right: 20px;
-    z-index: 1000;
-  }
+.alertContainer {
+  position: fixed;
+  bottom: 0;
+  right: 20px;
+  z-index: 1000;
+}
 
-  .alertContainer > .alertContent {
-    position: relative;
-    margin-bottom: 10px;
-  }
+.alertContainer > .alertContent {
+  position: relative;
+  margin-bottom: 10px;
+}
 </style>

+ 9 - 11
components/Layout/Alert/Content.vue

@@ -13,20 +13,20 @@
     @mouseout="onMouseOut"
   >
     <ul v-if="props.alert.messages.length > 1">
-       <li v-for="message in props.alert.messages">
+      <li v-for="message in props.alert.messages">
         {{ $t(message) }}
       </li>
     </ul>
     <span v-else>
-        {{ $t(props.alert.messages[0]) }}
+      {{ $t(props.alert.messages[0]) }}
     </span>
   </v-alert>
 </template>
 
 <script setup lang="ts">
-import type {Alert} from '~/types/interfaces'
-import type {Ref} from "@vue/reactivity";
-import {usePageStore} from "~/stores/page";
+import type { Alert } from '~/types/interfaces'
+import type { Ref } from '@vue/reactivity'
+import { usePageStore } from '~/stores/page'
 
 const props = defineProps({
   /**
@@ -34,7 +34,7 @@ const props = defineProps({
    */
   alert: {
     type: Object as () => Alert,
-    required: true
+    required: true,
   },
   /**
    * The time after which the alert disappears
@@ -42,8 +42,8 @@ const props = defineProps({
   timeout: {
     type: Number,
     required: false,
-    default: 3000
-  }
+    default: 3000,
+  },
 })
 
 const show: Ref<boolean> = ref(true)
@@ -77,8 +77,6 @@ const onMouseOut = () => {
 }
 
 clearAlert()
-
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 10 - 6
components/Layout/AlertBar.vue

@@ -11,19 +11,23 @@ Contient les différentes barres d'alertes qui s'affichent dans certains cas
     <LayoutAlertBarSwitchUser />
 
     <client-only>
-      <LayoutAlertBarCotisation v-if="organizationProfile.isCmf && ability.can('manage', 'cotisation')" />
+      <LayoutAlertBarCotisation
+        v-if="organizationProfile.isCmf && ability.can('manage', 'cotisation')"
+      />
     </client-only>
 
     <LayoutAlertBarSwitchYear />
     <LayoutAlertBarSuperAdmin />
-    <LayoutAlertBarRegistrationStatus v-if="organizationProfile.hasModule('IEL')" />
+    <LayoutAlertBarRegistrationStatus
+      v-if="organizationProfile.hasModule('IEL')"
+    />
   </main>
 </template>
 
 <script setup lang="ts">
-  import {useOrganizationProfileStore} from "~/stores/organizationProfile";
-  import {useAbility} from "@casl/vue";
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import { useAbility } from '@casl/vue'
 
-  const organizationProfile = useOrganizationProfileStore()
-  const ability = useAbility()
+const organizationProfile = useOrganizationProfileStore()
+const ability = useAbility()
 </script>

+ 43 - 24
components/Layout/AlertBar/Cotisation.vue

@@ -7,22 +7,22 @@ Barre d'alerte qui s'affiche pour donner l'état de la cotisation
 <template>
   <main>
     <UiSystemBar
-        v-if="alert && alert.text && alert.callback"
-        :text="$t(alert.text)"
-        icon="fas fa-info-circle"
-        :on-click="alert.callback"
-        class="theme-info"
+      v-if="alert && alert.text && alert.callback"
+      :text="$t(alert.text)"
+      icon="fas fa-info-circle"
+      :on-click="alert.callback"
+      class="theme-info"
     />
   </main>
 </template>
 
 <script setup lang="ts">
-import {useOrganizationProfileStore} from "~/stores/organizationProfile";
-import type {Ref} from "vue";
-import UrlUtils from "~/services/utils/urlUtils";
-import {ALERT_STATE_COTISATION} from "~/types/enum/enums";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import Cotisation from "~/models/Organization/Cotisation";
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import type { Ref } from 'vue'
+import UrlUtils from '~/services/utils/urlUtils'
+import { ALERT_STATE_COTISATION } from '~/types/enum/enums'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import Cotisation from '~/models/Organization/Cotisation'
 
 const organizationProfile = useOrganizationProfileStore()
 
@@ -38,7 +38,12 @@ const goToCotisation = () => {
   if (!organizationProfile.id) {
     throw new Error('missing organization id')
   }
-  window.location.href = UrlUtils.join(baseLegacyUrl, '/cotisation/cotisation_steps', organizationProfile.id, 'steps/1')
+  window.location.href = UrlUtils.join(
+    baseLegacyUrl,
+    '/cotisation/cotisation_steps',
+    organizationProfile.id,
+    'steps/1',
+  )
 }
 
 /**
@@ -48,21 +53,30 @@ const openInvoiceWindow = () => {
   if (!cotisationYear.value) {
     throw new Error('no cotisation year defined')
   }
-  window.open(UrlUtils.join(baseLegacyUrl, 'cotisation/invoice', cotisationYear.value), '_blank')
+  window.open(
+    UrlUtils.join(baseLegacyUrl, 'cotisation/invoice', cotisationYear.value),
+    '_blank',
+  )
 }
 
 /**
  * Redirige l'utilisateur vers la page des assurances
  */
 const goToInsurancePage = () => {
-  window.location.href = UrlUtils.join(baseLegacyUrl, 'cotisation/insuranceedit')
+  window.location.href = UrlUtils.join(
+    baseLegacyUrl,
+    'cotisation/insuranceedit',
+  )
 }
 
 /**
  * Redirige (dans un nouvel onglet) l'utilsateur vers le site web de la CMF
  */
 const openCmfSubscriptionPage = () => {
-  window.open('https://www.cmf-musique.org/services/assurances/assurance-de-groupe/', '_blank')
+  window.open(
+    'https://www.cmf-musique.org/services/assurances/assurance-de-groupe/',
+    '_blank',
+  )
 }
 
 // On récupère l'état des cotisations via l'API
@@ -71,7 +85,10 @@ if (!organizationProfile.id) {
 }
 
 const { fetch } = useEntityFetch()
-const { data: cotisation, pending } = await fetch(Cotisation, organizationProfile.id)
+const { data: cotisation, pending } = await fetch(
+  Cotisation,
+  organizationProfile.id,
+)
 
 interface Alert {
   text: string
@@ -86,10 +103,13 @@ const alert: ComputedRef<Alert | null> = computed(() => {
   cotisationYear.value = cotisation.value.cotisationYear
 
   const mapping: Record<ALERT_STATE_COTISATION, Alert> = {
-    'AFFILIATION': { text: 'cotisation_access', callback: goToCotisation },
-    'INVOICE': { text: 'upload_cotisation_invoice', callback: openInvoiceWindow },
-    'INSURANCE': { text: 'renew_insurance_cmf', callback: goToInsurancePage },
-    'ADVERTISINGINSURANCE': { text: 'insurance_cmf_subscription', callback: openCmfSubscriptionPage },
+    AFFILIATION: { text: 'cotisation_access', callback: goToCotisation },
+    INVOICE: { text: 'upload_cotisation_invoice', callback: openInvoiceWindow },
+    INSURANCE: { text: 'renew_insurance_cmf', callback: goToInsurancePage },
+    ADVERTISINGINSURANCE: {
+      text: 'insurance_cmf_subscription',
+      callback: openCmfSubscriptionPage,
+    },
   }
 
   if (!cotisation.value.alertState) {
@@ -98,11 +118,10 @@ const alert: ComputedRef<Alert | null> = computed(() => {
 
   return mapping[cotisation.value.alertState as ALERT_STATE_COTISATION]
 })
-
 </script>
 
 <style scoped lang="scss">
-  :deep(.clickable:hover) {
-    text-decoration: none !important;
-  }
+:deep(.clickable:hover) {
+  text-decoration: none !important;
+}
 </style>

+ 7 - 7
components/Layout/AlertBar/Env.vue

@@ -6,16 +6,16 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur n'est pas dans un environneme
 
 <template>
   <UiSystemBar
-      v-if="show"
-      :text="$t('not_production_environment', { env: env })"
-      icon="fas fa-exclamation-triangle"
-      class="theme-warning"
+    v-if="show"
+    :text="$t('not_production_environment', { env: env })"
+    icon="fas fa-exclamation-triangle"
+    class="theme-warning"
   />
 </template>
 
 <script setup lang="ts">
-  const runtimeConfig = useRuntimeConfig()
+const runtimeConfig = useRuntimeConfig()
 
-  const env = runtimeConfig.public.env ?? 'unknown'
-  const show = env !== 'production'
+const env = runtimeConfig.public.env ?? 'unknown'
+const show = env !== 'production'
 </script>

+ 17 - 16
components/Layout/AlertBar/OnlineRegistration.vue

@@ -5,37 +5,38 @@ 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"
+    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";
+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 { data: registrationAvailability, pending } = fetch(
+  RegistrationAvailability,
+  accessProfile.id ?? 0,
+)
 
 const show: ComputedRef<boolean> = computed(() => {
-  return !pending && (registrationAvailability.value as RegistrationAvailability).available
+  return (
+    !pending &&
+    (registrationAvailability.value as RegistrationAvailability).available
+  )
 })
 
 const message: ComputedRef<string> = computed(() => {
   return (registrationAvailability.value as RegistrationAvailability).message
 })
-
-
 </script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 17 - 16
components/Layout/AlertBar/RegistrationStatus.vue

@@ -5,30 +5,33 @@ Barre d'alerte quand au statut (l'avancement) de l'inscription en ligne de l'uti
 
 <template>
   <UiSystemBar
-      v-if="!pending && message"
-      :text="$t(message)"
-      icon="fas fa-id-card"
-      class="theme-secondary"
+    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 type {ComputedRef} from "@vue/reactivity";
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import RegistrationStatus from '~/models/OnlineRegistration/RegistrationStatus'
+import type { ComputedRef } from '@vue/reactivity'
 
 const { fetch } = useEntityFetch()
 
 const accessProfile = useAccessProfileStore()
 
-const { data: registrationStatus, pending } = fetch(RegistrationStatus, accessProfile.id ?? 0)
+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",
+  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(() => {
@@ -40,6 +43,4 @@ const message: ComputedRef<string> = computed(() => {
 })
 </script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 34 - 26
components/Layout/AlertBar/SuperAdmin.vue

@@ -13,33 +13,41 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur est un super admin en mode sw
 </template>
 
 <script setup lang="ts">
-  import {useAccessProfileStore} from "~/stores/accessProfile";
-  import UrlUtils from "~/services/utils/urlUtils";
-  import type {ComputedRef} from "@vue/reactivity";
-  import {navigateTo} from "#app";
-  import {useAdminUrl} from "~/composables/utils/useAdminUrl";
-
-  const { makeAdminUrl } = useAdminUrl()
-
-  const accessProfile = useAccessProfileStore()
-
-  const show: ComputedRef<boolean> = computed(() =>
-      accessProfile.originalAccess !== null && accessProfile.originalAccess.isSuperAdminAccess
-  )
-
-  const url: ComputedRef<string> = computed(() => {
-    const orgId = accessProfile.originalAccess ? accessProfile.originalAccess.organization.id : null
-    const originalAccessId = accessProfile.originalAccess ? accessProfile.originalAccess.id : null
-
-    if (show && orgId && originalAccessId) {
-      return makeAdminUrl(UrlUtils.join('#', 'switch_user', orgId, originalAccessId, 'exit'))
-    }
-    return ''
-  })
-
-  const onClick = () => {
-    navigateTo(url.value,{ external: true })
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import UrlUtils from '~/services/utils/urlUtils'
+import type { ComputedRef } from '@vue/reactivity'
+import { navigateTo } from '#app'
+import { useAdminUrl } from '~/composables/utils/useAdminUrl'
+
+const { makeAdminUrl } = useAdminUrl()
+
+const accessProfile = useAccessProfileStore()
+
+const show: ComputedRef<boolean> = computed(
+  () =>
+    accessProfile.originalAccess !== null &&
+    accessProfile.originalAccess.isSuperAdminAccess,
+)
+
+const url: ComputedRef<string> = computed(() => {
+  const orgId = accessProfile.originalAccess
+    ? accessProfile.originalAccess.organization.id
+    : null
+  const originalAccessId = accessProfile.originalAccess
+    ? accessProfile.originalAccess.id
+    : null
+
+  if (show && orgId && originalAccessId) {
+    return makeAdminUrl(
+      UrlUtils.join('#', 'switch_user', orgId, originalAccessId, 'exit'),
+    )
   }
+  return ''
+})
+
+const onClick = () => {
+  navigateTo(url.value, { external: true })
+}
 </script>
 
 <style scoped lang="scss">

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

@@ -8,23 +8,26 @@ Barre qui s'affiche lorsque l'utilisateur possède un compte multi user
   <UiSystemBar v-if="show" class="theme-info">
     <v-icon small icon="fas fa-info-circle" />
     <span>
-      {{ $t('multi_account_alert_part1') }} <strong>{{ fullName }}</strong> {{ $t('multi_account_alert_part2') }}
+      {{ $t('multi_account_alert_part1') }} <strong>{{ fullName }}</strong>
+      {{ $t('multi_account_alert_part2') }}
     </span>
 
-    <v-icon class="pl-1" small icon="fa fa-users"/> &nbsp;{{$t('multi_account_alert_part3')}}
+    <v-icon class="pl-1" small icon="fa fa-users" /> &nbsp;{{
+      $t('multi_account_alert_part3')
+    }}
   </UiSystemBar>
 </template>
 
 <script setup lang="ts">
-  import {useAccessProfileStore} from "~/stores/accessProfile";
-  import {useMenu} from "~/composables/layout/useMenu";
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { useMenu } from '~/composables/layout/useMenu'
 
-  const accessProfile = useAccessProfileStore()
-  const { hasMenu } = useMenu()
+const accessProfile = useAccessProfileStore()
+const { hasMenu } = useMenu()
 
-  const show = computed(() => hasMenu('MyFamily'))
+const show = computed(() => hasMenu('MyFamily'))
 
-  const fullName = `${accessProfile.givenName} ${accessProfile.name}`
+const fullName = `${accessProfile.givenName} ${accessProfile.name}`
 </script>
 
 <style scoped lang="scss">

+ 50 - 43
components/Layout/AlertBar/SwitchYear.vue

@@ -7,63 +7,70 @@ 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 flex-column">
-    {{$t('not_current_year')}}
+    {{ $t('not_current_year') }}
 
-    <a @click="resetYear" class="text-decoration-none on-warning" style="cursor: pointer;">
+    <a
+      @click="resetYear"
+      class="text-decoration-none on-warning"
+      style="cursor: pointer"
+    >
       <strong class="pl-2 text-neutral-strong">
-        {{$t('not_current_year_reset')}}
+        {{ $t('not_current_year_reset') }}
       </strong>
     </a>
   </UiSystemBar>
 </template>
 
 <script setup lang="ts">
-  import {useAccessProfileStore} from "~/stores/accessProfile";
-  import {useOrganizationProfileStore} from "~/stores/organizationProfile";
-  import type {ComputedRef} from "@vue/reactivity";
-  import {useFormStore} from "~/stores/form";
-  import Access from "~/models/Access/Access";
-  import {usePageStore} from "~/stores/page";
-  import {useEntityManager} from "~/composables/data/useEntityManager";
-  import {useRefreshProfile} from "~/composables/data/useRefreshProfile";
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import type { ComputedRef } from '@vue/reactivity'
+import { useFormStore } from '~/stores/form'
+import Access from '~/models/Access/Access'
+import { usePageStore } from '~/stores/page'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
 
-  const { em } = useEntityManager()
-  const accessProfile = useAccessProfileStore()
-  const organizationProfile = useOrganizationProfileStore()
-  const { setDirty } = useFormStore()
-  const pageStore = usePageStore()
-  const { refreshProfile } = useRefreshProfile()
+const { em } = useEntityManager()
+const accessProfile = useAccessProfileStore()
+const organizationProfile = useOrganizationProfileStore()
+const { setDirty } = useFormStore()
+const pageStore = usePageStore()
+const { refreshProfile } = useRefreshProfile()
 
-  const show: ComputedRef<boolean> = computed(() => {
-    return (
-        accessProfile.historical.past || accessProfile.historical.future ||
-        (accessProfile.historical.dateStart && accessProfile.historical.dateStart.length > 0) ||
-        (accessProfile.historical.dateEnd && accessProfile.historical.dateEnd.length > 0) ||
-        accessProfile.activityYear !== organizationProfile.currentActivityYear
-    )
-  })
+const show: ComputedRef<boolean> = computed(() => {
+  return (
+    accessProfile.historical.past ||
+    accessProfile.historical.future ||
+    (accessProfile.historical.dateStart &&
+      accessProfile.historical.dateStart.length > 0) ||
+    (accessProfile.historical.dateEnd &&
+      accessProfile.historical.dateEnd.length > 0) ||
+    accessProfile.activityYear !== organizationProfile.currentActivityYear
+  )
+})
 
-  const resetYear = async () => {
-      const defaultValues = {
-        historical: {
-            "future": false,
-            "past": false,
-            "present": true,
-        },
-        activityYear: organizationProfile.currentActivityYear
-      }
+const resetYear = async () => {
+  const defaultValues = {
+    historical: {
+      future: false,
+      past: false,
+      present: true,
+    },
+    activityYear: organizationProfile.currentActivityYear,
+  }
 
-    // Il faut ajouter un patch sur le profile ici
-    setDirty(false)
+  // Il faut ajouter un patch sur le profile ici
+  setDirty(false)
 
-    pageStore.loading = true
-    await em.patch(Access, accessProfile.currentAccessId, defaultValues)
-    if (process.server) {
-        // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
-      await refreshProfile()
-    }
-    window.location.reload()
+  pageStore.loading = true
+  await em.patch(Access, accessProfile.currentAccessId, defaultValues)
+  if (process.server) {
+    // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
+    await refreshProfile()
   }
+  window.location.reload()
+}
 </script>
 
 <style scoped lang="scss">

+ 10 - 10
components/Layout/BannerTop.vue

@@ -18,14 +18,14 @@ Troisième bandeau en partant du haut, contenant entre autre le numéro SIRET de
 </template>
 
 <style scoped>
-  .bannerTopForm{
-    min-height: 100px;
-    margin-top: 10px !important;
-    margin-bottom: 10px !important;
-  }
-  .bannerTopForm > .col{
-    min-height: 100px;
-    padding: 10px;
-    padding-left: 24px;
-  }
+.bannerTopForm {
+  min-height: 100px;
+  margin-top: 10px !important;
+  margin-bottom: 10px !important;
+}
+.bannerTopForm > .col {
+  min-height: 100px;
+  padding: 10px;
+  padding-left: 24px;
+}
 </style>

+ 3 - 3
components/Layout/Container.vue

@@ -12,7 +12,7 @@
 </template>
 
 <style scoped>
-  .container{
-    padding-top: 0;
-  }
+.container {
+  padding-top: 0;
+}
 </style>

+ 42 - 40
components/Layout/Dialog.vue

@@ -7,7 +7,12 @@
     :content-class="contentClass"
   >
     <v-card class="d-flex flex-row">
-      <div :class="'dialog-type flex-column justify-center d-none d-sm-flex theme-' + theme">
+      <div
+        :class="
+          'dialog-type flex-column justify-center d-none d-sm-flex theme-' +
+          theme
+        "
+      >
         <h3 class="d-flex">
           <slot name="dialogType" />
         </h3>
@@ -34,15 +39,14 @@
 </template>
 
 <script setup lang="ts">
-
 const props = defineProps({
   show: {
     type: [Boolean, Object],
-    required: true
+    required: true,
   },
   contentClass: {
     type: String,
-    required: false
+    required: false,
   },
   theme: {
     type: String,
@@ -52,55 +56,53 @@ const props = defineProps({
   maxWidth: {
     type: [Number, String],
     required: false,
-    default: 800
-  }
+    default: 800,
+  },
 })
 
 // @ts-ignore  -> just to avoid the error with the prop's type of v-dialog
 const _show = computed(() => props.show) as boolean
-
-
 </script>
 
 <style lang="scss" scoped>
-  .dialog-title {
-    padding-left: 40px;
+.dialog-title {
+  padding-left: 40px;
+  font-weight: normal;
+}
+
+.dialog-type {
+  width: 60px;
+  min-width: 60px;
+  max-width: 60px;
+  min-height: 120px;
+  padding: 25px 10px;
+
+  h3 {
+    font-size: 25px;
     font-weight: normal;
+    writing-mode: vertical-lr;
+    transform: rotate(-180deg);
   }
+}
 
-  .dialog-type {
-    width: 60px;
-    min-width: 60px;
-    max-width: 60px;
-    min-height: 120px;
-    padding: 25px 10px;
-
-   h3 {
-     font-size: 25px;
-     font-weight: normal;
-     writing-mode: vertical-lr;
-     transform: rotate(-180deg);
-    }
-  }
-
-  .dialog-container {
-    overflow-x: scroll;
-  }
+.dialog-container {
+  overflow-x: scroll;
+}
 
-  .dialog-text-container {
-    max-height: 70vh;
-    overflow: auto;
-  }
+.dialog-text-container {
+  max-height: 70vh;
+  overflow: auto;
+}
 
-  .modal-level-alert {
-    .dialog-type{
-      background: rgb(var(--v-theme-danger, #f56954));
-    }
+.modal-level-alert {
+  .dialog-type {
+    background: rgb(var(--v-theme-danger, #f56954));
   }
+}
 
-  .modal-level-warning {
-    .dialog-type{
-      background: rgb(var(--v-theme-warning, #f39c12));
-    }
+.modal-level-warning {
+  .dialog-type {
+    background: rgb(var(--v-theme-warning, #f39c12));
   }
+}
 </style>

+ 45 - 44
components/Layout/Header.vue

@@ -4,22 +4,15 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
 -->
 
 <template>
-  <v-app-bar
-      order="0"
-      density="compact"
-      class="theme-primary"
-  >
+  <v-app-bar order="0" density="compact" class="theme-primary">
     <template #prepend>
       <v-app-bar-nav-icon
-          v-if="hasMainMenu && layoutStore.name !== 'parameters'"
-          :icon="isMainMenuOpened ? 'mdi:mdi-menu-open' : 'mdi:mdi-menu'"
-          @click="toggleMainMenu"
+        v-if="hasMainMenu && layoutStore.name !== 'parameters'"
+        :icon="isMainMenuOpened ? 'mdi:mdi-menu-open' : 'mdi:mdi-menu'"
+        @click="toggleMainMenu"
       />
       <div v-else-if="hasParametersMenu && layoutStore.name === 'parameters'">
-        <v-app-bar-nav-icon
-          v-if="mdAndUp"
-          icon="fa fa-gear"
-        />
+        <v-app-bar-nav-icon v-if="mdAndUp" icon="fa fa-gear" />
         <v-app-bar-nav-icon
           v-else
           :icon="isParametersMenuOpened ? 'mdi:mdi-menu-open' : 'mdi:mdi-menu'"
@@ -28,11 +21,15 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
       </div>
     </template>
 
-    <v-toolbar-title v-if="mdAndUp" v-text="title"/>
+    <v-toolbar-title v-if="mdAndUp" v-text="title" />
 
-    <LayoutThemeSwitcher v-if="false" /> <!-- En attente validation PO -->
+    <LayoutThemeSwitcher v-if="false" />
+    <!-- En attente validation PO -->
 
-    <LayoutHeaderUniversalCreationCreateButton v-if="showUniversalButton" class="mr-3" />
+    <LayoutHeaderUniversalCreationCreateButton
+      v-if="showUniversalButton"
+      class="mr-3"
+    />
 
     <LayoutHeaderHomeBtn v-if="smAndUp" />
 
@@ -49,29 +46,34 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
     <LayoutHeaderMenu name="Account" color="on-primary" icon="fas fa-sun" />
 
     <a
-        :href="runtimeConfig.supportUrl"
-        class="text-body px-3 py-4 ml-2 theme-secondary text-decoration-none h-100"
-        target="_blank"
+      :href="runtimeConfig.supportUrl"
+      class="text-body px-3 py-4 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>
-      <v-icon icon="fas fa-question-circle" class="d-sm-flex d-md-none" color="on-secondary" />
+      <v-icon
+        icon="fas fa-question-circle"
+        class="d-sm-flex d-md-none"
+        color="on-secondary"
+      />
     </a>
   </v-app-bar>
 </template>
 
 <script setup lang="ts">
-
-import {computed} from "@vue/reactivity";
-import type {ComputedRef} from "@vue/reactivity";
-import {useMenu} from "~/composables/layout/useMenu";
-import {useAbility} from "@casl/vue";
-import {useDisplay} from 'vuetify'
-import {useOrganizationProfileStore} from "~/stores/organizationProfile";
-import {useLayoutStore} from "~/stores/layout";
+import { computed } from '@vue/reactivity'
+import type { ComputedRef } from '@vue/reactivity'
+import { useMenu } from '~/composables/layout/useMenu'
+import { useAbility } from '@casl/vue'
+import { useDisplay } from 'vuetify'
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import { useLayoutStore } from '~/stores/layout'
 
 const organizationProfile = useOrganizationProfileStore()
 const runtimeConfig = useRuntimeConfig()
-const title: ComputedRef<string> = computed(() => organizationProfile.name ?? 'Opentalent')
+const title: ComputedRef<string> = computed(
+  () => organizationProfile.name ?? 'Opentalent',
+)
 
 const { hasMenu, isMenuOpened, toggleMenu } = useMenu()
 
@@ -87,25 +89,24 @@ const toggleParametersMenu = () => toggleMenu('Parameters')
 
 const ability = useAbility()
 const showUniversalButton =
-    ability.can('manage', 'users')
-    || ability.can('manage', 'courses')
-    || ability.can('manage', 'examens')
-    || ability.can('manage', 'educationalprojects')
-    || ability.can('manage', 'events')
-    || ability.can('manage', 'emails')
-    || ability.can('manage', 'mails')
-    || ability.can('manage', 'texto')
-    || ability.can('display', 'message_send_page')
-    || ability.can('manage', 'equipments')
+  ability.can('manage', 'users') ||
+  ability.can('manage', 'courses') ||
+  ability.can('manage', 'examens') ||
+  ability.can('manage', 'educationalprojects') ||
+  ability.can('manage', 'events') ||
+  ability.can('manage', 'emails') ||
+  ability.can('manage', 'mails') ||
+  ability.can('manage', 'texto') ||
+  ability.can('display', 'message_send_page') ||
+  ability.can('manage', 'equipments')
 
 const layoutStore = useLayoutStore()
-
 </script>
 
 <style scoped>
-  .help {
-    padding: 14px 14px 13px;
-    font-size: 14px;
-    text-decoration: none;
-  }
+.help {
+  padding: 14px 14px 13px;
+  font-size: 14px;
+  text-decoration: none;
+}
 </style>

+ 11 - 13
components/Layout/Header/HomeBtn.vue

@@ -1,26 +1,24 @@
 <template>
   <div>
     <v-btn
-        ref="btn"
-        icon="fas fa-home"
-        size="small"
-        :href="homeUrl"
-        class="on-primary"
+      ref="btn"
+      icon="fas fa-home"
+      size="small"
+      :href="homeUrl"
+      class="on-primary"
     />
     <v-tooltip :activator="btn" :text="$t('welcome')" location="bottom" />
   </div>
 </template>
 
 <script setup lang="ts">
-  import {ref} from "@vue/reactivity";
-  import {useDisplay} from "vuetify";
-  import {useHomeUrl} from "~/composables/utils/useHomeUrl";
+import { ref } from '@vue/reactivity'
+import { useDisplay } from 'vuetify'
+import { useHomeUrl } from '~/composables/utils/useHomeUrl'
 
-  const { homeUrl } = useHomeUrl()
+const { homeUrl } = useHomeUrl()
 
-  const btn = ref(null);
+const btn = ref(null)
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 57 - 57
components/Layout/Header/Menu.vue

@@ -5,79 +5,79 @@ header principal (configuration, paramètres du compte...)
 
 <template>
   <div v-if="displayMenu">
-
-    <v-btn
-        ref="btn"
-        icon
-        size="small"
-        class="ml-2"
-    >
+    <v-btn ref="btn" icon size="small" class="ml-2">
       <v-avatar
-          v-if="menu.icon.avatarId || menu.icon.avatarByDefault"
-          size="30"
+        v-if="menu.icon.avatarId || menu.icon.avatarByDefault"
+        size="30"
       >
         <UiImage
-            :imageId="menu.icon.avatarId"
-            :defaultImage="menu.icon.avatarByDefault"
-            :width="30"
+          :imageId="menu.icon.avatarId"
+          :defaultImage="menu.icon.avatarByDefault"
+          :width="30"
         />
       </v-avatar>
 
-      <v-icon
-          v-else
-          :icon="menu.icon.name"
-          class="on-primary"
-      />
+      <v-icon v-else :icon="menu.icon.name" class="on-primary" />
     </v-btn>
 
-    <v-tooltip
-        :activator="btn"
-        :text="$t(menu.label)"
-        location="bottom"
-    />
+    <v-tooltip :activator="btn" :text="$t(menu.label)" location="bottom" />
 
     <v-menu
-        :activator="btn"
-        :model-value="isOpened()"
-        @update:modelValue="onStateUpdated"
+      :activator="btn"
+      :model-value="isOpened()"
+      @update:modelValue="onStateUpdated"
     >
       <v-card>
         <v-card-title class="theme-neutral text-body-2 font-weight-bold">
-          {{$t(menu.label)}}
+          {{ $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"
+                :id="child.label"
+                :href="!isInternalLink(child) ? child.to : undefined"
+                :to="isInternalLink(child) ? child.to : undefined"
               >
                 <span v-if="child.icon" class="pr-2 d-flex align-center">
-                  <v-avatar v-if="menu.icon.avatarId || child.icon.avatarByDefault" size="30" >
-                    <UiImage :imageId="child.icon.avatarId" :defaultImage="child.icon.avatarByDefault" :width="30" />
+                  <v-avatar
+                    v-if="menu.icon.avatarId || child.icon.avatarByDefault"
+                    size="30"
+                  >
+                    <UiImage
+                      :imageId="child.icon.avatarId"
+                      :defaultImage="child.icon.avatarByDefault"
+                      :width="30"
+                    />
                   </v-avatar>
                   <v-icon v-else class="on-primary" size="small">
                     {{ child.icon.name }}
                   </v-icon>
                 </span>
 
-                <span>{{ translateLabel ? $t(child.label) : child.label }}</span>
+                <span>{{
+                  translateLabel ? $t(child.label) : child.label
+                }}</span>
               </v-list-item>
-
             </template>
           </v-list>
         </v-card-text>
 
-        <v-card-actions v-if="menu.actions.length > 0" class="ma-0 pa-0 theme-primary">
+        <v-card-actions
+          v-if="menu.actions.length > 0"
+          class="ma-0 pa-0 theme-primary"
+        >
           <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"
+              :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-title
+                class="text-body-2"
+                v-text="$t(action.label)"
+              />
             </v-list-item>
           </template>
         </v-card-actions>
@@ -87,22 +87,23 @@ header principal (configuration, paramètres du compte...)
 </template>
 
 <script setup lang="ts">
-import {useMenu} from "~/composables/layout/useMenu";
-import {computed, ref} from "@vue/reactivity";
+import { useMenu } from '~/composables/layout/useMenu'
+import { computed, ref } from '@vue/reactivity'
 
 const props = defineProps({
   name: {
     type: String,
-    required: true
+    required: true,
   },
   translateLabel: {
     type: Boolean,
     required: false,
-    default: true
-  }
+    default: true,
+  },
 })
 
-const { getMenu, isInternalLink, hasMenu, setMenuState, isMenuOpened } = useMenu()
+const { getMenu, isInternalLink, hasMenu, setMenuState, isMenuOpened } =
+  useMenu()
 
 const menu = getMenu(props.name)
 const displayMenu = computed(() => hasMenu(props.name))
@@ -113,23 +114,22 @@ const onStateUpdated = (e: any) => {
 }
 
 const btn = ref(null)
-
 </script>
 
 <style scoped lang="scss">
-  :deep(.v-btn .v-icon) {
-    font-size: 1rem !important;
-  }
+:deep(.v-btn .v-icon) {
+  font-size: 1rem !important;
+}
 
-  .v-list {
-    padding: 0;
-  }
+.v-list {
+  padding: 0;
+}
 
-  .v-list-item {
-    width: 100%;
-  }
+.v-list-item {
+  width: 100%;
+}
 
-  .header-menu .v-list .v-list-item:last-child {
-    border-bottom: none;
-  }
+.header-menu .v-list .v-list-item:last-child {
+  border-bottom: none;
+}
 </style>

+ 90 - 85
components/Layout/Header/Notification.vue

@@ -1,19 +1,13 @@
 <template>
-  <v-btn
-      ref="btn"
-      icon
-      size="small"
-      class="ml-2"
-  >
+  <v-btn ref="btn" icon size="small" class="ml-2">
     <v-badge
-        color="warning"
-        offset-x="-4"
-        offset-y="17"
-        :model-value="unreadNotification.length > 0"
-        :content="unreadNotification.length">
-      <v-icon class="on-primary">
-        fa fa-bell
-      </v-icon>
+      color="warning"
+      offset-x="-4"
+      offset-y="17"
+      :model-value="unreadNotification.length > 0"
+      :content="unreadNotification.length"
+    >
+      <v-icon class="on-primary"> fa fa-bell </v-icon>
     </v-badge>
   </v-btn>
 
@@ -22,10 +16,10 @@
   </v-tooltip>
 
   <v-menu
-      v-if="btn !== null"
-      :activator="btn"
-      v-model="isOpen"
-      location="bottom left"
+    v-if="btn !== null"
+    :activator="btn"
+    v-model="isOpen"
+    location="bottom left"
   >
     <v-card max-width="400">
       <v-card-title class="bg-neutral text-body-2 font-weight-bold">
@@ -35,21 +29,23 @@
       <v-card-text class="ma-0 pa-0 header-menu">
         <v-list density="compact" :subheader="true" class="pa-0">
           <v-list-item
-              v-for="(notification, index) in notifications"
-              :key="index"
-              :class="'list_item py-3' + `${notification.notificationUsers.length === 0 ? ' unread' : ''}`"
+            v-for="(notification, index) in notifications"
+            :key="index"
+            :class="
+              'list_item py-3' +
+              `${notification.notificationUsers.length === 0 ? ' unread' : ''}`
+            "
           >
             <span class="">{{ getMessage(notification) }}</span>
 
             <template #append>
               <v-icon
-                  v-if="notification.link"
-                  icon="mdi:mdi-download"
-                  @click="download(notification.link)"
-                  class="pt-4"
+                v-if="notification.link"
+                icon="mdi:mdi-download"
+                @click="download(notification.link)"
+                class="pt-4"
               />
             </template>
-
           </v-list-item>
 
           <v-divider></v-divider>
@@ -58,19 +54,14 @@
           <span v-intersect="onLastNotificationIntersect" />
 
           <v-row
-              v-if="pending"
-              class="fill-height mt-3 mb-3"
-              align="center"
-              justify="center"
+            v-if="pending"
+            class="fill-height mt-3 mb-3"
+            align="center"
+            justify="center"
           >
-            <v-progress-circular
-                indeterminate
-                color="neutral"
-            />
+            <v-progress-circular indeterminate color="neutral" />
           </v-row>
-
         </v-list>
-
       </v-card-text>
 
       <v-card-actions class="ma-0 pa-0">
@@ -79,11 +70,11 @@
           :href="notificationUrl"
           router
           class="theme-primary"
-          style="width: 100%; height: 52px;"
+          style="width: 100%; height: 52px"
         >
           <v-list-item-title
-              class="text-body-2"
-              v-text="$t('all_notification')"
+            class="text-body-2"
+            v-text="$t('all_notification')"
           />
         </v-list-item>
       </v-card-actions>
@@ -92,18 +83,18 @@
 </template>
 
 <script setup lang="ts">
-import {NOTIFICATION_TYPE} from "~/types/enum/enums";
-import Notification from "~/models/Core/Notification";
-import NotificationUsers from "~/models/Core/NotificationUsers";
-import {useAccessProfileStore} from "~/stores/accessProfile";
-import {computed, ref} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import type {AnyJson, Pagination} from "~/types/data";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import UrlUtils from "~/services/utils/urlUtils";
-import {useRepo} from "pinia-orm";
-import NotificationRepository from "~/stores/repositories/NotificationRepository";
+import { NOTIFICATION_TYPE } from '~/types/enum/enums'
+import Notification from '~/models/Core/Notification'
+import NotificationUsers from '~/models/Core/NotificationUsers'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { computed, ref } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import type { AnyJson, Pagination } from '~/types/data'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import UrlUtils from '~/services/utils/urlUtils'
+import { useRepo } from 'pinia-orm'
+import NotificationRepository from '~/stores/repositories/NotificationRepository'
 
 const accessProfileStore = useAccessProfileStore()
 
@@ -121,10 +112,14 @@ const { fetchCollection } = useEntityFetch()
 const notificationRepo = useRepo(NotificationRepository)
 
 const query: ComputedRef<AnyJson> = computed(() => {
-  return { 'page': page.value }
+  return { page: page.value }
 })
 
-let { data: collection, pending, refresh } = await fetchCollection(Notification, null, query)
+let {
+  data: collection,
+  pending,
+  refresh,
+} = await fetchCollection(Notification, null, query)
 
 /**
  * On récupère les Notifications via le store (sans ça, les mises à jour SSE ne seront pas prises en compte)
@@ -144,10 +139,15 @@ const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
  * Les metadata dépendront de la dernière valeur du GET lancé
  */
 const pagination: ComputedRef<Pagination> = computed(() => {
-  return (!pending.value && 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
@@ -165,10 +165,10 @@ const onLastNotificationIntersect = (isIntersecting: boolean) => {
  */
 const update = async () => {
   if (
-      !pending.value &&
-      pagination.value &&
-      pagination.value.next &&
-      pagination.value.next > 0
+    !pending.value &&
+    pagination.value &&
+    pagination.value.next &&
+    pagination.value.next > 0
   ) {
     pending.value = true
     page.value = pagination.value.next
@@ -185,20 +185,27 @@ const update = async () => {
  * @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')}`
+  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
+          ?.fileName} ${i18n.t('is_ready_to_be')} ${
+          notification.message.action
+        }`
 
-      return `${i18n.t('your_message')} ${notification.message?.about ?? ''} ${i18n.t('has_been_sent')} `
+      return `${i18n.t('your_message')} ${
+        notification.message?.about ?? ''
+      } ${i18n.t('has_been_sent')} `
 
-    case NOTIFICATION_TYPE.SYSTEM :
+    case NOTIFICATION_TYPE.SYSTEM:
       if (notification.message?.about)
         return `${i18n.t(notification.message.about)}`
-      break;
+      break
 
     default:
       return i18n.t(notification.name)
@@ -209,7 +216,7 @@ const getMessage = (notification: Notification) => {
  * Dès la fermeture du menu, on indique que les notifications non lues, le sont.
  */
 const unwatch = watch(isOpen, (newValue, oldValue) => {
-  if (!newValue){
+  if (!newValue) {
     markNotificationsAsRead()
   }
 })
@@ -225,9 +232,9 @@ const markNotificationAsRead = (notification: Notification) => {
     throw new Error('Current access id is null')
   }
   const notificationUsers = em.newInstance(NotificationUsers, {
-    access:`/api/accesses/${accessProfileStore.currentAccessId}`,
-    notification:`/api/notifications/${notification.id}`,
-    isRead: true
+    access: `/api/accesses/${accessProfileStore.currentAccessId}`,
+    notification: `/api/notifications/${notification.id}`,
+    isRead: true,
   })
 
   em.persist(NotificationUsers, notificationUsers)
@@ -254,29 +261,27 @@ const download = (link: string) => {
   }
   // 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];
+  const path: string = link.split('/api')[1]
 
   // 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
+    runtimeConfig.baseUrlLegacy,
+    'api',
+    String(accessProfileStore.id),
+    String(accessProfileStore.switchId || ''),
+    path,
   )
 
-  window.open(url);
+  window.open(url)
 }
-
-
 </script>
 
 <style scoped lang="scss">
-  .list_item{
-    white-space: normal;
-  }
-  .unread{
-    background: rgb(var(--v-theme-neutral-soft, white));
-  }
+.list_item {
+  white-space: normal;
+}
+.unread {
+  background: rgb(var(--v-theme-neutral-soft, white));
+}
 </style>

+ 89 - 89
components/Layout/Header/UniversalCreation/Card.vue

@@ -19,15 +19,15 @@
     <v-row :no-gutters="true" style="height: 100px">
       <v-col cols="3" class="flex-grow-0 flex-shrink-0 d-flex justify-center">
         <v-icon
-            :icon="icon"
-            size="50"
-            class="ma-2 pa-2 align-self-center text-neutral-strong"
+          :icon="icon"
+          size="50"
+          class="ma-2 pa-2 align-self-center text-neutral-strong"
         />
       </v-col>
       <v-col
-          cols="9"
-          align-self="center"
-          class="pl-2 infos-container flex-grow-1 flex-shrink-1"
+        cols="9"
+        align-self="center"
+        class="pl-2 infos-container flex-grow-1 flex-shrink-1"
       >
         <h4 class="text-primary">{{ $t(title) }}</h4>
         <p class="text-neutral-strong">
@@ -39,100 +39,100 @@
 </template>
 
 <script setup lang="ts">
-  import type {PropType} from "@vue/runtime-core";
-  import {MENU_LINK_TYPE} from "~/types/enum/layout";
-  import {useAdminUrl} from "~/composables/utils/useAdminUrl";
-  import UrlUtils from "~/services/utils/urlUtils";
+import type { PropType } from '@vue/runtime-core'
+import { MENU_LINK_TYPE } from '~/types/enum/layout'
+import { useAdminUrl } from '~/composables/utils/useAdminUrl'
+import UrlUtils from '~/services/utils/urlUtils'
 
-  const props = defineProps({
-    /**
-     * Target location in the wizard
-     */
-    to: {
-      type: String,
-      required: false,
-      default: null
-    },
-    /**
-     * Target url
-     */
-    href: {
-      type: String,
-      required: false,
-      default: null
-    },
-    /**
-     * Target url
-     */
-    linkType: {
-      type: Number as PropType<MENU_LINK_TYPE>,
-      required: false,
-      default: MENU_LINK_TYPE.V1
-    },
-    /**
-     * Title displayed on the card
-     */
-    title: {
-      type: String,
-      required: true
-    },
-    /**
-     * Description displayed on the card
-     */
-    textContent: {
-      type: String,
-      required: true
-    },
-    /**
-     * Icon displayed on the card
-     */
-    icon: {
-      type: String,
-      required: true
-    }
-  })
+const props = defineProps({
+  /**
+   * Target location in the wizard
+   */
+  to: {
+    type: String,
+    required: false,
+    default: null,
+  },
+  /**
+   * Target url
+   */
+  href: {
+    type: String,
+    required: false,
+    default: null,
+  },
+  /**
+   * Target url
+   */
+  linkType: {
+    type: Number as PropType<MENU_LINK_TYPE>,
+    required: false,
+    default: MENU_LINK_TYPE.V1,
+  },
+  /**
+   * Title displayed on the card
+   */
+  title: {
+    type: String,
+    required: true,
+  },
+  /**
+   * Description displayed on the card
+   */
+  textContent: {
+    type: String,
+    required: true,
+  },
+  /**
+   * Icon displayed on the card
+   */
+  icon: {
+    type: String,
+    required: true,
+  },
+})
 
-  const emit = defineEmits(['click'])
+const emit = defineEmits(['click'])
 
-  const { makeAdminUrl } = useAdminUrl()
+const { makeAdminUrl } = useAdminUrl()
 
-  let url: string | null = null;
+let url: string | null = null
 
-  if (props.href !== null) {
-    switch (props.linkType) {
-      case MENU_LINK_TYPE.V1:
-        url = makeAdminUrl(props.href)
-        break;
-      case MENU_LINK_TYPE.EXTERNAL:
-        url = UrlUtils.prependHttps(props.href)
-        break;
-      default:
-        url = props.href
-    }
+if (props.href !== null) {
+  switch (props.linkType) {
+    case MENU_LINK_TYPE.V1:
+      url = makeAdminUrl(props.href)
+      break
+    case MENU_LINK_TYPE.EXTERNAL:
+      url = UrlUtils.prependHttps(props.href)
+      break
+    default:
+      url = props.href
   }
+}
 
-  const onClick = () => {
-    emit('click', props.to, url)
-  }
+const onClick = () => {
+  emit('click', props.to, url)
+}
 </script>
 
 <style lang="scss" scoped>
-  h4 {
-    font-size: 15px;
-    font-weight: bold;
-    margin-bottom: 6px;
-  }
+h4 {
+  font-size: 15px;
+  font-weight: bold;
+  margin-bottom: 6px;
+}
 
-  p {
-    font-size: 13px;
-  }
+p {
+  font-size: 13px;
+}
 
-  .infos-container {
-    padding: 15px 0;
-  }
+.infos-container {
+  padding: 15px 0;
+}
 
-  .v-card:hover {
-    cursor: pointer;
-    background: rgb(var(--v-theme-primary-alt));
-  }
+.v-card:hover {
+  cursor: pointer;
+  background: rgb(var(--v-theme-primary-alt));
+}
 </style>

+ 153 - 137
components/Layout/Header/UniversalCreation/CreateButton.vue

@@ -5,56 +5,74 @@
 <template>
   <main>
     <v-btn
-        v-if="asIcon"
-        :elevation="0"
-        class="theme-primary"
-        :icon="true"
-        size="small"
-        @click="show"
+      v-if="asIcon"
+      :elevation="0"
+      class="theme-primary"
+      :icon="true"
+      size="small"
+      @click="show"
     >
       <v-icon>fas fa-plus</v-icon>
     </v-btn>
 
     <v-btn
-        v-else
-        :elevation="2"
-        height="30"
-        class="theme-x-create-btn"
-        @click="show"
+      v-else
+      :elevation="2"
+      height="30"
+      class="theme-x-create-btn"
+      @click="show"
     >
       <span>{{ $t('create') }}</span>
     </v-btn>
 
-    <LayoutDialog :show="showCreateDialog" :max-width="850" >
+    <LayoutDialog :show="showCreateDialog" :max-width="850">
       <template #dialogType>{{ $t('creative_assistant') }}</template>
 
       <template #dialogTitle>
-        <span v-if="location === 'home'">{{ $t('what_do_you_want_to_create') }}</span>
-        <span v-else-if="location === 'access'">{{ $t('what_type_of_contact_do_you_want_to_create') }}</span>
-        <span v-else-if="location === 'event'">{{ $t('what_do_you_want_to_add_to_your_planning') }}</span>
-        <span v-else-if="location === 'message'">{{ $t('what_do_you_want_to_send') }}</span>
-        <span v-else-if="location === 'event-params'">{{ $t('which_date_and_which_hour') }}</span>
+        <span v-if="location === 'home'">{{
+          $t('what_do_you_want_to_create')
+        }}</span>
+        <span v-else-if="location === 'access'">{{
+          $t('what_type_of_contact_do_you_want_to_create')
+        }}</span>
+        <span v-else-if="location === 'event'">{{
+          $t('what_do_you_want_to_add_to_your_planning')
+        }}</span>
+        <span v-else-if="location === 'message'">{{
+          $t('what_do_you_want_to_send')
+        }}</span>
+        <span v-else-if="location === 'event-params'">{{
+          $t('which_date_and_which_hour')
+        }}</span>
       </template>
 
       <template #dialogText>
-         <LayoutHeaderUniversalCreationGenerateCardsSteps
-             :path="path"
-             @cardClick="onCardClick"
-             @urlUpdate="onUrlUpdate"
-         />
+        <LayoutHeaderUniversalCreationGenerateCardsSteps
+          :path="path"
+          @cardClick="onCardClick"
+          @urlUpdate="onUrlUpdate"
+        />
       </template>
 
       <template #dialogBtn>
         <div class="text-center">
-          <v-btn class="theme-neutral-soft" @click="close" >
+          <v-btn class="theme-neutral-soft" @click="close">
             {{ $t('cancel') }}
           </v-btn>
 
-          <v-btn v-if="path.length > 1" class="theme-neutral-soft" @click="goToPrevious" >
+          <v-btn
+            v-if="path.length > 1"
+            class="theme-neutral-soft"
+            @click="goToPrevious"
+          >
             {{ $t('previous_step') }}
           </v-btn>
 
-          <v-btn v-if="targetUrl !== null && !directRedirectionOngoing" class="theme-primary" @click="validate" >
+          <v-btn
+            v-if="targetUrl !== null && !directRedirectionOngoing"
+            class="theme-primary"
+            @click="validate"
+          >
             {{ $t('validate') }}
           </v-btn>
         </div>
@@ -64,122 +82,120 @@
 </template>
 
 <script setup lang="ts">
-  import {ref} from "@vue/reactivity";
-  import type {Ref} from "@vue/reactivity";
-  import {useDisplay} from "vuetify";
-  import type {ComputedRef} from "vue";
-  import {usePageStore} from "~/stores/page";
-
-  const { mdAndDown: asIcon } = useDisplay()
-
-  // Set to true to show the Create dialog
-  const showCreateDialog: Ref<boolean> = ref(false);
-
-  // The succession of menus the user has been through; used to keep track of the navigation
-  const path: Ref<Array<string>> = ref(['home'])
-
-  // The current menu
-  const location: ComputedRef<string> = computed(() => {
-    return path.value.at(-1) ?? 'home'
-  })
-
-  // The current target URL (@see onUrlUpdate())
-  const targetUrl: Ref<string | null> = ref(null)
-
-  // Already redirecting (to avoid the display of the 'validate' button when page has already been redirected and is loading)
-  const directRedirectionOngoing: Ref<boolean> = ref(false)
-
-  /**
-   * Return to the home menu
-   */
-  const reset = () => {
-    path.value = ['home']
-  }
-
-  /**
-   * Go back to the previous step
-   */
-  const goToPrevious = () => {
-    if (path.value.length === 1) {
-      return
-    }
-    path.value.pop()
-  }
-
-  /**
-   * Display the create dialog
-   */
-  const show = () => {
-    reset()
-    showCreateDialog.value = true
+import { ref } from '@vue/reactivity'
+import type { Ref } from '@vue/reactivity'
+import { useDisplay } from 'vuetify'
+import type { ComputedRef } from 'vue'
+import { usePageStore } from '~/stores/page'
+
+const { mdAndDown: asIcon } = useDisplay()
+
+// Set to true to show the Create dialog
+const showCreateDialog: Ref<boolean> = ref(false)
+
+// The succession of menus the user has been through; used to keep track of the navigation
+const path: Ref<Array<string>> = ref(['home'])
+
+// The current menu
+const location: ComputedRef<string> = computed(() => {
+  return path.value.at(-1) ?? 'home'
+})
+
+// The current target URL (@see onUrlUpdate())
+const targetUrl: Ref<string | null> = ref(null)
+
+// Already redirecting (to avoid the display of the 'validate' button when page has already been redirected and is loading)
+const directRedirectionOngoing: Ref<boolean> = ref(false)
+
+/**
+ * Return to the home menu
+ */
+const reset = () => {
+  path.value = ['home']
+}
+
+/**
+ * Go back to the previous step
+ */
+const goToPrevious = () => {
+  if (path.value.length === 1) {
+    return
   }
-
-  const pageStore = usePageStore()
-
-  /**
-   * Redirect the user to the given url
-   * @param url
-   */
-  const redirect = (url: string) => {
-    pageStore.loading = true
-    window.location.href = url
-  }
-
-  /**
-   * Go to the current targetUrl
-   */
-  const validate = () => {
-    if (targetUrl.value === null) {
-      console.warn('No url defined')
-      return
-    }
-    redirect(targetUrl.value)
+  path.value.pop()
+}
+
+/**
+ * Display the create dialog
+ */
+const show = () => {
+  reset()
+  showCreateDialog.value = true
+}
+
+const pageStore = usePageStore()
+
+/**
+ * Redirect the user to the given url
+ * @param url
+ */
+const redirect = (url: string) => {
+  pageStore.loading = true
+  window.location.href = url
+}
+
+/**
+ * Go to the current targetUrl
+ */
+const validate = () => {
+  if (targetUrl.value === null) {
+    console.warn('No url defined')
+    return
   }
-
-  /**
-   * Close the Create dialog
-   */
-  const close = () => {
-    showCreateDialog.value = false
-  }
-
-  /**
-   * A cart has been clicked. The reaction depends on the card's properties.
-   *
-   * @param to  Target location in the wizard
-   * @param href  Target absolute url
-   */
-  const onCardClick = (to: string | null, href: string | null) => {
-    if (to !== null) {
-      // La carte définit une nouvelle destination : on se dirige vers elle.
-      path.value.push(to)
-
-    } else if (href !== null) {
-      // La carte définit une url avec href, et pas de nouvelle destination : on suit directement le lien pour éviter
-      // l'étape de validation devenue inutile.
-      directRedirectionOngoing.value = true
-      redirect(href)
-
-    } else {
-      console.warn('Error: card has no `to` nor `href` defined')
-    }
-  }
-
-  /**
-   * The url has been updated in the GenerateCardsStep component
-   * @param url
-   */
-  const onUrlUpdate = (url: string) => {
-    targetUrl.value = url
+  redirect(targetUrl.value)
+}
+
+/**
+ * Close the Create dialog
+ */
+const close = () => {
+  showCreateDialog.value = false
+}
+
+/**
+ * A cart has been clicked. The reaction depends on the card's properties.
+ *
+ * @param to  Target location in the wizard
+ * @param href  Target absolute url
+ */
+const onCardClick = (to: string | null, href: string | null) => {
+  if (to !== null) {
+    // La carte définit une nouvelle destination : on se dirige vers elle.
+    path.value.push(to)
+  } else if (href !== null) {
+    // La carte définit une url avec href, et pas de nouvelle destination : on suit directement le lien pour éviter
+    // l'étape de validation devenue inutile.
+    directRedirectionOngoing.value = true
+    redirect(href)
+  } else {
+    console.warn('Error: card has no `to` nor `href` defined')
   }
+}
+
+/**
+ * The url has been updated in the GenerateCardsStep component
+ * @param url
+ */
+const onUrlUpdate = (url: string) => {
+  targetUrl.value = url
+}
 </script>
 
 <style scoped lang="scss">
-  :deep(.v-btn .v-icon) {
-    font-size: 16px !important;
-  }
-  :deep(.v-btn) {
-    text-transform: none !important;
-    font-weight: 600;
-  }
+:deep(.v-btn .v-icon) {
+  font-size: 16px !important;
+}
+:deep(.v-btn) {
+  text-transform: none !important;
+  font-weight: 600;
+}
 </style>

+ 72 - 62
components/Layout/Header/UniversalCreation/EventParams.vue

@@ -17,7 +17,10 @@ Event parameters page in the create dialog
     <v-row v-show="eventStart < now" class="anteriorDateWarning mt-0">
       <v-col cols="2" class="pt-1"></v-col>
       <v-col cols="9" class="pt-1">
-        <i class="fa fa-circle-info" /> {{ $t('please_note_that_this_reservation_start_on_date_anterior_to_now') }}
+        <i class="fa fa-circle-info" />
+        {{
+          $t('please_note_that_this_reservation_start_on_date_anterior_to_now')
+        }}
       </v-col>
     </v-row>
 
@@ -36,7 +39,6 @@ Event parameters page in the create dialog
         <UiInputNumber v-model="eventDurationMinutes" class="mx-3" :min="0" />
         <span>{{ $t('minute(s)') }}</span>
       </v-col>
-
     </v-row>
 
     <v-row>
@@ -52,69 +54,77 @@ Event parameters page in the create dialog
 </template>
 
 <script setup lang="ts">
-  import {ref} from "@vue/reactivity";
-  import type {Ref} from "@vue/reactivity";
-  import {add, format, startOfHour, formatISO} from "date-fns";
-  import type {ComputedRef} from "vue";
-  import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
-
-  const i18n = useI18n()
-
-  // An event is sent each time the resulting params are updated
-  const emit = defineEmits(['paramsUpdated'])
-
-  // Get the start of the next hour as a default event start
-  const now: Date = new Date()
-  const eventStart: Ref<Date> = ref(startOfHour(add(now, { 'hours': 1 })))
-
-  const eventDurationDays: Ref<number> = ref(0)
-  const eventDurationHours: Ref<number> = ref(1)
-  const eventDurationMinutes: Ref<number> = ref(0)
-
-  // Duration of the events, in minutes
-  const eventDuration: ComputedRef<number> = computed(() => {
-    return (eventDurationDays.value * 24 * 60) + (eventDurationHours.value * 60) + eventDurationMinutes.value
+import { ref } from '@vue/reactivity'
+import type { Ref } from '@vue/reactivity'
+import { add, format, startOfHour, formatISO } from 'date-fns'
+import type { ComputedRef } from 'vue'
+import DateUtils, { supportedLocales } from '~/services/utils/dateUtils'
+
+const i18n = useI18n()
+
+// An event is sent each time the resulting params are updated
+const emit = defineEmits(['paramsUpdated'])
+
+// Get the start of the next hour as a default event start
+const now: Date = new Date()
+const eventStart: Ref<Date> = ref(startOfHour(add(now, { hours: 1 })))
+
+const eventDurationDays: Ref<number> = ref(0)
+const eventDurationHours: Ref<number> = ref(1)
+const eventDurationMinutes: Ref<number> = ref(0)
+
+// Duration of the events, in minutes
+const eventDuration: ComputedRef<number> = computed(() => {
+  return (
+    eventDurationDays.value * 24 * 60 +
+    eventDurationHours.value * 60 +
+    eventDurationMinutes.value
+  )
+})
+
+// Event end
+const eventEnd: ComputedRef<Date> = computed(() =>
+  add(eventStart.value, { minutes: eventDuration.value }),
+)
+
+const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
+const formattedEventEnd: ComputedRef<string> = computed(() => {
+  return format(eventEnd.value, 'EEEE dd MMMM yyyy HH:mm', {
+    locale: fnsLocale,
   })
+})
 
-  // Event end
-  const eventEnd: ComputedRef<Date> = computed(() => add(eventStart.value, { 'minutes': eventDuration.value }))
-
-  const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
-  const formattedEventEnd: ComputedRef<string> = computed(() => {
-    return format(eventEnd.value, 'EEEE dd MMMM yyyy HH:mm', {locale: fnsLocale})
-  })
-
-  // Build the event params
-  const params: ComputedRef<{'start': string, 'end': string}> = computed(() => {
-    return {
-      'start': formatISO(eventStart.value),
-      'end': formatISO(eventEnd.value),
-    }
-  })
-
-  // Send an update event as soon as the page is mounted
-  onMounted(() => {
-    emit('paramsUpdated', params.value)
-  })
-
-  // Send an update event every time the params change
-  const unwatch = watch(params, (newParams) => {
-    emit('paramsUpdated', newParams)
-  })
-  onUnmounted(() => {
-    unwatch()
-  })
+// Build the event params
+const params: ComputedRef<{ start: string; end: string }> = computed(() => {
+  return {
+    start: formatISO(eventStart.value),
+    end: formatISO(eventEnd.value),
+  }
+})
+
+// Send an update event as soon as the page is mounted
+onMounted(() => {
+  emit('paramsUpdated', params.value)
+})
+
+// Send an update event every time the params change
+const unwatch = watch(params, (newParams) => {
+  emit('paramsUpdated', newParams)
+})
+onUnmounted(() => {
+  unwatch()
+})
 </script>
 
 <style scoped lang="scss">
-  .endDate {
-    font-weight: 600;
-    text-transform: capitalize;
-    color: rgb(var(--v-theme-on-neutral));
-  }
-
-  .anteriorDateWarning {
-    color: rgb(var(--v-theme-info));
-    font-weight: 600;
-  }
+.endDate {
+  font-weight: 600;
+  text-transform: capitalize;
+  color: rgb(var(--v-theme-on-neutral));
+}
+
+.anteriorDateWarning {
+  color: rgb(var(--v-theme-info));
+  font-weight: 600;
+}
 </style>

+ 190 - 178
components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue

@@ -3,57 +3,67 @@
 -->
 
 <template>
-
   <!-- Menu Accueil -->
   <v-container v-if="location === 'home'">
     <v-row>
-
       <!-- Une personne -->
       <v-col cols="6" v-if="ability.can('manage', 'users')">
-          <LayoutHeaderUniversalCreationCard
-              to="access"
-              title="a_person"
-              text-content="add_new_person_student"
-              icon="fa fa-user"
-              @click="onCardClick"
-          />
+        <LayoutHeaderUniversalCreationCard
+          to="access"
+          title="a_person"
+          text-content="add_new_person_student"
+          icon="fa fa-user"
+          @click="onCardClick"
+        />
       </v-col>
 
       <!-- Un évènement -->
-      <v-col cols="6" v-if="ability.can('display', 'agenda_page')
-                && (
-                   ability.can('display', 'course_page') ||
-                   ability.can('display', 'exam_page') ||
-                   ability.can('display', 'pedagogics_project_page')
-                )">
+      <v-col
+        cols="6"
+        v-if="
+          ability.can('display', 'agenda_page') &&
+          (ability.can('display', 'course_page') ||
+            ability.can('display', 'exam_page') ||
+            ability.can('display', 'pedagogics_project_page'))
+        "
+      >
         <LayoutHeaderUniversalCreationCard
-            to="event"
-            title="an_event"
-            text-content="add_an_event_course"
-            icon="fa fa-calendar-alt"
-            @click="onCardClick"
+          to="event"
+          title="an_event"
+          text-content="add_an_event_course"
+          icon="fa fa-calendar-alt"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Autre évènement -->
-      <v-col cols="6" v-else-if="ability.can('display', 'agenda_page') && ability.can('manage', 'events')">
+      <v-col
+        cols="6"
+        v-else-if="
+          ability.can('display', 'agenda_page') &&
+          ability.can('manage', 'events')
+        "
+      >
         <LayoutHeaderUniversalCreationCard
-            to="event-params"
-            title="other_event"
-            text-content="other_event_text_creation_card"
-            icon="far fa-calendar"
-            href="/calendar/create/events"
-            @click="onCardClick"
+          to="event-params"
+          title="other_event"
+          text-content="other_event_text_creation_card"
+          icon="far fa-calendar"
+          href="/calendar/create/events"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Une correspondance -->
-      <v-col cols="6" v-if="ability.can('display', 'message_send_page')
-                   && (
-                    ability.can('manage', 'emails') ||
-                    ability.can('manage', 'mails') ||
-                    ability.can('manage', 'texto')
-                  )">
+      <v-col
+        cols="6"
+        v-if="
+          ability.can('display', 'message_send_page') &&
+          (ability.can('manage', 'emails') ||
+            ability.can('manage', 'mails') ||
+            ability.can('manage', 'texto'))
+        "
+      >
         <LayoutHeaderUniversalCreationCard
           to="message"
           title="a_correspondence"
@@ -83,99 +93,99 @@
       <!-- Un adhérent -->
       <v-col cols="6" v-if="isLaw1901">
         <LayoutHeaderUniversalCreationCard
-            title="an_adherent"
-            text-content="adherent_text_creation_card"
-            icon="fa fa-user"
-            href="/universal_creation_person/adherent"
-            @click="onCardClick"
+          title="an_adherent"
+          text-content="adherent_text_creation_card"
+          icon="fa fa-user"
+          href="/universal_creation_person/adherent"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un membre du CA -->
       <v-col cols="6" v-if="isLaw1901">
         <LayoutHeaderUniversalCreationCard
-            title="a_ca_member"
-            text-content="ca_member_text_creation_card"
-            icon="fa fa-users"
-            href="/universal_creation_person/ca_member"
-            @click="onCardClick"
+          title="a_ca_member"
+          text-content="ca_member_text_creation_card"
+          icon="fa fa-users"
+          href="/universal_creation_person/ca_member"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un élève -->
       <v-col cols="6">
         <LayoutHeaderUniversalCreationCard
-            title="a_student"
-            text-content="student_text_creation_card"
-            icon="fa fa-user"
-            href="/universal_creation_person/student"
-            @click="onCardClick"
+          title="a_student"
+          text-content="student_text_creation_card"
+          icon="fa fa-user"
+          href="/universal_creation_person/student"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un tuteur -->
       <v-col cols="6">
         <LayoutHeaderUniversalCreationCard
-            title="a_guardian"
-            text-content="guardian_text_creation_card"
-            icon="fa fa-female"
-            href="/universal_creation_person/guardian"
-            @click="onCardClick"
+          title="a_guardian"
+          text-content="guardian_text_creation_card"
+          icon="fa fa-female"
+          href="/universal_creation_person/guardian"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un professeur -->
       <v-col cols="6">
         <LayoutHeaderUniversalCreationCard
-            title="a_teacher"
-            text-content="teacher_text_creation_card"
-            icon="fa fa-graduation-cap"
-            href="/universal_creation_person/teacher"
-            @click="onCardClick"
+          title="a_teacher"
+          text-content="teacher_text_creation_card"
+          icon="fa fa-graduation-cap"
+          href="/universal_creation_person/teacher"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un membre du personnel -->
       <v-col cols="6">
         <LayoutHeaderUniversalCreationCard
-            title="a_member_of_staff"
-            text-content="personnel_text_creation_card"
-            icon="fa fa-suitcase"
-            href="/universal_creation_person/personnel"
-            @click="onCardClick"
+          title="a_member_of_staff"
+          text-content="personnel_text_creation_card"
+          icon="fa fa-suitcase"
+          href="/universal_creation_person/personnel"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Une entité légale -->
       <v-col cols="6">
         <LayoutHeaderUniversalCreationCard
-            title="a_legal_entity"
-            text-content="moral_text_creation_card"
-            icon="fa fa-building"
-            href="/universal_creation_person/company"
-            @click="onCardClick"
+          title="a_legal_entity"
+          text-content="moral_text_creation_card"
+          icon="fa fa-building"
+          href="/universal_creation_person/company"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Une inscription en ligne -->
       <v-col cols="6" v-if="hasOnlineRegistrationModule">
         <LayoutHeaderUniversalCreationCard
-            title="online_registration"
-            text-content="online_registration_text_creation_card"
-            icon="fa fa-list-alt"
-            href="/online/registration/new_registration"
-            @click="onCardClick"
+          title="online_registration"
+          text-content="online_registration_text_creation_card"
+          icon="fa fa-list-alt"
+          href="/online/registration/new_registration"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un autre type de contact -->
       <v-col cols="6">
         <LayoutHeaderUniversalCreationCard
-            title="another_type_of_contact"
-            text-content="other_contact_text_creation_card"
-            icon="fa fa-plus"
-            href="/universal_creation_person/other_contact"
-            @click="onCardClick"
+          title="another_type_of_contact"
+          text-content="other_contact_text_creation_card"
+          icon="fa fa-plus"
+          href="/universal_creation_person/other_contact"
+          @click="onCardClick"
         />
       </v-col>
     </v-row>
@@ -187,48 +197,48 @@
       <!-- Un cours -->
       <v-col cols="6" v-if="ability.can('display', 'course_page')">
         <LayoutHeaderUniversalCreationCard
-            to="event-params"
-            href="/calendar/create/courses"
-            title="course"
-            text-content="course_text_creation_card"
-            icon="fa fa-users"
-            @click="onCardClick"
+          to="event-params"
+          href="/calendar/create/courses"
+          title="course"
+          text-content="course_text_creation_card"
+          icon="fa fa-users"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un examen -->
       <v-col cols="6" v-if="ability.can('display', 'exam_page')">
         <LayoutHeaderUniversalCreationCard
-            to="event-params"
-            href="/calendar/create/examens"
-            title="exam"
-            text-content="exam_text_creation_card"
-            icon="fa fa-graduation-cap"
-            @click="onCardClick"
+          to="event-params"
+          href="/calendar/create/examens"
+          title="exam"
+          text-content="exam_text_creation_card"
+          icon="fa fa-graduation-cap"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un projet pédagogique -->
       <v-col cols="6" v-if="ability.can('display', 'pedagogics_project_page')">
         <LayoutHeaderUniversalCreationCard
-            to="event-params"
-            href="/calendar/create/educational_projects"
-            title="educational_services"
-            text-content="educational_services_text_creation_card"
-            icon="fa fa-suitcase"
-            @click="onCardClick"
+          to="event-params"
+          href="/calendar/create/educational_projects"
+          title="educational_services"
+          text-content="educational_services_text_creation_card"
+          icon="fa fa-suitcase"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un autre évènement -->
       <v-col cols="6" v-if="ability.can('manage', 'events')">
         <LayoutHeaderUniversalCreationCard
-            to="event-params"
-            href="/calendar/create/events"
-            title="other_event"
-            text-content="other_event_text_creation_card"
-            icon="far fa-calendar"
-            @click="onCardClick"
+          to="event-params"
+          href="/calendar/create/events"
+          title="other_event"
+          text-content="other_event_text_creation_card"
+          icon="far fa-calendar"
+          @click="onCardClick"
         />
       </v-col>
     </v-row>
@@ -240,33 +250,33 @@
       <!-- Un email -->
       <v-col cols="6" v-if="ability.can('manage', 'emails')">
         <LayoutHeaderUniversalCreationCard
-            title="an_email"
-            text-content="email_text_creation_card"
-            icon="far fa-envelope"
-            href="/list/create/emails"
-            @click="onCardClick"
+          title="an_email"
+          text-content="email_text_creation_card"
+          icon="far fa-envelope"
+          href="/list/create/emails"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un courrier -->
       <v-col cols="6" v-if="ability.can('manage', 'mails')">
         <LayoutHeaderUniversalCreationCard
-            title="a_letter"
-            text-content="letter_text_creation_card"
-            icon="far fa-file-alt"
-            href="/list/create/mails"
-            @click="onCardClick"
+          title="a_letter"
+          text-content="letter_text_creation_card"
+          icon="far fa-file-alt"
+          href="/list/create/mails"
+          @click="onCardClick"
         />
       </v-col>
 
       <!-- Un SMS -->
       <v-col cols="6" v-if="ability.can('manage', 'texto')">
         <LayoutHeaderUniversalCreationCard
-            title="a_sms"
-            text-content="sms_text_creation_card"
-            icon="fa fa-mobile-alt"
-            href="/list/create/sms"
-            @click="onCardClick"
+          title="a_sms"
+          text-content="sms_text_creation_card"
+          icon="fa fa-mobile-alt"
+          href="/list/create/sms"
+          @click="onCardClick"
         />
       </v-col>
     </v-row>
@@ -274,74 +284,76 @@
 
   <!-- Page de pré-paramétrage des évènements -->
   <LayoutHeaderUniversalCreationEventParams
-      v-if="location === 'event-params'"
-      @params-updated="onEventParamsUpdated"
+    v-if="location === 'event-params'"
+    @params-updated="onEventParamsUpdated"
   />
 </template>
 
 <script setup lang="ts">
-  import type {Ref} from "@vue/reactivity";
-  import {useOrganizationProfileStore} from "~/stores/organizationProfile";
-  import {useAbility} from "@casl/vue";
-  import type {ComputedRef} from "vue";
-  import UrlUtils from "~/services/utils/urlUtils";
-
-  const props = defineProps({
-    /**
-     * The path that the user followed troughout the wizard
-     */
-    path: {
-      type: Array<string>,
-      required: true
-    }
-  })
-
-  const location: ComputedRef<string> = computed(() => {
-    return props.path.at(-1) ?? 'home'
-  })
-
-  const ability = useAbility()
-
-  const organizationProfile = useOrganizationProfileStore()
-  const isLaw1901: ComputedRef<boolean> = organizationProfile.isAssociation
-  const hasOnlineRegistrationModule: Ref<boolean> = ref(organizationProfile.hasModule('IEL'))
-
-  const baseUrl: Ref<string | null> = ref(null)
-  const query: Ref<Record<string, string>> = ref({})
-
-  const url: ComputedRef<string | null> = computed(() => {
-    if (baseUrl.value === null) {
-      return null
-    }
-    return UrlUtils.addQuery(baseUrl.value, query.value)
-  })
-
-  const emit = defineEmits(['cardClick', 'urlUpdate'])
+import type { Ref } from '@vue/reactivity'
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import { useAbility } from '@casl/vue'
+import type { ComputedRef } from 'vue'
+import UrlUtils from '~/services/utils/urlUtils'
 
+const props = defineProps({
   /**
-   * Called when a card is clicked
-   * @param to  Target location in the wizard
-   * @param href  Target absolute url
+   * The path that the user followed troughout the wizard
    */
-  const onCardClick = (to: string | null, href: string | null) => {
-    if (href !== null) {
-      baseUrl.value = href
-    }
-    emit('cardClick', to, url.value)
+  path: {
+    type: Array<string>,
+    required: true,
+  },
+})
+
+const location: ComputedRef<string> = computed(() => {
+  return props.path.at(-1) ?? 'home'
+})
+
+const ability = useAbility()
+
+const organizationProfile = useOrganizationProfileStore()
+const isLaw1901: ComputedRef<boolean> = organizationProfile.isAssociation
+const hasOnlineRegistrationModule: Ref<boolean> = ref(
+  organizationProfile.hasModule('IEL'),
+)
+
+const baseUrl: Ref<string | null> = ref(null)
+const query: Ref<Record<string, string>> = ref({})
+
+const url: ComputedRef<string | null> = computed(() => {
+  if (baseUrl.value === null) {
+    return null
   }
-
-  /**
-   * Called when the event parameters page is updated
-   * @param event
-   */
-  const onEventParamsUpdated = (event: {'start': string, 'end': string}) => {
-    query.value = event
+  return UrlUtils.addQuery(baseUrl.value, query.value)
+})
+
+const emit = defineEmits(['cardClick', 'urlUpdate'])
+
+/**
+ * Called when a card is clicked
+ * @param to  Target location in the wizard
+ * @param href  Target absolute url
+ */
+const onCardClick = (to: string | null, href: string | null) => {
+  if (href !== null) {
+    baseUrl.value = href
   }
-
-  const unwatch = watch(url, (newUrl: string | null) => {
-    emit('urlUpdate', newUrl)
-  })
-  onUnmounted(() => {
-    unwatch()
-  })
+  emit('cardClick', to, url.value)
+}
+
+/**
+ * Called when the event parameters page is updated
+ * @param event
+ */
+const onEventParamsUpdated = (event: { start: string; end: string }) => {
+  query.value = event
+}
+
+const unwatch = watch(url, (newUrl: string | null) => {
+  emit('urlUpdate', newUrl)
+})
+onUnmounted(() => {
+  unwatch()
+})
 </script>

+ 15 - 18
components/Layout/LoadingScreen.vue

@@ -2,31 +2,28 @@
 
 <template>
   <v-overlay
-          v-model="pageStore.loading"
-          z-index="9000"
-          persistent
-          class="align-center justify-center"
+    v-model="pageStore.loading"
+    z-index="9000"
+    persistent
+    class="align-center justify-center"
   >
-    <v-progress-circular
-      indeterminate
-      size="64"
-    />
+    <v-progress-circular indeterminate size="64" />
   </v-overlay>
 </template>
 
 <script setup lang="ts">
-  import {usePageStore} from "~/stores/page";
+import { usePageStore } from '~/stores/page'
 
-  const pageStore = usePageStore()
+const pageStore = usePageStore()
 </script>
 
 <style scoped>
-  .loading-page {
-    position: fixed;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    z-index: 1001!important;
-  }
+.loading-page {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1001 !important;
+}
 </style>

+ 74 - 75
components/Layout/MainMenu.vue

@@ -5,23 +5,18 @@ Prend en paramètre une liste de ItemMenu et les met en forme
 
 <template>
   <v-navigation-drawer
-      v-model="displayMenu"
-      :rail="isRail"
-      :disable-resize-watcher="true"
-      class="theme-secondary main-menu"
+    v-model="displayMenu"
+    :rail="isRail"
+    :disable-resize-watcher="true"
+    class="theme-secondary main-menu"
   >
     <template #prepend>
       <slot name="title"></slot>
     </template>
 
-    <v-list
-        open-strategy="single"
-        active-class="active"
-        class="left-menu"
-    >
+    <v-list open-strategy="single" active-class="active" class="left-menu">
       <!-- 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 (ou le menu est en mode réduit) -->
         <v-list-item
           v-if="!item.children || isRail"
@@ -42,11 +37,11 @@ Prend en paramètre une liste de ItemMenu et les met en forme
         >
           <template #activator="{ props }">
             <v-list-item
-                v-bind="props"
-                :prepend-icon="item.icon.name"
-                :title="$t(item.label)"
-                class="theme-secondary menu-item"
-                height="48px"
+              v-bind="props"
+              :prepend-icon="item.icon.name"
+              :title="$t(item.label)"
+              class="theme-secondary menu-item"
+              height="48px"
             />
           </template>
 
@@ -69,16 +64,16 @@ Prend en paramètre une liste de ItemMenu et les met en forme
       <slot name="foot"></slot>
     </template>
   </v-navigation-drawer>
-
 </template>
 
 <script setup lang="ts">
-import {useMenu} from "~/composables/layout/useMenu";
-import {computed} from "@vue/reactivity";
+import { useMenu } from '~/composables/layout/useMenu'
+import { computed } from '@vue/reactivity'
 import { useDisplay } from 'vuetify'
-import type { MenuGroup, MenuItem } from "~/types/layout";
+import type { MenuGroup, MenuItem } from '~/types/layout'
 
-const { getMenu, hasMenu, isInternalLink, setMenuState, isMenuOpened } = useMenu()
+const { getMenu, hasMenu, isInternalLink, setMenuState, isMenuOpened } =
+  useMenu()
 
 const { mdAndUp, lgAndUp } = useDisplay()
 
@@ -103,11 +98,16 @@ const displayMenu = computed(() => {
 // En vue md+, fermer le menu le passe simplement en mode rail
 // Sinon, le fermer le masque complètement
 const isRail = computed(() => {
-  return menu !== null && mdAndUp.value && !isOpened.value && !items.some((item) => item.expanded)
+  return (
+    menu !== null &&
+    mdAndUp.value &&
+    !isOpened.value &&
+    !items.some((item) => item.expanded)
+  )
 })
 
 const unwatch = watch(lgAndUp, (newValue, oldValue) => {
-// Par défaut si l'écran est trop petit au chargement de la page, le menu doit rester fermé.
+  // Par défaut si l'écran est trop petit au chargement de la page, le menu doit rester fermé.
   if (process.client && menu !== null) {
     setMenuState('Main', lgAndUp.value)
   }
@@ -119,68 +119,67 @@ onUnmounted(() => {
 </script>
 
 <style scoped lang="scss">
+.v-list-item {
+  min-height: 10px !important;
+}
 
-  .v-list-item {
-    min-height: 10px !important;
-  }
-
-  :deep(.v-list-item-title), :deep(.v-icon)
-  {
-    font-size: 14px;
-    color: rgb(var(--v-theme-on-secondary));
-  }
-
-  .v-list-item__prepend {
-    margin: 10px 0;
-    margin-right: 10px !important;
-  }
+:deep(.v-list-item-title),
+:deep(.v-icon) {
+  font-size: 14px;
+  color: rgb(var(--v-theme-on-secondary));
+}
 
-  .v-application--is-ltr .v-list-group--no-action > .v-list-group__header {
-    margin-left: 0;
-    padding-left: 0;
-  }
-  .v-application--is-ltr .v-list-group--no-action > .v-list-group__items > .v-list-item {
-    padding-left: 30px;
-  }
+.v-list-item__prepend {
+  margin: 10px 0;
+  margin-right: 10px !important;
+}
 
-  .v-list-item__content {
-    padding: 8px 0;
-  }
+.v-application--is-ltr .v-list-group--no-action > .v-list-group__header {
+  margin-left: 0;
+  padding-left: 0;
+}
+.v-application--is-ltr
+  .v-list-group--no-action
+  > .v-list-group__items
+  > .v-list-item {
+  padding-left: 30px;
+}
 
-  .v-list-group__items .v-list-item {
-    padding-inline-start: 30px !important;
-  }
+.v-list-item__content {
+  padding: 8px 0;
+}
 
-  .v-list-group--no-action > .v-list-group__header,
-  .v-list-item
-  {
-    border-left: 3px solid rgb(var(--v-theme-secondary));
-    height: 48px;
-  }
+.v-list-group__items .v-list-item {
+  padding-inline-start: 30px !important;
+}
 
-  .v-list-item:hover,
-  .v-list-item.active,
-  :deep(.v-list-group__items .v-list-item)
-  {
-    border-left: 3px solid rgb(var(--v-theme-primary));
-    background-color: rgb(var(--v-theme-secondary-alt)) !important;
-    color: rgb(var(--v-theme-on-secondary-alt)) !important;
-  }
+.v-list-group--no-action > .v-list-group__header,
+.v-list-item {
+  border-left: 3px solid rgb(var(--v-theme-secondary));
+  height: 48px;
+}
 
-  :deep(.v-list-group__items .v-list-item-title) {
-    color: rgb(var(--v-theme-on-secondary-alt));
-  }
+.v-list-item:hover,
+.v-list-item.active,
+:deep(.v-list-group__items .v-list-item) {
+  border-left: 3px solid rgb(var(--v-theme-primary));
+  background-color: rgb(var(--v-theme-secondary-alt)) !important;
+  color: rgb(var(--v-theme-on-secondary-alt)) !important;
+}
 
-  :deep(.v-list-group__items .v-icon) {
-    color: rgb(var(--v-theme-on-secondary-alt));
-  }
+:deep(.v-list-group__items .v-list-item-title) {
+  color: rgb(var(--v-theme-on-secondary-alt));
+}
 
-  :deep(.v-list-item .v-icon) {
-    margin-right: 10px;
-  }
+:deep(.v-list-group__items .v-icon) {
+  color: rgb(var(--v-theme-on-secondary-alt));
+}
 
-  :deep(.menu-item .fa) {
-    text-align: center;
-  }
+:deep(.v-list-item .v-icon) {
+  margin-right: 10px;
+}
 
+:deep(.menu-item .fa) {
+  text-align: center;
+}
 </style>

+ 101 - 98
components/Layout/ParametersMenu.vue

@@ -1,5 +1,9 @@
 <template>
-  <v-navigation-drawer v-if="displayMenu" v-model="isOpened" mobile-breakpoint="sm">
+  <v-navigation-drawer
+    v-if="displayMenu"
+    v-model="isOpened"
+    mobile-breakpoint="sm"
+  >
     <template v-slot:prepend>
       <div class="title">
         <h3>{{ $t('parameters') }}</h3>
@@ -8,21 +12,22 @@
 
     <v-list active-class="active">
       <v-list-item
-          v-for="(item, i) in menu!!.children"
-          :key="i"
-          :title="$t(item.label)"
-          :prepend-icon="item.icon ? item.icon.name : ''"
-          :to="(item as MenuItem).to">
+        v-for="(item, i) in menu!!.children"
+        :key="i"
+        :title="$t(item.label)"
+        :prepend-icon="item.icon ? item.icon.name : ''"
+        :to="(item as MenuItem).to"
+      >
       </v-list-item>
     </v-list>
 
     <template v-slot:append>
       <v-btn
-          :href="homeUrl"
-          prepend-icon="fa fa-right-from-bracket"
-          :flat="true"
-          color="on-neutral-very-soft"
-          class="cancel-btn py-2"
+        :href="homeUrl"
+        prepend-icon="fa fa-right-from-bracket"
+        :flat="true"
+        color="on-neutral-very-soft"
+        class="cancel-btn py-2"
       >
         {{ $t('exit') }}
       </v-btn>
@@ -31,102 +36,100 @@
 </template>
 
 <script setup lang="ts">
-  import {useMenu} from "~/composables/layout/useMenu";
-  import {useHomeUrl} from "~/composables/utils/useHomeUrl";
-  import {useDisplay} from "vuetify";
-  import {computed} from "@vue/reactivity";
-  import type {MenuGroup, MenuItem} from "~/types/layout";
+import { useMenu } from '~/composables/layout/useMenu'
+import { useHomeUrl } from '~/composables/utils/useHomeUrl'
+import { useDisplay } from 'vuetify'
+import { computed } from '@vue/reactivity'
+import type { MenuGroup, MenuItem } from '~/types/layout'
 
-  const { mdAndUp } = useDisplay()
+const { mdAndUp } = useDisplay()
 
-  const { getMenu, hasMenu, isMenuOpened, setMenuState } = useMenu()
+const { getMenu, hasMenu, isMenuOpened, setMenuState } = useMenu()
 
-  const menu: MenuGroup | null = getMenu('Parameters')
+const menu: MenuGroup | null = getMenu('Parameters')
 
-  const displayMenu = computed(() => {
-    return menu !== null && hasMenu('Parameters')
-  })
+const displayMenu = computed(() => {
+  return menu !== null && hasMenu('Parameters')
+})
 
-  const isOpened = computed(() => isMenuOpened('Parameters'))
+const isOpened = computed(() => isMenuOpened('Parameters'))
 
-  const unwatch = watch(mdAndUp, () => {
-    // Par défaut si l'écran est trop petit au chargement de la page, le menu doit rester fermé.
-    if (process.client && menu !== null) {
-      setMenuState('Parameters', mdAndUp.value)
-    }
-  })
+const unwatch = watch(mdAndUp, () => {
+  // Par défaut si l'écran est trop petit au chargement de la page, le menu doit rester fermé.
+  if (process.client && menu !== null) {
+    setMenuState('Parameters', mdAndUp.value)
+  }
+})
 
-  const { homeUrl } = useHomeUrl()
+const { homeUrl } = useHomeUrl()
 
-  onUnmounted(() => {
-    unwatch()
-  })
+onUnmounted(() => {
+  unwatch()
+})
 </script>
 
 <style scoped lang="scss">
-  .title {
-    display: flex;
-    align-items: center;
-    height: 48px;
-    vertical-align: center;
-    margin-top: 18px;
-    padding: 4px 16px;
-    font-size: 18px;
-    color: rgb(var(--v-theme-on-neutral-very-soft));
-  }
-
-  .v-navigation-drawer {
-    background-color: rgb(var(--v-theme-neutral-very-soft));
-    border-right: solid 1px rgb(var(--v-theme-neutral-strong));
-  }
-
-  :deep(.v-list-item-title), :deep(.v-icon)
-  {
-    font-size: 14px;
-    color: rgb(var(--v-theme-on-neutral-very-soft));
-  }
-
-  .v-list-item:hover,
-  .v-list-item.active,
-  :deep(.v-list-group__items .v-list-item)
-  {
-    background-color: rgb(var(--v-theme-neutral)) !important;
-    color: rgb(var(--v-theme-on-secondary-alt)) !important;
-  }
-
-  :deep(.v-list-item.active .v-list-item-title) {
-    font-weight: 800 !important;
-  }
-
-  :deep(.v-list-item-title), :deep(.v-icon)
-  {
-    font-size: 14px;
-    color: rgb(var(--v-theme-on-neutral-very-soft));
-  }
-
-  :deep(.v-list-item__prepend) {
-    margin: 10px 0;
-    margin-right: 10px !important;
-  }
-
-  :deep(.v-list-item .v-icon) {
-    max-width: 24px;
-    margin-right: 10px;
-  }
-
-  .cancel-btn {
-    height: 42px;
-    color: rgb(var(--v-theme-on-neutral-very-soft));
-    background-color: transparent;
-    width: 100%;
-    border-top: solid 1px rgb(var(--v-theme-on-neutral-very-soft));
-    display: flex;
-    flex-direction: row;
-    justify-content: flex-start;
-  }
-
-  :deep(.cancel-btn .v-btn__prepend) {
-    margin: 0 16px 4px 2px;
-  }
-
+.title {
+  display: flex;
+  align-items: center;
+  height: 48px;
+  vertical-align: center;
+  margin-top: 18px;
+  padding: 4px 16px;
+  font-size: 18px;
+  color: rgb(var(--v-theme-on-neutral-very-soft));
+}
+
+.v-navigation-drawer {
+  background-color: rgb(var(--v-theme-neutral-very-soft));
+  border-right: solid 1px rgb(var(--v-theme-neutral-strong));
+}
+
+:deep(.v-list-item-title),
+:deep(.v-icon) {
+  font-size: 14px;
+  color: rgb(var(--v-theme-on-neutral-very-soft));
+}
+
+.v-list-item:hover,
+.v-list-item.active,
+:deep(.v-list-group__items .v-list-item) {
+  background-color: rgb(var(--v-theme-neutral)) !important;
+  color: rgb(var(--v-theme-on-secondary-alt)) !important;
+}
+
+:deep(.v-list-item.active .v-list-item-title) {
+  font-weight: 800 !important;
+}
+
+:deep(.v-list-item-title),
+:deep(.v-icon) {
+  font-size: 14px;
+  color: rgb(var(--v-theme-on-neutral-very-soft));
+}
+
+:deep(.v-list-item__prepend) {
+  margin: 10px 0;
+  margin-right: 10px !important;
+}
+
+:deep(.v-list-item .v-icon) {
+  max-width: 24px;
+  margin-right: 10px;
+}
+
+.cancel-btn {
+  height: 42px;
+  color: rgb(var(--v-theme-on-neutral-very-soft));
+  background-color: transparent;
+  width: 100%;
+  border-top: solid 1px rgb(var(--v-theme-on-neutral-very-soft));
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+}
+
+:deep(.cancel-btn .v-btn__prepend) {
+  margin: 0 16px 4px 2px;
+}
 </style>

+ 45 - 35
components/Layout/SubHeader/ActivityYear.vue

@@ -1,8 +1,7 @@
 <template>
   <main class="d-flex flex-row align-center">
-    <span v-show="mdAndUp"
-          class="mr-2 font-weight-bold on-neutral">
-        {{ $t(label) }} :
+    <span v-show="mdAndUp" class="mr-2 font-weight-bold on-neutral">
+      {{ $t(label) }} :
     </span>
 
     <UiXeditableText
@@ -11,14 +10,17 @@
       :data="currentActivityYear"
       @update="setActivityYear"
     >
-      <template #xeditable.read="{inputValue}">
+      <template #xeditable.read="{ inputValue }">
         <div class="d-flex align-center on-neutral--clickable">
-          <v-icon aria-hidden="false" size="small" class="mr-1" icon="fas fa-edit" />
-          <strong >
+          <v-icon
+            aria-hidden="false"
+            size="small"
+            class="mr-1"
+            icon="fas fa-edit"
+          />
+          <strong>
             {{ inputValue }}
-            <span v-if="yearPlusOne">
-              / {{ parseInt(inputValue) + 1 }}
-            </span>
+            <span v-if="yearPlusOne"> / {{ parseInt(inputValue) + 1 }} </span>
           </strong>
         </div>
       </template>
@@ -27,14 +29,14 @@
 </template>
 
 <script setup lang="ts">
-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 {useDisplay} from "vuetify";
-import {usePageStore} from "~/stores/page";
-import {useRefreshProfile} from "~/composables/data/useRefreshProfile";
+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 { useDisplay } from 'vuetify'
+import { usePageStore } from '~/stores/page'
+import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
 
 const { em } = useEntityManager()
 const accessProfileStore = useAccessProfileStore()
@@ -44,9 +46,15 @@ const pageStore = usePageStore()
 const { mdAndUp } = useDisplay()
 const { refreshProfile } = useRefreshProfile()
 
-const currentActivityYear: ComputedRef<number | undefined> = computed(() => accessProfileStore.activityYear ?? undefined)
+const currentActivityYear: ComputedRef<number | undefined> = computed(
+  () => accessProfileStore.activityYear ?? undefined,
+)
 const yearPlusOne: boolean = !organizationProfileStore.isManagerProduct
-const label: string = organizationProfileStore.isSchool ? 'schooling_year' : organizationProfileStore.isArtist ? 'season_year' : 'cotisation_year'
+const label: string = organizationProfileStore.isSchool
+  ? 'schooling_year'
+  : organizationProfileStore.isArtist
+    ? 'season_year'
+    : 'cotisation_year'
 
 /**
  * Persist a new activityYear
@@ -61,9 +69,11 @@ const setActivityYear = async (event: string) => {
   formStore.setDirty(false)
 
   pageStore.loading = true
-  await em.patch(Access, accessProfileStore.currentAccessId, { activityYear: activityYear })
+  await em.patch(Access, accessProfileStore.currentAccessId, {
+    activityYear: activityYear,
+  })
   if (process.server) {
-      // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
+    // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }
 
@@ -72,21 +82,21 @@ const setActivityYear = async (event: string) => {
 </script>
 
 <style lang="scss">
-  .activity-year-input {
-    width: 120px;
-    max-height: 20px;
+.activity-year-input {
+  width: 120px;
+  max-height: 20px;
 
-    .v-input {
-      min-width: 70px;
-    }
+  .v-input {
+    min-width: 70px;
+  }
 
-    input{
-      font-size: 14px;
-      width: 55px !important;
-      padding: 0 !important;
-      margin-top: 0 !important;
-      min-height: 24px;
-      height: 24px;
-    }
+  input {
+    font-size: 14px;
+    width: 55px !important;
+    padding: 0 !important;
+    margin-top: 0 !important;
+    min-height: 24px;
+    height: 24px;
   }
+}
 </style>

+ 12 - 12
components/Layout/SubHeader/Breadcrumbs.vue

@@ -1,26 +1,24 @@
 <template>
-  <v-breadcrumbs
-    :items="items"
-  />
+  <v-breadcrumbs :items="items" />
 </template>
 
 <script setup lang="ts">
-import {computed} from "@vue/reactivity";
-import type {ComputedRef} from "@vue/reactivity";
-import type {AnyJson} from "~/types/data";
-import {useI18n} from "vue-i18n";
-import UrlUtils from "~/services/utils/urlUtils";
+import { computed } from '@vue/reactivity'
+import type { ComputedRef } from '@vue/reactivity'
+import type { AnyJson } from '~/types/data'
+import { useI18n } from 'vue-i18n'
+import UrlUtils from '~/services/utils/urlUtils'
 
 const runtimeConfig = useRuntimeConfig()
 const i18n = useI18n()
 const router = useRouter()
 
 const items: ComputedRef<Array<AnyJson>> = computed(() => {
-  const crumbs:Array<AnyJson> = []
+  const crumbs: Array<AnyJson> = []
 
   crumbs.push({
     title: i18n.t('welcome'),
-    href: UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#', 'dashboard')
+    href: UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#', 'dashboard'),
   })
 
   const pathPart: Array<string> = UrlUtils.split(router.currentRoute.value.path)
@@ -35,9 +33,11 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
     match = router.resolve(path)
     if (match.name) {
       crumbs.push({
-        title: !parseInt(part, 10) ? i18n.t(part + '_breadcrumbs') : i18n.t('item'),
+        title: !parseInt(part, 10)
+          ? i18n.t(part + '_breadcrumbs')
+          : i18n.t('item'),
         exact: true,
-        to: path
+        to: path,
       })
     }
   })

+ 44 - 30
components/Layout/SubHeader/DataTiming.vue

@@ -1,6 +1,8 @@
 <template>
   <main class="d-flex align-baseline">
-    <span v-show="mdAndUp" class="mr-2 font-weight-bold on-neutral">{{ $t('display_data') }} : </span>
+    <span v-show="mdAndUp" class="mr-2 font-weight-bold on-neutral"
+      >{{ $t('display_data') }} :
+    </span>
 
     <v-btn-toggle
       ref="toggle"
@@ -15,10 +17,15 @@
       @update:modelValue="onUpdate"
     >
       <v-btn
-          v-for="choice in historicalChoices"
-          :value="choice"
-          max-height="25"
-          :class="'font-weight-normal text-caption ' + (historicalValue.includes(choice) ? 'theme-primary' : 'theme-neutral-soft')"
+        v-for="choice in historicalChoices"
+        :value="choice"
+        max-height="25"
+        :class="
+          'font-weight-normal text-caption ' +
+          (historicalValue.includes(choice)
+            ? 'theme-primary'
+            : 'theme-neutral-soft')
+        "
       >
         <!-- TODO: on ne devrait pas avoir besoin du if et de la classe 'btn-selected' dans v-btn, mais à l'heure
          qu'il est, le component ne fonctionne pas comme attendu. A revoir quand vuetify 3 sera plus stable -->
@@ -29,14 +36,14 @@
 </template>
 
 <script setup lang="ts">
-import {useFormStore} from "~/stores/form";
-import {useAccessProfileStore} from "~/stores/accessProfile";
-import type {Ref} from "@vue/reactivity";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import {useDisplay, useTheme} from "vuetify";
-import Access from "~/models/Access/Access";
-import {usePageStore} from "~/stores/page";
-import {useRefreshProfile} from "~/composables/data/useRefreshProfile";
+import { useFormStore } from '~/stores/form'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import type { Ref } from '@vue/reactivity'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import { useDisplay, useTheme } from 'vuetify'
+import Access from '~/models/Access/Access'
+import { usePageStore } from '~/stores/page'
+import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
 
 // TODO: en v3.0.5, pas de solution documentée pour renseigner directement la couleur dans le template, à revoir
 const color = useTheme().current.value.colors['primary']
@@ -50,9 +57,15 @@ const { refreshProfile } = useRefreshProfile()
 
 const toggle = ref(null)
 
-const historicalChoices: Array<'past' | 'present' | 'future'> = ['past', 'present', 'future']
+const historicalChoices: Array<'past' | 'present' | 'future'> = [
+  'past',
+  'present',
+  'future',
+]
 
-const historicalValue: Ref<Array<string>> = ref(historicalChoices.filter((item) => accessProfileStore.historical[item]))
+const historicalValue: Ref<Array<string>> = ref(
+  historicalChoices.filter((item) => accessProfileStore.historical[item]),
+)
 
 const onUpdate = async (newValue: Array<string>) => {
   historicalValue.value = newValue
@@ -60,35 +73,36 @@ const onUpdate = async (newValue: Array<string>) => {
   const accessId = accessProfileStore.currentAccessId
 
   accessProfileStore.setHistorical(
-      historicalValue.value.includes('past'),
-      historicalValue.value.includes('present'),
-      historicalValue.value.includes('future')
+    historicalValue.value.includes('past'),
+    historicalValue.value.includes('present'),
+    historicalValue.value.includes('future'),
   )
 
   setDirty(false)
   pageStore.loading = true
 
-  await em.patch(Access, accessId, {'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
+    // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }
 
   window.location.reload()
 }
-
 </script>
 
 <style scoped lang="scss">
-  .v-btn-group {
-    max-height: 22px;
-  }
+.v-btn-group {
+  max-height: 22px;
+}
 
-  .v-btn {
-    padding: 0 8px;
-  }
+.v-btn {
+  padding: 0 8px;
+}
 
-  .v-btn.btn-selected {
-    background-color: rgb(var(--v-theme-primary)) !important;
-  }
+.v-btn.btn-selected {
+  background-color: rgb(var(--v-theme-primary)) !important;
+}
 </style>

+ 25 - 18
components/Layout/SubHeader/DataTimingRange.vue

@@ -6,23 +6,23 @@
       </span>
 
       <UiDateRangePicker
-          :model-value="datesRange"
-          :max-height="28"
-          @update:model-value="updateDateTimeRange"
+        :model-value="datesRange"
+        :max-height="28"
+        @update:model-value="updateDateTimeRange"
       />
     </div>
   </main>
 </template>
 
 <script setup lang="ts">
-import type {Ref} from "@vue/reactivity";
-import {useAccessProfileStore} from "~/stores/accessProfile";
-import {useFormStore} from "~/stores/form";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import Access from "~/models/Access/Access";
-import DateUtils from "~/services/utils/dateUtils";
-import {usePageStore} from "~/stores/page";
-import {useRefreshProfile} from "~/composables/data/useRefreshProfile";
+import type { Ref } from '@vue/reactivity'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { useFormStore } from '~/stores/form'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import Access from '~/models/Access/Access'
+import DateUtils from '~/services/utils/dateUtils'
+import { usePageStore } from '~/stores/page'
+import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
 
 const { setDirty } = useFormStore()
 const accessProfileStore = useAccessProfileStore()
@@ -33,18 +33,23 @@ const { refreshProfile } = useRefreshProfile()
 const start = accessProfileStore.historical.dateStart
 const end = accessProfileStore.historical.dateEnd
 
-const datesRange: Ref<Array<Date> | null> = ref((start && end) ? [new Date(start), new Date(end)] : null)
+const datesRange: Ref<Array<Date> | null> = ref(
+  start && end ? [new Date(start), new Date(end)] : null,
+)
 
 const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
-
   const accessId = accessProfileStore.currentAccessId
 
   datesRange.value = dates
 
-  if (datesRange.value !== null && datesRange.value[0] !== null && datesRange.value[1] !== null) {
+  if (
+    datesRange.value !== null &&
+    datesRange.value[0] !== null &&
+    datesRange.value[1] !== null
+  ) {
     accessProfileStore.setHistoricalRange(
-        DateUtils.formatIsoShortDate(datesRange.value[0]),
-        DateUtils.formatIsoShortDate(datesRange.value[1])
+      DateUtils.formatIsoShortDate(datesRange.value[0]),
+      DateUtils.formatIsoShortDate(datesRange.value[1]),
     )
   } else {
     accessProfileStore.setHistorical(false, true, false)
@@ -52,9 +57,11 @@ const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
   setDirty(false)
   pageStore.loading = true
 
-  await em.patch(Access, accessId, {'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
+    // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }
 

+ 45 - 39
components/Layout/SubHeader/PersonnalizedList.vue

@@ -5,12 +5,17 @@
     </a>
 
     <v-menu
-        :activator="btn"
-        offset="10"
-        min-width="440"
-        :close-on-content-click="false"
+      :activator="btn"
+      offset="10"
+      min-width="440"
+      :close-on-content-click="false"
     >
-      <v-card v-if="collection.totalItems === 0" height="80" width="440" class="pa-4">
+      <v-card
+        v-if="collection.totalItems === 0"
+        height="80"
+        width="440"
+        class="pa-4"
+      >
         <v-card-text class="ma-0 pa-0 header_menu">
           {{ $t('nothing_to_show') }}
         </v-card-text>
@@ -19,11 +24,11 @@
       <v-card v-else width="440">
         <v-card-title class="text-body-2 header-personalized">
           <v-text-field
-              v-model="search"
-              :label="$t('searchList')"
-              :loading="pending"
-              density="compact"
-              clear-icon="header-personalized"
+            v-model="search"
+            :label="$t('searchList')"
+            :loading="pending"
+            density="compact"
+            clear-icon="header-personalized"
           />
         </v-card-title>
 
@@ -36,24 +41,23 @@
               :href="getListURL(item)"
               exact
             >
-              <strong>{{item.menuKey}}</strong> - {{item.label}}
+              <strong>{{ item.menuKey }}</strong> - {{ item.label }}
             </v-list-item>
           </v-list>
         </v-card-text>
       </v-card>
-
     </v-menu>
   </main>
 </template>
 
 <script setup lang="ts">
 import PersonalizedList from '~/models/Access/PersonalizedList'
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import {ref} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
-import type {AnyJson} from "~/types/data";
-import ApiResource from "~/models/ApiResource";
-import UrlUtils from "~/services/utils/urlUtils";
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import { ref } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import type { AnyJson } from '~/types/data'
+import ApiResource from '~/models/ApiResource'
+import UrlUtils from '~/services/utils/urlUtils'
 
 const btn: Ref = ref(null)
 
@@ -64,24 +68,26 @@ const { data: collection, pending } = await fetchCollection(PersonalizedList)
 const i18n = useI18n()
 
 const items: ComputedRef<Array<AnyJson>> = computed(() => {
-  const lists: Array<ApiResource> = collection.value !== null ? collection.value.items : []
+  const lists: Array<ApiResource> =
+    collection.value !== null ? collection.value.items : []
 
-  lists.map(item => {
+  lists.map((item) => {
     item.menuKey = i18n.t(item.menuKey) as string
   })
 
   return lists
 })
 
-const search = ref('');
+const search = ref('')
 
 const filteredItems = computed(() => {
-  return items.value.filter( item => {
-      return !search.value ||
-             item.label.toLowerCase().indexOf(search.value.toLowerCase()) >= 0 ||
-             item.menuKey.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
-    }
-  )
+  return items.value.filter((item) => {
+    return (
+      !search.value ||
+      item.label.toLowerCase().indexOf(search.value.toLowerCase()) >= 0 ||
+      item.menuKey.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
+    )
+  })
 })
 
 const runtimeConfig = useRuntimeConfig()
@@ -93,16 +99,16 @@ const getListURL = (list: PersonalizedList) => {
 </script>
 
 <style scoped lang="scss">
-  #activator {
-    cursor: pointer;
-  }
-
-  #activator:hover {
-    color: rgb(var(--var-theme-on-neutral)) !important;
-  }
-
-  .header-personalized {
-    margin-bottom: 0;
-    padding-bottom: 0;
-  }
+#activator {
+  cursor: pointer;
+}
+
+#activator:hover {
+  color: rgb(var(--var-theme-on-neutral)) !important;
+}
+
+.header-personalized {
+  margin-bottom: 0;
+  padding-bottom: 0;
+}
 </style>

+ 55 - 42
components/Layout/Subheader.vue

@@ -15,40 +15,52 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 
       <span class="flex-fill" />
 
-      <v-card
-        class="d-flex flex-row align-center mr-6"
-        :flat="true"
-        tile
-      >
-        <LayoutSubHeaderActivityYear v-if="smAndUp && !showDateTimeRange" class="activity-year" />
-
-        <div v-if="hasMenuOrIsTeacher" class="d-flex flex-row align-center h-100">
+      <v-card class="d-flex flex-row align-center mr-6" :flat="true" tile>
+        <LayoutSubHeaderActivityYear
+          v-if="smAndUp && !showDateTimeRange"
+          class="activity-year"
+        />
+
+        <div
+          v-if="hasMenuOrIsTeacher"
+          class="d-flex flex-row align-center h-100"
+        >
           <LayoutSubHeaderDataTiming
-              v-if="smAndUp && !showDateTimeRange"
-              class="data-timing ml-2"
+            v-if="smAndUp && !showDateTimeRange"
+            class="data-timing ml-2"
           />
 
           <LayoutSubHeaderDataTimingRange
-              v-if="smAndUp && showDateTimeRange"
-              class="data-timing-range ml-n1"
+            v-if="smAndUp && showDateTimeRange"
+            class="data-timing-range ml-n1"
           />
 
           <v-btn
-              v-if="smAndUp"
-              ref="btn"
-              class="switch-btn ml-1 theme-neutral-soft"
-              height="22" min-height="22" max-height="22"
-              width="25" min-width="25" max-width="25"
-              elevation="0"
-              @click="showDateTimeRange = !showDateTimeRange"
+            v-if="smAndUp"
+            ref="btn"
+            class="switch-btn ml-1 theme-neutral-soft"
+            height="22"
+            min-height="22"
+            max-height="22"
+            width="25"
+            min-width="25"
+            max-width="25"
+            elevation="0"
+            @click="showDateTimeRange = !showDateTimeRange"
           >
-              <v-icon icon="fas fa-history" class="font-weight-normal" style="font-size: 14px;" />
+            <v-icon
+              icon="fas fa-history"
+              class="font-weight-normal"
+              style="font-size: 14px"
+            />
           </v-btn>
           <v-tooltip location="bottom" :activator="btn">
-              <span>{{ $t('history_help') }}</span>
+            <span>{{ $t('history_help') }}</span>
           </v-tooltip>
 
-          <LayoutSubHeaderPersonnalizedList class="personalized-list ml-2 d-flex align-center" />
+          <LayoutSubHeaderPersonnalizedList
+            class="personalized-list ml-2 d-flex align-center"
+          />
         </div>
       </v-card>
     </v-card>
@@ -56,29 +68,30 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 </template>
 
 <script setup lang="ts">
-    import {useAccessProfileStore} from "~/stores/accessProfile";
-    import {computed, ref} from "@vue/reactivity";
-    import type {ComputedRef, Ref} from "@vue/reactivity";
-    import {useMenu} from "~/composables/layout/useMenu";
-    import {useDisplay} from "vuetify";
-
-    const { smAndUp, lgAndUp } = useDisplay()
-    const accessProfile = useAccessProfileStore()
-    const { hasMenu } = useMenu()
-    const btn: Ref = ref(null)
-
-    const hasMenuOrIsTeacher: ComputedRef<boolean> = computed(
-        () => hasMenu('Main') || (accessProfile.isTeacher ?? false)
-    )
-
-    const showDateTimeRange: Ref<boolean> = ref(
-        Object.hasOwn(accessProfile.historical, 'dateStart') && accessProfile.historical.dateStart !== null &&
-        Object.hasOwn(accessProfile.historical, 'dateEnd') && accessProfile.historical.dateEnd !== null
-    )
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { computed, ref } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import { useMenu } from '~/composables/layout/useMenu'
+import { useDisplay } from 'vuetify'
+
+const { smAndUp, lgAndUp } = useDisplay()
+const accessProfile = useAccessProfileStore()
+const { hasMenu } = useMenu()
+const btn: Ref = ref(null)
+
+const hasMenuOrIsTeacher: ComputedRef<boolean> = computed(
+  () => hasMenu('Main') || (accessProfile.isTeacher ?? false),
+)
+
+const showDateTimeRange: Ref<boolean> = ref(
+  Object.hasOwn(accessProfile.historical, 'dateStart') &&
+    accessProfile.historical.dateStart !== null &&
+    Object.hasOwn(accessProfile.historical, 'dateEnd') &&
+    accessProfile.historical.dateEnd !== null,
+)
 </script>
 
 <style scoped lang="scss">
-
 main {
   font-size: 12px;
 }

+ 14 - 14
components/Layout/ThemeSwitcher.vue

@@ -1,26 +1,26 @@
 <template>
   <v-switch
-      v-model="theme.global.name.value"
-      density="compact"
-      :inline="true"
-      false-value="light"
-      false-icon="fas fa-sun"
-      true-value="dark"
-      true-icon="fas fa-moon"
+    v-model="theme.global.name.value"
+    density="compact"
+    :inline="true"
+    false-value="light"
+    false-icon="fas fa-sun"
+    true-value="dark"
+    true-icon="fas fa-moon"
   />
 </template>
 
 <script setup lang="ts">
-import {useTheme} from "vuetify";
+import { useTheme } from 'vuetify'
 
 const theme = useTheme()
 </script>
 
 <style scoped lang="scss">
-  .v-switch {
-    min-width: 60px;
-    max-width: 60px;
-    min-height: 40px;
-    max-height: 40px;
-  }
+.v-switch {
+  min-width: 60px;
+  max-width: 60px;
+  min-height: 40px;
+  max-height: 40px;
+}
 </style>

+ 27 - 27
components/Ui/Button/Delete.vue

@@ -8,9 +8,7 @@ Bouton Delete avec modale de confirmation de la suppression
       <v-icon>fas fa-trash</v-icon>
     </v-btn>
 
-    <LazyLayoutDialog
-      :show="showDialog"
-    >
+    <LazyLayoutDialog :show="showDialog">
       <template #dialogType>{{ $t('delete_assistant') }}</template>
       <template #dialogTitle>{{ $t('caution') }}</template>
       <template #dialogText>
@@ -31,27 +29,27 @@ Bouton Delete avec modale de confirmation de la suppression
 </template>
 
 <script setup lang="ts">
-import {TYPE_ALERT} from '~/types/enum/enums'
-import type {Ref} from "@vue/reactivity";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import ApiResource from "~/models/ApiResource";
-import {usePageStore} from "~/stores/page";
-import ApiModel from "~/models/ApiModel";
+import { TYPE_ALERT } from '~/types/enum/enums'
+import type { Ref } from '@vue/reactivity'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import ApiResource from '~/models/ApiResource'
+import { usePageStore } from '~/stores/page'
+import ApiModel from '~/models/ApiModel'
 
 const props = defineProps({
-    model: {
-      type: Function as any as () => typeof ApiModel,
-      required: true
-    },
-    entity: {
-      type: Object as () => ApiResource,
-      required: true
-    },
-    flat: {
-      type: Boolean,
-      required: false,
-      default: false
-    }
+  model: {
+    type: Function as any as () => typeof ApiModel,
+    required: true,
+  },
+  entity: {
+    type: Object as () => ApiResource,
+    required: true,
+  },
+  flat: {
+    type: Boolean,
+    required: false,
+    default: false,
+  },
 })
 
 const showDialog: Ref<boolean> = ref(false)
@@ -70,10 +68,12 @@ const deleteItem = async () => {
   showDialog.value = false
 }
 
-const alertDeleteItem = () => { showDialog.value = true }
-const closeDialog = () => { showDialog.value = false }
-
+const alertDeleteItem = () => {
+  showDialog.value = true
+}
+const closeDialog = () => {
+  showDialog.value = false
+}
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 24 - 23
components/Ui/Button/Submit.vue

@@ -1,12 +1,11 @@
 <template>
   <v-btn
-      class="mr-4 theme-primary"
-      :class="hasOtherActions ? 'pr-0' : ''"
-      @click="submitAction(mainAction)"
-      ref="mainBtn"
-      :disabled="validationPending"
+    class="mr-4 theme-primary"
+    :class="hasOtherActions ? 'pr-0' : ''"
+    @click="submitAction(mainAction)"
+    ref="mainBtn"
+    :disabled="validationPending"
   >
-
     {{ $t(mainAction) }}
 
     <v-divider class="ml-3" :vertical="true" v-if="hasOtherActions"></v-divider>
@@ -22,13 +21,13 @@
       <template #activator="{ on, attrs }">
         <v-toolbar-title v-on="on">
           <v-icon class="pl-3 pr-3">
-            {{ dropDirection === 'top' ? 'fa fa-caret-up' : 'fa fa-caret-down'}}
+            {{
+              dropDirection === 'top' ? 'fa fa-caret-up' : 'fa fa-caret-down'
+            }}
           </v-icon>
         </v-toolbar-title>
       </template>
-      <v-list
-        :min-width="menuSize"
-      >
+      <v-list :min-width="menuSize">
         <v-list-item
           dense
           v-for="(action, index) in actions"
@@ -36,7 +35,10 @@
           class="subAction"
           v-if="index > 0"
         >
-          <v-list-item-title v-text="$t(action)" @click="submitAction(action)" />
+          <v-list-item-title
+            v-text="$t(action)"
+            @click="submitAction(action)"
+          />
         </v-list-item>
       </v-list>
     </v-menu>
@@ -44,30 +46,30 @@
 </template>
 
 <script setup lang="ts">
-import {computed, ref} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
+import { computed, ref } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
 
 const props = defineProps({
   actions: {
     type: Array,
-    required: true
+    required: true,
   },
   dropDirection: {
     type: String,
     required: false,
-    default:'bottom'
+    default: 'bottom',
   },
   validationPending: {
     type: Boolean,
     required: false,
-    default: false
-  }
+    default: false,
+  },
 })
 
 const emit = defineEmits(['submit'])
 
 const mainBtn: Ref = ref(null)
-const menuSize = computed(()=>{
+const menuSize = computed(() => {
   // Btn size + 40px de padding
   return mainBtn.value?.$el.clientWidth + 40
 })
@@ -76,21 +78,20 @@ const submitAction = (action: string) => {
   emit('submit', action)
 }
 
-const mainAction: ComputedRef<string> = computed(()=>{
+const mainAction: ComputedRef<string> = computed(() => {
   return props.actions[0] as string
 })
 
-const hasOtherActions: ComputedRef<boolean> = computed(()=>{
+const hasOtherActions: ComputedRef<boolean> = computed(() => {
   return props.actions.length > 1
 })
-
 </script>
 
 <style scoped>
-.v-list-item--dense{
+.v-list-item--dense {
   min-height: 25px;
 }
-.subAction{
+.subAction {
   cursor: pointer;
 }
 </style>

+ 8 - 16
components/Ui/Card.vue

@@ -3,12 +3,7 @@ Container de type Card
 -->
 
 <template>
-  <v-card
-    elevation="2"
-    outlined
-    shaped
-    min-height="200"
-  >
+  <v-card elevation="2" outlined shaped min-height="200">
     <!-- Titre -->
     <v-card-title>
       <slot name="card.title" />
@@ -33,32 +28,29 @@ Container de type Card
 
       <slot name="card.action" />
     </v-card-actions>
-
   </v-card>
 </template>
 
 <script setup lang="ts">
-
 const props = defineProps({
   link: {
     type: String,
-    required: true
+    required: true,
   },
   model: {
     type: Object,
-    required: true
+    required: true,
   },
   entity: {
     type: Object,
-    required: true
+    required: true,
   },
-  withDeleteAction:{
+  withDeleteAction: {
     type: Boolean,
     required: false,
-    default: true
-  }
+    default: true,
+  },
 })
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 18 - 16
components/Ui/Collection.vue

@@ -4,15 +4,14 @@
   <main>
     <v-skeleton-loader v-if="pending" :type="loaderType" />
     <div v-else>
-
       <!-- Content -->
-      <slot name="list.item" v-bind="{items}" />
+      <slot name="list.item" v-bind="{ items }" />
 
       <!-- New button -->
       <v-btn v-if="newLink" class="theme-primary float-right">
         <NuxtLink :to="newLink" class="no-decoration">
           <v-icon>fa-plus-circle</v-icon>
-          <span>{{$t('add')}}</span>
+          <span>{{ $t('add') }}</span>
         </NuxtLink>
       </v-btn>
     </div>
@@ -21,38 +20,41 @@
 </template>
 
 <script setup lang="ts">
-
-import {computed, toRefs} from "@vue/reactivity";
-import type {ComputedRef, ToRefs} from "@vue/reactivity";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import type {Collection} from "~/types/data";
+import { computed, toRefs } from '@vue/reactivity'
+import type { ComputedRef, ToRefs } from '@vue/reactivity'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import type { Collection } from '~/types/data'
 
 const props = defineProps({
   model: {
     type: Object,
-    required: true
+    required: true,
   },
   parent: {
     type: Object,
-    required: false
+    required: false,
   },
   loaderType: {
     type: String,
     required: false,
-    default: 'text'
+    default: 'text',
   },
   newLink: {
     type: String,
-    required: false
-  }
+    required: false,
+  },
 })
 
 const { model, parent }: ToRefs = toRefs(props)
 
 const { fetchCollection } = useEntityFetch()
 
-const { data: collection, pending } = await fetchCollection(model.value, parent.value)
-
-const items: ComputedRef<Collection> = computed(() => collection.value ?? { items: [], pagination: {}, totalItems: 0 })
+const { data: collection, pending } = await fetchCollection(
+  model.value,
+  parent.value,
+)
 
+const items: ComputedRef<Collection> = computed(
+  () => collection.value ?? { items: [], pagination: {}, totalItems: 0 },
+)
 </script>

+ 17 - 29
components/Ui/DataTable.vue

@@ -5,10 +5,7 @@ Tableau interactif conçu pour l'affichage d'une collection d'entités
 -->
 
 <template>
-  <v-col
-    cols="12"
-    sm="12"
-  >
+  <v-col cols="12" sm="12">
     <v-data-table
       :headers="headersWithItem"
       :items="collection.items"
@@ -23,51 +20,39 @@ Tableau interactif conçu pour l'affichage d'une collection d'entités
       </template>
 
       <template #item.actions="{ item }">
-        <v-icon
-          small
-          class="mr-2"
-          @click="editItem(item)"
-        >
-          mdi-pencil
-        </v-icon>
-        <v-icon
-          small
-          @click="deleteItem(item)"
-        >
-          mdi-delete
-        </v-icon>
+        <v-icon small class="mr-2" @click="editItem(item)"> mdi-pencil </v-icon>
+        <v-icon small @click="deleteItem(item)"> mdi-delete </v-icon>
       </template>
     </v-data-table>
   </v-col>
 </template>
 
 <script setup lang="ts">
-
-import {ref, toRefs} from "@vue/reactivity";
-import type {Ref} from "@vue/reactivity";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import ApiResource from "~/models/ApiResource";
-import type {AnyJson} from "~/types/data";
+import { ref, toRefs } from '@vue/reactivity'
+import type { Ref } from '@vue/reactivity'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import ApiResource from '~/models/ApiResource'
+import type { AnyJson } from '~/types/data'
 
 const props = defineProps({
   parent: {
     type: Object,
-    required: true
+    required: true,
   },
   model: {
     type: Object,
-    required: true
+    required: true,
   },
   headers: {
     type: Array,
-    required: true
-  }
+    required: true,
+  },
 })
 
 const { parent, model, headers } = toRefs(props)
 
 const headersWithItem = computed(() => {
-  return headers.value.map((header:any) => {
+  return headers.value.map((header: any) => {
     header.item = 'item.' + header.value
     return header
   })
@@ -78,7 +63,10 @@ const entries: Ref<Array<AnyJson>> = ref(Array<AnyJson>())
 
 const { fetchCollection } = useEntityFetch()
 
-const { data: collection, pending } = await fetchCollection(model.value as typeof ApiResource, parent.value as ApiResource)
+const { data: collection, pending } = await fetchCollection(
+  model.value as typeof ApiResource,
+  parent.value as ApiResource,
+)
 
 const itemId: Ref<number> = ref(0)
 

+ 25 - 28
components/Ui/DatePicker.vue

@@ -7,26 +7,26 @@ Sélecteur de dates
 <template>
   <main>
     <VueDatePicker
-        :model-value="modelValue"
-        :locale="i18n.locale.value"
-        :format-locale="fnsLocale"
-        :format="dateFormat"
-        :enable-time-picker="withTime"
-        :teleport="true"
-        text-input
-        :auto-apply="true"
-        :select-text="$t('select')"
-        :cancel-text="$t('cancel')"
-        :disabled="readonly"
-        :position="position"
-        @update:model-value="onUpdate"
+      :model-value="modelValue"
+      :locale="i18n.locale.value"
+      :format-locale="fnsLocale"
+      :format="dateFormat"
+      :enable-time-picker="withTime"
+      :teleport="true"
+      text-input
+      :auto-apply="true"
+      :select-text="$t('select')"
+      :cancel-text="$t('cancel')"
+      :disabled="readonly"
+      :position="position"
+      @update:model-value="onUpdate"
     />
   </main>
 </template>
 
 <script setup lang="ts">
-import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
-import type {PropType} from "@vue/runtime-core";
+import DateUtils, { supportedLocales } from '~/services/utils/dateUtils'
+import type { PropType } from '@vue/runtime-core'
 
 const i18n = useI18n()
 
@@ -36,22 +36,22 @@ const props = defineProps({
   modelValue: {
     type: Object as PropType<Date>,
     required: false,
-    default: null
+    default: null,
   },
   readonly: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   format: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   withTime: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * @see https://vue3datepicker.com/props/positioning/#position
@@ -59,13 +59,13 @@ const props = defineProps({
   position: {
     type: String as PropType<'left' | 'center' | 'right'>,
     required: false,
-    default: 'center'
-  }
+    default: 'center',
+  },
 })
 
-const defaultFormatPattern = props.withTime ?
-    DateUtils.getFormatPattern(i18n.locale.value as supportedLocales) :
-    DateUtils.getShortFormatPattern(i18n.locale.value as supportedLocales)
+const defaultFormatPattern = props.withTime
+  ? DateUtils.getFormatPattern(i18n.locale.value as supportedLocales)
+  : DateUtils.getShortFormatPattern(i18n.locale.value as supportedLocales)
 
 const dateFormat: Ref<string> = ref(props.format ?? defaultFormatPattern)
 
@@ -74,9 +74,6 @@ const emit = defineEmits(['update:model-value'])
 const onUpdate = (event: Date) => {
   emit('update:model-value', event)
 }
-
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 69 - 66
components/Ui/DateRangePicker.vue

@@ -1,43 +1,43 @@
 <template>
   <!-- @see https://vue3datepicker.com/props/modes/#multi-calendars -->
   <VueDatePicker
-      :model-value="modelValue"
-      range
-      multi-calendars
-      :auto-apply="autoApply"
-      :locale="i18n.locale.value"
-      :format-locale="fnsLocale"
-      :format="dateFormatPattern"
-      :start-date="today"
-      :teleport="true"
-      :alt-position="dateRangePickerAltPosition"
-      :enable-time-picker="false"
-      close-on-scroll
-      text-input
-      :select-text="$t('select')"
-      :cancel-text="$t('cancel')"
-      input-class-name="date-range-picker-input"
-      @update:model-value="updateDateTimeRange"
-      class="date-range-picker"
-      :style="style"
+    :model-value="modelValue"
+    range
+    multi-calendars
+    :auto-apply="autoApply"
+    :locale="i18n.locale.value"
+    :format-locale="fnsLocale"
+    :format="dateFormatPattern"
+    :start-date="today"
+    :teleport="true"
+    :alt-position="dateRangePickerAltPosition"
+    :enable-time-picker="false"
+    close-on-scroll
+    text-input
+    :select-text="$t('select')"
+    :cancel-text="$t('cancel')"
+    input-class-name="date-range-picker-input"
+    @update:model-value="updateDateTimeRange"
+    class="date-range-picker"
+    :style="style"
   />
 </template>
 
 <script setup lang="ts">
-import DateUtils, {supportedLocales} from "~/services/utils/dateUtils";
-import type {PropType} from "@vue/runtime-core";
+import DateUtils, { supportedLocales } from '~/services/utils/dateUtils'
+import type { PropType } from '@vue/runtime-core'
 
 const props = defineProps({
   modelValue: {
     type: Array as PropType<Array<Date> | null>,
     required: false,
-    default: null
+    default: null,
   },
   maxHeight: {
     type: Number,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const emit = defineEmits(['update:modelValue'])
@@ -51,13 +51,16 @@ const updateDateTimeRange = (value: [string, string]) => {
 const i18n = useI18n()
 
 const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
-const dateFormatPattern = DateUtils.getShortFormatPattern(i18n.locale.value as supportedLocales)
+const dateFormatPattern = DateUtils.getShortFormatPattern(
+  i18n.locale.value as supportedLocales,
+)
 
 const today = new Date()
 
-let style = '';
+let style = ''
 if (props.maxHeight !== null) {
-  style += 'height: ' + props.maxHeight + 'px;max-height: ' + props.maxHeight + 'px;'
+  style +=
+    'height: ' + props.maxHeight + 'px;max-height: ' + props.maxHeight + 'px;'
 }
 
 /**
@@ -70,57 +73,57 @@ const dateRangePickerAltPosition = (el: HTMLElement) => {
   const rightPadding = 30
   const rect = el.getBoundingClientRect()
 
-  if ((rect.left + fullWidth + rightPadding) > window.innerWidth) {
+  if (rect.left + fullWidth + rightPadding > window.innerWidth) {
     xOffset = window.innerWidth - (rect.left + fullWidth + rightPadding)
   }
 
   return {
     top: rect.bottom,
-    left: rect.left + xOffset
+    left: rect.left + xOffset,
   }
 }
 </script>
 
 <style lang="scss">
+// @see https://vue3datepicker.com/customization/theming/
+// [!] Sass variables overriding does not work in scoped mode
+.dp__theme_light,
+.dp__theme_dark {
+  --dp-background-color: #ffffff;
+  --dp-text-color: #212121;
+  --dp-hover-color: #f3f3f3;
+  --dp-hover-text-color: #212121;
+  --dp-hover-icon-color: #959595;
+  --dp-primary-color: rgb(var(--v-theme-primary)) !important;
+  --dp-primary-text-color: rgb(var(--v-theme-on-primary)) !important;
+  --dp-secondary-color: rgb(var(--v-theme-neutral-strong)) !important;
+  --dp-border-color: #ddd;
+  --dp-menu-border-color: #ddd;
+  --dp-border-color-hover: #aaaeb7;
+  --dp-disabled-color: #f6f6f6;
+  --dp-scroll-bar-background: #f3f3f3;
+  --dp-scroll-bar-color: #959595;
+  --dp-success-color: rgb(var(--v-theme-success)) !important;
+  --dp-success-color-disabled: rgb(var(--v-theme-neutral-strong)) !important;
+  --dp-icon-color: #959595;
+  --dp-danger-color: #ff6f60;
+  --dp-highlight-color: rgba(25, 118, 210, 0.1);
+}
 
-  // @see https://vue3datepicker.com/customization/theming/
-  // [!] Sass variables overriding does not work in scoped mode
-  .dp__theme_light, .dp__theme_dark {
-    --dp-background-color: #ffffff;
-    --dp-text-color: #212121;
-    --dp-hover-color: #f3f3f3;
-    --dp-hover-text-color: #212121;
-    --dp-hover-icon-color: #959595;
-    --dp-primary-color: rgb(var(--v-theme-primary)) !important;
-    --dp-primary-text-color: rgb(var(--v-theme-on-primary)) !important;
-    --dp-secondary-color: rgb(var(--v-theme-neutral-strong)) !important;
-    --dp-border-color: #ddd;
-    --dp-menu-border-color: #ddd;
-    --dp-border-color-hover: #aaaeb7;
-    --dp-disabled-color: #f6f6f6;
-    --dp-scroll-bar-background: #f3f3f3;
-    --dp-scroll-bar-color: #959595;
-    --dp-success-color: rgb(var(--v-theme-success)) !important;
-    --dp-success-color-disabled: rgb(var(--v-theme-neutral-strong)) !important;
-    --dp-icon-color: #959595;
-    --dp-danger-color: #ff6f60;
-    --dp-highlight-color: rgba(25, 118, 210, 0.1);
+.date-range-picker {
+  div {
+    height: 100% !important;
+    max-height: 100% !important;
   }
 
-  .date-range-picker {
-    div {
-      height: 100% !important;
-      max-height: 100% !important;
-    }
-
-    .dp__input_wrap {
-      height: 100% !important;
-      max-height: 100% !important;
-    }
-
-    .date-range-picker-input {
-      height: 100% !important;
-      max-height: 100% !important;
-    }
+  .dp__input_wrap {
+    height: 100% !important;
+    max-height: 100% !important;
   }
+
+  .date-range-picker-input {
+    height: 100% !important;
+    max-height: 100% !important;
+  }
+}
 </style>

+ 30 - 30
components/Ui/ExpansionPanel.vue

@@ -25,47 +25,47 @@ Panneaux déroulants de type "accordéon"
 const props = defineProps({
   title: {
     type: String,
-    required: true
+    required: true,
   },
   icon: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 </script>
 
 <style scoped>
-  .icon {
-    width: 47px;
-    height: 47px;
-    padding: 10px;
-    margin-right: 10px;
-    flex: none !important;
-  }
+.icon {
+  width: 47px;
+  height: 47px;
+  padding: 10px;
+  margin-right: 10px;
+  flex: none !important;
+}
 
-  .v-expansion-panel-header {
-    padding: 0;
-    padding-right: 20px;
-  }
+.v-expansion-panel-header {
+  padding: 0;
+  padding-right: 20px;
+}
 
-  .v-expansion-panel-title {
-    padding-left: 0;
-    padding-top: 0;
-    padding-bottom: 0;
-    max-height: 47px;
-    min-height: 47px;
-  }
+.v-expansion-panel-title {
+  padding-left: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  max-height: 47px;
+  min-height: 47px;
+}
 
-  .v-expansion-panel--active > .v-expansion-panel-title {
-    min-height: 47px !important;
-  }
+.v-expansion-panel--active > .v-expansion-panel-title {
+  min-height: 47px !important;
+}
 
-  :deep(.v-expansion-panel-title__icon > .v-icon) {
-    font-size: 16px;
-  }
+:deep(.v-expansion-panel-title__icon > .v-icon) {
+  font-size: 16px;
+}
 
-  .icon {
-    text-align: center;
-  }
+.icon {
+  text-align: center;
+}
 </style>

+ 52 - 51
components/Ui/Form.vue

@@ -10,20 +10,20 @@ de quitter si des données ont été modifiées.
 <template>
   <LayoutContainer>
     <v-form
-        v-model="isValid"
-        ref="form"
-        :readonly="readonly"
-        @submit.prevent=""
+      v-model="isValid"
+      ref="form"
+      :readonly="readonly"
+      @submit.prevent=""
     >
       <!-- Top action bar -->
       <v-container
-          v-if="actionPosition === 'both' || actionPosition === 'top'"
-          :fluid="true"
-          class="container btnActions"
+        v-if="actionPosition === 'both' || actionPosition === 'top'"
+        :fluid="true"
+        class="container btnActions"
       >
         <v-row>
           <v-col cols="12" sm="12">
-            <slot name="form.button"/>
+            <slot name="form.button" />
 
             <UiButtonSubmit
               v-if="!readonly"
@@ -36,17 +36,17 @@ de quitter si des données ont été modifiées.
       </v-container>
 
       <!-- Content -->
-      <slot v-bind="{model, entity}"/>
+      <slot v-bind="{ model, entity }" />
 
       <!-- Bottom action bar -->
       <v-container
-          v-if="actionPosition === 'both' || actionPosition === 'bottom'"
-          :fluid="true"
-          class="container btnActions mt-6"
+        v-if="actionPosition === 'both' || actionPosition === 'bottom'"
+        :fluid="true"
+        class="container btnActions mt-6"
       >
         <v-row>
           <v-col cols="12" sm="12">
-            <slot name="form.button"/>
+            <slot name="form.button" />
 
             <UiButtonSubmit
               @submit="submit"
@@ -59,20 +59,21 @@ de quitter si des données ont été modifiées.
     </v-form>
 
     <!-- Confirmation dialog -->
-    <LazyLayoutDialog
-      :show="isConfirmationDialogShowing"
-    >
+    <LazyLayoutDialog :show="isConfirmationDialogShowing">
       <template #dialogText>
         <v-card-title class="text-h5 theme-neutral">
           {{ $t('caution') }}
         </v-card-title>
         <v-card-text>
-          <br>
+          <br />
           <p>{{ $t('quit_without_saving_warning') }}</p>
         </v-card-text>
       </template>
       <template #dialogBtn>
-        <v-btn class="mr-4 submitBtn theme-primary" @click="closeConfirmationDialog">
+        <v-btn
+          class="mr-4 submitBtn theme-primary"
+          @click="closeConfirmationDialog"
+        >
           {{ $t('back_to_form') }}
         </v-btn>
         <v-btn class="mr-4 submitBtn theme-primary" @click="saveAndQuit">
@@ -83,24 +84,23 @@ de quitter si des données ont été modifiées.
         </v-btn>
       </template>
     </LazyLayoutDialog>
-
   </LayoutContainer>
 </template>
 
 <script setup lang="ts">
-import {computed, ref} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
-import {FORM_FUNCTION, SUBMIT_TYPE, TYPE_ALERT} from "~/types/enum/enums";
-import { useFormStore } from "~/stores/form";
-import type {Route, RouteLocationRaw} from "@intlify/vue-router-bridge";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import ApiModel from "~/models/ApiModel";
-import {usePageStore} from "~/stores/page";
-import {watch} from "@vue/runtime-core";
-import type {PropType} from "@vue/runtime-core";
-import type {AnyJson} from "~/types/data";
+import { computed, ref } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import { FORM_FUNCTION, SUBMIT_TYPE, TYPE_ALERT } from '~/types/enum/enums'
+import { useFormStore } from '~/stores/form'
+import type { Route, RouteLocationRaw } from '@intlify/vue-router-bridge'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import ApiModel from '~/models/ApiModel'
+import { usePageStore } from '~/stores/page'
+import { watch } from '@vue/runtime-core'
+import type { PropType } from '@vue/runtime-core'
+import type { AnyJson } from '~/types/data'
 import * as _ from 'lodash-es'
-import {useRefreshProfile} from "~/composables/data/useRefreshProfile";
+import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
 
 const props = defineProps({
   /**
@@ -108,26 +108,26 @@ const props = defineProps({
    */
   model: {
     type: Function as any as () => typeof ApiModel,
-    required: true
+    required: true,
   },
   /**
    * Instance de l'objet
    */
   entity: {
     type: Object as () => ApiModel,
-    required: true
+    required: true,
   },
   /**
    * TODO: compléter
    */
   onChanged: {
     type: Function,
-    required: false
+    required: false,
   },
   goBackRoute: {
     type: Object as PropType<RouteLocationRaw>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Types de soumission disponibles (enregistrer / enregistrer et quitter)
@@ -139,7 +139,7 @@ const props = defineProps({
       let actions: AnyJson = {}
       actions[SUBMIT_TYPE.SAVE] = {}
       return actions
-    }
+    },
   },
   /**
    * La validation est en cours
@@ -147,7 +147,7 @@ const props = defineProps({
   validationPending: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Faut-il rafraichir le profil à la soumission du formulaire?
@@ -155,13 +155,13 @@ const props = defineProps({
   refreshProfile: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   actionPosition: {
     type: String as PropType<'top' | 'bottom' | 'both'>,
     required: false,
-    default: 'both'
-  }
+    default: 'both',
+  },
 })
 
 // ### Définitions
@@ -203,7 +203,7 @@ const closeConfirmationDialog = () => {
  *
  * @param next
  */
-const submit = async (next: string|null = null) => {
+const submit = async (next: string | null = null) => {
   if (props.validationPending) {
     return
   }
@@ -238,18 +238,21 @@ const submit = async (next: string|null = null) => {
     } else if (next === SUBMIT_TYPE.SAVE_AND_BACK) {
       onSaveAndQuitAction(actionArgs)
     }
-
   } catch (error: any) {
-
-    if (error.response && error.response.status === 422 && error.response.data['violations']) {
-
+    if (
+      error.response &&
+      error.response.status === 422 &&
+      error.response.data['violations']
+    ) {
       // TODO: à revoir
       const violations: Array<string> = []
       let fields: AnyJson = {}
 
       for (const violation of error.response.data['violations']) {
         violations.push(i18n.t(violation['message']) as string)
-        fields = Object.assign(fields, {[violation['propertyPath']] : violation['message']})
+        fields = Object.assign(fields, {
+          [violation['propertyPath']]: violation['message'],
+        })
       }
 
       useFormStore().addViolation(fields)
@@ -280,7 +283,7 @@ const saveAndQuit = async () => {
  * @param route
  * @param id
  */
-function onSaveAction(route: Route, id: number){
+function onSaveAction(route: Route, id: number) {
   if (useFormStore().formFunction === FORM_FUNCTION.CREATE) {
     route.path += id
     navigateTo(route)
@@ -294,7 +297,7 @@ function onSaveAction(route: Route, id: number){
  *
  * @param route
  */
-function onSaveAndQuitAction(route: Route){
+function onSaveAndQuitAction(route: Route) {
   navigateTo(route)
 }
 
@@ -314,7 +317,7 @@ const cancel = () => {
   }
 }
 
-const actions = computed(()=>{
+const actions = computed(() => {
   return _.keys(props.submitActions)
 })
 
@@ -346,7 +349,6 @@ const validate = async function () {
   errors.value = validation.errors
 }
 
-
 // #### Gestion de l'état dirty
 watch(props.entity, async (newEntity, oldEntity) => {
   setIsDirty(true)
@@ -372,7 +374,6 @@ const setIsDirty = (dirty: boolean) => {
 }
 
 defineExpose({ validate })
-
 </script>
 
 <style scoped>

+ 13 - 21
components/Ui/Form/Creation.vue

@@ -1,27 +1,22 @@
 <template>
-  <UiForm
-      :model="model"
-      :entity="entity"
-      :submitActions="submitActions"
-  >
+  <UiForm :model="model" :entity="entity" :submitActions="submitActions">
     <template #form.button>
       <v-btn v-if="goBackRoute" class="theme-neutral mr-3" @click="quit">
         {{ $t('cancel') }}
       </v-btn>
     </template>
 
-    <slot v-bind="{model, entity}"/>
+    <slot v-bind="{ model, entity }" />
   </UiForm>
 </template>
 
 <script setup lang="ts">
-
-import type {PropType} from "@vue/runtime-core";
-import type {RouteLocationRaw} from "@intlify/vue-router-bridge";
-import ApiModel from "~/models/ApiModel";
-import type {AnyJson} from "~/types/data";
-import {SUBMIT_TYPE} from "~/types/enum/enums";
-import {useEntityManager} from "~/composables/data/useEntityManager";
+import type { PropType } from '@vue/runtime-core'
+import type { RouteLocationRaw } from '@intlify/vue-router-bridge'
+import ApiModel from '~/models/ApiModel'
+import type { AnyJson } from '~/types/data'
+import { SUBMIT_TYPE } from '~/types/enum/enums'
+import { useEntityManager } from '~/composables/data/useEntityManager'
 
 const props = defineProps({
   /**
@@ -29,7 +24,7 @@ const props = defineProps({
    */
   model: {
     type: Function as any as () => typeof ApiModel,
-    required: true
+    required: true,
   },
   /**
    * Route de retour
@@ -37,7 +32,7 @@ const props = defineProps({
   goBackRoute: {
     type: Object as PropType<RouteLocationRaw>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * La validation est en cours
@@ -45,7 +40,7 @@ const props = defineProps({
   validationPending: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Faut-il rafraichir le profil à la soumission du formulaire ?
@@ -53,7 +48,7 @@ const props = defineProps({
   refreshProfile: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
 })
 
@@ -81,9 +76,6 @@ const quit = () => {
 
   router.push(props.goBackRoute)
 }
-
 </script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 22 - 28
components/Ui/Form/Edition.vue

@@ -2,10 +2,10 @@
   <LayoutContainer>
     <UiLoadingPanel v-if="pending" />
     <UiForm
-        v-else
-        :model="model"
-        :entity="entity"
-        :submitActions="submitActions"
+      v-else
+      :model="model"
+      :entity="entity"
+      :submitActions="submitActions"
     >
       <template #form.button>
         <v-btn v-if="goBackRoute" class="theme-neutral mr-3" @click="quit">
@@ -13,20 +13,19 @@
         </v-btn>
       </template>
 
-      <slot v-bind="{model, entity}"/>
+      <slot v-bind="{ model, entity }" />
     </UiForm>
   </LayoutContainer>
 </template>
 
 <script setup lang="ts">
-
-import type {PropType} from "@vue/runtime-core";
-import type {RouteLocationRaw} from "@intlify/vue-router-bridge";
-import ApiModel from "~/models/ApiModel";
-import type {AnyJson} from "~/types/data";
-import {SUBMIT_TYPE} from "~/types/enum/enums";
-import {useRoute} from "vue-router";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
+import type { PropType } from '@vue/runtime-core'
+import type { RouteLocationRaw } from '@intlify/vue-router-bridge'
+import ApiModel from '~/models/ApiModel'
+import type { AnyJson } from '~/types/data'
+import { SUBMIT_TYPE } from '~/types/enum/enums'
+import { useRoute } from 'vue-router'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
 
 const props = defineProps({
   /**
@@ -34,7 +33,7 @@ const props = defineProps({
    */
   model: {
     type: Function as any as () => typeof ApiModel,
-    required: true
+    required: true,
   },
   /**
    * Id de l'objet
@@ -43,7 +42,7 @@ const props = defineProps({
   id: {
     type: Number,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Route de retour
@@ -51,7 +50,7 @@ const props = defineProps({
   goBackRoute: {
     type: Object as PropType<RouteLocationRaw>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * La validation est en cours
@@ -59,7 +58,7 @@ const props = defineProps({
   validationPending: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Faut-il rafraichir le profil à la soumission du formulaire ?
@@ -67,20 +66,18 @@ const props = defineProps({
   refreshProfile: {
     type: Boolean,
     required: false,
-    default: false
-  }
+    default: false,
+  },
 })
 
 const { fetch } = useEntityFetch()
 const route = useRoute()
 const router = useRouter()
 
-const entityId = props.id !== null ? props.id : parseInt(route.params.id as string)
+const entityId =
+  props.id !== null ? props.id : parseInt(route.params.id as string)
 
-const { data: entity, pending } = fetch(
-    props.model,
-    entityId
-)
+const { data: entity, pending } = fetch(props.model, entityId)
 
 const submitActions = computed(() => {
   let actions: AnyJson = {}
@@ -100,9 +97,6 @@ const quit = () => {
 
   router.push(props.goBackRoute)
 }
-
 </script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 10 - 8
components/Ui/Help.vue

@@ -29,34 +29,34 @@
 </template>
 
 <script setup lang="ts">
-import type {Ref} from "@vue/reactivity";
+import type { Ref } from '@vue/reactivity'
 
 const props = defineProps({
   left: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   right: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   top: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   bottom: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   icon: {
     type: String,
     required: false,
-    default: 'mdi-help-circle'
-  }
+    default: 'mdi-help-circle',
+  },
 })
 
 const { $refs } = useNuxtApp()
@@ -67,7 +67,9 @@ const show: Ref<Boolean> = ref(false)
 const iconRef = ref(null)
 
 // Left is the default, set it to true if not any other is true
-const leftOrDefault: Ref<Boolean> = ref(props.left || (!props.right && !props.bottom && !props.top))
+const leftOrDefault: Ref<Boolean> = ref(
+  props.left || (!props.right && !props.bottom && !props.top),
+)
 
 const onIconClicked = (e: any) => {
   show.value = !show.value

+ 58 - 50
components/Ui/Image.vue

@@ -4,7 +4,7 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
 -->
 <template>
   <main>
-    <div class="image-wrapper" :style="{width: width + 'px'}">
+    <div class="image-wrapper" :style="{ width: width + 'px' }">
       <v-img
         :src="imageSrc ?? undefined"
         :lazy-src="defaultImagePath"
@@ -19,14 +19,15 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
             justify="center"
             v-if="pending"
           >
-            <v-progress-circular
-              :indeterminate="true"
-              color="neutral"
-            />
+            <v-progress-circular :indeterminate="true" color="neutral" />
           </v-row>
         </template>
 
-        <div v-if="!pending && overlayIcon" class="overlay" @click="emit('overlay-clicked')">
+        <div
+          v-if="!pending && overlayIcon"
+          class="overlay"
+          @click="emit('overlay-clicked')"
+        >
           <v-icon>{{ overlayIcon }}</v-icon>
         </div>
       </v-img>
@@ -35,10 +36,10 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
 </template>
 
 <script setup lang="ts">
-import {useImageFetch} from "~/composables/data/useImageFetch";
-import ImageManager from "~/services/data/imageManager";
-import type {WatchStopHandle} from "@vue/runtime-core";
-import type {Ref} from "@vue/reactivity";
+import { useImageFetch } from '~/composables/data/useImageFetch'
+import ImageManager from '~/services/data/imageManager'
+import type { WatchStopHandle } from '@vue/runtime-core'
+import type { Ref } from '@vue/reactivity'
 
 const props = defineProps({
   /**
@@ -47,28 +48,28 @@ const props = defineProps({
   imageId: {
     type: Number as PropType<number | null>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Image par défaut
    */
   defaultImage: {
     type: String,
-    required: false
+    required: false,
   },
   /**
    * Hauteur de l'image à l'écran (en px)
    */
   height: {
     type: Number,
-    required: false
+    required: false,
   },
   /**
    * Largeur de l'image à l'écran (en px)
    */
   width: {
     type: Number,
-    required: false
+    required: false,
   },
   /**
    * Icône à afficher en overlay au survol de la souris
@@ -76,8 +77,8 @@ const props = defineProps({
   overlayIcon: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const { fetch } = useImageFetch()
@@ -88,7 +89,11 @@ const emit = defineEmits(['overlay-clicked'])
 
 const fileId = toRef(props, 'imageId')
 
-const { data: imageSrc, pending, refresh: refreshImage } = await fetch(fileId, defaultImagePath, props.height, props.width) as any
+const {
+  data: imageSrc,
+  pending,
+  refresh: refreshImage,
+} = (await fetch(fileId, defaultImagePath, props.height, props.width)) as any
 
 const refresh = () => {
   refreshImage()
@@ -98,9 +103,12 @@ defineExpose({ refresh })
 /**
  * Si l'id change, on recharge l'image
  */
-const unwatch: WatchStopHandle = watch(() => props.imageId, async (value, oldValue) => {
-  refresh()
-})
+const unwatch: WatchStopHandle = watch(
+  () => props.imageId,
+  async (value, oldValue) => {
+    refresh()
+  },
+)
 
 /**
  * Lorsqu'on démonte le component, on supprime le watcher
@@ -111,38 +119,38 @@ onUnmounted(() => {
 </script>
 
 <style lang="scss">
-  div.image-wrapper {
-    display: block;
-    position: relative;
+div.image-wrapper {
+  display: block;
+  position: relative;
 
-    img {
-      display: block;
-      max-width: 100%;
-    }
+  img {
+    display: block;
+    max-width: 100%;
+  }
 
-    .overlay {
-      position: absolute;
-      top: 0;
-      bottom: 0;
-      left: 0;
-      right: 0;
-      height: 100%;
-      width: 100%;
-      opacity: 0;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      transition: .3s ease;
-    }
-    .overlay:hover {
-      opacity: 0.8;
-      background-color: rgb(var(--v-theme-neutral-strong));
-      cursor: pointer;
-    }
+  .overlay {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 100%;
+    width: 100%;
+    opacity: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: 0.3s ease;
+  }
+  .overlay:hover {
+    opacity: 0.8;
+    background-color: rgb(var(--v-theme-neutral-strong));
+    cursor: pointer;
+  }
 
-    .overlay .v-icon {
-      color: rgb(var(--v-theme-on-neutral-strong));
-      font-size: 36px;
-    }
+  .overlay .v-icon {
+    color: rgb(var(--v-theme-on-neutral-strong));
+    font-size: 36px;
   }
+}
 </style>

+ 92 - 67
components/Ui/Input/Autocomplete.vue

@@ -8,45 +8,49 @@ Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
   <main>
     <!--suppress TypeScriptValidateTypes -->
     <v-autocomplete
-        :model-value="modelValue"
-        autocomplete="search"
-        :items="items"
-        :label="$t(fieldLabel)"
-        :item-title="itemTitle"
-        :item-value="itemValue"
-        :no-filter="noFilter"
-        :auto-select-first="autoSelectFirst"
-        :multiple="multiple"
-        :loading="isLoading"
-        :return-object="returnObject"
-        :search-input.sync="search"
-        :prepend-icon="prependIcon"
-        :error="error || !!fieldViolations"
-        :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
-        :rules="rules"
-        :chips="chips"
-        :hide-no-data="hideNoData"
-        :no-data-text="isLoading ? $t('please_wait') : $t('no_result_matching_your_request')"
-        :variant="variant"
-        @update:model-value="onUpdate"
-        @update:search="emit('update:search', $event)"
-        @update:menu="emit('update:menu', $event)"
-        @update:focused="emit('update:focused', $event)"
+      :model-value="modelValue"
+      autocomplete="search"
+      :items="items"
+      :label="$t(fieldLabel)"
+      :item-title="itemTitle"
+      :item-value="itemValue"
+      :no-filter="noFilter"
+      :auto-select-first="autoSelectFirst"
+      :multiple="multiple"
+      :loading="isLoading"
+      :return-object="returnObject"
+      :search-input.sync="search"
+      :prepend-icon="prependIcon"
+      :error="error || !!fieldViolations"
+      :error-messages="
+        errorMessage || fieldViolations ? $t(fieldViolations) : ''
+      "
+      :rules="rules"
+      :chips="chips"
+      :hide-no-data="hideNoData"
+      :no-data-text="
+        isLoading ? $t('please_wait') : $t('no_result_matching_your_request')
+      "
+      :variant="variant"
+      @update:model-value="onUpdate"
+      @update:search="emit('update:search', $event)"
+      @update:menu="emit('update:menu', $event)"
+      @update:focused="emit('update:focused', $event)"
     >
       <template v-if="slotText" #item="data">
-<!--        <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>-->
+        <!--        <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>-->
       </template>
     </v-autocomplete>
   </main>
 </template>
 
 <script setup lang="ts">
-import {computed} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import ObjectUtils from "~/services/utils/objectUtils";
-import type {AnyJson} from "~/types/data";
-import type {PropType} from "@vue/runtime-core";
+import { computed } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
+import ObjectUtils from '~/services/utils/objectUtils'
+import type { AnyJson } from '~/types/data'
+import type { PropType } from '@vue/runtime-core'
 
 const props = defineProps({
   /**
@@ -55,7 +59,7 @@ const props = defineProps({
   modelValue: {
     type: [String, Number, Object, Array] as PropType<any>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -65,7 +69,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -74,7 +78,7 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Liste des éléments de la liste
@@ -83,7 +87,7 @@ const props = defineProps({
   items: {
     type: Array as PropType<Array<Object>>,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Définit si le champ est en lecture seule
@@ -91,7 +95,7 @@ const props = defineProps({
    */
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Le model est l'objet lui-même, et non pas son id (ou la propriété définie avec itemValue)
@@ -99,7 +103,7 @@ const props = defineProps({
    */
   returnObject: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Autorise la sélection multiple
@@ -107,7 +111,7 @@ const props = defineProps({
    */
   multiple: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Propriété de l'objet à utiliser comme label
@@ -115,7 +119,7 @@ const props = defineProps({
    */
   itemTitle: {
     type: String,
-    default: 'title'
+    default: 'title',
   },
   /**
    * Propriété de l'objet à utiliser comme clé (et correspondant au v-model)
@@ -123,14 +127,14 @@ const props = defineProps({
    */
   itemValue: {
     type: String,
-    default: 'id'
+    default: 'id',
   },
   /**
    * Icône de gauche
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-prepend-icon
    */
   prependIcon: {
-    type: String
+    type: String,
   },
   /**
    * Rends les résultats sous forme de puces
@@ -138,7 +142,7 @@ const props = defineProps({
    */
   chips: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Le contenu de la liste est en cours de chargement
@@ -146,7 +150,7 @@ const props = defineProps({
   isLoading: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Propriété de l'objet utilisé pour grouper les items ; laisser null pour ne pas grouper
@@ -154,7 +158,7 @@ const props = defineProps({
   group: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-hide-no-data
@@ -162,32 +166,32 @@ const props = defineProps({
   hideNoData: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   // TODO: c'est quoi?
   slotText: {
     type: Array,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-no-filter
    */
   noFilter: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-auto-select-first
    */
   autoSelectFirst: {
     type: Boolean,
-    default: true
+    default: true,
   },
   // TODO: c'est quoi?
   translate: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Règles de validation
@@ -196,14 +200,14 @@ const props = defineProps({
   rules: {
     type: Array,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Le champ est-il actuellement en état d'erreur
    */
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Si le champ est en état d'erreur, quel est le message d'erreur?
@@ -211,27 +215,41 @@ const props = defineProps({
   errorMessage: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
    */
   variant: {
-    type: String as PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled" | undefined>,
+    type: String as PropType<
+      | 'filled'
+      | 'outlined'
+      | 'plain'
+      | 'underlined'
+      | 'solo'
+      | 'solo-inverted'
+      | 'solo-filled'
+      | undefined
+    >,
     required: false,
-    default: 'filled'
-  }
+    default: 'filled',
+  },
 })
 
 const i18n = useI18n()
 
-const search: Ref<string|null> = ref(null)
+const search: Ref<string | null> = ref(null)
 
 const fieldLabel: string = props.label ?? props.field
 
-const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
+const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
-const emit = defineEmits(['update:model-value', 'update:search', 'update:focused', 'update:menu'])
+const emit = defineEmits([
+  'update:model-value',
+  'update:search',
+  'update:focused',
+  'update:menu',
+])
 
 const onUpdate = (event: string) => {
   updateViolationState(event)
@@ -292,16 +310,17 @@ const prepareGroups = (groupedItems: Array<Array<string>>): Array<AnyJson> => {
   let finalItems: Array<AnyJson> = []
 
   for (const group in groupedItems) {
-
     // Si un groupe est présent, alors on créé le groupe options header
     if (group !== 'undefined') {
-      finalItems.push({header: i18n.t(group as string)})
+      finalItems.push({ header: i18n.t(group as string) })
     }
 
     // On parcourt les items pour préparer les texts / slotTexts à afficher
-    finalItems = finalItems.concat(groupedItems[group].map((item: any) => {
-      return prepareItem(item)
-    }))
+    finalItems = finalItems.concat(
+      groupedItems[group].map((item: any) => {
+        return prepareItem(item)
+      }),
+    )
   }
   return finalItems
 }
@@ -321,16 +340,22 @@ const prepareItem = (item: Object): AnyJson => {
   // Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
   if (props.slotText) {
     for (const text of props.slotText) {
-      slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
+      slotTextDisplay.push(
+        props.translate ? i18n.t(item[text as string]) : item[text as string],
+      )
     }
   }
 
   for (const text of props.itemTitle) {
-    itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
+    itemTextDisplay.push(
+      props.translate ? i18n.t(item[text as string]) : item[text as string],
+    )
   }
 
   // On reconstruit l'objet
-  return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
+  return Object.assign({}, item, {
+    itemTextDisplay: itemTextDisplay.join(' '),
+    slotTextDisplay: slotTextDisplay.join(' '),
+  })
 }
-
 </script>

+ 62 - 50
components/Ui/Input/Autocomplete/Accesses.vue

@@ -7,34 +7,34 @@ Champs autocomplete dédié à la recherche des access d'une structure
 <template>
   <main>
     <UiInputAutocomplete
-        :model-value="modelValue"
-        :field="field"
-        :label="label"
-        :items="items"
-        item-value="id"
-        :isLoading="pending"
-        :multiple="multiple"
-        hide-no-data
-        :chips="chips"
-        :auto-select-first="false"
-        prependIcon="fas fa-magnifying-glass"
-        :return-object="false"
-        :variant="variant"
-        @update:model-value="onUpdateModelValue"
-        @update:search="onUpdateSearch"
+      :model-value="modelValue"
+      :field="field"
+      :label="label"
+      :items="items"
+      item-value="id"
+      :isLoading="pending"
+      :multiple="multiple"
+      hide-no-data
+      :chips="chips"
+      :auto-select-first="false"
+      prependIcon="fas fa-magnifying-glass"
+      :return-object="false"
+      :variant="variant"
+      @update:model-value="onUpdateModelValue"
+      @update:search="onUpdateSearch"
     />
   </main>
 </template>
 
 <script setup lang="ts">
-import type {PropType} from "@vue/runtime-core";
-import {computed} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
-import type {AnyJson, AssociativeArray} from "~/types/data";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import Access from "~/models/Access/Access";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import ArrayUtils from "~/services/utils/arrayUtils";
+import type { PropType } from '@vue/runtime-core'
+import { computed } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import type { AnyJson, AssociativeArray } from '~/types/data'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import Access from '~/models/Access/Access'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import ArrayUtils from '~/services/utils/arrayUtils'
 import * as _ from 'lodash-es'
 
 const props = defineProps({
@@ -44,7 +44,7 @@ const props = defineProps({
   modelValue: {
     type: [Object, Array],
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Filtres à transmettre à la source de données
@@ -52,7 +52,7 @@ const props = defineProps({
   filters: {
     type: Object as PropType<Ref<AssociativeArray>>,
     required: false,
-    default: ref(null)
+    default: ref(null),
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -62,7 +62,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -71,7 +71,7 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Définit si le champ est en lecture seule
@@ -79,7 +79,7 @@ const props = defineProps({
    */
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Autorise la sélection multiple
@@ -87,7 +87,7 @@ const props = defineProps({
    */
   multiple: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Rends les résultats sous forme de puces
@@ -95,30 +95,39 @@ const props = defineProps({
    */
   chips: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Closes the menu and clear the current search after the selection has been updated
    */
   clearSearchAfterUpdate: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
    */
   variant: {
-    type: String as PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled" | undefined>,
+    type: String as PropType<
+      | 'filled'
+      | 'outlined'
+      | 'plain'
+      | 'underlined'
+      | 'solo'
+      | 'solo-inverted'
+      | 'solo-filled'
+      | undefined
+    >,
     required: false,
-    default: 'filled'
-  }
+    default: 'filled',
+  },
 })
 
 /**
  * Element de la liste autocomplete
  */
 interface AccessListItem {
-  id: number | string,
+  id: number | string
   title: string
 }
 
@@ -133,7 +142,9 @@ const i18n = useI18n()
 const accessToItem = (access: Access): AccessListItem => {
   return {
     id: access.id,
-    title: access.person ? `${access.person.name} ${access.person.givenName}` : i18n.t('unknown')
+    title: access.person
+      ? `${access.person.name} ${access.person.givenName}`
+      : i18n.t('unknown'),
   }
 }
 
@@ -148,7 +159,7 @@ const nameFilter: Ref<string | null> = ref(null)
  * Query transmise à l'API lors des changements de filtre de recherche
  */
 const query: ComputedRef<AnyJson> = computed(() => {
-  let q: AnyJson = {'groups[]': 'access_people_ref', 'order[name]': 'asc'}
+  let q: AnyJson = { 'groups[]': 'access_people_ref', 'order[name]': 'asc' }
 
   if (!initialized.value && props.modelValue) {
     if (Array.isArray(props.modelValue) && props.modelValue.length > 0) {
@@ -169,18 +180,17 @@ const query: ComputedRef<AnyJson> = computed(() => {
 /**
  * On commence par fetcher les accesses déjà actifs, pour affichage des noms
  */
-const { data: collection, pending, refresh } = await fetchCollection(
-    Access,
-    null,
-    query
-)
+const {
+  data: collection,
+  pending,
+  refresh,
+} = await fetchCollection(Access, null, query)
 initialized.value = true
 
 // On a déjà récupéré les access actifs, on relance une requête pour récupérer la première page
 // des accesses suivants
 refresh()
 
-
 /**
  * Contenu de la liste autocomplete
  */
@@ -200,7 +210,10 @@ const items: ComputedRef<Array<AccessListItem>> = computed(() => {
   fetchedItems.sort((a, b) => {
     if (props.modelValue.includes(a.id) && !props.modelValue.includes(b.id)) {
       return -1
-    } else if (!props.modelValue.includes(a.id) && props.modelValue.includes(b.id)) {
+    } else if (
+      !props.modelValue.includes(a.id) &&
+      props.modelValue.includes(b.id)
+    ) {
       return 1
     } else {
       return a.title.localeCompare(b.title)
@@ -210,7 +223,6 @@ const items: ComputedRef<Array<AccessListItem>> = computed(() => {
   return fetchedItems
 })
 
-
 /**
  * Délai entre le dernier caractère saisi et la requête de vérification de la mise à jour des résultats (en ms)
  */
@@ -221,7 +233,7 @@ const inputDelay = 600
  * @see https://docs-lodash.com/v4/debounce/
  */
 const refreshDebounced: _.DebouncedFunc<() => void> = _.debounce(async () => {
-  await refresh();
+  await refresh()
 }, inputDelay)
 
 // ### Events
@@ -238,14 +250,14 @@ const onUpdateSearch = (event: string) => {
 
 const onUpdateModelValue = (event: Array<number>) => {
   if (props.clearSearchAfterUpdate) {
-    nameFilter.value = ""
+    nameFilter.value = ''
   }
   emit('update:model-value', event)
 }
 </script>
 
 <style scoped lang="scss">
-  .v-autocomplete {
-    min-width: 350px;
-  }
+.v-autocomplete {
+  min-width: 350px;
+}
 </style>

+ 44 - 45
components/Ui/Input/AutocompleteWithAPI.vue

@@ -7,95 +7,94 @@ d'une api)
 <template>
   <main>
     <UiInputAutocomplete
-        :field="field"
-        :label="label"
-        :data="remoteData ? remoteData : data"
-        :items="items"
-        :isLoading="isLoading"
-        :item-text="itemText"
-        :slotText="slotText"
-        :item-value="itemValue"
-        :multiple="multiple"
-        :chips="chips"
-        prependIcon="mdi-magnify"
-        :return-object="returnObject"
-        @research="search"
-        :no-filter="noFilter"
-        @update="$emit('update', $event, field)"
+      :field="field"
+      :label="label"
+      :data="remoteData ? remoteData : data"
+      :items="items"
+      :isLoading="isLoading"
+      :item-text="itemText"
+      :slotText="slotText"
+      :item-value="itemValue"
+      :multiple="multiple"
+      :chips="chips"
+      prependIcon="mdi-magnify"
+      :return-object="returnObject"
+      @research="search"
+      :no-filter="noFilter"
+      @update="$emit('update', $event, field)"
     />
   </main>
 </template>
 
 <script setup lang="ts">
-
-import {ref, toRefs} from "@vue/reactivity";
-import type {Ref} from "@vue/reactivity";
-import UrlUtils from "~/services/utils/urlUtils";
-import {useFetch} from "#app";
-import {watch} from "@vue/runtime-core";
+import { ref, toRefs } from '@vue/reactivity'
+import type { Ref } from '@vue/reactivity'
+import UrlUtils from '~/services/utils/urlUtils'
+import { useFetch } from '#app'
+import { watch } from '@vue/runtime-core'
 
 const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   searchFunction: {
     type: Function,
-    required: true
+    required: true,
   },
   data: {
     type: [String, Number, Object, Array],
     required: false,
-    default: null
+    default: null,
   },
   remoteUri: {
     type: [Array],
     required: false,
-    default: null
+    default: null,
   },
   remoteUrl: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   itemValue: {
     type: String,
-    default: 'id'
+    default: 'id',
   },
   itemTitle: {
     type: Array,
-    required: true
+    required: true,
   },
   slotText: {
     type: Array,
-    required: false
+    required: false,
   },
   returnObject: {
     type: Boolean,
-    default: false
+    default: false,
   },
   noFilter: {
     type: Boolean,
-    default: false
+    default: false,
   },
   multiple: {
     type: Boolean,
-    default: false
+    default: false,
   },
   chips: {
     type: Boolean,
-    default: false
-  }
+    default: false,
+  },
 })
 
 const { data } = toRefs(props)
@@ -103,19 +102,19 @@ const items = ref([])
 const remoteData: Ref<Array<string> | null> = ref(null)
 const isLoading = ref(false)
 
-
 if (props.data) {
-  items.value = props.multiple ? (data.value ?? []) : [data.value]
-
+  items.value = props.multiple ? data.value ?? [] : [data.value]
 } else if (props.remoteUri) {
+  const ids: Array<any> = []
 
-  const ids:Array<any> = []
-
-  for(const uri of props.remoteUri){
+  for (const uri of props.remoteUri) {
     ids.push(UrlUtils.extractIdFromUri(uri as string))
   }
 
-  const options: FetchOptions = { method: 'GET', query: {key: 'id', value: ids.join(',')} }
+  const options: FetchOptions = {
+    method: 'GET',
+    query: { key: 'id', value: ids.join(',') },
+  }
 
   useFetch(async () => {
     isLoading.value = true
@@ -128,14 +127,14 @@ if (props.data) {
   })
 }
 
-const search = async (research:string) => {
+const search = async (research: string) => {
   isLoading.value = true
   const func: Function = props.searchFunction
   items.value = items.value.concat(await func(research, props.field))
   isLoading.value = false
 }
 
-const unwatch = watch(data,(d) => {
+const unwatch = watch(data, (d) => {
   items.value = props.multiple ? d : [d]
 })
 

+ 37 - 28
components/Ui/Input/AutocompleteWithAp2i.vue

@@ -22,14 +22,13 @@ Liste déroulante avec autocompletion issue de Ap2i
 </template>
 
 <script setup lang="ts">
-
-import {computed} from "@vue/reactivity";
-import type {ComputedRef, Ref} from "@vue/reactivity";
-import type {PropType} from "@vue/runtime-core";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import ApiResource from "~/models/ApiResource";
-import ApiModel from "~/models/ApiModel";
-import type {AnyJson, AssociativeArray} from "~/types/data";
+import { computed } from '@vue/reactivity'
+import type { ComputedRef, Ref } from '@vue/reactivity'
+import type { PropType } from '@vue/runtime-core'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import ApiResource from '~/models/ApiResource'
+import ApiModel from '~/models/ApiModel'
+import type { AnyJson, AssociativeArray } from '~/types/data'
 
 const props = defineProps({
   /**
@@ -38,14 +37,14 @@ const props = defineProps({
   modelValue: {
     type: [String, Number, Object, Array],
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Classe de l'ApiModel (ex: Organization, Notification, ...) qui sert de source à la liste
    */
   model: {
     type: Function as any as () => typeof ApiModel,
-    required: true
+    required: true,
   },
   /**
    * Filtres à transmettre à la source de données
@@ -53,7 +52,7 @@ const props = defineProps({
   query: {
     type: Object as PropType<Ref<AssociativeArray>>,
     required: false,
-    default: ref(null)
+    default: ref(null),
   },
   /**
    * Fonction qui sera exécutée sur chaque item, et qui doit renvoyer un objet contenant les
@@ -63,9 +62,11 @@ const props = defineProps({
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-item-value
    */
   transformation: {
-    type: Function as PropType<(item: ApiResource) => { id: number | string, title: string }>,
+    type: Function as PropType<
+      (item: ApiResource) => { id: number | string; title: string }
+    >,
     required: false,
-    default: (item: ApiResource) => item
+    default: (item: ApiResource) => item,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -75,7 +76,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -84,7 +85,7 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Définit si le champ est en lecture seule
@@ -92,7 +93,7 @@ const props = defineProps({
    */
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Autorise la sélection multiple
@@ -100,7 +101,7 @@ const props = defineProps({
    */
   multiple: {
     type: Boolean,
-    default: false
+    default: false,
   },
   /**
    * Rends les résultats sous forme de puces
@@ -108,30 +109,38 @@ const props = defineProps({
    */
   chips: {
     type: Boolean,
-    default: false
+    default: false,
   },
   // TODO: c'est quoi?
   slotText: {
     type: Array,
     required: false,
-    default: null
+    default: null,
   },
 })
 
 const { fetchCollection } = useEntityFetch()
 
 const query: ComputedRef<AnyJson> = computed(() => {
-  return { ...(props.query.value ?? {}), ...{ 'groups[]': 'access_people_ref' } }
+  return {
+    ...(props.query.value ?? {}),
+    ...{ 'groups[]': 'access_people_ref' },
+  }
 })
 
-const { data: collection, pending } = await fetchCollection(props.model, null, query)
+const { data: collection, pending } = await fetchCollection(
+  props.model,
+  null,
+  query,
+)
 
-const items: ComputedRef<Array<{ id: number | string, title: string }>> = computed(() => {
-  if (!pending.value && collection.value && collection.value.items) {
-    console.log(collection)
+const items: ComputedRef<Array<{ id: number | string; title: string }>> =
+  computed(() => {
+    if (!pending.value && collection.value && collection.value.items) {
+      console.log(collection)
 
-    return collection.value.items.map(props.transformation)
-  }
-  return []
-})
+      return collection.value.items.map(props.transformation)
+    }
+    return []
+  })
 </script>

+ 31 - 28
components/Ui/Input/AutocompleteWithEnum.vue

@@ -1,55 +1,61 @@
-
 <template>
   <UiInputAutocomplete
-      :model-value="modelValue"
-      :field="field"
-      :items="items"
-      :is-loading="pending"
-      :return-object="false"
-      item-title="label"
-      item-value="value"
-      :variant="variant"
-      @update:model-value="$emit('update:model-value', $event)"
+    :model-value="modelValue"
+    :field="field"
+    :items="items"
+    :is-loading="pending"
+    :return-object="false"
+    item-title="label"
+    item-value="value"
+    :variant="variant"
+    @update:model-value="$emit('update:model-value', $event)"
   />
 </template>
 
 <script setup lang="ts">
-
-
-import {useEnumFetch} from "~/composables/data/useEnumFetch";
-import ArrayUtils from "~/services/utils/arrayUtils";
-import type {ComputedRef} from "@vue/reactivity";
-import type {Enum} from "~/types/data";
-import type {PropType} from "@vue/runtime-core";
+import { useEnumFetch } from '~/composables/data/useEnumFetch'
+import ArrayUtils from '~/services/utils/arrayUtils'
+import type { ComputedRef } from '@vue/reactivity'
+import type { Enum } from '~/types/data'
+import type { PropType } from '@vue/runtime-core'
 
 const props = defineProps({
   modelValue: {
     type: String as PropType<string | null>,
     required: false,
-    default: null
+    default: null,
   },
   enumName: {
     type: String,
-    required: true
+    required: true,
   },
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
    */
   variant: {
-    type: String as PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled" | undefined>,
+    type: String as PropType<
+      | 'filled'
+      | 'outlined'
+      | 'plain'
+      | 'underlined'
+      | 'solo'
+      | 'solo-inverted'
+      | 'solo-filled'
+      | undefined
+    >,
     required: false,
-    default: 'filled'
-  }
+    default: 'filled',
+  },
 })
 
 const { fetch } = useEnumFetch()
@@ -62,9 +68,6 @@ const items: ComputedRef<Array<Enum>> = computed(() => {
   }
   return ArrayUtils.sortObjectsByProp(enumItems.value, 'label') as Array<Enum>
 })
-
 </script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 11 - 13
components/Ui/Input/Checkbox.vue

@@ -17,7 +17,7 @@ Case à cocher, à placer dans un composant `UiForm`
 </template>
 
 <script setup lang="ts">
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
 
 const props = defineProps({
   /**
@@ -25,7 +25,7 @@ const props = defineProps({
    */
   modelValue: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -35,7 +35,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -44,7 +44,7 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Définit si le champ est en lecture seule
@@ -52,7 +52,7 @@ const props = defineProps({
   readonly: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Règles de validation
@@ -61,14 +61,14 @@ const props = defineProps({
   rules: {
     type: Array,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Le champ est-il actuellement en état d'erreur
    */
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Si le champ est en état d'erreur, quel est le message d'erreur?
@@ -76,11 +76,11 @@ const props = defineProps({
   errorMessage: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
-const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
+const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
 const fieldLabel: string = props.label ?? props.field
 
@@ -90,8 +90,6 @@ const onUpdate = (event: boolean) => {
   updateViolationState(event)
   emit('update:model-value', event)
 }
-
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 16 - 19
components/Ui/Input/Combobox.vue

@@ -5,10 +5,7 @@ Liste déroulante, à placer dans un composant `UiForm`
 -->
 
 <template>
-  <v-container
-    class="px-0"
-    fluid
-  >
+  <v-container class="px-0" fluid>
     <v-combobox
       :model-value="modelValue"
       :value="modelValue"
@@ -16,14 +13,16 @@ Liste déroulante, à placer dans un composant `UiForm`
       :items="items"
       :disabled="readonly"
       :error="error || !!fieldViolations"
-      :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
+      :error-messages="
+        errorMessage || fieldViolations ? $t(fieldViolations) : ''
+      "
       @update:model-value="onUpdate($event)"
     />
   </v-container>
 </template>
 
 <script setup lang="ts">
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
 
 const props = defineProps({
   /**
@@ -31,7 +30,7 @@ const props = defineProps({
    */
   modelValue: {
     type: [String, Number],
-    required: false
+    required: false,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -41,7 +40,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -50,21 +49,21 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Liste des éléments de la liste
    */
   items: {
     type: Array,
-    required: true
+    required: true,
   },
   /**
    * Définit si le champ est en lecture seule
    */
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Règles de validation
@@ -73,14 +72,14 @@ const props = defineProps({
   rules: {
     type: Array,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Le champ est-il actuellement en état d'erreur
    */
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Si le champ est en état d'erreur, quel est le message d'erreur?
@@ -88,11 +87,11 @@ const props = defineProps({
   errorMessage: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
-const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
+const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
 const fieldLabel: string = props.label ?? props.field
 
@@ -102,8 +101,6 @@ const onUpdate = (event: string) => {
   updateViolationState(event)
   emit('update:model-value', event)
 }
-
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 20 - 22
components/Ui/Input/DatePicker.vue

@@ -8,11 +8,11 @@ Sélecteur de dates, à placer dans un composant `UiForm`
       <span>{{ $t(fieldLabel) }}</span>
 
       <UiDatePicker
-          v-model="date"
-          :readonly="readonly"
-          :format="format"
-          :position="position"
-          @update:model-value="onUpdate($event)"
+        v-model="date"
+        :readonly="readonly"
+        :format="format"
+        :position="position"
+        @update:model-value="onUpdate($event)"
       />
 
       <span v-if="error || !!fieldViolations" class="theme-danger">
@@ -23,9 +23,9 @@ Sélecteur de dates, à placer dans un composant `UiForm`
 </template>
 
 <script setup lang="ts">
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import {formatISO} from "date-fns";
-import type {PropType} from "@vue/runtime-core";
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
+import { formatISO } from 'date-fns'
+import type { PropType } from '@vue/runtime-core'
 
 const props = defineProps({
   /**
@@ -34,7 +34,7 @@ const props = defineProps({
   modelValue: {
     type: String as PropType<string | null>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -44,7 +44,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -53,14 +53,14 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Définit si le champ est en lecture seule
    */
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Format d'affichage des dates
@@ -69,7 +69,7 @@ const props = defineProps({
   format: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Règles de validation
@@ -78,14 +78,14 @@ const props = defineProps({
   rules: {
     type: Array,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Le champ est-il actuellement en état d'erreur
    */
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Si le champ est en état d'erreur, quel est le message d'erreur?
@@ -93,7 +93,7 @@ const props = defineProps({
   errorMessage: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * @see https://vue3datepicker.com/props/positioning/#position
@@ -101,13 +101,13 @@ const props = defineProps({
   position: {
     type: String as PropType<'left' | 'center' | 'right'>,
     required: false,
-    default: 'center'
-  }
+    default: 'center',
+  },
 })
 
 const input = ref(null)
 
-const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
+const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
 const fieldLabel = props.label ?? props.field
 
@@ -121,6 +121,4 @@ const onUpdate = (event: string) => {
 }
 </script>
 
-<style scoped>
-
-</style>
+<style scoped></style>

+ 14 - 18
components/Ui/Input/Email.vue

@@ -15,64 +15,60 @@ Champs de saisie de type Text dédié à la saisie d'emails
 </template>
 
 <script setup lang="ts">
-
-import {useNuxtApp} from "#app";
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import {useValidationUtils} from "~/composables/utils/useValidationUtils";
+import { useNuxtApp } from '#app'
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
+import { useValidationUtils } from '~/composables/utils/useValidationUtils'
 
 const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   data: {
     type: [String, Number],
     required: false,
-    default: null
+    default: null,
   },
   readonly: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   required: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   errorMessage: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const { emit, i18n } = useNuxtApp()
 
 const fieldLabel = props.label ?? props.field
 
-const {violation, onChange} = useFieldViolation(props.field, emit)
+const { violation, onChange } = useFieldViolation(props.field, emit)
 
 const validationUtils = useValidationUtils()
 
 const rules = [
-  (email: string) => validationUtils.validEmail(email) || i18n.t('email_error')
+  (email: string) => validationUtils.validEmail(email) || i18n.t('email_error'),
 ]
 
 if (props.required) {
-  rules.push(
-    (email: string) => !!email || i18n.t('required')
-  )
+  rules.push((email: string) => !!email || i18n.t('required'))
 }
-
 </script>

+ 20 - 21
components/Ui/Input/Enum.vue

@@ -6,11 +6,7 @@ Liste déroulante dédiée à l'affichage d'objets Enum
 
 <template>
   <main>
-    <v-skeleton-loader
-      v-if="pending"
-      type="list-item"
-      loading
-    />
+    <v-skeleton-loader v-if="pending" type="list-item" loading />
 
     <v-select
       v-else
@@ -23,15 +19,20 @@ Liste déroulante dédiée à l'affichage d'objets Enum
       :rules="rules"
       :disabled="readonly"
       :error="error || !!fieldViolations"
-      :error-messages="errorMessage || (fieldViolations ? $t(fieldViolations) : '')"
-      @update:modelValue="updateViolationState($event); $emit('update:modelValue', $event)"
+      :error-messages="
+        errorMessage || (fieldViolations ? $t(fieldViolations) : '')
+      "
+      @update:modelValue="
+        updateViolationState($event)
+        $emit('update:modelValue', $event)
+      "
     />
   </main>
 </template>
 
 <script setup lang="ts">
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import {useEnumFetch} from "~/composables/data/useEnumFetch";
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
+import { useEnumFetch } from '~/composables/data/useEnumFetch'
 
 const props = defineProps({
   /**
@@ -40,14 +41,14 @@ const props = defineProps({
   modelValue: {
     String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Nom de l'Enum utilisée pour peupler la liste
    */
   enum: {
     type: String,
-    required: true
+    required: true,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -57,7 +58,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -66,14 +67,14 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Définit si le champ est en lecture seule
    */
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Règles de validation
@@ -82,14 +83,14 @@ const props = defineProps({
   rules: {
     type: Array,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Le champ est-il actuellement en état d'erreur
    */
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   /**
    * Si le champ est en état d'erreur, quel est le message d'erreur ?
@@ -97,8 +98,8 @@ const props = defineProps({
   errorMessage: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 if (typeof props.enum === 'undefined') {
@@ -117,8 +118,6 @@ const onModelUpdate = (event: any) => {
   emit('change', event)
   emit('update:modelValue', event)
 }
-
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>

+ 147 - 127
components/Ui/Input/Image.vue

@@ -4,16 +4,16 @@ Assistant de création d'image
 @see https://norserium.github.io/vue-advanced-cropper/
 -->
 <template>
-  <div class="input-image" >
+  <div class="input-image">
     <UiImage
-        ref="uiImage"
-        :image-id="modelValue"
-        :default-image="defaultImage"
-        :width="width"
-        :height="height"
-        class="image"
-        overlay-icon="fas fa-upload"
-        @overlay-clicked="openModal()"
+      ref="uiImage"
+      :image-id="modelValue"
+      :default-image="defaultImage"
+      :width="width"
+      :height="height"
+      class="image"
+      overlay-icon="fas fa-upload"
+      @overlay-clicked="openModal()"
     />
 
     <LazyLayoutDialog :show="showModal">
@@ -27,13 +27,11 @@ Assistant de création d'image
             align="center"
             justify="center"
           >
-            <v-progress-circular
-              :indeterminate="true"
-              color="neutral">
+            <v-progress-circular :indeterminate="true" color="neutral">
             </v-progress-circular>
           </v-row>
 
-          <div v-else >
+          <div v-else>
             <div class="upload__cropper-wrapper">
               <Cropper
                 ref="cropper"
@@ -46,10 +44,10 @@ Assistant de création d'image
               />
 
               <div
-                  v-if="currentImage.src"
-                  class="upload__reset-button"
-                  title="Reset Image"
-                  @click="reset()"
+                v-if="currentImage.src"
+                class="upload__reset-button"
+                title="Reset Image"
+                @click="reset()"
               >
                 <v-icon>fas fa-trash</v-icon>
               </div>
@@ -57,21 +55,29 @@ Assistant de création d'image
 
             <div class="upload__buttons-wrapper">
               <button class="upload__button" @click="fileInput?.click()">
-                <input ref="fileInput" type="file" accept="image/*" @change="uploadImage($event)" />
-                {{$t('upload_image')}}
+                <input
+                  ref="fileInput"
+                  type="file"
+                  accept="image/*"
+                  @change="uploadImage($event)"
+                />
+                {{ $t('upload_image') }}
               </button>
             </div>
 
             <span class="max-size-label">{{ $t('max_size_4_mb') }}</span>
           </div>
-
         </div>
       </template>
       <template #dialogBtn>
         <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="cancel">
           {{ $t('cancel') }}
         </v-btn>
-        <v-btn class="submitBtn theme-primary" @click="save" :disabled="pending">
+        <v-btn
+          class="submitBtn theme-primary"
+          @click="save"
+          :disabled="pending"
+        >
           {{ $t('save') }}
         </v-btn>
       </template>
@@ -80,16 +86,16 @@ Assistant de création d'image
 </template>
 
 <script setup lang="ts">
-import { Cropper } from 'vue-advanced-cropper';
-import 'vue-advanced-cropper/dist/style.css';
-import {type Ref, ref} from "@vue/reactivity";
+import { Cropper } from 'vue-advanced-cropper'
+import 'vue-advanced-cropper/dist/style.css'
+import { type Ref, ref } from '@vue/reactivity'
 import File from '~/models/Core/File'
-import type {PropType} from "@vue/runtime-core";
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import {useImageManager} from "~/composables/data/useImageManager";
-import {FILE_VISIBILITY, TYPE_ALERT} from "~/types/enum/enums";
-import {usePageStore} from "~/stores/page";
-import ImageUtils from "~/services/utils/imageUtils";
+import type { PropType } from '@vue/runtime-core'
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import { useImageManager } from '~/composables/data/useImageManager'
+import { FILE_VISIBILITY, TYPE_ALERT } from '~/types/enum/enums'
+import { usePageStore } from '~/stores/page'
+import ImageUtils from '~/services/utils/imageUtils'
 
 const props = defineProps({
   /**
@@ -98,7 +104,7 @@ const props = defineProps({
   modelValue: {
     type: Number as PropType<number | null>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -107,36 +113,36 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Image par défaut en cas d'absence d'une image uploadée
    */
   defaultImage: {
     type: String,
-    required: false
+    required: false,
   },
   /**
    * Hauteur de l'image à l'écran (en px)
    */
   height: {
     type: Number,
-    required: false
+    required: false,
   },
   /**
    * Largeur de l'image à l'écran (en px)
    */
   width: {
     type: Number,
-    required: false
+    required: false,
   },
   /**
    * TODO: completer
    */
   ownerId: {
     type: Number,
-    required: false
-  }
+    required: false,
+  },
 })
 
 const { em } = useEntityManager()
@@ -170,13 +176,16 @@ const file: Ref<File | null> = ref(null)
 /**
  * Données d'une nouvelle image uploadée par l'utilisateur
  */
-const currentImage: Ref<
-    {id: string | number | null, src: string | null, content: string | null, name: string | null}
-> = ref({
+const currentImage: Ref<{
+  id: string | number | null
+  src: string | null
+  content: string | null
+  name: string | null
+}> = ref({
   id: null,
   src: null,
   content: null,
-  name: null
+  name: null,
 })
 
 /**
@@ -187,19 +196,24 @@ const MAX_FILE_SIZE = 4 * 1024 * 1024
 /**
  * Coordonnées du cropper
  */
-const cropperConfig: Ref<{ left?: number, top?: number, height?: number, width?: number }> = ref({})
+const cropperConfig: Ref<{
+  left?: number
+  top?: number
+  height?: number
+  width?: number
+}> = ref({})
 
 /**
  * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultposition
  */
 const defaultPosition = () => {
-  return { left : cropperConfig.value.left, top : cropperConfig.value.top }
+  return { left: cropperConfig.value.left, top: cropperConfig.value.top }
 }
 
 /**
  * @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultsize
  */
-const defaultSize = (params: any): { width: number, height: number } | null => {
+const defaultSize = (params: any): { width: number; height: number } | null => {
   if (!params) {
     return null
   }
@@ -207,7 +221,7 @@ const defaultSize = (params: any): { width: number, height: number } | null => {
 
   return {
     width: cropperConfig.value.width ?? (visibleArea || imageSize).width,
-    height: cropperConfig.value.height ?? (visibleArea || imageSize).height
+    height: cropperConfig.value.height ?? (visibleArea || imageSize).height,
   }
 }
 
@@ -217,7 +231,7 @@ const defaultSize = (params: any): { width: number, height: number } | null => {
  * @param fileId
  */
 const loadImage = async (fileId: number) => {
-  file.value = await em.fetch(File, fileId) as File
+  file.value = (await em.fetch(File, fileId)) as File
 
   if (file.value.config) {
     const fileConfig = JSON.parse(file.value.config)
@@ -229,7 +243,7 @@ const loadImage = async (fileId: number) => {
 
   currentImage.value.name = file.value.name
   currentImage.value.id = file.value.id
-  currentImage.value.src = await imageManager.get(fileId) as string
+  currentImage.value.src = (await imageManager.get(fileId)) as string
 }
 
 /**
@@ -242,7 +256,6 @@ const openModal = async () => {
   if (props.modelValue !== null) {
     // Un objet File existe déjà: on le récupère
     await loadImage(props.modelValue)
-
   } else {
     // Nouveau File
     file.value = em.newInstance(File) as File
@@ -260,11 +273,17 @@ const reset = () => {
   }
 
   currentImage.value = {
-    src: null, content: null, name: null, id: null
+    src: null,
+    content: null,
+    name: null,
+    id: null,
   }
 
   cropperConfig.value = {
-    left: undefined, height: undefined, top: undefined, width: undefined
+    left: undefined,
+    height: undefined,
+    top: undefined,
+    width: undefined,
   }
 }
 
@@ -282,7 +301,10 @@ const uploadImage = async (event: any) => {
   const uploadedFile = files[0]
 
   if (uploadedFile.size > MAX_FILE_SIZE) {
-    pageStore.alerts.push({type: TYPE_ALERT.ALERT, messages: ['file_too_large'] })
+    pageStore.alerts.push({
+      type: TYPE_ALERT.ALERT,
+      messages: ['file_too_large'],
+    })
     return
   }
 
@@ -303,8 +325,8 @@ const uploadImage = async (event: any) => {
  * Lorsque le cropper change de position / taille, on met à jour les coordonnées
  * @param newCoordinates
  */
-const onCropperChange = ({ coordinates: newCoordinates } : any) => {
-  cropperConfig.value = newCoordinates;
+const onCropperChange = ({ coordinates: newCoordinates }: any) => {
+  cropperConfig.value = newCoordinates
 }
 
 /**
@@ -333,15 +355,15 @@ const saveNewImage = async (): Promise<number> => {
     x: cropperConfig.value.left,
     y: cropperConfig.value.top,
     height: cropperConfig.value.height,
-    width: cropperConfig.value.width
+    width: cropperConfig.value.width,
   })
 
-  const response = await imageManager.upload(
-      currentImage.value.name,
-      currentImage.value.content,
-      FILE_VISIBILITY.EVERYBODY,
-      config
-  ) as any
+  const response = (await imageManager.upload(
+    currentImage.value.name,
+    currentImage.value.content,
+    FILE_VISIBILITY.EVERYBODY,
+    config,
+  )) as any
 
   return response.fileId
 }
@@ -358,7 +380,7 @@ const saveExistingImage = async () => {
     x: cropperConfig.value.left,
     y: cropperConfig.value.top,
     height: cropperConfig.value.height,
-    width: cropperConfig.value.width
+    width: cropperConfig.value.width,
   })
 
   await em.persist(File, file.value)
@@ -375,12 +397,10 @@ const save = async () => {
     // Une nouvelle image a été uploadée
     const fileId = await saveNewImage()
     emit('update:modelValue', fileId)
-
   } else if (currentImage.value.id) {
     // L'image existante a été modifiée
     await saveExistingImage()
     uiImage.value.refresh()
-
   } else {
     // On a reset l'image
     emit('update:modelValue', null)
@@ -401,74 +421,74 @@ onUnmounted(() => {
 </script>
 
 <style scoped lang="scss">
-  :deep(.vue-advanced-cropper__stretcher) {
-    height: auto !important;
-    width: auto !important;
-  }
+:deep(.vue-advanced-cropper__stretcher) {
+  height: auto !important;
+  width: auto !important;
+}
 
-  .loading{
-    height: 300px;
-  }
+.loading {
+  height: 300px;
+}
 
-  .upload {
-    user-select: none;
-    padding: 20px;
-    display: block;
-    &__cropper {
-       border: solid 1px rgb(var(--v-theme-on-neutral-strong));;
-       min-height: 300px;
-       max-height: 300px;
-     }
-    &__cropper-wrapper {
-       position: relative;
-     }
-    &__reset-button {
-      position: absolute;
-      right: 20px;
-      bottom: 20px;
-      cursor: pointer;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      height: 42px;
-      width: 42px;
-      background: rgb(var(--v-theme-neutral));
-      transition: background 0.5s;
-      &:hover {
-        background: rgb(var(--v-theme-primary-alt));
-      }
+.upload {
+  user-select: none;
+  padding: 20px;
+  display: block;
+  &__cropper {
+    border: solid 1px rgb(var(--v-theme-on-neutral-strong));
+    min-height: 300px;
+    max-height: 300px;
+  }
+  &__cropper-wrapper {
+    position: relative;
+  }
+  &__reset-button {
+    position: absolute;
+    right: 20px;
+    bottom: 20px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 42px;
+    width: 42px;
+    background: rgb(var(--v-theme-neutral));
+    transition: background 0.5s;
+    &:hover {
+      background: rgb(var(--v-theme-primary-alt));
     }
-    &__buttons-wrapper {
-       display: flex;
-       justify-content: center;
-       margin-top: 17px;
-     }
-    &__button {
-       border: none;
-       outline: solid transparent;
-       color: rgb(var(--v-theme-on-neutral));
-       font-size: 16px;
-       padding: 10px 20px;
-       background: rgb(var(--v-theme-neutral));
-       cursor: pointer;
-       transition: background 0.5s;
-       margin: 0 16px;
-      &:hover,
-      &:focus {
-         background: rgb(var(--v-theme-primary-alt));
-       }
-      input {
-        display: none;
-      }
+  }
+  &__buttons-wrapper {
+    display: flex;
+    justify-content: center;
+    margin-top: 17px;
+  }
+  &__button {
+    border: none;
+    outline: solid transparent;
+    color: rgb(var(--v-theme-on-neutral));
+    font-size: 16px;
+    padding: 10px 20px;
+    background: rgb(var(--v-theme-neutral));
+    cursor: pointer;
+    transition: background 0.5s;
+    margin: 0 16px;
+    &:hover,
+    &:focus {
+      background: rgb(var(--v-theme-primary-alt));
+    }
+    input {
+      display: none;
     }
   }
+}
 
-  .max-size-label {
-    display: block;
-    width: 100%;
-    text-align: center;
-    font-size: 13px;
-    color: rgb(var(--v-theme-on-neutral-soft));
-    margin-top: 6px;
-  }
+.max-size-label {
+  display: block;
+  width: 100%;
+  text-align: center;
+  font-size: 13px;
+  color: rgb(var(--v-theme-on-neutral-soft));
+  margin-top: 6px;
+}
 </style>

+ 34 - 21
components/Ui/Input/Number.vue

@@ -4,26 +4,25 @@ An input for numeric values
 
 <template>
   <v-text-field
-      ref="input"
-      :modelValue.number="modelValue"
-      :label="(label || field) ? $t(label ?? field) : undefined"
-      hide-details
-      :density="density"
-      type="number"
-      :variant="variant"
-      @update:modelValue="modelValue = keepInRange(cast($event)); emitUpdate()"
+    ref="input"
+    :modelValue.number="modelValue"
+    :label="label || field ? $t(label ?? field) : undefined"
+    hide-details
+    :density="density"
+    type="number"
+    :variant="variant"
+    @update:modelValue="onModelUpdate($event)"
   />
 </template>
 
 <script setup lang="ts">
+import type { PropType } from 'vue'
 
-import type {PropType} from "@vue/runtime-core";
-
-type Density = null | 'default' | 'comfortable' | 'compact';
+type Density = null | 'default' | 'comfortable' | 'compact'
 
 const props = defineProps({
   modelValue: {
-    type: Number
+    type: Number,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -33,7 +32,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -42,36 +41,45 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   default: {
     type: Number,
     required: false,
-    default: 0
+    default: 0,
   },
   min: {
     type: Number,
     required: false,
-    default: null
+    default: null,
   },
   max: {
     type: Number,
     required: false,
-    default: null
+    default: null,
   },
   density: {
     type: String as PropType<Density>,
     required: false,
-    default: 'default'
+    default: 'default',
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
    */
   variant: {
-    type: String as PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled" | undefined>,
+    type: String as PropType<
+      | 'filled'
+      | 'outlined'
+      | 'plain'
+      | 'underlined'
+      | 'solo'
+      | 'solo-inverted'
+      | 'solo-filled'
+      | undefined
+    >,
     required: false,
-    default: 'filled'
-  }
+    default: 'filled',
+  },
 })
 
 /**
@@ -109,6 +117,11 @@ const keepInRange = (val: number) => {
   return val
 }
 
+const onModelUpdate = (event: string) => {
+  // eslint-disable-next-line vue/no-mutating-props
+  props.modelValue = keepInRange(cast(event))
+  emitUpdate()
+}
 
 const emit = defineEmits(['update:modelValue'])
 

+ 28 - 24
components/Ui/Input/Phone.vue

@@ -26,40 +26,39 @@ Champs de saisie d'un numéro de téléphone
 </template>
 
 <script setup lang="ts">
-
-import {useNuxtApp} from "#app";
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import type {Ref} from "@vue/reactivity";
+import { useNuxtApp } from '#app'
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
+import type { Ref } from '@vue/reactivity'
 
 const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: ''
+    default: '',
   },
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   data: {
     type: [String, Number],
     required: false,
-    default: null
+    default: null,
   },
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   errorMessage: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const { emit, i18n } = useNuxtApp()
@@ -71,7 +70,14 @@ const internationalNumber: Ref<string | number> = ref('')
 const isValid: Ref<boolean> = ref(false)
 const onInit: Ref<boolean> = ref(true)
 
-const onInput = (_: any, { number, valid, countryChoice }: { number: any, valid: boolean, countryChoice: any }) => {
+const onInput = (
+  _: any,
+  {
+    number,
+    valid,
+    countryChoice,
+  }: { number: any; valid: boolean; countryChoice: any },
+) => {
   isValid.value = valid
   nationalNumber.value = number.national
   internationalNumber.value = number.international
@@ -84,24 +90,22 @@ const onChangeValue = () => {
   }
 }
 
-const myPhone = computed(
-  {
-    get:()=>{
-      return onInit.value ? props.data : nationalNumber.value
-    },
-    set:(value)=>{
-      return props.data
-    }
-  }
-)
+const myPhone = computed({
+  get: () => {
+    return onInit.value ? props.data : nationalNumber.value
+  },
+  set: (value) => {
+    return props.data
+  },
+})
 
 const rules = [
-  (phone: string) => (!phone || isValid.value) || i18n.t('phone_error')
+  (phone: string) => !phone || isValid.value || i18n.t('phone_error'),
 ]
 </script>
 
 <style lang="scss">
-input:read-only{
+input:read-only {
   color: rgb(var(--v-theme-on-neutral));
 }
 </style>

+ 33 - 23
components/Ui/Input/Text.vue

@@ -8,12 +8,14 @@ Champs de saisie de texte, à placer dans un composant `UiForm`
   <v-text-field
     ref="input"
     :model-value="modelValue"
-    :label="(label || field) ? $t(label ?? field) : undefined"
+    :label="label || field ? $t(label ?? field) : undefined"
     :rules="rules"
     :disabled="readonly"
-    :type="(type === 'password' && show) ? 'text' : type"
+    :type="type === 'password' && show ? 'text' : type"
     :error="error || !!fieldViolations"
-    :error-messages="errorMessage || (fieldViolations ? $t(fieldViolations) : '')"
+    :error-messages="
+      errorMessage || (fieldViolations ? $t(fieldViolations) : '')
+    "
     :append-icon="type === 'password' ? (show ? 'mdi-eye' : 'mdi-eye-off') : ''"
     :variant="variant"
     @click:append="show = !show"
@@ -21,14 +23,13 @@ Champs de saisie de texte, à placer dans un composant `UiForm`
     @change="onChange($event)"
   />
 
-
-<!--  v-cleave="mask"-->
+  <!--  v-cleave="mask"-->
 </template>
 
 <script setup lang="ts">
-import {type Ref, ref} from "@vue/reactivity";
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
-import type {PropType} from "@vue/runtime-core";
+import { type Ref, ref } from '@vue/reactivity'
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
+import type { PropType } from '@vue/runtime-core'
 
 const props = defineProps({
   /**
@@ -37,7 +38,7 @@ const props = defineProps({
   modelValue: {
     type: [String, Number] as PropType<string | number | null>,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -47,7 +48,7 @@ const props = defineProps({
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Label du champ
@@ -56,7 +57,7 @@ const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Type d'input HTML
@@ -65,7 +66,7 @@ const props = defineProps({
   type: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Définit si le champ est en lecture seule
@@ -73,7 +74,7 @@ const props = defineProps({
   readonly: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Règles de validation
@@ -82,7 +83,7 @@ const props = defineProps({
   rules: {
     type: Array as PropType<any[]>,
     required: false,
-    default: () => []
+    default: () => [],
   },
   /**
    * Le champ est-il actuellement en état d'erreur
@@ -90,7 +91,7 @@ const props = defineProps({
   error: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   /**
    * Si le champ est en état d'erreur, quel est le message d'erreur?
@@ -98,7 +99,7 @@ const props = defineProps({
   errorMessage: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   /**
    * Masque de saisie
@@ -107,16 +108,25 @@ const props = defineProps({
   mask: {
     type: [Array, Boolean],
     required: false,
-    default: false
+    default: false,
   },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
    */
   variant: {
-    type: String as PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled" | undefined>,
+    type: String as PropType<
+      | 'filled'
+      | 'outlined'
+      | 'plain'
+      | 'underlined'
+      | 'solo'
+      | 'solo-inverted'
+      | 'solo-filled'
+      | undefined
+    >,
     required: false,
-    default: 'filled'
-  }
+    default: 'filled',
+  },
 })
 
 const input = ref(null)
@@ -138,7 +148,7 @@ const onChange = (event: Event | undefined) => {
 </script>
 
 <style scoped lang="scss">
-  input:read-only{
-    color: rgb(var(--v-theme-neutral));
-  }
+input:read-only {
+  color: rgb(var(--v-theme-neutral));
+}
 </style>

+ 23 - 25
components/Ui/Input/TextArea.vue

@@ -6,68 +6,66 @@ Champs de saisie de bloc texte
 
 <template>
   <v-textarea
-      outlined
-      :value="data"
-      :label="$t(fieldLabel)"
-      :rules="rules"
-      :disabled="readonly"
-      :error="error || !!violation"
-      :error-messages="errorMessage || violation ? $t(violation) : ''"
-      @change="onChange($event)"
-    />
+    outlined
+    :value="data"
+    :label="$t(fieldLabel)"
+    :rules="rules"
+    :disabled="readonly"
+    :error="error || !!violation"
+    :error-messages="errorMessage || violation ? $t(violation) : ''"
+    @change="onChange($event)"
+  />
 </template>
 
 <script setup lang="ts">
-
-import {useNuxtApp} from "#app";
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
+import { useNuxtApp } from '#app'
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
 
 const props = defineProps({
   label: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   field: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   data: {
     type: [String, Number],
     required: false,
-    default: null
+    default: null,
   },
   readonly: {
     type: Boolean,
-    required: false
+    required: false,
   },
   rules: {
     type: Array,
     required: false,
-    default: () => []
+    default: () => [],
   },
   error: {
     type: Boolean,
-    required: false
+    required: false,
   },
   errorMessage: {
     type: String,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const { emit } = useNuxtApp()
 
 const fieldLabel = props.label ?? props.field
 
-const {violation, onChange} = useFieldViolation(props.field, emit)
-
+const { violation, onChange } = useFieldViolation(props.field, emit)
 </script>
 
 <style lang="scss">
-  input:read-only{
-    color: rgb(var(--v-theme-on-neutral));
-  }
+input:read-only {
+  color: rgb(var(--v-theme-on-neutral));
+}
 </style>

+ 14 - 17
components/Ui/ItemFromUri.vue

@@ -3,12 +3,9 @@ Espace permettant de récupérer un item via une uri et de gérer son affichage
 -->
 <template>
   <main>
-    <v-skeleton-loader
-      v-if="pending"
-      :type="loaderType"
-    />
+    <v-skeleton-loader v-if="pending" :type="loaderType" />
     <div v-else>
-      <slot name="item.text" v-bind="{item}" />
+      <slot name="item.text" v-bind="{ item }" />
     </div>
     <slot />
   </main>
@@ -17,32 +14,32 @@ Espace permettant de récupérer un item via une uri et de gérer son affichage
 <script setup lang="ts">
 // TODO: renommer en EntityFromUri? voir si ce component est encore nécessaire, ou si ça ne peut pas être une méthode de l'entity manager
 
-import {Query} from "pinia-orm";
-import UrlUtils from "~/services/utils/urlUtils";
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import {computed} from "@vue/reactivity";
-import type {ComputedRef} from "@vue/reactivity";
-import ApiResource from "~/models/ApiResource";
+import { Query } from 'pinia-orm'
+import UrlUtils from '~/services/utils/urlUtils'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import { computed } from '@vue/reactivity'
+import type { ComputedRef } from '@vue/reactivity'
+import ApiResource from '~/models/ApiResource'
 
 const props = defineProps({
   uri: {
     type: String,
     required: false,
-    default: null
+    default: null,
   },
   model: {
     type: Object,
-    required: true
+    required: true,
   },
   query: {
     type: Object as () => Query,
-    required: true
+    required: true,
   },
   loaderType: {
     type: String,
     required: false,
-    default: 'text'
-  }
+    default: 'text',
+  },
 })
 
 const id = UrlUtils.extractIdFromUri(props.uri)
@@ -54,7 +51,7 @@ const { fetch } = useEntityFetch()
 
 const { data, pending } = fetch(props.model, id)
 
-const item: ComputedRef<ApiResource|null> = computed(() => {
+const item: ComputedRef<ApiResource | null> = computed(() => {
   return data.value
 })
 </script>

+ 4 - 14
components/Ui/LoadingPanel.vue

@@ -1,19 +1,9 @@
 <template>
-  <v-row
-      class="fill-height ma-0"
-      align="center"
-      justify="center"
-  >
-    <v-progress-circular
-        :indeterminate="true"
-        color="neutral"
-    />
+  <v-row class="fill-height ma-0" align="center" justify="center">
+    <v-progress-circular :indeterminate="true" color="neutral" />
   </v-row>
 </template>
 
-<script setup lang="ts">
-</script>
+<script setup lang="ts"></script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 38 - 36
components/Ui/SystemBar.vue

@@ -4,9 +4,11 @@ System bars
 
 <template>
   <v-system-bar
-      height="50"
-      :class="'d-flex flex-row justify-center align-center text-center ' + classes"
-      @click="onClick !== undefined ? onClick() : null"
+    height="50"
+    :class="
+      'd-flex flex-row justify-center align-center text-center ' + classes
+    "
+    @click="onClick !== undefined ? onClick() : null"
   >
     <slot>
       <v-icon v-if="icon" small :icon="icon" />
@@ -16,40 +18,40 @@ System bars
 </template>
 
 <script setup lang="ts">
-  const props = defineProps({
-    text: {
-      type: String,
-      required: false,
-      default: ''
-    },
-    icon: {
-      type: String,
-      required: false,
-      default: undefined
-    },
-    backgroundColor: {
-      type: String,
-      required: false,
-      default: 'neutral-soft'
-    },
-    textColor: {
-      type: String,
-      required: false,
-      default: 'on-neutral-soft'
-    },
-    onClick: {
-      type: Function,
-      required: false,
-      default: undefined
-    },
-  })
+const props = defineProps({
+  text: {
+    type: String,
+    required: false,
+    default: '',
+  },
+  icon: {
+    type: String,
+    required: false,
+    default: undefined,
+  },
+  backgroundColor: {
+    type: String,
+    required: false,
+    default: 'neutral-soft',
+  },
+  textColor: {
+    type: String,
+    required: false,
+    default: 'on-neutral-soft',
+  },
+  onClick: {
+    type: Function,
+    required: false,
+    default: undefined,
+  },
+})
 
-  // TODO: voir si possible d'utiliser les variables sass à la place?
-  const classes = [
-    'bg-' + props.backgroundColor,
-    'text-' + props.textColor,
-    (props.onClick !== undefined ? 'clickable' : '')
-  ].join(' ')
+// TODO: voir si possible d'utiliser les variables sass à la place?
+const classes = [
+  'bg-' + props.backgroundColor,
+  'text-' + props.textColor,
+  props.onClick !== undefined ? 'clickable' : '',
+].join(' ')
 </script>
 
 <style scoped lang="scss">

+ 6 - 15
components/Ui/Template/DataTable.vue

@@ -5,42 +5,33 @@ Template de base d'un tableau interactif
 -->
 
 <template>
-  <v-col
-    cols="12"
-    sm="12"
-  >
-    <v-data-table
-      :headers="headersWithItem"
-      :items="items"
-      class="elevation-1"
-    >
+  <v-col cols="12" sm="12">
+    <v-data-table :headers="headersWithItem" :items="items" class="elevation-1">
       <template v-for="header in headersWithItem" #[header.item]="props">
         <slot :name="header.item" v-bind="props">
           {{ props.item[header.value] }}
         </slot>
       </template>
-
     </v-data-table>
   </v-col>
 </template>
 
 <script setup lang="ts">
-
 const props = defineProps({
   items: {
     type: Array,
-    required: true
+    required: true,
   },
   headers: {
     type: Array,
-    required: true
-  }
+    required: true,
+  },
 })
 
 const { headers } = toRefs(props)
 
 const headersWithItem = computed(() => {
-  return headers.value.map((header:any) => {
+  return headers.value.map((header: any) => {
     header.item = 'item.' + header.value
     return header
   })

+ 5 - 5
components/Ui/Template/Date.vue

@@ -7,16 +7,16 @@ Date formatée
 </template>
 
 <script setup lang="ts">
-import DateUtils from "~/services/utils/dateUtils";
-import {computed} from "@vue/reactivity";
-import type {ComputedRef} from "@vue/reactivity";
+import DateUtils from '~/services/utils/dateUtils'
+import { computed } from '@vue/reactivity'
+import type { ComputedRef } from '@vue/reactivity'
 
 const props = defineProps({
   data: {
     type: [String, Array],
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const datesFormatted: ComputedRef<string> = computed(() => {

+ 39 - 44
components/Ui/Xeditable/Text.vue

@@ -8,69 +8,64 @@ Utilisé par exemple pour le choix de l'année active
   <main>
     <!-- Mode édition activé -->
     <div v-if="edit" class="d-flex align-center x-editable-input">
-      <UiInputText
-          class="ma-0 pa-0"
-          :type="type"
-          v-model="inputValue"
-      />
+      <UiInputText class="ma-0 pa-0" :type="type" v-model="inputValue" />
 
       <v-icon
-          icon="fas fa-check"
-          aria-hidden="false"
-          class="valid icons text-primary"
-          size="small"
-          @click="update"
+        icon="fas fa-check"
+        aria-hidden="false"
+        class="valid icons text-primary"
+        size="small"
+        @click="update"
       />
       <v-icon
-          icon="fas fa-times"
-          aria-hidden="false"
-          class="cancel icons text-neutral-strong"
-          size="small"
-          @click="close"
+        icon="fas fa-times"
+        aria-hidden="false"
+        class="cancel icons text-neutral-strong"
+        size="small"
+        @click="close"
       />
     </div>
 
     <!-- Mode édition désactivé -->
-    <div v-else class="edit-link d-flex align-center" @click="edit=true">
-      <slot name="xeditable.read" v-bind="{inputValue}" />
+    <div v-else class="edit-link d-flex align-center" @click="edit = true">
+      <slot name="xeditable.read" v-bind="{ inputValue }" />
     </div>
   </main>
 </template>
 
 <script setup lang="ts">
-import {ref} from "@vue/reactivity";
-import type {Ref} from "@vue/reactivity";
-
-  const props = defineProps({
-    type: {
-      type: String,
-      required: false,
-      default: null
-    },
-    data: {
-      type: [String, Number],
-      required: false,
-      default: null
-    }
-  })
+import { ref } from '@vue/reactivity'
+import type { Ref } from '@vue/reactivity'
 
-  const emit = defineEmits(['update'])
+const props = defineProps({
+  type: {
+    type: String,
+    required: false,
+    default: null,
+  },
+  data: {
+    type: [String, Number],
+    required: false,
+    default: null,
+  },
+})
 
-  const edit: Ref<boolean> = ref(false)
-  const inputValue: Ref<string|number|null> = ref(props.data)
+const emit = defineEmits(['update'])
 
-  const update = () => {
-    edit.value = false
-    if (inputValue.value !== props.data) {
-        emit('update', inputValue.value)
-    }
-  }
+const edit: Ref<boolean> = ref(false)
+const inputValue: Ref<string | number | null> = ref(props.data)
 
-  const close = () => {
-    edit.value = false
-    inputValue.value = props.data
+const update = () => {
+  edit.value = false
+  if (inputValue.value !== props.data) {
+    emit('update', inputValue.value)
   }
+}
 
+const close = () => {
+  edit.value = false
+  inputValue.value = props.data
+}
 </script>
 
 <style scoped lang="scss">

+ 2 - 3
config/abilities/config.yaml

@@ -1,3 +1,2 @@
-abilities:
-  !!import/shallow
-    - pages/
+abilities: !!import/shallow
+  - pages/

+ 52 - 31
config/abilities/pages/addressBook.yaml

@@ -1,36 +1,57 @@
-  accesses_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Users']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'users'}]}
+accesses_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Users'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'users' }],
+      }
 
-  student_registration_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['UsersSchool']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'student-registration'}]}
+student_registration_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['UsersSchool'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'student-registration' }],
+      }
 
-  education_student_next_year_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
+education_student_next_year_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'educationstudent' }],
+      }
 
-  commissions_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Users']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'commissions'}]}
+commissions_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Users'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'commissions' }],
+      }
 
-  network_children_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Network']}
-      - {function: organizationHasChildren}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'network'}]}
+network_children_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Network'] }
+    - { function: organizationHasChildren }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'network' }],
+      }
 
-  network_parents_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['NetworkOrganization']}
-      - {function: organizationHasChildren, expectedResult: false}
+network_parents_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['NetworkOrganization'],
+      }
+    - { function: organizationHasChildren, expectedResult: false }

+ 40 - 31
config/abilities/pages/admin2ios.yaml

@@ -1,37 +1,46 @@
-  all_accesses_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'user'}]}
+all_accesses_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Admin2IOS'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'user' }],
+      }
 
-  all_organizations_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'organization'}]}
+all_organizations_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Admin2IOS'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'organization' }],
+      }
 
-  tips_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CorePremium']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'tips'}]}
+tips_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CorePremium'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'tips' }],
+      }
 
-  dgv_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
+dgv_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Admin2IOS'] }
 
-  cmf_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
+cmf_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Admin2IOS'] }
 
-  right_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
+right_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Admin2IOS'] }
 
-  tree_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
+tree_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Admin2IOS'] }

+ 88 - 46
config/abilities/pages/billing.yaml

@@ -1,53 +1,95 @@
-  billing_product_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
+billing_product_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'billings-administration' }],
+      }
 
-  billing_products_by_student_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+billing_products_by_student_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
 
-  billing_edition_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
+billing_edition_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'billings-administration' }],
+      }
 
-  billing_accounting_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
+billing_accounting_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'billings-administration' }],
+      }
 
-  billing_payment_list_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
+billing_payment_list_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'billings-administration' }],
+      }
 
-  pes_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Pes']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
+pes_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Pes'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'billings-administration' }],
+      }
 
-  berger_levrault_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BergerLevrault']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
+berger_levrault_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['BergerLevrault'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'billings-administration' }],
+      }
 
-  jvs_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Jvs']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
-      -
-  afi_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['AFI']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
+jvs_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Jvs'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'billings-administration' }],
+      }
+    -
+afi_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['AFI'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'billings-administration' }],
+      }

+ 27 - 27
config/abilities/pages/communication.yaml

@@ -1,29 +1,29 @@
-  inbox_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['MessagesAdvanced']}
-      - function: accessHasAnyRoleAbility
-        parameters:
-          - {action: 'read', subject: 'mails'}
-          - {action: 'read', subject: 'emails'}
-          - {action: 'read', subject: 'texto'}
+inbox_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['MessagesAdvanced'] }
+    - function: accessHasAnyRoleAbility
+      parameters:
+        - { action: 'read', subject: 'mails' }
+        - { action: 'read', subject: 'emails' }
+        - { action: 'read', subject: 'texto' }
 
-  message_send_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['MessagesAdvanced']}
-      - function: accessHasAnyRoleAbility
-        parameters:
-          - {action: 'read', subject: 'mails'}
-          - {action: 'read', subject: 'emails'}
-          - {action: 'read', subject: 'texto'}
+message_send_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['MessagesAdvanced'] }
+    - function: accessHasAnyRoleAbility
+      parameters:
+        - { action: 'read', subject: 'mails' }
+        - { action: 'read', subject: 'emails' }
+        - { action: 'read', subject: 'texto' }
 
-  message_templates_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['MessagesAdvanced']}
-      - function: accessHasAnyRoleAbility
-        parameters:
-          - {action: 'read', subject: 'mails'}
-          - {action: 'read', subject: 'emails'}
-          - {action: 'read', subject: 'texto'}
+message_templates_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['MessagesAdvanced'] }
+    - function: accessHasAnyRoleAbility
+      parameters:
+        - { action: 'read', subject: 'mails' }
+        - { action: 'read', subject: 'emails' }
+        - { action: 'read', subject: 'texto' }

+ 176 - 101
config/abilities/pages/cotisations.yaml

@@ -1,101 +1,176 @@
-  rate_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationRate', 'CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  parameters_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  send_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  state_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  pay_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  check_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  ledger_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  magazine_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCMFAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  ventilated_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  pay_erase_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  resume_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationTransmissionState']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  history_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  call_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationStructure']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  history_structure_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationStructure']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  insurance_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationStructure', 'CotisationTransmissionState']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  resume_all_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationTransmission']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-
-  resume_pay_cotisation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['CotisationTransmission']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
+rate_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationRate', 'CotisationCall'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+parameters_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+send_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+state_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+pay_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+check_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+ledger_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+magazine_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationCMFAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+ventilated_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+pay_erase_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+resume_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationTransmissionState'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+history_cotisation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['CotisationCall'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+call_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationStructure'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+history_structure_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationStructure'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+insurance_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationStructure', 'CotisationTransmissionState'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+resume_all_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationTransmission'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }
+
+resume_pay_cotisation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['CotisationTransmission'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'cotisation' }],
+      }

+ 8 - 5
config/abilities/pages/donor.yaml

@@ -1,5 +1,8 @@
-  donors_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Donors']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'donors'}]}
+donors_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Donors'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'donors' }],
+      }

+ 68 - 35
config/abilities/pages/educational.yaml

@@ -1,41 +1,74 @@
-  criteria_notations_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+criteria_notations_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
 
-  education_notation_config_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['AdvancedEducationNotation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+education_notation_config_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['AdvancedEducationNotation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
 
-  seizure_period_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PeriodValidation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+seizure_period_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['PeriodValidation'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
 
-  test_seizure_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsSeizure']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
+test_seizure_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['PedagogicsSeizure'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-seizure' }],
+      }
 
-  test_validation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+test_validation_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
 
-  examen_results_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+examen_results_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
 
-  education_by_student_validation_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsSeizure']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
+education_by_student_validation_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['PedagogicsSeizure'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-seizure' }],
+      }

+ 8 - 5
config/abilities/pages/equipment.yaml

@@ -1,5 +1,8 @@
-  equipment_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Equipments']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'equipments'}]}
+equipment_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Equipments'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'equipments' }],
+      }

+ 8 - 5
config/abilities/pages/medals.yaml

@@ -1,5 +1,8 @@
-  medals_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Medals']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'medals'}]}
+medals_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Medals'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'medals' }],
+      }

+ 87 - 68
config/abilities/pages/myAccount.yaml

@@ -1,82 +1,101 @@
-  my_schedule_page:
-    action: 'display'
-    conditions:
-      - { function: accessIsAdminAccount, expectedResult: false }
+my_schedule_page:
+  action: 'display'
+  conditions:
+    - { function: accessIsAdminAccount, expectedResult: false }
 
-  attendance_bookings_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['Attendances'] }
-      # TODO: l'action write existe-t-elle?
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'write', subject: 'attendances'}] }
-      - { function: accessIsAdminAccount, expectedResult: false }
+attendance_bookings_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Attendances'] }
+    # TODO: l'action write existe-t-elle?
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'write', subject: 'attendances' }],
+      }
+    - { function: accessIsAdminAccount, expectedResult: false }
 
-  my_attendance_page:
-    action: 'display'
-    conditions:
-      - { function: accessIsAdminAccount, expectedResult: false }
+my_attendance_page:
+  action: 'display'
+  conditions:
+    - { function: accessIsAdminAccount, expectedResult: false }
 
-  my_invitation_page:
-    action: 'display'
-    conditions:
-      - { function: accessIsAdminAccount, expectedResult: false }
+my_invitation_page:
+  action: 'display'
+  conditions:
+    - { function: accessIsAdminAccount, expectedResult: false }
 
-  my_students_page:
-    action: 'display'
-    conditions:
-      - { function: accessHasAnyProfile, parameters: [ 'teacher'] }
+my_students_page:
+  action: 'display'
+  conditions:
+    - { function: accessHasAnyProfile, parameters: ['teacher'] }
 
-  my_students_education_students_page:
-    action: 'display'
-    conditions:
-      - { function: accessHasAnyProfile, parameters: [ 'teacher'] }
+my_students_education_students_page:
+  action: 'display'
+  conditions:
+    - { function: accessHasAnyProfile, parameters: ['teacher'] }
 
-  criteria_notations_page_from_account_menu:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'criterianotation'}]}
+criteria_notations_page_from_account_menu:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'criterianotation' }],
+      }
 
-  my_education_students_page:
-    action: 'display'
-    conditions:
-      - { function: accessHasAnyProfile, parameters: [ 'student'] }
+my_education_students_page:
+  action: 'display'
+  conditions:
+    - { function: accessHasAnyProfile, parameters: ['student'] }
 
-  send_an_email_page:
-    action: 'display'
-    conditions:
-      - { function: accessHasAnyProfile, parameters: [ 'admin', 'teacher' ] }
+send_an_email_page:
+  action: 'display'
+  conditions:
+    - { function: accessHasAnyProfile, parameters: ['admin', 'teacher'] }
 
-  my_documents_page:
-    action: 'display'
-    conditions:
-      - { function: accessIsAdminAccount, expectedResult: false }
+my_documents_page:
+  action: 'display'
+  conditions:
+    - { function: accessIsAdminAccount, expectedResult: false }
 
-  my_profile_page:
-    action: 'display'
-    conditions:
-      - { function: accessIsAdminAccount, expectedResult: false }
+my_profile_page:
+  action: 'display'
+  conditions:
+    - { function: accessIsAdminAccount, expectedResult: false }
 
-  adherent_list_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['Users'] }
-      - { function: organizationIsShowAdherentList }
-      - { function: accessHasAnyProfile, parameters: ['member'] }
+adherent_list_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Users'] }
+    - { function: organizationIsShowAdherentList }
+    - { function: accessHasAnyProfile, parameters: ['member'] }
 
-  subscription_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
-      - { function: accessHasAnyProfile, parameters: ['admin', 'administratifManager', 'pedagogicManager', 'financialManager', 'caMember'] }
+subscription_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyProfile,
+        parameters:
+          [
+            'admin',
+            'administratifManager',
+            'pedagogicManager',
+            'financialManager',
+            'caMember',
+          ],
+      }
 
-  my_bills_page:
-    action: 'display'
-    conditions:
-      - { function: accessHasAnyProfile, parameters: ['guardian', 'payor']}
+my_bills_page:
+  action: 'display'
+  conditions:
+    - { function: accessHasAnyProfile, parameters: ['guardian', 'payor'] }
 
-  cmf_licence_person_page:
-    action: 'display'
-    conditions:
-      - { function: organizationIsCmf }
-      - { function: accessIsAdminAccount, expectedResult: false }
+cmf_licence_person_page:
+  action: 'display'
+  conditions:
+    - { function: organizationIsCmf }
+    - { function: accessIsAdminAccount, expectedResult: false }

+ 194 - 122
config/abilities/pages/parameters.yaml

@@ -1,124 +1,196 @@
-  organization_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['GeneralConfig']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'organization'}]}
-
-  cmf_licence_page:
-    action: 'display'
-    conditions:
-        - { function: organizationIsCmf}
-        - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
-        - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'organization'}] }
-
-  parameters_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'general-config'}] }
-
-  parameters_communication_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-
-  parameters_student_page:
-    action: 'display'
-    conditions:
-      - { function: organizationIsSchool }
-      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-
-  parameters_education_page:
-    action: 'display'
-    conditions:
-      - { function: organizationIsSchool }
-      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-
-  parameters_bills_page:
-    action: 'display'
-    conditions:
-      - { function: organizationIsSchool }
-      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-
-  parameters_secure_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-
-  place_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'place'}]}
-
-  education_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-
-  tag_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['TaggAdvanced']}
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'tagg'}]}
-
-  activities_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'activity'}]}
-
-  template_systems_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: ['TemplateMessages']}
-      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'general-config'}]}
-
-  billing_settings_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
-
-  billing_schedules_settings_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
-
-  online_registration_settings_page:
-    action: 'display'
-    conditions:
-      - {function: organizationIsSchool}
-      - {function: organizationHasAnyModule, parameters: ['IEL']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'onlineregistration-administration'}]}
-
-  transition_next_year_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'pedagogics-administration'}]}
-
-  course_duplication_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'pedagogics-administration'}]}
-
-  import_page:
-    action: 'display'
-    conditions:
-      - function: organizationHasAnyModule
+organization_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'organization' }],
+      }
+
+cmf_licence_page:
+  action: 'display'
+  conditions:
+    - { function: organizationIsCmf }
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'organization' }],
+      }
+
+parameters_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+parameters_communication_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+parameters_student_page:
+  action: 'display'
+  conditions:
+    - { function: organizationIsSchool }
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+parameters_education_page:
+  action: 'display'
+  conditions:
+    - { function: organizationIsSchool }
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+parameters_bills_page:
+  action: 'display'
+  conditions:
+    - { function: organizationIsSchool }
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+parameters_secure_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+place_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'place' }],
+      }
+
+education_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
+
+tag_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['TaggAdvanced'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'tagg' }],
+      }
+
+activities_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'activity' }],
+      }
+
+template_systems_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['TemplateMessages'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'general-config' }],
+      }
+
+billing_settings_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'billings-administration' }],
+      }
+
+billing_schedules_settings_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'billings-administration' }],
+      }
+
+online_registration_settings_page:
+  action: 'display'
+  conditions:
+    - { function: organizationIsSchool }
+    - { function: organizationHasAnyModule, parameters: ['IEL'] }
+    - {
+        function: accessHasAnyRoleAbility,
         parameters:
-          - 'Users'
-          - 'Equipments'
-      - function: accessHasAnyRoleAbility
+          [{ action: 'read', subject: 'onlineregistration-administration' }],
+      }
+
+transition_next_year_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
         parameters:
-          - {action: 'manage', subject: 'user'}
-          - {action: 'manage', subject: 'equipments'}
+          [{ action: 'manage', subject: 'pedagogics-administration' }],
+      }
+
+course_duplication_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters:
+          [{ action: 'manage', subject: 'pedagogics-administration' }],
+      }
+
+import_page:
+  action: 'display'
+  conditions:
+    - function: organizationHasAnyModule
+      parameters:
+        - 'Users'
+        - 'Equipments'
+    - function: accessHasAnyRoleAbility
+      parameters:
+        - { action: 'manage', subject: 'user' }
+        - { action: 'manage', subject: 'equipments' }

+ 50 - 35
config/abilities/pages/schedule.yaml

@@ -1,39 +1,54 @@
-  agenda_page:
-    action: 'display'
-    conditions:
-      - function: organizationHasAnyModule
-        parameters:
-          - 'Events'
-          - 'Courses'
-          - 'Examens'
-          - 'EducationalProjects'
-      - function: accessHasAnyRoleAbility
-        parameters:
-          - {action: 'read', subject: 'events'}
-          - {action: 'read', subject: 'examens'}
-          - {action: 'read', subject: 'educationalprojects'}
-          - {action: 'read', subject: 'courses'}
+agenda_page:
+  action: 'display'
+  conditions:
+    - function: organizationHasAnyModule
+      parameters:
+        - 'Events'
+        - 'Courses'
+        - 'Examens'
+        - 'EducationalProjects'
+    - function: accessHasAnyRoleAbility
+      parameters:
+        - { action: 'read', subject: 'events' }
+        - { action: 'read', subject: 'examens' }
+        - { action: 'read', subject: 'educationalprojects' }
+        - { action: 'read', subject: 'courses' }
 
-  attendance_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Attendances']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'attendances'}]}
+attendance_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Attendances'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'attendances' }],
+      }
 
-  course_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: [ 'Courses' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'courses' } ] }
+course_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Courses'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'courses' }],
+      }
 
-  exam_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: [ 'Examens' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'examens' } ] }
+exam_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Examens'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'examens' }],
+      }
 
-  pedagogics_project_page:
-    action: 'display'
-    conditions:
-      - { function: organizationHasAnyModule, parameters: [ 'EducationalProjects' ] }
-      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'educationalprojects' } ] }
+pedagogics_project_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['EducationalProjects'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'educationalprojects' }],
+      }

+ 38 - 20
config/abilities/pages/stats.yaml

@@ -1,23 +1,41 @@
-  report_activity_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['Statistic']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'statistic'}]}
+report_activity_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['Statistic'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'statistic' }],
+      }
 
-  education_quotas_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
+education_quotas_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'educationstudent' }],
+      }
 
-  fede_stats_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['StatisticFederation']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'statistic'}]}
+fede_stats_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['StatisticFederation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'statistic' }],
+      }
 
-  structure_stats_page:
-    action: 'display'
-    conditions:
-      - {function: organizationHasAnyModule, parameters: ['StatisticStructure']}
-      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'statistic'}]}
+structure_stats_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['StatisticStructure'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'statistic' }],
+      }

+ 7 - 14
doc/abilities.md

@@ -2,7 +2,7 @@
 
 ### 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 
+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 :
@@ -10,13 +10,12 @@ 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 : 
+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
@@ -30,7 +29,7 @@ Les rôles de l'utilisateur sont récupérés auprès de l'API. Ils peuvent êtr
 
 L'ensemble des droits est défini dans les fichiers de configuration `./config/abilities`
 
-Chaque entrée est constituée : 
+Chaque entrée est constituée :
 
 - d'un nom
 - d'une action associée (`display` ou `manage`)
@@ -45,7 +44,6 @@ Exemple :
         - { function: organizationIsShowAdherentList }
         - { function: accessHasAnyProfile, parameters: ['member'] }
 
-
 #### Le plugin ability.ts
 
 Le plugin `plugins/ability.ts` :
@@ -55,7 +53,6 @@ Le plugin `plugins/ability.ts` :
 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`
@@ -64,19 +61,15 @@ La première fait appel à son tour au service `RoleUtils` pour convertir tous l
 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 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)`. 
+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.
-
-
-
-

+ 8 - 18
doc/colors.md

@@ -1,4 +1,3 @@
-
 ## Définition des couleurs de l'UI
 
 ### Thèmes vuetify
@@ -10,7 +9,7 @@
 Les couleurs du ou des thèmes sont enregistrées dans `plugins/vuetify.ts`
 
 | Nom            | Usage                                                                                                                                         |
-|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
 | background     | Fond par défaut des contenus                                                                                                                  |
 | surface        | Fond des components                                                                                                                           |
 | primary        | Couleur principale, utilisée pour le header, les boutons submit, les notifications, les liens de type actions dans les menus, les liens, etc. |
@@ -27,7 +26,7 @@ Les couleurs du ou des thèmes sont enregistrées dans `plugins/vuetify.ts`
 
 Pour chacune de ces couleurs, on définit une ou plusieurs couleurs complémentaires, comme les couleurs `on-[color]` (voir plus bas)
 
-On trouvera aussi des couleurs en `x-[color]`: ce sont des couleurs particulières appliquées à un élément précis. Ces 
+On trouvera aussi des couleurs en `x-[color]`: ce sont des couleurs particulières appliquées à un élément précis. Ces
 couleurs sont appelées à disparaitre à terme.
 
 ### Règles de nommage
@@ -38,35 +37,26 @@ On construit le nom d'une variable de couleur de la manière suivante :
 
 Si la partie 'name' comprend plusieurs mots, on les sépare, eux aussi, par des tirets `-`
 
-
 ##### Préfixe (optionnel)
 
 Pour chaque couleur du thème, on définira des couleurs nommées `on-[color]` qui correspondent au "négatif" de la couleur,
 et qui permet de rendre du contenu visible sur un fond de la couleur `color`.
 
-
 ##### Suffixe (optionnel)
 
 Un éventuel suffixe pourra aussi permettre de définir des variantes applicable à certains éléments.
 
-Par exemple, une couleur `on-color--clickable` sera une variante de la couleur `on-color` applicable aux éléments 
+Par exemple, une couleur `on-color--clickable` sera une variante de la couleur `on-color` applicable aux éléments
 cliquables, comme les liens.
 
-
 ### Thèmes SCSS
 
-Pour chaque couleur du thème, on a défini des classes css `theme-[color]` qui appliqueront les règles à la fois sur 
+Pour chaque couleur du thème, on a défini des classes css `theme-[color]` qui appliqueront les règles à la fois sur
 au fond de l'élément et à son contenu.
 
 Ces classes sont définies dans le fichier `assets/css/theme.scss`
 
-> Dans la majorité des cas, appliquer la classe vuetify `bg-[color]` aura le même effet que d'appliquer la classe 
-`theme-[color]`. Mais il peut arriver que certains thèmes définis dans cette application implémentent des règles 
-spéciales pour certains éléments, comme la couleur des liens d'un thème par exemple. 
-C'est pourquoi il vaut mieux utiliser les classes `theme-[color]`.
-
-
-
-
-
-
+> Dans la majorité des cas, appliquer la classe vuetify `bg-[color]` aura le même effet que d'appliquer la classe
+> `theme-[color]`. Mais il peut arriver que certains thèmes définis dans cette application implémentent des règles
+> spéciales pour certains éléments, comme la couleur des liens d'un thème par exemple.
+> C'est pourquoi il vaut mieux utiliser les classes `theme-[color]`.

+ 2 - 6
doc/unittests.md

@@ -1,20 +1,16 @@
-
 # Tests unitaires
 
 ## Exécuter les tests
 
-Pour exécuter les tests unitaires, lancer : 
+Pour exécuter les tests unitaires, lancer :
 
     yarn test
 
-
 ## Vitest
 
-Les tests unitaires sont exécutés avec [Vitest](https://vitest.dev/), qui est recommandé par 
+Les tests unitaires sont exécutés avec [Vitest](https://vitest.dev/), qui est recommandé par
 la documentation [Vue3](https://vuejs.org/guide/scaling-up/testing.html#recommendation-1).
 
-
 ## Coverage
 
 La couverture des tests peut être retrouvée dans le fichier `./coverage/index.html`
-

+ 27 - 33
env/setupEnv.mjs

@@ -7,51 +7,45 @@
  *     HOST=ci node ./env/setupEnv.mjs
  *
  */
-import os from "os";
-import fs from "fs";
-import {fileURLToPath} from "node:url";
-import path from 'path';
-
+import os from 'os'
+import fs from 'fs'
+import { fileURLToPath } from 'node:url'
+import path from 'path'
 
 const projectDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
 
 const hostname = process.env.HOST ?? os.hostname()
 
 const environments = {
-    'app': '.env.docker',
-    'prod-v2': '.env.prod',
-    'test-v2': '.env.test',
-    'test1': '.env.test1',
-    'test2': '.env.test2',
-    'test3': '.env.test3',
-    'test4': '.env.test4',
-    'test5': '.env.test5',
-    'ci': '.env.ci',
+  app: '.env.docker',
+  'prod-v2': '.env.prod',
+  'test-v2': '.env.test',
+  test1: '.env.test1',
+  test2: '.env.test2',
+  test3: '.env.test3',
+  test4: '.env.test4',
+  test5: '.env.test5',
+  ci: '.env.ci',
 }
 
 if (!environments.hasOwnProperty(hostname)) {
-    throw Error("Critical : unknown environment [" + hostname + "]")
+  throw Error('Critical : unknown environment [' + hostname + ']')
 }
 
 const targetEnvFile = path.join(projectDir, 'env', environments[hostname])
 const mainEnvFile = path.join(projectDir, '.env')
 
 fs.unlink(mainEnvFile, (err) => {
-    // 'ENOENT' is the error code for "no such file or directory", we ignore this one
-    if (err && err.code !== 'ENOENT') {
-        throw err
-    }
-    console.log(`${mainEnvFile} was deleted`);
-});
-
-fs.symlink(
-    targetEnvFile,
-    '.env',
-    'file',
-    (err) => {
-        if (err) {
-            throw (err);
-        }
-        console.log(`Symlink created : ${mainEnvFile} -> ${targetEnvFile}`)
-    }
-)
+  // 'ENOENT' is the error code for "no such file or directory", we ignore this one
+  if (err && err.code !== 'ENOENT') {
+    throw err
+  }
+  console.log(`${mainEnvFile} was deleted`)
+})
+
+fs.symlink(targetEnvFile, '.env', 'file', (err) => {
+  if (err) {
+    throw err
+  }
+  console.log(`Symlink created : ${mainEnvFile} -> ${targetEnvFile}`)
+})

+ 5 - 5
lang/fr.json

@@ -176,7 +176,7 @@
   "cycle": "Cycle",
   "timing": "Durée d'un enseignement (en minutes)",
   "educationTiming": "Durée d'un enseignement (en minutes)",
-  "new_education_timings" : "Nouvelle durée d'enseignement",
+  "new_education_timings": "Nouvelle durée d'enseignement",
   "superAdmin": "Compte super-admin",
   "username": "Login de connexion",
   "residenceArea": "Zones de résidence",
@@ -204,7 +204,7 @@
   "bulletinPrintAddress": "L'adresse postale de l'élève ou son tuteur",
   "bulletinWithTeacher": "Le nom du professeur",
   "bulletinCriteriaSort": "Ordre de tri des critères",
-  "superAdminEmail" : "Adresse mail associée",
+  "superAdminEmail": "Adresse mail associée",
   "bulletin_parameters": "Bulletins",
   "sms": "Sms",
   "web_parameters": "Site internet",
@@ -649,11 +649,11 @@
   "allow_teachers_to_consult_colleagues_informations": "Autoriser les professeurs à consulter le listing de leurs collègues (noms, prénoms, et coordonnées)",
   "allow_students_to_consult_their_pedagogical_followup": "Autoriser les élèves à consulter leur suivi pédagogique",
   "allow_teachers_to_create_courses": "Autoriser les professeurs à créer des cours",
-  "INITIATION_CYCLE":  "Cycle initiation",
+  "INITIATION_CYCLE": "Cycle initiation",
   "CYCLE_1": "Cycle 1",
-  "CYCLE_2":  "Cycle 2",
+  "CYCLE_2": "Cycle 2",
   "CYCLE_3": "Cycle 3",
-  "CYCLE_4":  "Cycle 4",
+  "CYCLE_4": "Cycle 4",
   "OUT_CYCLE": "Hors cycle",
   "originalLabel": "Libellés d'origine",
   "effectiveLabel": "Libellés actuellement utilisés",

+ 1 - 1
layouts/.eslintrc.cjs

@@ -2,5 +2,5 @@
 module.exports = {
   rules: {
     'vue/multi-word-component-names': 0,
-  }
+  },
 }

+ 5 - 3
package.json

@@ -2,8 +2,8 @@
   "name": "app",
   "private": true,
   "version": "2.5.0",
-  "engines" : {
-    "node" : "18.19"
+  "engines": {
+    "node": "18.19"
   },
   "scripts": {
     "postinstall": "node ./env/setupEnv.mjs",
@@ -17,7 +17,9 @@
     "test": "vitest run",
     "enable-devtools": "nuxi devtools enable",
     "nuxt-upgrade": "nuxi upgrade --force",
-    "eslint":  "eslint ."
+    "eslint": "eslint .",
+    "prettier-check": "yarn prettier . --check",
+    "prettier-fix": "yarn prettier . --write"
   },
   "dependencies": {
     "@casl/ability": "^6.5.0",

+ 1 - 1
pages/.eslintrc.cjs

@@ -2,5 +2,5 @@
 module.exports = {
   rules: {
     'vue/multi-word-component-names': 0,
-  }
+  },
 }

+ 5 - 5
tests/.eslintrc.cjs

@@ -1,9 +1,9 @@
 /** On désactive cette règle pour les pages et les layouts seulement : https://eslint.vuejs.org/rules/multi-word-component-names.html */
 module.exports = {
   rules: {
-    "@typescript-eslint/ban-ts-comment": 0,
-    "@typescript-eslint/no-unused-vars": 0,
-    "@typescript-eslint/no-explicit-any": 0,
-    "require-await": 0
-  }
+    '@typescript-eslint/ban-ts-comment': 0,
+    '@typescript-eslint/no-unused-vars': 0,
+    '@typescript-eslint/no-explicit-any': 0,
+    'require-await': 0,
+  },
 }

+ 0 - 1
tests/units/readme.md

@@ -1,4 +1,3 @@
-
 # Tests unitaires
 
 > Voir doc/unittest.md

+ 2 - 7
tsconfig.json

@@ -12,12 +12,7 @@
       "@nuxtjs/i18n",
       "vitest/globals"
     ],
-    "exclude": [
-      "node_modules",
-      ".nuxt"
-    ],
-    "typeRoots": [
-      "./types"
-    ]
+    "exclude": ["node_modules", ".nuxt"],
+    "typeRoots": ["./types"]
   }
 }

+ 29 - 30
types/data.d.ts

@@ -1,58 +1,57 @@
-import ApiResource from "~/models/ApiResource";
-import type {EnumChoice} from "~/types/interfaces";
+import ApiResource from '~/models/ApiResource'
+import type { EnumChoice } from '~/types/interfaces'
 
 type AnyJson = Record<string, any>
 
 interface AssociativeArray {
-    [key: string]: any;
+  [key: string]: any
 }
 
 interface Connector {
-    request(
-        method: HTTP_METHOD,
-        url: string,
-        body: null | any,
-        params: null | AssociativeArray,
-        query: null | AssociativeArray
-    )
+  request(
+    method: HTTP_METHOD,
+    url: string,
+    body: null | any,
+    params: null | AssociativeArray,
+    query: null | AssociativeArray,
+  )
 }
 
 interface HydraMetadata {
-    readonly totalItems?: number
-    firstPage?: number
-    lastPage?: number
-    nextPage?: number
-    previousPage?: number
-    type?: METADATA_TYPE
+  readonly totalItems?: number
+  firstPage?: number
+  lastPage?: number
+  nextPage?: number
+  previousPage?: number
+  type?: METADATA_TYPE
 }
 
 interface ApiResponse {
-    data: AnyJson
-    metadata: HydraMetadata
+  data: AnyJson
+  metadata: HydraMetadata
 }
 
 interface ApiCollection extends ApiResponse {
-    data: AnyJson
-    metadata: HydraMetadata
+  data: AnyJson
+  metadata: HydraMetadata
 }
 
-
 interface Pagination {
-    first?: number
-    last?: number
-    next?: number
-    previous?: number
+  first?: number
+  last?: number
+  next?: number
+  previous?: number
 }
 
 interface Collection {
-    items: Array<ApiResource>
-    pagination: Pagination
-    totalItems: number | undefined
+  items: Array<ApiResource>
+  pagination: Pagination
+  totalItems: number | undefined
 }
 
 interface EnumItem {
-    value: string
-    label: string
+  value: string
+  label: string
 }
 
 type Enum = Array<EnumChoice>

+ 9 - 13
types/interfaces.d.ts

@@ -1,12 +1,8 @@
 import { Store } from 'pinia'
-import {AnyJson} from "~/types/enum/data";
-import {
-  ABILITIES,
-  GENDER,
-  TYPE_ALERT,
-} from '~/types/enum/enums'
-import {type ComputedRef, Ref} from "@vue/reactivity";
-import type {MenuGroup, MenuItem} from "~/types/layout";
+import { AnyJson } from '~/types/enum/data'
+import { ABILITIES, GENDER, TYPE_ALERT } from '~/types/enum/enums'
+import { type ComputedRef, Ref } from '@vue/reactivity'
+import type { MenuGroup, MenuItem } from '~/types/layout'
 
 declare module '@vuex-orm/core' {
   interface Query {
@@ -168,11 +164,11 @@ interface DolibarrAccount {
   socId: number
   clientNumber: string
   product:
-      | 'PRODUCT_ARTIST'
-      | 'PRODUCT_ARTIST_PREMIUM'
-      | 'PRODUCT_SCHOOL'
-      | 'PRODUCT_SCHOOL_PREMIUM'
-      | 'PRODUCT_MANAGER'
+    | 'PRODUCT_ARTIST'
+    | 'PRODUCT_ARTIST_PREMIUM'
+    | 'PRODUCT_SCHOOL'
+    | 'PRODUCT_SCHOOL_PREMIUM'
+    | 'PRODUCT_MANAGER'
   contract: DolibarrContract
   bills: Array<DolibarrBill>
 }

+ 18 - 18
types/layout.d.ts

@@ -1,33 +1,33 @@
-import {MENU_LINK_TYPE} from "~/types/enum/layout";
+import { MENU_LINK_TYPE } from '~/types/enum/layout'
 
 interface IconItem {
-    name?: string
-    avatarId?: number | null
-    avatarByDefault?: string
+  name?: string
+  avatarId?: number | null
+  avatarByDefault?: string
 }
 
 /**
  * Entrée d'un menu, correspondant à un lien de navigation
  */
 interface MenuItem {
-    label: string
-    icon?: IconItem
-    to?: string
-    /** Type de lien (interne, externe, v1)  */
-    type: MENU_LINK_TYPE
-    avatar?: number
-    /** Correspond à la page actuelle */
-    active: boolean
+  label: string
+  icon?: IconItem
+  to?: string
+  /** Type de lien (interne, externe, v1)  */
+  type: MENU_LINK_TYPE
+  avatar?: number
+  /** Correspond à la page actuelle */
+  active: boolean
 }
 
 /**
  * Sous-menu, regroupant d'autres MenuItem et/ou MenuGroup
  */
 interface MenuGroup {
-    label: string
-    icon?: IconItem
-    children?: MenuItems
-    actions?: MenuItems
+  label: string
+  icon?: IconItem
+  children?: MenuItems
+  actions?: MenuItems
 }
 
 type MenuItems = Array<MenuItem | MenuGroup>
@@ -36,6 +36,6 @@ type MenuItems = Array<MenuItem | MenuGroup>
  * Service responsable de la génération d'un menu
  */
 interface MenuBuilder {
-    getMenuName: () => string
-    build: () => MenuItem | MenuGroup | null
+  getMenuName: () => string
+  build: () => MenuItem | MenuGroup | null
 }

部分文件因为文件数量过多而无法显示