Преглед изворни кода

Merge branch 'address-contact-point-pr' into 'develop'

Pages des points de contacts et address postales en Edit pour la fiche de l'organisation

See merge request opentalent/admin!4
Guffon пре 4 година
родитељ
комит
013cd3d66b
49 измењених фајлова са 1229 додато и 681 уклоњено
  1. 3 0
      assets/css/global.scss
  2. 1 1
      components/Layout/AlertBar/Cotisation.vue
  3. 3 3
      components/Layout/Header/Notification.vue
  4. 1 1
      components/Layout/Loading.vue
  5. 0 2
      components/Ui/DataTable.vue
  6. 2 1
      components/Ui/Form.vue
  7. 64 16
      components/Ui/Input/Autocomplete.vue
  8. 38 52
      components/Ui/Input/AutocompleteWithAPI.vue
  9. 8 3
      components/Ui/Input/Email.vue
  10. 31 23
      components/Ui/Input/Phone.vue
  11. 72 0
      components/Ui/ItemFromUri.vue
  12. 16 20
      components/Ui/Map.vue
  13. 7 3
      components/Ui/SubList.vue
  14. 3 3
      config/abilities/pages/communication.yaml
  15. 2 1
      config/nuxtConfig/env.js
  16. 2 0
      lang/field/fr-FR.js
  17. 1 0
      lang/form/fr-FR.js
  18. 9 4
      models/Core/AddressPostal.ts
  19. 1 1
      models/Core/Country.ts
  20. 1 1
      models/Organization/OrganizationAddressPostal.ts
  21. 1 1
      package.json
  22. 12 1
      pages/organization.vue
  23. 150 39
      pages/organization/address/_id.vue
  24. 6 1
      pages/organization/contact_points/_id.vue
  25. 26 14
      pages/organization/index.vue
  26. 2 2
      pages/subscription.vue
  27. 12 5
      plugins/Data/axios.js
  28. 25 15
      services/connection/connection.ts
  29. 2 14
      services/connection/urlBuilder.ts
  30. 6 0
      services/connection/urlOptionsBuilder.ts
  31. 1 4
      services/rights/abilitiesUtils.ts
  32. 11 2
      services/serializer/normalizer/model.ts
  33. 3 2
      services/store/repository.ts
  34. 1 1
      services/utils/i18n.ts
  35. 17 0
      services/utils/modelsUtils.ts
  36. 1 1
      store/index.js
  37. 2 1
      tests/unit/services/connection/connection.spec.ts
  38. 1 1
      tests/unit/services/connection/urlBuilder.spec.ts
  39. 7 1
      tests/unit/services/connection/urlOptionsBuilder.spec.ts
  40. 48 2
      tests/unit/services/serializer/normalizer/model.spec.ts
  41. 1 1
      tests/unit/services/store/repository.spec.ts
  42. 15 0
      tests/unit/services/utils/modelsUtils.spec.ts
  43. 13 5
      types/interfaces.d.ts
  44. 41 0
      use/data/useAccess.ts
  45. 75 0
      use/data/useAddresspostal.ts
  46. 42 0
      use/data/useCountry.ts
  47. 1 1
      use/form/useValidator.ts
  48. 7 4
      use/layout/Menus/websiteMenu.ts
  49. 435 428
      yarn.lock

+ 3 - 0
assets/css/global.scss

@@ -28,4 +28,7 @@ header .v-toolbar__content{
   overflow-y: scroll
 }
 
+.v-menu__content{
+  z-index: 400 !important;
+}
 

+ 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)

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

