瀏覽代碼

merge release/2.5

Olivier Massot 6 月之前
父節點
當前提交
3ea627ef40
共有 100 個文件被更改,包括 1450 次插入967 次删除
  1. 0 58
      .eslintrc.cjs
  2. 33 19
      .gitlab-ci.yml
  3. 11 0
      Dockerfile
  4. 16 0
      README.md
  5. 42 0
      assets/css/global.scss
  6. 28 0
      assets/css/vue-date-picker.scss
  7. 6 6
      components/Layout/Alert/Container.vue
  8. 4 3
      components/Layout/Alert/Content.vue
  9. 63 13
      components/Layout/AlertBar.vue
  10. 46 43
      components/Layout/AlertBar/Cotisation.vue
  11. 0 1
      components/Layout/AlertBar/Env.vue
  12. 1 1
      components/Layout/AlertBar/OnlineRegistration.vue
  13. 1 1
      components/Layout/AlertBar/RegistrationStatus.vue
  14. 3 3
      components/Layout/AlertBar/SuperAdmin.vue
  15. 3 3
      components/Layout/AlertBar/SwitchYear.vue
  16. 8 8
      components/Layout/Dialog.vue
  17. 5 2
      components/Layout/Dialog/Trial/AlreadyDid.vue
  18. 45 24
      components/Layout/Header.vue
  19. 1 1
      components/Layout/Header/HomeBtn.vue
  20. 9 10
      components/Layout/Header/Menu.vue
  21. 1 1
      components/Layout/Header/Notification.vue
  22. 27 0
      components/Layout/Header/Title.vue
  23. 10 3
      components/Layout/Header/UniversalCreation/Card.vue
  24. 4 5
      components/Layout/Header/UniversalCreation/CreateButton.vue
  25. 5 5
      components/Layout/Header/UniversalCreation/EventParams.vue
  26. 33 25
      components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue
  27. 21 14
      components/Layout/MainMenu.vue
  28. 2 0
      components/Layout/Pages/Subscription/Card.vue
  29. 3 3
      components/Layout/Pages/Subscription/List.vue
  30. 13 6
      components/Layout/Parameters/EntityTable.vue
  31. 65 38
      components/Layout/Parameters/Menu.vue
  32. 19 0
      components/Layout/Parameters/Section.vue
  33. 97 31
      components/Layout/Parameters/Table.vue
  34. 86 0
      components/Layout/Parameters/Website/ActivationSwitch.vue
  35. 5 5
      components/Layout/SubHeader/ActivityYear.vue
  36. 5 8
      components/Layout/SubHeader/Breadcrumbs.vue
  37. 7 6
      components/Layout/SubHeader/DataTiming.vue
  38. 10 4
      components/Layout/SubHeader/DataTimingRange.vue
  39. 6 6
      components/Layout/SubHeader/PersonnalizedList.vue
  40. 5 4
      components/Layout/Subheader.vue
  41. 2 2
      components/Layout/UpgradePremiumButton.vue
  42. 1 1
      components/Ui/Button/Delete.vue
  43. 25 15
      components/Ui/Button/Submit.vue
  44. 5 3
      components/Ui/Collection.vue
  45. 17 7
      components/Ui/DataTable.vue
  46. 70 94
      components/Ui/DatePicker.vue
  47. 0 129
      components/Ui/DateRangePicker.vue
  48. 37 15
      components/Ui/Form.vue
  49. 7 7
      components/Ui/Form/Creation.vue
  50. 2 9
      components/Ui/Form/DeletionConfirmationDialog.vue
  51. 9 9
      components/Ui/Form/Edition.vue
  52. 10 10
      components/Ui/Help.vue
  53. 27 10
      components/Ui/Image.vue
  54. 32 12
      components/Ui/Input/Autocomplete.vue
  55. 17 5
      components/Ui/Input/Autocomplete/Accesses.vue
  56. 16 12
      components/Ui/Input/AutocompleteWithAPI.vue
  57. 7 10
      components/Ui/Input/AutocompleteWithAp2i.vue
  58. 2 0
      components/Ui/Input/AutocompleteWithEnum.vue
  59. 18 2
      components/Ui/Input/Checkbox.vue
  60. 2 0
      components/Ui/Input/Combobox.vue
  61. 47 23
      components/Ui/Input/DatePicker.vue
  62. 5 2
      components/Ui/Input/Email.vue
  63. 5 4
      components/Ui/Input/Enum.vue
  64. 54 17
      components/Ui/Input/Image.vue
  65. 1 7
      components/Ui/Input/Number.vue
  66. 14 5
      components/Ui/Input/Phone.vue
  67. 7 4
      components/Ui/Input/Text.vue
  68. 4 4
      components/Ui/ItemFromUri.vue
  69. 14 26
      components/Ui/SystemBar.vue
  70. 15 5
      components/Ui/Template/DataTable.vue
  71. 2 2
      components/Ui/Template/Date.vue
  72. 3 3
      components/Ui/Xeditable/Text.vue
  73. 1 1
      composables/data/useAp2iRequestService.ts
  74. 1 1
      composables/data/useApiLegacyRequestService.ts
  75. 2 2
      composables/data/useEntityFetch.ts
  76. 4 5
      composables/data/useImageFetch.ts
  77. 1 2
      composables/utils/useDownloadFile.ts
  78. 0 2
      composables/utils/useDownloadFromRoute.ts
  79. 12 4
      composables/utils/useRedirect.ts
  80. 9 0
      config/abilities/pages/billing.yaml
  81. 33 0
      config/abilities/pages/parameters.yaml
  82. 2 2
      env/setupEnv.mjs
  83. 41 40
      eslint.config.mjs
  84. 54 28
      i18n/lang/fr.json
  85. 0 6
      layouts/.eslintrc.cjs
  86. 1 1
      layouts/default.vue
  87. 1 1
      layouts/error.vue
  88. 17 8
      layouts/parameters.vue
  89. 2 2
      models/Access/MyProfile.ts
  90. 0 1
      models/ApiResource.ts
  91. 1 1
      models/Core/Notification.ts
  92. 1 5
      models/Custom/Search/UserSearchItem.ts
  93. 1 1
      models/Organization/Parameters.ts
  94. 3 5
      models/decorators.ts
  95. 2 2
      nuxt.config.ts
  96. 4 5
      package.json
  97. 0 6
      pages/.eslintrc.cjs
  98. 8 5
      pages/parameters/attendance_booking_reasons/[id].vue
  99. 9 6
      pages/parameters/attendance_booking_reasons/new.vue
  100. 14 11
      pages/parameters/attendances.vue

+ 0 - 58
.eslintrc.cjs

@@ -1,58 +0,0 @@
-module.exports = {
-  root: true,
-  env: {
-    browser: true,
-    node: true,
-  },
-  parser: 'vue-eslint-parser',
-  parserOptions: {
-    ecmaVersion: 2020,
-    parser: '@typescript-eslint/parser',
-    sourceType: 'module',
-    tsconfigRootDir: __dirname,
-  },
-  extends: [
-    '@nuxtjs/eslint-config-typescript',
-    'plugin:nuxt/recommended',
-    'eslint:recommended',
-    'plugin:@typescript-eslint/recommended',
-    'plugin:vue/vue3-recommended',
-    'plugin:prettier/recommended',
-    'plugin:you-dont-need-lodash-underscore/compatible',
-  ],
-  ignorePatterns: [
-    '.nuxt',
-    'coverage/*',
-    'vendor/*',
-    'dist/*',
-    'models/models.ts',
-  ],
-  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,
-      },
-    ],
-    'vue/multi-word-component-names': 0,
-    '@typescript-eslint/no-inferrable-types': 0,
-  },
-  globals: {
-    useRuntimeConfig: 'readonly',
-    useAsyncData: 'readonly',
-    navigateTo: 'readonly',
-    computed: 'readonly',
-    ref: 'readonly',
-    definePageMeta: 'readonly',
-    useRouter: 'readonly',
-    useRoute: 'readonly',
-    useI18n: 'readonly',
-    onMounted: 'readonly',
-    onUnmounted: 'readonly',
-    watch: 'readonly',
-    useRepo: 'readonly',
-  },
-}

+ 33 - 19
.gitlab-ci.yml

@@ -1,39 +1,41 @@
 stages:
+  - build
   - test
-  - qa
+  - analysis
 
 variables:
   APP_ENV: ci
 
-before_script:
-  - echo "" > ./env/local.app.opentalent.fr.crt
-  - echo "" > ./env/local.app.opentalent.fr.key
-  - corepack enable
-  - yarn set version berry
-  - yarn install --network-timeout 10000
-  - HOSTNAME=ci yarn prepare
-
 cache:
   paths:
     - ./node_modules
     - .yarn
+    - yarn.lock
 
-code_quality:
-  stage: qa
+build_image:
+  stage: build
+  image: docker:20.10
+  before_script:
+    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
   script:
-    - yarn eslint
-
-code_style:
-  stage: qa
-  script:
-    - yarn prettier . --check
+    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
+    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
+    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
+    - docker push $CI_REGISTRY_IMAGE:latest
+
+.default_config: &default_config
+  image: $CI_REGISTRY_IMAGE:latest
+  before_script:
+    - echo "" > ./env/local.app.opentalent.fr.crt
+    - echo "" > ./env/local.app.opentalent.fr.key
+    - yarn install --network-timeout 10000
+    - HOSTNAME=ci yarn prepare
 
 unit:
+  <<: *default_config
   stage: test
-
   script:
     - yarn test
-
   artifacts:
     paths:
       - ./coverage/
@@ -46,3 +48,15 @@ unit:
 
   # Extract total coverage from job logs (https://docs.gitlab.com/15.6/ee/ci/yaml/index.html#coverage)
   coverage: '/All files\s*|\s*\d+\.\d+/'
+
+code_quality:
+  <<: *default_config
+  stage: analysis
+  script:
+    - yarn eslint
+
+code_style:
+  <<: *default_config
+  stage: analysis
+  script:
+    - yarn prettier . --check

+ 11 - 0
Dockerfile

@@ -0,0 +1,11 @@
+FROM node:20-slim
+
+ENV WORKDIR /home/workspace
+
+RUN corepack enable; \
+    yarn set version 4.3.1;
+
+# Define working directory.
+WORKDIR ${WORKDIR}
+
+CMD node

+ 16 - 0
README.md

@@ -67,6 +67,22 @@ Attention, sur les environnements de test, il faut utiliser nvm pour exécuter l
 
     nvm exec yarn install
 
+### Activer / désactiver le mode maintenance
+
+Pour activer le mode maintenance en production :
+
+    # (éditer les dates et heures de la maintenance, ligne 75)
+    nano /var/opentalent/git/app/public/maintenance.html
+
+    # activer la maintenance
+    touch /var/opentalent/git/app/.maintenance
+
+Pour le désactiver :
+
+    rm /var/opentalent/git/app/.maintenance
+
+> Les Ips internes sont exclues du mode maintenance
+
 ## Autres
 
 ### Lancer les tests

+ 42 - 0
assets/css/global.scss

@@ -70,3 +70,45 @@ header .v-toolbar__content {
 .v-application {
   font-size: 0.9rem;
 }
+
+h3,
+h4 {
+  color: rgb(var(--v-theme-on-neutral-soft));
+}
+
+h3 {
+  font-size: 1.3rem;
+  color: rgb(var(--v-theme-on-neutral));
+}
+
+h4 {
+  font-size: 1.1rem;
+  color: rgb(var(--v-theme-on-neutral));
+}
+
+// Encart informatif
+.explanation {
+  display: flex;
+  flex-direction: row;
+  margin: 32px;
+  padding: 8px 4px;
+  border-radius: 6px;
+  text-align: justify;
+  color: rgb(var(--v-theme-info));
+  border: solid 1px rgb(var(--v-theme-info));
+
+  .v-icon {
+    color: rgb(var(--v-theme-info));
+    font-size: 22px;
+    border-radius: 16px;
+    margin: 3px 1px;
+    padding: 3px;
+    height: 28px;
+    width: 28px;
+  }
+
+  @media (max-width: 600px) {
+    flex-direction: column;
+    justify-content: center;
+  }
+}

+ 28 - 0
assets/css/vue-date-picker.scss

@@ -0,0 +1,28 @@
+// @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);
+}
+
+:root {
+  --dp-action-button-height: 35px;
+}

+ 6 - 6
components/Layout/Alert/Container.vue

@@ -1,13 +1,14 @@
 <!--
 Container principal pour l'affichage d'une ou plusieurs alertes
 -->
+<!-- eslint-disable vue/valid-v-for -->
 
 <template>
   <main class="alertContainer">
     <client-only>
       <LayoutAlertContent
-        v-for="(alert, key) in alerts"
-        :key="key"
+        v-for="(alert, index) in alerts"
+        :key="index"
         :alert="alert"
         class="alertContent"
       />
@@ -16,15 +17,14 @@ Container principal pour l'affichage d'une ou plusieurs alertes
 </template>
 
 <script setup lang="ts">
-import type { ComputedRef } from 'vue'
+import { computed } from 'vue'
 import type { Alert } from '~/types/interfaces'
 import { usePageStore } from '~/stores/page'
 
 const pageStore = usePageStore()
 
-const alerts: ComputedRef<Array<Alert>> = computed(() => {
-  return pageStore.alerts
-})
+// Using alerts in the template's v-for directive
+const alerts = computed<Array<Alert>>(() => pageStore.alerts)
 </script>
 
 <style scoped>

+ 4 - 3
components/Layout/Alert/Content.vue

@@ -1,4 +1,5 @@
 <!-- Message d'alerte -->
+<!-- eslint-disable vue/valid-v-for -->
 
 <template>
   <v-alert
@@ -47,7 +48,7 @@ const props = defineProps({
 })
 
 const show: Ref<boolean> = ref(true)
-let timeout: any = null
+let timeoutId: ReturnType<typeof setTimeout> | null = null
 
 const pageStore = usePageStore()
 
