Olivier Massot 4 лет назад
Родитель
Сommit
2e9d116dd7
100 измененных файлов с 3737 добавлено и 1143 удалено
  1. 19 0
      .env.local
  2. 19 0
      .env.preprod
  3. 20 0
      .env.prod
  4. 5 0
      .gitignore
  5. 15 0
      .gitlab-ci.yml
  6. BIN
      .output/public/images/Artist-Square.jpg
  7. BIN
      .output/public/images/School-Square.jpg
  8. BIN
      .output/public/images/nom-de-domaine.jpg
  9. BIN
      .output/public/images/sms_big.png
  10. 0 0
      .output/public/robots.txt
  11. 109 16
      README.md
  12. 12 0
      assets/css/global.scss
  13. 3 0
      assets/css/variables.scss
  14. BIN
      assets/images/byDefault/default_pic.jpeg
  15. BIN
      assets/images/byDefault/men-1.png
  16. BIN
      assets/images/byDefault/women-1.png
  17. 18 14
      components/Layout/Alert/Container.vue
  18. 43 42
      components/Layout/Alert/Content.vue
  19. 122 0
      components/Layout/AlertBar/Cotisation.vue
  20. 24 0
      components/Layout/AlertBar/Env.vue
  21. 43 0
      components/Layout/AlertBar/SuperAdmin.vue
  22. 36 0
      components/Layout/AlertBar/SwitchUser.vue
  23. 57 0
      components/Layout/AlertBar/SwitchYear.vue
  24. 33 0
      components/Layout/Alertbar.vue
  25. 6 3
      components/Layout/BannerTop.vue
  26. 5 2
      components/Layout/Container.vue
  27. 14 12
      components/Layout/Dialog.vue
  28. 84 53
      components/Layout/Header.vue
  29. 102 0
      components/Layout/Header/Menu.vue
  30. 259 0
      components/Layout/Header/Notification.vue
  31. 0 62
      components/Layout/HeaderMenu.vue
  32. 48 0
      components/Layout/Loading.vue
  33. 30 26
      components/Layout/Menu.vue
  34. 0 48
      components/Layout/Notification.vue
  35. 62 0
      components/Layout/SubHeader/ActivityYear.vue
  36. 48 0
      components/Layout/SubHeader/Breadcrumbs.vue
  37. 93 0
      components/Layout/SubHeader/DataTiming.vue
  38. 108 0
      components/Layout/SubHeader/DataTimingRange.vue
  39. 120 0
      components/Layout/SubHeader/PersonnalizedList.vue
  40. 44 0
      components/Layout/Subheader.vue
  41. 80 0
      components/Ui/Button/Delete.vue
  42. 47 4
      components/Ui/Card.vue
  43. 97 94
      components/Ui/DataTable.vue
  44. 32 23
      components/Ui/ExpansionPanel.vue
  45. 113 141
      components/Ui/Form.vue
  46. 83 0
      components/Ui/Image.vue
  47. 95 0
      components/Ui/Input/Autocomplete.vue
  48. 120 0
      components/Ui/Input/AutocompleteWithAPI.vue
  49. 34 26
      components/Ui/Input/Checkbox.vue
  50. 105 64
      components/Ui/Input/DatePicker.vue
  51. 82 0
      components/Ui/Input/Email.vue
  52. 58 48
      components/Ui/Input/Enum.vue
  53. 99 0
      components/Ui/Input/Phone.vue
  54. 63 51
      components/Ui/Input/Text.vue
  55. 106 0
      components/Ui/Map.vue
  56. 53 53
      components/Ui/SubList.vue
  57. 31 0
      components/Ui/SystemBar.vue
  58. 77 0
      components/Ui/Xeditable/Text.vue
  59. 8 0
      config/abilities/pages/educational.yaml
  60. 100 0
      config/abilities/pages/myAccount.yaml
  61. 8 0
      config/abilities/pages/parameters.yaml
  62. 8 0
      config/abilities/pages/stats.yaml
  63. 10 11
      config/nuxtConfig/build.js
  64. 13 10
      config/nuxtConfig/env.js
  65. 7 6
      config/nuxtConfig/head.js
  66. 5 2
      config/nuxtConfig/modules.js
  67. 2 0
      config/nuxtConfig/plugins.js
  68. 8 3
      config/nuxtConfig/vuetify.js
  69. 13 0
      ecosystem.config.js
  70. 4 1
      jest.config.js
  71. 9 0
      lang/breadcrumbs/fr-FR.js
  72. 61 52
      lang/enum/fr-FR.js
  73. 61 53
      lang/field/fr-FR.js
  74. 8 1
      lang/form/fr-FR.js
  75. 5 1
      lang/fr-FR.js
  76. 72 4
      lang/layout/fr-FR.js
  77. 7 0
      lang/menuKey/fr-FR.js
  78. 4 2
      lang/rulesAndErrors/fr-FR.js
  79. 39 28
      layouts/default.vue
  80. 20 28
      layouts/error.vue
  81. 5 5
      layouts/login.vue
  82. 1 1
      middleware/auth.ts
  83. 15 0
      models/Access/MyProfile.ts
  84. 17 0
      models/Access/PersonalizedList.ts
  85. 12 12
      models/Core/AddressPostal.ts
  86. 9 9
      models/Core/BankAccount.ts
  87. 22 7
      models/Core/ContactPoint.ts
  88. 3 3
      models/Core/Country.ts
  89. 24 0
      models/Core/Notification.ts
  90. 17 0
      models/Core/NotificationMessage.ts
  91. 17 0
      models/Core/NotificationUsers.ts
  92. 0 6
      models/Model.ts
  93. 38 39
      models/Organization/Organization.ts
  94. 5 5
      models/Organization/OrganizationAddressPostal.ts
  95. 16 0
      models/Organization/OrganizationContactPoint.ts
  96. 7 7
      models/Organization/OrganizationLicence.ts
  97. 7 7
      models/Organization/OrganizationNetwork.ts
  98. 1 2
      nuxt.config.js
  99. 61 47
      package.json
  100. 12 9
      pages/index.vue

+ 19 - 0
.env.local

@@ -0,0 +1,19 @@
+## LOCAL ENVIRONMENT FILE
+NODE_ENV=dev
+DEBUG=1
+
+## API Base Url
+SSR_API_BASE_URL=http://nginx_new
+CLIENT_API_BASE_URL=https://local.new.api.opentalent.fr
+
+# Legacy API Base Url
+SSR_APILEG_BASE_URL=http://nginx
+CLIENT_APILEG_BASE_URL=https://local.api.opentalent.fr
+
+# Legacy Admin Base Url
+SSR_ADMINLEG_BASE_URL=https://local.admin.opentalent.fr/#
+CLIENT_ADMINLEG_BASE_URL=https://local.admin.opentalent.fr/#
+
+# Typo3 Base Url
+SSR_TYPO3_BASE_URL=https://local.sub.opentalent.fr/###subDomain###
+CLIENT_TYPO3_BASE_URL=https://local.sub.opentalent.fr/###subDomain###

+ 19 - 0
.env.preprod

@@ -0,0 +1,19 @@
+## PREPROD ENVIRONMENT FILE
+NODE_ENV=production
+DEBUG=1
+
+## API Base Url
+SSR_API_BASE_URL=https://ap2i.preprod.opentalent.fr
+CLIENT_API_BASE_URL=https://ap2i.preprod.opentalent.fr
+
+# Legacy API Base Url
+SSR_APILEG_BASE_URL=https://api.preprod.opentalent.fr
+CLIENT_APILEG_BASE_URL=https://api.preprod.opentalent.fr
+
+# Legacy Admin Base Url
+SSR_ADMINLEG_BASE_URL=https://admin.preprod.opentalent.fr/#
+CLIENT_ADMINLEG_BASE_URL=https://admin.preprod.opentalent.fr/#
+
+# Typo3 Base Url
+SSR_TYPO3_BASE_URL=https://preprod.opentalent.fr/###subDomain###
+CLIENT_TYPO3_BASE_URL=https://preprod.opentalent.fr/###subDomain###

+ 20 - 0
.env.prod

@@ -0,0 +1,20 @@
+## PROD ENVIRONMENT FILE
+# /!\ -- USE ONLY IN PRODUCTION --
+NODE_ENV=production
+DEBUG=0
+
+## API Base Url
+SSR_API_BASE_URL = https://ap2i.opentalent.fr
+CLIENT_API_BASE_URL = https://ap2i.opentalent.fr
+
+# Legacy API Base Url
+SSR_APILEG_BASE_URL=https://api.opentalent.fr
+CLIENT_APILEG_BASE_URL=https://api.opentalent.fr
+
+# Legacy Admin Base Url
+SSR_ADMINLEG_BASE_URL=https://admin.opentalent.fr/#
+CLIENT_ADMINLEG_BASE_URL=https://admin.opentalent.fr/#
+
+# Typo3 Base Url
+SSR_TYPO3_BASE_URL=https://###subDomain###.opentalent.fr
+CLIENT_TYPO3_BASE_URL=https://###subDomain###.opentalent.fr

+ 5 - 0
.gitignore

@@ -68,6 +68,9 @@ typings/
 # nuxt.js build output
 .nuxt
 
+# nuxt.js build output
+.output
+
 # Nuxt generate
 dist
 
@@ -88,3 +91,5 @@ sw.*
 
 # Vim swap files
 *.swp
+/.project
+/todo.md

+ 15 - 0
.gitlab-ci.yml

@@ -0,0 +1,15 @@
+stages:
+  - test
+
+before_script:
+  - apt-get update
+  - curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
+  - apt-get install -y nodejs
+  - npm install --global yarn
+  - yarn install
+
+unit:
+  stage: test
+
+  script:
+    - yarn test

BIN
.output/public/images/Artist-Square.jpg


BIN
.output/public/images/School-Square.jpg


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


BIN
.output/public/images/sms_big.png


+ 0 - 0
.output/public/robots.txt


+ 109 - 16
README.md

@@ -1,23 +1,116 @@
-# admin
+# app
 
