瀏覽代碼

Merge branch 'release/0.2.0'

# Conflicts:
#	.env.local
Vincent GUFFON 3 年之前
父節點
當前提交
b8fd2e72d3
共有 100 個文件被更改,包括 3037 次插入527 次删除
  1. 二進制
      .output/public/images/Artist-Square.jpg
  2. 二進制
      .output/public/images/School-Square.jpg
  3. 二進制
      .output/public/images/nom-de-domaine.jpg
  4. 二進制
      .output/public/images/sms_big.png
  5. 0 0
      .output/public/robots.txt
  6. 11 4
      assets/css/global.scss
  7. 238 0
      components/Form/Organization/Address.vue
  8. 98 0
      components/Form/Organization/BankAccount.vue
  9. 115 0
      components/Form/Organization/ContactPoint.vue
  10. 29 0
      components/Form/Toolbar.vue
  11. 2 2
      components/Layout/Alert/Container.vue
  12. 8 3
      components/Layout/Alert/Content.vue
  13. 1 1
      components/Layout/AlertBar/Cotisation.vue
  14. 2 5
      components/Layout/AlertBar/SuperAdmin.vue
  15. 3 4
      components/Layout/AlertBar/SwitchUser.vue
  16. 8 6
      components/Layout/AlertBar/SwitchYear.vue
  17. 61 9
      components/Layout/Dialog.vue
  18. 17 9
      components/Layout/Header.vue
  19. 4 5
      components/Layout/Header/Menu.vue
  20. 15 12
      components/Layout/Header/Notification.vue
  21. 61 0
      components/Layout/Header/UniversalCreation/CreateButton.vue
  22. 143 0
      components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue
  23. 91 0
      components/Layout/Header/UniversalCreation/TypeCard.vue
  24. 5 1
      components/Layout/Loading.vue
  25. 1 1
      components/Layout/Menu.vue
  26. 6 6
      components/Layout/SubHeader/ActivityYear.vue
  27. 6 7
      components/Layout/SubHeader/DataTiming.vue
  28. 7 8
      components/Layout/SubHeader/DataTimingRange.vue
  29. 1 1
      components/Layout/SubHeader/PersonnalizedList.vue
  30. 11 19
      components/Ui/Button/Delete.vue
  31. 95 0
      components/Ui/Button/Submit.vue
  32. 2 4
      components/Ui/DataTable.vue
  33. 46 0
      components/Ui/Display/MobytStatus.vue
  34. 93 62
      components/Ui/Form.vue
  35. 120 47
      components/Ui/Image.vue
  36. 126 21
      components/Ui/Input/Autocomplete.vue
  37. 38 52
      components/Ui/Input/AutocompleteWithAPI.vue
  38. 10 3
      components/Ui/Input/Checkbox.vue
  39. 7 3
      components/Ui/Input/DatePicker.vue
  40. 7 13
      components/Ui/Input/Email.vue
  41. 10 4
      components/Ui/Input/Enum.vue
  42. 305 0
      components/Ui/Input/Image.vue
  43. 37 34
      components/Ui/Input/Phone.vue
  44. 31 13
      components/Ui/Input/Text.vue
  45. 76 0
      components/Ui/Input/TextArea.vue
  46. 72 0
      components/Ui/ItemFromUri.vue
  47. 23 32
      components/Ui/Map.vue
  48. 18 3
      components/Ui/SubResource.vue
  49. 65 0
      components/Ui/Template/DataTable.vue
  50. 34 0
      components/Ui/Template/Date.vue
  51. 41 0
      composables/data/useAccess.ts
  52. 75 0
      composables/data/useAddresspostal.ts
  53. 42 0
      composables/data/useCountry.ts
  54. 74 0
      composables/data/useDataUtils.ts
  55. 64 0
      composables/data/useImage.ts
  56. 104 0
      composables/data/useMyProfile.ts
  57. 42 0
      composables/data/useTypeOfPractice.ts
  58. 41 0
      composables/form/useError.ts
  59. 13 7
      composables/form/useForm.ts
  60. 5 0
      composables/form/useNavigationHelpers.ts
  61. 51 0
      composables/form/useNextStepFactory.ts
  62. 2 2
      composables/form/useValidator.ts
  63. 2 2
      composables/layout/Menus/accessMenu.ts
  64. 2 2
      composables/layout/Menus/accountMenu.ts
  65. 2 2
      composables/layout/Menus/admin2iosMenu.ts
  66. 2 2
      composables/layout/Menus/agendaMenu.ts
  67. 1 1
      composables/layout/Menus/baseMenu.ts
  68. 2 2
      composables/layout/Menus/billingMenu.ts
  69. 2 2
      composables/layout/Menus/communicationMenu.ts
  70. 2 2
      composables/layout/Menus/configurationMenu.ts
  71. 3 3
      composables/layout/Menus/cotisationsMenu.ts
  72. 2 2
      composables/layout/Menus/donorsMenu.ts
  73. 2 2
      composables/layout/Menus/educationalMenu.ts
  74. 2 2
      composables/layout/Menus/equipmentMenu.ts
  75. 2 2
      composables/layout/Menus/medalsMenu.ts
  76. 2 2
      composables/layout/Menus/myAccessesMenu.ts
  77. 2 2
      composables/layout/Menus/myFamilyMenu.ts
  78. 2 2
      composables/layout/Menus/statsMenu.ts
  79. 9 6
      composables/layout/Menus/websiteMenu.ts
  80. 17 17
      composables/layout/menu.ts
  81. 3 3
      config/abilities/pages/communication.yaml
  82. 2 1
      config/nuxtConfig/env.js
  83. 1 0
      config/nuxtConfig/plugins.js
  84. 1 1
      jest.config.js
  85. 42 0
      lang/content/subscription/fr-FR.js
  86. 44 3
      lang/enum/fr-FR.js
  87. 11 0
      lang/field/fr-FR.js
  88. 7 0
      lang/form/fr-FR.js
  89. 3 1
      lang/fr-FR.js
  90. 41 0
      lang/layout/fr-FR.js
  91. 9 1
      lang/rulesAndErrors/fr-FR.js
  92. 1 1
      layouts/default.vue
  93. 1 1
      models/Access/MyProfile.ts
  94. 3 3
      models/Access/PersonalizedList.ts
  95. 22 17
      models/Core/AddressPostal.ts
  96. 21 12
      models/Core/BankAccount.ts
  97. 21 18
      models/Core/ContactPoint.ts
  98. 1 1
      models/Core/Country.ts
  99. 26 0
      models/Core/File.ts
  100. 6 6
      models/Core/Notification.ts

二進制
.output/public/images/Artist-Square.jpg


二進制
.output/public/images/School-Square.jpg


二進制
.output/public/images/nom-de-domaine.jpg


二進制
.output/public/images/sms_big.png


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


+ 11 - 4
assets/css/global.scss

@@ -14,10 +14,6 @@ header .v-toolbar__content{
   color: inherit !important;
 }
 
-.margin-bottom-20{
-  margin-bottom: 20px;
-}
-
 .v-application a{
   color: var(--v-ot_green-base, white)
 }
@@ -28,4 +24,15 @@ header .v-toolbar__content{
   overflow-y: scroll
 }
 
+.v-menu__content{
+  z-index: 400 !important;
+}
+
+.toolbarForm .v-toolbar__content{
+  padding-left: 0 !important;
+}
 
+.toolbarForm .v-toolbar__title .v-icon{
+  height: 46px;
+  width: 46px;
+}

+ 238 - 0
components/Form/Organization/Address.vue

@@ -0,0 +1,238 @@
+<!-- Component d'un formulaire d'une adresse postale d'organization -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5">
+        <FormToolbar title="address_postal" icon="fa-globe-europe"/>
+
+        <UiForm
+          :id="id"
+          :model="model"
+          :query="query()"
+          :submitActions="submitActions">
+          <template #form.input="{entry, updateRepository}">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiInputAutocompleteWithAPI
+                    field="owner"
+                    label="importAddress"
+                    :item-text="['person.givenName', 'person.name']"
+                    :searchFunction="accessSearch"
+                    @update="updateAccessAddress"
+                  />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputEnum field="type" label="addresstype" :data="entry['type']" enum-type="address_postal_organization" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="addressPostal.addressOwner" label="addressOwner" :data="entry['addressPostal.addressOwner']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="addressPostal.streetAddress" label="streetAddress" :data="entry['addressPostal.streetAddress']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="addressPostal.streetAddressSecond" label="streetAddressSecond" :data="entry['addressPostal.streetAddressSecond']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="addressPostal.streetAddressThird" label="streetAddressThird" :data="entry['addressPostal.streetAddressThird']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputAutocompleteWithAPI
+                    field="addressPostal.postalCode"
+                    label="postalCode"
+                    :data="getAutoCompleteAddressItem"
+                    :item-text="['postcode']"
+                    :slotText="['postcode', 'city']"
+                    :searchFunction="addressSearch"
+                    :returnObject="true"
+                    @update="updateCpAddress($event, updateRepository)"
+                  />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputAutocompleteWithAPI
+                    field="addressPostal.addressCity"
+                    label="addressCity"
+                    :data="getAutoCompleteAddressItem"
+                    :item-text="['city']"
+                    :slotText="['postcode', 'city']"
+                    :searchFunction="addressSearch"
+                    :returnObject="true"
+                    @update="updateCpAddress($event, updateRepository)"
+                  />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputAutocomplete
+                    field="addressPostal.addressCountry"
+                    label="addressCountry"
+                    :data="getIdFromUri(entry['addressPostal.addressCountry'])"
+                    :items="countries"
+                    :isLoading="countriesFetchingState.pending"
+                    :item-text="['name']"
+                    @update="updateRepository(`/api/countries/${$event}`, 'addressPostal.addressCountry')"
+                  />
+                </v-col>
+
+              </v-row>
+            </v-container>
+
+            <UiMap :zoom="12" :address="addressPostalItem()" @updateAddress="updateAddressFromMap" />
+
+          </template>
+
+          <template #form.button>
+            <NuxtLink :to="{ path: '/organization', query: { accordion: 'address_postal' }}" class="no-decoration">
+              <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                {{ $t('back') }}
+              </v-btn>
+            </NuxtLink>
+          </template>
+
+        </UiForm>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext, computed, ComputedRef, Ref, ref } from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import { Query, Model } from '@vuex-orm/core'
+import { OrganizationAddressPostal } from '~/models/Organization/OrganizationAddressPostal'
+import {Country} from "~/models/Core/Country";
+import {QUERY_TYPE, SUBMIT_TYPE} from '~/types/enums'
+import { repositoryHelper } from '~/services/store/repository'
+import { queryHelper } from '~/services/store/query'
+import { AddressPostal } from '~/models/Core/AddressPostal'
+import {UseCountry} from "~/composables/data/useCountry";
+import ModelsUtils from "~/services/utils/modelsUtils";
+import {UseAddressPostal} from "~/composables/data/useAddresspostal";
+import {AnyJson} from "~/types/interfaces";
+import DataProvider from "~/services/data/dataProvider";
+import {UseAccess} from "~/composables/data/useAccess";
+
+export default defineComponent({
+  props: {
+    id:{
+      type: [Number, String],
+      required: true
+    }
+  },
+  setup (props) {
+    const { $dataProvider } = useContext()
+
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(OrganizationAddressPostal)
+    const query: Query = repository.with('addressPostal')
+
+    const organizationAddressPostalItem: ComputedRef<OrganizationAddressPostal> = computed(() => {
+      return queryHelper.getItem(query, props.id) as OrganizationAddressPostal
+    })
+
+    const addressPostalItem: ComputedRef<AddressPostal|null> = computed(() => {
+      return organizationAddressPostalItem.value?.addressPostal || null
+    })
+
+    const updateAddressFromMap = (addressPostal: AddressPostal) => {
+      repositoryHelper.persist(AddressPostal, addressPostal)
+    }
+
+    const getAutoCompleteAddressItem = computed(() => {
+      if(addressPostalItem.value?.addressCity || addressPostalItem.value?.postalCode)
+        return {id:0, city: addressPostalItem.value?.addressCity, postcode: addressPostalItem.value?.postalCode}
+      return {}
+    })
+
+    const {countries, fetchState: countriesFetchingState} = new UseCountry().getAll()
+
+    const {searchFunction: addressSearch, updateCpAddress} = new UseAddressPostal().invoke()
+
+    const {getPhysicalByFullName: accessSearch} = new UseAccess().invoke()
+
+    const {updateAccessAddress} = accessOwnerResearch($dataProvider, organizationAddressPostalItem, addressPostalItem)
+
+    const getIdFromUri = (uri: string) => {
+      return ModelsUtils.extractIdFromUri(uri)
+    }
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/organization`, query: { accordion: 'address_postal' } }
+      actions[SUBMIT_TYPE.SAVE] = { path: `/organization/address/` }
+      return actions
+    })
+
+    /** Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    return {
+      model: OrganizationAddressPostal,
+      query: () => query,
+      panel: 0,
+      addressPostalItem: () => addressPostalItem,
+      countries,
+      countriesFetchingState,
+      addressSearch,
+      getIdFromUri,
+      getAutoCompleteAddressItem,
+      accessSearch,
+      updateAccessAddress,
+      updateAddressFromMap,
+      updateCpAddress,
+      submitActions
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(OrganizationAddressPostal)
+    repositoryHelper.cleanRepository(Country)
+    repositoryHelper.cleanRepository(AddressPostal)
+  }
+})
+
+/**
+ * Fonction permettant la mise à jour des champs de l'adresse suivant une adresse d'un Access
+ * @param $dataProvider
+ * @param organizationAddressPostalItem
+ * @param addressItem
+ */
+function accessOwnerResearch($dataProvider:DataProvider, organizationAddressPostalItem:ComputedRef<OrganizationAddressPostal>, addressItem:ComputedRef<AddressPostal|null>){
+  const updateAccessAddress = async (accessId: number) =>{
+    const response = await $dataProvider.invoke({
+      type: QUERY_TYPE.DEFAULT,
+      url: `api/access_addresses`,
+      id:accessId
+    })
+
+    //On ne conserve que l'adresse principale
+    const principalPersonalAddress = response.data.person.personAddressPostal.filter((personAddress: AnyJson) => {
+      return personAddress.type === 'ADDRESS_PRINCIPAL'
+    })
+
+    if(principalPersonalAddress.length > 0){
+      const personalAddress = principalPersonalAddress.pop()
+
+      //On créer la nouvelle adresse et on initialise les champs restants...
+      const addressPostal:AddressPostal = new AddressPostal(personalAddress.addressPostal)
+      addressPostal.addressOwner = `${response.data.person.name} ${response.data.person.givenName}`
+      addressPostal['@id'] = ''
+
+      if(addressItem.value)
+        addressPostal.id = addressItem.value.id
+
+      //On l'associe à l'OrganizationAddressPostal qui est éditée, et on persist
+      organizationAddressPostalItem.value.addressPostal = addressPostal
+      repositoryHelper.persist(OrganizationAddressPostal, organizationAddressPostalItem.value)
+    }
+  }
+
+  return {
+    updateAccessAddress
+  }
+}
+
+</script>

+ 98 - 0
components/Form/Organization/BankAccount.vue

@@ -0,0 +1,98 @@
+<!-- Component d'un formulaire d'un banque account d'organization -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5">
+        <FormToolbar title="bank_account" icon="fa-euro-sign"/>
+
+        <UiForm
+          :id="id"
+          :model="model"
+          :query="query()"
+          :submitActions="submitActions">
+          <template #form.input="{entry, updateRepository}">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="bankName" :data="entry['bankName']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="bic" :data="entry['bic']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="iban" :data="entry['iban']" @update="updateRepository" :mask="['AA ## ##### ##### XXXXXXXXXXX ##']" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="debitAddress" :data="entry['debitAddress']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="holder" :data="entry['holder']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="principal" :data="entry['principal']" @update="updateRepository" />
+                </v-col>
+
+              </v-row>
+            </v-container>
+
+          </template>
+
+          <template #form.button>
+            <NuxtLink :to="{ path: '/organization', query: { accordion: 'bank_account' }}" class="no-decoration">
+              <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                {{ $t('back') }}
+              </v-btn>
+            </NuxtLink>
+          </template>
+
+        </UiForm>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed} from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import { Query, Model } from '@vuex-orm/core'
+import {SUBMIT_TYPE} from '~/types/enums'
+import { repositoryHelper } from '~/services/store/repository'
+import {AnyJson} from "~/types/interfaces";
+import {BankAccount} from "~/models/Core/BankAccount";
+
+export default defineComponent({
+  props: {
+    id:{
+      type: [Number, String],
+      required: true
+    }
+  },
+  setup () {
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(BankAccount)
+    const query: Query = repository.query()
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/organization`, query: { accordion: 'bank_account' } }
+      actions[SUBMIT_TYPE.SAVE] = { path: `/organization/bank_account/` }
+      return actions
+    })
+
+    return {
+      model: BankAccount,
+      query: () => query,
+      submitActions
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(BankAccount)
+  }
+})
+
+</script>

+ 115 - 0
components/Form/Organization/ContactPoint.vue