@@ -56,7 +57,7 @@ const pageStore = usePageStore()
  * @param time
  */
 const clearAlert = (time: number = 4000) => {
-  timeout = setTimeout(() => {
+  timeoutId = setTimeout(() => {
     show.value = false
     pageStore.removeSlowlyAlert()
   }, time)
@@ -66,7 +67,7 @@ const clearAlert = (time: number = 4000) => {
  * Réinitialise et suspend le délai avant le retrait de l'alerte au survol du curseur
  */
 const onMouseOver = () => {
-  clearTimeout(timeout)
+  clearTimeout(timeoutId)
 }
 
 /**

+ 63 - 13
components/Layout/AlertBar.vue

@@ -6,28 +6,78 @@ Contient les différentes barres d'alertes qui s'affichent dans certains cas
 
 <template>
   <main>
-    <LayoutAlertBarEnv />
+    <v-expand-transition>
+      <div v-if="showAlertBars">
+        <LayoutAlertBarEnv style="z-index: 510" />
 
-    <LayoutAlertBarSwitchUser />
+        <LayoutAlertBarSwitchUser style="z-index: 509" />
 
-    <client-only>
-      <LayoutAlertBarCotisation
-        v-if="organizationProfile.isCmf && ability.can('manage', 'cotisation')"
-      />
-    </client-only>
+        <client-only>
+          <LayoutAlertBarCotisation
+            v-if="
+              organizationProfile.isCmf && ability.can('manage', 'cotisation')
+            "
+            style="z-index: 508"
+          />
+        </client-only>
 
-    <LayoutAlertBarSwitchYear />
-    <LayoutAlertBarSuperAdmin />
-    <LayoutAlertBarRegistrationStatus
-      v-if="organizationProfile.hasModule('IEL')"
-    />
+        <LayoutAlertBarSwitchYear style="z-index: 507" />
+        <LayoutAlertBarSuperAdmin style="z-index: 506" />
+        <LayoutAlertBarRegistrationStatus
+          v-if="organizationProfile.hasModule('IEL')"
+          style="z-index: 505"
+        />
+      </div>
+    </v-expand-transition>
+
+    <div v-if="smAndDown">
+      <div
+        class="folded-bar theme-warning"
+        style="z-index: 504"
+        @click="onFoldedWarningClick"
+      >
+        <v-icon small icon="fas fa-exclamation-triangle mx-1" />
+        <span>{{ $t('show_warnings') }}</span>
+        <v-icon
+          small
+          :icon="
+            'fas mx-1' +
+            (unfoldWarnings ? ' fa-chevron-up' : ' fa-chevron-down')
+          "
+        />
+      </div>
+    </div>
   </main>
 </template>
 
 <script setup lang="ts">
-import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import { useAbility } from '@casl/vue'
+import { useDisplay } from 'vuetify'
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 const organizationProfile = useOrganizationProfileStore()
 const ability = useAbility()
+const { mdAndUp, smAndDown } = useDisplay()
+
+const unfoldWarnings = ref(false)
+
+const onFoldedWarningClick = () => {
+  unfoldWarnings.value = !unfoldWarnings.value
+}
+
+const showAlertBars = computed(() => mdAndUp.value || unfoldWarnings.value)
 </script>
+
+<style scoped lang="scss">
+.folded-bar {
+  position: relative;
+  font-size: 14px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  text-align: center;
+  padding: 12px;
+  cursor: pointer;
+}
+</style>

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

@@ -18,9 +18,8 @@ Barre d'alerte qui s'affiche pour donner l'état de la cotisation
 
 <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 type { ALERT_STATE_COTISATION } from '~/types/enum/enums'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Cotisation from '~/models/Organization/Cotisation'
 
@@ -29,7 +28,51 @@ const organizationProfile = useOrganizationProfileStore()
 const runtimeConfig = useRuntimeConfig()
 const baseLegacyUrl: string = runtimeConfig.baseUrlAdminLegacy
 
-const cotisationYear: Ref<number | null> = ref(null)
+// On récupère l'état des cotisations via l'API
+if (!organizationProfile.id) {
+  throw new Error('missing organization id')
+}
+
+const { fetch } = useEntityFetch()
+const { data: cotisation, pending } = await fetch(
+  Cotisation,
+  organizationProfile.id,
+)
+
+interface Alert {
+  text: string
+  callback: () => void
+}
+
+const cotisationYear: ComputedRef<number | null> = computed(() => {
+  if (pending.value || cotisation.value === null) {
+    return null
+  }
+
+  return cotisation.value.cotisationYear
+})
+
+const alert: ComputedRef<Alert | null> = computed(() => {
+  if (pending.value || cotisation.value === null) {
+    return null
+  }
+
+  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,
+    },
+  }
+
+  if (!cotisation.value.alertState) {
+    return null
+  }
+
+  return mapping[cotisation.value.alertState as ALERT_STATE_COTISATION]
+})
 
 /**
  * Redirige l'utilisateur vers la page des cotisations
@@ -78,46 +121,6 @@ const openCmfSubscriptionPage = () => {
     '_blank',
   )
 }
-
-// On récupère l'état des cotisations via l'API
-if (!organizationProfile.id) {
-  throw new Error('missing organization id')
-}
-
-const { fetch } = useEntityFetch()
-const { data: cotisation, pending } = await fetch(
-  Cotisation,
-  organizationProfile.id,
-)
-
-interface Alert {
-  text: string
-  callback: () => void
-}
-
-const alert: ComputedRef<Alert | null> = computed(() => {
-  if (pending.value) {
-    return null
-  }
-
-  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,
-    },
-  }
-
-  if (!cotisation.value.alertState) {
-    return null
-  }
-
-  return mapping[cotisation.value.alertState as ALERT_STATE_COTISATION]
-})
 </script>
 
 <style scoped lang="scss">

+ 0 - 1
components/Layout/AlertBar/Env.vue

@@ -10,7 +10,6 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur n'est pas dans un environneme
     :text="$t('not_production_environment', { env: env })"
     icon="fas fa-exclamation-triangle"
     class="theme-warning"
-    style="z-index: 1005"
   />
   <!--
   Le z-index est précisé pour éviter cette erreur : https://github.com/vuetifyjs/nuxt-module/issues/205

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

@@ -13,10 +13,10 @@ Barre d'alerte sur l'ouverture ou non de l'inscription en ligne
 </template>
 
 <script setup lang="ts">
+import type { ComputedRef } from 'vue'
 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()
 

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

@@ -13,10 +13,10 @@ Barre d'alerte quand au statut (l'avancement) de l'inscription en ligne de l'uti
 </template>
 
 <script setup lang="ts">
+import type { ComputedRef } from 'vue'
 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()
 

+ 3 - 3
components/Layout/AlertBar/SuperAdmin.vue

@@ -13,10 +13,10 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur est un super admin en mode sw
 </template>
 
 <script setup lang="ts">
+import type { ComputedRef } from 'vue'
+import { navigateTo } from '#app'
 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()
@@ -37,7 +37,7 @@ const url: ComputedRef<string> = computed(() => {
     ? accessProfile.originalAccess.id
     : null
 
-  if (show && orgId && originalAccessId) {
+  if (show.value && orgId && originalAccessId) {
     return makeAdminUrl(
       UrlUtils.join('switch_user', orgId, originalAccessId, 'exit'),
     )

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

@@ -10,9 +10,9 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur n'est pas sur l'année couran
     {{ $t('not_current_year') }}
 
     <a
-      @click="resetYear"
       class="text-decoration-none on-warning"
       style="cursor: pointer"
+      @click="resetYear"
     >
       <strong class="pl-2 text-neutral-strong">
         {{ $t('not_current_year_reset') }}
@@ -22,9 +22,9 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur n'est pas sur l'année couran
 </template>
 
 <script setup lang="ts">
+import type { ComputedRef } from 'vue'
 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'
@@ -65,7 +65,7 @@ const resetYear = async () => {
 
   pageStore.loading = true
   await em.patch(Access, accessProfile.currentAccessId, defaultValues)
-  if (process.server) {
+  if (import.meta.server) {
     // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }

+ 8 - 8
components/Layout/Dialog.vue

@@ -13,9 +13,9 @@
           theme
         "
       >
-        <h3 class="d-flex">
-          <v-icon icon="fa-solid fa-bullhorn" />
-          <slot name="dialogType" />
+        <h3 :class="'d-flex theme-' + theme">
+          <v-icon icon="fa-solid fa-bullhorn" width="25" htight="25" />
+          <span class="pt-4"><slot name="dialogType" /></span>
         </h3>
       </div>
 
@@ -48,6 +48,7 @@ const props = defineProps({
   contentClass: {
     type: String,
     required: false,
+    default: '',
   },
   theme: {
     type: String,
@@ -61,13 +62,13 @@ const props = defineProps({
   },
 })
 
-// @ts-ignore  -> just to avoid the error with the prop's type of v-dialog
+// @ts-expect-error This is 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;
+  padding-left: 16px;
   font-weight: normal;
 }
 
@@ -79,17 +80,16 @@ const _show = computed(() => props.show) as boolean
   padding: 25px 10px;
 
   h3 {
-    font-size: 25px;
     font-weight: normal;
     writing-mode: vertical-lr;
     transform: rotate(-180deg);
+    padding-left: 6px;
   }
 
   .v-icon {
     font-size: 25px;
     transform: rotate(90deg);
-    padding-right: 20px;
-    padding-bottom: 10px;
+    padding-right: 25px;
   }
 }
 

+ 5 - 2
components/Layout/Dialog/Trial/AlreadyDid.vue

@@ -28,7 +28,10 @@
       <v-btn class="mr-4 submitBtn theme-warning" @click="goSubscribe">
         {{ $t('i_subscribe') }}
       </v-btn>
-      <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="contactOpentalent">
+      <v-btn
+        class="mr-4 submitBtn theme-neutral-strong"
+        @click="contactOpentalent"
+      >
         {{ $t('opentalent_contact') }}
       </v-btn>
     </template>
@@ -36,7 +39,7 @@
 </template>
 
 <script setup lang="ts">
-import UrlUtils from "~/services/utils/urlUtils";
+import UrlUtils from '~/services/utils/urlUtils'
 
 const runtimeConfig = useRuntimeConfig()
 

+ 45 - 24
components/Layout/Header.vue

@@ -7,28 +7,24 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
   <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="hasLateralMenu"
+        :icon="isLateralMenuOpened ? 'mdi:mdi-menu-open' : 'mdi:mdi-menu'"
+        @click="toggleLateralMenu"
       />
-      <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-else
-          :icon="isParametersMenuOpened ? 'mdi:mdi-menu-open' : 'mdi:mdi-menu'"
-          @click="toggleParametersMenu"
-        />
-      </div>
     </template>
 
-    <v-toolbar-title v-if="mdAndUp" v-text="title" />
+    <v-toolbar-title v-if="smAndUp">
+      <LayoutHeaderTitle>
+        {{ title }}
+      </LayoutHeaderTitle>
+    </v-toolbar-title>
 
     <LayoutThemeSwitcher v-if="false" />
     <!-- En attente validation PO -->
 
     <LayoutHeaderUniversalCreationCreateButton
       v-if="showUniversalButton"
-      class="mr-3"
+      :class="smAndUp ? 'mr-3' : ''"
     />
 
     <LayoutHeaderHomeBtn v-if="smAndUp" />
@@ -47,7 +43,7 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
 
     <a
       :href="runtimeConfig.supportUrl || runtimeConfig.public.supportUrl"
-      class="text-body px-3 py-4 ml-2 theme-secondary text-decoration-none h-100"
+      class="help theme-secondary"
       target="_blank"
     >
       <span class="d-none d-sm-none d-md-flex">
@@ -79,15 +75,29 @@ const title: ComputedRef<string> = computed(
 
 const { hasMenu, isMenuOpened, toggleMenu } = useMenu()
 
-const { smAndUp, mdAndUp } = useDisplay()
-
-const hasMainMenu = computed(() => hasMenu('Main'))
-const isMainMenuOpened = computed(() => isMenuOpened('Main'))
-const toggleMainMenu = () => toggleMenu('Main')
-
-const hasParametersMenu = computed(() => hasMenu('Parameters'))
-const isParametersMenuOpened = computed(() => isMenuOpened('Parameters'))
-const toggleParametersMenu = () => toggleMenu('Parameters')
+const { smAndUp } = useDisplay()
+
+const hasLateralMenu = computed(() => {
+  return (
+    (layoutStore.name !== 'parameters' && hasMenu('Main')) ||
+    (layoutStore.name === 'parameters' && hasMenu('Parameters'))
+  )
+})
+
+const isLateralMenuOpened = computed(() => {
+  return (
+    (layoutStore.name !== 'parameters' && isMenuOpened('Main')) ||
+    (layoutStore.name === 'parameters' && isMenuOpened('Parameters'))
+  )
+})
+
+const toggleLateralMenu = () => {
+  if (layoutStore.name === 'parameters') {
+    toggleMenu('Parameters')
+  } else {
+    toggleMenu('Main')
+  }
+}
 
 const ability = useAbility()
 const showUniversalButton =
@@ -106,9 +116,20 @@ const layoutStore = useLayoutStore()
 </script>
 
 <style scoped>
+:deep(.v-toolbar__content > .v-toolbar-title) {
+  margin-left: 2px;
+
+  .v-img {
+    background-color: rgb(var(--v-theme-on-primary));
+    border-radius: 18px;
+  }
+}
+
 .help {
-  padding: 14px 14px 13px;
+  padding: 12px 16px;
+  margin-left: 4px;
   font-size: 14px;
   text-decoration: none;
+  height: 100%;
 }
 </style>

+ 1 - 1
components/Layout/Header/HomeBtn.vue

@@ -12,7 +12,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from '@vue/reactivity'
+import { ref } from 'vue'
 import { useDisplay } from 'vuetify'
 import { useHomeUrl } from '~/composables/utils/useHomeUrl'
 

+ 9 - 10
components/Layout/Header/Menu.vue

@@ -16,7 +16,7 @@ header principal (configuration, paramètres du compte...)
           :defaultImage="menu.icon.avatarByDefault"
           :width="30"
         /> -->
-        <UiImage :defaultImage="menu.icon.avatarByDefault" :width="30" />
+        <UiImage :default-image="menu.icon.avatarByDefault" :width="30" />
       </v-avatar>
 
       <v-icon v-else :icon="menu.icon.name" class="on-primary" />
@@ -27,7 +27,7 @@ header principal (configuration, paramètres du compte...)
     <v-menu
       :activator="btn"
       :model-value="isOpened()"
-      @update:modelValue="onStateUpdated"
+      @update:model-value="onStateUpdated"
     >
       <v-card>
         <v-card-title class="theme-neutral text-body-2 font-weight-bold">
@@ -48,8 +48,8 @@ header principal (configuration, paramètres du compte...)
                     size="30"
                   >
                     <UiImage
-                      :imageId="child.icon.avatarId"
-                      :defaultImage="child.icon.avatarByDefault"
+                      :image-id="child.icon.avatarId"
+                      :default-image="child.icon.avatarByDefault"
                       :width="30"
                     />
                   </v-avatar>
@@ -76,10 +76,9 @@ header principal (configuration, paramètres du compte...)
               :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">
+                {{ $t(action.label) }}
+              </v-list-item-title>
             </v-list-item>
           </template>
         </v-card-actions>
@@ -89,8 +88,8 @@ header principal (configuration, paramètres du compte...)
 </template>
 
 <script setup lang="ts">
+import { computed, ref } from 'vue'
 import { useMenu } from '~/composables/layout/useMenu'
-import { computed, ref } from '@vue/reactivity'
 
 const props = defineProps({
   name: {
@@ -111,7 +110,7 @@ const menu = getMenu(props.name)
 const displayMenu = computed(() => hasMenu(props.name))
 const isOpened = () => isMenuOpened(props.name)
 
-const onStateUpdated = (e: any) => {
+const onStateUpdated = (e: boolean) => {
   setMenuState(props.name, e)
 }
 

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

@@ -48,7 +48,7 @@
             </template>
           </v-list-item>
 
-          <v-divider></v-divider>
+          <v-divider />
 
           <!--suppress VueUnrecognizedDirective -->
           <span v-intersect="onLastNotificationIntersect" />

+ 27 - 0
components/Layout/Header/Title.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="d-flex flex-row">
+    <a
+      :href="homeUrl"
+      :title="$t('go_back_home')"
+      class="d-flex flex-row align-center"
+    >
+      <v-img src="/favicon.ico" height="42" width="42" class="mr-2" />
+      <span v-if="mdAndUp"><slot /></span>
+    </a>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useDisplay } from 'vuetify'
+import { useHomeUrl } from '~/composables/utils/useHomeUrl'
+
+const { homeUrl } = useHomeUrl()
+const { mdAndUp } = useDisplay()
+</script>
+
+<style scoped lang="scss">
+a {
+  color: rgb(var(--v-theme-on-primary)) !important;
+  text-decoration: none;
+}
+</style>

+ 10 - 3
components/Layout/Header/UniversalCreation/Card.vue

@@ -16,7 +16,7 @@
     border="solid 1px"
     @click="onClick"
   >
-    <v-row :no-gutters="true" style="height: 100px">
+    <v-row :no-gutters="true" :style="mdAndUp ? 'height: 100px' : ''">
       <v-col cols="3" class="flex-grow-0 flex-shrink-0 d-flex justify-center">
         <v-icon
           :icon="icon"
@@ -24,12 +24,16 @@
           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"
       >
-        <h4 class="text-primary">{{ $t(title) }}</h4>
+        <h4 class="text-primary">
+          {{ $t(title) }}
+        </h4>
+
         <p class="text-neutral-strong">
           {{ $t(textContent) }}
         </p>
@@ -39,7 +43,8 @@
 </template>
 
 <script setup lang="ts">
-import type { PropType } from '@vue/runtime-core'
+import type { PropType } from 'vue'
+import { useDisplay } from 'vuetify'
 import { MENU_LINK_TYPE } from '~/types/enum/layout'
 import { useAdminUrl } from '~/composables/utils/useAdminUrl'
 import UrlUtils from '~/services/utils/urlUtils'
@@ -96,6 +101,8 @@ const emit = defineEmits(['click'])
 
 const { makeAdminUrl } = useAdminUrl()
 
+const { mdAndUp } = useDisplay()
+
 let url: string | null = null
 
 if (props.href !== null) {

+ 4 - 5
components/Layout/Header/UniversalCreation/CreateButton.vue

@@ -49,8 +49,8 @@
       <template #dialogText>
         <LayoutHeaderUniversalCreationGenerateCardsSteps
           :path="path"
-          @cardClick="onCardClick"
-          @urlUpdate="onUrlUpdate"
+          @card-click="onCardClick"
+          @url-update="onUrlUpdate"
         />
       </template>
 
@@ -82,10 +82,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from '@vue/reactivity'
-import type { Ref } from '@vue/reactivity'
+import { ref } from 'vue'
+import type { Ref, ComputedRef } from 'vue'
 import { useDisplay } from 'vuetify'
-import type { ComputedRef } from 'vue'
 import { usePageStore } from '~/stores/page'
 
 const { mdAndDown: asIcon } = useDisplay()

+ 5 - 5
components/Layout/Header/UniversalCreation/EventParams.vue

@@ -15,7 +15,7 @@ Event parameters page in the create dialog
     </v-row>
 
     <v-row v-show="eventStart < now" class="anteriorDateWarning mt-0">
-      <v-col cols="2" class="pt-1"></v-col>
+      <v-col cols="2" class="pt-1" />
       <v-col cols="9" class="pt-1">
         <i class="fa fa-circle-info" />
         {{
@@ -54,11 +54,11 @@ Event parameters page in the create dialog
 </template>
 
 <script setup lang="ts">
-import { ref } from '@vue/reactivity'
-import type { Ref } from '@vue/reactivity'
+import { ref } from 'vue'
+import type { Ref, ComputedRef } from 'vue'
 import { add, format, startOfHour, formatISO } from 'date-fns'
-import type { ComputedRef } from 'vue'
-import DateUtils, { supportedLocales } from '~/services/utils/dateUtils'
+import type { supportedLocales } from '~/services/utils/dateUtils'
+import DateUtils from '~/services/utils/dateUtils'
 
 const i18n = useI18n()
 

+ 33 - 25
components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue

@@ -11,39 +11,41 @@
         <v-container v-if="location === 'home'">
           <v-row>
             <!-- Une personne -->
-            <v-col cols="6" v-if="ability.can('manage', 'users')">
+            <v-col v-if="ability.can('manage', 'users')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
-                @click="onCardClick('access')"
                 title="a_person"
                 text-content="add_new_person_student"
                 icon="fa fa-user"
+                @click="onCardClick('access')"
               />
             </v-col>
             <v-col
-              cols="6"
               v-if="
                 ability.can('display', 'agenda_page') &&
                 (ability.can('display', 'course_page') ||
                   ability.can('display', 'exam_page') ||
                   ability.can('display', 'pedagogics_project_page'))
               "
+              cols="12"
+              md="6"
             >
               <!-- Un évènement -->
               <LayoutHeaderUniversalCreationCard
-                @click="onCardClick('event')"
                 title="an_event"
                 text-content="add_an_event_course"
                 icon="fa fa-calendar"
+                @click="onCardClick('event')"
               />
             </v-col>
 
             <!-- Autre évènement -->
             <v-col
-              cols="6"
               v-else-if="
                 ability.can('display', 'agenda_page') &&
                 ability.can('manage', 'events')
               "
+              cols="12"
+              md="6"
             >
               <LayoutHeaderUniversalCreationCard
                 to="event-params"
@@ -57,24 +59,25 @@
 
             <!-- 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'))
               "
+              cols="12"
+              md="6"
             >
               <LayoutHeaderUniversalCreationCard
-                @click="onCardClick('message')"
                 title="a_correspondence"
                 text-content="send_email_letter"
                 icon="fa fa-envelope"
+                @click="onCardClick('message')"
               />
             </v-col>
 
             <!-- Un matériel (direct link) -->
-            <v-col cols="6" v-if="ability.can('manage', 'equipments')">
+            <v-col v-if="ability.can('manage', 'equipments')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_materiel"
                 text-content="add_any_type_material"
@@ -92,7 +95,7 @@
         <v-container v-if="location === 'access'">
           <v-row>
             <!-- Un adhérent -->
-            <v-col cols="6" v-if="isLaw1901">
+            <v-col v-if="isLaw1901" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="an_adherent"
                 text-content="adherent_text_creation_card"
@@ -103,7 +106,7 @@
             </v-col>
 
             <!-- Un membre du CA -->
-            <v-col cols="6" v-if="isLaw1901">
+            <v-col v-if="isLaw1901" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_ca_member"
                 text-content="ca_member_text_creation_card"
@@ -114,7 +117,7 @@
             </v-col>
 
             <!-- Un élève -->
-            <v-col cols="6">
+            <v-col cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_student"
                 text-content="student_text_creation_card"
@@ -125,7 +128,7 @@
             </v-col>
 
             <!-- Un tuteur -->
-            <v-col cols="6">
+            <v-col cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_guardian"
                 text-content="guardian_text_creation_card"
@@ -136,7 +139,7 @@
             </v-col>
 
             <!-- Un professeur -->
-            <v-col cols="6">
+            <v-col cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_teacher"
                 text-content="teacher_text_creation_card"
@@ -147,7 +150,7 @@
             </v-col>
 
             <!-- Un membre du personnel -->
-            <v-col cols="6">
+            <v-col cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_member_of_staff"
                 text-content="personnel_text_creation_card"
@@ -158,7 +161,7 @@
             </v-col>
 
             <!-- Une entité légale -->
-            <v-col cols="6">
+            <v-col cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_legal_entity"
                 text-content="moral_text_creation_card"
@@ -169,7 +172,7 @@
             </v-col>
 
             <!-- Une inscription en ligne -->
-            <v-col cols="6" v-if="hasOnlineRegistrationModule">
+            <v-col v-if="hasOnlineRegistrationModule" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="online_registration"
                 text-content="online_registration_text_creation_card"
@@ -180,7 +183,7 @@
             </v-col>
 
             <!-- Un autre type de contact -->
-            <v-col cols="6">
+            <v-col cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="another_type_of_contact"
                 text-content="other_contact_text_creation_card"
@@ -196,7 +199,11 @@
         <v-container v-if="location === 'event'">
           <v-row>
             <!-- Un cours -->
-            <v-col cols="6" v-if="ability.can('display', 'course_page')">
+            <v-col
+              v-if="ability.can('display', 'course_page')"
+              cols="12"
+              md="6"
+            >
               <LayoutHeaderUniversalCreationCard
                 title="course"
                 text-content="course_text_creation_card"
@@ -207,7 +214,7 @@
             </v-col>
 
             <!-- Un examen -->
-            <v-col cols="6" v-if="ability.can('display', 'exam_page')">
+            <v-col v-if="ability.can('display', 'exam_page')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="exam"
                 text-content="exam_text_creation_card"
@@ -219,8 +226,9 @@
 
             <!-- Un projet pédagogique -->
             <v-col
-              cols="6"
               v-if="ability.can('display', 'pedagogics_project_page')"
+              cols="12"
+              md="6"
             >
               <LayoutHeaderUniversalCreationCard
                 title="educational_services"
@@ -232,7 +240,7 @@
             </v-col>
 
             <!-- Un autre évènement -->
-            <v-col cols="6" v-if="ability.can('manage', 'events')">
+            <v-col v-if="ability.can('manage', 'events')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 to="event-params"
                 href="/calendar/create/events"
@@ -249,7 +257,7 @@
         <v-container v-if="location === 'message'">
           <v-row>
             <!-- Un email -->
-            <v-col cols="6" v-if="ability.can('manage', 'emails')">
+            <v-col v-if="ability.can('manage', 'emails')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="an_email"
                 text-content="email_text_creation_card"
@@ -260,7 +268,7 @@
             </v-col>
 
             <!-- Un courrier -->
-            <v-col cols="6" v-if="ability.can('manage', 'mails')">
+            <v-col v-if="ability.can('manage', 'mails')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_letter"
                 text-content="letter_text_creation_card"
@@ -271,7 +279,7 @@
             </v-col>
 
             <!-- Un SMS -->
-            <v-col cols="6" v-if="ability.can('manage', 'texto')">
+            <v-col v-if="ability.can('manage', 'texto')" cols="12" md="6">
               <LayoutHeaderUniversalCreationCard
                 title="a_sms"
                 text-content="sms_text_creation_card"
@@ -296,8 +304,8 @@
 </template>
 <script setup lang="ts">
 import { ref, computed } from 'vue'
-import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import { useAbility } from '@casl/vue'
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 const props = defineProps({
   /**

+ 21 - 14
components/Layout/MainMenu.vue

@@ -18,6 +18,14 @@ Prend en paramètre une liste de ItemMenu et les met en forme
     </template>
 
     <v-list open-strategy="single" active-class="active" class="left-menu">
+      <v-list-item
+        v-if="smAndDown"
+        prepend-icon="fas fa-home"
+        :title="$t('homepage')"
+        class="theme-secondary menu-item"
+        height="48px"
+      />
+
       <!-- 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) -->
@@ -37,8 +45,8 @@ Prend en paramètre une liste de ItemMenu et les met en forme
         <!-- Cas 2 : l'item a des enfants, c'est un groupe -->
         <v-list-group
           v-else
-          expand-icon="fas fa-angle-down"
-          collapse-icon="fas fa-angle-up"
+          expand-icon="fas fa-angle-right"
+          collapse-icon="fas fa-angle-down"
         >
           <template #activator="{ props }">
             <v-list-item
@@ -52,10 +60,10 @@ Prend en paramètre une liste de ItemMenu et les met en forme
           </template>
 
           <v-list-item
-            v-for="child in item.children"
-            :key="child.label"
-            :title="$t(child.label)"
+            v-for="(child, index) in item.children"
             :id="'main-menu-item' + item.label + '-' + child.label"
+            :key="index"
+            :title="$t(child.label)"
             :prepend-icon="child.icon.name"
             :href="!isInternalLink(child) ? child.to : undefined"
             :to="isInternalLink(child) ? child.to : undefined"
@@ -74,9 +82,9 @@ Prend en paramètre une liste de ItemMenu et les met en forme
 </template>
 
 <script setup lang="ts">
-import { useMenu } from '~/composables/layout/useMenu'
-import { computed } from '@vue/reactivity'
+import { computed } from 'vue'
 import { useDisplay } from 'vuetify'
+import { useMenu } from '~/composables/layout/useMenu'
 import type { MenuGroup, MenuItem } from '~/types/layout'
 import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
 
@@ -85,9 +93,9 @@ const organizationProfile = useOrganizationProfileStore()
 const { getMenu, hasMenu, isInternalLink, setMenuState, isMenuOpened } =
   useMenu()
 
-const { mdAndUp, lgAndUp } = useDisplay()
+const { smAndDown, mdAndUp, lgAndUp } = useDisplay()
 
-const menu = getMenu('Main')
+const menu: MenuGroup | null = getMenu('Main')
 
 // En vue lg+, on affiche toujours le menu
 const displayMenu = computed(() => {
@@ -111,7 +119,7 @@ const isRail = computed(() => {
 
 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é.
-  if (process.client && menu !== null) {
+  if (import.meta.client && menu !== null) {
     setMenuState('Main', lgAndUp.value)
   }
 })
@@ -131,7 +139,7 @@ function getItems(
 
   if (menu === null) {
     items = []
-  } else if (menu.hasOwnProperty('children')) {
+  } else if (Object.prototype.hasOwnProperty.call(menu, 'children')) {
     items = (menu as MenuGroup).children ?? []
   } else {
     items = [menu]
@@ -152,9 +160,8 @@ function getItems(
   color: rgb(var(--v-theme-on-secondary));
 }
 
-.v-list-item__prepend {
-  margin: 10px 0;
-  margin-right: 10px !important;
+:deep(.v-list-item__prepend > .v-icon ~ .v-list-item__spacer) {
+  width: 12px;
 }
 
 .v-application--is-ltr .v-list-group--no-action > .v-list-group__header {

+ 2 - 0
components/Layout/Pages/Subscription/Card.vue

@@ -46,10 +46,12 @@ const props = defineProps({
   subTitle: {
     type: String,
     required: false,
+    default: '',
   },
   extraHeader: {
     type: String,
     required: false,
+    default: '',
   },
   color: {
     type: String,

+ 3 - 3
components/Layout/Pages/Subscription/List.vue

@@ -1,19 +1,19 @@
 <template>
   <ul>
-    <li v-for="li in elements">
+    <li v-for="(li, index) in elements" :key="index">
       <v-icon
         class="check"
         :color="color"
         icon="fa-solid fa-check"
         size="large"
-      ></v-icon>
+      />
       <span class="pl-2">{{ li }}</span>
     </li>
   </ul>
 </template>
 
 <script setup lang="ts">
-const props = defineProps({
+defineProps({
   elements: {
     type: Array,
     required: true,

+ 13 - 6
components/Layout/Parameters/EntityTable.vue

@@ -7,18 +7,19 @@ A data table for the parameters page
     <div v-else>
       <LayoutParametersTable
         :items="items"
+        :title="title"
         :columns-definitions="columns"
         :actions="actions"
         :actions-route="actionsRoute"
-        @editClicked="onEditClicked"
-        @deleteClicked="onDeleteClicked"
-        @addClicked="goToCreatePage"
+        @edit-clicked="onEditClicked"
+        @delete-clicked="onDeleteClicked"
+        @add-clicked="goToCreatePage"
       />
 
       <UiFormDeletionConfirmationDialog
         v-model="showDeletionConfirmationDialog"
-        @deleteClicked="onDeleteConfirmed"
-        @cancelClicked="onCancelClicked"
+        @delete-clicked="onDeleteConfirmed"
+        @cancel-clicked="onCancelClicked"
       />
     </div>
   </div>
@@ -42,6 +43,12 @@ const props = defineProps({
     type: Object as PropType<typeof ApiResource>,
     required: true,
   },
+  /** Titre du tableau */
+  title: {
+    type: String,
+    required: false,
+    default: null,
+  },
   /**
    * If provided, define the columns to show.
    * Else, all the entity's props are shown.
@@ -62,7 +69,7 @@ const props = defineProps({
   actions: {
     type: Array as PropType<Array<TABLE_ACTION>>,
     required: false,
-    default: [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD],
+    default: () => [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD],
   },
   /**
    * The base URL for the edit / create pages

+ 65 - 38
components/Layout/ParametersMenu.vue → components/Layout/Parameters/Menu.vue

@@ -1,16 +1,24 @@
 <template>
   <v-navigation-drawer
-    v-if="displayMenu"
-    v-model="isOpened"
+    v-model="displayMenu"
+    :rail="isRail"
+    :disable-resize-watcher="true"
     mobile-breakpoint="sm"
-    style="z-index: 1005"
+    class="parameters-menu theme-neutral-very-soft"
   >
     <!--
     Le z-index est précisé pour éviter cette erreur : https://github.com/vuetifyjs/nuxt-module/issues/205
     Il pourra être retiré dès que le bug aura été corrigé
     -->
     <template #prepend>
-      <div class="title">
+      <div v-if="!isRail" class="title">
+        <v-btn
+          flat
+          :title="$t('go_back_home')"
+          :href="homeUrl"
+          icon="fa fa-arrow-left"
+          class="mr-1"
+        />
         <h3>{{ $t('parameters') }}</h3>
       </div>
     </template>
@@ -22,21 +30,9 @@
         :title="$t(item.label)"
         :prepend-icon="item.icon ? item.icon.name : ''"
         :to="(item as MenuItem).to"
-      >
-      </v-list-item>
+        @click="onItemClicked"
+      />
     </v-list>
-
-    <template #append>
-      <v-btn
-        :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>
-    </template>
   </v-navigation-drawer>
 </template>
 
@@ -47,21 +43,36 @@ import { useMenu } from '~/composables/layout/useMenu'
 import { useHomeUrl } from '~/composables/utils/useHomeUrl'
 import type { MenuGroup, MenuItem } from '~/types/layout'
 
-const { mdAndUp } = useDisplay()
+const { mdAndUp, lgAndUp } = useDisplay()
 
-const { getMenu, hasMenu, isMenuOpened, setMenuState } = useMenu()
+const { getMenu, hasMenu, isMenuOpened, setMenuState, closeMenu } = useMenu()
 
 const menu: MenuGroup | null = getMenu('Parameters')
 
+const isOpened = computed(() => isMenuOpened('Parameters'))
+
+// En vue lg+, on affiche toujours le menu
 const displayMenu = computed(() => {
-  return menu !== null && hasMenu('Parameters')
+  return (
+    menu !== null && hasMenu('Parameters') && (lgAndUp.value || isOpened.value)
+  )
 })
 
-const isOpened = computed(() => isMenuOpened('Parameters'))
+// 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
+})
+
+const onItemClicked = () => {
+  if (!lgAndUp.value) {
+    closeMenu('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) {
+  if (import.meta.client && menu !== null) {
     setMenuState('Parameters', mdAndUp.value)
   }
 })
@@ -71,11 +82,34 @@ const { homeUrl } = useHomeUrl()
 onUnmounted(() => {
   unwatch()
 })
+
+// TODO voir à factoriser avec LayoutMainMenu
 </script>
 
 <style scoped lang="scss">
+.parameters-menu {
+  position: relative;
+  overflow: hidden;
+}
+
+.parameters-menu::before {
+  content: '\f013';
+  font-family: 'Font Awesome 6 Free', serif;
+  font-weight: 900;
+  font-size: 300px;
+  color: rgb(var(--v-theme-neutral-soft));
+  position: absolute;
+  top: 90%;
+  left: 12%;
+  transform: translate(-50%, -50%);
+  pointer-events: none;
+  user-select: none;
+}
+
 .title {
   display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
   align-items: center;
   height: 48px;
   vertical-align: center;
@@ -116,25 +150,18 @@ onUnmounted(() => {
 :deep(.v-list-item__prepend) {
   margin: 10px 0;
   margin-right: 10px !important;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  width: 32px;
+
+  .v-list-item__spacer {
+    display: none;
+  }
 }
 
 :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>

+ 19 - 0
components/Layout/Parameters/Section.vue

@@ -0,0 +1,19 @@
+<template>
+  <v-card class="parameters-page-card">
+    <slot />
+  </v-card>
+</template>
+
+<style scoped lang="scss">
+.parameters-page-card {
+  background-color: rgb(var(--v-theme-neutral-very-soft));
+  color: rgb(var(--v-theme-on-neutral-very-soft));
+  padding: 24px;
+  margin: 28px auto;
+  max-width: 1200px;
+
+  @media (max-width: 600px) {
+    max-width: 95%;
+  }
+}
+</style>

+ 97 - 31
components/Layout/Parameters/Table.vue

@@ -3,54 +3,88 @@ A data table for the parameters page
 -->
 <template>
   <div class="container">
+    <div class="d-flex flex-row mb-2">
+      <h4 v-if="title" class="align-self-center">
+        {{ title }}
+      </h4>
+    </div>
+
     <v-table>
       <thead>
         <tr>
-          <td v-for="col in columns">
+          <td v-for="(col, index) in columns" :key="index">
             {{ col.label }}
           </td>
-          <td>{{ i18n.t('actions') }}</td>
+          <td>{{ $t('actions') }}</td>
         </tr>
       </thead>
-      <tbody v-if="items">
+      <tbody v-if="items.length > 0">
         <tr v-for="(item, i) in items" :key="i">
-          <td v-for="col in columnsDefinitions" class="cycle-editable-cell">
+          <td
+            v-for="(col, index) in columnsDefinitions"
+            :key="index"
+            class="cycle-editable-cell"
+          >
             {{ item[col.property] }}
           </td>
 
-          <td class="d-flex flex-row actions-cell">
-            <slot name="actions" :item="item">
-              <v-btn
-                v-if="actions.includes(TABLE_ACTION.EDIT)"
-                :flat="true"
-                icon="fa fa-pen"
-                class="mr-3"
-                @click="emit('editClicked', item)"
-              />
-              <v-btn
-                v-if="actions.includes(TABLE_ACTION.DELETE)"
-                :flat="true"
-                icon="fas fa-trash"
-                @click="emit('deleteClicked', item)"
-              />
-            </slot>
+          <td class="d-flex flex-row justify-center actions-cell">
+            <v-menu min-width="120" location="end" class="action-menu">
+              <template #activator="{ props: menuProps }">
+                <v-btn
+                  v-if="
+                    actions.includes(TABLE_ACTION.EDIT) ||
+                    actions.includes(TABLE_ACTION.DELETE)
+                  "
+                  v-bind="menuProps"
+                  :flat="true"
+                  icon="fas fa-ellipsis-vertical"
+                />
+              </template>
+
+              <v-list>
+                <v-list-item
+                  v-if="actions.includes(TABLE_ACTION.EDIT)"
+                  @click="emit('editClicked', item)"
+                >
+                  <v-list-item-title>
+                    <v-icon>fas fa-pen</v-icon>
+                    {{ $t('edit') }}
+                  </v-list-item-title>
+                </v-list-item>
+
+                <v-list-item
+                  v-if="actions.includes(TABLE_ACTION.DELETE)"
+                  class="theme-danger"
+                  @click="emit('deleteClicked', item)"
+                >
+                  <v-list-item-title icon="fas fa-trash">
+                    <v-icon>fas fa-trash</v-icon>
+                    {{ $t('delete') }}
+                  </v-list-item-title>
+                </v-list-item>
+              </v-list>
+            </v-menu>
           </td>
         </tr>
       </tbody>
       <tbody v-else>
-        <tr class="theme-neutral">
+        <tr>
           <td>
             <i>{{ i18n.t('nothing_to_show') }}</i>
           </td>
-          <td></td>
+          <td />
         </tr>
       </tbody>
     </v-table>
-    <div class="d-flex justify-end" v-if="actions.includes(TABLE_ACTION.ADD)">
+
+    <div
+      v-if="actions.includes(TABLE_ACTION.ADD)"
+      class="d-flex justify-center my-3"
+    >
       <v-btn
-        :flat="true"
         prepend-icon="fa fa-plus"
-        class="theme-primary mt-4"
+        class="theme-neutral"
         @click="emit('addClicked')"
       >
         {{ i18n.t('add') }}
@@ -60,8 +94,8 @@ A data table for the parameters page
 </template>
 
 <script setup lang="ts">
+import { useDisplay } from 'vuetify'
 import { TABLE_ACTION } from '~/types/enum/enums'
-import UrlUtils from '~/services/utils/urlUtils'
 import type { ColumnDefinition } from '~/types/interfaces'
 
 const props = defineProps({
@@ -72,6 +106,12 @@ const props = defineProps({
     type: Array as PropType<Array<object>>,
     required: true,
   },
+  /** Titre du tableau */
+  title: {
+    type: String,
+    required: false,
+    default: null,
+  },
   /**
    * If provided, define the columns to show.
    * Else, all the entity's props are shown.
@@ -100,7 +140,7 @@ const props = defineProps({
   actions: {
     type: Array as PropType<Array<TABLE_ACTION>>,
     required: false,
-    default: [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD],
+    default: () => [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD],
   },
   /**
    * The URL for the edit / create pages
@@ -120,6 +160,8 @@ const i18n = useI18n()
 
 const emit = defineEmits(['editClicked', 'deleteClicked', 'addClicked'])
 
+const { smAndUp, xs } = useDisplay()
+
 const getId = (item: object) => {
   return item[props.identifier]
 }
@@ -136,12 +178,13 @@ const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
 
 <style scoped lang="scss">
 .container {
-  max-width: 1000px;
+  //max-width: 1000px;
+  //margin: 0 auto;
+  display: inline-block;
+  min-width: 65%;
 }
 
 .v-table {
-  width: 100%;
-
   thead {
     color: rgb(var(--v-theme-neutral-strong));
     font-weight: 600;
@@ -156,6 +199,13 @@ const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
     }
   }
 
+  :deep(tr:hover) {
+    .fa-ellipsis-vertical {
+      color: rgb(var(--v-theme-on-neutral-soft));
+      font-size: 20px;
+    }
+  }
+
   th,
   td {
     padding: 10px;
@@ -167,8 +217,24 @@ const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
   }
 }
 
+.action-menu {
+  margin: 0 auto;
+
+  .v-list {
+    top: 26px;
+    left: 2px;
+    padding: 0;
+  }
+
+  .v-icon {
+    opacity: 0.7;
+    font-size: 16px;
+    margin-right: 12px;
+  }
+}
+
 :deep(.actions-cell .v-icon) {
-  color: rgb(var(--v-theme-neutral-strong));
+  color: rgb(var(--v-theme-on-neutral));
   font-size: 18px;
 }
 </style>

+ 86 - 0
components/Layout/Parameters/Website/ActivationSwitch.vue

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <v-switch
+      :model-value="modelValue"
+      :label="$t('activateOpentalentSiteWeb')"
+      inset
+      :color="color"
+      :base-color="color"
+      false-icon="fas fa-xmark"
+      true-icon="fas fa-check"
+      hide-details
+      @update:model-value="onUpdate"
+    />
+
+    <LazyLayoutDialog :show="showWebsiteDeactivationDialog" theme="warning">
+      <template #dialogTitle>
+        {{ $t('please_confirm') }}
+      </template>
+      <template #dialogText>
+        <v-col>
+          <div>
+            {{
+              $t('yourOpentalentWebsiteWillBeDeactivatedOnceYouLlHaveSaved')
+            }}.
+          </div>
+          <span>{{ $t('doYouWantToContinue') }} ?</span>
+        </v-col>
+      </template>
+      <template #dialogBtn>
+        <v-btn class="theme-neutral-soft mr-4" @click="onDialogNoBtnClick">
+          {{ $t('cancel') }}
+        </v-btn>
+
+        <v-btn class="theme-primary" @click="onDialogYesBtnClick">
+          {{ $t('yes') }}
+        </v-btn>
+      </template>
+    </LazyLayoutDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useTheme } from 'vuetify'
+import type { Ref } from 'vue'
+
+const theme = useTheme()
+
+const emit = defineEmits(['update:modelValue'])
+
+const i18n = useI18n()
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    required: true,
+  },
+})
+
+const color = computed(() =>
+  props.modelValue
+    ? theme.current.value.colors.success
+    : theme.current.value.colors.danger,
+)
+
+const showWebsiteDeactivationDialog: Ref<boolean> = ref(false)
+
+const onUpdate = (value: boolean) => {
+  if (!value) {
+    showWebsiteDeactivationDialog.value = true
+  } else {
+    emit('update:modelValue', value)
+  }
+}
+
+const onDialogYesBtnClick = () => {
+  showWebsiteDeactivationDialog.value = false
+  emit('update:modelValue', false)
+}
+
+const onDialogNoBtnClick = () => {
+  showWebsiteDeactivationDialog.value = false
+  emit('update:modelValue', true)
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 5 - 5
components/Layout/SubHeader/ActivityYear.vue

@@ -29,12 +29,12 @@
 </template>
 
 <script setup lang="ts">
+import { useDisplay } from 'vuetify'
 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'
 
@@ -63,16 +63,16 @@ const label: string = organizationProfileStore.isSchool
 const setActivityYear = async (event: string) => {
   const activityYear = parseInt(event)
 
-  if (!(1900 < activityYear) || !(activityYear <= 2100)) {
+  if (!(activityYear > 1900) || !(activityYear <= 2100)) {
     throw new Error("Error: 'year' shall be a valid year")
   }
   formStore.setDirty(false)
 
   pageStore.loading = true
   await em.patch(Access, accessProfileStore.currentAccessId, {
-    activityYear: activityYear,
+    activityYear,
   })
-  if (process.server) {
+  if (import.meta.server) {
     // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }
@@ -93,7 +93,7 @@ const setActivityYear = async (event: string) => {
   input {
     font-size: 14px;
     width: 55px !important;
-    padding: 0 !important;
+    padding: 0 2px 0 6px !important;
     margin-top: 0 !important;
     min-height: 24px;
     height: 24px;

+ 5 - 8
components/Layout/SubHeader/Breadcrumbs.vue

@@ -3,10 +3,10 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from '@vue/reactivity'
-import type { ComputedRef } from '@vue/reactivity'
-import type { AnyJson } from '~/types/data'
+import { computed } from 'vue'
+import type { ComputedRef } from 'vue'
 import { useI18n } from 'vue-i18n'
+import type { AnyJson } from '~/types/data'
 import UrlUtils from '~/services/utils/urlUtils'
 
 const runtimeConfig = useRuntimeConfig()
@@ -16,8 +16,7 @@ const router = useRouter()
 const items: ComputedRef<Array<AnyJson>> = computed(() => {
   const crumbs: Array<AnyJson> = []
   const baseUrl =
-    runtimeConfig.baseUrlAdminLegacy ??
-    runtimeConfig.public.baseUrlAdminLegacy
+    runtimeConfig.baseUrlAdminLegacy ?? runtimeConfig.public.baseUrlAdminLegacy
 
   crumbs.push({
     title: i18n.t('welcome'),
@@ -31,9 +30,7 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
   pathPart.forEach((part) => {
     path = UrlUtils.join(path, part)
 
-    let match
-
-    match = router.resolve(path)
+    const match = router.resolve(path)
     if (match.name) {
       crumbs.push({
         title: !parseInt(part, 10)

+ 7 - 6
components/Layout/SubHeader/DataTiming.vue

@@ -14,10 +14,11 @@
       border
       :rounded="true"
       class="toggle-btn"
-      @update:modelValue="onUpdate"
+      @update:model-value="onUpdate"
     >
       <v-btn
-        v-for="choice in historicalChoices"
+        v-for="(choice, index) in historicalChoices"
+        :key="index"
         :value="choice"
         max-height="25"
         :class="
@@ -36,17 +37,17 @@
 </template>
 
 <script setup lang="ts">
+import type { Ref } from 'vue'
+import { useDisplay, useTheme } from 'vuetify'
 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']
+const color = useTheme().current.value.colors.primary
 
 const { setDirty } = useFormStore()
 const accessProfileStore = useAccessProfileStore()
@@ -84,7 +85,7 @@ const onUpdate = async (newValue: Array<string>) => {
   await em.patch(Access, accessId, {
     historical: accessProfileStore.historical,
   })
-  if (process.server) {
+  if (import.meta.server) {
     // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }

+ 10 - 4
components/Layout/SubHeader/DataTimingRange.vue

@@ -5,9 +5,11 @@
         {{ $t('period_choose') }}
       </span>
 
-      <UiDateRangePicker
+      <UiDatePicker
         :model-value="datesRange"
         :max-height="28"
+        :range="true"
+        :auto-apply="false"
         @update:model-value="updateDateTimeRange"
       />
     </div>
@@ -15,7 +17,7 @@
 </template>
 
 <script setup lang="ts">
-import type { Ref } from '@vue/reactivity'
+import type { Ref } from 'vue'
 import { useAccessProfileStore } from '~/stores/accessProfile'
 import { useFormStore } from '~/stores/form'
 import { useEntityManager } from '~/composables/data/useEntityManager'
@@ -37,7 +39,7 @@ 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 updateDateTimeRange = async (dates: Array<Date>): Promise<void> => {
   const accessId = accessProfileStore.currentAccessId
 
   datesRange.value = dates
@@ -60,7 +62,7 @@ const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
   await em.patch(Access, accessId, {
     historical: accessProfileStore.historical,
   })
-  if (process.server) {
+  if (import.meta.server) {
     // Force profile refresh server side to avoid a bug where server and client stores diverge on profile refresh
     await refreshProfile()
   }
@@ -73,4 +75,8 @@ const updateDateTimeRange = async (dates: Array<Date>): Promise<any> => {
 .label {
   min-width: 150px;
 }
+
+:deep(.dp__input) {
+  max-height: 28px;
+}
 </style>

+ 6 - 6
components/Layout/SubHeader/PersonnalizedList.vue

@@ -1,6 +1,6 @@
 <template>
   <main>
-    <a ref="btn" id="activator">
+    <a id="activator" ref="btn">
       {{ $t('my_list') }}
     </a>
 
@@ -51,12 +51,12 @@
 </template>
 
 <script setup lang="ts">
+import { ref } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
 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 type ApiResource from '~/models/ApiResource'
 import UrlUtils from '~/services/utils/urlUtils'
 
 const btn: Ref = ref(null)
@@ -84,8 +84,8 @@ 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
+      item.label.toLowerCase().includes(search.value.toLowerCase()) ||
+      item.menuKey.toLowerCase().includes(search.value.toLowerCase())
     )
   })
 })

+ 5 - 4
components/Layout/Subheader.vue

@@ -68,11 +68,11 @@ 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 { computed, ref } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
 import { useDisplay } from 'vuetify'
+import { useMenu } from '~/composables/layout/useMenu'
+import { useAccessProfileStore } from '~/stores/accessProfile'
 
 const { smAndUp, lgAndUp } = useDisplay()
 const accessProfile = useAccessProfileStore()
@@ -101,6 +101,7 @@ main {
 }
 
 :deep(#subheader .v-card) {
+  min-height: 36px;
   max-height: 36px;
   background-color: transparent !important;
 }

+ 2 - 2
components/Layout/UpgradePremiumButton.vue

@@ -13,15 +13,15 @@
 
     <LayoutDialogTrialAlreadyDid
       :show="showDialog"
-      @closeDialog="showDialog = false"
+      @close-dialog="showDialog = false"
     />
   </div>
 </template>
 
 <script setup lang="ts">
+import { computed } from 'vue'
 import UrlUtils from '~/services/utils/urlUtils'
 import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
-import { computed } from '@vue/reactivity'
 
 const runtimeConfig = useRuntimeConfig()
 const organizationProfile = useOrganizationProfileStore()

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

@@ -17,7 +17,7 @@ Bouton Delete avec modale de confirmation de la suppression
 
 <script setup lang="ts">
 import type { Ref, PropType } from 'vue'
-import ApiResource from '~/models/ApiResource'
+import type ApiResource from '~/models/ApiResource'
 import { useDeleteItem } from '~/composables/form/useDeleteItem'
 
 const props = defineProps({

+ 25 - 15
components/Ui/Button/Submit.vue

@@ -1,24 +1,24 @@
 <template>
   <v-btn
-    class="mr-4 theme-primary"
-    :class="hasOtherActions ? 'pr-0' : ''"
-    @click="submitAction(mainAction)"
     ref="mainBtn"
+    class="submit-btn theme-primary"
+    :class="hasOtherActions ? 'pr-0' : ''"
     :disabled="validationPending"
+    @click="submitAction(mainAction)"
   >
     {{ $t(mainAction) }}
 
-    <v-divider class="ml-3" :vertical="true" v-if="hasOtherActions"></v-divider>
+    <v-divider v-if="hasOtherActions" class="ml-3" :vertical="true" />
 
     <v-menu
+      v-if="hasOtherActions"
       :top="dropDirection === 'top'"
       offset-y
       left
-      v-if="hasOtherActions"
       :nudge-top="dropDirection === 'top' ? 6 : 0"
       :nudge-bottom="dropDirection === 'bottom' ? 6 : 0"
     >
-      <template #activator="{ on, attrs }">
+      <template #activator="{ on }">
         <v-toolbar-title v-on="on">
           <v-icon class="pl-3 pr-3">
             {{
@@ -29,16 +29,14 @@
       </template>
       <v-list :min-width="menuSize">
         <v-list-item
-          dense
-          v-for="(action, index) in actions"
+          v-for="(action, index) in otherActions"
           :key="index"
+          dense
           class="subAction"
-          v-if="index > 0"
         >
-          <v-list-item-title
-            v-text="$t(action)"
-            @click="submitAction(action)"
-          />
+          <v-list-item-title @click="submitAction(action)">
+            {{ $t(action) }}
+          </v-list-item-title>
         </v-list-item>
       </v-list>
     </v-menu>
@@ -46,8 +44,8 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from '@vue/reactivity'
-import type { ComputedRef, Ref } from '@vue/reactivity'
+import { computed, ref } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
 
 const props = defineProps({
   actions: {
@@ -85,9 +83,21 @@ const mainAction: ComputedRef<string> = computed(() => {
 const hasOtherActions: ComputedRef<boolean> = computed(() => {
   return props.actions.length > 1
 })
+
+const otherActions: ComputedRef<Array<string>> = computed(() => {
+  return props.actions.filter((_, index) => index > 0) as Array<string>
+})
 </script>
 
 <style scoped>
+.submit-btn {
+  margin-right: 12px;
+
+  @media (max-width: 600px) {
+    margin: 0;
+  }
+}
+
 .v-list-item--dense {
   min-height: 25px;
 }

+ 5 - 3
components/Ui/Collection.vue

@@ -5,7 +5,7 @@
     <v-skeleton-loader v-if="pending" :type="loaderType" />
     <div v-else>
       <!-- Content -->
-      <slot name="list.item" v-bind="{ collection.items }" />
+      <slot name="list.item" v-bind="{ items: collection?.items }" />
 
       <!-- New button -->
       <v-btn v-if="newLink" class="theme-primary float-right">
@@ -20,8 +20,8 @@
 </template>
 
 <script setup lang="ts">
-import { computed, toRefs } from '@vue/reactivity'
-import type { ComputedRef, ToRefs } from '@vue/reactivity'
+import { computed, toRefs } from 'vue'
+import type { ComputedRef, ToRefs } from 'vue'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import type { Collection } from '~/types/data'
 
@@ -33,6 +33,7 @@ const props = defineProps({
   parent: {
     type: Object,
     required: false,
+    default: () => null,
   },
   loaderType: {
     type: String,
@@ -42,6 +43,7 @@ const props = defineProps({
   newLink: {
     type: String,
     required: false,
+    default: null,
   },
 })
 

+ 17 - 7
components/Ui/DataTable.vue

@@ -13,9 +13,13 @@ Tableau interactif conçu pour l'affichage d'une collection d'entités
       :loading="$fetchState.pending"
       class="elevation-1"
     >
-      <template v-for="header in headersWithItem" #[header.item]="props">
-        <slot :name="header.item" v-bind="props">
-          {{ props.item[header.value] }}
+      <template
+        v-for="(header, index) in headersWithItem"
+        :key="index"
+        #[header.item]="slotProps"
+      >
+        <slot :name="header.item" v-bind="slotProps">
+          {{ slotProps.item[header.value] }}
         </slot>
       </template>
 
@@ -28,10 +32,10 @@ Tableau interactif conçu pour l'affichage d'une collection d'entités
 </template>
 
 <script setup lang="ts">
-import { ref, toRefs } from '@vue/reactivity'
-import type { Ref } from '@vue/reactivity'
+import { ref, toRefs } from 'vue'
+import type { Ref } from 'vue'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import ApiResource from '~/models/ApiResource'
+import type ApiResource from '~/models/ApiResource'
 import type { AnyJson } from '~/types/data'
 
 const props = defineProps({
@@ -51,8 +55,14 @@ const props = defineProps({
 
 const { parent, model, headers } = toRefs(props)
 
+interface TableHeader {
+  value: string
+  item?: string
+  [key: string]: string | undefined
+}
+
 const headersWithItem = computed(() => {
-  return headers.value.map((header: any) => {
+  return headers.value.map((header: TableHeader) => {
     header.item = 'item.' + header.value
     return header
   })

+ 70 - 94
components/Ui/DatePicker.vue

@@ -1,119 +1,95 @@
 <!--
-Sélecteur de dates avec Vuetify
+Sélecteur de dates
 
-@see https://vuetifyjs.com/en/components/date-pickers/
+@see https://vue3datepicker.com/
 -->
 
 <template>
-  <v-layout row wrap>
-    <!--
-    TODO: remplacer par <v-date-input> quand celui ci ne sera plus expérimental
-    (@see https://vuetifyjs.com/en/components/date-inputs)
-    -->
-    <v-menu
-      v-model="menu"
-      :close-on-content-click="false"
-      :nudge-right="40"
-      lazy
-      transition="scale-transition"
-      offset-y
-      :max-width="290"
-      :min-width="290"
-      :position-x="positionX"
-      :position-y="positionY"
-    >
-      <template #activator="{ props: attrs }">
-        <v-text-field
-          v-model="displayDate"
-          :label="label"
-          :readonly="true"
-          v-bind="attrs"
-          prepend-inner-icon="far fa-calendar"
-          variant="outlined"
-          density="compact"
-        />
-      </template>
-
-      <v-date-picker
-        :model-value="modelValue"
-        :locale="i18n.locale.value"
-        no-title
-        scrollable
-        @update:model-value="updateDate"
-      />
-    </v-menu>
-  </v-layout>
+  <div class="date-picker">
+    <VueDatePicker
+      :model-value="modelValue"
+      :locale="locale"
+      :format="dateFormatPattern"
+      :format-locale="fnsLocale"
+      :range="range"
+      :multi-calendars="range"
+      :enable-time-picker="withTimePicker"
+      :auto-apply="autoApply"
+      :auto-position="true"
+      :start-date="today"
+      close-on-scroll
+      text-input
+      :readonly="readonly"
+      position="left"
+      :teleport="true"
+      :select-text="$t('select')"
+      :cancel-text="$t('cancel')"
+      input-class-name="date-picker-input"
+      class="mb-6"
+      @update:model-value="emit('update:modelValue', $event)"
+    />
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref, computed, nextTick, watch, type PropType } from 'vue'
 import { useI18n } from 'vue-i18n'
+import type { PropType } from 'vue'
+import type { Locale } from 'date-fns'
+import type { supportedLocales } from '~/services/utils/dateUtils'
+import DateUtils from '~/services/utils/dateUtils'
+
+const emit = defineEmits(['update:modelValue'])
 
 const props = defineProps({
-  modelValue: Date,
-  label: {
-    type: String,
-    default: '',
-  },
-  format: {
-    type: String,
+  modelValue: {
+    type: Object as PropType<Date | Array<Date> | null>,
+    required: false,
     default: null,
   },
-  /**
-   * Position du date-picker
-   * @see https://vuetifyjs.com/en/api/v-menu/#props-position
-   */
-  position: {
-    type: String as PropType<'left' | 'center' | 'right'>,
-    default: 'center',
+  range: {
+    type: Boolean,
+    default: false,
+  },
+  withTimePicker: {
+    type: Boolean,
+    default: false,
+  },
+  autoApply: {
+    type: Boolean,
+    default: true,
+  },
+  readonly: {
+    type: Boolean,
+    default: false,
   },
 })
 
-const emit = defineEmits(['update:modelValue'])
-
 const i18n = useI18n()
-const menu = ref(false)
-const positionX = ref(0)
-const positionY = ref(0)
 
-const displayDate = computed({
-  get: () => {
-    if (!props.modelValue) return ''
-    if (props.format) {
-      return new Intl.DateTimeFormat(i18n.locale.value, {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-      }).format(props.modelValue)
-    }
-    return props.modelValue.toLocaleDateString(i18n.locale.value)
-  },
-  set: () => {},
-})
+const locale: Ref<string> = i18n.locale
 
-function updateDate(value: Date) {
-  emit('update:modelValue', value)
-  menu.value = false
-}
+const fnsLocale: ComputedRef<Locale> = computed(() =>
+  DateUtils.getFnsLocale(locale.value as supportedLocales),
+)
 
-function updatePosition() {
-  nextTick(() => {
-    const activator = document.querySelector('.v-menu__activator')
-    if (activator) {
-      const rect = activator.getBoundingClientRect()
-      positionX.value = rect.left
-      positionY.value = rect.bottom
-    }
-  })
-}
+const dateFormatPattern: ComputedRef<string> = computed(() =>
+  DateUtils.getShortFormatPattern(locale.value as supportedLocales),
+)
 
-watch(menu, (val) => {
-  if (val) updatePosition()
-})
+const today = new Date()
 </script>
 
-<style scoped>
-.v-menu__content {
-  position: absolute !important;
+<style lang="scss">
+:deep(.dp__active_date) {
+  border-radius: 12px;
+}
+
+:deep(.dp__today) {
+  border-radius: 12px;
+  border: 1px solid rgb(var(--v-theme-neutral-strong)) !important;
+}
+
+:deep(.dp__action_button) {
+  height: 32px;
 }
 </style>

+ 0 - 129
components/Ui/DateRangePicker.vue

@@ -1,129 +0,0 @@
-<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"
-  />
-</template>
-
-<script setup lang="ts">
-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,
-  },
-  maxHeight: {
-    type: Number,
-    required: false,
-    default: null,
-  },
-})
-
-const emit = defineEmits(['update:modelValue'])
-
-const autoApply = false
-
-const updateDateTimeRange = (value: [string, string]) => {
-  emit('update:modelValue', value)
-}
-
-const i18n = useI18n()
-
-const fnsLocale = DateUtils.getFnsLocale(i18n.locale.value as supportedLocales)
-const dateFormatPattern = DateUtils.getShortFormatPattern(
-  i18n.locale.value as supportedLocales,
-)
-
-const today = new Date()
-
-let style = ''
-if (props.maxHeight !== null) {
-  style +=
-    'height: ' + props.maxHeight + 'px;max-height: ' + props.maxHeight + 'px;'
-}
-
-/**
- * Recalcule la position du panneau de sélection des dates si trop près du bord droit de l'écran
- * @param el
- */
-const dateRangePickerAltPosition = (el: HTMLElement) => {
-  let xOffset = 0
-  const fullWidth = 500
-  const rightPadding = 30
-  const rect = el.getBoundingClientRect()
-
-  if (rect.left + fullWidth + rightPadding > window.innerWidth) {
-    xOffset = window.innerWidth - (rect.left + fullWidth + rightPadding)
-  }
-
-  return {
-    top: rect.bottom,
-    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);
-}
-
-.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;
-  }
-}
-</style>