-## Build Setup
+[![pipeline status](http://gitlab.2iopenservice.com/vincent/admin/badges/master/pipeline.svg)](http://gitlab.2iopenservice.com/opentalent/admin/-/commits/master)
+[![coverage report](http://gitlab.2iopenservice.com/vincent/admin/badges/master/coverage.svg)](http://gitlab.2iopenservice.com/opentalent/admin/-/commits/master)
 
-```bash
-# install dependencies
-$ yarn install
+Frontend développé avec Vue.js 2 + NuxtJs 2
 
-# serve with hot reload at localhost:3000
-$ yarn dev
+A voir:
 
-# build for production and launch server
-$ yarn build
-$ yarn start
+* [vuejs.org](https://vuejs.org/v2/guide/)
+* [nuxtjs.org](https://fr.nuxtjs.org/docs/2.x/get-started/installation)
+* [vuex-orm.org](https://vuex-orm.org/)
+* [vuetifyjs.com](https://vuetifyjs.com/en/)
+* [typescriptlang.org](https://www.typescriptlang.org/)
+* [jestjs.io](https://jestjs.io/docs/getting-started)
+* [cypress.io](https://docs.cypress.io/guides/getting-started/installing-cypress)
+* [pm2.keymetrics.io](https://pm2.keymetrics.io/docs/usage/quick-start//)
 
-# generate static project
-$ yarn generate
 
-# generate doc
-$ yarn docs
-```
+## Opérations courantes
+
+## Déploiement
+
+### Premier déploiement en tant que service
+
+> Prérequis 1 : PM2 doit être installé de manière globale sur la machine hôte (https://nuxtjs.org/deployments/pm2/)
+
+On commence par cloner le projet app, puis par se placer dans le répertoire ainsi créé.
+On créé un symlink vers le fichier .env.xxx voulu sous le nom de .env (selon l'environnement)
+
+    ln -s .env.xxx .env
+
+Pour déployer le projet en mode SSR, on commence par mettre à jour et compiler :
+
+    yarn install
+    yarn build
+
+On lance ensuite le service avec :
+
+    pm2 start
+
+Vérifier le statut avec :
+ 
+    pm2 show app
+
+On pourra ensuite interagir avec le service ainsi :
+
+    pm2 restart app
+    pm2 stop app
+    pm2 logs app
+
+On enregistre le service pour qu'il soit relancé lors du boot de la machine:
+https://pm2.keymetrics.io/docs/usage/startup/
+
+En tant que non-root:
+
+    pm2 startup
+    
+Puis, comme la commande précédente le suggère, on lance **en tant que root**:
+
+    env PATH=$PATH:/usr/bin /usr/local/share/.config/yarn/global/node_modules/pm2/bin/pm2 startup systemd -u exploitation --hp /home/exploitation
+
+On sauvegarde la nouvelle configuration avec: 
+
+    pm2 save
+
+### Mettre à jour
+
+Se placer dans le répertoire de l'application, puis lancer:
+
+    yarn deploy
+
+Cette commande est un alias qui équivaut à lancer:
+
+    git pull
+    yarn install
+    yarn build
+    pm2 restart
+
+## Autres
+
+### Lancer les tests
+
+Pour lancer les tests unitaires:
+
+    jest
+
+### Générer la doc
+
+Pour regénérer la documentation automatique:
+
+    yarn docs
+
+
+## Plus d'infos
+
+## Structure du projet
+
+| Répertoire | Rôle |
+| --- | --- |
+| `assets` | Contient les fichiers style et medias |
+| `components` | Les différents composants graphiques qui composent l'application |
+| `config` | La configuration de l'application |
+| `lang` | Les fichiers de traduction |
+| `layouts` | Layouts des pages |
+| `middleware` | Code exécuté avant le rendu des pages (ex: pour vérifier l'authentification) |
+| `models` | Définition des entités (ou modèles) |
+| `node_modules` | Modules node installés via npm |
+| `pages` | Définition des pages qui composent l'application |
+| `plugins` | ... |
+| `services` | Rassemble les classes utilitaires non graphiques |
+| `static` | Ressources statiques et publiques |
+| `store` | Le store et ses composants servent d'entrepôt de donnés, et s'assurent de la cohérence de celles-ci |
+| `tests` | Regroupe les tests (unitaires, end-to-end...) |
+| `use` | ... |
 
-For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).

+ 12 - 0
assets/css/global.scss

@@ -17,3 +17,15 @@ header .v-toolbar__content{
 .margin-bottom-20{
   margin-bottom: 20px;
 }
+
+.v-application a{
+  color: var(--v-ot_green-base, white)
+}
+
+.header_menu{
+  max-height: 300px;
+  min-width: 300px;
+  overflow-y: scroll
+}
+
+

+ 3 - 0
assets/css/variables.scss

@@ -3,3 +3,6 @@
 // The variables you want to modify
 // $font-size-root: 20px;
 $btn-text-transform: none;
+
+$breadcrumbs-padding: 5px 5px;
+$breadcrumbs-even-child-padding: 0 10px;

BIN
assets/images/byDefault/default_pic.jpeg


BIN
assets/images/byDefault/men-1.png


BIN
assets/images/byDefault/women-1.png


+ 18 - 14
components/Layout/Alert/Container.vue

@@ -1,30 +1,34 @@
+<!--
+Container principal pour l'affichage d'une ou plusieurs alertes
+-->
+
 <template>
   <main class="alertContainer">
     <LayoutAlertContent
-      class="alertContent"
       v-for="(alert, key) in alerts"
       :key="key"
+      class="alertContent"
       :alert="alert"
     />
   </main>
 </template>
 
 <script lang="ts">
-  import {defineComponent, computed} from "@vue/composition-api";
-  import {useContext} from "@nuxtjs/composition-api";
+import { defineComponent, computed, ComputedRef, useContext } from '@nuxtjs/composition-api'
+import { alert } from '~/types/interfaces'
 
-  export default defineComponent({
-    setup() {
-      const {store} = useContext()
+export default defineComponent({
+  setup () {
+    const { store } = useContext()
 
-      const alerts = computed(() => {
-        return store.state.page.alerts
-      })
-      return {
-        alerts
-      }
+    const alerts: ComputedRef<Array<alert>> = computed(() => {
+      return store.state.page.alerts
+    })
+    return {
+      alerts
     }
-  })
+  }
+})
 </script>
 
 <style scoped>
@@ -32,7 +36,7 @@
     position: fixed;
     bottom: 0;
     right: 20px;
-    z-index: 100;
+    z-index: 1000;
   }
 
   .alertContainer > .alertContent {

+ 43 - 42
components/Layout/Alert/Content.vue

@@ -1,3 +1,5 @@
+<!-- Message d'alerte -->
+
 <template>
   <v-alert
     v-model="show"
@@ -7,54 +9,53 @@
     width="400"
     dismissible
     transition="fade-transition"
-    v-on:mouseover="onMouseOver"
-    v-on:mouseout="onMouseOut"
+    @mouseover="onMouseOver"
+    @mouseout="onMouseOut"
   >
-    {{alert.message}}
+    {{ $t(alert.message) }}
   </v-alert>
 </template>
 
 <script lang="ts">
-  import {defineComponent, ref} from "@vue/composition-api";
-  import {alert} from "~/types/interfaces";
-  import {useContext} from "@nuxtjs/composition-api";
-
-  export default defineComponent({
-    props: {
-      alert: {
-        type: Object as () => alert,
-        required: true
-      }
-    },
-    setup() {
-      const show = ref(true)
-      const {store} = useContext()
-      let timeout: any = null;
-
-      const clearAlert = (time = 2000) => {
-        timeout = setTimeout(() => {
-          show.value = false;
-          store.dispatch('page/removeSlowlyAlert')
-        }, time)
-      }
-
-      const onMouseOver = () => {
-        clearTimeout(timeout);
-      }
-
-      const onMouseOut = () => {
-        clearAlert(1000);
-      }
-
-      clearAlert()
-
-      return {
-        show,
-        onMouseOver,
-        onMouseOut
-      }
+import { defineComponent, ref, Ref, useContext } from '@nuxtjs/composition-api'
+import { alert } from '~/types/interfaces'
+
+export default defineComponent({
+  props: {
+    alert: {
+      type: Object as () => alert,
+      required: true
+    }
+  },
+  setup () {
+    const { store } = useContext()
+    const show: Ref<boolean> = ref(true)
+    let timeout: any = null
+
+    const clearAlert = (time: number = 2000) => {
+      timeout = setTimeout(() => {
+        show.value = false
+        store.dispatch('page/removeSlowlyAlert')
+      }, time)
+    }
+
+    const onMouseOver = () => {
+      clearTimeout(timeout)
+    }
+
+    const onMouseOut = () => {
+      clearAlert(1000)
+    }
+
+    clearAlert()
+
+    return {
+      show,
+      onMouseOver,
+      onMouseOut
     }
-  })
+  }
+})
 </script>
 
 <style scoped>

+ 122 - 0
components/Layout/AlertBar/Cotisation.vue

@@ -0,0 +1,122 @@
+<!--
+Cotisation bar : Barre qui s'affiche pour donner l'état de la cotisation
+-->
+
+<template>
+  <main>
+    <UiSystemBar color="ot_info" v-if="show_cotisation_access">
+      <template #bar.text>
+        <a @click="goOn('AFFILIATION')" class="ot_white--text">
+          <v-icon small>fas fa-exclamation-triangle</v-icon> {{$t('cotisation_access')}}
+        </a>
+      </template>
+    </UiSystemBar>
+
+    <UiSystemBar color="ot_info" v-else-if="show_upload_invoice">
+      <template #bar.text>
+        <a @click="goOn('INVOICE')" class="ot_white--text">
+          <v-icon small>fas fa-exclamation-triangle</v-icon> {{$t('upload_cotisation_invoice')}}
+        </a>
+      </template>
+    </UiSystemBar>
+
+    <UiSystemBar color="ot_info" v-else-if="show_renew_insurance">
+      <template #bar.text>
+        <a @click="goOn('INSURANCE')" class="ot_white--text">
+          <v-icon small>fas fa-exclamation-triangle</v-icon> {{$t('renew_insurance_cmf')}}
+        </a>
+      </template>
+    </UiSystemBar>
+
+    <UiSystemBar color="ot_info" v-else-if="show_insurance_subscription">
+      <template #bar.text>
+        <a @click="goOn('ADVERTISINGINSURANCE')" class="ot_white--text">
+          <v-icon small>fas fa-exclamation-triangle</v-icon> {{$t('insurance_cmf_subscription')}}
+        </a>
+      </template>
+    </UiSystemBar>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, useFetch, useContext, Ref, ref} from '@nuxtjs/composition-api'
+import {ALERT_STATE_COTISATION, QUERY_TYPE} from "~/types/enums";
+import {organizationState} from "~/types/interfaces";
+
+export default defineComponent({
+  setup () {
+    const {$dataProvider, store, $config} = useContext()
+    const profileOrganization:organizationState = store.state.profile.organization
+
+    const baseLegacyUrl:string = $config.baseURL_adminLegacy
+    const show_cotisation_access: Ref<Boolean> = ref(false)
+    const show_upload_invoice: Ref<Boolean> = ref(false)
+    const show_renew_insurance: Ref<Boolean> = ref(false)
+    const show_insurance_subscription: Ref<Boolean> = ref(false)
+    const cotisation_year: Ref<Number> = ref(0)
+
+    /**
+     * On récupère l'état des cotisations via l'API
+     */
+    useFetch(async () => {
+      const response = await $dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        id: profileOrganization.id,
+        url: 'cotisations'
+      })
+      cotisation_year.value = response.data.cotisationYear
+      handleShow(response.data.alertState)
+    })
+
+    /**
+     * Suivant l'état de l'alerte on affiche tel ou tel message
+     * @param alertState
+     */
+    const handleShow = (alertState: ALERT_STATE_COTISATION) =>{
+      switch(alertState){
+        case ALERT_STATE_COTISATION.AFFILIATION :
+          show_cotisation_access.value = true
+          break;
+        case ALERT_STATE_COTISATION.INVOICE :
+          show_upload_invoice.value = true
+          break;
+        case ALERT_STATE_COTISATION.INSURANCE :
+          show_renew_insurance.value = true
+          break;
+        case ALERT_STATE_COTISATION.ADVERTISINGINSURANCE :
+          show_insurance_subscription.value = true
+          break;
+      }
+    }
+
+    /**
+     * Suivant le bandeau, une action différente est réalisée
+     * @param type
+     */
+    const goOn = (type: ALERT_STATE_COTISATION) => {
+      switch(type){
+        case ALERT_STATE_COTISATION.AFFILIATION :
+          window.location.href = `${baseLegacyUrl}/cotisation/cotisation_steps/${profileOrganization.id}/steps/1`
+          break;
+        case ALERT_STATE_COTISATION.INVOICE :
+          window.open(`${baseLegacyUrl}/cotisation/invoice/${cotisation_year.value}`, '_blank')
+          break;
+        case ALERT_STATE_COTISATION.INSURANCE :
+          window.location.href = `${baseLegacyUrl}/cotisation/insuranceedit`
+          break;
+        case ALERT_STATE_COTISATION.ADVERTISINGINSURANCE :
+          window.open('https://www.cmf-musique.org/services/assurances/assurance-de-groupe/', '_blank')
+          break;
+      }
+    }
+
+    return{
+      show_cotisation_access,
+      show_upload_invoice,
+      show_renew_insurance,
+      show_insurance_subscription,
+      goOn
+    }
+  }
+})
+</script>

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

@@ -0,0 +1,24 @@
+<!--
+Switch year bar : Barre qui s'affiche lorsque l'utilisateur n'est pas dans un environnement de production
+-->
+
+<template>
+  <UiSystemBar color="ot_warning" v-if="show">
+    <template #bar.text>
+      <v-icon small>fas fa-exclamation-triangle</v-icon> {{$t('not_production_environment', { env })}}
+    </template>
+  </UiSystemBar>
+</template>
+
+<script lang="ts">
+import { defineComponent} from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  setup () {
+    return{
+      show: process.env.NODE_ENV !== 'production',
+      env: process.env.NODE_ENV
+    }
+  }
+})
+</script>

+ 43 - 0
components/Layout/AlertBar/SuperAdmin.vue

@@ -0,0 +1,43 @@
+<!--
+Super Admin bar : Barre qui s'affiche lorsque l'utilisateur est un super admin en mode switch
+-->
+
+<template>
+  <UiSystemBar v-if="show" color="ot_danger">
+    <template #bar.text>
+      <v-icon small>
+        fas fa-exclamation-triangle
+      </v-icon>
+      <span>{{ $t('super_admin_switch_account') }}</span>
+      <a :href="url" class="ot_black--text text-decoration-none"><strong>{{ $t('click_here') }}</strong></a>
+    </template>
+  </UiSystemBar>
+</template>
+
+<script lang="ts">
+import { defineComponent, useStore, useContext } from '@nuxtjs/composition-api'
+import { State } from '@vuex-orm/core'
+import { AnyStore } from '~/types/interfaces'
+
+export default defineComponent({
+  setup () {
+    const { $config } = useContext()
+    const baseLegacyUrl:string = $config.baseURL_adminLegacy
+    const store:AnyStore = useStore<State>()
+    const originalAccess = store.state.profile.access.originalAccess
+    let show = false
+    let url = ''
+
+    if (originalAccess !== null) {
+      show = originalAccess.isSuperAdminAccess
+      url = `${baseLegacyUrl}/switch_user/${originalAccess.organization.id}/${originalAccess.id}/exit`
+    }
+
+    return { show, url }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 36 - 0
components/Layout/AlertBar/SwitchUser.vue

@@ -0,0 +1,36 @@
+<!--
+Switch year bar : Barre qui s'affiche lorsque l'utilisateur possède un compte multi user
+-->
+
+<template>
+  <UiSystemBar color="ot_info" v-if="show">
+    <template #bar.text>
+      <v-icon small>fas fa-exclamation-triangle</v-icon>
+      <span v-html="$t('multi_account_alert', { fullname })"></span>
+       <v-icon class="ml-1" small>fa fa-users</v-icon>
+      {{$t('multi_account_alert_next')}}
+    </template>
+  </UiSystemBar>
+</template>
+
+<script lang="ts">
+import { defineComponent, useStore} from '@nuxtjs/composition-api'
+import {accessState, AnyStore} from "~/types/interfaces";
+import {State} from "@vuex-orm/core";
+
+export default defineComponent({
+  setup () {
+    const store:AnyStore = useStore<State>()
+    const profileAccess:accessState = store.state.profile.access
+
+    return {
+      show: profileAccess.hasFamilyMenu,
+      fullname: `${profileAccess.givenName} ${profileAccess.name}`
+    }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 57 - 0
components/Layout/AlertBar/SwitchYear.vue

@@ -0,0 +1,57 @@
+<!--
+Switch year bar : Barre qui s'affiche lorsque l'utilisateur n'est pas sur l'année courante.
+-->
+
+<template>
+  <UiSystemBar color="ot_warning" v-if="isShow">
+    <template #bar.text>
+      {{$t('not_current_year')}}
+      <a @click="resetYear"><strong class="ot_black--text">{{$t('not_current_year_reset')}}</strong></a>
+    </template>
+  </UiSystemBar>
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext, computed} from '@nuxtjs/composition-api'
+import {accessState, organizationState} from "~/types/interfaces";
+import {$useDirtyForm} from "~/use/form/useDirtyForm";
+import {$useMyProfileUpdater} from "~/use/updater/useMyProfileUpdater";
+
+export default defineComponent({
+  setup () {
+    const { store, $dataPersister } = useContext()
+    const { markFormAsNotDirty } = $useDirtyForm(store)
+    const { updateMyProfile, setHistorical, setActivityYear } = $useMyProfileUpdater(store, $dataPersister)
+
+    const profileAccess:accessState = store.state.profile.access
+    const profileOrganization:organizationState = store.state.profile.organization
+
+    const isShow = computed(() => {
+      return (
+        profileAccess.historical.past || profileAccess.historical.future
+        ||
+        profileAccess.historical.dateStart || profileAccess.historical.dateEnd
+        ||
+        profileAccess.activityYear !== profileOrganization.currentActivityYear
+      )
+    })
+
+    const resetYear = async () =>{
+      setHistorical(['present'])
+      setActivityYear(profileOrganization.currentActivityYear)
+      markFormAsNotDirty()
+      await updateMyProfile()
+      window.location.reload()
+    }
+
+    return {
+      isShow,
+      resetYear
+    }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 33 - 0
components/Layout/Alertbar.vue

@@ -0,0 +1,33 @@
+<!--
+Alert bars
+Contient les différentes barre d'alertes qui s'affichent selon certains cas...
+-->
+
+<template>
+  <main>
+    <client-only><LayoutAlertBarEnv></LayoutAlertBarEnv></client-only>
+    <LayoutAlertBarSwitchUser></LayoutAlertBarSwitchUser>
+    <LayoutAlertBarCotisation v-if="isCmf && $can('manage', 'cotisation')"></LayoutAlertBarCotisation>
+    <LayoutAlertBarSwitchYear></LayoutAlertBarSwitchYear>
+    <LayoutAlertBarSuperAdmin></LayoutAlertBarSuperAdmin>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext } from '@nuxtjs/composition-api'
+import {$organizationProfile} from "~/services/profile/organizationProfile";
+
+export default defineComponent({
+  setup () {
+    const { store } = useContext()
+    const organizationProfile = $organizationProfile(store)
+    return {
+      isCmf: organizationProfile.isCmf(),
+    }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 6 - 3
components/Layout/BannerTop.vue

@@ -1,13 +1,15 @@
+<!-- Troisième bandeau en partant du haut, contenant entre autre le numéro SIRET de l'organisation -->
+
 <template>
   <v-row justify="center" align="center" class="bannerTopForm">
     <v-col cols="3" class="ot_dark_grey ot_white--text">
-      <slot name="bloc1"></slot>
+      <slot name="block1" />
     </v-col>
     <v-col cols="6" class="ot_white ot_grey--text">
-      <slot name="bloc2"></slot>
+      <slot name="block2" />
     </v-col>
     <v-col cols="3" class="ot_light_grey ot_grey--text">
-      <slot name="bloc3"></slot>
+      <slot name="block3" />
     </v-col>
   </v-row>
 </template>
@@ -21,5 +23,6 @@
   .bannerTopForm > .col{
     min-height: 100px;
     padding: 10px;
+    padding-left: 24px;
   }
 </style>

+ 5 - 2
components/Layout/Container.vue

@@ -1,8 +1,11 @@
+<!-- Container générique pleine page, utilisé entre autres
+ pour porter le contenu principal de la page -->
+
 <template>
   <v-container fluid class="container">
     <v-row justify="center" align="center">
       <v-col cols="12" sm="12" md="12">
-        <slot></slot>
+        <slot />
       </v-col>
     </v-row>
   </v-container>
@@ -10,6 +13,6 @@
 
 <style scoped>
   .container{
-    padding: 30px;
+    padding-top: 0;
   }
 </style>

+ 14 - 12
components/Layout/Dialog.vue

@@ -1,3 +1,5 @@
+<!-- Fenêtre de dialogue -->
+
 <template>
   <v-dialog
     v-model="show"
@@ -5,29 +7,29 @@
     max-width="800"
   >
     <v-card>
-      <slot name="dialogText"></slot>
+      <slot name="dialogText" />
 
-      <v-divider></v-divider>
+      <v-divider />
 
       <v-card-actions>
-        <v-spacer></v-spacer>
-          <slot name="dialogBtn"></slot>
+        <v-spacer />
+        <slot name="dialogBtn" />
       </v-card-actions>
     </v-card>
   </v-dialog>
 </template>
 
 <script lang="ts">
-  import {defineComponent} from "@vue/composition-api";
+import { defineComponent } from '@nuxtjs/composition-api'
 
-  export default defineComponent({
-    props: {
-      show : {
-        type: Boolean,
-        required: true
-      }
+export default defineComponent({
+  props: {
+    show: {
+      type: Boolean,
+      required: true
     }
-  })
+  }
+})
 </script>
 
 <style scoped>

+ 84 - 53
components/Layout/Header.vue

@@ -1,3 +1,8 @@
+<!--
+Header de l'application, contient entre autres le nom de l'organisation, l'accès à l'aide
+et aux préférences de l'utilisateur
+-->
+
 <template>
   <v-app-bar
     clipped-left
@@ -8,88 +13,114 @@
     class="ot_green ot_white--text"
   >
     <v-btn
-      v-if="properties.displayedMiniVariant"
+      v-if="displayedMiniVariant"
       class="menu-btn"
       icon
       @click.stop="displayedMenu()"
     >
-      <v-icon class="ot_white--text">mdi-menu{{ `${properties.miniVariant ? '' : '-open'}` }}</v-icon>
+      <v-icon class="ot_white--text">
+        mdi-menu{{ `${properties.miniVariant ? '' : '-open'}` }}
+      </v-icon>
     </v-btn>
 
-    <v-toolbar-title v-text="properties.title"/>
+    <v-toolbar-title v-text="title" />
 
-    <v-spacer/>
+    <v-spacer />
 
     <v-btn
       elevation="2"
       color="ot_warning ot_white--text"
-    >{{$t('create')}}</v-btn>
-
-<!--    <v-btn icon>-->
-<!--      <a class="no-decoration" :href="properties.homeUrl + '/'"><v-icon class="ot_white&#45;&#45;text" small>fa-home</v-icon></a>-->
-<!--    </v-btn>-->
-
-    <v-btn icon>
-      <NuxtLink to="/" class="no-decoration"><v-icon class="ot_white--text" small>fa-home</v-icon></NuxtLink>
+    >
+      {{ $t('create') }}
     </v-btn>
 
-    <LayoutHeaderMenu :menu="properties.webSiteMenu"></LayoutHeaderMenu>
+    <v-tooltip bottom>
+      <template #activator="{ on, attrs }">
+        <v-btn
+          icon
+          class="ml-2"
+          v-bind="attrs"
+          v-on="on"
+        >
+          <a class="no-decoration" :href="properties.homeUrl + '/'"><v-icon class="ot_white--text" small>fa-home</v-icon></a>
+        </v-btn>
+      </template>
+      <span>{{ $t('welcome') }}</span>
+    </v-tooltip>
 
-    <LayoutHeaderMenu :menu="properties.myAccessesMenu" v-if="properties.hasAccessesMenu"></LayoutHeaderMenu>
+    <LayoutHeaderMenu :menu="webSiteMenu" />
 
-    <LayoutHeaderMenu :menu="properties.myFamilyMenu" v-if="properties.hasFamilyMenu"></LayoutHeaderMenu>
+    <LayoutHeaderMenu v-if="hasAccessesMenu" :menu="myAccessesMenu" />
 
-    <LayoutNotification></LayoutNotification>
+    <LayoutHeaderMenu v-if="hasFamilyMenu" :menu="myFamilyMenu" />
 
-    <LayoutHeaderMenu :menu="properties.configurationMenu" v-if="properties.hasConfigurationMenu"></LayoutHeaderMenu>
+    <LayoutHeaderNotification />
 
-    <LayoutHeaderMenu :menu="properties.accountMenu" :avatar="true"></LayoutHeaderMenu>
+    <LayoutHeaderMenu v-if="hasConfigurationMenu" :menu="configurationMenu" />
 
-    <a class="help ot_dark_grey ot_menu_color--text" href="https://support.opentalent.fr/" target="_blank">{{$t('help_access')}}</a>
+    <LayoutHeaderMenu :menu="accountMenu" :avatar="true" />
 
+    <a class="text-body pa-3 ml-2 ot_dark_grey ot_white--text text-decoration-none" href="https://support.opentalent.fr/" target="_blank">
+      <span class="d-none d-sm-none d-md-flex">{{ $t('help_access') }}</span>
+      <v-icon class="d-sm-flex d-md-none" color="white">fas fa-question-circle</v-icon>
+    </a>
   </v-app-bar>
 </template>
 
 <script lang="ts">
-  import {defineComponent, reactive, useContext} from '@nuxtjs/composition-api'
-  import {$useMenu} from "~/use/layout/menu";
-
-  export default defineComponent({
-    setup(props, {emit}) {
-      const {store, $config} = useContext();
-
-      const properties = reactive({
-        miniVariant: false,
-        displayedMiniVariant: store.state.profile.access.hasLateralMenu,
-        hasConfigurationMenu: store.state.profile.access.hasConfigurationMenu,
-        hasAccessesMenu: store.state.profile.access.hasAccessesMenu,
-        hasFamilyMenu: store.state.profile.access.hasFamilyMenu,
-        title: store.state.profile.organization.name,
-        homeUrl : $config.baseURL_adminLegacy,
-        webSiteMenu: $useMenu.setUpContext().useWebSiteMenuConstruct(),
-        myAccessesMenu : $useMenu.setUpContext().useMyAccessesMenuConstruct(),
-        myFamilyMenu : $useMenu.setUpContext().useMyFamilyMenuConstruct(),
-        configurationMenu: $useMenu.setUpContext().useConfigurationMenuConstruct(),
-        accountMenu : $useMenu.setUpContext().useAccountMenuConstruct()
-      })
-
-      const displayedMenu = () => {
-        properties.miniVariant = !properties.miniVariant
-        emit('handle-open-menu-click', properties.miniVariant)
-      }
-
-      return {
-        properties,
-        displayedMenu
-      }
+import {
+  defineComponent, reactive, useContext, computed, ComputedRef, Ref, UnwrapRef
+} from '@nuxtjs/composition-api'
+import { $useMenu } from '~/use/layout/menu'
+import { AnyJson } from '~/types/interfaces'
+
+export default defineComponent({
+  setup (_props, { emit }) {
+    const { store, $config } = useContext()
+
+    const properties:UnwrapRef<AnyJson> = reactive({
+      miniVariant: false,
+      homeUrl: $config.baseURL_adminLegacy
+    })
+
+    const displayedMiniVariant:ComputedRef<boolean> = computed(() => store.state.profile.access.hasLateralMenu)
+    const hasConfigurationMenu:ComputedRef<boolean> = computed(() => store.state.profile.access.hasConfigurationMenu)
+    const hasAccessesMenu:ComputedRef<boolean> = computed(() => store.state.profile.access.hasAccessesMenu)
+    const hasFamilyMenu:ComputedRef<boolean> = computed(() => store.state.profile.access.hasFamilyMenu)
+    const title:ComputedRef<string> = computed(() => store.state.profile.organization.name)
+
+    const webSiteMenu:Ref<any> = $useMenu.setupContext().useWebSiteMenuConstruct()
+    const myAccessesMenu:Ref<any> = $useMenu.setupContext().useMyAccessesMenuConstruct()
+    const myFamilyMenu:Ref<any> = $useMenu.setupContext().useMyFamilyMenuConstruct()
+    const configurationMenu:Ref<any> = $useMenu.setupContext().useConfigurationMenuConstruct()
+    const accountMenu:Ref<any> = $useMenu.setupContext().useAccountMenuConstruct()
+
+    const displayedMenu = () => {
+      properties.miniVariant = !properties.miniVariant
+      emit('handle-open-menu-click', properties.miniVariant)
     }
-  })
+
+    return {
+      properties,
+      displayedMiniVariant,
+      hasConfigurationMenu,
+      hasAccessesMenu,
+      hasFamilyMenu,
+      title,
+      displayedMenu,
+      webSiteMenu,
+      myAccessesMenu,
+      myFamilyMenu,
+      configurationMenu,
+      accountMenu
+    }
+  }
+})
 </script>
 
 <style scoped>
   .help {
-    padding: 14px;
-    padding-bottom: 13px;
+    padding: 14px 14px 13px;
     font-size: 14px;
     text-decoration: none;
   }

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

@@ -0,0 +1,102 @@
+<!--
+Menu déroulant générique pour l'affichage des menus du
+header principal (configuration, paramètres du compte...)
+-->
+
+<template>
+  <v-menu offset-y left>
+    <template v-slot:activator="{ on: { click }, attrs }">
+      <v-tooltip bottom>
+        <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
+          <v-btn
+            icon
+            v-bind="[attrs, attrs_tooltips]"
+            color=""
+            v-on="on_tooltips"
+            @click="click"
+          >
+            <v-avatar
+              v-if="avatar"
+              size="30"
+            >
+              <UiImage :id="avatarId" :imageByDefault="avatarByDefault" :width="30"></UiImage>
+            </v-avatar>
+            <v-icon  v-else class="ot_white--text" small>
+              {{ menu.icon }}
+            </v-icon>
+          </v-btn>
+        </template>
+        <span>{{ $t(menu.title) }}</span>
+      </v-tooltip>
+    </template>
+    <v-card scrollable>
+      <v-card-title class="ot_header_menu text-body-2 font-weight-bold">
+        {{$t(menu.title)}}
+      </v-card-title>
+      <v-card-text class="ma-0 pa-0 header_menu">
+        <v-list dense :subheader="true">
+          <template v-for="(item, index) in menu.children">
+            <v-list-item
+              :id="item.title"
+              :key="index"
+              :href="item.isExternalLink ? item.to : undefined"
+              :to="!item.isExternalLink ? item.to : undefined"
+              router
+              exact
+            >
+              <v-list-item-title v-text="$t(item.title)"/>
+            </v-list-item>
+          </template>
+        </v-list>
+      </v-card-text>
+      <v-card-actions class="ma-0 pa-0">
+        <template v-for="(item, index) in menu.actions">
+          <v-list-item
+            :id="item.title"
+            :key="index"
+            :href="item.isExternalLink ? item.to : undefined"
+            :to="!item.isExternalLink ? item.to : undefined"
+            router
+          >
+            <v-list-item-title class="text-body-2 ot_white--text" v-text="$t(item.title)"/>
+          </v-list-item>
+        </template>
+      </v-card-actions>
+    </v-card>
+
+
+  </v-menu>
+</template>
+
+<script lang="ts">
+import {defineComponent, useStore} from '@nuxtjs/composition-api'
+import {accessState} from "~/types/interfaces";
+import {Store} from "vuex";
+
+export default defineComponent({
+  props: {
+    menu: {
+      type: Object,
+      required: true
+    },
+    avatar: {
+      type: Boolean,
+      required: false
+    }
+  },
+  setup () {
+    const store:Store<any> = useStore()
+    const accessStore: accessState = store.state.profile.access
+    return {
+      avatarId: accessStore.avatarId,
+      avatarByDefault: accessStore.gender == 'MISTER' ? 'men-1.png' : 'women-1.png'
+    }
+  }
+})
+</script>
+<style scoped>
+  #logout{
+    background: var(--v-ot_green-base, white);
+    color: white;
+  }
+</style>

+ 259 - 0
components/Layout/Header/Notification.vue

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

+ 0 - 62
components/Layout/HeaderMenu.vue

@@ -1,62 +0,0 @@
-<template>
-  <v-menu offset-y left max-height="300">
-    <template v-slot:activator="{ on, attrs }">
-      <v-avatar v-if="avatar"
-             size="30"
-             v-bind="attrs"
-             v-on="on"
-      >
-        <img
-          src="https://cdn.vuetifyjs.com/images/john.jpg"
-          alt="John"
-        >
-      </v-avatar>
-      <v-btn v-else
-             icon
-             v-bind="attrs"
-             v-on="on"
-             color=""
-      >
-        <v-icon class="ot_white--text" small>{{menu.icon}}</v-icon>
-      </v-btn>
-    </template>
-    <v-list dense :subheader="true">
-      <v-list-item dense class="ot_light_grey">
-        <v-list-item-title v-text="$t(menu.title)"></v-list-item-title>
-      </v-list-item>
-      <template v-for="(item, index) in menu.children">
-        <v-list-item
-          :key="item.title"
-          :href="item.isExternalLink ? item.to : undefined"
-          :to="!item.isExternalLink ? item.to : undefined"
-          router
-          exact
-        >
-          <v-list-item-title v-text="$t(item.title)"></v-list-item-title>
-        </v-list-item>
-        <v-divider
-          v-if="index < menu.length - 1"
-          :key="index"
-        ></v-divider>
-      </template>
-
-    </v-list>
-  </v-menu>
-</template>
-
-<script lang="ts">
-  import {defineComponent} from '@nuxtjs/composition-api'
-
-  export default defineComponent({
-    props: {
-      menu: {
-        type: Object,
-        required: true
-      },
-      avatar: {
-        type: Boolean,
-        required: false
-      }
-    }
-  })
-</script>

+ 48 - 0
components/Layout/Loading.vue

@@ -0,0 +1,48 @@
+<!-- Animation circulaire à afficher durant les chargements -->
+
+<template>
+  <v-overlay :value="loading" class="loading-page">
+    <v-progress-circular
+      indeterminate
+      size="64"
+    />
+  </v-overlay>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, Ref } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  setup () {
+    const loading: Ref<boolean> = ref(false)
+
+    const set = (_num: number) => {
+      loading.value = true
+    }
+    const start = () => {
+      loading.value = true
+    }
+    const finish = () => {
+      loading.value = false
+    }
+
+    return {
+      loading,
+      start,
+      finish,
+      set
+    }
+  }
+})
+</script>
+
+<style scoped>
+  .loading-page {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 100!important;
+  }
+</style>

+ 30 - 26
components/Layout/Menu.vue

@@ -1,3 +1,8 @@
+<!--
+Menu principal de l'application
+Prend en paramètre une liste de ItemMenu et les met en forme
+-->
+
 <template>
   <v-navigation-drawer
     :mini-variant.sync="miniVariant"
@@ -7,7 +12,7 @@
     app
   >
     <v-list>
-      <div v-for="(item, i) in properties.menu" :key="i">
+      <div v-for="(item, i) in menu" :key="i">
         <v-list-item
           v-if="!item.children"
           :href="item.isExternalLink ? item.to : undefined"
@@ -16,10 +21,12 @@
           exact
         >
           <v-list-item-action>
-            <v-icon class="ot_menu_color--text" small>{{ item.icon }}</v-icon>
+            <v-icon class="ot_menu_color--text" small>
+              {{ item.icon }}
+            </v-icon>
           </v-list-item-action>
           <v-list-item-content>
-            <v-list-item-title class="ot_menu_color--text" v-text="$t(item.title)"/>
+            <v-list-item-title class="ot_menu_color--text" v-text="$t(item.title)" />
           </v-list-item-content>
         </v-list-item>
 
@@ -30,10 +37,12 @@
         >
           <template v-slot:activator>
             <v-list-item-action>
-              <v-icon class="ot_menu_color--text" small>{{ item.icon }}</v-icon>
+              <v-icon class="ot_menu_color--text" small>
+                {{ item.icon }}
+              </v-icon>
             </v-list-item-action>
             <v-list-item-content>
-              <v-list-item-title class="ot_menu_color--text" v-text="$t(item.title)"/>
+              <v-list-item-title class="ot_menu_color--text" v-text="$t(item.title)" />
             </v-list-item-content>
           </template>
 
@@ -46,41 +55,36 @@
             exact
           >
             <v-list-item-action>
-              <v-icon class="ot_menu_color--text" small>{{ child.icon }}</v-icon>
+              <v-icon class="ot_menu_color--text" small>
+                {{ child.icon }}
+              </v-icon>
             </v-list-item-action>
             <v-list-item-content>
-              <v-list-item-title class="ot_menu_color--text" v-text="$t(child.title)"/>
+              <v-list-item-title class="ot_menu_color--text" v-text="$t(child.title)" />
             </v-list-item-content>
           </v-list-item>
         </v-list-group>
       </div>
-
     </v-list>
-
   </v-navigation-drawer>
 </template>
 
 <script lang="ts">
-  import {$useMenu} from '@/use/layout/menu'
-  import {defineComponent, reactive} from '@nuxtjs/composition-api'
+import { defineComponent } from '@nuxtjs/composition-api'
+import { ItemsMenu } from '~/types/interfaces'
 
-  export default defineComponent({
-    props: {
-      miniVariant: {
-        type: Boolean,
-        required: true
-      }
+export default defineComponent({
+  props: {
+    menu: {
+      type: Array as () => ItemsMenu,
+      required: true
     },
-    setup() {
-      const properties = reactive({
-        menu: $useMenu.setUpContext().useLateralMenuConstruct()
-      })
-
-      return {
-        properties
-      }
+    miniVariant: {
+      type: Boolean,
+      required: true
     }
-  })
+  }
+})
 </script>
 
 <style scoped>

+ 0 - 48
components/Layout/Notification.vue

@@ -1,48 +0,0 @@
-<template>
-  <v-menu offset-y>
-    <template v-slot:activator="{ on, attrs }">
-      <v-btn icon
-             v-bind="attrs"
-             v-on="on"
-      >
-        <v-icon class="ot_white--text" small>fa-bell</v-icon>
-      </v-btn>
-    </template>
-    <v-list dense>
-      <template v-for="(item, index) in properties.menu">
-        <v-list-item :key="item.title">
-          <v-list-item-title v-text="$t(item.title)"></v-list-item-title>
-        </v-list-item>
-        <v-divider
-          v-if="index < properties.menu.length - 1"
-          :key="index"
-        ></v-divider>
-      </template>
-
-    </v-list>
-  </v-menu>
-</template>
-
-<script lang="ts">
-  import {$useMenu} from '@/use/layout/menu'
-  import {defineComponent, reactive} from '@nuxtjs/composition-api'
-  import {AnyJson} from "~/types/interfaces";
-
-  export default defineComponent({
-    setup() {
-      const menu: AnyJson = $useMenu.setUpContext().useConfigurationMenuConstruct()
-
-      const properties = reactive({
-        menu: menu
-      })
-
-      return {
-        properties
-      }
-    }
-  })
-</script>
-
-<style scoped>
-
-</style>

+ 62 - 0
components/Layout/SubHeader/ActivityYear.vue

@@ -0,0 +1,62 @@
+<template>
+  <main class="d-flex">
+    <span class="mr-2 ot_dark_grey--text font-weight-bold">{{ $t(label) }} : </span>
+    <UiXeditableText
+      class="activity-year-input"
+      type="number"
+      :data="activityYear"
+      @update="updateActivityYear"
+    >
+      <template v-slot:xeditable.read="{inputValue}">
+        <v-icon aria-hidden="false" class="ot_green--text" x-small>
+          fas fa-edit
+        </v-icon>
+        <strong class="ot_green--text"> {{ inputValue }} <span v-if="yearPlusOne">/ {{ parseInt(inputValue) + 1 }}</span></strong>
+      </template>
+    </UiXeditableText>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext } from '@nuxtjs/composition-api'
+import { $useMyProfileUpdater } from '~/use/updater/useMyProfileUpdater'
+import { $organizationProfile } from '~/services/profile/organizationProfile'
+import { $useDirtyForm } from '~/use/form/useDirtyForm'
+
+export default defineComponent({
+  setup () {
+    const { store, $dataPersister } = useContext()
+    const { updateMyProfile, setActivityYear, activityYear } = $useMyProfileUpdater(store, $dataPersister)
+    const { markFormAsNotDirty } = $useDirtyForm(store)
+
+    const organizationProfile = $organizationProfile(store)
+
+    const yearPlusOne:boolean = !organizationProfile.isManagerProduct()
+    const label:string = organizationProfile.isSchool() ? 'schooling_year' : organizationProfile.isArtist() ? 'season_year' : 'cotisation_year'
+
+    const updateActivityYear = async (newDate:number) => {
+      markFormAsNotDirty()
+      setActivityYear(newDate)
+      await updateMyProfile()
+      window.location.reload()
+    }
+
+    return {
+      activityYear,
+      label,
+      yearPlusOne,
+      updateActivityYear
+    }
+  }
+})
+</script>
+
+<style lang="scss">
+  .activity-year-input{
+    max-height: 20px;
+    input{
+      font-size: 14px;
+      width: 55px !important;
+    }
+  }
+</style>

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

@@ -0,0 +1,48 @@
+<template>
+  <v-breadcrumbs
+    :items="items"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, useContext, useRouter, ComputedRef } from '@nuxtjs/composition-api'
+import { AnyJson } from '~/types/interfaces'
+
+export default defineComponent({
+  setup () {
+    const { route, $config, app: { i18n } } = useContext()
+    const $router = useRouter()
+    const homeUrl: string = $config.baseURL_adminLegacy
+
+    const items: ComputedRef<Array<AnyJson>> = computed(() => {
+      const crumbs:Array<AnyJson> = []
+      crumbs.push({
+        text: i18n.t('welcome'),
+        href: homeUrl
+      })
+
+      const fullPath:string = route.value.path
+      const params:Array<string> = fullPath.startsWith('/') ? fullPath.substring(1).split('/') : fullPath.split('/')
+      let path:string = ''
+      params.forEach((param) => {
+        path = `${path}/${param}`
+        const match = $router.match(path)
+        if (match.name !== null) {
+          crumbs.push({
+            text: !parseInt(param, 10) ? i18n.t(param + '_breadcrumbs') : i18n.t('item'),
+            nuxt: true,
+            'exact-path': true,
+            to: path
+          })
+        }
+      })
+
+      return crumbs
+    })
+
+    return {
+      items
+    }
+  }
+})
+</script>

+ 93 - 0
components/Layout/SubHeader/DataTiming.vue

@@ -0,0 +1,93 @@
+<template>
+  <main class="d-flex align-baseline">
+    <span class="mr-2 ot_dark_grey--text font-weight-bold">{{ $t('display_data') }} : </span>
+
+    <v-btn-toggle
+      v-model="historicalBtn"
+      dense
+      class="ot_light_grey toggle-btn"
+      active-class="ot_green ot_white--text"
+      multiple
+    >
+      <v-btn max-height="25" class="font-weight-normal text-caption">
+        {{ $t('past') }}
+      </v-btn>
+
+      <v-btn max-height="25" class="font-weight-normal text-caption">
+        {{ $t('present') }}
+      </v-btn>
+
+      <v-btn max-height="25" class="font-weight-normal text-caption">
+        {{ $t('future') }}
+      </v-btn>
+    </v-btn-toggle>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, onUnmounted, ref, useContext, watch, Ref, WatchStopHandle } from '@nuxtjs/composition-api'
+import { $useDirtyForm } from '~/use/form/useDirtyForm'
+import { $useMyProfileUpdater } from '~/use/updater/useMyProfileUpdater'
+
+export default defineComponent({
+  setup () {
+    const { store, $dataPersister } = useContext()
+    const { markFormAsNotDirty } = $useDirtyForm(store)
+    const { updateMyProfile, setHistorical, historical } = $useMyProfileUpdater(store, $dataPersister)
+
+    const historicalBtn: Ref<Array<number>> = initHistoricalBtn(historical.value)
+
+    const unwatch: WatchStopHandle = watch(historicalBtn, async (newValue) => {
+      const historicalChoice: Array<string> = initHistoricalChoice(newValue)
+
+      setHistorical(historicalChoice)
+      markFormAsNotDirty()
+      await updateMyProfile()
+      window.location.reload()
+    })
+
+    onUnmounted(() => {
+      unwatch()
+    })
+
+    return {
+      historicalBtn
+    }
+  }
+})
+
+/**
+   * Prépare le tableau de valeur numéraire devant être passé au component v-btn-toggle
+   * @param historical
+   */
+function initHistoricalBtn (historical: Array<any>) {
+  const timeChoice:Ref<Array<number>> = ref(Array<number>())
+  const historicalArray:Array<any> = ['past', 'present', 'future']
+
+  for (const key in historicalArray) {
+    if (historical[historicalArray[key]]) { timeChoice.value.push(parseInt(key)) }
+  }
+  return timeChoice
+}
+
+/**
+   * Transforme le résultat renvoyé par le component v-btn-toggle pour l'enregistrer coté AccessProfile
+   * @param historical
+   */
+function initHistoricalChoice (historical:Array<any>) {
+  const historicalArray:Array<any> = ['past', 'present', 'future']
+
+  const historicalChoice:Array<string> = []
+  for (const key in historical) {
+    historicalChoice.push(historicalArray[historical[key]])
+  }
+  return historicalChoice
+}
+</script>
+
+<style scoped lang="scss">
+  .toggle-btn{
+    z-index: 1;
+    border-radius: 4px 0px 0px 4px;
+  }
+</style>

+ 108 - 0
components/Layout/SubHeader/DataTimingRange.vue

@@ -0,0 +1,108 @@
+<template>
+  <main class="d-flex align-baseline">
+    <div v-if="show" class="d-flex align-baseline">
+      <span class="mr-2 ot_dark_grey--text font-weight-bold">{{ $t('period_choose') }}</span>
+      <UiInputDatePicker
+        class="time-range"
+        label="date_choose"
+        :data="datesRange"
+        :range="true"
+        :dense="true"
+        :single-line="true"
+        @update="updateDateTimeRange"
+      />
+    </div>
+
+    <v-tooltip bottom>
+      <template v-slot:activator="{ on, attrs }">
+        <v-btn
+          class="time-btn"
+          max-height="25"
+          v-bind="attrs"
+          elevation="0"
+          max-width="10px"
+          min-width="10px"
+          v-on="on"
+          @click="show=!show"
+        >
+          <v-icon color="ot_grey" class="font-weight-normal" x-small>
+            fas fa-history
+          </v-icon>
+        </v-btn>
+      </template>
+      <span>{{ $t('history_help') }}</span>
+    </v-tooltip>
+  </main>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent, onUnmounted, ref, useContext, watch, computed, ComputedRef, Ref, WatchStopHandle
+} from '@nuxtjs/composition-api'
+import { $useMyProfileUpdater } from '~/use/updater/useMyProfileUpdater'
+import { $useDirtyForm } from '~/use/form/useDirtyForm'
+
+export default defineComponent({
+  setup (_, { emit }) {
+    const { store, $dataPersister } = useContext()
+    const { markFormAsNotDirty } = $useDirtyForm(store)
+    const { updateMyProfile, setHistoricalRange, historical } = $useMyProfileUpdater(store, $dataPersister)
+
+    const datesRange:ComputedRef<Array<string>> = computed(() => {
+      return [historical.value.dateStart, historical.value.dateEnd]
+    })
+
+    const show:Ref<boolean> = ref(false)
+    if (historical.value.dateStart || historical.value.dateEnd) {
+      show.value = true
+      emit('showDateTimeRange', true)
+    }
+
+    const unwatch:WatchStopHandle = watch(show, (newValue) => {
+      emit('showDateTimeRange', newValue)
+    })
+
+    onUnmounted(() => {
+      unwatch()
+    })
+
+    const updateDateTimeRange = async (dates:Array<string>): Promise<any> => {
+      setHistoricalRange(dates)
+      markFormAsNotDirty()
+      await updateMyProfile()
+      window.location.reload()
+    }
+
+    return {
+      show,
+      datesRange,
+      updateDateTimeRange
+    }
+  }
+})
+</script>
+
+<style lang="scss">
+  .v-btn--active .v-icon{
+    color: #FFF !important;
+  }
+  .time-btn{
+    border-width: 1px 1px 1px 0px;
+    border-style: solid;
+    border-color: rgba(0, 0, 0, 0.12) !important;
+  }
+  .time-range{
+    max-height: 20px;
+    .v-text-field{
+      padding-top: 0 !important;
+      margin-top: 0 !important;
+    }
+    .v-icon{
+      font-size: 20px;
+    }
+    input{
+      font-size: 14px;
+      width: 400px !important;
+    }
+  }
+</style>

+ 120 - 0
components/Layout/SubHeader/PersonnalizedList.vue

@@ -0,0 +1,120 @@
+<template>
+  <main>
+    <v-menu
+      bottom
+      left
+      transition="slide-y-transition"
+      :close-on-content-click="false"
+      min-width="500"
+    >
+      <template v-slot:activator="{ on, attrs }">
+        <span
+          v-bind="attrs"
+          class="ot_green--text"
+          v-on="on"
+        >
+          {{ $t('my_list') }}
+        </span>
+      </template>
+      <v-list>
+        <v-list-item>
+          <v-list-item-title>
+            <UiInputAutocomplete
+              :label="$t('searchList')"
+              :is-loading="isLoading"
+              :items="items"
+              :item-text="['label']"
+              :return-object="true"
+              @update="goOn"
+            />
+          </v-list-item-title>
+        </v-list-item>
+      </v-list>
+    </v-menu>
+  </main>
+</template>
+
+<script lang="ts">
+import {
+  computed, defineComponent, useContext, useFetch, ref, Ref, ComputedRef
+} from '@nuxtjs/composition-api'
+import { Collection } from '@vuex-orm/core'
+import { QUERY_TYPE } from '~/types/enums'
+import { PersonalizedList } from '~/models/Access/PersonalizedList'
+import { repositoryHelper } from '~/services/store/repository'
+import { AnyJson } from '~/types/interfaces'
+import { $objectProperties } from '~/services/utils/objectProperties'
+import VueI18n from "vue-i18n";
+
+export default defineComponent({
+  fetchOnServer: false,
+  setup () {
+    const { $dataProvider, $config } = useContext()
+    const isLoading: Ref<boolean> = ref(true)
+    const homeUrl:string = $config.baseURL_adminLegacy
+
+    useFetch(async () => {
+      await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: PersonalizedList
+      })
+      isLoading.value = false
+    })
+
+    const items:ComputedRef<Array<AnyJson>> = computed(() => {
+      const lists = repositoryHelper.findCollectionFromModel(PersonalizedList, {'id':'desc'}) as Collection<PersonalizedList>
+
+      let listsGroupByKeyMenu:Array<AnyJson> = groupListByKey(lists)
+
+      listsGroupByKeyMenu = $objectProperties.sortObjectByKey(listsGroupByKeyMenu)
+
+      return constructAutoCompleteItems(listsGroupByKeyMenu)
+    })
+
+    const goOn = (list:PersonalizedList) => {
+      window.location.href = `${homeUrl}/${list.entity}/list/${list.id}`
+    }
+
+    return {
+      items,
+      isLoading,
+      goOn
+    }
+  }
+})
+
+/**
+   * On regroupe la list par clé afin de constituer les groups
+   * @param lists
+   * @param i18n
+   */
+function groupListByKey (lists:Collection<PersonalizedList>): Array<AnyJson> {
+  const { app: { i18n } } = useContext()
+  const listsGroupByKeyMenu:any = {}
+  for (const list of lists) {
+    const key = i18n.t(list.menuKey) as string
+    listsGroupByKeyMenu[key] = listsGroupByKeyMenu[key] ?? []
+    listsGroupByKeyMenu[key].push(list)
+  }
+  return listsGroupByKeyMenu
+}
+
+/**
+   * On construit la list d'Item, constituée de Header et d'Item "sélectionnable"
+   * @param listsGroupByKeyMenu
+   */
+function constructAutoCompleteItems (listsGroupByKeyMenu:Array<any>) {
+  const items: any = []
+  for (const key in listsGroupByKeyMenu) {
+    items.push({ header: key })
+    for (const item of listsGroupByKeyMenu[key]) {
+      items.push(item)
+    }
+  }
+  return items
+}
+</script>
+
+<style scoped>
+
+</style>

+ 44 - 0
components/Layout/Subheader.vue

@@ -0,0 +1,44 @@
+<!--
+Second header de l'application
+Contient entre autres le breadcrumb, les commandes de changement d'année et les listes personnalisées
+-->
+
+<template>
+  <main>
+    <v-card
+      class="d-none d-sm-none d-md-flex ot_light_grey text-body-2"
+      flat
+      tile
+    >
+      <LayoutSubHeaderBreadcrumbs class="mr-auto" />
+
+      <v-card
+        class="d-md-flex ot_super_light_grey pt-2 mr-6  align-baseline"
+        flat
+        tile
+      >
+        <LayoutSubHeaderActivityYear v-if="!showDateTimeRange" class="activity-year" />
+        <LayoutSubHeaderDataTiming v-if="!showDateTimeRange" class="data-timing ml-2" />
+        <LayoutSubHeaderDataTimingRange class="data-timing-range ml-n1" @showDateTimeRange="showDateTimeRange=$event" />
+        <LayoutSubHeaderPersonnalizedList class="personalized-list ml-2" />
+      </v-card>
+    </v-card>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, Ref } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  setup () {
+    const showDateTimeRange: Ref<boolean> = ref(false)
+    return {
+      showDateTimeRange
+    }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 80 - 0
components/Ui/Button/Delete.vue

@@ -0,0 +1,80 @@
+<!--
+Bouton Delete avec modale de confirmation de la suppression
+-->
+
+<template>
+  <main>
+    <v-btn icon @click="alertDeleteItem()">
+      <v-icon>mdi-delete</v-icon>
+    </v-btn>
+
+    <lazy-LayoutDialog
+      :show="showDialog"
+    >
+      <template v-slot:dialogText>
+        <v-card-title class="text-h5 grey lighten-2">
+          {{ $t('attention') }}
+        </v-card-title>
+        <v-card-text>
+          <br>
+          <p>{{ $t('confirm_to_delete') }}</p>
+        </v-card-text>
+      </template>
+      <template v-slot:dialogBtn>
+        <v-btn class="mr-4 submitBtn ot_grey ot_white--text" @click="closeDialog">
+          {{ $t('cancel') }}
+        </v-btn>
+        <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="deleteItem">
+          {{ $t('delete') }}
+        </v-btn>
+      </template>
+    </lazy-LayoutDialog>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext, ref, Ref } from '@nuxtjs/composition-api'
+import { DataDeleterArgs, alert } from '~/types/interfaces'
+import { TYPE_ALERT } from '~/types/enums'
+
+export default defineComponent({
+  props: {
+    deleteArgs: {
+      type: Object as () => DataDeleterArgs,
+      required: true
+    }
+  },
+  setup (props) {
+    const { $dataDeleter, store, app: { i18n } } = useContext()
+    const showDialog: Ref<boolean> = ref(false)
+
+    const deleteItem = async () => {
+      try {
+        await $dataDeleter.invoke(props.deleteArgs)
+        const alert: alert = {
+          type: TYPE_ALERT.SUCCESS,
+          message: i18n.t('deleteSuccess') as string
+        }
+        store.commit('page/setAlert', alert)
+      } catch (error) {
+        const alert: alert = {
+          type: TYPE_ALERT.ALERT,
+          message: error.message
+        }
+        store.commit('page/setAlert', alert)
+      }
+      showDialog.value = false
+    }
+
+    return {
+      alertDeleteItem: () => { showDialog.value = true },
+      closeDialog: () => { showDialog.value = false },
+      deleteItem,
+      showDialog
+    }
+  }
+})
+</script>
+
+<style scoped>
+</style>

+ 47 - 4
components/Ui/Card.vue

@@ -1,3 +1,7 @@
+<!--
+Container de type Card
+-->
+
 <template>
   <v-card
     elevation="2"
@@ -6,17 +10,56 @@
     min-height="200"
   >
     <v-card-title>
-      <slot name="card.title"></slot>
+      <slot name="card.title" />
     </v-card-title>
     <v-card-text>
-      <slot name="card.text"></slot>
+      <slot name="card.text" />
     </v-card-text>
     <v-card-actions>
-      <v-spacer></v-spacer>
-      <slot name="card.action"></slot>
+      <v-spacer />
+      <v-btn icon>
+        <NuxtLink :to="link" class="no-decoration">
+          <v-icon>mdi-pencil</v-icon>
+        </NuxtLink>
+      </v-btn>
+      <UiButtonDelete :delete-args="args" />
+      <slot name="card.action" />
     </v-card-actions>
   </v-card>
 </template>
 
+<script lang="ts">
+import { defineComponent } from '@nuxtjs/composition-api'
+import { QUERY_TYPE } from '~/types/enums'
+import { DataDeleterArgs } from '~/types/interfaces'
+
+export default defineComponent({
+  props: {
+    link: {
+      type: String,
+      required: true
+    },
+    model: {
+      type: Function,
+      required: true
+    },
+    id: {
+      type: Number,
+      required: true
+    }
+  },
+  setup (props) {
+    const args: DataDeleterArgs = {
+      type: QUERY_TYPE.MODEL,
+      model: props.model,
+      id: props.id
+    }
+    return {
+      args
+    }
+  }
+})
+</script>
+
 <style scoped>
 </style>

+ 97 - 94
components/Ui/DataTable.vue

@@ -1,113 +1,116 @@
-<template>
-    <v-col
-      cols="12"
-      sm="12"
-    >
-      <v-data-table
-        :headers="headersWithItem"
-        :items="entries"
-        :server-items-length="totalEntries"
-        :loading="$fetchState.pending"
-        class="elevation-1"
-      >
+<!--
+Tableau interactif
 
-        <template v-for="header in headersWithItem" v-slot:[header.item]="props">
-          <slot :name="header.item" v-bind="props">
-            {{props.item[header.value]}}
-          </slot>
-        </template>
+@see https://vuetifyjs.com/en/components/data-tables/
+-->
 
-        <template v-slot:item.actions="{ item }">
-          <v-icon
-            small
-            class="mr-2"
-            @click="editItem(item)"
-          >
-            mdi-pencil
-          </v-icon>
-          <v-icon
-            small
-            @click="deleteItem(item)"
-          >
-            mdi-delete
-          </v-icon>
-        </template>
+<template>
+  <v-col
+    cols="12"
+    sm="12"
+  >
+    <v-data-table
+      :headers="headersWithItem"
+      :items="entries"
+      :server-items-length="totalEntries"
+      :loading="$fetchState.pending"
+      class="elevation-1"
+    >
+      <template v-for="header in headersWithItem" v-slot:[header.item]="props">
+        <slot :name="header.item" v-bind="props">
+          {{ props.item[header.value] }}
+        </slot>
+      </template>
 
-      </v-data-table>
-    </v-col>
+      <template v-slot:item.actions="{ item }">
+        <v-icon
+          small
+          class="mr-2"
+          @click="editItem(item)"
+        >
+          mdi-pencil
+        </v-icon>
+        <v-icon
+          small
+          @click="deleteItem(item)"
+        >
+          mdi-delete
+        </v-icon>
+      </template>
+    </v-data-table>
+  </v-col>
 </template>
 
 <script lang="ts">
-  import {defineComponent, ref, useContext, useFetch, computed, toRefs, onUnmounted} from '@nuxtjs/composition-api'
-  import {Query} from "@vuex-orm/core";
-  import {AnyJson} from "~/types/interfaces";
-  import {queryHelper} from "~/services/store/query";
-  import {QUERY_TYPE} from "~/types/enums";
-  import {Organization} from "~/models/Organization/Organization";
+import { defineComponent, ref, useContext, useFetch, computed, toRefs, Ref } from '@nuxtjs/composition-api'
+import { Query } from '@vuex-orm/core'
+import { AnyJson } from '~/types/interfaces'
+import { queryHelper } from '~/services/store/query'
+import { QUERY_TYPE } from '~/types/enums'
 
-  export default defineComponent({
-    props: {
-      rootModel:{
-        type: Function,
-        required: true
-      },
-      rootId:{
-        type: Number,
-        required: true
-      },
-      model:{
-        type: Function,
-        required: true
-      },
-      query:{
-        type: Object as () => Query,
-        required: true
-      },
-      headers:{
-        type: Array,
-        required: true
-      }
+export default defineComponent({
+  props: {
+    rootModel: {
+      type: Function,
+      required: true
+    },
+    rootId: {
+      type: Number,
+      required: true
     },
-    setup(props) {
-      const {rootModel, rootId, model, headers, query} = toRefs(props);
+    model: {
+      type: Function,
+      required: true
+    },
+    query: {
+      type: Object as () => Query,
+      required: true
+    },
+    headers: {
+      type: Array,
+      required: true
+    }
+  },
+  setup (props) {
+    const { rootModel, rootId, model, headers, query } = toRefs(props)
 
-      const headersWithItem = computed(()=>{
-        return headers.value.map((header:any)=>{
-          header['item'] = 'item.' + header.value
-          return header
-        })
+    const headersWithItem = computed(() => {
+      return headers.value.map((header:any) => {
+        header.item = 'item.' + header.value
+        return header
       })
+    })
 
-      const totalEntries = ref(0)
-      const entries = ref([] as Array<AnyJson>)
+    const totalEntries:Ref<number> = ref(0)
+    const entries:Ref<Array<AnyJson>> = ref(Array<AnyJson>())
 
-      const {$dataProvider} = useContext()
-      const {fetch, fetchState} = useFetch(async ()=>{
-        await $dataProvider.invoke({
-          type: QUERY_TYPE.MODEL,
-          model: model.value,
-          root_model: rootModel.value,
-          root_id: rootId.value
-        })
-        entries.value = queryHelper.getFlattenEntries(query.value);
-        totalEntries.value = entries.value.length
+    const { $dataProvider } = useContext()
+    useFetch(async () => {
+      await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: model.value,
+        rootModel: rootModel.value,
+        rootId: rootId.value
       })
+      entries.value = queryHelper.getFlattenEntries(query.value)
+      totalEntries.value = entries.value.length
+    })
 
-      const itemId = ref(0);
+    const itemId:Ref<number> = ref(0)
 
-      const editItem = (item: AnyJson) => {
-        itemId.value=item.id;
-      }
+    const editItem = (item: AnyJson) => {
+      itemId.value = item.id
+    }
 
-      // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
+    // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
 
-      return {
-        entries,
-        totalEntries,
-        headersWithItem,
-        editItem,
-        itemId
-      }
+    return {
+      entries,
+      totalEntries,
+      headersWithItem,
+      editItem,
+      itemId
     }
-  })
+  }
+})
 </script>

+ 32 - 23
components/Ui/ExpansionPanel.vue

@@ -1,28 +1,37 @@
+<!--
+Panneaux déroulants de type "accordéon"
+
+@see https://vuetifyjs.com/en/components/expansion-panels/
+-->
+
 <template>
-     <v-expansion-panel :id="id" >
-       <v-expansion-panel-header class="ot_light_grey">
-         <v-icon class="ot_white--text ot_green icon">{{icon}}</v-icon>
-         {{$t(id)}}
-       </v-expansion-panel-header>
-       <v-expansion-panel-content>
-        <slot></slot>
-       </v-expansion-panel-content>
-     </v-expansion-panel>
- </template>
+  <v-expansion-panel :id="id">
+    <v-expansion-panel-header class="ot_light_grey">
+      <v-icon class="ot_white--text ot_green icon">
+        {{ icon }}
+      </v-icon>
+      {{ $t(id) }}
+    </v-expansion-panel-header>
+    <v-expansion-panel-content>
+      <slot />
+    </v-expansion-panel-content>
+  </v-expansion-panel>
+</template>
 
 <script lang="ts">
-    export default {
-      props: {
-        id:{
-          type: String,
-          required: true
-        },
-        icon:{
-          type: String,
-          required: false
-        }
-      }
+export default {
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+    icon: {
+      type: String,
+      required: false,
+      default: null
     }
+  }
+}
 </script>
 
 <style scoped>
@@ -31,10 +40,10 @@
     height: 47px;
     padding: 10px;
     margin-right: 10px;
-    flex: none;
+    flex: none !important;
   }
   .v-expansion-panel-header{
-    padding: 0px;
+    padding: 0;
   }
   .v-expansion-panel--active > .v-expansion-panel-header{
     min-height: 47px;

+ 113 - 141
components/Ui/Form.vue

@@ -1,30 +1,36 @@
+<!--
+Formulaire générique
+
+@see https://vuetifyjs.com/en/components/forms/#usage
+-->
+
 <template>
   <main>
     <v-form
       ref="form"
       v-model="properties.valid"
       lazy-validation
-      :readonly="readOnly"
+      :readonly="readonly"
     >
       <v-container fluid class="container btnActions">
         <v-row>
           <v-col cols="12" sm="12">
-            <slot name="form.button"></slot>
-            <v-btn v-if="!readOnly" class="mr-4 ot_green ot_white--text" @click="submit">
-              {{$t('save')}}
+            <slot name="form.button" />
+            <v-btn v-if="!readonly" class="mr-4 ot_green ot_white--text" @click="submit">
+              {{ $t('save') }}
             </v-btn>
           </v-col>
         </v-row>
       </v-container>
 
-      <slot name="form.input" v-bind="{entry,updateRepository}"></slot>
+      <slot name="form.input" v-bind="{entry,updateRepository}" />
 
       <v-container fluid class="container btnActions">
         <v-row>
           <v-col cols="12" sm="12">
-            <slot name="form.button"></slot>
-            <v-btn v-if="!readOnly" class="mr-4 ot_green ot_white--text" @click="submit">
-              {{$t('save')}}
+            <slot name="form.button" />
+            <v-btn v-if="!readonly" class="mr-4 ot_green ot_white--text" @click="submit">
+              {{ $t('save') }}
             </v-btn>
           </v-col>
         </v-row>
@@ -36,22 +42,22 @@
     >
       <template v-slot:dialogText>
         <v-card-title class="text-h5 grey lighten-2">
-          {{$t('attention')}}
+          {{ $t('attention') }}
         </v-card-title>
         <v-card-text>
           <br>
-          <p>{{$t('quit_without_saving_warning')}}</p>
+          <p>{{ $t('quit_without_saving_warning') }}</p>
         </v-card-text>
       </template>
       <template v-slot:dialogBtn>
         <v-btn class="mr-4 submitBtn ot_green ot_white--text" @click="closeDialog">
-          {{$t('back_to_form')}}
+          {{ $t('back_to_form') }}
         </v-btn>
         <v-btn class="mr-4 submitBtn ot_green ot_white--text" @click="saveAndQuit">
-          {{$t('save_and_quit')}}
+          {{ $t('save_and_quit') }}
         </v-btn>
         <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="goEvenUnsavedData">
-          {{$t('quit_form')}}
+          {{ $t('quit_form') }}
         </v-btn>
       </template>
     </lazy-LayoutDialog>
@@ -59,150 +65,116 @@
 </template>
 
 <script lang="ts">
-  import {computed, defineComponent, onBeforeMount, onBeforeUnmount, reactive, toRefs, useContext} from '@nuxtjs/composition-api'
-  import {repositoryHelper} from "~/services/store/repository";
-  import {queryHelper} from "~/services/store/query";
-  import {Query} from "@vuex-orm/core";
-  import {QUERY_TYPE, TYPE_ALERT} from "~/types/enums";
-  import {alert} from "~/types/interfaces";
-
-  export default defineComponent({
-    props: {
-      model: {
-        type: Function,
-        required: true
-      },
-      id: {
-        type: Number,
-        required: true
-      },
-      query: {
-        type: Object as () => Query,
-        required: true
-      },
+import {
+  computed, defineComponent, reactive, toRefs, useContext, ref, Ref, ComputedRef, ToRefs, UnwrapRef
+} from '@nuxtjs/composition-api'
+import { Query } from '@vuex-orm/core'
+import { repositoryHelper } from '~/services/store/repository'
+import { queryHelper } from '~/services/store/query'
+import { QUERY_TYPE, TYPE_ALERT } from '~/types/enums'
+import { alert, AnyJson } from '~/types/interfaces'
+import { $useDirtyForm } from '~/use/form/useDirtyForm'
+
+export default defineComponent({
+  props: {
+    model: {
+      type: Function,
+      required: true
     },
-    setup: function (props) {
-      const {$dataPersister, store, app: {router, i18n}} = useContext()
-
-      const {id, query} = toRefs(props)
-      const repository = repositoryHelper.getRepository(props.model)
-      const properties = reactive({
-        valid: false,
-        saveOk: false,
-        saveKo: false
-      })
-
-      const readOnly = computed(() => {
-        return false
-      })
-
-      const entry = computed(() => {
-        return queryHelper.getFlattenEntry(query.value, id.value)
-      })
-
-      const handler = getEventHandler()
-
-      const updateRepository = (newValue: string, field: string) => {
-        addEventListener(handler)
-        store.commit('form/setDirty', true)
-        repositoryHelper.updateStoreFromField(repository, entry.value, newValue, field)
-      }
+    id: {
+      type: Number,
+      required: true
+    },
+    query: {
+      type: Object as () => Query,
+      required: true
+    }
+  },
+  setup (props) {
+    const { $dataPersister, store, app: { router, i18n } } = useContext()
+    const { markFormAsDirty, markFormAsNotDirty } = $useDirtyForm(store)
+
+    const { id, query }: ToRefs = toRefs(props)
+    const properties: UnwrapRef<AnyJson> = reactive({
+      valid: false,
+      saveOk: false,
+      saveKo: false
+    })
+
+    const readonly: Ref<boolean> = ref(false)
+
+    const entry: ComputedRef<AnyJson> = computed(() => {
+      return queryHelper.getFlattenEntry(query.value, id.value)
+    })
+
+    const updateRepository = (newValue: string, field: string) => {
+      markFormAsDirty()
+      repositoryHelper.updateStoreFromField(props.model, entry.value, newValue, field)
+    }
 
-      const submit = async () => {
-        try {
-          store.commit('form/setDirty', false)
-          clearEventListener(handler)
-          await $dataPersister.invoke({
-            type: QUERY_TYPE.MODEL,
-            model: props.model,
-            id: id.value
-          })
-
-          const alert:alert = {
-            type: TYPE_ALERT.SUCCESS,
-            message: i18n.t('saveSuccess') as string
-          }
-          store.commit('page/setAlert', alert)
-
-        } catch (error) {
-          const alert:alert = {
-            type: TYPE_ALERT.ALERT,
-            message: error.message
-          }
-          store.commit('page/setAlert', alert)
+    const submit = async () => {
+      try {
+        markFormAsNotDirty()
+        await $dataPersister.invoke({
+          type: QUERY_TYPE.MODEL,
+          model: props.model,
+          id: id.value
+        })
+
+        const alert:alert = {
+          type: TYPE_ALERT.SUCCESS,
+          message: i18n.t('saveSuccess') as string
         }
+        store.commit('page/setAlert', alert)
+      } catch (error) {
+        const alert:alert = {
+          type: TYPE_ALERT.ALERT,
+          message: error.message
+        }
+        store.commit('page/setAlert', alert)
       }
+    }
 
-      onBeforeMount(() => {
-        clearEventListener(handler)
-      })
-
-      onBeforeUnmount(() => {
-        clearEventListener(handler)
-      })
-
-      const showDialog = computed(() => {
-        return store.state.form.showConfirmToLeave
-      })
-
-      const closeDialog = () => {
-        store.commit('form/setShowConfirmToLeave', false)
-      }
+    const showDialog:ComputedRef<boolean> = computed(() => {
+      return store.state.form.showConfirmToLeave
+    })
 
-      const saveAndQuit = async () => {
-        await submit()
-        goEvenUnsavedData()
-      }
+    const closeDialog = () => {
+      store.commit('form/setShowConfirmToLeave', false)
+    }
 
-      const goEvenUnsavedData = () => {
-        store.commit('form/setDirty', false)
-        store.commit('form/setShowConfirmToLeave', false)
+    const saveAndQuit = async () => {
+      await submit()
+      goEvenUnsavedData()
+    }
 
-        const entryCopy = query.value.first()
-        if (entryCopy && entryCopy.$getAttributes()['originalState']) {
-          repositoryHelper.persist(repository, entryCopy.$getAttributes()['originalState'])
-        }
+    const goEvenUnsavedData = () => {
+      markFormAsNotDirty()
+      store.commit('form/setShowConfirmToLeave', false)
 
-        if (router) {
-          router.push(store.state.form.goAfterLeave)
-        }
+      const entryCopy = query.value.first()
+      if (entryCopy && entryCopy.$getAttributes().originalState) {
+        repositoryHelper.persist(props.model, entryCopy.$getAttributes().originalState)
       }
 
-      return {
-        submit,
-        updateRepository,
-        properties,
-        readOnly,
-        showDialog,
-        entry,
-        goEvenUnsavedData,
-        closeDialog,
-        saveAndQuit
+      if (router) {
+        router.push(store.state.form.goAfterLeave)
       }
     }
-  })
-
-  function getEventHandler() {
-    return function (e: any) {
-      // Cancel the event
-      e.preventDefault();
-      // Chrome requires returnValue to be set
-      e.returnValue = '';
-    };
-  }
 
-  function addEventListener(handler: any) {
-    if (process.browser) {
-      window.addEventListener('beforeunload', handler);
+    return {
+      submit,
+      updateRepository,
+      properties,
+      readonly,
+      showDialog,
+      entry,
+      goEvenUnsavedData,
+      closeDialog,
+      saveAndQuit
     }
   }
-
-  function clearEventListener(handler: any) {
-    if (process.browser) {
-      window.removeEventListener('beforeunload', handler);
-    }
-  }
-
+})
 </script>
 
 <style scoped>

+ 83 - 0
components/Ui/Image.vue

@@ -0,0 +1,83 @@
+<template>
+  <main>
+    <v-img
+      :src="imageLoaded"
+      :lazy-src="require(`/assets/images/byDefault/${imageByDefault}`)"
+      :min-height="height"
+      :min-width="width"
+      aspect-ratio="1"
+    >
+      <template v-slot:placeholder>
+        <v-row
+          class="fill-height ma-0"
+          align="center"
+          justify="center"
+        >
+          <v-progress-circular
+            indeterminate
+            color="grey lighten-1"
+          ></v-progress-circular>
+        </v-row>
+      </template>
+    </v-img>
+  </main>
+</template>
+
+
+<script lang="ts">
+import {defineComponent, ref, Ref, useContext, useFetch} from '@nuxtjs/composition-api'
+import {QUERY_TYPE} from "~/types/enums";
+
+export default defineComponent({
+  props: {
+    id: {
+      type: Number,
+      required: false
+    },
+    imageByDefault: {
+      type: String,
+      required: false,
+      default: 'default_pic.jpeg'
+    },
+    height: {
+      type: Number,
+      required: false,
+      default: 0
+    },
+    width: {
+      type: Number,
+      required: false,
+      default: 0
+    }
+  },
+  fetchOnServer: false,
+  setup(props) {
+    const {$dataProvider, $config} = useContext()
+    const imageLoaded: Ref<String> = ref('')
+
+    useFetch(async () => {
+        try{
+          if(props.id){
+            imageLoaded.value = await $dataProvider.invoke({
+              type: QUERY_TYPE.IMAGE,
+              baseUrl: $config.baseURL_Legacy,
+              imgArgs: {
+                id: props.id,
+                height: props.height,
+                width: props.width
+              }
+            })
+          }else
+            throw new Error('id is null')
+        }catch (e){
+          imageLoaded.value = require(`/assets/images/byDefault/${props.imageByDefault}`)
+        }
+      }
+    )
+
+    return {
+      imageLoaded
+    }
+  }
+})
+</script>

+ 95 - 0
components/Ui/Input/Autocomplete.vue

@@ -0,0 +1,95 @@
+<!--
+Liste déroulante avec autocompletion
+
+@see https://vuetifyjs.com/en/components/autocompletes/#usage
+-->
+
+<template>
+  <main>
+    <v-autocomplete
+      :items="itemsToDisplayed"
+      :label="$t(label)"
+      item-text="textDisplay"
+      :item-value="itemValue"
+      :multiple="multiple"
+      :loading="isLoading"
+      :return-object="returnObject"
+      @change="$emit('update', $event, field)"
+    />
+  </main>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, ComputedRef } from '@nuxtjs/composition-api'
+import { AnyJson } from '~/types/interfaces'
+
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: false,
+      default: null
+    },
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: String,
+      required: false,
+      default: null
+    },
+    items: {
+      type: Array,
+      required: false,
+      default: () => []
+    },
+    readonly: {
+      type: Boolean,
+      required: false
+    },
+    itemValue: {
+      type: String,
+      default: 'id'
+    },
+    itemText: {
+      type: Array,
+      required: true
+    },
+    returnObject: {
+      type: Boolean,
+      default: false
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    isLoading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  setup (props) {
+    // On reconstruit les items à afficher car le text de l'Item doit être construit par rapport au itemText passé en props
+    const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
+      return props.items.map((item: any) => {
+        const textDisplay: Array<string> = []
+        for (const text of props.itemText) {
+          textDisplay.push(item[text as string])
+        }
+        return Object.assign({}, item, { textDisplay: textDisplay.join(' ') })
+      })
+    })
+
+    return {
+      label_field: props.label ?? props.field,
+      itemsToDisplayed
+    }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 120 - 0
components/Ui/Input/AutocompleteWithAPI.vue

@@ -0,0 +1,120 @@
+<!--
+Liste déroulante avec autocompletion (les données sont issues
+de l'api Opentalent)
+
+@see https://vuetifyjs.com/en/components/autocompletes/#usage
+-->
+
+<template>
+  <main>
+    <v-autocomplete
+      v-model="model"
+      :value="data"
+      :items="items"
+      :loading="isLoading"
+      :search-input.sync="search"
+      hide-no-data
+      hide-selected
+      item-text="textDisplay"
+      :item-value="itemValue"
+      :label="$t(label_field)"
+      :placeholder="$t('start_your_research')"
+      prepend-icon="mdi-magnify"
+      :return-object="returnObject"
+    />
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, watch, ref, useContext, onUnmounted, Ref } from '@nuxtjs/composition-api'
+import * as _ from 'lodash'
+import { QUERY_TYPE } from '~/types/enums'
+
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: false,
+      default: null
+    },
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: String,
+      required: false,
+      default: null
+    },
+    readonly: {
+      type: Boolean,
+      required: false
+    },
+    itemValue: {
+      type: String,
+      default: 'id'
+    },
+    itemText: {
+      type: Array,
+      required: true
+    },
+    returnObject: {
+      type: Boolean,
+      default: false
+    }
+  },
+  setup (props) {
+    const { $dataProvider } = useContext()
+
+    const search:Ref<string|null> = ref(null)
+    const model = ref(null)
+    const count = ref(0)
+    const entries = ref([])
+    const isLoading = ref(false)
+
+    const items = computed(() => {
+      return entries.value.map((entry) => {
+        const textDisplay:Array<string> = []
+        for (const text of props.itemText) {
+          textDisplay.push(entry[text as string])
+        }
+        return Object.assign({}, entry, { textDisplay: textDisplay.join(' ') })
+      })
+    })
+
+    const unwatch = watch(search, _.debounce(async (research) => {
+      // Items have already been requested
+      if (isLoading.value) { return }
+
+      isLoading.value = true
+
+      const response = await $dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        url: `gps-coordinate-searching?city=${research}`
+      })
+
+      count.value = response.length
+      entries.value = response
+      isLoading.value = false
+    }, 500))
+
+    onUnmounted(() => {
+      unwatch()
+    })
+
+    return {
+      label_field: props.label ?? props.field,
+      count,
+      isLoading,
+      items,
+      search,
+      model
+    }
+  }
+})
+</script>
+
+<style scoped>
+
+</style>

+ 34 - 26
components/Ui/Input/Checkbox.vue

@@ -1,3 +1,9 @@
+<!--
+Case à cocher
+
+@see https://vuetifyjs.com/en/components/checkboxes/
+-->
+
 <template>
   <v-container
     class="px-0"
@@ -6,40 +12,42 @@
     <v-checkbox
       :value="data"
       :label="$t(label_field)"
+      :disabled="readonly"
       @change="$emit('update', $event, field)"
-      :disabled="readOnly"
-    ></v-checkbox>
+    />
   </v-container>
 </template>
 
 <script lang="ts">
-  import {defineComponent} from '@nuxtjs/composition-api'
+import { defineComponent } from '@nuxtjs/composition-api'
 
-  export default defineComponent({
-    props: {
-      field: {
-        type: String,
-        required: false
-      },
-      label: {
-        type: String,
-        required: false
-      },
-      data: {
-        type: Boolean,
-        required: false
-      },
-      readOnly: {
-        type: Boolean,
-        required: false
-      },
+export default defineComponent({
+  props: {
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    label: {
+      type: String,
+      required: false,
+      default: null
     },
-    setup(props){
-      return {
-        label_field : props.label ?? props.field,
-      }
+    data: {
+      type: Boolean,
+      required: false
+    },
+    readonly: {
+      type: Boolean,
+      required: false
+    }
+  },
+  setup (props) {
+    return {
+      label_field: props.label ?? props.field
     }
-  })
+  }
+})
 </script>
 
 <style scoped>

+ 105 - 64
components/Ui/Input/DatePicker.vue

@@ -1,78 +1,119 @@
+<!--
+Sélecteur de dates
+
+@see https://vuetifyjs.com/en/components/date-pickers/
+-->
+
 <template>
-  <v-menu
-    v-model="dateOpen"
-    :close-on-content-click="false"
-    :nudge-right="40"
-    transition="scale-transition"
-    offset-y
-    min-width="auto"
-  >
-    <template v-slot:activator="{ on, attrs }">
-      <v-text-field
-        v-model="dateFormatted"
-        :label="$t(label_field)"
-        prepend-icon="mdi-calendar"
-        :disabled="readOnly"
-        v-bind="attrs"
-        v-on="on"
-      ></v-text-field>
-    </template>
-    <v-date-picker
-      v-model="dateParsed"
-      @input="dateOpen = false"
-      locale="fr"
-      color="ot_green lighten-1"
-    ></v-date-picker>
-  </v-menu>
+  <main>
+    <v-menu
+      v-model="dateOpen"
+      :close-on-content-click="false"
+      :nudge-right="40"
+      transition="scale-transition"
+      offset-y
+      min-width="auto"
+    >
+      <template v-slot:activator="{ on, attrs }">
+        <v-text-field
+          v-model="datesFormatted"
+          autocomplete="off"
+          :label="$t(label_field)"
+          prepend-icon="mdi-calendar"
+          :disabled="readonly"
+          v-bind="attrs"
+          :dense="dense"
+          :single-line="singleLine"
+          v-on="on"
+        />
+      </template>
+      <v-date-picker
+        v-model="datesParsed"
+        locale="fr"
+        :range="range"
+        color="ot_green lighten-1"
+        @input="dateOpen = range && datesParsed.length < 2"
+      />
+    </v-menu>
+  </main>
 </template>
 
-
 <script lang="ts">
-  import {defineComponent, watch, ref, useContext, onUnmounted} from '@nuxtjs/composition-api'
+import { defineComponent, watch, ref, useContext, onUnmounted, computed, Ref, ComputedRef } from '@nuxtjs/composition-api'
+import { WatchStopHandle } from '@vue/composition-api'
+import DatesUtils from '~/services/utils/datesUtils'
 
-  export default defineComponent({
-    props: {
-      field: {
-        type: String,
-        required: false
-      },
-      label: {
-        type: String,
-        required: false
-      },
-      data: {
-        type: String,
-        required: false
-      },
-      readOnly: {
-        type: Boolean,
-        required: false
-      },
+export default defineComponent({
+  props: {
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    label: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: [String, Array],
+      required: false,
+      default: null
+    },
+    readonly: {
+      type: Boolean,
+      required: false
+    },
+    range: {
+      type: Boolean,
+      required: false
     },
-    setup(props, {emit}){
-      const {data, field} = props
-      const {$moment} = useContext()
+    dense: {
+      type: Boolean,
+      required: false
+    },
+    singleLine: {
+      type: Boolean,
+      required: false
+    }
+  },
+  setup (props, { emit }) {
+    const { data, field, range } = props
+    const { $moment } = useContext()
+    const dateUtils = new DatesUtils($moment)
 
-      const dateFormatted = ref($moment(data).format('DD/MM/YYYY'))
-      const dateParsed = ref($moment(data).format('YYYY-MM-DD'))
+    const datesParsed: Ref<Array<string>|string> = range ? ref(Array<string>()) : ref('')
 
-      const unwatch = watch(dateParsed, (newValue) => {
-        dateFormatted.value = $moment(newValue).format('DD/MM/YYYY')
-        emit('update', newValue, field)
-      })
+    if (Array.isArray(datesParsed.value)) {
+      for (const date of data as Array<string>) {
+        if (date) { datesParsed.value.push($moment(date).format('YYYY-MM-DD')) }
+      }
+    } else {
+      datesParsed.value = $moment(data as string).format('YYYY-MM-DD')
+    }
 
-      onUnmounted(()=>{
-        unwatch()
-      })
+    const datesFormatted: ComputedRef<string> = computed(() => {
+      return dateUtils.formattedDate(datesParsed.value, 'DD/MM/YYYY')
+    })
 
-      return {
-        label_field : props.label ?? props.field,
-        dateParsed,
-        dateFormatted,
-        dateOpen: false
-      }
+    const unwatch: WatchStopHandle = watch(datesParsed, (newValue, oldValue) => {
+      if (newValue === oldValue) { return }
+      if (Array.isArray(newValue) && newValue.length < 2) { return }
+      emit('update', Array.isArray(newValue) ? dateUtils.sortDate(newValue) : newValue, field)
+    })
+
+    onUnmounted(() => {
+      unwatch()
+    })
+
+    return {
+      label_field: props.label ?? props.field,
+      datesParsed,
+      datesFormatted,
+      dateOpen: ref(false)
     }
-  })
+  }
+})
 </script>
 
 <style scoped>

+ 82 - 0
components/Ui/Input/Email.vue

@@ -0,0 +1,82 @@
+<!--
+Champs de saisie de type Text dédié à la saisie d'emails
+-->
+
+<template>
+  <UiInputText
+    :data="data"
+    :label="label"
+    :readonly="readonly"
+    :error="error"
+    :error-message="errorMessage"
+    :rules="rules"
+    @change="$emit('update', $event, field)"
+  />
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: false,
+      default: ''
+    },
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: [String, Number],
+      required: false,
+      default: null
+    },
+    readonly: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    required: {
+      type: Boolean,
+      required: false,
+      default: false
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
+    }
+  },
+  setup (props) {
+    const { app: { i18n } } = useContext()
+
+    const rules = [
+      (email: string) => validEmail(email) || i18n.t('email_error')
+    ]
+
+    if (props.required) {
+      rules.push(
+        (email: string) => !!email || i18n.t('required')
+      )
+    }
+
+    return {
+      rules
+    }
+  }
+})
+
+function validEmail(email: string) {
+  // regex from https://fr.vuejs.org/v2/cookbook/form-validation.html#Utiliser-une-validation-personnalisee
+  const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+  return re.test(email)
+}
+
+</script>

+ 58 - 48
components/Ui/Input/Enum.vue

@@ -1,10 +1,16 @@
+<!--
+Liste déroulante dédiée à l'affichage d'objets Enum
+
+@see https://vuetifyjs.com/en/components/selects/
+-->
+
 <template>
   <main>
     <v-skeleton-loader
       v-if="$fetchState.pending"
       type="list-item"
       loading
-    ></v-skeleton-loader>
+    />
 
     <v-select
       v-else
@@ -13,65 +19,69 @@
       :items="items"
       item-text="label"
       item-value="value"
-      @change="$emit('update', $event, field)"
       :rules="rules"
-      :disabled="readOnly"
-    ></v-select>
+      :disabled="readonly"
+      @change="$emit('update', $event, field)"
+    />
   </main>
 </template>
 
 <script lang="ts">
-  import {defineComponent, ref, useContext, useFetch} from '@nuxtjs/composition-api'
-  import {AnyJson} from "~/types/interfaces";
-  import {QUERY_TYPE} from "~/types/enums";
+import { defineComponent, ref, useContext, useFetch, Ref } from '@nuxtjs/composition-api'
+import { EnumChoices } from '~/types/interfaces'
+import { QUERY_TYPE } from '~/types/enums'
 
-  export default defineComponent({
-    props: {
-      enumType: {
-        type: String,
-        required: true
-      },
-      label:{
-        type: String,
-        required: false
-      },
-      field:{
-        type: String,
-        required: false
-      },
-      data: {
-        type: String,
-        required: false
-      },
-      readOnly: {
-        type: Boolean,
-        required: false
-      },
-      rules: {
-        type: Array,
-        required: false
-      }
+export default defineComponent({
+  props: {
+    enumType: {
+      type: String,
+      required: true
+    },
+    label: {
+      type: String,
+      required: false,
+      default: null
     },
-    setup(props){
-      const label_field = props.label ?? props.field
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: String,
+      required: false,
+      default: null
+    },
+    readonly: {
+      type: Boolean,
+      required: false
+    },
+    rules: {
+      type: Array,
+      required: false,
+      default: () => []
+    }
+  },
+  setup (props) {
+    const labelField = props.label ?? props.field
 
-      const { enumType } = props
-      const {$dataProvider} = useContext()
+    const { enumType } = props
+    const { $dataProvider } = useContext()
 
-      const items: AnyJson = ref([])
-      const {fetch, fetchState} = useFetch(async () => {
-        items.value = await $dataProvider.invoke({
-          type: QUERY_TYPE.ENUM,
-          enumType: enumType
-        })
+    const items: Ref<Array<EnumChoices>> = ref([])
+    useFetch(async () => {
+      items.value = await $dataProvider.invoke({
+        type: QUERY_TYPE.ENUM,
+        enumType
       })
+    })
 
-      return {
-        items,
-        label_field
-      }
+    return {
+      items,
+      label_field: labelField
     }
-  })
+  }
+})
 </script>
 
 <style scoped>

+ 99 - 0
components/Ui/Input/Phone.vue

@@ -0,0 +1,99 @@
+<!--
+Champs de saisie d'un numéro de téléphone
+
+@see https://github.com/yogakurniawan/vue-tel-input-vuetify
+-->
+
+<template>
+  <client-only>
+    <vue-tel-input-vuetify
+      :field="field"
+      :label="label"
+      :value="data"
+      :readonly="readonly"
+      clearable
+      valid-characters-only
+      validate-on-blur
+      :rules="rules"
+      :active-country="{iso2: 'FR'}"
+      @input="onInput"
+      @change="onChange"
+    />
+  </client-only>
+</template>
+
+<script lang="ts">
+import { defineComponent, Ref, ref, useContext } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: false,
+      default: ''
+    },
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: [String, Number],
+      required: false,
+      default: null
+    },
+    readonly: {
+      type: Boolean,
+      required: false
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
+    }
+  },
+  setup () {
+    const { app: { i18n } } = useContext()
+
+    const nationalNumber: Ref<string | number> = ref('')
+    const internationalNumber: Ref<string | number> = ref('')
+    const isValid: Ref<boolean> = ref(false)
+    const country: Ref<string> = ref('')
+
+    return {
+      nationalNumber,
+      internationalNumber,
+      isValid,
+      country,
+      rules: [
+        () => isValid.value || i18n.t('phone_error')
+      ]
+    }
+  },
+  methods: {
+    onInput (_: any, { number, valid, country }: { number: any, valid: boolean, country: any }) {
+      this.isValid = valid
+      this.nationalNumber = number.national
+      this.internationalNumber = number.international
+      this.country = country && country.name
+      // console.log(this.field, this.isValid, this.nationalNumber, this.internationalNumber, this.country)
+    },
+    onChange () {
+      if (this.isValid) {
+        this.$emit('update', this.internationalNumber, this.field)
+      }
+    }
+  }
+})
+
+</script>
+
+<style>
+input:read-only{
+  color: #666 !important;
+}
+</style>

+ 63 - 51
components/Ui/Input/Text.vue

@@ -1,61 +1,73 @@
+<!--
+Champs de saisie de texte
+
+@see https://vuetifyjs.com/en/components/text-fields/
+-->
+
 <template>
-    <v-text-field
-      :value="data"
-      :label="$t(label_field)"
-      @change="$emit('update', $event, field)"
-      :rules="rules"
-      :disabled="readOnly"
-      :type="type"
-      :error="error"
-      :error-messages="errorMessage"
-    >
-    </v-text-field>
+  <v-text-field
+    autocomplete="off"
+    :value="data"
+    :label="$t(label_field)"
+    :rules="rules"
+    :disabled="readonly"
+    :type="type"
+    :error="error"
+    :error-messages="errorMessage"
+    @change="$emit('update', $event, field)"
+  />
 </template>
 
 <script lang="ts">
-  import {defineComponent} from '@nuxtjs/composition-api'
+import { defineComponent } from '@nuxtjs/composition-api'
 
-  export default defineComponent({
-    props: {
-      label:{
-        type: String,
-        required: false
-      },
-      field:{
-        type: String,
-        required: false
-      },
-      type:{
-        type: String,
-        required: false
-      },
-      data: {
-        type: String,
-        required: false
-      },
-      readOnly: {
-        type: Boolean,
-        required: false
-      },
-      rules:{
-        type: Array,
-        required: false
-      },
-      error:{
-        type: Boolean,
-        required: false
-      },
-      errorMessage:{
-        type: String,
-        required: false
-      }
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: false,
+      default: null
+    },
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    type: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: [String, Number],
+      required: false,
+      default: null
     },
-    setup(props){
-      return {
-        label_field : props.label ?? props.field
-      }
+    readonly: {
+      type: Boolean,
+      required: false
+    },
+    rules: {
+      type: Array,
+      required: false,
+      default: () => []
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
     }
-  })
+  },
+  setup (props) {
+    return {
+      label_field: props.label ?? props.field
+    }
+  }
+})
 </script>
 
 <style>

+ 106 - 0
components/Ui/Map.vue

@@ -0,0 +1,106 @@
+<!--
+Leaflet map
+
+@see https://leafletjs.com/
+-->
+
+<template>
+  <div id="map-wrap">
+    <client-only>
+      <l-map :zoom="zoom" :center="center">
+        <l-tile-layer :url="layerUrl" />
+        <l-marker
+          :lat-lng="latLong"
+          draggable
+          @update:latLng="onMoveMarker"
+        />
+      </l-map>
+
+      <v-btn class="mr-4 ot_green ot_white--text" @click="updateMap">
+        {{ $t('updateMap') }}
+      </v-btn>
+    </client-only>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent, computed, ref, toRefs, useContext, ComputedRef, Ref, ToRefs
+} from '@nuxtjs/composition-api'
+import { QUERY_TYPE, TYPE_ALERT } from '~/types/enums'
+import { AddressPostal } from '~/models/Core/AddressPostal'
+import { AnyJson, alert } from '~/types/interfaces'
+
+export default defineComponent({
+  props: {
+    address: {
+      type: Object as () => AddressPostal,
+      required: true
+    },
+    zoom: {
+      type: Number,
+      required: true
+    }
+  },
+  setup (props, { emit }) {
+    const { $dataProvider, store } = useContext()
+    const { address }: ToRefs = toRefs(props)
+    const latitude: Ref<number> = ref(address.value.latitude)
+    const longitude: Ref<number> = ref(address.value.longitude)
+
+    const center: ComputedRef<Array<number>> = computed(() => [latitude.value, longitude.value])
+    const latLong: ComputedRef<Array<number>> = computed(() => [latitude.value, longitude.value])
+    const layerUrl: string = 'https://{s}.tile.osm.org/{z}/{x}/{y}.png'
+
+    const updateMap = async () => {
+      const response = await $dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        url: `gps-coordinate-searching?street=${address.value.streetAddress} ${address.value.streetAddressSecond} ${address.value.streetAddressThird}&cp=${address.value.postalCode}&city=${address.value.addressCity}`
+      })
+      if (response.length > 0) {
+        latitude.value = response[0].latitude
+        longitude.value = response[0].longitude
+
+        address.value.latitude = response[0].latitude
+        address.value.longitude = response[0].longitude
+        emit('updateAddress', address.value)
+      } else {
+        const alert: alert = {
+          type: TYPE_ALERT.ALERT,
+          message: 'no_coordinate_corresponding'
+        }
+        store.commit('page/setAlert', alert)
+      }
+    }
+
+    const onMoveMarker = async (event: AnyJson) => {
+      const response = await $dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        url: `gps-coordinate-reverse/${event.lat}/${event.lng}`
+      })
+      address.value.streetAddress = response.streetAddress
+      address.value.streetAddressSecond = response.streetAddressSecond
+      address.value.streetAddressThird = response.streetAddressThird
+      address.value.postalCode = response.cp
+      address.value.addressCity = response.city
+
+      emit('updateAddress', address.value)
+    }
+
+    return {
+      updateMap,
+      center,
+      latLong,
+      layerUrl,
+      onMoveMarker
+    }
+  }
+})
+
+</script>
+
+<style scoped>
+  #map-wrap{
+    height: 30vh;
+  }
+</style>