@@ -0,0 +1,115 @@
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5">
+        <FormToolbar title="contact_point" icon="fa-phone"/>
+
+        <UiForm
+          :id="id"
+          :model="model"
+          :query="query()"
+          :submitActions="submitActions">
+        >
+          <template #form.input="{entry, updateRepository}">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiInputEnum
+                    field="contactType"
+                    label="contactpoint_type"
+                    :data="entry['contactType']"
+                    enum-type="contact_point_type"
+                    @update="updateRepository"
+                  />
+                </v-col>
+                <v-col cols="12" sm="6">
+                  <UiInputEmail
+                    field="email"
+                    label="email"
+                    :data="entry['email']"
+                    @update="updateRepository"
+                  />
+                </v-col>
+                <v-col cols="12" sm="6">
+                  <UiInputPhone
+                    field="telphone"
+                    :label="$t('telphone')"
+                    :data="entry['telphone']"
+                    @update="updateRepository"
+                  />
+                </v-col>
+                <v-col cols="12" sm="6">
+                  <UiInputPhone
+                    field="faxNumber"
+                    :label="$t('faxNumber')"
+                    :data="entry['faxNumber']"
+                    @update="updateRepository"
+                  />
+                </v-col>
+                <v-col cols="12" sm="6">
+                  <UiInputPhone
+                    field="mobilPhone"
+                    :label="$t('mobilPhone')"
+                    :data="entry['mobilPhone']"
+                    @update="updateRepository"
+                  />
+                </v-col>
+              </v-row>
+            </v-container>
+          </template>
+
+          <template #form.button>
+            <NuxtLink :to="{ path: '/organization', query: { accordion: 'contact_point' }}" class="no-decoration">
+              <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                {{ $t('back') }}
+              </v-btn>
+            </NuxtLink>
+          </template>
+        </UiForm>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import {computed, defineComponent} from "@nuxtjs/composition-api";
+import {repositoryHelper} from "~/services/store/repository";
+import {ContactPoint} from "~/models/Core/ContactPoint";
+import {Repository as VuexRepository} from "@vuex-orm/core/dist/src/repository/Repository";
+import {Model, Query} from "@vuex-orm/core";
+import {AnyJson} from "~/types/interfaces";
+import {SUBMIT_TYPE} from "~/types/enums";
+
+export default defineComponent({
+  props: {
+    id:{
+      type: [Number, String],
+      required: true
+    }
+  },
+  setup () {
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(ContactPoint)
+    const query: Query = repository.query()
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/organization`, query: { accordion: 'contact_point' } }
+      actions[SUBMIT_TYPE.SAVE] = { path: `/organization/contact_points/` }
+      return actions
+    })
+
+    return {
+      model: ContactPoint,
+      query: () => query,
+      submitActions
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(ContactPoint)
+  },
+})
+</script>
+
+<style scoped>
+
+</style>

+ 29 - 0
components/Form/Toolbar.vue

@@ -0,0 +1,29 @@
+<template>
+  <v-toolbar flat class="ot_light_grey toolbarForm" dark dense rounded>
+    <v-toolbar-title class="ot_dark_grey--text form_main_title">
+      <v-icon class="ot_white--text ot_green icon">
+         {{icon}}
+      </v-icon>
+      {{ $t(title) }}
+    </v-toolbar-title>
+  </v-toolbar>
+</template>
+
+<script>
+export default {
+  props: {
+    title: {
+      type: String,
+      required: true
+    },
+    icon: {
+      type: String,
+      required: true
+    },
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

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

@@ -15,13 +15,13 @@ Container principal pour l'affichage d'une ou plusieurs alertes
 
 <script lang="ts">
 import { defineComponent, computed, ComputedRef, useContext } from '@nuxtjs/composition-api'
-import { alert } from '~/types/interfaces'
+import { Alert } from '~/types/interfaces'
 
 export default defineComponent({
   setup () {
     const { store } = useContext()
 
-    const alerts: ComputedRef<Array<alert>> = computed(() => {
+    const alerts: ComputedRef<Array<Alert>> = computed(() => {
       return store.state.page.alerts
     })
     return {

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

@@ -12,18 +12,23 @@
     @mouseover="onMouseOver"
     @mouseout="onMouseOut"
   >
-    {{ $t(alert.message) }}
+    <ul v-if="alert.messages.length > 1">
+       <li v-for="message in alert.messages">
+        {{ message }}
+      </li>
+    </ul>
+    <span v-else>{{alert.messages[0]}}</span>
   </v-alert>
 </template>
 
 <script lang="ts">
 import { defineComponent, ref, Ref, useContext } from '@nuxtjs/composition-api'
-import { alert } from '~/types/interfaces'
+import {Alert} from '~/types/interfaces'
 
 export default defineComponent({
   props: {
     alert: {
-      type: Object as () => alert,
+      type: Object as () => Alert,
       required: true
     }
   },

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

@@ -62,7 +62,7 @@ export default defineComponent({
       const response = await $dataProvider.invoke({
         type: QUERY_TYPE.DEFAULT,
         id: profileOrganization.id,
-        url: 'cotisations'
+        url: '/api/cotisations'
       })
       cotisation_year.value = response.data.cotisationYear
       handleShow(response.data.alertState)

+ 2 - 5
components/Layout/AlertBar/SuperAdmin.vue

@@ -15,15 +15,12 @@ Super Admin bar : Barre qui s'affiche lorsque l'utilisateur est un super admin e
 </template>
 
 <script lang="ts">
-import { defineComponent, useStore, useContext } from '@nuxtjs/composition-api'
-import { State } from '@vuex-orm/core'
-import { AnyStore } from '~/types/interfaces'
+import { defineComponent, useContext } from '@nuxtjs/composition-api'
 
 export default defineComponent({
   setup () {
-    const { $config } = useContext()
+    const { $config, store } = useContext()
     const baseLegacyUrl:string = $config.baseURL_adminLegacy
-    const store:AnyStore = useStore<State>()
     const originalAccess = store.state.profile.access.originalAccess
     let show = false
     let url = ''

+ 3 - 4
components/Layout/AlertBar/SwitchUser.vue

@@ -14,13 +14,12 @@ Switch year bar : Barre qui s'affiche lorsque l'utilisateur possède un compte m
 </template>
 
 <script lang="ts">
-import { defineComponent, useStore} from '@nuxtjs/composition-api'
-import {accessState, AnyStore} from "~/types/interfaces";
-import {State} from "@vuex-orm/core";
+import { defineComponent, useContext} from '@nuxtjs/composition-api'
+import {accessState} from "~/types/interfaces";
 
 export default defineComponent({
   setup () {
-    const store:AnyStore = useStore<State>()
+    const {store} = useContext()
     const profileAccess:accessState = store.state.profile.access
 
     return {

+ 8 - 6
components/Layout/AlertBar/SwitchYear.vue

@@ -14,14 +14,14 @@ Switch year bar : Barre qui s'affiche lorsque l'utilisateur n'est pas sur l'ann
 <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";
+import {$useForm} from "~/composables/form/useForm";
+import { useMyProfile } from '~/composables/data/useMyProfile'
 
 export default defineComponent({
   setup () {
-    const { store, $dataPersister } = useContext()
-    const { markFormAsNotDirty } = $useDirtyForm(store)
-    const { updateMyProfile, setHistorical, setActivityYear } = $useMyProfileUpdater(store, $dataPersister)
+    const { store } = useContext()
+    const { markFormAsNotDirty } = $useForm()
+    const { updateMyProfile, setHistorical, setActivityYear } = useMyProfile()
 
     const profileAccess:accessState = store.state.profile.access
     const profileOrganization:organizationState = store.state.profile.organization
@@ -38,7 +38,9 @@ export default defineComponent({
 
     const resetYear = async () =>{
       setHistorical(['present'])
-      setActivityYear(profileOrganization.currentActivityYear)
+      if(profileOrganization.currentActivityYear)
+        setActivityYear(profileOrganization.currentActivityYear)
+      
       markFormAsNotDirty()
       await updateMyProfile()
       window.location.reload()

+ 61 - 9
components/Layout/Dialog.vue

@@ -1,20 +1,35 @@
 <!-- Fenêtre de dialogue -->
-
 <template>
   <v-dialog
     v-model="show"
     persistent
     max-width="800"
+    :content-class="contentClass"
   >
-    <v-card>
-      <slot name="dialogText" />
+    <v-card  class="d-flex">
+
+        <div class="dialog-type flex-column justify-center d-none d-sm-flex">
+          <h3 class="d-flex"> <slot name="dialogType"></slot></h3>
+        </div>
+
+        <div class="dialog-container flex-column flex-grow-1">
+          <div class="d-flex flex-column">
+            <v-card-title class="dialog-title">
+              <slot name="dialogTitle"></slot>
+            </v-card-title>
+            <div class="dialog-text-container">
+              <slot name="dialogText" />
+            </div>
+
+            <v-divider />
 
-      <v-divider />
+            <v-card-actions class="justify-center">
+              <slot name="dialogBtn" />
+            </v-card-actions>
+          </div>
+
+        </div>
 
-      <v-card-actions>
-        <v-spacer />
-        <slot name="dialogBtn" />
-      </v-card-actions>
     </v-card>
   </v-dialog>
 </template>
@@ -27,10 +42,47 @@ export default defineComponent({
     show: {
       type: Boolean,
       required: true
+    },
+    contentClass: {
+      type: String,
+      required: false
     }
   }
 })
 </script>
 
-<style scoped>
+<style lang="scss" scoped>
+  .dialog-title{
+    background: #e6e6e6;
+    padding-left: 40px;
+    font-weight: normal;
+  }
+  .dialog-type{
+    background: var(--v-ot_green-base, #00AD8E);
+    color: #fff;
+   h3{
+     font-size: 25px;
+     font-weight: normal;
+     writing-mode: tb-lr;
+     writing-mode: vertical-lr;
+     transform: rotate(
+         -180deg);
+     padding: 10px;
+    }
+  }
+
+  .dialog-text-container{
+    max-height: 70vh;
+    overflow: auto;
+  }
+  .modal-level-alert{
+    .dialog-type{
+      background: var(--v-ot_danger-base, #f56954);
+    }
+  }
+  .modal-level-warning{
+    .dialog-type{
+      background: var(--v-ot_warning-base, #f39c12);
+    }
+  }
 </style>

+ 17 - 9
components/Layout/Header.vue

@@ -27,12 +27,7 @@ et aux préférences de l'utilisateur
 
     <v-spacer />
 
-    <v-btn
-      elevation="2"
-      color="ot_warning ot_white--text"
-    >
-      {{ $t('create') }}
-    </v-btn>
+    <LayoutHeaderUniversalCreationCreateButton v-if="showUniversalButton" />
 
     <v-tooltip bottom>
       <template #activator="{ on, attrs }">
@@ -71,12 +66,12 @@ et aux préférences de l'utilisateur
 import {
   defineComponent, reactive, useContext, computed, ComputedRef, Ref, UnwrapRef
 } from '@nuxtjs/composition-api'
-import { $useMenu } from '~/use/layout/menu'
+import { $useMenu } from '~/composables/layout/menu'
 import { AnyJson } from '~/types/interfaces'
 
 export default defineComponent({
   setup (_props, { emit }) {
-    const { store, $config } = useContext()
+    const { store, $config,$ability } = useContext()
 
     const properties:UnwrapRef<AnyJson> = reactive({
       miniVariant: false,
@@ -100,6 +95,18 @@ export default defineComponent({
       emit('handle-open-menu-click', properties.miniVariant)
     }
 
+    const showUniversalButton =
+        $ability.can('manage', 'users')
+      || $ability.can('manage', 'courses')
+      || $ability.can('manage', 'examens')
+      || $ability.can('manage', 'educationalprojects')
+      || $ability.can('manage', 'events')
+      || $ability.can('manage', 'emails')
+      || $ability.can('manage', 'mails')
+      || $ability.can('manage', 'texto')
+      || $ability.can('display', 'message_send_page')
+      || $ability.can('manage', 'equipments') ;
+
     return {
       properties,
       displayedMiniVariant,
@@ -112,7 +119,8 @@ export default defineComponent({
       myAccessesMenu,
       myFamilyMenu,
       configurationMenu,
-      accountMenu
+      accountMenu,
+      showUniversalButton
     }
   }
 })

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

@@ -5,9 +5,9 @@ header principal (configuration, paramètres du compte...)
 
 <template>
   <v-menu offset-y left>
-    <template v-slot:activator="{ on: { click }, attrs }">
+    <template #activator="{ on: { click }, attrs }">
       <v-tooltip bottom>
-        <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
+        <template #activator="{ on: on_tooltips , attrs: attrs_tooltips }">
           <v-btn
             icon
             v-bind="[attrs, attrs_tooltips]"
@@ -69,9 +69,8 @@ header principal (configuration, paramètres du compte...)
 </template>
 
 <script lang="ts">
-import {defineComponent, useStore} from '@nuxtjs/composition-api'
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
 import {accessState} from "~/types/interfaces";
-import {Store} from "vuex";
 
 export default defineComponent({
   props: {
@@ -85,7 +84,7 @@ export default defineComponent({
     }
   },
   setup () {
-    const store:Store<any> = useStore()
+    const {store} = useContext()
     const accessStore: accessState = store.state.profile.access
     return {
       avatarId: accessStore.avatarId,

+ 15 - 12
components/Layout/Header/Notification.vue

@@ -1,8 +1,8 @@
 <template>
   <v-menu offset-y v-model="isOpen">
-    <template v-slot:activator="{ on: { click }, attrs }">
+    <template #activator="{ on: { click }, attrs }">
       <v-tooltip bottom>
-        <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
+        <template #activator="{ on: on_tooltips , attrs: attrs_tooltips }">
           <v-btn
             icon
             v-bind="[attrs, attrs_tooltips]"
@@ -13,8 +13,8 @@
             <v-badge
               color="orange"
               offset-y="10"
-              :value="unreadNotification.length > 0"
-              :content="unreadNotification.length"
+              :value="unreadNotification().length > 0"
+              :content="unreadNotification().length"
             >
               <v-icon class="ot_white--text" small>
                 fa-bell
@@ -73,22 +73,21 @@
 </template>
 
 <script lang="ts">
-import {computed, ComputedRef, defineComponent, Ref, ref, useContext, useFetch, useStore, watch} from '@nuxtjs/composition-api'
+import {computed, ComputedRef, defineComponent, onUnmounted, Ref, ref, useContext, useFetch, 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 {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 {$dataProvider, $dataPersister, $config, store, app: { i18n }} = useContext()
     const profileAccess = store.state.profile.access
-    const currentAccessId = $accessProfile(store).getCurrentAccessId()
+    $accessProfile.setStore(store)
+    const currentAccessId = $accessProfile.getCurrentAccessId()
 
     const loading: Ref<Boolean> = ref(true)
     const isOpen: Ref<Boolean> = ref(false)
@@ -178,12 +177,16 @@ export default defineComponent({
     /**
      * Dès l'ouverture du menu, on indique que les notifications non lues, le sont.
      */
-    watch(isOpen, (newValue, oldValue) => {
+    const unwatch = watch(isOpen, (newValue, oldValue) => {
       if(newValue){
         markNotificationsAsRead()
       }
     })
 
+    onUnmounted(() => {
+      unwatch()
+    })
+
     /**
      * Marque les notification non lues comme lues
      */
@@ -237,7 +240,7 @@ export default defineComponent({
       loading,
       notifications,
       update,
-      unreadNotification,
+      unreadNotification: () => unreadNotification,
       isOpen,
       download
     }

+ 61 - 0
components/Layout/Header/UniversalCreation/CreateButton.vue

@@ -0,0 +1,61 @@
+<!--
+bouton Créer
+-->
+
+<template>
+  <main>
+    <v-btn
+      elevation="2"
+      color="ot_warning ot_white--text"
+      @click="showDialog=true"
+    >
+      {{ $t('create') }}
+    </v-btn>
+    <lazy-LayoutDialog
+      :show="showDialog"
+    >
+
+      <template #dialogType>{{ $t('creative_assistant') }}</template>
+      <template #dialogTitle>{{ $t('what_do_you_want_to_create') }}</template>
+      <template #dialogText>
+        <LayoutHeaderUniversalCreationGenerateCardsSteps :step="step" @updateStep="step=$event" />
+
+      </template>
+      <template #dialogBtn>
+        <div class="text-center">
+          <v-btn
+            color="ot_super_light_grey"
+            @click="showDialog=false;step=1"
+          >
+            {{ $t('cancel') }}
+          </v-btn>
+          <v-btn
+            v-if="step > 1"
+            color="ot_super_light_grey"
+            @click="step=1"
+          >
+            {{ $t('previous') }}
+          </v-btn>
+        </div>
+      </template>
+    </lazy-LayoutDialog>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, Ref,ref} from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  setup () {
+    const showDialog:Ref<Boolean> = ref(false);
+    const step:Ref<Number> = ref(1);
+    return {
+      showDialog,
+      step
+    }
+  }
+})
+</script>
+<style scoped>
+
+</style>

+ 143 - 0
components/Layout/Header/UniversalCreation/GenerateCardsSteps.vue

@@ -0,0 +1,143 @@
+<!--
+
+-->
+
+<template>
+  <v-stepper v-model="step"
+  >
+    <v-stepper-items>
+      <v-stepper-content step="1">
+        <div class="row">
+          <LayoutHeaderUniversalCreationTypeCard
+            v-if="$can('manage', 'users')"
+            title="a_person"
+            text-content="add_new_person_student"
+            icon="fa fa-user"
+            type="access"
+            @typeClick="onTypeClick"
+          >
+          </LayoutHeaderUniversalCreationTypeCard>
+          <LayoutHeaderUniversalCreationTypeCard
+            v-if="$can('display', 'agenda_page')
+                && ($ability.can('manage', 'courses')
+            || $ability.can('manage', 'examens')
+            || $ability.can('manage', 'educationalprojects')
+            || $ability.can('manage', 'events'))"
+            title="an_event"
+            text-content="add_an_event_course"
+            icon="fa fa-comment"
+            type="event"
+            @typeClick="onTypeClick"
+          >
+          </LayoutHeaderUniversalCreationTypeCard>
+          <LayoutHeaderUniversalCreationTypeCard
+            v-if="$can('display', 'message_send_page')
+            && ($ability.can('manage', 'emails')
+            || $ability.can('manage', 'mails')
+            || $ability.can('manage', 'texto'))"
+            title="a_correspondence"
+            text-content="sen_email_letter"
+            icon="fa fa-comment"
+            type="message"
+            @typeClick="onTypeClick"
+          >
+          </LayoutHeaderUniversalCreationTypeCard>
+          <LayoutHeaderUniversalCreationTypeCard
+            v-if="$can('manage', 'equipments')"
+            title="a_materiel"
+            text-content="add_any_type_material"
+            icon="fa fa-cube"
+            :link="adminLegacy+ '/list/create/equipment'"
+          >
+          </LayoutHeaderUniversalCreationTypeCard>
+        </div>
+      </v-stepper-content>
+
+      <v-stepper-content step="2">
+        <div class="row">
+          <div v-if="type === 'access'" class="row">
+            <LayoutHeaderUniversalCreationTypeCard title="a_student" text-content="student_text_creation_card" icon="fa fa-user" :link="adminLegacy+ '/universal_creation_person/student'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard title="a_guardian" text-content="guardian_text_creation_card" icon="fa fa-female" :link="adminLegacy+ '/universal_creation_person/guardian'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard title="a_teacher" text-content="teacher_text_creation_card" icon="fa fa-graduation-cap" :link="adminLegacy+ '/universal_creation_person/teacher'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard title="a_member_of_staff" text-content="personnel_text_creation_card" icon="fa fa-suitcase" :link="adminLegacy+ '/universal_creation_person/personnel'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard title="a_legal_entity" text-content="moral_text_creation_card" icon="fa fa-building" :link="adminLegacy+ '/universal_creation_person/company'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard title="another_type_of_contact" text-content="other_contact_text_creation_card" icon="fa fa-plus" :link="adminLegacy+ '/universal_creation_person/other_contact'"></LayoutHeaderUniversalCreationTypeCard>
+          </div>
+          <div v-if="type === 'event'" class="row">
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'courses')" title="course" text-content="course_text_creation_card" icon="fa fa-users" :link="adminLegacy+ '/calendar/create/courses'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'examens')" title="exam" text-content="exam_text_creation_card" icon="fa fa-graduation-cap" :link="adminLegacy+ '/calendar/create/examens'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'educationalprojects')" title="educational_services" text-content="educational_services_text_creation_card" icon="fa fa-suitcase" :link="adminLegacy+ '/calendar/create/educational_projects'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'events')" title="other_event" text-content="other_event_text_creation_card" icon="far fa-calendar" :link="adminLegacy+ '/calendar/create/events'"></LayoutHeaderUniversalCreationTypeCard>
+          </div>
+          <div v-if="type === 'message'" class="row">
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'emails')" title="an_email" text-content="email_text_creation_card" icon="far fa-envelope" :link="adminLegacy+ '/list/create/emails'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'mails')" title="a_letter" text-content="letter_text_creation_card" icon="far fa-file-alt" :link="adminLegacy+ '/list/create/mails'"></LayoutHeaderUniversalCreationTypeCard>
+            <LayoutHeaderUniversalCreationTypeCard v-if="$can('manage', 'texto')" title="an_sms" text-content="sms_text_creation_card" icon="fa fa-mobile-alt" :link="adminLegacy+ '/list/create/sms'"></LayoutHeaderUniversalCreationTypeCard>
+          </div>
+        </div>
+
+      </v-stepper-content>
+
+    </v-stepper-items>
+  </v-stepper>
+</template>
+
+<script lang="ts">
+import {defineComponent, ref, Ref, useContext} from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    step: {
+      type: Number,
+      required: true
+    }
+  },
+  setup (_,{emit}) {
+    const { $config } = useContext()
+    const onTypeClick = (step:Number,Cardtype:String)=>{
+      type.value = Cardtype;
+      emit('updateStep',step);
+    }
+    const type:Ref<String> = ref('');
+    return {
+      type,
+      onTypeClick,
+      adminLegacy: $config.baseURL_adminLegacy
+    }
+  }
+})
+</script>
+<style lang="scss" scoped>
+  .creation-type-container{
+    border: none!important;
+    .icon{
+      i{
+        font-size: 50px;
+        color: var(--v-ot_grey-base, #777777);
+      }
+    }
+    .infos-container{
+      padding: 15px 0;
+      h4{
+        font-size: 15px;
+        color: var(--v-ot_green-base, #00AD8E);
+        font-weight: bold;
+        margin-bottom: 6px;
+      }
+      p{
+        font-size: 13px;
+        padding: 0;
+        margin: 0;
+        color: #767676;
+      }
+    }
+    &>div{
+      &:hover{
+        cursor: pointer;
+        background: var(--v-ot_light_green-base, #a9e0d6);
+      }
+    }
+
+
+  }
+</style>

+ 91 - 0
components/Layout/Header/UniversalCreation/TypeCard.vue

@@ -0,0 +1,91 @@
+<!--
+
+-->
+
+<template>
+
+    <v-card
+      class="col-md-6 creation-type-container"
+      color=""
+      :outlined=true
+      :href="link"
+      @click="$emit('typeClick',2,type)"
+    >
+      <div class="row no-gutters" style="height: 100px">
+        <div class="flex-grow-0 flex-shrink-0 d-flex justify-center col col-3" style="">
+          <div class="icon align-self-center">
+            <i :class="icon" aria-hidden="true"></i>
+          </div>
+        </div>
+        <div class="infos-container flex-grow-1 flex-shrink-1 col col-9" style="">
+          <h4>{{ $t(title) }}</h4>
+          <p>
+            {{ $t(textContent) }}
+          </p>
+        </div>
+      </div>
+    </v-card>
+
+</template>
+
+<script lang="ts">
+import {defineComponent, Ref,ref} from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    title: {
+      type: String,
+      required: true
+    },
+    textContent: {
+      type: String,
+      required: true
+    },
+    icon: {
+      type: String,
+      required: true
+    },
+    link: {
+      type: String,
+      required: false
+    },
+    type: {
+      type: String,
+      required: false
+    }
+  }
+})
+</script>
+<style lang="scss" scoped>
+.creation-type-container{
+  border: none!important;
+  .icon{
+    i{
+      font-size: 50px;
+      color: var(--v-ot_grey-base, #777777);
+    }
+  }
+  .infos-container{
+    padding: 15px 0;
+    h4{
+      font-size: 15px;
+      color: var(--v-ot_green-base, #00AD8E);
+      font-weight: bold;
+      margin-bottom: 6px;
+    }
+    p{
+      font-size: 13px;
+      padding: 0;
+      margin: 0;
+      color: #767676;
+    }
+  }
+  &>div{
+    &:hover{
+      cursor: pointer;
+      background: var(--v-ot_light_green-base, #a9e0d6);
+    }
+  }
+
+}
+</style>

+ 5 - 1
components/Layout/Loading.vue

@@ -25,11 +25,15 @@ export default defineComponent({
     const finish = () => {
       loading.value = false
     }
+    const fail = () => {
+      loading.value = false
+    }
 
     return {
       loading,
       start,
       finish,
+      fail,
       set
     }
   }
@@ -43,6 +47,6 @@ export default defineComponent({
     left: 0;
     width: 100%;
     height: 100%;
-    z-index: 100!important;
+    z-index: 1001!important;
   }
 </style>

+ 1 - 1
components/Layout/Menu.vue

@@ -35,7 +35,7 @@ Prend en paramètre une liste de ItemMenu et les met en forme
           v-model="item.active"
           no-action
         >
-          <template v-slot:activator>
+          <template #activator>
             <v-list-item-action>
               <v-icon class="ot_menu_color--text" small>
                 {{ item.icon }}

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

@@ -7,7 +7,7 @@
       :data="activityYear"
       @update="updateActivityYear"
     >
-      <template v-slot:xeditable.read="{inputValue}">
+      <template #xeditable.read="{inputValue}">
         <v-icon aria-hidden="false" class="ot_green--text" x-small>
           fas fa-edit
         </v-icon>
@@ -19,15 +19,15 @@
 
 <script lang="ts">
 import { defineComponent, useContext } from '@nuxtjs/composition-api'
-import { $useMyProfileUpdater } from '~/use/updater/useMyProfileUpdater'
+import { useMyProfile } from '~/composables/data/useMyProfile'
 import { $organizationProfile } from '~/services/profile/organizationProfile'
-import { $useDirtyForm } from '~/use/form/useDirtyForm'
+import { $useForm } from '~/composables/form/useForm'
 
 export default defineComponent({
   setup () {
-    const { store, $dataPersister } = useContext()
-    const { updateMyProfile, setActivityYear, activityYear } = $useMyProfileUpdater(store, $dataPersister)
-    const { markFormAsNotDirty } = $useDirtyForm(store)
+    const { store } = useContext()
+    const { updateMyProfile, setActivityYear, activityYear } = useMyProfile()
+    const { markFormAsNotDirty } = $useForm()
 
     const organizationProfile = $organizationProfile(store)
 

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

@@ -25,17 +25,16 @@
 </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'
+import { defineComponent, onUnmounted, ref, watch, Ref, WatchStopHandle } from '@nuxtjs/composition-api'
+import { $useForm } from '~/composables/form/useForm'
+import { useMyProfile } from '~/composables/data/useMyProfile'
 
 export default defineComponent({
   setup () {
-    const { store, $dataPersister } = useContext()
-    const { markFormAsNotDirty } = $useDirtyForm(store)
-    const { updateMyProfile, setHistorical, historical } = $useMyProfileUpdater(store, $dataPersister)
+    const { markFormAsNotDirty } = $useForm()
+    const { updateMyProfile, setHistorical, historical } = useMyProfile()
 
-    const historicalBtn: Ref<Array<number>> = initHistoricalBtn(historical.value)
+    const historicalBtn: Ref<Array<number>> = initHistoricalBtn(historical.value as Array<any>)
 
     const unwatch: WatchStopHandle = watch(historicalBtn, async (newValue) => {
       const historicalChoice: Array<string> = initHistoricalChoice(newValue)

+ 7 - 8
components/Layout/SubHeader/DataTimingRange.vue

@@ -14,7 +14,7 @@
     </div>
 
     <v-tooltip bottom>
-      <template v-slot:activator="{ on, attrs }">
+      <template #activator="{ on, attrs }">
         <v-btn
           class="time-btn"
           max-height="25"
@@ -37,18 +37,17 @@
 
 <script lang="ts">
 import {
-  defineComponent, onUnmounted, ref, useContext, watch, computed, ComputedRef, Ref, WatchStopHandle
+  defineComponent, onUnmounted, ref, watch, computed, ComputedRef, Ref, WatchStopHandle
 } from '@nuxtjs/composition-api'
-import { $useMyProfileUpdater } from '~/use/updater/useMyProfileUpdater'
-import { $useDirtyForm } from '~/use/form/useDirtyForm'
+import { useMyProfile } from '~/composables/data/useMyProfile'
+import { $useForm } from '~/composables/form/useForm'
 
 export default defineComponent({
   setup (_, { emit }) {
-    const { store, $dataPersister } = useContext()
-    const { markFormAsNotDirty } = $useDirtyForm(store)
-    const { updateMyProfile, setHistoricalRange, historical } = $useMyProfileUpdater(store, $dataPersister)
+    const { markFormAsNotDirty } = $useForm()
+    const { updateMyProfile, setHistoricalRange, historical } = useMyProfile()
 
-    const datesRange:ComputedRef<Array<string>> = computed(() => {
+    const datesRange:ComputedRef<Array<any>> = computed(() => {
       return [historical.value.dateStart, historical.value.dateEnd]
     })
 

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

@@ -7,7 +7,7 @@
       :close-on-content-click="false"
       min-width="500"
     >
-      <template v-slot:activator="{ on, attrs }">
+      <template #activator="{ on, attrs }">
         <span
           v-bind="attrs"
           class="ot_green--text"

+ 11 - 19
components/Ui/Button/Delete.vue

@@ -11,16 +11,14 @@ Bouton Delete avec modale de confirmation de la suppression
     <lazy-LayoutDialog
       :show="showDialog"
     >
-      <template v-slot:dialogText>
-        <v-card-title class="text-h5 grey lighten-2">
-          {{ $t('attention') }}
-        </v-card-title>
+      <template #dialogType>{{ $t('delete_assistant') }}</template>
+      <template #dialogTitle>{{ $t('attention') }}</template>
+      <template #dialogText>
         <v-card-text>
-          <br>
           <p>{{ $t('confirm_to_delete') }}</p>
         </v-card-text>
       </template>
-      <template v-slot:dialogBtn>
+      <template #dialogBtn>
         <v-btn class="mr-4 submitBtn ot_grey ot_white--text" @click="closeDialog">
           {{ $t('cancel') }}
         </v-btn>
@@ -33,9 +31,10 @@ Bouton Delete avec modale de confirmation de la suppression
 </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'
+import {defineComponent, ref, Ref, useContext} from '@nuxtjs/composition-api'
+import {DataDeleterArgs} from '~/types/interfaces'
+import {TYPE_ALERT} from '~/types/enums'
+import Page from "~/services/store/page";
 
 export default defineComponent({
   props: {
@@ -47,21 +46,14 @@ export default defineComponent({
   setup (props) {
     const { $dataDeleter, store, app: { i18n } } = useContext()
     const showDialog: Ref<boolean> = ref(false)
+    const page = new Page(store)
 
     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)
+        page.addAlerts(TYPE_ALERT.SUCCESS, [i18n.t('deleteSuccess') as string])
       } catch (error) {
-        const alert: alert = {
-          type: TYPE_ALERT.ALERT,
-          message: error.message
-        }
-        store.commit('page/setAlert', alert)
+        page.addAlerts(TYPE_ALERT.ALERT, [error.message])
       }
       showDialog.value = false
     }

+ 95 - 0
components/Ui/Button/Submit.vue

@@ -0,0 +1,95 @@
+<template>
+  <v-btn class="mr-4 ot_green ot_white--text" :class="otherActions ? 'pr-0' : ''" @click="onClick(mainAction)" ref="mainBtn">
+    {{ $t(mainAction) }}
+
+    <v-divider class="ml-3" vertical v-if="otherActions"></v-divider>
+
+    <v-menu
+      :top="dropDirection==='top'"
+      offset-y
+      left
+      v-if="otherActions"
+      :nudge-top="dropDirection==='top' ? 6 : 0"
+      :nudge-bottom="dropDirection==='bottom' ? 6 : 0"
+    >
+      <template #activator="{ on, attrs }">
+        <v-toolbar-title v-on="on">
+          <v-icon class="pl-3 pr-3">
+            {{ dropDirection==='top' ? 'fa-caret-up' : 'fa-caret-down'}}
+          </v-icon>
+        </v-toolbar-title>
+      </template>
+      <v-list
+        :min-width="menuSize"
+      >
+        <v-list-item
+          dense
+          v-for="(action, index) in actions"
+          :key="index"
+          class="subAction"
+          v-if="index > 0"
+        >
+          <v-list-item-title v-text="$t(action)" @click="onClick(action)" />
+        </v-list-item>
+      </v-list>
+    </v-menu>
+  </v-btn>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, ref, Ref} from "@nuxtjs/composition-api";
+import {$useForm} from "~/composables/form/useForm";
+
+export default defineComponent({
+  props: {
+    actions: {
+      type: Array,
+      required: true
+    },
+    dropDirection: {
+      type: String,
+      required: false,
+      default:'bottom'
+    }
+  },
+
+  setup(props, {emit}) {
+    const mainBtn:Ref<any> = ref(null)
+    const menuSize = computed(()=>{
+      //Btn size + 40px de padding
+      return mainBtn.value?.$el.clientWidth + 40
+    })
+
+    const onClick = (action: string) => {
+      emit('submit', action)
+    }
+
+
+    const mainAction:ComputedRef<any> = computed(()=>{
+      return props.actions[0] as string
+    })
+
+    const otherActions:ComputedRef<boolean> = computed(()=>{
+      return props.actions.length > 1
+    })
+
+    return{
+      mainBtn,
+      menuSize,
+      onClick,
+      mainAction,
+      otherActions
+    }
+
+  }
+})
+</script>
+
+<style scoped>
+.v-list-item--dense{
+  min-height: 25px;
+}
+.subAction{
+  cursor: pointer;
+}
+</style>

+ 2 - 4
components/Ui/DataTable.vue

@@ -16,13 +16,13 @@ Tableau interactif
       :loading="$fetchState.pending"
       class="elevation-1"
     >
-      <template v-for="header in headersWithItem" v-slot:[header.item]="props">
+      <template v-for="header in headersWithItem" #[header.item]="props">
         <slot :name="header.item" v-bind="props">
           {{ props.item[header.value] }}
         </slot>
       </template>
 
-      <template v-slot:item.actions="{ item }">
+      <template #item.actions="{ item }">
         <v-icon
           small
           class="mr-2"
@@ -102,8 +102,6 @@ export default defineComponent({
       itemId.value = item.id
     }
 
-    // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
-
     return {
       entries,
       totalEntries,

+ 46 - 0
components/Ui/Display/MobytStatus.vue

@@ -0,0 +1,46 @@
+<!--
+Texte descriptif du statut Mobyt de la structure
+
+@see https://vuetifyjs.com/en/components/text-fields/
+-->
+
+<template>
+  <span v-if="mobytStatus && !mobytStatusFetch.pending">
+    {{ mobytStatus.money.toLocaleString($i18n.locale, { style: 'currency', currency: 'EUR' }) }}
+    ({{ mobytStatus.amount }} SMS)
+  </span>
+</template>
+
+<script lang="ts">
+
+import { defineComponent, ref, Ref, useContext, useFetch } from '@nuxtjs/composition-api'
+import { ApiResponse, MobytUserStatus } from '~/types/interfaces'
+import { QUERY_TYPE } from '~/types/enums'
+
+export default defineComponent({
+  setup () {
+    const { store, $dataProvider } = useContext()
+    const id: number = store.state.profile.organization.id
+    const mobytStatus: Ref<MobytUserStatus | null> = ref(null)
+
+    // fetch the mobyt status
+    const { fetchState } = useFetch(async () => {
+      try {
+        const response:ApiResponse = await $dataProvider.invoke({
+          type: QUERY_TYPE.DEFAULT,
+          url: '/api/mobyt/status/' + id
+        })
+        mobytStatus.value = response.data as MobytUserStatus
+      } catch (Error) {
+        // eslint-disable-next-line no-console
+        console.error('Error: Mobyt status not found')
+      }
+    })
+
+    return {
+      mobytStatus,
+      mobytStatusFetch: fetchState
+    }
+  }
+})
+</script>

+ 93 - 62
components/Ui/Form.vue

@@ -8,30 +8,32 @@ Formulaire générique
   <main>
     <v-form
       ref="form"
-      v-model="properties.valid"
       lazy-validation
       :readonly="readonly"
     >
       <v-container fluid class="container btnActions">
         <v-row>
           <v-col cols="12" sm="12">
-            <slot name="form.button" />
-            <v-btn v-if="!readonly" class="mr-4 ot_green ot_white--text" @click="submit">
-              {{ $t('save') }}
-            </v-btn>
+            <slot name="form.button"/>
+            <UiButtonSubmit
+              v-if="!readonly"
+              @submit="submit"
+              :actions="actions"
+            ></UiButtonSubmit>
           </v-col>
         </v-row>
       </v-container>
 
-      <slot name="form.input" v-bind="{entry,updateRepository}" />
+      <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" />
-            <v-btn v-if="!readonly" class="mr-4 ot_green ot_white--text" @click="submit">
-              {{ $t('save') }}
-            </v-btn>
+            <slot name="form.button"/>
+            <UiButtonSubmit
+              @submit="submit"
+              :actions="actions"
+            ></UiButtonSubmit>
           </v-col>
         </v-row>
       </v-container>
@@ -40,7 +42,7 @@ Formulaire générique
     <lazy-LayoutDialog
       :show="showDialog"
     >
-      <template v-slot:dialogText>
+      <template #dialogText>
         <v-card-title class="text-h5 grey lighten-2">
           {{ $t('attention') }}
         </v-card-title>
@@ -49,31 +51,34 @@ Formulaire générique
           <p>{{ $t('quit_without_saving_warning') }}</p>
         </v-card-text>
       </template>
-      <template v-slot:dialogBtn>
+      <template #dialogBtn>
         <v-btn class="mr-4 submitBtn ot_green ot_white--text" @click="closeDialog">
           {{ $t('back_to_form') }}
         </v-btn>
         <v-btn class="mr-4 submitBtn ot_green ot_white--text" @click="saveAndQuit">
           {{ $t('save_and_quit') }}
         </v-btn>
-        <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="goEvenUnsavedData">
+        <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="quitForm">
           {{ $t('quit_form') }}
         </v-btn>
       </template>
     </lazy-LayoutDialog>
+
   </main>
 </template>
 
 <script lang="ts">
-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'
+import {computed, ComputedRef, defineComponent, ref, Ref, toRefs, ToRefs, useContext} from '@nuxtjs/composition-api'
+import {Query} from '@vuex-orm/core'
+import {repositoryHelper} from '~/services/store/repository'
+import {queryHelper} from '~/services/store/query'
+import {FORM_STATUS, QUERY_TYPE, SUBMIT_TYPE, TYPE_ALERT} from '~/types/enums'
+import {AnyJson} from '~/types/interfaces'
+import {$useForm} from '~/composables/form/useForm'
+import * as _ from 'lodash'
+import Form from "~/services/store/form";
+import Page from "~/services/store/page";
+import UseNextStepFactory from "~/composables/form/useNextStepFactory";
 
 export default defineComponent({
   props: {
@@ -82,26 +87,30 @@ export default defineComponent({
       required: true
     },
     id: {
-      type: Number,
+      type: [Number, String],
       required: true
     },
     query: {
       type: Object as () => Query,
       required: true
+    },
+    submitActions: {
+      type: Object,
+      required: false,
+      default: () => {
+        let actions:AnyJson = {}
+        actions[SUBMIT_TYPE.SAVE] = {}
+        return actions
+      }
     }
   },
-  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)
+  setup(props) {
+    const {$dataPersister, store, app: {router, i18n}} = useContext()
+    const {markFormAsDirty, markFormAsNotDirty, readonly} = $useForm()
+    const nextStepFactory = new UseNextStepFactory()
+    const {id, query}: ToRefs = toRefs(props)
+    const page = new Page(store)
+    const form:Ref<any> = ref(null)
 
     const entry: ComputedRef<AnyJson> = computed(() => {
       return queryHelper.getFlattenEntry(query.value, id.value)
@@ -112,30 +121,47 @@ export default defineComponent({
       repositoryHelper.updateStoreFromField(props.model, entry.value, newValue, field)
     }
 
-    const submit = async () => {
-      try {
+    const submit = async (next: string|null = null) => {
+      if(form.value.validate()){
         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
+
+        try {
+          const response = await $dataPersister.invoke({
+            type: QUERY_TYPE.MODEL,
+            model: props.model,
+            id: store.state.form.formStatus === FORM_STATUS.EDIT ? id.value : null,
+            idTemp: store.state.form.formStatus === FORM_STATUS.CREATE ? id.value : null,
+            query: props.query
+          })
+
+          page.addAlerts(TYPE_ALERT.SUCCESS, [i18n.t('saveSuccess') as string])
+          nextStep(next, response.data)
+        } catch (error) {
+          if (error.response.status === 422) {
+            if(error.response.data['violations']){
+              const violations:Array<string> = []
+              const fields:Array<string> = []
+              for(const violation of error.response.data['violations']){
+                violations.push(i18n.t(violation['message']) as string)
+                fields.push(violation['propertyPath'])
+              }
+
+              new Form(store).addViolations(fields)
+              page.addAlerts(TYPE_ALERT.ALERT, violations)
+            }
+          }
         }
-        store.commit('page/setAlert', alert)
+      }else{
+        page.addAlerts(TYPE_ALERT.ALERT, [i18n.t('invalide_form') as string])
       }
     }
 
-    const showDialog:ComputedRef<boolean> = computed(() => {
+    const nextStep = (next: string|null, response: AnyJson) =>{
+      if(next === null) return
+      nextStepFactory.invoke(props.submitActions[next], response)[next]()
+    }
+
+    const showDialog: ComputedRef<boolean> = computed(() => {
       return store.state.form.showConfirmToLeave
     })
 
@@ -145,10 +171,10 @@ export default defineComponent({
 
     const saveAndQuit = async () => {
       await submit()
-      goEvenUnsavedData()
+      quitForm()
     }
 
-    const goEvenUnsavedData = () => {
+    const quitForm = () => {
       markFormAsNotDirty()
       store.commit('form/setShowConfirmToLeave', false)
 
@@ -162,23 +188,28 @@ export default defineComponent({
       }
     }
 
+    const actions = computed(()=>{
+      return _.keys(props.submitActions)
+    })
+
     return {
+      form,
       submit,
       updateRepository,
-      properties,
       readonly,
       showDialog,
       entry,
-      goEvenUnsavedData,
+      quitForm,
       closeDialog,
-      saveAndQuit
+      saveAndQuit,
+      actions
     }
   }
 })
 </script>
 
 <style scoped>
-  .btnActions {
-    text-align: right;
-  }
+.btnActions {
+  text-align: right;
+}
 </style>

+ 120 - 47
components/Ui/Image.vue

@@ -1,32 +1,49 @@
 <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>
+    <div class="image-wrapper" :style="{width:width + 'px'}">
+      <v-img
+        :src="imgSrcReload ? imgSrcReload : imageLoaded"
+        :lazy-src="require(`/assets/images/byDefault/${imageByDefault}`)"
+        :height="height"
+        :width="width"
+        aspect-ratio="1"
+      >
+        <template #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>
+
+      <div>
+        <div v-if="upload" class="click-action hover" @click="openUpload=true"><v-icon>mdi-upload</v-icon></div>
+        <UiInputImage
+          v-if="openUpload"
+          @close="openUpload=false"
+          :existingImageId="id"
+          :field="field"
+          :ownerId="ownerId"
+          @update="$emit('update', $event, field); openUpload=false"
+          @reload="fetch();openUpload=false"
+          @reset="reset"
+        ></UiInputImage>
+      </div>
+    </div>
   </main>
 </template>
 
 
 <script lang="ts">
-import {defineComponent, ref, Ref, useContext, useFetch} from '@nuxtjs/composition-api'
-import {QUERY_TYPE} from "~/types/enums";
+import {defineComponent, onUnmounted, ref, Ref, watch} from '@nuxtjs/composition-api'
+import {UseImage} from "~/composables/data/useImage";
+import {WatchStopHandle} from "@vue/composition-api";
 
 export default defineComponent({
   props: {
@@ -34,6 +51,10 @@ export default defineComponent({
       type: Number,
       required: false
     },
+    field: {
+      type: String,
+      required: false
+    },
     imageByDefault: {
       type: String,
       required: false,
@@ -41,43 +62,95 @@ export default defineComponent({
     },
     height: {
       type: Number,
-      required: false,
-      default: 0
+      required: false
     },
     width: {
       type: Number,
+      required: false
+    },
+    upload: {
+      type: Boolean,
       required: false,
-      default: 0
+      default: false
+    },
+    ownerId:{
+      type: Number,
+      required: false
     }
   },
   fetchOnServer: false,
   setup(props) {
-    const {$dataProvider, $config} = useContext()
-    const imageLoaded: Ref<String> = ref('')
+    const openUpload: Ref<Boolean> = ref(false)
+    const imgSrcReload: Ref<any> = ref(null)
 
-    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}`)
-        }
-      }
-    )
+    const useImg = new UseImage()
+
+    const { imageLoaded, fetch } = useImg.getOne(props.id, props.imageByDefault, props.height, props.width)
+    const unwatch: WatchStopHandle = watch(() => props.id, async (newValue, oldValue) => {
+      imgSrcReload.value = await useImg.provideImg(newValue as number, props.height, props.width)
+    })
+
+    /**
+     * Quand on souhaite faire un reset de l'image
+     */
+    const reset = () => {
+      imgSrcReload.value = null
+      imageLoaded.value = require(`assets/images/byDefault/${props.imageByDefault}`)
+      openUpload.value = false
+    }
+
+    /**
+     * Lorsqu'on démonte le component on supprime le watcher
+     */
+    onUnmounted(() => {
+      unwatch()
+    })
 
     return {
-      imageLoaded
+      imgSrcReload,
+      imageLoaded,
+      openUpload,
+      fetch,
+      reset
     }
   }
 })
 </script>