+ 37 - 15
components/Ui/Form.vue

@@ -30,11 +30,11 @@ de quitter si des données ont été modifiées.
               :actions="actions"
               :validation-pending="validationPending || !isValid"
               @submit="submit"
-            ></UiButtonSubmit>
+            />
           </v-col>
         </v-row>
       </v-container>
-      <div v-else class="mt-12" />
+      <div v-else class="mt-6" />
 
       <!-- Content -->
       <slot v-bind="{ modelValue }" />
@@ -43,7 +43,7 @@ de quitter si des données ont été modifiées.
       <v-container
         v-if="actionPosition === 'both' || actionPosition === 'bottom'"
         :fluid="true"
-        class="container btnActions mt-6"
+        class="container btnActions"
       >
         <v-row>
           <v-col cols="12" sm="12">
@@ -53,7 +53,7 @@ de quitter si des données ont été modifiées.
               :validation-pending="validationPending || !isValid"
               :actions="actions"
               @submit="submit"
-            ></UiButtonSubmit>
+            />
           </v-col>
         </v-row>
       </v-container>
@@ -98,7 +98,7 @@ import * as _ from 'lodash-es'
 import { FORM_FUNCTION, SUBMIT_TYPE, TYPE_ALERT } from '~/types/enum/enums'
 import { useFormStore } from '~/stores/form'
 import { useEntityManager } from '~/composables/data/useEntityManager'