+ 53 - 53
components/Ui/SubList.vue

@@ -1,64 +1,64 @@
+<!-- ? -->
+
 <template>
-    <main>
-      <v-skeleton-loader
-        type="text"
-        v-if="$fetchState.pending"
-      ></v-skeleton-loader>
-      <div v-else>
-        <slot name="list.item" v-bind="{items}"></slot>
-      </div>
-      <slot>
-      </slot>
-    </main>
+  <main>
+    <v-skeleton-loader
+      v-if="$fetchState.pending"
+      type="text"
+    />
+    <div v-else>
+      <slot name="list.item" v-bind="{items}" />
+    </div>
+    <slot />
+  </main>
 </template>
 
 <script lang="ts">
-  import {defineComponent, ref, useContext, useFetch, toRefs, onUnmounted} from '@nuxtjs/composition-api'
-  import {Model, Query} from "@vuex-orm/core";
-  import {queryHelper} from "~/services/store/query";
-  import {QUERY_TYPE} from "~/types/enums";
+import {
+  defineComponent, computed, useContext, useFetch, toRefs, ToRefs, ComputedRef
+} from '@nuxtjs/composition-api'
+import { Query } from '@vuex-orm/core'
+import { Collection } from '@vuex-orm/core/dist/src/data/Data'
+import { queryHelper } from '~/services/store/query'
+import { QUERY_TYPE } from '~/types/enums'
 