+
+<style lang="scss">
+  div.image-wrapper {
+    display: block;
+    position: relative;
+    img{
+      max-width: 100%;
+    }
+    .click-action{
+      position: absolute;
+      top:0;
+      left:0;
+      width: 100%;
+      height: 100%;
+      background: transparent;
+      opacity: 0;
+      transition: all .2s;
+      &:hover{
+        opacity: 1;
+        background:rgba(0,0,0,0.3);
+        cursor: pointer;
+      }
+      i{
+        color: #fff;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50% , -50%);
+        font-size: 50px;
+        z-index: 1;
+        opacity: 1;
+        &:hover{
+          color: rgba(#3fb37f, 0.7);
+        }
+      }
+    }
+  }
+</style>

+ 126 - 21
components/Ui/Input/Autocomplete.vue

@@ -7,21 +7,37 @@ Liste déroulante avec autocompletion
 <template>
   <main>
     <v-autocomplete
+      autocomplete="search"
+      :value="data"
       :items="itemsToDisplayed"
-      :label="$t(label)"
-      item-text="textDisplay"
+      :label="$t(label_field)"
+      item-text="itemTextDisplay"
       :item-value="itemValue"
+      :no-data-text="$t('autocomplete_research')"
+      :no-filter="noFilter"
+      auto-select-first
       :multiple="multiple"
       :loading="isLoading"
       :return-object="returnObject"
-      @change="$emit('update', $event, field)"
-    />
+      :search-input.sync="search"
+      :prepend-icon="prependIcon"
+      :error="error"
+      :rules="rules"
+      @input="onChange($event)"
+    >
+      <template v-if="slotText" #item="data">
+        <v-list-item-content v-text="data.item.slotTextDisplay"></v-list-item-content>
+      </template>
+    </v-autocomplete>
   </main>
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, ComputedRef } from '@nuxtjs/composition-api'
+import {computed, defineComponent, ComputedRef, Ref, ref, watch, onUnmounted, useContext} from '@nuxtjs/composition-api'
 import { AnyJson } from '~/types/interfaces'