-import ApiModel from '~/models/ApiModel'
+import type ApiModel from '~/models/ApiModel'
 import { usePageStore } from '~/stores/page'
 import type { AnyJson } from '~/types/data'
 import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
@@ -145,9 +145,9 @@ const props = defineProps({
     default: false,
   },
   /**
-   * Faut-il rafraichir le profil à la soumission du formulaire?
+   * Faut-il rafraichir le profil à la soumission du formulaire ?
    */
-  refreshProfile: {
+  refreshProfileOnSubmit: {
     type: Boolean,
     required: false,
     default: false,
@@ -230,7 +230,7 @@ const submit = async (next: string | null = null) => {
 
     emit('update:model-value', updatedEntity)
 
-    if (props.refreshProfile) {
+    if (props.refreshProfileOnSubmit) {
       await refreshProfile()
     }
 
@@ -246,17 +246,23 @@ const submit = async (next: string | null = null) => {
     } else if (next === SUBMIT_TYPE.SAVE_AND_BACK) {
       onSaveAndQuitAction(actionArgs)
     }
-  } catch (error: any) {
+  } catch (error: unknown) {
+    const err = error as {
+      response?: {
+        status: number
+        data: { violations?: Array<{ message: string; propertyPath: string }> }
+      }
+    }
     if (
-      error.response &&
-      error.response.status === 422 &&
-      error.response.data.violations
+      err.response &&
+      err.response.status === 422 &&
+      err.response.data.violations
     ) {
       // TODO: à revoir
       const violations: Array<string> = []
       let fields: AnyJson = {}
 
-      for (const violation of error.response.data.violations) {
+      for (const violation of err.response.data.violations) {
         violations.push(i18n.t(violation.message) as string)
         fields = Object.assign(fields, {
           [violation.propertyPath]: violation.message,
@@ -390,11 +396,12 @@ watch(props.modelValue, async (newEntity, oldEntity) => {
  * @param e
  */
 // TODO: voir si encore nécessaire avec le @submit.prevent
-const preventSubmit = (e: any) => {
+const preventSubmit = (e: Event) => {
   // Cancel the event
   e.preventDefault()
   // Chrome requires returnValue to be set
-  e.returnValue = ''
+  const event = e as { returnValue: string }
+  event.returnValue = ''
 }
 
 /**
@@ -410,6 +417,21 @@ defineExpose({ validate })
 <style scoped>
 .btnActions {
   text-align: right;
+
+  @media (max-width: 600px) {
+    :deep(.v-col-12) {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      width: 100%;
+
+      .v-btn {
+        margin: 12px 0 !important;
+        max-width: 250px;
+      }
+    }
+  }
 }
 
 .confirmation-dlg-actions {

+ 7 - 7
components/Ui/Form/Creation.vue

@@ -1,5 +1,5 @@
 <template>
-  <UiForm v-model="entity" :submitActions="submitActions">
+  <UiForm v-model="entity" :submit-actions="submitActions">
     <template #form.button>
       <v-btn v-if="goBackRoute" class="theme-neutral mr-3" @click="quit">
         {{ $t('cancel') }}
@@ -11,9 +11,9 @@
 </template>
 
 <script setup lang="ts">
-import type { PropType } from '@vue/runtime-core'
+import type { PropType } from 'vue'
 import type { RouteLocationRaw } from '@intlify/vue-router-bridge'
-import ApiModel from '~/models/ApiModel'
+import type ApiModel from '~/models/ApiModel'
 import type { AnyJson } from '~/types/data'
 import { SUBMIT_TYPE } from '~/types/enum/enums'
 import { useEntityManager } from '~/composables/data/useEntityManager'
@@ -23,7 +23,7 @@ const props = defineProps({
    * Classe de l'ApiModel (ex: Organization, Notification, ...)
    */
   model: {
-    type: Function as any as () => typeof ApiModel,
+    type: Function as PropType<() => typeof ApiModel>,
     required: true,
   },
   /**
@@ -55,11 +55,11 @@ const props = defineProps({
 const router = useRouter()
 const { em } = useEntityManager()
 
-//@ts-ignore Pour une raison que j'ignore, le type Ref<ApiModel> met en erreur la prop entity de UiForm...
+// @ts-expect-error Pour une raison que j'ignore, le type Ref<ApiModel> met en erreur la prop entity de UiForm...
 const entity: ApiModel = reactive(em.newInstance(props.model))
 
 const submitActions = computed(() => {
-  let actions: AnyJson = {}
+  const actions: AnyJson = {}
 
   if (props.goBackRoute !== null) {
     actions[SUBMIT_TYPE.SAVE_AND_BACK] = props.goBackRoute
@@ -71,7 +71,7 @@ const submitActions = computed(() => {
 
 const quit = () => {
   if (!props.goBackRoute) {
-    throw Error('no go back route defined')
+    throw new Error('no go back route defined')
   }
 
   router.push(props.goBackRoute)

+ 2 - 9
components/Ui/Form/DeletionConfirmationDialog.vue

@@ -1,9 +1,5 @@
 <template>
-  <LazyLayoutDialog :show="modelValue">
-    <template #dialogType>
-      {{ $t('delete_assistant') }}
-    </template>
-
+  <LazyLayoutDialog :show="modelValue" theme="danger">
     <template #dialogTitle>
       {{ $t('caution') }}
     </template>
@@ -15,10 +11,7 @@
     </template>
 
     <template #dialogBtn>
-      <v-btn
-        class="mr-4 submitBtn theme-neutral-strong"
-        @click="onCancelClicked"
-      >
+      <v-btn class="mr-4 submitBtn theme-neutral" @click="onCancelClicked">
         {{ $t('cancel') }}
       </v-btn>
       <v-btn class="mr-4 submitBtn theme-danger" @click="onDeleteClicked">

+ 9 - 9
components/Ui/Form/Edition.vue

@@ -1,7 +1,7 @@
 <template>
   <LayoutContainer>
     <UiLoadingPanel v-if="pending" />
-    <UiForm v-else v-model="entity" :submitActions="submitActions">
+    <UiForm v-else v-model="entity" :submit-actions="submitActions">
       <template #form.button>
         <v-btn v-if="goBackRoute" class="theme-neutral mr-3" @click="quit">
           {{ $t('cancel') }}
@@ -14,21 +14,21 @@
 </template>
 
 <script setup lang="ts">
-import type { PropType } from '@vue/runtime-core'
-import ApiModel from '~/models/ApiModel'
-import type { AnyJson } from '~/types/data'
-import { SUBMIT_TYPE } from '~/types/enum/enums'
+import type { PropType } from 'vue'
 import { useRoute } from 'vue-router'
 import type { RouteLocationRaw } from 'vue-router'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import type { AsyncData } from '#app'
+import type ApiModel from '~/models/ApiModel'
+import type { AnyJson } from '~/types/data'
+import { SUBMIT_TYPE } from '~/types/enum/enums'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
 
 const props = defineProps({
   /**
    * Classe de l'ApiModel (ex: Organization, Notification, ...)
    */
   model: {
-    type: Function as any as () => typeof ApiModel,
+    type: Function as PropType<() => typeof ApiModel>,
     required: true,
   },
   /**
@@ -79,7 +79,7 @@ const { data: entity, pending } = fetch(props.model, entityId) as AsyncData<
 >
 
 const submitActions = computed(() => {
-  let actions: AnyJson = {}
+  const actions: AnyJson = {}
 
   if (props.goBackRoute !== null) {
     actions[SUBMIT_TYPE.SAVE_AND_BACK] = props.goBackRoute
@@ -91,7 +91,7 @@ const submitActions = computed(() => {
 
 const quit = () => {
   if (!props.goBackRoute) {
-    throw Error('no go back route defined')
+    throw new Error('no go back route defined')
   }
 
   router.push(props.goBackRoute)

+ 10 - 10
components/Ui/Help.vue

@@ -12,24 +12,24 @@
   >
     <template #activator="{}">
       <v-icon
-        @click="onIconClicked"
+        ref="iconRef"
         icon
         class="ml-3"
         size="18px"
-        ref="iconRef"
+        @click="onIconClicked"
       >
         {{ icon }}
       </v-icon>
     </template>
 
-    <div ref="slotDiv" class="tooltip" v-click-out="onClickOutside">
-      <slot></slot>
+    <div ref="slotDiv" v-click-out="onClickOutside" class="tooltip">
+      <slot />
     </div>
   </v-tooltip>
 </template>
 
 <script setup lang="ts">
-import type { Ref } from '@vue/reactivity'
+import type { Ref } from 'vue'
 
 const props = defineProps({
   left: {
@@ -61,24 +61,24 @@ const props = defineProps({
 
 const { $refs } = useNuxtApp()
 
-const show: Ref<Boolean> = ref(false)
+const show: Ref<boolean> = ref(false)
 
 // Template reference to the icon object
 const iconRef = ref(null)
 
 // Left is the default, set it to true if not any other is true
-const leftOrDefault: Ref<Boolean> = ref(
+const leftOrDefault: Ref<boolean> = ref(
   props.left || (!props.right && !props.bottom && !props.top),
 )
 
-const onIconClicked = (e: any) => {
+const onIconClicked = (e: MouseEvent) => {
   show.value = !show.value
   e.stopPropagation()
 }
 
-const onClickOutside = (e: any) => {
+const onClickOutside = (e: MouseEvent) => {
   if (show.value) {
-    if (e.target === (iconRef.value as any).$el) {
+    if (e.target === (iconRef.value as { $el: HTMLElement }).$el) {
       return
     }
     show.value = false

+ 27 - 10
components/Ui/Image.vue

@@ -14,10 +14,10 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
       >
         <template #placeholder>
           <v-row
+            v-if="pending"
             class="fill-height ma-0"
             align="center"
             justify="center"
-            v-if="pending"
           >
             <v-progress-circular :indeterminate="true" color="neutral" />
           </v-row>
@@ -36,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 type { WatchStopHandle } from 'vue'
 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 { IMAGE_SIZE } from '~/types/enum/enums'
 
 const props = defineProps({
   /**
@@ -56,6 +56,7 @@ const props = defineProps({
   defaultImage: {
     type: String,
     required: false,
+    default: null,
   },
   /**
    * Hauteur de l'image à l'écran (en px)
@@ -63,6 +64,7 @@ const props = defineProps({
   height: {
     type: Number,
     required: false,
+    default: null,
   },
   /**
    * Largeur de l'image à l'écran (en px)
@@ -70,6 +72,15 @@ const props = defineProps({
   width: {
     type: Number,
     required: false,
+    default: null,
+  },
+  /**
+   * Taille de l'image fetchée depuis l'API (prédimensionnement)
+   */
+  size: {
+    type: String as PropType<IMAGE_SIZE>,
+    required: false,
+    default: IMAGE_SIZE.MD,
   },
   /**
    * Icône à afficher en overlay au survol de la souris
@@ -89,23 +100,29 @@ 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 refresh = () => {
   refreshImage()
 }
 defineExpose({ refresh })
 
+interface FetchResult {
+  data: Ref<string | null>
+  pending: Ref<boolean>
+  refresh: () => void
+}
+
+const {
+  data: imageSrc,
+  pending,
+  refresh: refreshImage,
+} = (await fetch(fileId, props.size, defaultImagePath)) as FetchResult
+
 /**
  * Si l'id change, on recharge l'image
  */
 const unwatch: WatchStopHandle = watch(
   () => props.imageId,
-  async (value, oldValue) => {
+  async () => {
     refresh()
   },
 )

+ 32 - 12
components/Ui/Input/Autocomplete.vue

@@ -8,6 +8,7 @@ Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
   <main>
     <!--suppress TypeScriptValidateTypes -->
     <v-autocomplete
+      v-model:search-input="search"
       :model-value="modelValue"
       autocomplete="search"
       :items="items"
@@ -19,7 +20,6 @@ Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
       :multiple="multiple"
       :loading="isLoading"
       :return-object="returnObject"
-      :search-input.sync="search"
       :prepend-inner-icon="prependInnerIcon"
       :error="error || !!fieldViolations"
       :error-messages="
@@ -27,37 +27,41 @@ Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
       "
       :rules="rules"
       :chips="chips"
+      :closable-chips="closableChips"
       :hide-no-data="hideNoData"
       :no-data-text="
         isLoading ? $t('please_wait') : $t('no_result_matching_your_request')
       "
       :variant="variant"
+      density="compact"
+      class="mb-3"
       @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>-->
+      <template v-if="slotText" #item="{ item }">
+        <div>{{ item.slotTextDisplay }}</div>
       </template>
     </v-autocomplete>
   </main>
 </template>
 
 <script setup lang="ts">
-import { computed } from '@vue/reactivity'
-import type { ComputedRef, Ref } from '@vue/reactivity'
+import { computed } from 'vue'
+import type { ComputedRef, Ref, PropType } from 'vue'
 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({
   /**
    * v-model
    */
   modelValue: {
-    type: [String, Number, Object, Array] as PropType<any>,
+    type: [String, Number, Object, Array] as PropType<
+      string | number | object | Array<unknown>
+    >,
     required: false,
     default: null,
   },
@@ -85,7 +89,7 @@ const props = defineProps({
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-items
    */
   items: {
-    type: Array as PropType<Array<Object>>,
+    type: Array as PropType<Array<object>>,
     required: false,
     default: () => [],
   },
@@ -135,6 +139,7 @@ const props = defineProps({
    */
   prependInnerIcon: {
     type: String,
+    default: null,
   },
   /**
    * Rends les résultats sous forme de puces
@@ -144,6 +149,14 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  /**
+   * Permet de retirer une puce directement
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-closable-chips
+   */
+  closableChips: {
+    type: Boolean,
+    default: false,
+  },
   /**
    * Le contenu de la liste est en cours de chargement
    */
@@ -261,7 +274,7 @@ const onUpdate = (event: string) => {
  * TODO: à revoir
  */
 const items: ComputedRef<Array<AnyJson>> = computed(() => {
-  let _items: Array<any> = props.items
+  const _items: Array<AnyJson> = props.items
   return _items
   // if (props.group !== null) {
   //   _items = groupItems(props.items)
@@ -276,7 +289,7 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
  *
  * @param items
  */
-const groupItems = (items: Array<any>): Array<Array<string>> => {
+const groupItems = (items: Array<AnyJson>): Array<Array<string>> => {
   const group = props.group as string | null
   if (group === null) {
     return items
@@ -317,7 +330,7 @@ const prepareGroups = (groupedItems: Array<Array<string>>): Array<AnyJson> => {
 
     // On parcourt les items pour préparer les texts / slotTexts à afficher
     finalItems = finalItems.concat(
-      groupedItems[group].map((item: any) => {
+      groupedItems[group].map((item: AnyJson) => {
         return prepareItem(item)
       }),
     )
@@ -331,7 +344,7 @@ const prepareGroups = (groupedItems: Array<Array<string>>): Array<AnyJson> => {
  *
  * @param item
  */
-const prepareItem = (item: Object): AnyJson => {
+const prepareItem = (item: object): AnyJson => {
   const slotTextDisplay: Array<string> = []
   const itemTextDisplay: Array<string> = []
 
@@ -359,3 +372,10 @@ const prepareItem = (item: Object): AnyJson => {
   })
 }
 </script>
+
+<style scoped lang="scss">
+:deep(.v-chip__close .v-icon) {
+  font-size: 16px;
+  color: rgb(var(--v-theme-on-neutral));
+}
+</style>

+ 17 - 5
components/Ui/Input/Autocomplete/Accesses.vue

@@ -12,14 +12,16 @@ Champs autocomplete dédié à la recherche des Accesses d'une structure
       :label="label"
       :items="items"
       item-value="id"
-      :isLoading="pending"
+      :is-loading="pending"
       :multiple="multiple"
       hide-no-data
       :chips="chips"
+      :closable-chips="true"
       :auto-select-first="false"
       prepend-inner-icon="fas fa-magnifying-glass"
       :return-object="false"
       :variant="variant"
+      :class="pending || pageStore.loading ? 'hide-selection' : ''"
       @update:model-value="onUpdateModelValue"
       @update:search="onUpdateSearch"
     />
@@ -27,13 +29,12 @@ Champs autocomplete dédié à la recherche des Accesses d'une structure
 </template>
 
 <script setup lang="ts">
-import type { PropType } from '@vue/runtime-core'
-import type { ComputedRef, Ref } from '@vue/reactivity'
-import { computed } from '@vue/reactivity'
+import type { PropType, ComputedRef, Ref } from 'vue'
+import { computed } from 'vue'
+import * as _ from 'lodash-es'
 import type { AssociativeArray } from '~/types/data'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Access from '~/models/Access/Access'
-import * as _ from 'lodash-es'
 import Query from '~/services/data/Query'
 import OrderBy from '~/services/data/Filters/OrderBy'
 import { ORDER_BY_DIRECTION, SEARCH_STRATEGY } from '~/types/enum/data'
@@ -139,6 +140,7 @@ interface UserListItem {
 
 const { fetchCollection } = useEntityFetch()
 const i18n = useI18n()
+const pageStore = usePageStore()
 
 /**
  * Génère un AccessListItem à partir d'un Access
@@ -257,4 +259,14 @@ const onUpdateModelValue = (event: Array<number>) => {
 .v-autocomplete {
   min-width: 350px;
 }
+
+.hide-selection {
+  /**
+      On cache le contenu au chargement en attendant de résoudre le bug qui fait
+      que ce sont les ids ou les IRIs qui s'affichent le temps du chargement
+   */
+  :deep(.v-chip__content) {
+    color: transparent !important;
+  }
+}
 </style>

+ 16 - 12
components/Ui/Input/AutocompleteWithAPI.vue

@@ -11,27 +11,26 @@ d'une api)
       :label="label"
       :data="remoteData ? remoteData : data"
       :items="items"
-      :isLoading="isLoading"
+      :is-loading="isLoading"
       :item-text="itemText"
-      :slotText="slotText"
+      :slot-text="slotText"
       :item-value="itemValue"
       :multiple="multiple"
       :chips="chips"
-      prependIcon="mdi-magnify"
+      prepend-icon="mdi-magnify"
       :return-object="returnObject"
-      @research="search"
       :no-filter="noFilter"
+      @research="search"
       @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 { ref, toRefs, watch } from 'vue'
+import type { Ref } from 'vue'
 import { useFetch } from '#app'
-import { watch } from '@vue/runtime-core'
+import UrlUtils from '~/services/utils/urlUtils'
 
 const props = defineProps({
   label: {
@@ -45,7 +44,9 @@ const props = defineProps({
     default: null,
   },
   searchFunction: {
-    type: Function,
+    type: Function as PropType<
+      (research: string, field?: string) => Promise<Array<unknown>>
+    >,
     required: true,
   },
   data: {
@@ -78,6 +79,7 @@ const props = defineProps({
   slotText: {
     type: Array,
     required: false,
+    default: () => [],
   },
   returnObject: {
     type: Boolean,
@@ -97,6 +99,8 @@ const props = defineProps({
   },
 })
 
+const emit = defineEmits(['update'])
+
 const { data } = toRefs(props)
 const items = ref([])
 const remoteData: Ref<Array<string> | null> = ref(null)
@@ -105,7 +109,7 @@ const isLoading = ref(false)
 if (props.data) {
   items.value = props.multiple ? (data.value ?? []) : [data.value]
 } else if (props.remoteUri) {
-  const ids: Array<any> = []
+  const ids: Array<string | number> = []
 
   for (const uri of props.remoteUri) {
     ids.push(UrlUtils.extractIdFromUri(uri as string))
@@ -119,7 +123,7 @@ if (props.data) {
   useFetch(async () => {
     isLoading.value = true
 
-    const r: any = await $fetch(props.remoteUrl, options)
+    const r: { data: Array<string> } = await $fetch(props.remoteUrl, options)
 
     isLoading.value = false
     remoteData.value = r.data
@@ -129,7 +133,7 @@ if (props.data) {
 
 const search = async (research: string) => {
   isLoading.value = true
-  const func: Function = props.searchFunction
+  const func = props.searchFunction
   items.value = items.value.concat(await func(research, props.field))
   isLoading.value = false
 }

+ 7 - 10
components/Ui/Input/AutocompleteWithAp2i.vue

@@ -10,24 +10,23 @@ Liste déroulante avec autocompletion issue de Ap2i
       :field="field"
       :label="label"
       :items="items"
-      :isLoading="pending"
+      :is-loading="pending"
       item-title="title"
       item-value="id"
       :multiple="multiple"
       :chips="chips"
-      prependIcon="fas fa-magnifying-glass"
+      prepend-icon="fas fa-magnifying-glass"
       :return-object="false"
     />
   </main>
 </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 { computed } from 'vue'
+import type { ComputedRef, Ref, PropType } from 'vue'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import ApiResource from '~/models/ApiResource'
-import ApiModel from '~/models/ApiModel'
+import type ApiResource from '~/models/ApiResource'
+import type ApiModel from '~/models/ApiModel'
 import type { AnyJson, AssociativeArray } from '~/types/data'
 
 const props = defineProps({
@@ -43,7 +42,7 @@ const props = defineProps({
    * Classe de l'ApiModel (ex: Organization, Notification, ...) qui sert de source à la liste
    */
   model: {
-    type: Function as any as () => typeof ApiModel,
+    type: Function as PropType<typeof ApiModel>,
     required: true,
   },
   /**
@@ -137,8 +136,6 @@ const { data: collection, pending } = await fetchCollection(
 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 []

+ 2 - 0
components/Ui/Input/AutocompleteWithEnum.vue

@@ -18,6 +18,8 @@ import { useEnumFetch } from '~/composables/data/useEnumFetch'
 import ArrayUtils from '~/services/utils/arrayUtils'
 import type { Enum } from '~/types/data'
 
+const emit = defineEmits(['update:model-value'])
+
 const props = defineProps({
   modelValue: {
     type: String as PropType<string | null>,

+ 18 - 2
components/Ui/Input/Checkbox.vue

@@ -9,9 +9,10 @@ Case à cocher, à placer dans un composant `UiForm`
     :model-value="modelValue"
     :label="$t(fieldLabel)"
     :disabled="readonly"
+    density="compact"
     :error="error || !!fieldViolations"
     :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
-    class="py-1"
+    class="checkbox py-1"
     @update:model-value="onUpdate"
   />
 </template>
@@ -92,4 +93,19 @@ const onUpdate = (event: boolean) => {
 }
 </script>
 
-<style scoped></style>
+<style scoped lang="scss">
+.checkbox {
+  margin-top: -4px;
+  margin-bottom: 8px;
+}
+
+@media (min-width: 600px) {
+  :deep(.v-input__control) {
+    max-height: 32px;
+  }
+}
+
+:deep(.v-label) {
+  padding-left: 8px;
+}
+</style>

+ 2 - 0
components/Ui/Input/Combobox.vue

@@ -12,6 +12,7 @@ Liste déroulante, à placer dans un composant `UiForm`
       :label="$t(fieldLabel)"
       :items="items"
       :disabled="readonly"
+      density="compact"
       :error="error || !!fieldViolations"
       :error-messages="
         errorMessage || fieldViolations ? $t(fieldViolations) : ''
@@ -31,6 +32,7 @@ const props = defineProps({
   modelValue: {
     type: [String, Number],
     required: false,
+    default: null,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété

+ 47 - 23
components/Ui/Input/DatePicker.vue

@@ -4,14 +4,15 @@ Sélecteur de dates, à placer dans un composant `UiForm`
 
 <template>
   <main>
-    <div class="d-flex flex-column">
-      <span>{{ $t(fieldLabel) }}</span>
+    <div class="d-flex flex-column container">
+      <span class="label">
+        {{ $t(fieldLabel) }}
+      </span>
 
       <UiDatePicker
-        v-model="date"
+        :model-value="date"
         :readonly="readonly"
-        :format="format"
-        :position="position"
+        class="date-picker"
         @update:model-value="onUpdate($event)"
       />
 
@@ -63,15 +64,6 @@ const props = defineProps({
     type: Boolean,
     required: false,
   },
-  /**
-   * Format d'affichage des dates
-   * @see https://vue3datepicker.com/props/formatting/
-   */
-  format: {
-    type: String,
-    required: false,
-    default: null,
-  },
   /**
    * Règles de validation
    * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
@@ -96,14 +88,6 @@ const props = defineProps({
     required: false,
     default: null,
   },
-  /**
-   * @see https://vue3datepicker.com/props/positioning/#position
-   */
-  position: {
-    type: String as PropType<'left' | 'center' | 'right'>,
-    required: false,
-    default: 'center',
-  },
 })
 
 const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
@@ -118,8 +102,48 @@ const date: Ref<Date | undefined> = ref(
 
 const onUpdate = (event: string) => {
   updateViolationState(event)
+  date.value = event ? new Date(event) : undefined
   emit('update:model-value', date.value ? formatISO(date.value) : undefined)
 }
 </script>
 
-<style scoped></style>
+<style scoped lang="scss">
+.container {
+  position: relative;
+}
+
+.label {
+  position: absolute;
+  color: #8e8e8e;
+  top: -0.7rem;
+  left: 0.75rem;
+  background-color: rgb(var(--v-theme-surface));
+  padding: 0 0.3rem;
+  font-size: 0.8rem;
+  z-index: 1;
+  transition:
+    color 0.2s,
+    font-size 0.2s,
+    top 0.2s;
+}
+
+.date-picker:hover {
+  :deep(.dp__input) {
+    border-color: #333333;
+  }
+}
+
+.container:focus-within {
+  .label {
+    color: #333333;
+  }
+
+  :deep(.dp__input_focus) {
+    border: solid 2px #333333;
+  }
+
+  :deep(.dp__input_icon) {
+    color: #333333;
+  }
+}
+</style>

+ 5 - 2
components/Ui/Input/Email.vue

@@ -16,9 +16,10 @@ Champs de saisie de type Text dédié à la saisie d'emails
 
 <script setup lang="ts">
 import { useNuxtApp } from '#app'
+import type { PropType } from 'vue'
+import { useI18n } from 'vue-i18n'
 import { useFieldViolation } from '~/composables/form/useFieldViolation'
 import { useValidationUtils } from '~/composables/utils/useValidationUtils'
-import type { PropType } from '@vue/runtime-core'
 
 const props = defineProps({
   label: {
@@ -74,7 +75,9 @@ const props = defineProps({
   },
 })
 
-const { emit, i18n } = useNuxtApp()
+const { emit } = useNuxtApp()
+
+const i18n = useI18n()
 
 const fieldLabel = props.label ?? props.field
 

+ 5 - 4
components/Ui/Input/Enum.vue

@@ -22,7 +22,8 @@ Liste déroulante dédiée à l'affichage d'objets Enum
       :error-messages="
         errorMessage || (fieldViolations ? $t(fieldViolations) : '')
       "
-      @update:modelValue="
+      density="compact"
+      @update:model-value="
         updateViolationState($event)
         $emit('update:modelValue', $event)
       "
@@ -39,7 +40,7 @@ const props = defineProps({
    * v-model
    */
   modelValue: {
-    String,
+    type: String,
     required: false,
     default: null,
   },
@@ -103,7 +104,7 @@ const props = defineProps({
 })
 
 if (typeof props.enum === 'undefined') {
-  throw new Error("missing 'enum' property for input")
+  throw new TypeError("missing 'enum' property for input")
 }
 
 const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
@@ -113,7 +114,7 @@ const { data: items, pending } = fetch(props.enum)
 
 const emit = defineEmits(['update:modelValue', 'change'])
 
-const onModelUpdate = (event: any) => {
+const onModelUpdate = (event: string | null) => {
   updateViolationState(event)
   emit('change', event)
   emit('update:modelValue', event)

+ 54 - 17
components/Ui/Input/Image.vue

@@ -27,13 +27,13 @@ Assistant de création d'image
             align="center"
             justify="center"
           >
-            <v-progress-circular :indeterminate="true" color="neutral">
-            </v-progress-circular>
+            <v-progress-circular :indeterminate="true" color="neutral" />
           </v-row>
 
           <div v-else>
             <div class="upload__cropper-wrapper">
               <Cropper
+                v-if="croppingEnabled"
                 ref="cropper"
                 class="upload__cropper"
                 check-orientation
@@ -43,6 +43,12 @@ Assistant de création d'image
                 @change="onCropperChange"
               />
 
+              <v-img
+                v-else
+                :src="currentImage.src ?? ''"
+                class="upload__cropper"
+              />
+
               <div
                 v-if="currentImage.src"
                 class="upload__reset-button"
@@ -75,8 +81,8 @@ Assistant de création d'image
         </v-btn>
         <v-btn
           class="submitBtn theme-primary"
-          @click="save"
           :disabled="pending"
+          @click="save"
         >
           {{ $t('save') }}
         </v-btn>
@@ -88,15 +94,27 @@ Assistant de création d'image
 <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 { type Ref, ref } from 'vue'
 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 { FILE_VISIBILITY, IMAGE_SIZE, TYPE_ALERT } from '~/types/enum/enums'
 import { usePageStore } from '~/stores/page'
 import FileUtils from '~/services/utils/fileUtils'
 
+interface CropperChangeEvent {
+  coordinates: {
+    left: number
+    top: number
+    width: number
+    height: number
+  }
+}
+
+interface UploadResponse {
+  fileId: number
+}
+
 const props = defineProps({
   /**
    * Id de l'objet File, ou null
@@ -107,8 +125,7 @@ const props = defineProps({
     default: null,
   },
   /**
-   * Label du champ
-   * Si non défini, c'est le nom de propriété qui est utilisé
+   * Nom du champ
    */
   field: {
     type: String,
@@ -121,6 +138,7 @@ const props = defineProps({
   defaultImage: {
     type: String,
     required: false,
+    default: null,
   },
   /**
    * Hauteur de l'image à l'écran (en px)
@@ -128,6 +146,7 @@ const props = defineProps({
   height: {
     type: Number,
     required: false,
+    default: null,
   },
   /**
    * Largeur de l'image à l'écran (en px)
@@ -135,6 +154,15 @@ const props = defineProps({
   width: {
     type: Number,
     required: false,
+    default: null,
+  },
+  /**
+   * Donne la possibilité de rogner les images
+   */
+  croppingEnabled: {
+    type: Boolean,
+    required: false,
+    default: true,
   },
   /**
    * TODO: completer
@@ -142,6 +170,7 @@ const props = defineProps({
   ownerId: {
     type: Number,
     required: false,
+    default: null,
   },
 })
 
@@ -154,9 +183,9 @@ const emit = defineEmits(['update:modelValue'])
 /**
  * Références à des composants
  */
-const fileInput: Ref<null | any> = ref(null)
-const cropper: Ref<any> = ref(null)
-const uiImage: Ref<any> = ref(null)
+const fileInput: Ref<HTMLInputElement | null> = ref(null)
+const cropper: Ref<typeof Cropper | null> = ref(null)
+const uiImage: Ref<{ $el: HTMLElement } | null> = ref(null)
 
 /**
  * L'objet File ou l'image sont en cours de chargement
@@ -213,7 +242,12 @@ const defaultPosition = () => {
 /**
  * @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: {
+    imageSize?: { width: number; height: number }
+    visibleArea?: { width: number; height: number }
+  } | null,
+): { width: number; height: number } | null => {
   if (!params) {
     return null
   }
@@ -243,7 +277,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, IMAGE_SIZE.RAW)
 }
 
 /**
@@ -291,7 +325,7 @@ const reset = () => {
  * Upload une image depuis le poste client
  * @param event
  */
-const uploadImage = async (event: any) => {
+const uploadImage = async (event: Event) => {
   const { files } = event.target
 
   if (!files || !files[0]) {
@@ -325,7 +359,9 @@ 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) => {
+const onCropperChange = ({
+  coordinates: newCoordinates,
+}: CropperChangeEvent) => {
   cropperConfig.value = newCoordinates
 }
 
@@ -363,7 +399,7 @@ const saveNewImage = async (): Promise<number> => {
     currentImage.value.content,
     FILE_VISIBILITY.EVERYBODY,
     config,
-  )) as any
+  )) as UploadResponse
 
   return response.fileId
 }
@@ -400,13 +436,14 @@ const save = async () => {
   } 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)
   }
 
   showModal.value = false
+
+  uiImage.value.refresh()
   pageStore.loading = false
 }
 

+ 1 - 7
components/Ui/Input/Number.vue

@@ -9,9 +9,9 @@ An input for numeric values
     :model-value.number="modelValue"
     :label="label || field ? $t(label ?? field) : undefined"
     hide-details
-    :density="density"
     type="number"
     :variant="variant"
+    density="compact"
     @update:model-value="onModelUpdate($event)"
   />
 </template>
@@ -60,11 +60,6 @@ const props = defineProps({
     required: false,
     default: null,
   },
-  density: {
-    type: String as PropType<Density>,
-    required: false,
-    default: 'default',
-  },
   /**
    * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
    */
@@ -123,7 +118,6 @@ const keepInRange = (val: number) => {
 const emit = defineEmits(['update:modelValue'])
 
 const onModelUpdate = (event: string) => {
-  // eslint-disable-next-line vue/no-mutating-props
   // props.modelValue = keepInRange(cast(event))
   // emitUpdate()
   emit('update:modelValue', keepInRange(cast(event)))

+ 14 - 5
components/Ui/Input/Phone.vue

@@ -9,11 +9,11 @@ Champs de saisie d'un numéro de téléphone
 <template>
   <client-only>
     <vue-tel-input-vuetify
+      v-model="myPhone"
       :error="error || !!violation"
       :error-messages="errorMessage || violation ? $t(violation) : ''"
       :field="field"
       :label="label"
-      v-model="myPhone"
       :readonly="readonly"
       clearable
       valid-characters-only
@@ -27,8 +27,8 @@ Champs de saisie d'un numéro de téléphone
 
 <script setup lang="ts">
 import { useNuxtApp } from '#app'
+import type { Ref } from 'vue'
 import { useFieldViolation } from '~/composables/form/useFieldViolation'
-import type { Ref } from '@vue/reactivity'
 
 const props = defineProps({
   label: {
@@ -61,7 +61,9 @@ const props = defineProps({
   },
 })
 
-const { emit, i18n } = useNuxtApp()
+const { emit } = useNuxtApp()
+
+const i18n = useI18n()
 
 const { violation, onChange } = useFieldViolation(props.field, emit)
 
@@ -71,12 +73,19 @@ const isValid: Ref<boolean> = ref(false)
 const onInit: Ref<boolean> = ref(true)
 
 const onInput = (
-  _: any,
+  _: unknown,
   {
     number,
     valid,
     countryChoice,
-  }: { number: any; valid: boolean; countryChoice: any },
+  }: {
+    number: {
+      national: string | number
+      international: string | number
+    }
+    valid: boolean
+    countryChoice: Record<string, unknown>
+  },
 ) => {
   isValid.value = valid
   nationalNumber.value = number.national

+ 7 - 4
components/Ui/Input/Text.vue

@@ -1,5 +1,5 @@
 <!--
-Champs de saisie de texte, à placer dans un composant `UiForm`
+Champs de saisie de texte, à placer dans un composant UiForm
 
 @see https://vuetifyjs.com/en/components/text-fields/
 -->
@@ -18,6 +18,7 @@ Champs de saisie de texte, à placer dans un composant `UiForm`
     "
     :append-icon="type === 'password' ? (show ? 'mdi-eye' : 'mdi-eye-off') : ''"
     :variant="variant"
+    density="compact"
     @click:append="show = !show"
     @update:model-value="onUpdate($event)"
     @change="onChange($event)"
@@ -27,9 +28,11 @@ Champs de saisie de texte, à placer dans un composant `UiForm`
 </template>
 
 <script setup lang="ts">
-import { type Ref, ref } from '@vue/reactivity'
+import type { PropType, Ref } from 'vue'
+import { ref } from 'vue'
 import { useFieldViolation } from '~/composables/form/useFieldViolation'
-import type { PropType } from '@vue/runtime-core'
+
+type ValidationRule = (value: string | number | null) => boolean | string
 
 const props = defineProps({
   /**
@@ -81,7 +84,7 @@ const props = defineProps({
    * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
    */
   rules: {
-    type: Array as PropType<any[]>,
+    type: Array as PropType<ValidationRule[]>,
     required: false,
     default: () => [],
   },

+ 4 - 4
components/Ui/ItemFromUri.vue

@@ -14,12 +14,12 @@ 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 type { Query } from 'pinia-orm'
+import { computed } from 'vue'
+import type { ComputedRef } from 'vue'
 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 type ApiResource from '~/models/ApiResource'
 
 const props = defineProps({
   uri: {

+ 14 - 26
components/Ui/SystemBar.vue

@@ -3,23 +3,20 @@ System bars
 -->
 
 <template>
-  <v-system-bar
-    height="50"
-    :class="
-      'd-flex flex-row justify-center align-center text-center ' + classes
-    "
-    style="z-index: 1006"
+  <div
+    :class="'alert-bar ' + (onClick ? 'clickable' : '')"
     @click="onClick !== undefined ? onClick() : null"
   >
-    <!-- Forcing z-index to avoid this : https://github.com/vuetifyjs/nuxt-module/issues/205 -->
     <slot>
       <v-icon v-if="icon" small :icon="icon" />
       {{ text }}
     </slot>
-  </v-system-bar>
+  </div>
 </template>
 
 <script setup lang="ts">
+import { useDisplay } from 'vuetify'
+
 const props = defineProps({
   text: {
     type: String,
@@ -31,34 +28,25 @@ const props = defineProps({
     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(' ')
 </script>
 
 <style scoped lang="scss">
-.v-system-bar {
+.alert-bar {
+  position: relative;
   font-size: 14px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  text-align: center;
+  padding: 12px;
+  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
 }
 
 .v-icon {

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

@@ -7,9 +7,13 @@ Template de base d'un tableau interactif
 <template>
   <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] }}
+      <template
+        v-for="(header, index) in headersWithItem"
+        :key="index"
+        #[header.item]="slotProps"
+      >
+        <slot :name="header.item" v-bind="slotProps">
+          {{ slotProps.item[header.value] }}
         </slot>
       </template>
     </v-data-table>
@@ -17,13 +21,19 @@ Template de base d'un tableau interactif
 </template>
 
 <script setup lang="ts">
+interface TableHeader {
+  value: string
+  item?: string
+  [key: string]: unknown
+}
+
 const props = defineProps({
   items: {
     type: Array,
     required: true,
   },
   headers: {
-    type: Array,
+    type: Array as PropType<TableHeader[]>,
     required: true,
   },
 })
@@ -31,7 +41,7 @@ const props = defineProps({
 const { headers } = toRefs(props)
 
 const headersWithItem = computed(() => {
-  return headers.value.map((header: any) => {
+  return headers.value.map((header: TableHeader) => {
     header.item = 'item.' + header.value
     return header
   })

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

@@ -7,9 +7,9 @@ Date formatée
 </template>
 
 <script setup lang="ts">
+import { computed } from 'vue'
+import type { ComputedRef } from 'vue'
 import DateUtils from '~/services/utils/dateUtils'
-import { computed } from '@vue/reactivity'
-import type { ComputedRef } from '@vue/reactivity'
 
 const props = defineProps({
   data: {

+ 3 - 3
components/Ui/Xeditable/Text.vue

@@ -8,7 +8,7 @@ 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 v-model="inputValue" class="ma-0 pa-0" :type="type" />
 
       <v-icon
         icon="fas fa-check"
@@ -34,8 +34,8 @@ Utilisé par exemple pour le choix de l'année active
 </template>
 
 <script setup lang="ts">
-import { ref } from '@vue/reactivity'
-import type { Ref } from '@vue/reactivity'
+import { ref } from 'vue'
+import type { Ref } from 'vue'
 
 const props = defineProps({
   type: {

+ 1 - 1
composables/data/useAp2iRequestService.ts

@@ -43,7 +43,7 @@ export const useAp2iRequestService = () => {
     options.headers = headers
 
     pending.value = true
-    console.log('Request : ' + request + ' (SSR: ' + process.server + ')')
+    console.log('Request : ' + request + ' (SSR: ' + import.meta.server + ')')
   }
 
   const onRequestError = function (_: FetchContext) {

+ 1 - 1
composables/data/useApiLegacyRequestService.ts

@@ -29,7 +29,7 @@ export const useApiLegacyRequestService = () => {
    * @param request
    * @param options
    */
-  const onRequest = function ({ request, options }: FetchContext) {
+  const onRequest = function ({ request: _request, options }: FetchContext) {
     // @ts-expect-error options is not aware of noXaccessId
     if (options && options.noXaccessId) {
       return

+ 2 - 2
composables/data/useEntityFetch.ts

@@ -6,9 +6,9 @@ import type {
   AsyncDataRequestStatus,
 } from '#app/composables/asyncData'
 import { useEntityManager } from '~/composables/data/useEntityManager'
-import ApiResource from '~/models/ApiResource'
+import type ApiResource from '~/models/ApiResource'
 import type { Collection } from '~/types/data'
-import Query from '~/services/data/Query'
+import type Query from '~/services/data/Query'
 
 interface useEntityFetchReturnType {
   fetch: (

+ 4 - 5
composables/data/useImageFetch.ts

@@ -2,13 +2,13 @@ import type { AsyncData } from '#app'
 import { v4 as uuid4 } from 'uuid'
 import type { Ref } from 'vue'
 import { useImageManager } from '~/composables/data/useImageManager'
+import { IMAGE_SIZE } from '~/types/enum/enums'
 
 interface useImageFetchReturnType {
   fetch: (
     id: Ref<number | null>,
+    size?: IMAGE_SIZE,
     defaultImage?: string | null,
-    height?: number,
-    width?: number,
   ) => AsyncData<string | ArrayBuffer | null, Error | null>
 }
 
@@ -20,13 +20,12 @@ export const useImageFetch = (): useImageFetchReturnType => {
 
   const fetch = (
     id: Ref<number | null>, // If id is null, fetch shall return the default image url
+    size: IMAGE_SIZE = IMAGE_SIZE.MD,
     defaultImage: string | null = null,
-    height: number = 0,
-    width: number = 0,
   ) =>
     useAsyncData(
       'img' + (id ?? defaultImage ?? 0) + '_' + uuid4(),
-      () => imageManager.get(id.value, defaultImage, height, width),
+      () => imageManager.get(id.value, size, defaultImage),
       { lazy: true, server: false }, // Always fetch images client-side
     )
 

+ 1 - 2
composables/utils/useDownloadFile.ts

@@ -1,6 +1,6 @@
 import FileSaver from 'file-saver'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
-import File from '~/models/Core/File'
+import type File from '~/models/Core/File'
 
 export const useDownloadFile = async (file: File) => {
   const { apiRequestService } = useAp2iRequestService()
@@ -17,6 +17,5 @@ export const useDownloadFile = async (file: File) => {
 
   const blob = new Blob([blobPart], { type: response.type })
 
-  // eslint-disable-next-line import/no-named-as-default-member
   FileSaver.saveAs(blob, file.name ?? 'unknown')
 }

+ 0 - 2
composables/utils/useDownloadFromRoute.ts

@@ -1,6 +1,5 @@
 import FileSaver from 'file-saver'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
-import File from '~/models/Core/File'
 
 /**
  * Permet de télécharger un fichier fourni par la route donnée
@@ -19,6 +18,5 @@ export const useDownloadFromRoute = async (route: string, filename: string) => {
 
   const blob = new Blob([response], { type: response.type })
 
-  // eslint-disable-next-line import/no-named-as-default-member
   FileSaver.saveAs(blob, filename)
 }

+ 12 - 4
composables/utils/useRedirect.ts

@@ -4,19 +4,27 @@ export const useRedirect = () => {
   const runtimeConfig = useRuntimeConfig()
 
   const redirectToLogout = () => {
-    if (!runtimeConfig.baseUrlAdminLegacy) {
+    const baseUrl =
+      runtimeConfig.baseUrlAdminLegacy ??
+      runtimeConfig.public.baseUrlAdminLegacy
+
+    if (!baseUrl) {
       throw new Error('Configuration error : no redirection target')
     }
-    navigateTo(UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/logout'), {
+    navigateTo(UrlUtils.join(baseUrl, '#/logout'), {
       external: true,
     })
   }
 
   const redirectToHome = () => {
-    if (!runtimeConfig.baseUrlAdminLegacy) {
+    const baseUrl =
+      runtimeConfig.baseUrlAdminLegacy ??
+      runtimeConfig.public.baseUrlAdminLegacy
+
+    if (!baseUrl) {
       throw new Error('Configuration error : no redirection target')
     }
-    navigateTo(UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/dashboard'), {
+    navigateTo(UrlUtils.join(baseUrl, '#/dashboard'), {
       external: true,
     })
   }

+ 9 - 0
config/abilities/pages/billing.yaml

@@ -93,3 +93,12 @@ afi_page:
         function: accessHasAnyRoleAbility,
         parameters: [{ action: 'manage', subject: 'billings-administration' }],
       }
+
+sdd_regie_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['SddRegie'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'manage', subject: 'billings-administration' }],
+      }

+ 33 - 0
config/abilities/pages/parameters.yaml

@@ -254,3 +254,36 @@ import_page:
       parameters:
         - { action: 'manage', subject: 'user' }
         - { action: 'manage', subject: 'equipments' }
+
+parcours_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['PedagogicsAdministation'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'pedagogics-administration' }],
+      }
+
+family_quotient_models_page:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAnyModule,
+        parameters: ['BillingAdministration'],
+      }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'billings-seizure' }],
+      }
+
+pseudonymization_page:
+  action: 'display'
+  conditions:
+    - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+    - {
+        function: accessHasAnyRoleAbility,
+        parameters: [{ action: 'read', subject: 'organization' }],
+      }

+ 2 - 2
env/setupEnv.mjs

@@ -32,8 +32,8 @@ const environments = {
   ci: '.env.ci',
 }
 
-if (!environments.hasOwnProperty(hostname)) {
-  throw Error('Critical : unknown environment [' + hostname + ']')
+if (!Object.prototype.hasOwnProperty.call(environments, hostname)) {
+  throw new Error('Critical : unknown environment [' + hostname + ']')
 }
 
 const targetEnvFile = path.join(projectDir, 'env', environments[hostname])

+ 41 - 40
eslint.config.mjs

@@ -1,21 +1,15 @@
-import vue from 'eslint-plugin-vue'
-import typescriptEslint from '@typescript-eslint/eslint-plugin'
 import globals from 'globals'
-import parser from 'vue-eslint-parser'
-import path from 'node:path'
-import { fileURLToPath } from 'node:url'
-import js from '@eslint/js'
-import { FlatCompat } from '@eslint/eslintrc'
+import withNuxt from './.nuxt/eslint.config.mjs'
 
-const __filename = fileURLToPath(import.meta.url)
-const __dirname = path.dirname(__filename)
-const compat = new FlatCompat({
-  baseDirectory: __dirname,
-  recommendedConfig: js.configs.recommended,
-  allConfig: js.configs.all,
-})
+// Polyfill for structuredClone if it doesn't exist
+if (typeof structuredClone !== 'function') {
+  globalThis.structuredClone = function (obj) {
+    return JSON.parse(JSON.stringify(obj))
+  }
+}
 
-export default [
+// Configuration de base personnalisée
+const customConfig = [
   {
     ignores: [
       '**/.nuxt',
@@ -25,21 +19,7 @@ export default [
       'models/models.ts',
     ],
   },
-  ...compat.extends(
-    '@nuxtjs/eslint-config-typescript',
-    'plugin:nuxt/recommended',
-    'eslint:recommended',
-    'plugin:@typescript-eslint/recommended',
-    'plugin:vue/vue3-recommended',
-    'plugin:prettier/recommended',
-    'plugin:you-dont-need-lodash-underscore/compatible',
-  ),
   {
-    plugins: {
-      vue,
-      '@typescript-eslint': typescriptEslint,
-    },
-
     languageOptions: {
       globals: {
         ...globals.browser,
@@ -58,29 +38,50 @@ export default [
         watch: 'readonly',
         useRepo: 'readonly',
       },
-
-      parser: parser,
-      ecmaVersion: 2020,
-      sourceType: 'module',
-
-      parserOptions: {
-        parser: '@typescript-eslint/parser',
-        tsconfigRootDir: '/home/workspace',
-      },
     },
 
     rules: {
       'no-console': 0,
-
       'vue/valid-v-slot': [
         'error',
         {
           allowModifiers: true,
         },
       ],
-
       'vue/multi-word-component-names': 0,
       '@typescript-eslint/no-inferrable-types': 0,
+      '@typescript-eslint/no-extraneous-class': 0,
+    },
+  },
+  // Directory-specific configurations
+  {
+    files: ['**/*.vue'],
+    rules: {
+      '@typescript-eslint/no-unused-vars': 0,
+    },
+  },
+  {
+    files: ['layouts/**/*.vue'],
+    rules: {
+      'vue/multi-word-component-names': 0,
+    },
+  },
+  {
+    files: ['pages/**/*.vue'],
+    rules: {
+      'vue/multi-word-component-names': 0,
+    },
+  },
+  {
+    files: ['tests/**/*'],
+    rules: {
+      '@typescript-eslint/ban-ts-comment': 0,
+      '@typescript-eslint/no-unused-vars': 0,
+      '@typescript-eslint/no-explicit-any': 0,
+      'require-await': 0,
     },
   },
 ]
+
+// Utiliser withNuxt avec l'option standalone: false pour éviter les conflits
+export default withNuxt(customConfig)

+ 54 - 28
i18n/lang/fr.json

@@ -20,6 +20,7 @@
   "access_rewards_command": "Commande de distinctions",
   "access_rewards": "Distinctions",
   "afi_export": "Export AFI",
+  "sdd_regie_export": "Export prélèvements SDD Régie",
   "item": "Détails",
   "organization_breadcrumbs": "Fiche de la structure",
   "subscription_breadcrumbs": "Mon abonnement",
@@ -39,7 +40,7 @@
   "subdomain_need_to_have_0_to_60_cars": "Le sous-domaine doit comporter de 2 à 60 caractères",
   "this_subdomain_is_already_in_use": "Ce sous-domaine est déjà utilisé",
   "this_subdomain_is_available": "Ce sous-domaine est disponible",
-  "subdomain_can_not_contain_spaces_or_special_cars": "Le sous-domaine ne doit pas contenir d'espaces ni de caractères spéciaux",
+  "subdomain_can_not_contain_spaces_caps_or_special_cars": "Le sous-domaine ne doit pas contenir d'espaces, de majuscules ni de caractères spéciaux",
   "please_enter_a_value": "Veuillez saisir une valeur",
   "please_enter_a_value_for_the_subdomain": "Veuillez saisir une valeur pour le sous-domaine",
   "validation_ongoing": "Validation en cours",
@@ -195,37 +196,44 @@
   "OTHER": "Autre",
   "CONTACT": "Contact",
   "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",
+  "teaching_cycles": "Cycles d'enseignement",
+  "timing_title": "Durée des cours",
+  "timing": "Durée des cours (en minutes)",
+  "educationTiming": "Durée des cours (en minutes)",
+  "new_education_timings": "Nouvelle durée de cours",
   "superAdmin": "Compte super-admin",
   "username": "Nom d'utilisateur",
   "residenceArea": "Zones de résidence",
-  "deactivateOpentalentSiteWeb": "Désactiver le site opentalent",
+  "activateOpentalentSiteWeb": "Activer le site Opentalent",
+  "deactivateOpentalentSiteWeb": "Désactiver le site Opentalent",
   "reactivateOpentalentSiteWeb": "Réactiver le site Opentalent",
   "passwordSMS": "Mot de passe SMS",
   "usernameSMS": "Nom d'utilisateur SMS",
-  "smsSenderName": "Personnaliser le nom de l'expéditeur SMS",
+  "smsSenderName": "Nom d'expéditeur SMS personnalisé",
   "attendance": "Absences",
   "parameters_attendances_page": "Absences",
-  "attendanceBookingReason": "Motif d'absence / retard",
-  "attendanceBookingReasons": "Motifs d'absence / retard",
-  "new_attendance_booking_reason": "Nouveau motif d'absence / retard",
+  "alert_configuration": "Configuration des alertes",
+  "attendanceBookingReason": "Motif d'absence ou de retard",
+  "attendanceBookingReasons": "Motifs d'absence ou de retard",
+  "new_attendance_booking_reason": "Nouveau motif d'absence ou de retard",
   "reason": "Motif",
   "notifyAdministrationAbsence": "Prévenir l'administrateur en cas d'absences consécutives",
+  "numberConsecutiveAbsences": "Nombre d'absences consécutives",
   "sendAttendanceEmail": "Prévenir automatiquement la famille par mail en cas d'absence non justifiée",
   "sendAttendanceSms": "Prévenir automatiquement la famille par sms en cas d'absence non justifiée",
   "bulletinReceiver": "Adresser le bulletin à",
-  "bulletinEditWithoutEvaluation": "Editer également les bulletins ne contenant aucune évaluation",
-  "bulletinShowAverages": "Afficher les moyennes",
-  "bulletinShowAbsences": "Afficher les absences",
-  "bulletinViewTestResults": "Afficher les résultats des examens",
-  "bulletinShowEducationWithoutEvaluation": "Afficher les enseignements ne contenant aucune évaluation",
-  "bulletinDisplayLevelAcquired": "Affichage niveau acquis",
-  "bulletinSignatureDirector": "Un cadre « Tampon / Signature » pour l'administration",
-  "bulletinPrintAddress": "L'adresse postale de l'élève ou son tuteur",
-  "bulletinWithTeacher": "Le nom du professeur",
+  "bulletinEditWithoutEvaluation": "Éditer également les bulletins ne contenant aucune évaluation",
+  "bulletinShowAverages": "Moyennes",
+  "bulletinShowAbsences": "Absences",
+  "bulletinViewTestResults": "Résultats des examens",
+  "bulletinShowEducationWithoutEvaluation": "Enseignements ne contenant aucune évaluation",
+  "bulletinDisplayLevelAcquired": "Niveau acquis",
+  "bulletinSignatureDirector": "Cadre « Tampon / Signature » pour l'administration",
+  "bulletinPrintAddress": "Adresse postale de l'élève ou son tuteur",
+  "bulletinWithTeacher": "Nom du professeur",
   "bulletinCriteriaSort": "Ordre de tri des critères",
+  "itemsToDisplayOnBulletins": "Eléments à afficher sur les bulletins",
+  "bulletinSettings": "Configuration",
   "superAdminEmail": "Adresse mail associée",
   "bulletin_parameters": "Bulletins",
   "sms": "Sms",
@@ -239,11 +247,12 @@
   "otherWebsite": "Autre site web",
   "newSubDomain": "Nouveau sous domaine",
   "yourSubdomains": "Vos sous-domaines",
+  "a_short_subdomains_definition": "Le sous-domaine est la première partie de l'adresse de votre site web Opentalent.",
   "timezone": "Fuseau horaire",
   "qrCode": "QrCode",
   "qrCodeForLicence": "QrCode pour la licence",
   "studentsAreAdherents": "Les élèves sont également adhérents de l'association",
-  "showAdherentList": "Afficher la liste des adhérents et leurs coordonnées",
+  "showAdherentList": "Autoriser l'affichage de la liste des adhérents de votre structure, avec leurs coordonnées, dans le compte utilisateur de vos membres.",
   "endCourseDate": "Date de fin des cours ",
   "startCourseDate": "Date de début des cours ",
   "generalParams": "Paramètres généraux",
@@ -505,6 +514,7 @@
   "advanced_modification": "Administration site internet",
   "simple_modification": "Modifications simplifiées",
   "create": "Créer",
+  "edit": "Modifier",
   "help_access": "Accès aide",
   "configuration": "Configuration",
   "organization_page": "Fiche de la structure",
@@ -514,6 +524,10 @@
   "cmf_licence_details_url": "Consulter les avantages de la licence CMF",
   "generate": "Générer",
   "parameters": "Préférences",
+  "parcours": "Parcours",
+  "family_quotient_models": "Modèles de quotients familiaux",
+  "pseudonymization": "Pseudonymisation",
+  "parameters_page": "Préférences",
   "places": "Lieux",
   "education": "Enseignements",
   "tags": "Tags",
@@ -633,7 +647,7 @@
   "start_date_of_activity_season": "Début de saison d'activité",
   "start_date_of_courses": "Date de début des cours",
   "end_date_of_courses": "Date de fin des cours",
-  "show_adherents_list_and_their_coordinates": "Afficher la liste des adhérents et leurs coordonnées",
+  "show_adherents_list_and_their_coordinates": "Autoriser l'affichage de la liste des adhérents de votre structure, avec leurs coordonnées, dans le compte utilisateur de vos membres.",
   "students_are_also_association_members": "Les élèves sont adhérents également de l'association",
   "parameters_general_page": "Paramètres généraux",
   "general_parameters_breadcrumbs": "Paramètres généraux",
@@ -655,14 +669,20 @@
   "parameters_residence_areas_page": "Zones de résidence",
   "parameters_sms_page": "Option SMS",
   "sms_option": "Option SMS",
+  "sms_option_configuration": "Configuration de l'option SMS",
+  "sms_option_configuration_notice": "Pour utiliser l'option SMS, renseignez les informations d'identification Mobyt de votre structure",
+  "sms_option_configuration_tip": "Pour utiliser l'option SMS, renseignez les informations d'identification Mobyt de votre structure",
   "sms_breadcrumbs": "SMS",
   "super_admin": "Compte super-admin",
   "parameters_super_admin_page": "Compte super-admin",
   "super_admin_breadcrumbs": "Compte super-admin",
   "an_error_happened": "Une erreur s'est produite",
-  "your_opentalent_website_address_is": "L'adresse de votre site Opentalent est",
+  "your_website": "Votre site web",
+  "your_website_address_is": "L'adresse de votre site internet est",
   "record_a_new_subdomain": "Enregistrer un nouveau sous-domaine",
+  "record_a_new_subdomain_short": "Nouveau sous-domaine",
   "your_subdomains": "Vos sous-domaines",
+  "other_website": "Autre site internet",
   "Not Found": "Données non trouvée",
   "subdomains_breadcrumbs": "Sous-domaines",
   "new_breadcrumbs": "Nouveau",
@@ -672,10 +692,11 @@
   "This organization can not register new subdomains": "Nombre maximum de sous-domaines enregistrés atteint",
   "This subdomain is not available": "Ce sous-domaine n'est pas disponible",
   "This subdomain is already registered": "Ce sous-domaine est déjà enregistré",
+  "activate_a_subdomain": "Activer un sous-domaine",
   "subdomain_activated_and_available_in_a_few_minutes": "Le sous-domaine a bien été activé, et sera accessible d'ici quelques minutes",
   "unknown": "Inconnu",
   "allow_teachers_to_generate_attendance_reports": "Autoriser les professeurs à générer des fiches de présence",
-  "send_teachers_mail_reports_copy_to_administration": "Mettre l'administration en copie du rapport d'envoi des mails envoyés par les professeurs dans le logiciel",
+  "send_teachers_mail_reports_copy_to_administration": "Envoyer à l'administration une copie par email de chaque message envoyé par les professeurs.",
   "allow_members_to_change_their_names_and_firstnames": "Autoriser les membres à modifier leur nom et prénom",
   "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",
@@ -688,11 +709,11 @@
   "OUT_CYCLE": "Hors cycle",
   "originalLabel": "Libellés d'origine",
   "effectiveLabel": "Libellés actuellement utilisés",
-  "allow_to_configure_teachings_with_played_instrument_choice": "Permettre de configurer les enseignements avec le choix sur l'instrument joué",
+  "allow_to_configure_teachings_with_played_instrument_choice": "Permettre de configurer un enseignement comme une pratique collective, avec précision sur l'activité de l'élève",
   "label": "Libellé",
   "undefined": "Indéfini",
-  "define_validation_periods_for_teachers": "Définir des périodes de saisie pour les professeurs",
-  "mandatory_validation_for_evaluations": "Valider obligatoirement les évaluations",
+  "define_validation_periods_for_teachers": "Définir les périodes de saisie des évaluations pour les professeurs",
+  "mandatory_validation_for_evaluations": "Valider obligatoirement les évaluations pour qu'elles soient visibles par les élèves",
   "evaluation_criterium_edition_is_admin_only": "Autoriser uniquement l'administration à modifier les critères d'évaluation",
   "max_note_for_pedagogical_followup": "Note maximale pour les notes du suivi pédagogique (entre 1 et 100) ",
   "Bad Request": "Requête invalide",
@@ -705,6 +726,7 @@
   "education_timings_breadcrumbs": "Durée des cours",
   "create_a_new_residence_area": "Créer une nouvelle zone de résidence",
   "residence_areas_breadcrumbs": "Zones de résidence",
+  "edit_resident_area": "Éditer la zone de résidence",
   "super_admin_explanation_text": "Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise entre autre pour la gestion de votre site internet, pour créer les comptes des membres de votre structure à la première connexion au logiciel, ou dans des situations de dépannage.",
   "exit": "Quitter",
   "max_size_4_mb": "Taille maximum: 4 MO",
@@ -721,9 +743,12 @@
   "id": "Id",
   "missing_name": "Nom manquant",
   "warning": "Avertissement",
+  "show_warnings": "Afficher les avertissements",
   "please_enter_a_value_for_the_sms_sender_name": "Le nom d'expediteur ne doit pas comporter plus de 11 caractères, et être composé uniquement de chiffres et/ou de lettres.",
-  "associated_email": "Adresse Email associée",
-  "An error occured": "Une erreur s'est produite.",
+  "associated_email": "Adresse email associée",
+  "homepage": "Accueil",
+  "go_back_home": "Revenir à l'accueil",
+  "passwordSMS: Invalid Mobyt credentials": "Identifiants SMS non reconnus",
   "you_want_to_stop_your_premium_trial_period": "Vous souhaitez arrêter votre période d’essai Opentalent Artist Premium",
   "stop_trial_period_warning_1a": "En choisissant d’arrêter votre période d'essai, votre compte reviendra automatiquement à la version",
   "stop_trial_period_warning_1b": "sans perte de vos données essentielles.",
@@ -737,5 +762,6 @@
   "trial_ongoing": "En cours d'essai",
   "try_premium_version": "Essayer la version premium",
   "subscribe_to_the_offer": "Souscrire à l'offre",
-  "to_know_more": "En savoir plus"
+  "to_know_more": "En savoir plus",
+  "placeListMenuKey": "Lieu"
 }

+ 0 - 6
layouts/.eslintrc.cjs

@@ -1,6 +0,0 @@
-/** 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: {
-    'vue/multi-word-component-names': 0,
-  },
-}

+ 1 - 1
layouts/default.vue

@@ -20,7 +20,7 @@
       <v-main class="main">
         <LayoutSubheader />
 
-        <LayoutAlertBar class="mt-1" />
+        <LayoutAlertBar />
 
         <!-- Page will be rendered here-->
         <slot />

+ 1 - 1
layouts/error.vue

@@ -21,7 +21,7 @@ const props = defineProps({
 })
 
 if (
-  process.client &&
+  import.meta.client &&
   props.error.statusCode === 404 &&
   process.env.NODE_ENV === 'production'
 ) {

+ 17 - 8
layouts/parameters.vue

@@ -13,12 +13,14 @@
       <v-main class="main">
         <LayoutSubheader />
 
-        <LayoutAlertBar class="mt-1" />
+        <LayoutAlertBar />
 
         <!-- Page will be rendered here-->
-        <v-card class="parameters-page-card">
+        <div class="inner-container">
+          <h3>{{ pageTitle }}</h3>
+
           <slot />
-        </v-card>
+        </div>
       </v-main>
 
       <LazyLayoutAlertContainer />
@@ -31,13 +33,20 @@ import { useLayoutStore } from '~/stores/layout'
 
 const layoutStore = useLayoutStore()
 layoutStore.name = 'parameters'
+
+const route = useRoute()
+const i18n = useI18n()
+
+const pageTitle = computed(() => i18n.t(route.name || 'parameters_page'))
 </script>
 
 <style scoped lang="scss">
-.parameters-page-card {
-  background-color: rgb(var(--v-theme-neutral-very-soft));
-  color: rgb(var(--v-theme-on-neutral-very-soft));
-  margin: 3%;
-  padding: 24px;
+.inner-container {
+  max-width: 1200px;
+  margin: 0 auto;
+
+  h3 {
+    margin: 36px 0 18px 2%;
+  }
 }
 </style>

+ 2 - 2
models/Access/MyProfile.ts

@@ -1,7 +1,7 @@
 import { Num, Uid, Attr, Bool, Str } from 'pinia-orm/dist/decorators'
 import type { Historical } from '~/types/interfaces'
-import Access from '~/models/Access/Access'
-import OrganizationProfile from '~/models/Organization/OrganizationProfile'
+import type Access from '~/models/Access/Access'
+import type OrganizationProfile from '~/models/Organization/OrganizationProfile'
 import ApiResource from '~/models/ApiResource'
 
 /**

+ 0 - 1
models/ApiResource.ts

@@ -4,7 +4,6 @@ import { Model } from 'pinia-orm'
  * Base class for resources that can be fetched from the API
  */
 class ApiResource extends Model {
-  // eslint-disable-next-line no-use-before-define
   protected static _iriEncodedFields: Record<string, ApiResource>
   protected static _idField: string
 

+ 1 - 1
models/Core/Notification.ts

@@ -1,5 +1,5 @@
 import { Str, Uid, Attr } from 'pinia-orm/dist/decorators'
-import NotificationMessage from '~/models/Core/NotificationMessage'
+import type NotificationMessage from '~/models/Core/NotificationMessage'
 import ApiModel from '~/models/ApiModel'
 
 /**

+ 1 - 5
models/Custom/Search/UserSearchItem.ts

@@ -1,9 +1,5 @@
-import { Num, Uid, Attr, Str } from 'pinia-orm/dist/decorators'
-import type { Historical } from '~/types/interfaces'
-import Person from '~/models/Person/Person'
+import { Uid, Str } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import { IriEncoded } from '~/models/decorators'
-import Organization from '~/models/Organization/Organization'
 
 /**
  * AP2i Model : UserSearchItem

+ 1 - 1
models/Organization/Parameters.ts

@@ -40,7 +40,7 @@ export default class Parameters extends ApiModel {
   declare logoDonorsMove: boolean
 
   @Str(null)
-  declare otherWebsite: string | null
+  declare website: string | null
 
   @Str(null)
   declare customDomain: string | null

+ 3 - 5
models/decorators.ts

@@ -1,4 +1,4 @@
-import ApiResource from '~/models/ApiResource'
+import type ApiResource from '~/models/ApiResource'
 
 /**
  * Decorates an ApiResource's property to signal it as a field that is provided
@@ -9,8 +9,7 @@ import ApiResource from '~/models/ApiResource'
  */
 export function IriEncoded(apiResource: typeof ApiResource): PropertyDecorator {
   // We have to comply with the PropertyDecorator return type
-  // eslint-disable-next-line @typescript-eslint/ban-types
-  return (target: Object, propertyKey: string | symbol) => {
+  return (target: object, propertyKey: string | symbol) => {
     // @ts-expect-error The object is an ApiResource
     const self = target.$self()
 
@@ -27,8 +26,7 @@ export function IriEncoded(apiResource: typeof ApiResource): PropertyDecorator {
  */
 export function IdField(): PropertyDecorator {
   // We have to comply with the PropertyDecorator return type
-  // eslint-disable-next-line @typescript-eslint/ban-types
-  return (target: Object, propertyKey: string | symbol) => {
+  return (target: object, propertyKey: string | symbol) => {
     // @ts-expect-error The object is an ApiResource
     const self = target.$self()
 

+ 2 - 2
nuxt.config.ts

@@ -130,6 +130,7 @@ export default defineNuxtConfig({
     '@/assets/css/theme.scss',
     '@/assets/css/import.scss',
     '@vuepic/vue-datepicker/dist/main.css',
+    '@/assets/css/vue-date-picker.scss',
   ],
 
   typescript: {
@@ -137,7 +138,6 @@ export default defineNuxtConfig({
   },
 
   modules: [
-    // eslint-disable-next-line require-await
     async (_, nuxt) => {
       nuxt.hooks.hook('vite:extendConfig', (config) =>
         // @ts-expect-error A revoir après que les lignes aient été décommentées
@@ -169,8 +169,8 @@ export default defineNuxtConfig({
     '@nuxt/image',
     'nuxt-prepare',
     'nuxt-vitalizer',
+    '@nuxt/eslint',
   ],
-
   vite: {
     esbuild: {
       drop: process.env.DEBUG ? [] : ['console', 'debugger'],

+ 4 - 5
package.json

@@ -28,11 +28,13 @@
     "@casl/vue": "2.2.2",
     "@fortawesome/fontawesome-free": "^6.7.2",
     "@mdi/font": "^7.4.47",
+    "@nuxt/eslint": "1.4.1",
     "@nuxt/image": "1.9.0",
+    "@nuxtjs/eslint-config-typescript": "^12.1.0",
     "@nuxtjs/i18n": "^9.1.3",
     "@pinia-orm/nuxt": "^1.10.1",
     "@pinia/nuxt": "^0.5.1",
-    "@vuepic/vue-datepicker": "^7.4.0",
+    "@vuepic/vue-datepicker": "^11.0",
     "cleave.js": "^1.6.0",
     "date-fns": "^4.1.0",
     "event-source-polyfill": "^1.0.31",
@@ -51,6 +53,7 @@
     "uuid": "^9.0.1",
     "vite-plugin-vuetify": "^2.0.4",
     "vue-advanced-cropper": "^2.8.9",
+    "vue-matomo": "^4.2.0",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
     "vuetify": "3.6.14",
@@ -60,9 +63,6 @@
     "@nuxt/devtools": "^1.7.0",
     "@nuxt/test-utils": "^3.15.4",
     "@nuxt/test-utils-edge": "3.8.0-28284309.b3d3d7f4",
-    "@nuxtjs/eslint-config": "^12.0.0",
-    "@nuxtjs/eslint-config-typescript": "^12.1.0",
-    "@nuxtjs/eslint-module": "^4.1.0",
     "@types/cleave.js": "^1.4.12",
     "@types/event-source-polyfill": "^1.0.5",
     "@types/file-saver": "^2.0.7",
@@ -76,7 +76,6 @@
     "@typescript-eslint/parser": "^8.22.0",
     "@vitejs/plugin-vue": "^5.2.1",
     "@vitest/coverage-v8": "3.0.4",
-    "@vue/eslint-config-standard": "^9.0.0",
     "@vue/test-utils": "^2.4.6",
     "blob-polyfill": "^9.0.20240710",
     "eslint": "^9.19.0",

+ 0 - 6
pages/.eslintrc.cjs

@@ -1,6 +0,0 @@
-/** 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: {
-    'vue/multi-word-component-names': 0,
-  },
-}

+ 8 - 5
pages/parameters/attendance_booking_reasons/[id].vue

@@ -1,7 +1,6 @@
 <template>
-  <LayoutContainer>
-    <div>
-      <h2>{{ $t('attendanceBookingReason') }}</h2>
+  <div>
+    <LayoutParametersSection>
       <UiFormEdition
         :model="AttendanceBookingReason"
         go-back-route="/parameters/attendances"
@@ -14,13 +13,17 @@
           />
         </template>
       </UiFormEdition>
-    </div>
-  </LayoutContainer>
+    </LayoutParametersSection>
+  </div>
 </template>
 <script setup lang="ts">
 import { useI18n } from 'vue-i18n'
 import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 
+definePageMeta({
+  name: 'attendanceBookingReason',
+})
+
 const i18n = useI18n()
 
 const rules = () => [

+ 9 - 6
pages/parameters/attendance_booking_reasons/new.vue

@@ -1,7 +1,6 @@
 <template>
-  <LayoutContainer>
-    <div>
-      <h2>{{ $t('new_attendance_booking_reason') }}</h2>
+  <div>
+    <LayoutParametersSection>
       <UiFormCreation
         :model="AttendanceBookingReason"
         go-back-route="/parameters/attendances"
@@ -9,7 +8,7 @@
         <template #default="{ entity }">
           <v-container :fluid="true" class="container">
             <v-row>
-              <v-col cols="12" sm="6"> </v-col>
+              <v-col cols="12" sm="6" />
             </v-row>
             <v-row>
               <v-col cols="12" sm="6">
@@ -23,14 +22,18 @@
           </v-container>
         </template>
       </UiFormCreation>
-    </div>
-  </LayoutContainer>
+    </LayoutParametersSection>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { useI18n } from 'vue-i18n'
 import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 
+definePageMeta({
+  name: 'new_attendance_booking_reason',
+})
+
 const i18n = useI18n()
 
 const rules = () => [

+ 14 - 11
pages/parameters/attendances.vue

@@ -1,6 +1,7 @@
 <template>
-  <LayoutContainer>
-    <div v-if="organizationProfile.isSchool">
+  <div>
+    <LayoutParametersSection v-if="organizationProfile.isSchool">
+      <h4>{{ $t('alert_configuration') }}</h4>
       <UiLoadingPanel v-if="pending" />
       <UiForm v-else-if="parameters !== null" v-model="parameters">
         <v-row>
@@ -22,22 +23,24 @@
             />
 
             <UiInputNumber
+              v-if="parameters.notifyAdministrationAbsence"
               v-model="parameters.numberConsecutiveAbsences"
-              field="notifyAdministrationAbsence"
+              field="numberConsecutiveAbsences"
               :rules="rules()"
             />
           </v-col>
         </v-row>
       </UiForm>
+    </LayoutParametersSection>
 
-      <v-divider class="my-10" />
-    </div>
-
-    <LayoutParametersEntityTable
-      :model="AttendanceBookingReason"
-      :columns-definitions="[{ property: 'reason' }]"
-    />
-  </LayoutContainer>
+    <LayoutParametersSection>
+      <LayoutParametersEntityTable
+        :model="AttendanceBookingReason"
+        :title="$t('attendanceBookingReasons')"
+        :columns-definitions="[{ property: 'reason' }]"
+      />
+    </LayoutParametersSection>
+  </div>
 </template>
 <script setup lang="ts">
 import type { AsyncData } from '#app'

部分文件因文件數量過多而無法顯示