-  export default defineComponent({
-    props: {
-      rootModel:{
-        type: Function,
-        required: true
-      },
-      rootId:{
-        type: Number,
-        required: true
-      },
-      model:{
-        type: Function,
-        required: true
-      },
-      query:{
-        type: Object as () => Query,
-        required: true
-      }
+export default defineComponent({
+  props: {
+    rootModel: {
+      type: Function,
+      required: true
     },
-    setup(props) {
-      const {rootModel, rootId, model, query} = toRefs(props);
-      const items = ref([] as Array<Model>)
-
-      const {$dataProvider} = useContext()
-      const {fetch, fetchState} = useFetch(async ()=>{
-
-        await $dataProvider.invoke({
-          type: QUERY_TYPE.MODEL,
-          model: model.value,
-          root_model: rootModel.value,
-          root_id: rootId.value
-        })
-
-        items.value = queryHelper.getCollection(query.value);
+    rootId: {
+      type: Number,
+      required: true
+    },
+    model: {
+      type: Function,
+      required: true
+    },
+    query: {
+      type: Object as () => Query,
+      required: true
+    }
+  },
+  setup (props) {
+    const { rootModel, rootId, model, query }: ToRefs = toRefs(props)
+    const { $dataProvider } = useContext()
+    useFetch(async () => {
+      await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: model.value,
+        rootModel: rootModel.value,
+        rootId: rootId.value
       })
+    })
 
-      // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
+    const items: ComputedRef<Collection> = computed(() => queryHelper.getCollection(query.value))
+    // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
 
-      return {
-        items
-      }
+    return {
+      items
     }
-  })
+  }
+})
 </script>

+ 31 - 0
components/Ui/SystemBar.vue

@@ -0,0 +1,31 @@
+<!--
+System bars
+-->
+
+<template>
+    <v-system-bar
+      dark
+      height="40"
+      :color="color"
+    >
+        <div class="flex text-center ot_white--text">
+          <slot name="bar.text"></slot>
+        </div>
+    </v-system-bar>
+</template>
+
+<script lang="ts">
+import { defineComponent} from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    color: {
+      type: String,
+      required: true
+    }
+  }
+})
+</script>
+
+<style scoped>
+</style>