+import * as _ from 'lodash'
+import {$objectProperties} from "~/services/utils/objectProperties";
+import {$useError} from "~/composables/form/useError";
 
 export default defineComponent({
   props: {
@@ -36,7 +52,7 @@ export default defineComponent({
       default: null
     },
     data: {
-      type: String,
+      type: [String, Number, Object, Array],
       required: false,
       default: null
     },
@@ -57,6 +73,16 @@ export default defineComponent({
       type: Array,
       required: true
     },
+    group:{
+      type: String,
+      required: false,
+      default: null
+    },
+    slotText: {
+      type: Array,
+      required: false,
+      default: null
+    },
     returnObject: {
       type: Boolean,
       default: false
@@ -68,28 +94,107 @@ export default defineComponent({
     isLoading: {
       type: Boolean,
       default: false
-    }
+    },
+    noFilter: {
+      type: Boolean,
+      default: false
+    },
+    prependIcon: {
+      type: String
+    },
+    translate: {
+      type: Boolean,
+      default: false
+    },
+    rules: {
+      type: Array,
+      required: false,
+      default: () => []
+    },
   },
-  setup (props) {
-    // On reconstruit les items à afficher car le text de l'Item doit être construit par rapport au itemText passé en props
+  setup (props, { emit }) {
+    const {app:{i18n}} = useContext()
+    const search:Ref<string|null> = ref(null)
+    const {error, onChange} = $useError(props.field, emit)
+
+    // On reconstruit les items à afficher...
     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(' ') })
-      })
+      const itemsByGroup:Array<Array<string>> = classItemsByGroup(props.items)
+      return prepareItemsToDisplayed(itemsByGroup)
+    })
+
+    const unwatch = watch(search, _.debounce(async (newResearch, oldResearch) => {
+      if(newResearch !== oldResearch && oldResearch !== null)
+        emit('research', newResearch)
+    }, 500))
+
+    onUnmounted(() => {
+      unwatch()
     })
 
+
+    /**
+     * On construit l'Array à double entrée contenant les groups (headers) et les propositions
+     * @param items
+     */
+    const classItemsByGroup = (items:Array<any>): Array<Array<string>> => {
+      const itemsByGroup:Array<Array<string>> = []
+      for (const item of items){
+        if(item){
+          if(!itemsByGroup[item[props.group as string]])
+            itemsByGroup[item[props.group as string]] = []
+
+          itemsByGroup[item[props.group as string]].push(item)
+        }
+      }
+      return itemsByGroup
+    }
+
+    /**
+     * Construction de l'Array JSON contenant toutes les propositions à afficher dans le select
+     * @param itemsByGroup
+     */
+    const prepareItemsToDisplayed = (itemsByGroup:Array<Array<string>>):Array<AnyJson> => {
+      let finalItems:Array<AnyJson> = []
+      for(const group in itemsByGroup){
+
+        //Si un group est présent, alors on créer le group options header
+        if(group !== 'undefined'){
+          finalItems.push({header: i18n.t(group as string)})
+        }
+
+        //On parcours les items pour préparer les texts/slotTexts à afficher
+        finalItems = finalItems.concat(itemsByGroup[group].map((item: any) => {
+          const slotTextDisplay: Array<string> = []
+          const itemTextDisplay: Array<string> = []
+
+          item = $objectProperties.cloneAndFlatten(item)
+
+          //Si on souhaite avoir un texte différent dans les propositions que dans la sélection finale de select
+          if(props.slotText){
+            for (const text of props.slotText) {
+              slotTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
+            }
+          }
+
+          for (const text of props.itemText) {
+            itemTextDisplay.push(props.translate ? i18n.t(item[text as string]) : item[text as string])
+          }
+
+          //On reconstruit l'objet
+          return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
+        }))
+      }
+      return finalItems
+    }
+
     return {
       label_field: props.label ?? props.field,
-      itemsToDisplayed
+      itemsToDisplayed,
+      search,
+      error,
+      onChange
     }
   }
 })
 </script>
-
-<style scoped>
-
-</style>

+ 38 - 52
components/Ui/Input/AutocompleteWithAPI.vue