@@ -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
@@ -237,7 +237,7 @@ export default defineComponent({
       loading,
       notifications,
       update,
-      unreadNotification,
+      unreadNotification: () => unreadNotification,
       isOpen,
       download
     }

+ 1 - 1
components/Layout/Loading.vue

@@ -43,6 +43,6 @@ export default defineComponent({
     left: 0;
     width: 100%;
     height: 100%;
-    z-index: 100!important;
+    z-index: 1001!important;
   }
 </style>

+ 0 - 2
components/Ui/DataTable.vue

@@ -102,8 +102,6 @@ export default defineComponent({
       itemId.value = item.id
     }
 
-    // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
-
     return {
       entries,
       totalEntries,

+ 2 - 1
components/Ui/Form.vue

@@ -118,7 +118,8 @@ export default defineComponent({
         await $dataPersister.invoke({
           type: QUERY_TYPE.MODEL,
           model: props.model,
-          id: id.value
+          id: id.value,
+          query: props.query
         })
 
         const alert:alert = {

+ 64 - 16
components/Ui/Input/Autocomplete.vue

@@ -7,21 +7,34 @@ Liste déroulante avec autocompletion
 <template>
   <main>
     <v-autocomplete
+      autocomplete="search"
+      :value="data"
       :items="itemsToDisplayed"
       :label="$t(label)"
-      item-text="textDisplay"
+      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"
+      @input="$emit('update', $event, field)"
+    >
+      <template v-if="slotText" v-slot: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 } from '@nuxtjs/composition-api'
 import { AnyJson } from '~/types/interfaces'
+import * as _ from 'lodash'
+import {$objectProperties} from "~/services/utils/objectProperties";
 
 export default defineComponent({
   props: {
@@ -36,7 +49,7 @@ export default defineComponent({
       default: null
     },
     data: {
-      type: String,
+      type: [String, Number, Object],
       required: false,
       default: null
     },
@@ -57,6 +70,11 @@ export default defineComponent({
       type: Array,
       required: true
     },
+    slotText: {
+      type: Array,
+      required: false,
+      default: null
+    },
     returnObject: {
       type: Boolean,
       default: false
@@ -68,28 +86,58 @@ export default defineComponent({
     isLoading: {
       type: Boolean,
       default: false
-    }
+    },
+    noFilter: {
+      type: Boolean,
+      default: false
+    },
+    prependIcon: {
+      type: String
+    },
   },
-  setup (props) {
+  setup (props, { emit }) {
+    const search:Ref<string|null> = ref(null)
+
     // On reconstruit les items à afficher car le text de l'Item doit être construit par rapport au itemText passé en props
     const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
       return props.items.map((item: any) => {
-        const textDisplay: Array<string> = []
-        for (const text of props.itemText) {
-          textDisplay.push(item[text as string])
+        const slotTextDisplay: Array<string> = []
+        const itemTextDisplay: Array<string> = []
+
+        if(item){
+          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(item[text as string])
+            }
+          }
+
+          for (const text of props.itemText) {
+            itemTextDisplay.push(item[text as string])
+          }
         }
-        return Object.assign({}, item, { textDisplay: textDisplay.join(' ') })
+
+        //On reconstruit l'objet
+        return Object.assign({}, item, { itemTextDisplay: itemTextDisplay.join(' '), slotTextDisplay: slotTextDisplay.join(' ') })
       })
     })
 
+    const unwatch = watch(search, _.debounce(async (newResearch, oldResearch) => {
+      if(newResearch !== oldResearch && oldResearch !== null)
+        emit('research', newResearch)
+    }, 500))
+
+    onUnmounted(() => {
+      unwatch()
+    })
+
     return {
       label_field: props.label ?? props.field,
-      itemsToDisplayed
+      itemsToDisplayed,
+      search
     }
   }
 })
 </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],
       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,(data) => {
+      items.value = [data]
+    })
 
     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>

+ 8 - 3
components/Ui/Input/Email.vue

@@ -10,7 +10,7 @@ Champs de saisie de type Text dédié à la saisie d'emails
     :error="error"
     :error-message="errorMessage"
     :rules="rules"
-    @change="$emit('update', $event, field)"
+    @update="update"
   />
 </template>
 
@@ -54,7 +54,7 @@ export default defineComponent({
       default: null
     }
   },
-  setup (props) {
+  setup (props, {emit}) {
     const { app: { i18n } } = useContext()
 
     const rules = [
@@ -67,8 +67,13 @@ export default defineComponent({
       )
     }
 
+    const update = (value: string) =>{
+      emit('update', value, props.field)
+    }
+
     return {
-      rules
+      rules,
+      update
     }
   }
 })

+ 31 - 23
components/Ui/Input/Phone.vue

@@ -9,13 +9,12 @@ Champs de saisie d'un numéro de téléphone
     <vue-tel-input-vuetify
       :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"
     />
@@ -23,7 +22,7 @@ Champs de saisie d'un numéro de téléphone
 </template>
 
 <script lang="ts">
-import { defineComponent, Ref, ref, useContext } from '@nuxtjs/composition-api'
+import { defineComponent, Ref, ref, useContext, computed } from '@nuxtjs/composition-api'
 
 export default defineComponent({
   props: {
@@ -56,37 +55,46 @@ export default defineComponent({
       default: null
     }
   },
-  setup () {
+  setup (props, {emit}) {
     const { app: { i18n } } = useContext()
 
     const nationalNumber: Ref<string | number> = ref('')
     const internationalNumber: Ref<string | number> = ref('')
     const isValid: Ref<boolean> = ref(false)
-    const country: Ref<string> = ref('')
+    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 onChange = () => {
+      if (isValid.value) {
+        emit('update', internationalNumber.value, props.field)
+      }
+    }
+
+    const myPhone = computed(
+      {
+        get:()=>{
+          return onInit.value ? props.data : nationalNumber.value
+        },
+        set:(value)=>{
+          return props.data
+        }
+      }
+    )
 
     return {
-      nationalNumber,
-      internationalNumber,
-      isValid,
-      country,
+      myPhone,
+      onInput,
+      onChange,
       rules: [
         () => isValid.value || i18n.t('phone_error')
       ]
     }
-  },
-  methods: {
-    onInput (_: any, { number, valid, country }: { number: any, valid: boolean, country: any }) {
-      this.isValid = valid
-      this.nationalNumber = number.national
-      this.internationalNumber = number.international
-      this.country = country && country.name
-      // console.log(this.field, this.isValid, this.nationalNumber, this.internationalNumber, this.country)
-    },
-    onChange () {
-      if (this.isValid) {
-        this.$emit('update', this.internationalNumber, this.field)
-      }
-    }
   }
 })
 

+ 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>

+ 16 - 20
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>
@@ -45,24 +45,26 @@ export default defineComponent({
   setup (props, { emit }) {
     const { $dataProvider, store } = useContext()
     const { address }: ToRefs = toRefs(props)
-    const latitude: Ref<number> = ref(address.value.latitude)
-    const longitude: Ref<number> = ref(address.value.longitude)
+    const 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 = {
@@ -74,17 +76,11 @@ export default defineComponent({
     }
 
     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 {

+ 7 - 3
components/Ui/SubList.vue

@@ -4,7 +4,7 @@
   <main>
     <v-skeleton-loader
       v-if="$fetchState.pending"
-      type="text"
+      :type="loaderType"
     />
     <div v-else>
       <slot name="list.item" v-bind="{items}" />
@@ -39,7 +39,12 @@ export default defineComponent({
     query: {
       type: Object as () => Query,
       required: true
-    }
+    },
+    loaderType: {
+      type: String,
+      required: false,
+      default: 'text'
+    },
   },
   setup (props) {
     const { rootModel, rootId, model, query }: ToRefs = toRefs(props)
@@ -54,7 +59,6 @@ export default defineComponent({
     })
 
     const items: ComputedRef<Collection> = computed(() => queryHelper.getCollection(query.value))
-    // onUnmounted( useRepositoryHelper.cleanRepository(repository.value) )
 
     return {
       items

+ 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: {

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

@@ -1,5 +1,7 @@
 export default (context, locale) => {
   return ({
+    importAddress: 'Importer l\'adresse d\'une des personnes de votre structure',
+    addressCountry: 'Pays',
     legalInformation: 'Informations légales',
     agrements: 'Agréments',
     salary: 'Salariés',

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

@@ -1,5 +1,6 @@
 export default (context, locale) => {
   return ({
+    autocomplete_research: 'Aucun résultat ne correspond à votre recherche',
     save: 'Enregistrer',
     back: 'Retour',
     cancel: 'Annuler',

+ 9 - 4
models/Core/AddressPostal.ts

@@ -1,5 +1,4 @@
-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'
@@ -7,8 +6,14 @@ export class AddressPostal extends Model {
   @Uid()
   id!: number | null
 
-  @HasOne(() => Country, 'id')
-  addressCountry!: Country | null
+  @Str('', { nullable: true })
+  '@id'!: string
+
+  @Attr(null)
+  organizationAddressPostalId!: number | null
+
+  @Str('', { nullable: true })
+  addressCountry!: string
 
   @Str('', { nullable: true })
   addressCity!: string

+ 1 - 1
models/Core/Country.ts

@@ -1,4 +1,4 @@
-import {Str, Model, Uid} from '@vuex-orm/core'
+import {Str, Model, Uid, Attr} from '@vuex-orm/core'
 
 export class Country extends Model {
   static entity = 'countries'

+ 1 - 1
models/Organization/OrganizationAddressPostal.ts

@@ -7,7 +7,7 @@ export class OrganizationAddressPostal extends Model {
   @Uid()
   id!: number | null
 
-  @HasOne(() => AddressPostal, 'id')
+  @HasOne(() => AddressPostal, 'organizationAddressPostalId')
   addressPostal!: AddressPostal | null
 
   @Str('PRINCIPAL', { nullable: false })

+ 1 - 1
package.json

@@ -33,7 +33,7 @@
     "@nuxtjs/i18n": "^7.0",
     "@nuxtjs/vuetify": "^1.12.1",
     "@types/lodash": "^4.14",
-    "@vuex-orm/core": "1.0.0-draft.14",
+    "@vuex-orm/core": "1.0.0-draft.16",
     "cookieparser": "^0.1",
     "core-js": "^3.17",
     "js-yaml": "^4.0",

+ 12 - 1
pages/organization.vue

@@ -27,6 +27,10 @@ import { QUERY_TYPE } from '~/types/enums'
 import { Organization } from '~/models/Organization/Organization'
 import { queryHelper } from '~/services/store/query'
 import { repositoryHelper } from '~/services/store/repository'
+import {ContactPoint} from "~/models/Core/ContactPoint";
+import {BankAccount} from "~/models/Core/BankAccount";
+import {OrganizationAddressPostal} from "~/models/Organization/OrganizationAddressPostal";
+import {Country} from "~/models/Core/Country";
 
 export default defineComponent({
   name: 'Organization',
@@ -49,7 +53,14 @@ export default defineComponent({
       model: Organization,
       id: store.state.profile.organization.id
     })
-  }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Organization)
+    repositoryHelper.cleanRepository(ContactPoint)
+    repositoryHelper.cleanRepository(BankAccount)
+    repositoryHelper.cleanRepository(OrganizationAddressPostal)
+    repositoryHelper.cleanRepository(Country)
+  },
 })
 
 </script>

+ 150 - 39
pages/organization/address/_id.vue

@@ -1,9 +1,8 @@
 <!-- Page de détails d'une adresse postale -->
-
 <template>
   <main>
     <v-skeleton-loader
-      v-if="loading"
+      v-if="organizationAddressPostalFetching.pending"
       type="text"
     />
     <LayoutContainer v-else>
@@ -18,23 +17,24 @@
         </v-toolbar>
 
         <UiForm :id="id" :model="model" :query="query()">
-          <template v-slot:form.input="{entry, updateRepository}">
-            <v-skeleton-loader
-              v-if="loading"
-              type="text"
-            />
-            <v-container v-else fluid class="container">
+          <template #form.input="{entry, updateRepository}">
+
+            <v-container fluid class="container">
               <v-row>
                 <v-col cols="12" sm="12">
-                  <UiInputAutocomplete
+                  <UiInputAutocompleteWithAPI
                     field="owner"
-                    label="addressOwner"
-                    item-value="@id"
-                    :item-text="['cp', 'city']"
-                    @update="updateAddressFromOwner"
+                    label="importAddress"
+                    :item-text="['person.givenName', 'person.name']"
+                    :searchFunction="accessSearch"
+                    @update="updateAccessAddress"
                   />
                 </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">
                   <UiInputEnum field="type" label="addresstype" :data="entry['type']" enum-type="address_postal_organization" @update="updateRepository" />
                 </v-col>
@@ -42,31 +42,68 @@
                 <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">
-                  <UiInputText field="addressPostal.postalCode" label="postalCode" :data="entry['addressPostal.postalCode']" @update="updateRepository" />
+                  <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">
-                  <UiInputText field="addressPostal.addressCity" label="addressCity" :data="entry['addressPostal.addressCity']" @update="updateRepository" />
+                  <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="addressItem" @updateAddress="updateAddress" />
+            <UiMap :zoom="12" :address="addressPostalItem()" @updateAddress="updateAddressFromMap" />
+
           </template>
 
-          <template v-slot:form.button>
+          <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>
@@ -74,45 +111,66 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, useContext, useFetch, ref, computed, Ref, ComputedRef } from '@nuxtjs/composition-api'
+import { defineComponent, useContext, useFetch, computed, ComputedRef } from '@nuxtjs/composition-api'
 import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
 import { Model, Query } from '@vuex-orm/core'
 import { OrganizationAddressPostal } from '~/models/Organization/OrganizationAddressPostal'
+import {Country} from "~/models/Core/Country";
 import { QUERY_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 "~/use/data/useCountry";
+import ModelsUtils from "~/services/utils/modelsUtils";
+import {UseAddressPostal} from "~/use/data/useAddresspostal";
+import {AnyJson} from "~/types/interfaces";
+import DataProvider from "~/services/data/dataProvider";
+import {UseAccess} from "~/use/data/useAccess";
 
 export default defineComponent({
   name: 'OrganizationAddress',
   setup () {
     const { route, $dataProvider } = useContext()
-    const loading: Ref<boolean> = ref(true)
     const id: number = parseInt(route.value.params.id)
 
     const repository: VuexRepository<Model> = repositoryHelper.getRepository(OrganizationAddressPostal)
-    const query: Query = repository.with('addressPostal', (query) => {
-      query.with('addressCountry')
-    })
-
-    useFetch(async () => {
+    const query: Query = repository.with('addressPostal')
+    const {fetchState: organizationAddressPostalFetching} = useFetch(async () => {
       await $dataProvider.invoke({
         type: QUERY_TYPE.MODEL,
         model: OrganizationAddressPostal,
         id
       })
-      loading.value = false
     })
 
-    const addressItem: ComputedRef<AddressPostal|null> = computed(() => {
-      const organizationAddressPostal = queryHelper.getFirstItem(query) as OrganizationAddressPostal
-      return organizationAddressPostal.addressPostal
+    const organizationAddressPostalItem: ComputedRef<OrganizationAddressPostal> = computed(() => {
+      return queryHelper.getFirstItem(query) as OrganizationAddressPostal
+    })
+
+    const addressPostalItem: ComputedRef<AddressPostal|null> = computed(() => {
+      return organizationAddressPostalItem.value?.addressPostal || null
     })
 
-    const updateAddress = (address: AddressPostal) => {
-      const organizationAddressPostal = queryHelper.getFirstItem(query) as OrganizationAddressPostal
-      organizationAddressPostal.addressPostal = address
-      repositoryHelper.persist(OrganizationAddressPostal, organizationAddressPostal)
+    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)
     }
 
     /** Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
@@ -120,17 +178,70 @@ export default defineComponent({
       model: OrganizationAddressPostal,
       query: () => query,
       id,
-      loading,
+      organizationAddressPostalFetching,
       panel: 0,
-      addressItem,
-      updateAddress,
-      updateAddressFromOwner: (data: any) => {
-        console.log(data)
-      }
+      addressPostalItem: () => addressPostalItem,
+      countries,
+      countriesFetchingState,
+      addressSearch,
+      getIdFromUri,
+      getAutoCompleteAddressItem,
+      accessSearch,
+      updateAccessAddress,
+      updateAddressFromMap,
+      updateCpAddress
     }
-  }
+  },
+  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>
+
 <style>
   .toolbarForm .v-toolbar__content{
     padding-left: 0 !important;

+ 6 - 1
pages/organization/contact_points/_id.vue

@@ -90,6 +90,8 @@ import { Model, Query } from '@vuex-orm/core'
 import { QUERY_TYPE } from '~/types/enums'
 import { repositoryHelper } from '~/services/store/repository'
 import { ContactPoint } from '~/models/Core/ContactPoint'
+import {OrganizationAddressPostal} from "~/models/Organization/OrganizationAddressPostal";
+import {Country} from "~/models/Core/Country";
 
 export default defineComponent({
   name: 'ContactPoint',
@@ -116,7 +118,10 @@ export default defineComponent({
       id,
       loading
     }
-  }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(ContactPoint)
+  },
 })
 </script>
 <style>

+ 26 - 14
pages/organization/index.vue

@@ -72,12 +72,11 @@ Contient toutes les informations sur l'organization courante
               <v-row>
                 <v-col cols="12" sm="12">
                   <UiSubList
-                    :query="repositories.addressRepository.with('addressPostal', (query) => {
-                      query.with('addressCountry')
-                    })"
+                    :query="repositories.addressRepository.with('addressPostal')"
                     :root-model="models.Organization"
                     :root-id="id"
                     :model="models.OrganizationAddressPostal"
+                    loaderType="image"
                   >
                     <template #list.item="{items}">
                       <v-container fluid>
@@ -100,7 +99,17 @@ Contient toutes les informations sur l'organization courante
                                 <span v-if="item.addressPostal.streetAddressSecond">{{ item.addressPostal.streetAddressSecond }} <br></span>
                                 <span v-if="item.addressPostal.streetAddressThird">{{ item.addressPostal.streetAddressThird }} <br></span>
                                 {{ item.addressPostal.postalCode }} {{ item.addressPostal.addressCity }}<br>
-                                <span v-if="item.addressPostal.addressCountry">{{ item.addressPostal.addressCountry.name }}</span>
+                                <span v-if="item.addressPostal.addressCountry">
+                                  <UiItemFromUri
+                                    :model="models.Country"
+                                    :query="repositories.countryRepository.query()"
+                                    :uri="item.addressPostal.addressCountry"
+                                  >
+                                    <template #item.text="{item}">
+                                      {{item.name}}
+                                    </template>
+                                  </UiItemFromUri>
+                                </span>
                               </template>
                             </UiCard>
                           </v-col>
@@ -123,6 +132,7 @@ Contient toutes les informations sur l'organization courante
                     :root-model="models.Organization"
                     :root-id="id"
                     :model="models.ContactPoint"
+                    loaderType="image"
                   >
                     <template #list.item="{items}">
                       <v-container fluid>
@@ -145,10 +155,10 @@ Contient toutes les informations sur l'organization courante
                                 <span v-if="item.emailInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('emailInvalid') }}</strong> : {{ item.emailInvalid }} <br></span>
 
                                 <span v-if="item.telphone"><strong>{{ $t('telphone') }}</strong> : {{ formatPhoneNumber(item.telphone) }} <br></span>
-                                <span v-if="item.telphoneInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('telphoneInvalid') }}</strong> : {{ item.telphoneInvalid }} <br></span>
+                                <span v-if="item.telphoneInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('telphoneInvalid') }}</strong> : {{ formatPhoneNumber(item.telphoneInvalid) }} <br></span>
 
-                                <span v-if="item.mobilPhone"><strong>{{ $t('mobilPhone') }}</strong> : {{ item.mobilPhone }} <br></span>
-                                <span v-if="item.mobilPhoneInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('mobilPhoneInvalid') }}</strong> : {{ item.mobilPhoneInvalid }} </span>
+                                <span v-if="item.mobilPhone"><strong>{{ $t('mobilPhone') }}</strong> : {{ formatPhoneNumber(item.mobilPhone) }} <br></span>
+                                <span v-if="item.mobilPhoneInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('mobilPhoneInvalid') }}</strong> : {{ formatPhoneNumber(item.mobilPhoneInvalid) }} </span>
                               </template>
                             </UiCard>
                           </v-col>
@@ -324,6 +334,7 @@ import { repositoryHelper } from '~/services/store/repository'
 import UseValidator from '~/use/form/useValidator'
 import { UseNavigationHelpers } from '~/use/form/useNavigationHelpers'
 import I18N from '~/services/utils/i18n'
+import {Country} from "~/models/Core/Country";
 
 export default defineComponent({
   name: 'OrganizationParent',
@@ -343,23 +354,23 @@ export default defineComponent({
 
     const { panel } = UseNavigationHelpers.expansionPanels()
 
+    const formatPhoneNumber = (number: string): string => {
+      return I18N.formatPhoneNumber(number)
+    }
+
     return {
       repositories,
       id,
       organizationProfile,
-      models: { Organization, ContactPoint, BankAccount, OrganizationAddressPostal },
+      models: { Organization, ContactPoint, BankAccount, OrganizationAddressPostal, Country },
       datatableHeaders: getDataTablesHeaders(i18n),
       rules: getRules(i18n),
       siretError,
       siretErrorMessage,
       checkSiretHook,
+      formatPhoneNumber,
       panel
     }
-  },
-  methods: {
-    formatPhoneNumber (number: string): string {
-      return I18N.formatPhoneNumber(number)
-    }
   }
 })
 
@@ -402,7 +413,8 @@ function getRepositories () {
     organizationRepository: repositoryHelper.getRepository(Organization),
     contactPointRepository: repositoryHelper.getRepository(ContactPoint),
     bankAccountRepository: repositoryHelper.getRepository(BankAccount),
-    addressRepository: repositoryHelper.getRepository(OrganizationAddressPostal)
+    addressRepository: repositoryHelper.getRepository(OrganizationAddressPostal),
+    countryRepository: repositoryHelper.getRepository(Country)
   }
 }
 </script>

+ 2 - 2
pages/subscription.vue

@@ -354,7 +354,7 @@ export default defineComponent({
       try {
         const reponse:ApiResponse = await $dataProvider.invoke({
           type: QUERY_TYPE.DEFAULT,
-          url: 'dolibarr/account/' + id
+          url: '/api/dolibarr/account/' + id
         })
         dolibarrAccount.value = reponse.data as DolibarrAccount
       } catch (Error) {
@@ -368,7 +368,7 @@ export default defineComponent({
       try {
         const reponse:ApiResponse = await $dataProvider.invoke({
           type: QUERY_TYPE.DEFAULT,
-          url: 'mobyt/status/' + id
+          url: '/api/mobyt/status/' + id
         })
         mobytStatus.value = reponse.data as MobytUserStatus
       } catch (Error) {

+ 12 - 5
plugins/Data/axios.js

@@ -1,12 +1,19 @@
 export default function ({ $axios, redirect, store }) {
   $axios.onRequest(config => {
-    $axios.setHeader('x-accessid', `${store.state.profile.access.id}`)
+    if(!config.params.noXaccessId){
+      $axios.setHeader('x-accessid', `${store.state.profile.access.id}`)
 
-    if (store.state.profile.access.switchId) {
-      $axios.setHeader('x-switch-user', `${store.state.profile.access.switchId}`)
-    }
+      if (store.state.profile.access.switchId) {
+        $axios.setHeader('x-switch-user', `${store.state.profile.access.switchId}`)
+      }
 
-    $axios.setToken(`${store.state.profile.access.bearer}`, 'Bearer')
+      $axios.setToken(`${store.state.profile.access.bearer}`, 'Bearer')
+    }else{
+      delete config.headers.common['Authorization']
+      delete config.headers.common['x-accessid']
+      $axios.setHeader('x-accessid', false)
+      $axios.setToken(false)
+    }
   })
 
   $axios.onResponse(response => {

+ 25 - 15
services/connection/connection.ts

@@ -32,9 +32,9 @@ class Connection {
     switch (method) {
       case HTTP_METHOD.GET:
         if (args.id) {
-          return Connection.getItem(url, args.id, args.showProgress)
+          return Connection.getItem(url, args.id, args.showProgress, args.params)
         } else {
-          return Connection.getCollection(url, args.showProgress, args.type)
+          return Connection.getCollection(url, args.type, args.showProgress, args.params)
         }
 
       case HTTP_METHOD.PUT:
@@ -45,11 +45,11 @@ class Connection {
         if (!args.data) {
           throw new Error('*args* has no data')
         }
-        return method === HTTP_METHOD.PUT ? Connection.put(url, args.id, args.data, args.showProgress) :
-                                            Connection.post(url, args.data, args.showProgress)
+        return method === HTTP_METHOD.PUT ? Connection.put(url, args.id, args.data, args.showProgress, args.params) :
+                                            Connection.post(url, args.data, args.showProgress, args.params)
 
       case HTTP_METHOD.DELETE:
-        return Connection.deleteItem(url, args.id, args.showProgress)
+        return Connection.deleteItem(url, args.id, args.showProgress, args.params)
     }
 
     throw new Error('Unknown connection method was invoked')
@@ -60,13 +60,15 @@ class Connection {
    * @param {string} url
    * @param {number} id
    * @param {boolean} showProgress
+   * @param {AnyJson} params
    * @return {Promise<any>}
    */
-  public static getItem (url: string, id: number, showProgress: boolean = true): Promise<any> {
+  public static getItem (url: string, id: number, showProgress: boolean = true, params: AnyJson = {}): Promise<any> {
     const config: AxiosRequestConfig = {
       url: `${url}/${id}`,
       method: HTTP_METHOD.GET,
-      progress: showProgress
+      progress: showProgress,
+      params: params
     }
     return Connection.request(config)
   }
@@ -76,13 +78,15 @@ class Connection {
    * @param {string} url
    * @param {boolean} progress
    * @param {QUERY_TYPE} type
+   * @param {AnyJson} params
    * @return {Promise<any>}
    */
-  public static getCollection (url: string, progress: boolean = true, type: QUERY_TYPE): Promise<any> {
+  public static getCollection (url: string, type: QUERY_TYPE, progress: boolean = true, params: AnyJson = {}): Promise<any> {
     let config: AxiosRequestConfig = {
       url: `${url}`,
       method: HTTP_METHOD.GET,
-      progress
+      progress,
+      params: params
     }
     if(type === QUERY_TYPE.IMAGE)
       config = {...config, responseType: 'blob'}
@@ -95,14 +99,16 @@ class Connection {
    * @param {string} url
    * @param {AnyJson} data
    * @param {boolean} progress
+   * @param {AnyJson} params
    * @return {Promise<any>}
    */
-  public static post (url: string, data: AnyJson, progress: boolean = true): Promise<any> {
+  public static post (url: string, data: AnyJson, progress: boolean = true, params: AnyJson = {}): Promise<any> {
     const config: AxiosRequestConfig = {
       url: `${url}`,
       method: HTTP_METHOD.POST,
       data,
-      progress
+      progress,
+      params: params
     }
     return Connection.request(config)
   }
@@ -113,14 +119,16 @@ class Connection {
    * @param {number} id
    * @param {AnyJson} data
    * @param {boolean} progress
+   * @param {AnyJson} params
    * @return {Promise<any>}
    */
-  public static put (url: string, id: number, data: AnyJson, progress: boolean = true): Promise<any> {
+  public static put (url: string, id: number, data: AnyJson, progress: boolean = true, params: AnyJson = {}): Promise<any> {
     const config: AxiosRequestConfig = {
       url: `${url}/${id}`,
       method: HTTP_METHOD.PUT,
       data,
-      progress
+      progress,
+      params: params
     }
     return Connection.request(config)
   }
@@ -130,13 +138,15 @@ class Connection {
    * @param {string} url
    * @param {number} id
    * @param {boolean} progress
+   * @param {AnyJson} params
    * @return {Promise<any>}
    */
-  public static deleteItem (url: string, id: number, progress: boolean = true): Promise<any> {
+  public static deleteItem (url: string, id: number, progress: boolean = true, params: AnyJson = {}): Promise<any> {
     const config: AxiosRequestConfig = {
       url: `${url}/${id}`,
       method: HTTP_METHOD.DELETE,
-      progress
+      progress,
+      params: params
     }
     return Connection.request(config)
   }

+ 2 - 14
services/connection/urlBuilder.ts

@@ -12,7 +12,7 @@ class UrlBuilder {
   static ROOT = '/api/'
 
   /**
-   * Main méthode qui appellera les méthode privées correspondantes (getDefaultUrl, getEnumUrl, getModelUrl, getImageUrl)
+   * Main méthode qui appellera les méthode privées correspondantes (getEnumUrl, getModelUrl, getImageUrl)
    * @param {UrlArgs} args
    * @return {string}
    */
@@ -20,7 +20,7 @@ class UrlBuilder {
     let url: string = ''
     switch (args.type) {
       case QUERY_TYPE.DEFAULT:
-        url = UrlBuilder.getDefaultUrl(args.url)
+        url = args.url
         break;
 
       case QUERY_TYPE.ENUM:
@@ -50,18 +50,6 @@ class UrlBuilder {
     return options.length > 0 ? `${url}?${UrlOptionsBuilder.build(args).join('&')}` : url
   }
 
-  /**
-   * Construction d'une URL "traditionnelle" qui ira concaténer l'url passée en paramètre avec la ROOT Url définie
-   * @param {string} url
-   * @return {string}
-   */
-  private static getDefaultUrl (url?: string): string {
-    if (typeof url === 'undefined') {
-      throw new TypeError('url must be defined')
-    }
-    return UrlBuilder.concat(UrlBuilder.ROOT, url)
-  }
-
   /**
    * Construction d'une URL Type Enum qui ira concaténer le type enum passé en paramètre avec la ROOT Url définie
    * @param {string} enumType

+ 6 - 0
services/connection/urlOptionsBuilder.ts

@@ -50,6 +50,12 @@ class UrlOptionsBuilder {
       options.push(`page=${listArgs.page}`)
     }
 
+    if (listArgs.filters) {
+      for(const filter of listArgs.filters){
+        options.push(`${filter.key}=${filter.value}`)
+      }
+    }
+
     return options
   }
 }

+ 1 - 4
services/rights/abilitiesUtils.ts

@@ -57,12 +57,9 @@ class AbilitiesUtils {
     this.$ability.update(this.$store.state.profile.access.abilities)
 
     // Au moment où l'on effectue un SetProfile via le store Organization, il faut aller récupérer
-    // les différentes habilitées que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
+    // les différentes abilitées que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
     const unsubscribe = this.$store.subscribeAction({
       after: (action, _state) => {
-        // On récupère les habilités
-        const abilities = this.getAbilities()
-
         switch (action.type) {
           case 'profile/organization/setProfile':
             //On récupère les abilités

+ 11 - 2
services/serializer/normalizer/model.ts

@@ -3,6 +3,8 @@ import BaseNormalizer from '~/services/serializer/normalizer/baseNormalizer'
 import {AnyJson, DataPersisterArgs} from '~/types/interfaces'
 import { QUERY_TYPE } from '~/types/enums'
 import { repositoryHelper } from '~/services/store/repository'
+import { Item } from '@vuex-orm/core'
+import {queryHelper} from "~/services/store/query";
 
 /**
  * @category Services/serializer/normalizer
@@ -24,7 +26,14 @@ class Model extends BaseNormalizer {
       throw new Error('*args* has no model attribute')
     }
 
-    const item = repositoryHelper.findItemFromModel(args.model, args.idTemp ? args.idTemp : args.id)
+    let item:Item = null
+    const itemId = args.idTemp ? args.idTemp : args.id
+
+    if(args.query){
+      item = queryHelper.getItem(args.query, itemId)
+    }else{
+      item = repositoryHelper.findItemFromModel(args.model, itemId)
+    }
 
     if (!item || typeof item === 'undefined') {
       throw new Error('Item not found')
@@ -50,7 +59,7 @@ class Model extends BaseNormalizer {
    * @param data
    */
   public static sanitizeBeforePost(data:AnyJson): AnyJson{
-    data.id = null
+    delete data.id
     return data
   }
 }

+ 3 - 2
services/store/repository.ts

@@ -125,9 +125,10 @@ class Repository {
   /**
    * Supprime tous les Items du repository
    *
-   * @param {VuexRepository} repository
+   * @param {Model} model
    */
-  public cleanRepository (repository: VuexRepository) {
+  public cleanRepository (model: typeof Model) {
+    const repository = this.getRepository(model)
     repository.flush()
   }
 }

+ 1 - 1
services/utils/i18n.ts

@@ -1,6 +1,6 @@
 import { parsePhoneNumber } from 'libphonenumber-js'
 
-export default class I18N {
+export default class UseI18N {
   static formatPhoneNumber (number: string): string {
     const parsed = parsePhoneNumber(number)
     return parsed ? parsed.formatNational() : ''

+ 17 - 0
services/utils/modelsUtils.ts

@@ -0,0 +1,17 @@
+export default class ModelsUtils {
+  /**
+   * Extrait l'ID de l'URI passée en paramètre
+   * @param uri
+   */
+  static extractIdFromUri (uri: string): number|null {
+    if(!uri) return null
+
+    const partUri: Array<string> = uri.split('/')
+    const id:any = partUri.pop()
+
+    if(isNaN(id))
+      throw new Error('id is not a number')
+
+    return parseInt(id)
+  }
+}

+ 1 - 1
store/index.js

@@ -48,7 +48,7 @@ export const actions = {
   async getUserProfile ({ dispatch }) {
     const myProfile = await this.app.context.$dataProvider.invoke({
       type: QUERY_TYPE.DEFAULT,
-      url: 'my_profile'
+      url: '/api/my_profile'
     })
     await dispatch('profile/access/setProfile', myProfile.data)
   }

+ 2 - 1
tests/unit/services/connection/connection.spec.ts

@@ -39,7 +39,8 @@ describe('invoke()', () => {
           'method': HTTP_METHOD.GET,
           'progress': false,
           'responseType': 'blob',
-          'url': 'files/1/download'
+          'url': 'files/1/download',
+          'params': {}
         }
       )
     })

+ 1 - 1
tests/unit/services/connection/urlBuilder.spec.ts

@@ -17,7 +17,7 @@ describe('invoke()', () => {
       expect(UrlBuilder.build({
         type: QUERY_TYPE.DEFAULT,
         url: 'users'
-      })).toEqual('/api/users')
+      })).toEqual('users')
     })
   })
 

+ 7 - 1
tests/unit/services/connection/urlOptionsBuilder.spec.ts

@@ -25,11 +25,17 @@ describe('build()', () => {
       type: QUERY_TYPE.MODEL,
       listArgs: {
         itemsPerPage:10,
-        page:1
+        page:1,
+        filters:[
+          {key: 'name', value: 'Foo'},
+          {key: 'givenName', value: 'Bar'}
+        ]
       }
     }
 
     const options = UrlOptionsBuilder.build(args)
+    expect(options.pop()).toEqual('givenName=Bar')
+    expect(options.pop()).toEqual('name=Foo')
     expect(options.pop()).toEqual('page=1')
     expect(options.pop()).toEqual('itemsPerPage=10')
   })

+ 48 - 2
tests/unit/services/serializer/normalizer/model.spec.ts

@@ -1,13 +1,17 @@
 import Model from '~/services/serializer/normalizer/model'
-import { DataPersisterArgs } from '~/types/interfaces'
+import {AnyJson, DataPersisterArgs} from '~/types/interfaces'
 import { QUERY_TYPE } from '~/types/enums'
 import { repositoryHelper } from '~/services/store/repository'
 import User from '~/tests/unit/fixture/models/User'
 import { createStore } from '~/tests/unit/Helpers'
+import {queryHelper} from "~/services/store/query";
 
 jest.mock('~/services/store/repository')
 const repositoryHelperMock = repositoryHelper as jest.Mocked<typeof repositoryHelper>
 
+jest.mock('~/services/store/query')
+const queryHelperMock = queryHelper as jest.Mocked<typeof queryHelper>
+
 describe('support()', () => {
   it('should support model query type', () => {
     expect(Model.support(QUERY_TYPE.MODEL)).toBeTruthy()
@@ -39,7 +43,7 @@ describe('normalize()', () => {
     expect(() => Model.normalize(args)).toThrowError('Item not found')
   })
 
-  it('should normalize model to JSON', async () => {
+  it('should find an item thanks to a repository, and normalize it to JSON', async () => {
     const store = createStore()
     const user = store.$repo(User).make()
     repositoryHelperMock.findItemFromModel = jest.fn().mockReturnValue(user)
@@ -51,4 +55,46 @@ describe('normalize()', () => {
     }
     expect(Model.normalize(args)).toStrictEqual({ id: 1, name: 'John Doe' })
   })
+
+  it('should find an item thanks to a query, and normalize it to JSON', async () => {
+    const store = createStore()
+    const user = store.$repo(User).make()
+    const query = store.$repo(User).query()
+    queryHelperMock.getItem = jest.fn().mockReturnValue(user)
+
+    const args:DataPersisterArgs = {
+      type: QUERY_TYPE.MODEL,
+      model: User,
+      id: 1,
+      query: query
+    }
+    expect(Model.normalize(args)).toStrictEqual({ id: 1, name: 'John Doe' })
+  })
+})
+
+describe('isPostQuery()', () => {
+  it('should return true if args has got a temp ID', () => {
+    const args:DataPersisterArgs = {
+      type: QUERY_TYPE.MODEL,
+      idTemp: 1
+    }
+    expect(Model.isPostQuery(args)).toBeTruthy()
+  })
+
+  it('should return false if args has not a temp ID', () => {
+    const args:DataPersisterArgs = {
+      type: QUERY_TYPE.MODEL
+    }
+    expect(Model.isPostQuery(args)).toBeFalsy()
+  })
+})
+
+describe('sanitizeBeforePost()', () => {
+  it('should return data without id', () => {
+    const data:AnyJson = {
+      id: 1,
+      foo: 'bar'
+    }
+    expect(Model.sanitizeBeforePost(data)).toEqual({foo:'bar'})
+  })
 })

+ 1 - 1
tests/unit/services/store/repository.spec.ts

@@ -84,7 +84,7 @@ describe('cleanRepository()', () => {
   it('should clean the entire repository', () => {
     const user = repository.make()
     repository.save(user)
-    repositoryHelper.cleanRepository(repository)
+    repositoryHelper.cleanRepository(User)
     expect(repository.all()).toEqual([])
   })
 })

+ 15 - 0
tests/unit/services/utils/modelsUtils.spec.ts

@@ -0,0 +1,15 @@
+import ModelsUtils from "~/services/utils/modelsUtils";
+
+describe('extractIdFromUri()', () => {
+  it('should return null if uri is empty', () => {
+    expect(ModelsUtils.extractIdFromUri('')).toBeNull()
+  })
+
+  it('should return Error if uri is not an number', () => {
+    expect(() => ModelsUtils.extractIdFromUri('/api/person/id')).toThrow()
+  })
+
+  it('should return the id s uri', () => {
+    expect(ModelsUtils.extractIdFromUri('/api/person/2')).toEqual(2)
+  })
+})

+ 13 - 5
types/interfaces.d.ts

@@ -1,4 +1,4 @@
-import { Model } from '@vuex-orm/core'
+import { Model, Query } from '@vuex-orm/core'
 import { Ability } from '@casl/ability'
 import { Store } from 'vuex'
 import { Context } from '@nuxt/types/app'
@@ -154,7 +154,8 @@ interface UrlArgs {
   readonly idTemp?: any,
   readonly rootId?: number,
   readonly showProgress?: boolean,
-  readonly hook?: string
+  readonly hook?: string,
+  readonly params?: AnyJson
 }
 
 interface ImageArgs {
@@ -163,9 +164,15 @@ interface ImageArgs {
   readonly width: number
 }
 
+interface Filter{
+  readonly key: string,
+  readonly value: string|boolean|number
+}
+
 interface ListArgs {
-  readonly itemsPerPage: number,
-  readonly page: number
+  readonly itemsPerPage?: number,
+  readonly page?: number
+  readonly filters?: Array<Filter>
 }
 
 interface DataProviderArgs extends UrlArgs {
@@ -173,7 +180,8 @@ interface DataProviderArgs extends UrlArgs {
   listArgs?: ListArgs,
 }
 interface DataPersisterArgs extends UrlArgs {
-  data?: AnyJson
+  data?: AnyJson,
+  query?: Query
 }
 interface DataDeleterArgs extends UrlArgs {}
 

+ 41 - 0
use/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 Use/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
use/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 Use/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
use/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 Use/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
+    }
+  }
+}

+ 1 - 1
use/form/useValidator.ts

@@ -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') {

+ 7 - 4
use/layout/Menus/websiteMenu.ts

@@ -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

Разлика између датотеке није приказан због своје велике величине
+ 435 - 428
yarn.lock


Неке датотеке нису приказане због велике количине промена