+ 77 - 0
components/Ui/Xeditable/Text.vue

@@ -0,0 +1,77 @@
+<!--
+?
+-->
+
+<template>
+  <main>
+    <div v-if="edit" class="d-flex align-baseline x-editable-input mt-n1">
+      <UiInputText
+        class="mt-0 pt-0 mt-n1"
+        :type="type"
+        :data="inputValue"
+        @update="inputValue=$event"
+      />
+      <v-icon aria-hidden="false" class="valid icons ot_green--text" small @click="update">
+        fas fa-check
+      </v-icon>
+      <v-icon aria-hidden="false" class="cancel icons ot_grey--text" small @click="close">
+        fas fa-times
+      </v-icon>
+    </div>
+    <div v-else class="edit-link" @click="edit=true">
+      <slot name="xeditable.read" v-bind="{inputValue}" />
+    </div>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, Ref } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    type: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: [String, Number],
+      required: false,
+      default: null
+    }
+  },
+  setup (props, { emit }) {
+    const edit: Ref<boolean> = ref(false)
+    const inputValue: Ref<string|number|null> = ref(props.data)
+
+    const update = () => {
+      edit.value = false
+      if (inputValue.value !== props.data) { emit('update', inputValue.value) }
+    }
+    const close = () => {
+      edit.value = false
+      inputValue.value = props.data
+    }
+    return {
+      inputValue,
+      edit,
+      update,
+      close
+    }
+  }
+})
+</script>
+
+<style lang="scss">
+  .x-editable-input{
+    input{
+      padding: 0 !important;
+    }
+    .icons{
+      padding: 5px;
+    }
+  }
+  .edit-link{
+    cursor: pointer;
+  }
+</style>

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