@@ -1,34 +1,31 @@
 <!--
 Liste déroulante avec autocompletion (les données sont issues
-de l'api Opentalent)
+d'une api)
 
 @see https://vuetifyjs.com/en/components/autocompletes/#usage
 -->
-
 <template>
   <main>
-    <v-autocomplete
-      v-model="model"
-      :value="data"
+    <UiInputAutocomplete
+      :field="field"
+      :label="label"
+      :data="data"
       :items="items"
-      :loading="isLoading"
-      :search-input.sync="search"
-      hide-no-data
-      hide-selected
-      item-text="textDisplay"
+      :isLoading="isLoading"
+      :item-text="itemText"
+      :slotText="slotText"
       :item-value="itemValue"
-      :label="$t(label_field)"
-      :placeholder="$t('start_your_research')"
-      prepend-icon="mdi-magnify"
+      prependIcon="mdi-magnify"
       :return-object="returnObject"
+      @research="search"
+      :no-filter="noFilter"
+      @update="$emit('update', $event, field)"
     />
   </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'
+import {defineComponent, ref, Ref, watch, onUnmounted, toRefs} from '@nuxtjs/composition-api'
 
 export default defineComponent({
   props: {
@@ -42,8 +39,12 @@ export default defineComponent({
       required: false,
       default: null
     },
+    searchFunction: {
+      type: Function,
+      required: true
+    },
     data: {
-      type: String,
+      type: [String, Number, Object, Array],
       required: false,
       default: null
     },
@@ -59,45 +60,36 @@ export default defineComponent({
       type: Array,
       required: true
     },
+    slotText: {
+      type: Array,
+      required: false
+    },
     returnObject: {
       type: Boolean,
       default: false
+    },
+    noFilter: {
+      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([])
+  setup(props) {
+    const {data} = toRefs(props)
+    const items:Ref<Array<any>> = ref([data.value])
     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 }
-
+    const search = async (research:string) => {
       isLoading.value = true
 
-      const response = await $dataProvider.invoke({
-        type: QUERY_TYPE.DEFAULT,
-        url: `gps-coordinate-searching?city=${research}`
-      })
+      const func: Function = props.searchFunction
+      items.value = await func(research, props.field)
 
-      count.value = response.length
-      entries.value = response
       isLoading.value = false
-    }, 500))
+    }
+
+    const unwatch = watch(data,(d) => {
+      items.value = [d]
+    })
 
     onUnmounted(() => {
       unwatch()
@@ -105,16 +97,10 @@ export default defineComponent({
 
     return {
       label_field: props.label ?? props.field,
-      count,
       isLoading,
       items,
-      search,
-      model
+      search
     }
   }
 })
 </script>
-
-<style scoped>
-
-</style>

+ 10 - 3
components/Ui/Input/Checkbox.vue

@@ -10,16 +10,19 @@ Case à cocher
     fluid
   >
     <v-checkbox
+      v-model="data"
       :value="data"
       :label="$t(label_field)"
       :disabled="readonly"
-      @change="$emit('update', $event, field)"
+      :error="error"
+      @change="onChange($event)"
     />
   </v-container>
 </template>
 
 <script lang="ts">
 import { defineComponent } from '@nuxtjs/composition-api'
+import {$useError} from "~/composables/form/useError";
 
 export default defineComponent({
   props: {
@@ -42,9 +45,13 @@ export default defineComponent({
       required: false
     }
   },
-  setup (props) {
+  setup (props, {emit}) {
+    const {error, onChange} = $useError(props.field, emit)
+
     return {
-      label_field: props.label ?? props.field
+      label_field: props.label ?? props.field,
+      error,
+      onChange
     }
   }
 })

+ 7 - 3
components/Ui/Input/DatePicker.vue

@@ -14,7 +14,7 @@ Sélecteur de dates
       offset-y
       min-width="auto"
     >
-      <template v-slot:activator="{ on, attrs }">
+      <template #activator="{ on, attrs }">
         <v-text-field
           v-model="datesFormatted"
           autocomplete="off"
@@ -25,6 +25,7 @@ Sélecteur de dates
           :dense="dense"
           :single-line="singleLine"
           v-on="on"
+          :error="error"
         />
       </template>
       <v-date-picker
@@ -42,6 +43,7 @@ Sélecteur de dates
 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'
+import {$useError} from "~/composables/form/useError";
 
 export default defineComponent({
   props: {
@@ -81,6 +83,7 @@ export default defineComponent({
     const { data, field, range } = props
     const { $moment } = useContext()
     const dateUtils = new DatesUtils($moment)
+    const {error, onChange} = $useError(props.field, emit)
 
     const datesParsed: Ref<Array<string>|string> = range ? ref(Array<string>()) : ref('')
 
@@ -99,7 +102,7 @@ export default defineComponent({
     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)
+      onChange(Array.isArray(newValue) ? dateUtils.sortDate(newValue) : newValue)
     })
 
     onUnmounted(() => {
@@ -110,7 +113,8 @@ export default defineComponent({
       label_field: props.label ?? props.field,
       datesParsed,
       datesFormatted,
-      dateOpen: ref(false)
+      dateOpen: ref(false),
+      error
     }
   }
 })

+ 7 - 13
components/Ui/Input/Email.vue

@@ -8,14 +8,14 @@ Champs de saisie de type Text dédié à la saisie d'emails
     :label="label"
     :readonly="readonly"
     :error="error"
-    :error-message="errorMessage"
     :rules="rules"
-    @change="$emit('update', $event, field)"
+    @update="onChange"
   />
 </template>
 
 <script lang="ts">
 import { defineComponent, useContext } from '@nuxtjs/composition-api'
+import {$useError} from "~/composables/form/useError";
 
 export default defineComponent({
   props: {
@@ -43,19 +43,11 @@ export default defineComponent({
       type: Boolean,
       required: false,
       default: false
-    },
-    error: {
-      type: Boolean,
-      required: false
-    },
-    errorMessage: {
-      type: String,
-      required: false,
-      default: null
     }
   },
-  setup (props) {
+  setup (props, {emit}) {
     const { app: { i18n } } = useContext()
+    const {error, onChange} = $useError(props.field, emit)
 
     const rules = [
       (email: string) => validEmail(email) || i18n.t('email_error')
@@ -68,7 +60,9 @@ export default defineComponent({
     }
 
     return {
-      rules
+      rules,
+      error,
+      onChange
     }
   }
 })

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

@@ -21,15 +21,17 @@ Liste déroulante dédiée à l'affichage d'objets Enum
       item-value="value"
       :rules="rules"
       :disabled="readonly"
-      @change="$emit('update', $event, field)"
+      :error="error"
+      @change="onChange($event)"
     />
   </main>
 </template>
 
 <script lang="ts">
-import { defineComponent, ref, useContext, useFetch, Ref } from '@nuxtjs/composition-api'
+import {defineComponent, ref, useContext, useFetch, Ref} from '@nuxtjs/composition-api'
 import { EnumChoices } from '~/types/interfaces'
 import { QUERY_TYPE } from '~/types/enums'
+import {$useError} from "~/composables/form/useError";
 
 export default defineComponent({
   props: {
@@ -62,11 +64,12 @@ export default defineComponent({
       default: () => []
     }
   },
-  setup (props) {
+  setup (props, {emit}) {
     const labelField = props.label ?? props.field
 
     const { enumType } = props
     const { $dataProvider } = useContext()
+    const {error, onChange} = $useError(props.field, emit)
 
     const items: Ref<Array<EnumChoices>> = ref([])
     useFetch(async () => {
@@ -76,9 +79,12 @@ export default defineComponent({
       })
     })
 
+
     return {
       items,
-      label_field: labelField
+      label_field: labelField,
+      error,
+      onChange
     }
   }
 })

+ 305 - 0
components/Ui/Input/Image.vue

@@ -0,0 +1,305 @@
+<!--
+Assistant de création d'image
+https://norserium.github.io/vue-advanced-cropper/
+-->
+<template>
+    <lazy-LayoutDialog :show="true">
+      <template #dialogType>{{ $t('image_assistant') }}</template>
+      <template #dialogTitle>{{ $t('modif_picture') }}</template>
+      <template #dialogText>
+        <div class="upload">
+          <v-row
+            v-if="fetchState.pending"
+            class="fill-height ma-0 loading"
+            align="center"
+            justify="center"
+          >
+            <v-progress-circular
+              indeterminate
+              color="grey lighten-1"
+            ></v-progress-circular>
+          </v-row>
+
+          <div v-else >
+            <div class="upload__cropper-wrapper">
+              <cropper
+                ref="cropper"
+                class="upload__cropper"
+                check-orientation
+                :src="image.src"
+                :default-position="{left : coordinates.left, top : coordinates.top}"
+                :default-size="coordinates.width ? {width : coordinates.width, height : coordinates.height}: defaultSize"
+                @change="onChange"
+              />
+              <div v-if="image.src" class="upload__reset-button" title="Reset Image" @click="reset()">
+                <v-icon>mdi-delete</v-icon>
+              </div>
+            </div>
+            <div class="upload__buttons-wrapper">
+              <button class="upload__button" @click="$refs.file.click()">
+                <input ref="file" type="file" accept="image/*" @change="loadImage($event)" />
+                {{$t('upload_image')}}
+              </button>
+            </div>
+          </div>
+
+        </div>
+      </template>
+      <template #dialogBtn>
+        <v-btn class="mr-4 submitBtn ot_grey ot_white--text" @click="$emit('close')">
+          {{ $t('cancel') }}
+        </v-btn>
+        <v-btn class="mr-4 submitBtn ot_danger ot_white--text" @click="save">
+          {{ $t('save') }}
+        </v-btn>
+      </template>
+    </lazy-LayoutDialog>
+
+</template>
+
+<script lang="ts">
+import {defineComponent, onUnmounted, Ref, ref, useContext, useFetch, watch} from '@nuxtjs/composition-api'
+import { Cropper } from 'vue-advanced-cropper'
+import 'vue-advanced-cropper/dist/style.css';
+import {AnyJson, ApiResponse} from "~/types/interfaces";
+import {UseImage} from "~/composables/data/useImage";
+import {WatchStopHandle} from "@vue/composition-api";
+import {QUERY_TYPE} from "~/types/enums";
+import {File} from "~/models/Core/File";
+import {repositoryHelper} from "~/services/store/repository";
+
+export default defineComponent({
+  components: {
+    Cropper,
+  },
+  props: {
+    existingImageId:{
+      type: Number,
+      required: false
+    },
+    ownerId:{
+      type: Number,
+      required: false
+    },
+    field:{
+      type: String,
+      required: true
+    }
+  },
+  fetchOnServer: false,
+  setup (props, {emit}) {
+    const fileToSave = new File()
+    const cropper:Ref<any> = ref(null)
+    const image: Ref<AnyJson> = ref({
+      id: null,
+      src: null,
+      file: null,
+      name: null
+    })
+    const coordinates:Ref<AnyJson> = ref({})
+    const defaultSize = ({ imageSize, visibleArea }: any) => {
+      return {
+        width: (visibleArea || imageSize).width,
+        height: (visibleArea || imageSize).height,
+      };
+    }
+    const {$dataProvider, $config, $dataPersister} = useContext()
+
+    //Si l'id est renseigné, il faut récupérer l'Item File afin d'avoir les informations de config, le nom, etc.
+    if(props.existingImageId){
+      useFetch(async () => {
+        const result: ApiResponse = await $dataProvider.invoke({
+          type: QUERY_TYPE.DEFAULT,
+          url: 'api/files',
+          id: props.existingImageId
+        })
+
+        const config = JSON.parse(result.data.config)
+        coordinates.value.left = config.x
+        coordinates.value.top = config.y
+        coordinates.value.height = config.height
+        coordinates.value.width = config.width
+        image.value.name = result.data.name
+        image.value.id = result.data.id
+      })
+    }
+
+    //On récupère l'image...
+    const { fetchState, imageLoaded } = new UseImage().getOne(props.existingImageId)
+    const unwatch: WatchStopHandle = watch(imageLoaded, (newValue, oldValue) => {
+      if (newValue === oldValue || typeof newValue === 'undefined') { return }
+      image.value.src = newValue
+    })
+
+    /**
+     * Quand l'utilisateur choisit une image sur sa machine
+     * @param event
+     */
+    const loadImage = (event:any) => {
+      const { files } = event.target
+      if (files && files[0]) {
+        reset()
+        image.value.name = files[0].name
+        image.value.src = URL.createObjectURL(files[0])
+        image.value.file = files[0]
+      }
+    }
+
+    /**
+     * Losrque le cropper change de position/taille, on met à jour les coordonnées
+     * @param config
+     */
+    const onChange = ({ coordinates: config } : any) => {
+      coordinates.value = config;
+    }
+
+    /**
+     * Lorsque l'on sauvegarde l'image
+     */
+    const save = async () => {
+      fileToSave.config = JSON.stringify({
+        x: coordinates.value.left,
+        y: coordinates.value.top,
+        height: coordinates.value.height,
+        width: coordinates.value.width
+      })
+
+      //Cas d'un PUT : l'image existe déjà on bouge simplement le cropper
+      if(image.value.id){
+        fileToSave.id = image.value.id
+        repositoryHelper.persist(File, fileToSave)
+        await $dataPersister.invoke({
+          type: QUERY_TYPE.MODEL,
+          model: File,
+          id: fileToSave.id
+        })
+        //On émet un évent afin de mettre à jour le formulaire de départ
+        emit('reload')
+      }
+
+      //Post : on créer une nouvelle image donc on passe par l'api legacy...
+      else{
+        if(image.value.file){
+          //On créer l'objet File à sauvegarder
+          fileToSave.name = image.value.name
+          fileToSave.imgFieldName = props.field
+          fileToSave.visibility = 'EVERYBODY'
+          fileToSave.folder = 'IMAGES'
+
+          if(props.ownerId)
+            fileToSave.ownerId = props.ownerId
+
+          //Appel au datapersister
+          const response: ApiResponse = await $dataPersister.invoke({
+            type: QUERY_TYPE.FILE,
+            baseUrl: $config.baseURL_Legacy,
+            data: fileToSave.$toJson(),
+            file: image.value.file
+          })
+          //On émet un évent afin de mettre à jour le formulaire de départ
+          emit('update', response.data['@id'])
+        }else{
+          //On reset l'image : on a appuyer sur "poubelle" puis on enregistre
+          emit('reset')
+        }
+      }
+    }
+
+    /**
+     * On choisit de supprimer l'image présente
+     */
+    const reset = () => {
+      image.value.src = null
+      image.value.file = null
+      image.value.name = null
+      image.value.id = null
+      URL.revokeObjectURL(image.value.src)
+    }
+
+    /**
+     * Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
+     */
+    onUnmounted(() => {
+      unwatch()
+        if (image.value.src) {
+          URL.revokeObjectURL(image.value.src)
+        }
+    })
+
+    return {
+      image,
+      save,
+      loadImage,
+      cropper,
+      reset,
+      fetchState,
+      coordinates,
+      onChange,
+      defaultSize
+    }
+  }
+})
+
+</script>
+
+<style lang="scss">
+  .vue-advanced-cropper__stretcher{
+    height: auto !important;
+    width: auto !important;
+  }
+  .loading{
+    height: 300px;
+  }
+  .upload {
+    user-select: none;
+    padding: 20px;
+    display: block;
+    &__cropper {
+       border: solid 1px #eee;
+       min-height: 500px;
+       max-height: 500px;
+     }
+    &__cropper-wrapper {
+       position: relative;
+     }
+    &__reset-button {
+      position: absolute;
+      right: 20px;
+      bottom: 20px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 42px;
+      width: 42px;
+      background: rgba(#3fb37f, 0.7);
+      transition: background 0.5s;
+      &:hover {
+        background: #3fb37f;
+      }
+    }
+    &__buttons-wrapper {
+       display: flex;
+       justify-content: center;
+       margin-top: 17px;
+     }
+    &__button {
+       border: none;
+       outline: solid transparent;
+       color: white;
+       font-size: 16px;
+       padding: 10px 20px;
+       background: #3fb37f;
+       cursor: pointer;
+       transition: background 0.5s;
+       margin: 0 16px;
+      &:hover,
+      &:focus {
+         background: #38d890;
+       }
+      input {
+        display: none;
+      }
+    }
+  }
+</style>

+ 37 - 34
components/Ui/Input/Phone.vue

@@ -7,23 +7,24 @@ Champs de saisie d'un numéro de téléphone
 <template>
   <client-only>
     <vue-tel-input-vuetify
+      :error="error"
       :field="field"
       :label="label"
-      :value="data"
+      v-model="myPhone"
       :readonly="readonly"
       clearable
       valid-characters-only
       validate-on-blur
       :rules="rules"
-      :active-country="{iso2: 'FR'}"
       @input="onInput"
-      @change="onChange"
+      @change="onChangeValue"
     />
   </client-only>
 </template>
 
 <script lang="ts">
-import { defineComponent, Ref, ref, useContext } from '@nuxtjs/composition-api'
+import { defineComponent, Ref, ref, useContext, computed } from '@nuxtjs/composition-api'
+import {$useError} from "~/composables/form/useError";
 
 export default defineComponent({
   props: {
@@ -45,48 +46,50 @@ export default defineComponent({
     readonly: {
       type: Boolean,
       required: false
-    },
-    error: {
-      type: Boolean,
-      required: false
-    },
-    errorMessage: {
-      type: String,
-      required: false,
-      default: null
     }
   },
-  setup () {
+  setup (props, {emit}) {
     const { app: { i18n } } = useContext()
+    const {error, onChange} = $useError(props.field, emit)
 
     const nationalNumber: Ref<string | number> = ref('')
     const internationalNumber: Ref<string | number> = ref('')
     const isValid: Ref<boolean> = ref(false)
-    const country: Ref<string> = ref('')
+    const onInit: Ref<boolean> = ref(true)
+
+    const onInput = (_: any, { number, valid, countryChoice }: { number: any, valid: boolean, countryChoice: any }) => {
+      isValid.value = valid
+      nationalNumber.value = number.national
+      internationalNumber.value = number.international
+      onInit.value = false
+    }
+
+    const onChangeValue = () => {
+      if (isValid.value) {
+        onChange(internationalNumber.value)
+      }
+    }
+
+    const myPhone = computed(
+      {
+        get:()=>{
+          return onInit.value ? props.data : nationalNumber.value
+        },
+        set:(value)=>{
+          return props.data
+        }
+      }
+    )
 
     return {
-      nationalNumber,
-      internationalNumber,
-      isValid,
-      country,
+      myPhone,
+      error,
+      onInput,
+      onChangeValue,
       rules: [
-        () => isValid.value || i18n.t('phone_error')
+        (phone: string) => (!phone || 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)
-      }
-    }
   }
 })
 

+ 31 - 13
components/Ui/Input/Text.vue

@@ -5,21 +5,24 @@ Champs de saisie de texte
 -->
 
 <template>
-  <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)"
-  />
+    <v-text-field
+      autocomplete="off"
+      :value="data"
+      :label="$t(label_field)"
+      :rules="rules"
+      :disabled="readonly"
+      :type="type"
+      :error="error || violations"
+      :error-messages="errorMessage"
+      @change="onChange($event)"
+      v-mask="mask"
+    />
 </template>
 
 <script lang="ts">
 import { defineComponent } from '@nuxtjs/composition-api'
+import {$useError} from "~/composables/form/useError";
+import {mask} from 'vue-the-mask';
 
 export default defineComponent({
   props: {
@@ -60,11 +63,26 @@ export default defineComponent({
       type: String,
       required: false,
       default: null
+    },
+    mask: {
+      type: [Array, Boolean],
+      required: false,
+      default: false
     }
   },
-  setup (props) {
+  setup (props, {emit}) {
+    const {error: violations, onChange} = $useError(props.field, emit)
+
     return {
-      label_field: props.label ?? props.field
+      label_field: props.label ?? props.field,
+      violations,
+      onChange
+    }
+  },
+  directives: {
+    mask: (el, binding, vnode, oldVnode) => {
+      if (!binding.value) return;
+      mask(el, binding, vnode, oldVnode);
     }
   }
 })

+ 76 - 0
components/Ui/Input/TextArea.vue

@@ -0,0 +1,76 @@
+<!--
+Champs de saisie de bloc texte
+
+@see https://vuetifyjs.com/en/components/textareas/
+-->
+
+<template>
+  <v-textarea
+      outlined
+      :value="data"
+      :label="$t(label_field)"
+      :rules="rules"
+      :disabled="readonly"
+      :error="error || violations"
+      :error-messages="errorMessage"
+      @change="onChange($event)"
+    />
+</template>
+
+<script lang="ts">
+import { defineComponent } from '@nuxtjs/composition-api'
+import {$useError} from "~/composables/form/useError";
+
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: false,
+      default: null
+    },
+    field: {
+      type: String,
+      required: false,
+      default: null
+    },
+    data: {
+      type: [String, Number],
+      required: false,
+      default: null
+    },
+    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, {emit}) {
+    const {error: violations, onChange} = $useError(props.field, emit)
+
+    return {
+      label_field: props.label ?? props.field,
+      violations,
+      onChange
+    }
+  }
+})
+</script>
+
+<style>
+  input:read-only{
+    color: #666 !important;
+  }
+</style>

+ 72 - 0
components/Ui/ItemFromUri.vue

@@ -0,0 +1,72 @@
+<!--
+Espace permettant de récupérer un item via une uri et de gérer son affichage via un slot
+-->
+
+<template>
+  <main>
+    <v-skeleton-loader
+      v-if="$fetchState.pending"
+      :type="loaderType"
+    />
+    <div v-else>
+      <slot name="item.text" v-bind="{item}" />
+    </div>
+    <slot />
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, useFetch, useContext, ComputedRef, computed } from '@nuxtjs/composition-api'
+import {QUERY_TYPE} from "~/types/enums";
+import { Query } from '@vuex-orm/core'
+import { Item } from '@vuex-orm/core/dist/src/data/Data'
+import ModelsUtils from "~/services/utils/modelsUtils";
+import {queryHelper} from "~/services/store/query";
+
+export default defineComponent({
+  props: {
+    uri: {
+      type: String,
+      required: false,
+      default: null
+    },
+    model: {
+      type: Function,
+      required: true
+    },
+    query: {
+      type: Object as () => Query,
+      required: true
+    },
+    loaderType: {
+      type: String,
+      required: false,
+      default: 'text'
+    }
+  },
+  setup (props) {
+    const { $dataProvider } = useContext()
+    const getIdFromUri = (uri: string) => {
+      return ModelsUtils.extractIdFromUri(uri)
+    }
+    const id = getIdFromUri(props.uri)
+
+    useFetch(async () => {
+      await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: props.model,
+        id: id
+      })
+    })
+    const item: ComputedRef<Item|null> = computed(() => {
+      if(id)
+        return queryHelper.getItem(props.query, id)
+      else return null
+    })
+
+    return {
+      item
+    }
+  }
+})
+</script>

+ 23 - 32
components/Ui/Map.vue

@@ -16,7 +16,7 @@ Leaflet map
         />
       </l-map>
 
-      <v-btn class="mr-4 ot_green ot_white--text" @click="updateMap">
+      <v-btn class="mr-4 mt-2 mb-2 ot_green ot_white--text" @click="updateMap">
         {{ $t('updateMap') }}
       </v-btn>
     </client-only>
@@ -24,12 +24,11 @@ Leaflet map
 </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'
+import {computed, ComputedRef, defineComponent, ref, Ref, toRefs, ToRefs, useContext} from '@nuxtjs/composition-api'
+import {QUERY_TYPE, TYPE_ALERT} from '~/types/enums'
+import {AddressPostal} from '~/models/Core/AddressPostal'
+import {AnyJson} from '~/types/interfaces'
+import Page from "~/services/store/page";
 
 export default defineComponent({
   props: {
@@ -43,48 +42,40 @@ export default defineComponent({
     }
   },
   setup (props, { emit }) {
-    const { $dataProvider, store } = useContext()
+    const { $dataProvider, store, app: {i18n} } = useContext()
     const { address }: ToRefs = toRefs(props)
-    const latitude: Ref<number> = ref(address.value.latitude)
-    const longitude: Ref<number> = ref(address.value.longitude)
+    const latitude: Ref<number> = ref(address.value.latitude ?? 0.0)
+    const longitude: Ref<number> = ref(address.value.longitude ?? 0.0)
 
     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}`
+        url: `/api/gps-coordinate-searching?street=${address.value.streetAddress}&cp=${address.value.postalCode}&city=${address.value.addressCity}`
       })
-      if (response.length > 0) {
-        latitude.value = response[0].latitude
-        longitude.value = response[0].longitude
+      const data = response.data
+      if (data.length > 0) {
+        latitude.value = data[0].latitude
+        longitude.value = data[0].longitude
 
-        address.value.latitude = response[0].latitude
-        address.value.longitude = response[0].longitude
+        address.value.latitude = data[0].latitude
+        address.value.longitude = data[0].longitude
         emit('updateAddress', address.value)
       } else {
-        const alert: alert = {
-          type: TYPE_ALERT.ALERT,
-          message: 'no_coordinate_corresponding'
-        }
-        store.commit('page/setAlert', alert)
+        new Page(store).addAlerts(TYPE_ALERT.ALERT, [i18n.t('no_coordinate_corresponding') as string])
       }
     }
 
     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)
+      if(event){
+        address.value.latitude = event.lat
+        address.value.longitude = event.lng
+        emit('updateAddress', address.value)
+      }
     }
 
     return {

+ 18 - 3
components/Ui/SubList.vue → components/Ui/SubResource.vue

@@ -1,13 +1,20 @@
-<!-- ? -->
+<!-- Permet de requêter une subResource et de donner son contenu à un slot -->
 
 <template>
   <main>
     <v-skeleton-loader
       v-if="$fetchState.pending"
-      type="text"
+      :type="loaderType"
     />
     <div v-else>
       <slot name="list.item" v-bind="{items}" />
+
+      <v-btn v-if="newLink" class="ot_white--text ot_green float-right">
+        <NuxtLink :to="newLink" class="no-decoration">
+          <v-icon>fa-plus-circle</v-icon>
+          <span>{{$t('add')}}</span>
+        </NuxtLink>
+      </v-btn>
     </div>
     <slot />
   </main>
@@ -39,6 +46,15 @@ export default defineComponent({
     query: {
       type: Object as () => Query,
       required: true
+    },
+    loaderType: {
+      type: String,
+      required: false,
+      default: 'text'
+    },
+    newLink: {
+      type: String,
+      required: false
     }
   },
   setup (props) {
@@ -54,7 +70,6 @@ export default defineComponent({
     })
 
     const items: ComputedRef<Collection> = computed(() => queryHelper.getCollection(query.value))
-    // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
 
     return {
       items

+ 65 - 0
components/Ui/Template/DataTable.vue

@@ -0,0 +1,65 @@
+<!--
+Tableau interactif
+
+@see https://vuetifyjs.com/en/components/data-tables/
+-->
+
+<template>
+  <v-col
+    cols="12"
+    sm="12"
+  >
+    <v-data-table
+      :headers="headersWithItem"
+      :items="items"
+      :locale="$i18n.locale"
+      class="elevation-1"
+      :footer-props="{
+        'items-per-page-text':  $t('itemsPerPage'),
+        'items-per-page-all-text':  $t('allResult')
+      }"
+    >
+      <template v-for="header in headersWithItem" #[header.item]="props">
+        <slot :name="header.item" v-bind="props">
+          {{ props.item[header.value] }}
+        </slot>
+      </template>
+
+      <template #[`footer.page-text`]="items">
+        {{ items.pageStart }} - {{ items.pageStop }} {{$t('of')}} {{ items.itemsLength }}
+      </template>
+
+    </v-data-table>
+  </v-col>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, toRefs } from '@nuxtjs/composition-api'
+
+export default defineComponent({
+  props: {
+    items: {
+      type: Array,
+      required: true
+    },
+    headers: {
+      type: Array,
+      required: true
+    }
+  },
+  setup (props) {
+    const { headers } = toRefs(props)
+
+    const headersWithItem = computed(() => {
+      return headers.value.map((header:any) => {
+        header.item = 'item.' + header.value
+        return header
+      })
+    })
+
+    return {
+      headersWithItem
+    }
+  }
+})
+</script>

+ 34 - 0
components/Ui/Template/Date.vue

@@ -0,0 +1,34 @@
+<!--
+template date formatée
+-->
+
+<template>
+  <span>{{datesFormatted}}</span>
+</template>
+
+<script lang="ts">
+import { defineComponent, useContext, computed, ComputedRef } from '@nuxtjs/composition-api'
+import DatesUtils from '~/services/utils/datesUtils'
+
+export default defineComponent({
+  props: {
+    data: {
+      type: [String, Array],
+      required: false,
+      default: null
+    }
+  },
+  setup (props) {
+    const { $moment } = useContext()
+    const dateUtils = new DatesUtils($moment)
+
+    const datesFormatted: ComputedRef<string> = computed(() => {
+      return dateUtils.formattedDate(props.data, 'DD/MM/YYYY')
+    })
+
+    return {
+      datesFormatted
+    }
+  }
+})
+</script>

+ 41 - 0
composables/data/useAccess.ts

@@ -0,0 +1,41 @@
+import { AnyJson } from '~/types/interfaces'
+import {QUERY_TYPE} from "~/types/enums";
+import { useContext} from '@nuxtjs/composition-api'
+import DataProvider from "~/services/data/dataProvider";
+
+/**
+ * @category composables/data
+ * @class UseAccess
+ * Use Classe qui va récupérer les Accesses suivant des critères de recherche
+ */
+export class UseAccess {
+  private $dataProvider!: DataProvider
+
+  constructor() {
+    const {$dataProvider} = useContext()
+    this.$dataProvider = $dataProvider
+  }
+
+  public invoke(): AnyJson{
+    return {
+      getPhysicalByFullName: (research: string) => this.getPhysicalByFullName(research),
+    }
+  }
+
+  private async getPhysicalByFullName(research: string): Promise<Array<AnyJson>>{
+    if(research){
+      const response = await this.$dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        url: `api/access_people`,
+        listArgs: {
+          filters:[
+            {key: 'person.isPhysical', value: 1},
+            {key: 'fullname', value: research}
+          ]
+        }
+      })
+      return response.data
+    }
+    return []
+  }
+}

+ 75 - 0
composables/data/useAddresspostal.ts

@@ -0,0 +1,75 @@
+import { AnyJson } from '~/types/interfaces'
+import {QUERY_TYPE} from "~/types/enums";
+import { useContext } from '@nuxtjs/composition-api'
+import DataProvider from "~/services/data/dataProvider";
+
+/**
+ * @category composables/data
+ * @class UseAddressPostal
+ * Use Classe pour gérer les deux champs postal code et adresseCity
+ */
+export class UseAddressPostal {
+  private $dataProvider!: DataProvider
+
+  constructor() {
+    const {$dataProvider} = useContext()
+    this.$dataProvider = $dataProvider
+  }
+
+  public invoke(): AnyJson{
+    return {
+      searchFunction: (research: string, field: string) => this.searchFunction(research, field),
+      updateCpAddress: (value:AnyJson, updateRepository: Function) => this.updateCpAddress(value, updateRepository),
+    }
+  }
+
+  /**
+   * Fonction de recherche qui utilise l'API gouvernematal pour autocompléter les CP et villes.
+   * @param research
+   * @param field
+   * @private
+   */
+  private async searchFunction (research: string, field: string): Promise<Array<AnyJson>>{
+    if(research){
+      const response = await this.$dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        url: `https://api-adresse.data.gouv.fr/search/?q=${research}&type=municipality&autocomplete=1&limit=20`,
+        params: {
+          noXaccessId: true
+        }
+      })
+      const apiResponse = response.data.features.map((data:AnyJson)=>data.properties)
+
+      // Par défaut on insère les valeurs que l'utilisateur a écrit, car un nom de ville ou de CP peut être absent de l'API
+      const defaultResponse = []
+      if(field === 'addressPostal.addressCity'){
+        defaultResponse.push({id:0, postcode: null, city: research})
+      }else{
+        defaultResponse.push({id:0, postcode: research, city: null})
+      }
+
+      return defaultResponse.concat(apiResponse)
+    }
+    return []
+  }
+
+  /**
+   * Fonction permettant de mettre à jour le repo par rapport à la réponse (objet) de l'autocomplete
+   * @param value
+   * @param updateRepository
+   * @private
+   */
+  private updateCpAddress(value:AnyJson, updateRepository: Function): void{
+    //Si une valeur est présente
+    if(value){
+      if(value.city)
+        updateRepository(value.city, 'addressPostal.addressCity')
+      if(value.postcode)
+        updateRepository(value.postcode, 'addressPostal.postalCode')
+    }else{
+      //Cas où on efface les valeurs des champs
+      updateRepository(null, 'addressPostal.addressCity')
+      updateRepository(null, 'addressPostal.postalCode')
+    }
+  }
+}

+ 42 - 0
composables/data/useCountry.ts

@@ -0,0 +1,42 @@
+import { AnyJson } from '~/types/interfaces'
+import {QUERY_TYPE} from "~/types/enums";
+import {Country} from "~/models/Core/Country";
+import { useContext, useFetch, computed } from '@nuxtjs/composition-api'
+import DataProvider from "~/services/data/dataProvider";
+import {repositoryHelper} from "~/services/store/repository";
+
+/**
+ * @category composables/data
+ * @class UseCountry
+ * Use Classe qui va récupérer les Countries
+ */
+export class UseCountry {
+  private $dataProvider!: DataProvider
+
+  constructor() {
+    const {$dataProvider} = useContext()
+    this.$dataProvider = $dataProvider
+  }
+
+  /**
+   * Récupération des Country via l'API
+   */
+  public getAll(): AnyJson{
+    const {fetch, fetchState} = useFetch(async () => {
+      await this.$dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: Country
+      })
+    })
+
+    const countries =  computed(() => {
+      return repositoryHelper.findCollectionFromModel(Country)
+    })
+
+    return {
+      countries,
+      fetch,
+      fetchState
+    }
+  }
+}

+ 74 - 0
composables/data/useDataUtils.ts

@@ -0,0 +1,74 @@
+import { AnyJson } from '~/types/interfaces'
+import {FORM_STATUS, QUERY_TYPE} from "~/types/enums";
+import {ref, Ref, useContext, useFetch} from '@nuxtjs/composition-api'
+import DataProvider from "~/services/data/dataProvider";
+import {Model} from "@vuex-orm/core";
+import {repositoryHelper} from "~/services/store/repository";
+import {Store} from "vuex";
+
+/**
+ * @category composables/data
+ * @class UseDataUtils
+ * Use Classe qui va récupérer les Accesses suivant des critères de recherche
+ */
+export class UseDataUtils {
+  private $dataProvider: DataProvider
+  private route: Ref
+  private store: Store<any>
+
+  constructor() {
+    const { route, $dataProvider, store } = useContext()
+    this.$dataProvider = $dataProvider
+    this.store = store
+    this.route = route
+  }
+
+  public invoke(): AnyJson{
+    return {
+      getItemToEdit: (model: typeof Model) => this.getItemToEdit(model),
+      createItem: () => this.createItem(),
+    }
+  }
+
+  /**
+   * recherche l'item a éditer
+   * @param model
+   * @private
+   */
+  private getItemToEdit(model: typeof Model){
+    const id = parseInt(this.route.value.params.id)
+    const {fetchState} = useFetch(async () => {
+      await this.$dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model,
+        id
+      })
+    })
+    return {
+      fetchState,
+      id
+    }
+  }
+
+  /**
+   * Créer l'Item quand l'utilisateur veux créer un nouvel enregistrement
+   * @param itemToCreate
+   * @param model
+   * @private
+   */
+  private createItem(){
+    this.store.commit('form/setFormStatus', FORM_STATUS.CREATE)
+    const loading:Ref<Boolean> = ref(true)
+    const item: Ref<any> = ref('')
+    const create = async (itemToCreate: AnyJson, model: typeof Model) =>{
+      item.value = await repositoryHelper.persist(model, itemToCreate)
+      loading.value = false
+    }
+
+    return {
+      loading,
+      create,
+      item
+    }
+  }
+}

+ 64 - 0
composables/data/useImage.ts

@@ -0,0 +1,64 @@
+import {AnyJson, ApiResponse} from '~/types/interfaces'
+import {QUERY_TYPE} from "~/types/enums";
+import {useContext, useFetch, Ref, ref} from '@nuxtjs/composition-api'
+import DataProvider from "~/services/data/dataProvider";
+
+/**
+ * @category composables/data
+ * @class UseImage
+ * Use Classe qui va récupérer les Images
+ */
+export class UseImage {
+  private $dataProvider!: DataProvider
+  private $config!: AnyJson
+
+  constructor() {
+    const {$dataProvider, $config} = useContext()
+    this.$dataProvider = $dataProvider
+    this.$config = $config
+  }
+
+  /**
+   * Récupération d'une image via l'ancienne API
+   */
+  public getOne(id: number|undefined, imageByDefault: string = '', height: number|undefined = 0, width: number|undefined = 0): AnyJson{
+    const imageLoaded: Ref<String> = ref('')
+
+    const {fetchState, fetch} = useFetch(async () => {
+        try{
+          if(id){
+            imageLoaded.value = await this.provideImg(id, height, width)
+          }else
+            throw new Error('id is null')
+        }catch (e){
+          if(imageByDefault)
+            imageLoaded.value = require(`assets/images/byDefault/${imageByDefault}`)
+        }
+      }
+    )
+
+    return {
+      fetch,
+      fetchState,
+      imageLoaded
+    }
+  }
+
+  /**
+   * retourne l'image demandée
+   * @param id
+   * @param height
+   * @param width
+   */
+  public async provideImg(id: number, height: number = 0, width: number = 0){
+    return await this.$dataProvider.invoke({
+      type: QUERY_TYPE.IMAGE,
+      baseUrl: this.$config.baseURL_Legacy,
+      imgArgs: {
+        id: id,
+        height: height,
+        width: width
+      }
+    })
+  }
+}

+ 104 - 0
composables/data/useMyProfile.ts

@@ -0,0 +1,104 @@
+import {computed, ComputedRef} from '@nuxtjs/composition-api'
+import { Item, Model } from '@vuex-orm/core'
+import { repositoryHelper } from '~/services/store/repository'
+import { QUERY_TYPE } from '~/types/enums'
+import { Historical } from '~/types/interfaces'
+import { MyProfile } from '~/models/Access/MyProfile'
+import { $accessProfile } from '@/services/profile/accessProfile'
+import { useContext } from '@nuxtjs/composition-api'
+
+/**
+ * Composable function
+ */
+export function useMyProfile(){
+  const {$dataPersister, store} = useContext()
+  $accessProfile.setStore(store)
+  const currentAccessId = $accessProfile.getCurrentAccessId()
+  const myProfile = getMyProfileInstance(currentAccessId) as MyProfile
+  const activityYear:ComputedRef<number> = computed(() => myProfile.activityYear)
+  const historical:ComputedRef<Historical> = computed(() => myProfile.historical)
+
+  /**
+   * Effectue la mise à jour (coté API) de MyProfile
+   */
+  async function updateMyProfile (): Promise<any> {
+    await $dataPersister.invoke({
+      type: QUERY_TYPE.MODEL,
+      model: MyProfile,
+      id: myProfile.id
+    })
+  }
+
+  /**
+   * Mets à jour l'activity de my profile
+   * @param activityYear
+   */
+  function setActivityYear (activityYear:number) {
+    if (activityYear <= 0) { throw new Error('year must be positive') }
+    repositoryHelper.updateStoreFromField(MyProfile, myProfile, activityYear, 'activityYear')
+  }
+
+  /**
+   * Mets à jour l'historical de my profile
+   * @param historicalChoices
+   */
+  function setHistorical (historicalChoices:Array<string>) {
+    repositoryHelper.updateStoreFromField(MyProfile, myProfile, getHistoricalEntry(historicalChoices), 'historical')
+  }
+
+
+  /**
+   * Mets à jour l'historical de my profile
+   * @param dates
+   */
+  function setHistoricalRange (dates:Array<string>) {
+    repositoryHelper.updateStoreFromField(MyProfile, myProfile, getHistoricalRangeEntry(dates), 'historical')
+  }
+
+  return {
+    currentAccessId,
+    activityYear,
+    historical,
+    updateMyProfile,
+    setActivityYear,
+    setHistorical,
+    setHistoricalRange
+  }
+}
+
+/**
+ * récupère l'instance MyProfile
+ * @param myProfileId
+ */
+function getMyProfileInstance (myProfileId:any): Item<Model> {
+  return repositoryHelper.findItemFromModel(MyProfile, parseInt(myProfileId))
+}
+
+/**
+ * Transform les choix de l'historique en objet JSON reconnaissable coté API
+ * @param historicalChoices
+ */
+function getHistoricalEntry (historicalChoices:Array<string>) {
+  const historicalDefault:any = { past: false, future: false, present: false }
+  for (const historicalChoice of historicalChoices) {
+    historicalDefault[historicalChoice] = true
+  }
+  return historicalDefault
+}
+
+/**
+ * Transforme le choix des période en Objet JSON reconnaissable coté API
+ * @param dates
+ */
+function getHistoricalRangeEntry (dates:Array<string>) {
+  return { past: false, future: false, present: false, dateStart: dates[0], dateEnd: dates[1] }
+}
+
+/**
+ * Const servant à assurer les tests des fonctions non exportées
+ */
+export const _exportedForTesting = {
+  getMyProfileInstance,
+  getHistoricalEntry,
+  getHistoricalRangeEntry
+}

+ 42 - 0
composables/data/useTypeOfPractice.ts

@@ -0,0 +1,42 @@
+import { AnyJson } from '~/types/interfaces'
+import {QUERY_TYPE} from "~/types/enums";
+import { useContext, useFetch, computed } from '@nuxtjs/composition-api'
+import DataProvider from "~/services/data/dataProvider";
+import {repositoryHelper} from "~/services/store/repository";
+import {TypeOfPractice} from "~/models/Organization/TypeOfPractice";
+
+/**
+ * @category composables/data
+ * @class UseTypeOfPractice
+ * Use Classe qui va récupérer les UseTypeOfPractices
+ */
+export class UseTypeOfPractice {
+  private $dataProvider!: DataProvider
+
+  constructor() {
+    const {$dataProvider} = useContext()
+    this.$dataProvider = $dataProvider
+  }
+
+  /**
+   * Récupération des UseTypeOfPractices via l'API
+   */
+  public getAll(): AnyJson{
+    const {fetch, fetchState} = useFetch(async () => {
+      await this.$dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: TypeOfPractice
+      })
+    })
+
+    const typeOfPractices =  computed(() => {
+      return repositoryHelper.findCollectionFromModel(TypeOfPractice)
+    })
+
+    return {
+      typeOfPractices,
+      fetch,
+      fetchState
+    }
+  }
+}

+ 41 - 0
composables/form/useError.ts

@@ -0,0 +1,41 @@
+import { AnyJson, AnyStore } from '~/types/interfaces'
+import {computed, ComputedRef, useContext} from "@nuxtjs/composition-api";
+
+/**
+ * @category composables/form
+ * @class UseError
+ * Use Classe pour gérer l'apparition de message si le formulaire est dirty
+ */
+export class UseError {
+  private store: AnyStore
+
+  constructor () {
+    const {store} = useContext()
+    this.store = store
+  }
+
+  /**
+   * Composition function
+   */
+  public invoke (field: string, emit: any): AnyJson {
+    const error:ComputedRef<Boolean> = computed(()=>{
+      return this.store.state.form.violations.indexOf(field) >= 0
+    })
+
+    return {
+      onChange: (fieldValue:any) => this.onChange(emit, fieldValue, field),
+      error
+    }
+  }
+
+  /**
+   *
+   */
+  private onChange (emit:any, fieldValue:any, changeField:string) {
+    const errors = this.store.state.form.violations.filter((field:string) => field !== changeField)
+    this.store.commit('form/setViolations', errors)
+    emit('update', fieldValue, changeField)
+  }
+}
+
+export const $useError = (field: string, emit: any) => new UseError().invoke(field, emit)

+ 13 - 7
use/form/useDirtyForm.ts → composables/form/useForm.ts

@@ -1,26 +1,33 @@
 import { AnyJson, AnyStore } from '~/types/interfaces'
+import {computed, ComputedRef, useContext} from "@nuxtjs/composition-api";
 
 /**
- * @category Use/form
+ * @category composables/form
  * @class UseDirtyForm
  * Use Classe pour gérer l'apparition de message si le formulaire est dirty
  */
-export class UseDirtyForm {
+export class UseForm {
   private store: AnyStore
   private readonly handler: any
 
-  constructor (handler: any, store: AnyStore) {
+  constructor () {
+    const {store} = useContext()
     this.store = store
-    this.handler = handler
+    this.handler = getEventHandler()
   }
 
   /**
    * Composition function
    */
   public invoke (): AnyJson {
+    const readonly: ComputedRef<boolean> = computed(() => {
+      return this.store.state.form.readonly
+    })
+
     return {
       markFormAsDirty: () => this.markAsDirty(),
-      markFormAsNotDirty: () => this.markAsNotDirty()
+      markFormAsNotDirty: () => this.markAsNotDirty(),
+      readonly
     }
   }
 
@@ -67,5 +74,4 @@ function getEventHandler () {
     e.returnValue = ''
   }
 }
-const handler = getEventHandler()
-export const $useDirtyForm = (store: AnyStore) => new UseDirtyForm(handler, store).invoke()
+export const $useForm = () => new UseForm().invoke()

+ 5 - 0
use/form/useNavigationHelpers.ts → composables/form/useNavigationHelpers.ts

@@ -1,6 +1,11 @@
 import { onMounted, ref, useContext, Ref } from '@nuxtjs/composition-api'
 import * as _ from 'lodash'
 
+/**
+ * @category composables/form
+ * @class UseNavigationHelpers
+ * Use Classe pour gérer les expansions des accordions
+ */
 export class UseNavigationHelpers {
   public static expansionPanels () {
     const { route } = useContext()

+ 51 - 0
composables/form/useNextStepFactory.ts

@@ -0,0 +1,51 @@
+import {FORM_STATUS, SUBMIT_TYPE} from "~/types/enums";
+import {useContext} from "@nuxtjs/composition-api";
+import {Store} from "vuex";
+import {AnyJson} from "~/types/interfaces";
+import {Route} from "vue-router";
+
+/**
+ * @category composables/form
+ * @class UseNextStepFactory
+ * Use Classe pour gérer actions post submit
+ */
+export default class UseNextStepFactory{
+  private store: Store<any>
+  private router: any
+
+  constructor() {
+    const {store, app: {router}} = useContext()
+
+    this.store = store
+    this.router = router
+  }
+
+
+  invoke(args: any, response: AnyJson){
+    const factory: AnyJson = {}
+    factory[SUBMIT_TYPE.SAVE] = () => this.save(args, response.id)
+    factory[SUBMIT_TYPE.SAVE_AND_BACK] = () => this.saveAndGoTo(args)
+
+    return factory
+  }
+
+  /**
+   * Action Sauvegarder qui redirige vers la page d'edit si on est en mode create
+   * @param route
+   * @param id
+   */
+  save(route: Route, id: number){
+    if(this.store.state.form.formStatus === FORM_STATUS.CREATE){
+      route.path += id
+      this.router.push(route)
+    }
+  }
+
+  /**
+   * Action sauvegarder et route suivante qui redirige vers une route
+   * @param route
+   */
+  saveAndGoTo(route: Route){
+    this.router.push(route)
+  }
+}

+ 2 - 2
use/form/useValidator.ts → composables/form/useValidator.ts

@@ -4,7 +4,7 @@ import { QUERY_TYPE } from '~/types/enums'
 import { DataManager } from '~/types/interfaces'
 
 /**
- * @category Use/form
+ * @category composables/form
  * @class UseValidator
  * Use Classe pour des utils de verifications
  */
@@ -19,7 +19,7 @@ class UseValidator {
     const checkSiret = async (siret: string) => {
       const response = await $dataManager.invoke({
         type: QUERY_TYPE.DEFAULT,
-        url: 'siret-checking',
+        url: '/api/siret-checking',
         id: siret
       })
       if (typeof response !== 'undefined') {

+ 2 - 2
use/layout/Menus/accessMenu.ts → composables/layout/Menus/accessMenu.ts

@@ -2,10 +2,10 @@ import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
 import { $organizationProfile } from '~/services/profile/organizationProfile'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class AccessMenu
  * Classe pour la construction du Menu Répertoire
  */

+ 2 - 2
use/layout/Menus/accountMenu.ts → composables/layout/Menus/accountMenu.ts

@@ -1,10 +1,10 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class AccountMenu
  * Classe pour la construction du Menu Mon compte
  */

+ 2 - 2
use/layout/Menus/admin2iosMenu.ts → composables/layout/Menus/admin2iosMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
 import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class Admin2iosMenu
  * Classe pour la construction du Menu Admin 2IOS
  */

+ 2 - 2
use/layout/Menus/agendaMenu.ts → composables/layout/Menus/agendaMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
 import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class AgendaMenu
  * Classe pour la construction du Menu agenda
  */

+ 1 - 1
use/layout/Menus/baseMenu.ts → composables/layout/Menus/baseMenu.ts

@@ -2,7 +2,7 @@ import { NuxtConfig } from '@nuxt/types/config'
 import { ItemMenu } from '~/types/interfaces'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class BaseMenu
  * Classe abstraite pour chacun des menu
  */

+ 2 - 2
use/layout/Menus/billingMenu.ts → composables/layout/Menus/billingMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
 import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class BillingMenu
  * Classe pour la construction du Menu Facturation
  */

+ 2 - 2
use/layout/Menus/communicationMenu.ts → composables/layout/Menus/communicationMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
 import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class CommunicationMenu
  * Classe pour la construction du Menu Communication
  */

+ 2 - 2
use/layout/Menus/configurationMenu.ts → composables/layout/Menus/configurationMenu.ts

@@ -1,10 +1,10 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class ConfigurationMenu
  * Classe pour la construction du Menu Paramètres
  */

+ 3 - 3
use/layout/Menus/cotisationsMenu.ts → composables/layout/Menus/cotisationsMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
-import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class CotisationsMenu
  * Classe pour la construction du Menu Cotisation (CMF)
  */

+ 2 - 2
use/layout/Menus/donorsMenu.ts → composables/layout/Menus/donorsMenu.ts

@@ -1,10 +1,10 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import { ItemMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class DonorsMenu
  * Classe pour la construction du Menu Donneurs
  */

+ 2 - 2
use/layout/Menus/educationalMenu.ts → composables/layout/Menus/educationalMenu.ts

@@ -1,10 +1,10 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class EducationalMenu
  * Classe pour la construction du Menu Suivi pédagogique
  */

+ 2 - 2
use/layout/Menus/equipmentMenu.ts → composables/layout/Menus/equipmentMenu.ts

@@ -1,10 +1,10 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import { ItemMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class EquipmentMenu
  * Classe pour la construction du Menu Matériel
  */

+ 2 - 2
use/layout/Menus/medalsMenu.ts → composables/layout/Menus/medalsMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
 import { ItemMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class MedalsMenu
  * Classe pour la construction du Menu Médailles
  */

+ 2 - 2
use/layout/Menus/myAccessesMenu.ts → composables/layout/Menus/myAccessesMenu.ts

@@ -1,11 +1,11 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import * as _ from 'lodash'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class MyAccessesMenu
  * Classe pour la construction du Menu Mon profile
  */

+ 2 - 2
use/layout/Menus/myFamilyMenu.ts → composables/layout/Menus/myFamilyMenu.ts

@@ -1,11 +1,11 @@
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
 import * as _ from 'lodash'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class MyFamilyMenu
  * Classe pour la construction du Menu Famille
  */

+ 2 - 2
use/layout/Menus/statsMenu.ts → composables/layout/Menus/statsMenu.ts

@@ -1,10 +1,10 @@
 import { NuxtConfig } from '@nuxt/types/config'
 import { Ability } from '@casl/ability'
 import { ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class StatsMenu
  * Classe pour la construction du Menu Statistiques
  */

+ 9 - 6
use/layout/Menus/websiteMenu.ts → composables/layout/Menus/websiteMenu.ts

@@ -1,11 +1,11 @@
 import * as _ from 'lodash'
 import { Ability } from '@casl/ability'
 import { NuxtConfig } from '@nuxt/types/config'
-import BaseMenu from '~/use/layout/Menus/baseMenu'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
 import { AnyStore, ItemMenu, ItemsMenu, Menu, organizationState } from '~/types/interfaces'
 
 /**
- * @category Use/layout/Menus
+ * @category composables/layout/Menus
  * @class WebsiteMenu
  * Classe pour la construction du Menu Sites internet
  */
@@ -31,11 +31,12 @@ class WebsiteMenu extends BaseMenu implements Menu {
     const children: ItemsMenu = []
 
     if (!this.$store.state.profile.organization.website && this.$store.state.profile.access.isAdminAccess) {
-      children.push(this.constructMenu('fa-globe-europe', 'simple_modification', this.getWebsite(this.$store.state.profile.organization), false, undefined, true))
-      children.push(this.constructMenu('fa-globe-europe', 'advanced_modification', this.getWebsite(this.$store.state.profile.organization) + '/typo3', false, undefined, true))
+      children.push(this.constructMenu(this.$store.state.profile.organization.name, undefined, this.getWebsite(this.$store.state.profile.organization), false, undefined, true))
+      children.push(this.constructMenu('simple_modification', 'fa-globe-europe', this.getWebsite(this.$store.state.profile.organization), false, undefined, true))
+      children.push(this.constructMenu('advanced_modification', 'fa-globe-europe', this.getWebsite(this.$store.state.profile.organization) + '/typo3', false, undefined, true))
     }
 
-    return children.length > 0 ? this.constructMenu('fa-globe-europe', 'website', undefined, undefined, children) : null
+    return children.length > 0 ? this.constructMenu('website','fa-globe-europe',  undefined, undefined, children) : null
   }
 
   /**
@@ -48,7 +49,9 @@ class WebsiteMenu extends BaseMenu implements Menu {
     children.push(this.constructMenu(this.$store.state.profile.organization.name, undefined, this.getWebsite(this.$store.state.profile.organization), false, undefined, true))
 
     _.each(this.$store.state.profile.organization.parents, (parent) => {
-      children.push(this.constructMenu(parent.name, undefined, this.getWebsite(parent), false, undefined, true))
+      if(parent.id != process.env.OPENTALENT_MANAGER_ID){
+        children.push(this.constructMenu(parent.name, undefined, this.getWebsite(parent), false, undefined, true))
+      }
     })
 
     return children.length > 0 ? this.constructMenu('website', 'fa-globe-europe', undefined, undefined, children) : null

+ 17 - 17
use/layout/menu.ts → composables/layout/menu.ts

@@ -1,25 +1,25 @@
 import { ref, useContext, Ref } from '@nuxtjs/composition-api'
 import { Ability } from '@casl/ability'
 import { ItemMenu, ItemsMenu, OrganizationStore } from '~/types/interfaces'
-import { getAccessMenu } from '~/use/layout/Menus/accessMenu'
-import { getAgendaMenu } from '~/use/layout/Menus/agendaMenu'
-import { getEquipmentMenu } from '~/use/layout/Menus/equipmentMenu'
-import { getEducationalMenu } from '~/use/layout/Menus/educationalMenu'
-import { getBillingMenu } from '~/use/layout/Menus/billingMenu'
-import { getCommunicationMenu } from '~/use/layout/Menus/communicationMenu'
-import { getDonorsMenu } from '~/use/layout/Menus/donorsMenu'
-import { getMedalsMenu } from '~/use/layout/Menus/medalsMenu'
-import { getStatsMenu } from '~/use/layout/Menus/statsMenu'
-import { getCotisationsMenu } from '~/use/layout/Menus/cotisationsMenu'
-import { getAdmin2iosMenu } from '~/use/layout/Menus/admin2iosMenu'
-import { getWebsiteMenu } from '~/use/layout/Menus/websiteMenu'
-import { getConfigurationMenu } from '~/use/layout/Menus/configurationMenu'
-import { getMyFamilyMenu } from '~/use/layout/Menus/myFamilyMenu'
-import { getMyAccessesMenu } from '~/use/layout/Menus/myAccessesMenu'
-import { getAccountMenu } from '~/use/layout/Menus/accountMenu'
+import { getAccessMenu } from '~/composables/layout/Menus/accessMenu'
+import { getAgendaMenu } from '~/composables/layout/Menus/agendaMenu'
+import { getEquipmentMenu } from '~/composables/layout/Menus/equipmentMenu'
+import { getEducationalMenu } from '~/composables/layout/Menus/educationalMenu'
+import { getBillingMenu } from '~/composables/layout/Menus/billingMenu'
+import { getCommunicationMenu } from '~/composables/layout/Menus/communicationMenu'
+import { getDonorsMenu } from '~/composables/layout/Menus/donorsMenu'
+import { getMedalsMenu } from '~/composables/layout/Menus/medalsMenu'
+import { getStatsMenu } from '~/composables/layout/Menus/statsMenu'
+import { getCotisationsMenu } from '~/composables/layout/Menus/cotisationsMenu'
+import { getAdmin2iosMenu } from '~/composables/layout/Menus/admin2iosMenu'
+import { getWebsiteMenu } from '~/composables/layout/Menus/websiteMenu'
+import { getConfigurationMenu } from '~/composables/layout/Menus/configurationMenu'
+import { getMyFamilyMenu } from '~/composables/layout/Menus/myFamilyMenu'
+import { getMyAccessesMenu } from '~/composables/layout/Menus/myAccessesMenu'
+import { getAccountMenu } from '~/composables/layout/Menus/accountMenu'
 
 /**
- * @category Use/layout
+ * @category composables/layout
  * @class Menu
  * Use Classe pour la construction du Menu
  */

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

@@ -6,7 +6,7 @@
           parameters:
             - {action: 'read', subject: 'mails'}
             - {action: 'read', subject: 'emails'}
-            - {action: 'read', subject: 'sms'}
+            - {action: 'read', subject: 'texto'}
       organization:
         - {function: hasModule, parameters: ['MessagesAdvanced']}
 
@@ -18,7 +18,7 @@
           parameters:
             - {action: 'read', subject: 'mails'}
             - {action: 'read', subject: 'emails'}
-            - {action: 'read', subject: 'sms'}
+            - {action: 'read', subject: 'texto'}
       organization:
         - {function: hasModule, parameters: ['MessagesAdvanced']}
 
@@ -30,6 +30,6 @@
           parameters:
             - {action: 'read', subject: 'mails'}
             - {action: 'read', subject: 'emails'}
-            - {action: 'read', subject: 'sms'}
+            - {action: 'read', subject: 'texto'}
       organization:
         - {function: hasModule, parameters: ['MessagesAdvanced']}

+ 2 - 1
config/nuxtConfig/env.js

@@ -6,7 +6,8 @@ export default {
     artist_premium_product: 'artist-premium',
     manager_product: 'manager',
     cmf_network: 'CMF',
-    ffec_network: 'FFEC'
+    ffec_network: 'FFEC',
+    OPENTALENT_MANAGER_ID: 93931
   },
   publicRuntimeConfig: {
     http: {

+ 1 - 0
config/nuxtConfig/plugins.js

@@ -9,6 +9,7 @@ export default {
     '~/plugins/Data/dataPersister',
     '~/plugins/Data/dataProvider',
     '~/plugins/Data/dataDeleter',
+    '~/plugins/vuexOrm.js',
     '~/plugins/phone-input'
   ]
 }

+ 1 - 1
jest.config.js

@@ -23,7 +23,7 @@ module.exports = {
     '<rootDir>/components/**/*.vue',
     '<rootDir>/middleware/**/*.ts',
     '<rootDir>/services/**/*.ts',
-    '<rootDir>/use/**/*.ts',
+    '<rootDir>/composables/**/*.ts',
     '<rootDir>/pages/**/*.vue'
   ],
   setupFiles: ['<rootDir>/tests/unit/index.ts']

+ 42 - 0
lang/content/subscription/fr-FR.js

@@ -0,0 +1,42 @@
+/**
+ * Specific translations for the /subscription page
+ *
+ * @param context
+ * @param locale
+ * @returns {{get_more_functionalities_with_version: string, only_for_cmf_members: string, contact_us_at: string, contact_us_for_show_and_demo: string, starting_from_x_eur_ttc_per_month: string, download_order_form: string, for_x_sms: string, for_only_x_eur_ttc_by_month: string, example: string, domain_name: string, and_benefit: string, public_price_x_ttc_a_year: string, product_sheet: string, get_your_own_domain_and_up_to_five_emails_for_only_x_eur_ttc_per_month: string, dummy_domain_name: string, website: string, version_x_up_to_x_students: string, download_cmf_order_form: string, send_sms_from_app_to_your_members: string, freely_try_our_software: string, starting_from_x_eur_ttc_per_ssm: string, yearly_paid_giving_x_eur_ttc_per_year: string, excluding_license_and_training_fees: string, a_suitable_solution_for_your_artistic_school: string, dummy_email_address: string, associated_mail_address: string, switch_to_version: string, or_by_mail_at: string, of_accounts_for_teachers_and_students: string, of_a_complete_website: string}}
+ */
+export default (context, locale) => {
+  return ({
+    get_more_functionalities_with_version: 'Bénéficiez de plus de fonctionnalités avec la version',
+    for_only_x_eur_ttc_by_month: 'Pour seulement {price} TTC par mois',
+    yearly_paid_giving_x_eur_ttc_per_year: 'Payable annuellement, soit {price} TTC / an',
+    only_for_cmf_members: 'Offre réservée aux adhérents CMF',
+    public_price_x_ttc_a_year: 'Prix public: {price} TTC/an',
+    product_sheet: 'Fiche produit',
+    download_order_form: 'Télécharger le bon de commande',
+    download_cmf_order_form: 'Télécharger le bon de commande CMF',
+    a_suitable_solution_for_your_artistic_school: 'Une solution économique adaptée à votre établissement d\'enseignement artistique',
+    starting_from_x_eur_ttc_per_month: 'A partir de {price} TTC par mois',
+    version_x_up_to_x_students: 'Version {product} jusqu\'à {max_students} étudiants',
+    excluding_license_and_training_fees: 'Hors frais de licence d\'utilisation et de formation',
+    freely_try_our_software: 'Essayez notre logiciel en toute liberté',
+    contact_us_for_show_and_demo: 'Contactez-nous sans plus tarder pour obtenir une présentation ainsi qu\'un accès de démonstration',
+    contact_us_at: 'Contactez-nous au',
+    or_by_mail_at: 'ou par mail à l\'adresse',
+    switch_to_version: 'Passez à la version',
+    and_benefit: 'et bénéficiez',
+    of_accounts_for_teachers_and_students: 'de comptes pour vos professeurs et élèves',
+    of_a_complete_website: 'd\'un site internet complet',
+    send_sms: 'Envoyez des SMS',
+    to_your_members_from_app: 'à vos membres / élèves depuis votre logiciel',
+    starting_from_x_eur_ttc_per_sms: 'A partir de {price} TTC / sms',
+    for_x_sms: 'pour {amount} SMS',
+    get_your_own_domain_and_up_to_five_emails_for_only_x_eur_ttc_per_month: 'Bénéficiez de votre propre nom de domaine et 5 adresses email pour seulement {price} TTC / mois',
+    example: 'Exemple',
+    domain_name: 'Nom de domaine',
+    dummy_domain_name: 'ma-structure.fr',
+    associated_mail_address: 'Adresse email associée',
+    dummy_email_address: 'contact@ma-structure.fr',
+    sms: 'SMS'
+  })
+}

+ 44 - 3
lang/enum/fr-FR.js

@@ -1,5 +1,46 @@
 export default (context, locale) => {
   return ({
+    CATEGORY_ORCHESTRE: 'Orchestre',
+    CATEGORY_AMBULATORY: 'Musique ambulatoire',
+    CATEGORY_OTHER: 'Autres activités',
+    CATEGORY_CHORUS: 'Chorale / Groupe vocal',
+    CATEGORY_BAND: 'Ensemble',
+    BRASS_BAND: 'Brass band',
+    HUNTING_HORNS: 'Trompes de chasse',
+    PHILHARMONIC_ORCHESTRA: 'Orchestre philharmonique',
+    ACCORDION_ORCHESTRA: 'Orchestre d\'accordéons',
+    HARMONY_ORCHESTRA: 'Orchestre d\'harmonie',
+    ORCHESTRA_CLASS: 'Classe d\'orchestre',
+    SYMPHONY_ORCHESTRA: 'Orchestre symphonique',
+    STRING_ORCHESTRA: 'Orchestre à cordes',
+    PLUCKED_ORCHESTRA: 'Orchestre à plectres',
+    FANFARE_BAND: 'Orchestre de fanfare',
+    BAGAD: 'Bagad',
+    BANDAS: 'Bandas ou Fanfare de rue',
+    BATTERY_FANFARE: 'Batterie fanfare',
+    BATTUCADA: 'Battucada',
+    FOLKLORIC_BAND: 'Ensemble folklorique',
+    FIFE_AND_DRUM: 'Fifres et tambours',
+    MARCHING_BAND: 'Marching band ou Show parade',
+    CHILDRENS_CHOIR: 'Choeur d\'enfants',
+    FEMAL_CHOIR: 'Choeur de femmes',
+    MENS_CHOIR: 'Choeur d\'hommes',
+    MIXED_CHORUS: 'Choeur mixte',
+    VOCAL_BAND_UP_16: 'Ensemble vocal (jusqu\'à 16)',
+    CLARINET_CHOIR: 'Ensemble de clarinettes',
+    COPPER_BAND: 'Ensemble de cuivres',
+    FLUTE_ENSEMBLE: 'Ensemble de flûtes',
+    SAXOPHONES_BAND: 'Ensemble de saxophones',
+    VIOLIN_BAND: 'Ensemble de violons',
+    PERCUSSION_BAND: 'Ensemble de percussions',
+    CURRENT_MUSIC_GROUP: 'Groupe de Musique actuelle',
+    CHAMBER_MUSIC_ENSEMBLE: 'Ensemble de Musique de chambre',
+    TRADITIONAL_MUSIC_ENSEMBLE: 'Ensemble de Musique traditionnelle',
+    JAZZ_BAND: 'Ensemble de Jazz',
+    EDUCATION: 'Enseignement',
+    CHEERLEADER: 'Majorettes',
+    TROOP: 'Troupe',
+    BIG_BAND: 'Big band',
     PRODUCT_ARTIST: 'Opentalent Artist',
     PRODUCT_ARTIST_PREMIUM: 'Opentalent Artist Premium',
     PRODUCT_SCHOOL: 'Opentalent School',
@@ -18,13 +59,13 @@ export default (context, 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",
+    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',
-    EENC: "EENC Établissement d'enseignement artistique non classé",
+    EENC: 'EENC Établissement d\'enseignement artistique non classé',
     EMP: 'EMP Ecole de musique privée',
     MULTIPLE: 'Multiple',
     UNIQUE: 'Unique',
@@ -64,7 +105,7 @@ export default (context, locale) => {
     ADDRESS_OTHER: 'Autre adresse',
     PRINCIPAL: 'Contact principal',
     BILL: 'Contact de facturation',
-    OTHER: 'Autre contact',
+    OTHER: 'Autre',
     CONTACT: 'Contact'
   })
 }

+ 11 - 0
lang/field/fr-FR.js

@@ -1,5 +1,16 @@
 export default (context, locale) => {
   return ({
+    title: 'Titre',
+    link: 'Lien',
+    organizationArticle: 'Coups de projecteur',
+    youtube: 'Lien YouTube',
+    first_subscription: 'Date de première adhésion',
+    bicInvalid: 'Code BIC invalide',
+    ibanInvalid: 'IBAN invalide',
+    invalid_bic: 'Votre BIC est invalide',
+    invalid_iban: 'Votre IBAN est invalide',
+    importAddress: 'Importer l\'adresse d\'une des personnes de votre structure',
+    addressCountry: 'Pays',
     legalInformation: 'Informations légales',
     agrements: 'Agréments',
     salary: 'Salariés',

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

@@ -1,6 +1,13 @@
 export default (context, locale) => {
   return ({
+    upload_image: 'Sélectionner une image',
+    of: 'de',
+    allResult: 'Tous',
+    itemsPerPage: 'Nombre de résultats par page',
+    autocomplete_research: 'Aucun résultat ne correspond à votre recherche',
+    add: 'Ajouter',
     save: 'Enregistrer',
+    save_and_back: 'Enregistrer et retour',
     back: 'Retour',
     cancel: 'Annuler',
     delete: 'Supprimer',

+ 3 - 1
lang/fr-FR.js

@@ -5,6 +5,7 @@ import rulesAndErrors from '@/lang/rulesAndErrors/fr-FR'
 import form from '@/lang/form/fr-FR'
 import breadcrumbs from '@/lang/breadcrumbs/fr-FR'
 import menuKey from '@/lang/menuKey/fr-FR'
+import contentSubscription from '@/lang/content/subscription/fr-FR'
 
 export default (context, locale) => {
   return {
@@ -14,6 +15,7 @@ export default (context, locale) => {
     ...rulesAndErrors(context, locale),
     ...form(context, locale),
     ...breadcrumbs(context, locale),
-    ...menuKey(context, locale)
+    ...menuKey(context, locale),
+    ...contentSubscription(context, locale)
   }
 }

+ 41 - 0
lang/layout/fr-FR.js

@@ -1,5 +1,46 @@
 export default (context, locale) => {
   return ({
+    modif_picture:'Modifier l\'image',
+    image_assistant:'Assistant de téléchargement',
+    delete_assistant:'Assistant de suppression',
+    creative_assistant:'Assistant de création',
+    what_do_you_want_to_create:'Que souhaitez-vous créer ?',
+    previous:'Précédent',
+    cancel:'Annuler',
+    add_any_type_material:'Ajoutez tout type de matériel ou de documents tels que des partitions à votre parc de matériel',
+    a_materiel:'Un matériel',
+    sen_email_letter:'Envoyez un email, un courrier, ou un SMS aux personnes de votre carnet d\'adresses',
+    a_correspondence:'Une Correspondance',
+    add_an_event_course:'Ajoutez un évenement, un cours, une prestation pédagogique, un examen... à votre planning',
+    an_event:'Un évènement',
+    add_new_person_student:'Ajoutez un nouveau membre parent, élève, professeur, personnel... à votre répertoire',
+    a_person:'une personne',
+    other_event_text_creation_card:'Comprend entre autres: auditions, concerts, répétitions, spectacles, stages...',
+    educational_services_text_creation_card:'Correspond aux interventions en milieu scolaire, pénitentiaire, ou hospitalier',
+    exam_text_creation_card:'Permet d\'organiser des examens avec la gestion des jurys, des convocations et des résultats',
+    course_text_creation_card:'On associe les élèves à leurs enseignements, puis à leurs cours, qui peut être périodique ou ponctuel',
+    other_event:'Autre événement',
+    educational_services:'Prestations pédagogiques',
+    exam:'Examen',
+    course:'Cours',
+    sms_text_creation_card: 'Les SMS sont disponible sur option, vous devez disposer de suffisament de crédit',
+    letter_text_creation_card: 'Un courrier est imprimé pour être envoyé par la Poste mais peut aussi être envoyé par mail',
+    email_text_creation_card: 'Les emails peuvent également être des newsletters / lettre d\'information',
+    an_sms: 'Un sms',
+    a_letter: 'Un courrier',
+    an_email: 'Un email',
+    another_type_of_contact: 'Un autre type de contact',
+    a_legal_entity: 'Une personne morale',
+    a_member_of_staff: 'Un membre du personnel',
+    a_teacher: 'Un professeur',
+    a_guardian: 'Un tuteur',
+    a_student: 'Un élève',
+    other_contact_text_creation_card: 'Ajoutez un autre type de contact qui n\'a pas été défini précédemment',
+    moral_text_creation_card: 'Ajoutez les structures qui vous soutiennent ou avec qui vous travaillez',
+    personnel_text_creation_card: 'Ajoutez un membre à votre personnel et donnez-lui un accès administratif',
+    teacher_text_creation_card: 'Ajoutez un professeur à votre personnel et donnez-lui un accès pédagogique',
+    student_text_creation_card: 'Inscrivez un nouvel élève via le formulaire de la vue famille',
+    guardian_text_creation_card: 'Ajoutez un tuteur à votre carnet d\'adresses afin de l\'associer ultérieurement à un élève',
     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',

+ 9 - 1
lang/rulesAndErrors/fr-FR.js

@@ -1,9 +1,17 @@
 export default (context, locale) => {
   return ({
+    invalide_form: 'Formulaire invalide',
     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',
     email_error: 'Adresse email invalide',
-    phone_error: 'Numéro de téléphone invalide'
+    phone_error: 'Numéro de téléphone invalide',
+    ADDRESS_PRACTICE_non_unique: 'Vous ne pouvez pas avoir 2 adresses de pratique',
+    ADDRESS_HEAD_OFFICE_non_unique: 'Vous ne pouvez pas avoir 2 adresses de siège social',
+    ADDRESS_CONTACT_non_unique: 'Vous ne pouvez pas avoir 2 adresses de contact',
+    ADDRESS_BILL_non_unique: 'Vous ne pouvez pas avoir 2 adresses de facturation',
+    PRINCIPAL_non_unique: 'Vous ne pouvez pas avoir 2 points de contact principaux',
+    BILL_non_unique: 'Vous ne pouvez pas avoir 2 points de contact de facturation',
+    CONTACT_non_unique: 'Vous ne pouvez pas avoir 2 points de contact',
   })
 }

+ 1 - 1
layouts/default.vue

@@ -24,7 +24,7 @@
 
 <script lang="ts">
 import { computed, ComputedRef, defineComponent, reactive, useContext } from '@nuxtjs/composition-api'
-import { $useMenu } from '@/use/layout/menu'
+import { $useMenu } from '@/composables/layout/menu'
 
 export default defineComponent({
   name: 'DefaultLayout',

+ 1 - 1
models/Access/MyProfile.ts

@@ -5,7 +5,7 @@ export class MyProfile extends Model {
   static entity = 'accesses'
 
   @Uid()
-  id!: number | null
+  id!: number | string | null
 
   @Num(0, { nullable: true })
   activityYear!: number

+ 3 - 3
models/Access/PersonalizedList.ts

@@ -4,12 +4,12 @@ export class PersonalizedList extends Model {
   static entity = 'personalized_lists'
 
   @Uid()
-  id!: number | null
+  id!: number | string | null
 
-  @Str('')
+  @Str(null, { nullable: true })
   label!:string|null
 
-  @Str('')
+  @Str(null, { nullable: true })
   entity!:string|null
 
   @Str('')

+ 22 - 17
models/Core/AddressPostal.ts

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

+ 21 - 12
models/Core/BankAccount.ts

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

+ 21 - 18
models/Core/ContactPoint.ts

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

+ 1 - 1
models/Core/Country.ts

@@ -4,7 +4,7 @@ export class Country extends Model {
   static entity = 'countries'
 
   @Uid()
-  id!: number | null
+  id!: number | string | null
 
   @Str('')
   name!: string

+ 26 - 0
models/Core/File.ts

@@ -0,0 +1,26 @@
+import {Str, Model, Uid, Num} from '@vuex-orm/core'
+
+export class File extends Model {
+  static entity = 'files'
+
+  @Uid()
+  id!: number | string | null
+
+  @Str('')
+  name!: string
+
+  @Str('')
+  imgFieldName!: string
+
+  @Str('')
+  visibility!: string
+
+  @Str('')
+  config!: string
+
+  @Str('')
+  folder!: string
+
+  @Num(null, { nullable: true })
+  ownerId!: number
+}

+ 6 - 6
models/Core/Notification.ts

@@ -5,19 +5,19 @@ export class Notification extends Model {
   static entity = 'notifications'
 
   @Uid()
-  id!: number | null
+  id!: number | string | null
 
-  @Str('', { nullable: true })
+  @Str('')
   name!: string
 
   @HasOne(() => NotificationMessage, 'id')
   message!: NotificationMessage | null
 
-  @Str('', { nullable: true })
-  type!: string
+  @Str(null, { nullable: true })
+  type!: string|null
 
-  @Str('', { nullable: true })
-  link!: string
+  @Str(null, { nullable: true })
+  link!: string|null
 
   @Attr({})
   notificationUsers!: Array<string>

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