@@ -6,6 +6,14 @@
       organization:
         - {function: hasModule, parameters: ['PedagogicsAdministation']}
 
+  education_notation_config_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+      organization:
+        - {function: hasModule, parameters: ['AdvancedEducationNotation']}
+
   seizure_period_page:
     action: 'display'
     services:

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

@@ -0,0 +1,100 @@
+  my_schedule_page:
+    action: 'display'
+    services:
+      access:
+        - { function: isAdminAccount, result: false }
+
+  attendance_bookings_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'write', subject: 'attendances'}]}
+      organization:
+        - {function: hasModule, parameters: ['Attendances']}
+
+  my_attendance_page:
+    action: 'display'
+    services:
+      access:
+        - { function: isAdminAccount, result: false }
+
+  my_invitation_page:
+    action: 'display'
+    services:
+      access:
+        - { function: isAdminAccount, result: false }
+
+  my_students_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: [ 'teacher'] }
+
+  my_students_education_students_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: [ 'teacher'] }
+
+  criteria_notations_page_from_account_menu:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'criterianotation'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+
+  my_education_students_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: [ 'student'] }
+
+  send_an_email_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: [ 'admin', 'teacher' ] }
+
+  my_documents_page:
+    action: 'display'
+    services:
+      access:
+        - { function: isAdminAccount, result: false }
+
+  my_profile_page:
+    action: 'display'
+    services:
+      access:
+        - { function: isAdminAccount, result: false }
+
+  adherent_list_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: [ 'member'] }
+      organization:
+        - {function: isShowAdherentList}
+        - {function: hasModule, parameters: ['Users']}
+
+  subscription_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: ['admin', 'administratifManager', 'pedagogicManager', 'financialManager']}
+      organization:
+        - {function: hasModule, parameters: ['GeneralConfig']}
+
+  my_bills_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasProfile, parameters: ['guardian', 'payor']}
+
+  cmf_licence_page:
+    action: 'display'
+    services:
+      access:
+        - { function: isAdminAccount, result: false }
+      organization:
+        - {function: isCmf}

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

@@ -55,6 +55,14 @@
       organization:
         - {function: hasModule, parameters: ['GeneralConfig']}
 
+  template_systems_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'general-config'}]}
+      organization:
+        - {function: hasModule, parameters: ['TemplateMessages']}
+
   billing_settings_page:
     action: 'display'
     services:

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

@@ -6,6 +6,14 @@
       organization:
         - {function: hasModule, parameters: ['Statistic']}
 
+  education_quotas_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+
   fede_stats_page:
     action: 'display'
     services:

+ 10 - 11
config/nuxtConfig/build.js

@@ -1,33 +1,32 @@
 import webpack from 'webpack'
 
 export default {
-  // Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
-  ssr: true,
+  // Enable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
+  target: 'server',
 
-  // // Auto import components (https://go.nuxtjs.dev/config-components)
+  // Auto import components (https://go.nuxtjs.dev/config-components)
   components: true,
 
+  loading: '~/components/Layout/Loading.vue',
+
   // Build Configuration (https://go.nuxtjs.dev/config-build)
   build: {
     extend (config, { isDev, isClient }) {
       config.node = {
-        fs: "empty"
+        fs: 'empty'
       }
     },
     plugins: [
       new webpack.ProvidePlugin({
         _: 'lodash'
       })
+    ],
+    transpile: [
+      'vue-tel-input-vuetify'
     ]
   },
 
-  //Port and local host
-  server: {
-    port: 3002,
-    host: '0.0.0.0', // default: localhost,
-  },
-
-  //Poll for hot reloading with docker
+  // Poll for hot reloading with docker
   watchers: {
     webpack: {
       aggregateTimeout: 300,

+ 13 - 10
config/nuxtConfig/env.js

@@ -6,28 +6,31 @@ export default {
     artist_premium_product: 'artist-premium',
     manager_product: 'manager',
     cmf_network: 'CMF',
-    ffec_network: 'FFEC',
+    ffec_network: 'FFEC'
   },
   publicRuntimeConfig: {
     http: {
-      browserBaseURL: process.env.NODE_ENV !== 'production' ? 'https://local.new.api.opentalent.fr' : 'https://local.new.api.opentalent.fr'
+      browserBaseURL: process.env.CLIENT_API_BASE_URL
     },
     axios: {
       https: true,
-      browserBaseURL: process.env.NODE_ENV !== 'production' ? 'https://local.new.api.opentalent.fr' : 'https://local.new.api.opentalent.fr'
+      browserBaseURL: process.env.CLIENT_API_BASE_URL,
+      baseURL: process.env.CLIENT_API_BASE_URL
     },
-    baseURL_Legacy: process.env.NODE_ENV !== 'production' ? 'https://local.api.opentalent.fr' : 'https://local.api.opentalent.fr',
-    baseURL_adminLegacy: process.env.NODE_ENV !== 'production' ? 'https://local.admin.opentalent.fr/#' : 'https://admin.opentalent.fr/#',
-    baseURL_typo3: process.env.NODE_ENV !== 'production' ? `https://local.sub.opentalent.fr/###subDomain###` : `https://###subDomain###.opentalent.fr/#`,
+    baseURL_Legacy: process.env.CLIENT_APILEG_BASE_URL,
+    baseURL_adminLegacy: process.env.CLIENT_ADMINLEG_BASE_URL,
+    baseURL_typo3: process.env.CLIENT_TYPO3_BASE_URL
   },
   privateRuntimeConfig: {
     http: {
-      https:true,
-      baseURL: process.env.NODE_ENV !== 'production' ? 'http://nginx_new' : 'https://local.api.opentalent.fr'
+      https: true,
+      baseURL: process.env.SSR_API_BASE_URL
     },
     axios: {
-      baseURL: process.env.NODE_ENV !== 'production' ? 'http://nginx_new' : 'https://local.api.opentalent.fr'
+      baseURL: process.env.SSR_API_BASE_URL
     },
-    baseURL_Legacy: process.env.NODE_ENV !== 'production' ? 'http://nginx' : 'https://local.api.opentalent.fr'
+    baseURL_Legacy: process.env.SSR_APILEG_BASE_URL,
+    baseURL_adminLegacy: process.env.SSR_ADMINLEG_BASE_URL,
+    baseURL_typo3: process.env.SSR_TYPO3_BASE_URL
   }
 }

+ 7 - 6
config/nuxtConfig/head.js

@@ -4,19 +4,20 @@ export default {
     titleTemplate: '%s - admin',
     title: 'admin',
     meta: [
-      {charset: 'utf-8'},
-      {name: 'viewport', content: 'width=device-width, initial-scale=1'},
-      {hid: 'description', name: 'description', content: ''}
+      { charset: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+      { hid: 'description', name: 'description', content: '' }
     ],
     link: [
-      {rel: 'icon', type: 'image/x-icon', href: '/favicon.ico'}
+      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
     ]
   },
 
   // Global CSS (https://go.nuxtjs.dev/config-css)
+  // (https://nuxtjs.org/docs/2.x/features/configuration#the-css-property)
   css: [
-    '@/assets/css/import.scss',
-    '@/assets/css/global.scss',
+    '~/assets/css/import.scss',
+    '~/assets/css/global.scss',
     '@fortawesome/fontawesome-free/css/all.css'
   ]
 }

+ 5 - 2
config/nuxtConfig/modules.js

@@ -6,12 +6,15 @@ export default {
     // // https://go.nuxtjs.dev/vuetify
     '@nuxtjs/vuetify',
     '@nuxtjs/moment',
-    '@nuxtjs/composition-api/module'
+    '@nuxtjs/composition-api/module',
+    '@nuxt/image'
   ],
 
   // Modules (https://go.nuxtjs.dev/config-modules)
   modules: [
     '@nuxtjs/axios',
-    'nuxt-i18n'
+    'nuxt-leaflet',
+    '@nuxtjs/i18n',
+    '@nuxt/image'
   ]
 }

+ 2 - 0
config/nuxtConfig/plugins.js

@@ -8,5 +8,7 @@ export default {
     '~/plugins/Data/axios',
     '~/plugins/Data/dataPersister',
     '~/plugins/Data/dataProvider',
+    '~/plugins/Data/dataDeleter',
+    '~/plugins/phone-input'
   ]
 }

+ 8 - 3
config/nuxtConfig/vuetify.js

@@ -4,12 +4,14 @@ export default {
   // Vuetify module configuration (https://go.nuxtjs.dev/config-vuetify)
   vuetify: {
     icons: {
-      iconfont: 'fa' || 'mdi',
+      iconfont: 'fa' || 'mdi'
     },
     customVariables: ['~/assets/css/variables.scss'],
-    customProperties: true,
     treeShake: true,
     theme: {
+      options: {
+        customProperties: true
+      },
       dark: false,
       themes: {
         dark: {
@@ -22,10 +24,12 @@ export default {
           success: colors.green.accent3
         },
         light: {
+          primary: colors.blue.darken2,
           ot_green: '#00ad8e',
           ot_light_green: '#a9e0d6',
           ot_dark_grey: '#2c3a48',
           ot_grey: '#777777',
+          ot_header_menu: '#ECE7E5',
           ot_light_grey: '#f5f5f5',
           ot_super_light_grey: '#ecf0f5',
           ot_danger: '#f56954',
@@ -34,7 +38,8 @@ export default {
           ot_info: '#3c8dbc',
           ot_menu_color: '#b8c7ce',
           ot_content_color: '#ecf0f4',
-          ot_white: '#ffffff'
+          ot_white: '#ffffff',
+          ot_black: '#000000'
         },
       }
     }

+ 13 - 0
ecosystem.config.js

@@ -0,0 +1,13 @@
+// PM2 configuration: https://nuxtjs.org/deployments/pm2/
+
+module.exports = {
+  apps: [
+    {
+      name: 'app',
+      exec_mode: 'cluster',
+      instances: '1',
+      script: 'yarn',
+      args: 'start'
+    }
+  ]
+}

+ 4 - 1
jest.config.js

@@ -1,4 +1,5 @@
 module.exports = {
+  resetMocks: true,
   preset: '@nuxt/test-utils',
   moduleNameMapper: {
     '^@/(.*)$': '<rootDir>/$1',
@@ -11,6 +12,7 @@ module.exports = {
     'vue',
     'json'
   ],
+  testEnvironment: 'jsdom',
   transform: {
     '^.+\\.ts$': 'ts-jest',
     '^.+\\.js$': 'babel-jest',
@@ -23,5 +25,6 @@ module.exports = {
     '<rootDir>/services/**/*.ts',
     '<rootDir>/use/**/*.ts',
     '<rootDir>/pages/**/*.vue'
-  ]
+  ],
+  setupFiles: ['<rootDir>/tests/unit/index.ts']
 }

+ 9 - 0
lang/breadcrumbs/fr-FR.js

@@ -0,0 +1,9 @@
+export default (context, locale) => {
+  return ({
+    item: 'Détails',
+    organization_breadcrumbs: 'Fiche de la structure',
+    subscription_breadcrumbs: 'Mon abonnement',
+    address_breadcrumbs: 'Adresse postale',
+    contact_points_breadcrumbs: 'Points de contact'
+  })
+}

+ 61 - 52
lang/enum/fr-FR.js

@@ -1,61 +1,70 @@
 export default (context, locale) => {
   return ({
+    PRODUCT_ARTIST: 'Opentalent Artist',
+    PRODUCT_ARTIST_PREMIUM: 'Opentalent Artist Premium',
+    PRODUCT_SCHOOL: 'Opentalent School',
+    PRODUCT_SCHOOL_PREMIUM: 'Opentalent School Premium',
+    PRODUCT_MANAGER: 'Opentalent Manager',
     LOCAL_AUTHORITY: 'Collectivité territoriale (Mairie, SIVOM, SIVU, EPIC, …)',
     ASSOCIATION_LAW_1901: 'Association loi 1901 ou assimilée (Droit local, ...)',
     COMMERCIAL_SOCIETY: 'Entreprise commerciale (SARL, SAS, EURL, Autoentrepreneur, …)',
-    ARTISTIC_EDUCATION_ONLY: "Enseignement artistique seul",
-    ARTISTIC_PRACTICE_EDUCATION: "Pratique et enseignement artistique",
-    ARTISTIC_PRACTICE_ONLY: "Pratique artistique seule",
-    DELEGATION: "Délégation",
-    DEPARTEMENTAL_FEDERATION: "Fédération départementale",
-    GROUPMENT: "Groupement",
-    LOCAL_FEDERATION: "Fédération locale",
-    MUSIC_OPENTALENT: "Opentalent",
-    NATIONAL_FEDERATION: "Fédération nationale",
-    REGIONAL_FEDERATION: "Fédération régionale",
+    ARTISTIC_EDUCATION_ONLY: 'Enseignement artistique seul',
+    ARTISTIC_PRACTICE_EDUCATION: 'Pratique et enseignement artistique',
+    ARTISTIC_PRACTICE_ONLY: 'Pratique artistique seule',
+    DELEGATION: 'Délégation',
+    DEPARTEMENTAL_FEDERATION: 'Fédération départementale',
+    GROUPMENT: 'Groupement',
+    LOCAL_FEDERATION: 'Fédération locale',
+    MUSIC_OPENTALENT: 'Opentalent',
+    NATIONAL_FEDERATION: 'Fédération nationale',
+    REGIONAL_FEDERATION: 'Fédération régionale',
     CESMD: "CESMD Centre d'études supérieures de musique et de danse",
-    CNSMD: "CNSMD Conservatoire national supérieur de musique",
-    CRC: "CRC Conservatoire à rayonnement communal",
-    CRD: "CRD Conservatoire à rayonnement départemental",
-    CRI: "CRI Conservatoire à rayonnement intercommunal",
-    CRR: "CRR Conservatoire à rayonnement régional",
+    CNSMD: 'CNSMD Conservatoire national supérieur de musique',
+    CRC: 'CRC Conservatoire à rayonnement communal',
+    CRD: 'CRD Conservatoire à rayonnement départemental',
+    CRI: 'CRI Conservatoire à rayonnement intercommunal',
+    CRR: 'CRR Conservatoire à rayonnement régional',
     EENC: "EENC Établissement d'enseignement artistique non classé",
-    EMP: "EMP Ecole de musique privée",
-    MULTIPLE: "Multiple",
-    UNIQUE: "Unique",
-    MAIN_BUILDING: "Etablissement principal",
-    SECONDARY_SCHOOL: "Etablissement secondaire",
-    ACTALIANS: "Actalians",
-    AFDAS: "Afdas",
-    AGEFOS: "Agefos",
-    AGEFOS_PME: "Agefos pme",
-    ANFA: "Anfa",
-    ANFH: "Anfh",
-    APCMA: "Apcma",
-    CNFPT: "Cnfpt",
-    CONSTRUCTYS: "Constructys",
-    FAF_TT: "Faf-tt",
-    FAFIEC: "Fafiec",
-    FAFIH: "Fafih",
-    FAFSEA: "Fafsea",
-    FIF_PL: "Fif pl",
-    FONGECIF: "Fongecif",
-    FORCO: "Forco",
-    INTERGROS: "Intergros",
-    OPCA3_PLUS: "Opca3+",
-    OPCAIM: "Opcaim",
-    OPCALIA: "Opcalia",
-    OPCALIM: "Opcalim",
-    OPCA_BAIA: "Opca baia",
-    OPCA_DEFI: "Opca defi",
-    OPCA_TRANSPORTS: "Opca transports",
-    UNIFAF: "Unifaf",
-    UNIFORMATION: "Uniformation",
-    VIVEA: "Vivea",
-    ADDRESS_PRACTICE: "Adresse de pratique",
-    ADDRESS_HEAD_OFFICE: "Adresse du siège social",
-    ADDRESS_CONTACT: "Adresse de contact",
-    ADDRESS_BILL: "Adresse de facturation",
-    ADDRESS_OTHER: "Autre adresse",
+    EMP: 'EMP Ecole de musique privée',
+    MULTIPLE: 'Multiple',
+    UNIQUE: 'Unique',
+    MAIN_BUILDING: 'Etablissement principal',
+    SECONDARY_SCHOOL: 'Etablissement secondaire',
+    ACTALIANS: 'Actalians',
+    AFDAS: 'Afdas',
+    AGEFOS: 'Agefos',
+    AGEFOS_PME: 'Agefos pme',
+    ANFA: 'Anfa',
+    ANFH: 'Anfh',
+    APCMA: 'Apcma',
+    CNFPT: 'Cnfpt',
+    CONSTRUCTYS: 'Constructys',
+    FAF_TT: 'Faf-tt',
+    FAFIEC: 'Fafiec',
+    FAFIH: 'Fafih',
+    FAFSEA: 'Fafsea',
+    FIF_PL: 'Fif pl',
+    FONGECIF: 'Fongecif',
+    FORCO: 'Forco',
+    INTERGROS: 'Intergros',
+    OPCA3_PLUS: 'Opca3+',
+    OPCAIM: 'Opcaim',
+    OPCALIA: 'Opcalia',
+    OPCALIM: 'Opcalim',
+    OPCA_BAIA: 'Opca baia',
+    OPCA_DEFI: 'Opca defi',
+    OPCA_TRANSPORTS: 'Opca transports',
+    UNIFAF: 'Unifaf',
+    UNIFORMATION: 'Uniformation',
+    VIVEA: 'Vivea',
+    ADDRESS_PRACTICE: 'Adresse de pratique',
+    ADDRESS_HEAD_OFFICE: 'Adresse du siège social',
+    ADDRESS_CONTACT: 'Adresse de contact',
+    ADDRESS_BILL: 'Adresse de facturation',
+    ADDRESS_OTHER: 'Autre adresse',
+    PRINCIPAL: 'Contact principal',
+    BILL: 'Contact de facturation',
+    OTHER: 'Autre contact',
+    CONTACT: 'Contact'
   })
 }

+ 61 - 53
lang/field/fr-FR.js

@@ -5,63 +5,71 @@ export default (context, locale) => {
     salary: 'Salariés',
     network: 'Informations réseau',
     communication: 'Communication',
-    legalStatus: "Statut juridique",
-    siretNumber: "N° SIRET",
-    apeNumber: "Code APE",
-    waldecNumber: "RNA (ancien Waldec)",
-    identifierCmf: "Matricule CMF",
-    identifierFfec: "Matricule FFEC",
-    ffecApproval: "N° agrément FFEC",
-    description: "Description",
-    typeOfPractices: "Type de pratiques",
-    otherPractice: "Autres ensembles (préciser)",
-    principalType: "Type principal",
-    contact_point: "Point de contact",
-    name: "Nom",
-    acronym: "Sigle",
-    creationDate: "Date de création",
-    prefectureName: "Préfecture ou sous-préfecture",
-    prefectureNumber: "Numéro de déclaration",
-    declarationDate: "Date de déclaration",
-    tvaNumber: "TVA Intracommunautaire",
+    legalStatus: 'Statut juridique',
+    siretNumber: 'N° SIRET',
+    apeNumber: 'Code APE',
+    waldecNumber: 'RNA (ancien Waldec)',
+    identifierCmf: 'Matricule CMF',
+    identifierFfec: 'Matricule FFEC',
+    ffecApproval: 'N° agrément FFEC',
+    description: 'Description',
+    typeOfPractices: 'Type de pratiques',
+    otherPractice: 'Autres ensembles (préciser)',
+    principalType: 'Type principal',
+    contact_point: 'Point de contact',
+    name: 'Nom',
+    acronym: 'Sigle',
+    creationDate: 'Date de création',
+    prefectureName: 'Préfecture ou sous-préfecture',
+    prefectureNumber: 'Numéro de déclaration',
+    declarationDate: 'Date de déclaration',
+    tvaNumber: 'TVA Intracommunautaire',
     schoolCategory: "Catégorie d'école",
     typeEstablishment: "Type d'établissement",
-    typeEstablishmentDetail: "Détails du type",
-    youngApproval: "Jeunesse-éducation populaire",
-    trainingApproval: "Organisme de formation",
-    otherApproval: "Si autre, lesquels",
-    collectiveAgreement: "Nom de la convention collective",
+    typeEstablishmentDetail: 'Détails du type',
+    youngApproval: 'Jeunesse-éducation populaire',
+    trainingApproval: 'Organisme de formation',
+    otherApproval: 'Si autre, lesquels',
+    collectiveAgreement: 'Nom de la convention collective',
     opca: "Nom de l'OPCA",
-    icomNumber: "N° ICOM",
-    urssafNumber: "N° URSSAF",
-    email: "E-mail",
-    telphone: "Téléphone",
-    mobilPhone: "Portable",
-    actions: "Actions",
-    twitter: "Lien Twitter",
-    facebook: "Lien Facebook",
-    instagram: "Lien Instagram",
-    image: "Image",
+    icomNumber: 'N° ICOM',
+    urssafNumber: 'N° URSSAF',
+    email: 'E-mail',
+    emailInvalid: 'E-mail invalide',
+    telphone: 'Téléphone',
+    telphoneInvalid: 'Téléphone invalide',
+    faxNumber: 'Fax',
+    faxNumberInvalid: 'Fax invalide',
+    mobilPhone: 'Portable',
+    mobilPhoneInvalid: 'Portable invalide',
+    actions: 'Actions',
+    twitter: 'Lien Twitter',
+    facebook: 'Lien Facebook',
+    instagram: 'Lien Instagram',
+    image: 'Image',
     portailVisibility: "Répertorier la structure dans l'annuaire du portail Opentalent",
-    pedagogicBudget: "Budget pédagogique",
-    budget: "Montant du dernier budget réalisé",
+    pedagogicBudget: 'Budget pédagogique',
+    budget: 'Montant du dernier budget réalisé',
     isPedagogicIsPrincipalActivity: "L'activité principale de la stucture est « la pédagogie des arts du cirque »",
-    bank_account: "IBAN",
-    bankName: "Nom de la banque",
-    bic: "Code BIC",
-    iban: "IBAN",
-    holder: "Titulaire du compte",
-    debitAddress: "Domiciliation",
-    principal: "Principal",
-    address_postal: "Adresses postales",
-    address: "Adresse",
-    address_postal_type: "Nature",
-    addressOwner: "Chez",
-    streetAddress: "Adresses",
-    streetAddressSecond: "Adresses suite",
-    streetAddressThird: "Adresses suite 2",
-    postalCode: "Code postal",
-    addressCity: "Ville",
-    country: "Pays",
+    bank_account: 'IBAN',
+    bankName: 'Nom de la banque',
+    bic: 'Code BIC',
+    iban: 'IBAN',
+    holder: 'Titulaire du compte',
+    debitAddress: 'Domiciliation',
+    principal: 'Principal',
+    address_postal: 'Adresses postales',
+    address: 'Adresse',
+    address_postal_type: 'Nature',
+    addressOwner: 'Chez',
+    streetAddress: 'Adresses',
+    streetAddressSecond: 'Adresses suite',
+    streetAddressThird: 'Adresses suite 2',
+    postalCode: 'Code postal',
+    addressCity: 'Ville',
+    country: 'Pays',
+    addresstype: 'Nature',
+    contactpoint_type: 'Type de contact',
+    phoneNumberInvalid: 'Numéro de téléphone invalide'
   })
 }

+ 8 - 1
lang/form/fr-FR.js

@@ -2,11 +2,18 @@ export default (context, locale) => {
   return ({
     save: 'Enregistrer',
     back: 'Retour',
+    cancel: 'Annuler',
+    delete: 'Supprimer',
+    confirm_to_delete: 'Vous êtes sur le point de supprimer un élément.',
     saveSuccess: 'Sauvegarde effectuée',
+    deleteSuccess: 'Suppression effectuée',
     quit_form: 'Quitter le formulaire',
     save_and_quit: 'Sauvegarder et quitter le formulaire',
     back_to_form: 'Retourner au formulaire',
     attention: 'Attention',
-    quit_without_saving_warning: 'Vous souhaitez quitter ce formulaire sans avoir enregistré',
+    updateMap: 'Mise à jour de la carte',
+    start_your_research: 'Commencer à écrire pour rechercher...',
+    no_coordinate_corresponding: 'Aucune coordonnées GPS ne correspondent à votre adresse',
+    quit_without_saving_warning: 'Vous souhaitez quitter ce formulaire sans avoir enregistré'
   })
 }

+ 5 - 1
lang/fr-FR.js

@@ -3,13 +3,17 @@ import enums from '@/lang/enum/fr-FR'
 import fields from '@/lang/field/fr-FR'
 import rulesAndErrors from '@/lang/rulesAndErrors/fr-FR'
 import form from '@/lang/form/fr-FR'
-export default (context, locale) => {
+import breadcrumbs from '@/lang/breadcrumbs/fr-FR'
+import menuKey from '@/lang/menuKey/fr-FR'
 
+export default (context, locale) => {
   return {
     ...layout(context, locale),
     ...enums(context, locale),
     ...fields(context, locale),
     ...rulesAndErrors(context, locale),
     ...form(context, locale),
+    ...breadcrumbs(context, locale),
+    ...menuKey(context, locale)
   }
 }

+ 72 - 4
lang/layout/fr-FR.js

@@ -1,5 +1,17 @@
 export default (context, locale) => {
   return ({
+    click_here: 'cliquez ici',
+    super_admin_switch_account: 'Vous utilisez une connexion SWITCH. Afin de retourner sur votre compte veuillez',
+    insurance_cmf_subscription: 'Souscrire un contrat assurance CMF',
+    renew_insurance_cmf: 'Accéder au renouvellement de votre assurance CMF',
+    upload_cotisation_invoice: 'Télécharger la facture de votre appel de cotisation',
+    cotisation_access: 'Accéder au renouvellement de cotisation à ma fédération',
+    information_new_online_registration: 'Nouvelle préinscription',
+    not_production_environment: 'ATTENTION ! Vous êtes sur un environnement {env}.',
+    multi_account_alert: 'Vous êtes connecté, en tant que <strong>{fullname}</strong>, avec un accès famille. Utilisez l\'icône',
+    multi_account_alert_next: 'en haut à droite pour changer les informations des autres membres de votre famille.',
+    not_current_year: 'Votre logiciel est actuellement placé dans une autre année que celle actuelle, et/ou affiche des données passées/futures.',
+    not_current_year_reset: 'Cliquez ici pour afficher les données de l\'année actuelle.',
     welcome: 'Accueil',
     address_book: 'Répertoire',
     person: 'Personnes',
@@ -13,6 +25,7 @@ export default (context, locale) => {
     equipment: 'Parc matériel',
     education_state: 'Suivi pédagogique',
     criteria_notations: "Critère d'évaluation",
+    education_notation_configs: "Grilles d'évaluation",
     seizure_period: 'Périodes de saisie',
     test_seizure: 'Saisie des évaluations',
     test_validation: 'Validation par évaluation',
@@ -35,18 +48,19 @@ export default (context, locale) => {
     medals: 'Médailles',
     stats: 'Statistiques',
     report_activity: 'Rapport d\'activité',
+    educations_quotas_by_education: 'Quotas par enseignement',
     fede_stats: 'Fédérations',
     structure_stats: 'Structures',
     rate_cotisation: 'Saisie du tarif',
     parameters_cotisation: 'Paramètrer l\'appel de cotisation',
     send_cotisation: 'Appel des cotisations',
     state_cotisation: 'Suivi des cotisations',
-    pay_cotisation: 'Saisie des réglements',
+    pay_cotisation: 'Saisie des règlements',
     check_cotisation: 'Remise de chèques',
-    ledger_cotisation: 'Journal des réglements',
+    ledger_cotisation: 'Journal des règlements',
     magazine_cotisation: 'Bulletin',
-    ventilated_cotisation: 'Cotisations ventilées par sous total',
-    pay_erase_cotisation: 'Suppression de réglements',
+    ventilated_cotisation: 'Cotisations ventilées par sous-total',
+    pay_erase_cotisation: 'Suppression de règlements',
     resume_cotisation: 'Etat des transmissions',
     history_cotisation: 'Historique des cotisations',
     call_cotisation: 'Règlements à la fédération',
@@ -87,5 +101,59 @@ export default (context, locale) => {
     transition_next_year: 'Passage à l\'année suivante',
     course_duplication: 'Dupliquer les cours hebdomadaires',
     import: 'Importer',
+    schooling_year: 'Année scolaire',
+    season_year: 'Saison',
+    cotisation_year: 'Année de cotisation',
+    multiAccesses: 'Mes structures',
+    familyAccesses: 'Changement d\'utilisateur',
+    display_data: 'Afficher les données',
+    past: 'Passée',
+    present: 'Présent',
+    future: 'Future',
+    notification: 'Notifications',
+    history_help: 'Personnaliser la période d\'affichage',
+    period_choose: 'Période à afficher',
+    date_choose: 'Choix de la période',
+    my_list: 'Mes listes',
+    searchList: 'Rechercher parmi mes listes personnalisées',
+    template_systems: 'Mails système',
+    informations: 'Informations',
+    more_features: 'Plus de fonctionnalités',
+    client_id: 'Numéro de client',
+    version: 'Version',
+    services: 'Services',
+    bills: 'Factures',
+    paid: 'Payée',
+    unpaid: 'Impayée',
+    reference: 'Référence',
+    date: 'Date',
+    taxExcludedAmount: 'Montant H.T.',
+    taxIncludedAmount: 'Montant TTC',
+    status: 'Statut',
+    remaining_sms_credit: 'Crédit SMS restant',
+    paying_structure: 'Établissement payeur',
+    no_bill_to_display: 'Aucune facture à afficher',
+    my_account: 'Mon compte',
+    my_schedule_page: "Mon planning",
+    attendance_bookings_menu: "Gestion des absences & fiches de présence",
+    my_attendance: "Mes absences",
+    my_invitation: "Mes invitations",
+    my_students: "Mes élèves",
+    my_students_education_students: "Suivi pédagogique",
+    my_education_students: "Mes évaluations",
+    send_an_email: "Envoyer un email",
+    my_documents: "Mes documents",
+    my_profile: "Mon profil",
+    adherent_list: "Liste des adhérents avec leurs coordonnées",
+    my_subscription: "Mon abonnement",
+    my_bills: "Mes factures",
+    print_my_licence: "Imprimer ma licence CMF",
+    logout: "Se déconnecter",
+    all_notification: "Toutes les notifications",
+    your_file: "Votre fichier",
+    is_ready_to_be_downloaded: "est près à être téléchargé",
+    your_message: "Votre message",
+    has_been_sent: "a été envoyé",
+    ready_to_be: "est prêt à être",
   })
 }

+ 7 - 0
lang/menuKey/fr-FR.js

@@ -0,0 +1,7 @@
+export default (context, locale) => {
+  return ({
+    attendanceListMenuKey: 'Absences',
+    billAndBillCreditListMenuKey: 'Facturation',
+    personRepertoryListMenuKey: 'Répertoire'
+  })
+}

+ 4 - 2
lang/rulesAndErrors/fr-FR.js

@@ -1,7 +1,9 @@
 export default (context, locale) => {
   return ({
     required: 'Ce champs est obligatoire',
-    name_length_rule: "La taille du nom doit être de moins de 128 caractères",
-    siret_error: "N° de siret non valide",
+    name_length_rule: 'La taille du nom doit être de moins de 128 caractères',
+    siret_error: 'N° de siret non valide',
+    email_error: 'Adresse email invalide',
+    phone_error: 'Numéro de téléphone invalide'
   })
 }

+ 39 - 28
layouts/default.vue

@@ -1,54 +1,65 @@
 <template>
   <main>
-    <client-only placeholder=" ">
-    </client-only>
+    <!-- The client only is used to show the loading picture (@see https://nuxtjs.org/docs/2.x/features/nuxt-components#the-client-only-component) -->
+    <client-only placeholder=" " />
 
     <v-app dark>
+      <LayoutMenu v-if="displayedMenu" :menu="menu" :mini-variant="properties.miniVariant" />
 
-      <LayoutMenu  v-if="properties.displayedMenu" :miniVariant="properties.miniVariant"></LayoutMenu>
-
-      <LayoutHeader v-on:handle-open-menu-click="handleOpenMenu"></LayoutHeader>
+      <LayoutHeader @handle-open-menu-click="handleOpenMenu" />
 
       <v-main class="ot_content_color">
-          <nuxt/>
+        <LayoutSubheader v-if="displayedSubHeader" />
+
+        <LayoutAlertbar class="mt-1"></LayoutAlertbar>
+
+        <!-- Page will be rendered here-->
+        <nuxt />
       </v-main>
 
-      <lazy-LayoutAlertContainer></lazy-LayoutAlertContainer>
+      <lazy-LayoutAlertContainer />
     </v-app>
   </main>
 </template>
 
 <script lang="ts">
-  import {defineComponent, reactive, useContext} from '@nuxtjs/composition-api'
+import { computed, ComputedRef, defineComponent, reactive, useContext } from '@nuxtjs/composition-api'
+import { $useMenu } from '@/use/layout/menu'
 
-  export default defineComponent({
-    name: 'defaultLayout',
-    setup() {
-      const {store} = useContext()
+export default defineComponent({
+  name: 'DefaultLayout',
 
-      const properties = reactive({
-        clipped: false,
-        miniVariant: false,
-        displayedMenu:  store.state.profile.access.hasLateralMenu
-      })
+  middleware: ['auth'],
+  setup () {
+    const { store } = useContext()
+    const menu = $useMenu.setupContext().useLateralMenuConstruct()
 
-      const handleOpenMenu = (miniVariant:boolean) => {
-        properties.miniVariant = miniVariant
-      }
+    const properties = reactive({
+      clipped: false,
+      miniVariant: false
+    })
 
-      return {
-        properties,
-        handleOpenMenu
-      }
-    },
+    const displayedMenu: ComputedRef<boolean> = computed(() => store.state.profile.access.hasLateralMenu)
+    const displayedSubHeader: ComputedRef<boolean> = computed(
+      () => store.state.profile.access.hasLateralMenu || store.state.profile.access.isTeacher
+    )
 
+    const handleOpenMenu = (miniVariant: boolean) => {
+      properties.miniVariant = miniVariant
+    }
 
-    middleware: ['auth']
-  })
+    return {
+      properties,
+      menu,
+      displayedMenu,
+      displayedSubHeader,
+      handleOpenMenu
+    }
+  }
+})
 </script>
 
 <style scoped>
-
   .client-only-placeholder {
     height: 100%;
     width: 100%;

+ 20 - 28
layouts/error.vue

@@ -1,40 +1,32 @@
 <template>
   <v-app dark>
-    <h1 v-if="error.statusCode === 404">
-      {{ pageNotFound }}
-    </h1>
-    <h1 v-else>
+    <h1 v-if="error.statusCode !== 404">
       {{ otherError }}
     </h1>
-    <NuxtLink to="/">
-      Home page
-    </NuxtLink>
   </v-app>
 </template>
 
-<script>
-  export default {
-    layout: 'empty',
-    props: {
-      error: {
-        type: Object,
-        default: null
-      }
-    },
-    data () {
-      return {
-        pageNotFound: '404 Not Found',
-        otherError: 'An error occurred'
-      }
-    },
-    head () {
-      const title =
-        this.error.statusCode === 404 ? this.pageNotFound : this.otherError
-      return {
-        title
-      }
+<script lang="ts">
+import { defineComponent, useContext} from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    error: {
+      type: Object,
+      default: null
+    }
+  },
+  setup (props) {
+    const {$config} = useContext()
+    const baseLegacyUrl:string = $config.baseURL_adminLegacy
+    if(process.client && props.error.statusCode === 404)
+      window.location.href= `${baseLegacyUrl}`
+
+    return {
+      otherError: 'Une erreur est parvenue'
     }
   }
+})
 </script>
 
 <style scoped>

+ 5 - 5
layouts/login.vue

@@ -9,12 +9,12 @@
 </template>
 
 <script>
-  export default {
-    name: 'loginLayout',
-    data () {
-      return {
+export default {
+  name: 'LoginLayout',
+  data () {
+    return {
 
-      }
     }
   }
+}
 </script>

+ 1 - 1
middleware/auth.ts

@@ -1,6 +1,6 @@
 import { Middleware } from '@nuxt/types'
 
-const auth: Middleware = async ({ store, redirect }) => {
+const auth: Middleware = ({ store, redirect }) => {
   // Si l'utilisateur n'est pas connecté on le redirige vers la page login
   if (!store.state.profile.access) {
     return redirect('/login')

+ 15 - 0
models/Access/MyProfile.ts

@@ -0,0 +1,15 @@
+import {Attr, Num, Model, Uid} from '@vuex-orm/core'
+import { Historical } from '~/types/interfaces'
+
+export class MyProfile extends Model {
+  static entity = 'accesses'
+
+  @Uid()
+  id!: number | null
+
+  @Num(0, { nullable: true })
+  activityYear!: number
+
+  @Attr({})
+  historical!: Historical
+}

+ 17 - 0
models/Access/PersonalizedList.ts

@@ -0,0 +1,17 @@
+import {Model, Str, Uid} from '@vuex-orm/core'
+
+export class PersonalizedList extends Model {
+  static entity = 'personalized_lists'
+
+  @Uid()
+  id!: number | null
+
+  @Str('')
+  label!:string|null
+
+  @Str('')
+  entity!:string|null
+
+  @Str('')
+  menuKey!:string
+}

+ 12 - 12
models/Core/AddressPostal.ts

@@ -1,36 +1,36 @@
-import {Attr, Str, HasOne, Num, Model} from '@vuex-orm/core'
-import {Country} from "~/models/Core/Country";
+import {Str, HasOne, Num, Model, Uid} from '@vuex-orm/core'
+import { Country } from '~/models/Core/Country'
 
-export class AddressPostal extends Model{
+export class AddressPostal extends Model {
   static entity = 'address_postals'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @HasOne(() => Country, 'id')
   addressCountry!: Country | null
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   addressCity!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   addressOwner!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   postalCode!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   streetAddress!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   streetAddressSecond!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   streetAddressThird!: string
 
-  @Num(0, {nullable: true})
+  @Num(0, { nullable: true })
   latitude!: number
 
-  @Num(0, {nullable: true})
+  @Num(0, { nullable: true })
   longitude!: number
 }

+ 9 - 9
models/Core/BankAccount.ts

@@ -1,26 +1,26 @@
-import {Attr, Str, Bool, Model} from '@vuex-orm/core'
+import {Str, Bool, Model, Uid} from '@vuex-orm/core'
 
-export class BankAccount extends Model{
+export class BankAccount extends Model {
   static entity = 'bank_accounts'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   bankName!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   bic!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   iban!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   debitAddress!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   holder!: string
 
-  @Bool(false, {nullable: false})
+  @Bool(false, { nullable: false })
   principal!: boolean
 }

+ 22 - 7
models/Core/ContactPoint.ts

@@ -1,20 +1,35 @@
-import {Attr, Str, Model} from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
-export class ContactPoint extends Model{
+export class ContactPoint extends Model {
   static entity = 'contact_points'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
-  @Str('', {nullable: true})
+  @Str('PRINCIPAL', { nullable: false })
+  contactType!: string
+
+  @Str('', { nullable: true })
   email!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
+  emailInvalid!: string
+
+  @Str('', { nullable: true })
   telphone!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
+  telphoneInvalid!: string
+
+  @Str('', { nullable: true })
   mobilPhone!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
+  mobilPhoneInvalid!: string
+
+  @Str('', { nullable: true })
   faxNumber!: string
+
+  @Str('', { nullable: true })
+  faxNumberInvalid!: string
 }

+ 3 - 3
models/Core/Country.ts

@@ -1,9 +1,9 @@
-import {Attr, Str, Model} from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
-export class Country extends Model{
+export class Country extends Model {
   static entity = 'countries'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('')

+ 24 - 0
models/Core/Notification.ts

@@ -0,0 +1,24 @@
+import {Attr, Str, Model, HasOne, Uid} from '@vuex-orm/core'
+import {NotificationMessage} from "~/models/Core/NotificationMessage";
+
+export class Notification extends Model {
+  static entity = 'notifications'
+
+  @Uid()
+  id!: number | null
+
+  @Str('', { nullable: true })
+  name!: string
+
+  @HasOne(() => NotificationMessage, 'id')
+  message!: NotificationMessage | null
+
+  @Str('', { nullable: true })
+  type!: string
+
+  @Str('', { nullable: true })
+  link!: string
+
+  @Attr({})
+  notificationUsers!: Array<string>
+}

+ 17 - 0
models/Core/NotificationMessage.ts

@@ -0,0 +1,17 @@
+import {Str, Model, Uid} from '@vuex-orm/core'
+
+export class NotificationMessage extends Model {
+  static entity = 'notification_messages'
+
+  @Uid()
+  id!: number | null
+
+  @Str('', { nullable: true })
+  about!: string
+
+  @Str('', { nullable: true })
+  action!: string
+
+  @Str('', { nullable: true })
+  fileName!: string
+}

+ 17 - 0
models/Core/NotificationUsers.ts

@@ -0,0 +1,17 @@
+import { Str, Model, Bool, Uid } from '@vuex-orm/core'
+
+export class NotificationUsers extends Model {
+  static entity = 'notification_users'
+
+  @Uid()
+  id!: number | null
+
+  @Str('', { nullable: true })
+  notification!: string
+
+  @Str('', { nullable: true })
+  access!: string
+
+  @Bool(false, { nullable: false })
+  isRead!: boolean
+}

+ 0 - 6
models/Model.ts

@@ -1,6 +0,0 @@
-import {Model as BaseModel, Attr} from '@vuex-orm/core'
-
-export class Model extends BaseModel{
-  @Attr({})
-  originalState!: object | null
-}

+ 38 - 39
models/Organization/Organization.ts

@@ -1,117 +1,116 @@
-import {Attr, Str, Bool, Num} from '@vuex-orm/core'
-import {Model} from "~/models/Model";
+import {Attr, Str, Bool, Num, Model, Uid} from '@vuex-orm/core'
 
-export class Organization extends Model{
+export class Organization extends Model {
   static entity = 'organizations'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Attr({})
   originalState!: object | null
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   name!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   acronym!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   siretNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   apeNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   waldecNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   identifier!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   ffecApproval!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   description!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   otherPractice!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   legalStatus!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   principalType!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   youngApproval!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   trainingApproval!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   otherApproval!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   collectiveAgreement!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   opca!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   icomNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   urssafNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   twitter!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   facebook!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   instagram!: string
 
-  @Bool(true, {nullable: false})
+  @Bool(true, { nullable: false })
   portailVisibility!: boolean
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   image!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   creationDate!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   prefectureName!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   prefectureNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   declarationDate!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   tvaNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   schoolCategory!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   typeEstablishment!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   typeEstablishmentDetail!: string
 
-  @Bool(false, {nullable: false})
+  @Bool(false, { nullable: false })
   isPerformanceContractor!: boolean
 
-  @Num(0, {nullable: true})
+  @Num(0, { nullable: true })
   budget!: number
 
-  @Bool(false, {nullable: false})
+  @Bool(false, { nullable: false })
   isPedagogicIsPrincipalActivity!: boolean
 
-  @Num(0, {nullable: true})
+  @Num(0, { nullable: true })
   pedagogicBudget!: number
 }

+ 5 - 5
models/Organization/OrganizationAddressPostal.ts

@@ -1,15 +1,15 @@
-import {Attr, Str, HasOne, Model} from '@vuex-orm/core'
-import {AddressPostal} from "~/models/Core/AddressPostal";
+import {Str, HasOne, Model, Uid} from '@vuex-orm/core'
+import { AddressPostal } from '~/models/Core/AddressPostal'
 
-export class OrganizationAddressPostal extends Model{
+export class OrganizationAddressPostal extends Model {
   static entity = 'organization_address_postals'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @HasOne(() => AddressPostal, 'id')
   addressPostal!: AddressPostal | null
 
-  @Str('PRINCIPAL', {nullable: false})
+  @Str('PRINCIPAL', { nullable: false })
   type!: string
 }

+ 16 - 0
models/Organization/OrganizationContactPoint.ts

@@ -0,0 +1,16 @@
+import { Attr, Str, HasOne, Model } from '@vuex-orm/core'
+import { AddressPostal } from '~/models/Core/AddressPostal'
+import { ContactPoint } from '~/models/Core/ContactPoint'
+
+export class OrganizationAddressPostal extends Model {
+  static entity = 'organization_contact_points'
+
+  @Attr(null)
+  id!: number | null
+
+  @HasOne(() => ContactPoint, 'id')
+  contactPoint!: ContactPoint | null
+
+  @Str('PRINCIPAL', { nullable: false })
+  type!: string
+}

+ 7 - 7
models/Organization/OrganizationLicence.ts

@@ -1,20 +1,20 @@
-import {Attr, Str, Model} from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
-export class OrganizationLicence extends Model{
+export class OrganizationLicence extends Model {
   static entity = 'organization_licences'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
-  @Str('', {nullable: false})
+  @Str('', { nullable: false })
   licensee!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   licenceNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   categorie!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   validityDate!: string
 }

+ 7 - 7
models/Organization/OrganizationNetwork.ts

@@ -1,20 +1,20 @@
-import {Attr, Str, Model} from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
-export class OrganizationLicence extends Model{
+export class OrganizationLicence extends Model {
   static entity = 'organization_licences'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
-  @Str('', {nullable: false})
+  @Str('', { nullable: false })
   licensee!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   licenceNumber!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   categorie!: string
 
-  @Str('', {nullable: true})
+  @Str('', { nullable: true })
   validityDate!: string
 }

+ 1 - 2
nuxt.config.js

@@ -15,6 +15,5 @@ export default {
   ...plugins,
   ...i18n,
   ...vuetify,
-  ...moment,
+  ...moment
 }
-

+ 61 - 47
package.json

@@ -1,65 +1,79 @@
 {
   "name": "admin",
-  "version": "1.0.0",
+  "version": "0.1.0",
   "private": true,
   "scripts": {
-    "dev": "nuxt",
+    "dev": "nuxt --hostname '0.0.0.0' --port 3002",
+    "dev:local": "yarn dev --dotenv .env.local",
+    "dev:preprod": "yarn dev --dotenv .env.preprod",
+    "dev:prod": "yarn dev --dotenv .env.prod",
     "build": "nuxt build",
-    "start": "nuxt start",
-    "generate": "nuxt generate",
+    "build:local": "yarn build --dotenv .env.local",
+    "build:preprod": "yarn build --dotenv .env.preprod",
+    "build:prod": "yarn build --dotenv .env.prod",
+    "start": "nuxt start --hostname '127.0.0.1' --port 3002",
+    "start:local": "yarn start --dotenv .env.local",
+    "start:preprod": "yarn start --dotenv .env.preprod",
+    "start:prod": "yarn start --dotenv .env.prod",
+    "deploy": "git pull && yarn install && yarn build && pm2 start app",
     "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
     "lint": "yarn lint:js",
     "test": "jest",
     "docs": "jsdoc -c 'jsdoc.json' ./"
   },
   "dependencies": {
-    "@casl/ability": "^5.1.0",
-    "@casl/vue": "^1.2.1",
-    "@nuxt/components": "^2.1.8",
-    "@nuxt/typescript-runtime": "^2.0.0",
-    "@nuxtjs/axios": "^5.13.1",
-    "@nuxtjs/composition-api": "0.24.2",
-    "@types/lodash": "^4.14.168",
+    "@casl/ability": "^5.4",
+    "@casl/vue": "^1.2",
+    "@fortawesome/fontawesome-free": "^5.15",
+    "@nuxt/components": "^2.2",
+    "@nuxt/image": "^0.6.0",
+    "@nuxt/typescript-runtime": "^2.0",
+    "@nuxtjs/axios": "^5.13",
+    "@nuxtjs/composition-api": "^0.29.3",
+    "@nuxtjs/i18n": "^7.0",
+    "@nuxtjs/vuetify": "^1.12.1",
+    "@types/lodash": "^4.14",
     "@vuex-orm/core": "1.0.0-draft.14",
-    "cookieparser": "^0.1.0",
-    "core-js": "^3.6.5",
-    "js-yaml": "^4.0.0",
-    "lodash": "^4.17.20",
-    "marked": "^1.2.7",
-    "nuxt": "2.15.6",
-    "nuxt-i18n": "6.27.0",
-    "vue-i18n-composable": "^0.2.1",
-    "vue-template-compiler": "^2.6.13",
-    "yaml-import": "^2.0.0"
+    "cookieparser": "^0.1",
+    "core-js": "^3.17",
+    "js-yaml": "^4.0",
+    "libphonenumber-js": "^1.9.39",
+    "lodash": "^4.17",
+    "marked": "^3.0",
+    "nuxt": "^2.15",
+    "nuxt-leaflet": "^0.0",
+    "postcss": "^8.3.6",
+    "vue-i18n-composable": "^0.2",
+    "vue-tel-input-vuetify": "^1.4.1",
+    "vue-template-compiler": "^2.6",
+    "webpack": "^4.46",
+    "yaml-import": "^2.0"
   },
   "devDependencies": {
-    "@fortawesome/fontawesome-free": "^5.15.2",
-    "sass": "~1.32.12",
-    "@nuxt/test-utils": "^0.2.2",
-    "@nuxt/types": "^2.14.6",
-    "@nuxt/typescript-build": "^2.0.3",
-    "@nuxtjs/eslint-config": "^3.1.0",
-    "@nuxtjs/eslint-config-typescript": "^3.0.0",
-    "@nuxtjs/eslint-module": "^2.0.0",
-    "@nuxtjs/moment": "^1.6.1",
-    "@nuxtjs/vuetify": "1.11.3",
-    "@types/jest": "^26.0.23",
-    "@vue/test-utils": "^1.1.0",
+    "@nuxt/test-utils": "^0.2",
+    "@nuxt/types": "^2.15",
+    "@nuxt/typescript-build": "^2.0",
+    "@nuxtjs/eslint-config": "^6.0",
+    "@nuxtjs/eslint-config-typescript": "^6.0",
+    "@nuxtjs/eslint-module": "^3.0",
+    "@nuxtjs/moment": "^1.6",
+    "@types/jest": "^27.0",
+    "@vue/test-utils": "^1.1",
     "babel-core": "7.0.0-bridge.0",
-    "babel-eslint": "^10.1.0",
-    "babel-jest": "^26.5.0",
-    "better-docs": "^2.3.2",
-    "css-loader": "^5.0.0",
-    "eslint": "^7.10.0",
-    "eslint-plugin-nuxt": "^1.0.0",
-    "jest": "^26.5.0",
-    "jsdoc": "^3.6.6",
-    "postcss": "^8.1.10",
-    "postcss-import": "^13.0.0",
-    "postcss-loader": "^4.1.0",
-    "postcss-url": "^10.1.1",
-    "ts-jest": "^26.4.1",
-    "vue-jest": "^3.0.4"
+    "babel-eslint": "^10.1",
+    "babel-jest": "^27.1",
+    "better-docs": "^2.3",
+    "css-loader": "^4.2",
+    "eslint": "^7.32",
+    "eslint-plugin-nuxt": "^2.0",
+    "jest": "^27.1",
+    "jsdoc": "^3.6",
+    "postcss-import": "^13.0",
+    "postcss-loader": "^4.1",
+    "postcss-url": "^10.1",
+    "sass": "^1.32.12",
+    "ts-jest": "^27.0",
+    "vue-jest": "^3.0"
   },
   "resolutions": {
     "@nuxtjs/vuetify/**/sass": "1.32.12"

+ 12 - 9
pages/index.vue

@@ -1,23 +1,26 @@
+<!-- Page d'accueil de l'application -->
+
 <template>
   <v-row justify="center" align="center">
     <v-col cols="12" sm="12" md="12">
       <h3>Bienvenue !</h3>
-      <NuxtLink to="organization">Organization</NuxtLink>
+      <NuxtLink to="organization">
+        Organization
+      </NuxtLink>
     </v-col>
   </v-row>
 </template>
 
-
 <script lang="ts">
-  import {defineComponent} from '@nuxtjs/composition-api'
+import { defineComponent } from '@nuxtjs/composition-api'
 
-  export default defineComponent({
-    name: 'index',
-    setup() {
-      return {
+export default defineComponent({
+  name: 'Index',
+  setup () {
+    return {
 
-      }
     }
-  })
+  }
+})
 
 </script>

Некоторые файлы не были показаны из-за большого количества измененных файлов