Browse Source

Merge branch 'feature/orga_contact_points_crud' into develop

Olivier Massot 4 năm trước cách đây
mục cha
commit
4f75144db3

+ 1 - 0
.gitignore

@@ -89,3 +89,4 @@ sw.*
 # Vim swap files
 *.swp
 /.project
+/todo.md

+ 1 - 0
README.md

@@ -13,6 +13,7 @@ A voir:
 * [vuetifyjs.com](https://vuetifyjs.com/en/)
 * [typescriptlang.org](https://www.typescriptlang.org/)
 * [jestjs.io](https://jestjs.io/docs/getting-started)
+* [cypress.io](https://docs.cypress.io/guides/getting-started/installing-cypress)
 
 ### Build Setup
 

+ 8 - 8
components/Ui/Form.vue

@@ -10,13 +10,13 @@ Formulaire générique
       ref="form"
       v-model="properties.valid"
       lazy-validation
-      :readonly="readOnly"
+      :readonly="readonly"
     >
       <v-container fluid class="container btnActions">
         <v-row>
           <v-col cols="12" sm="12">
             <slot name="form.button" />
-            <v-btn v-if="!readOnly" class="mr-4 ot_green ot_white--text" @click="submit">
+            <v-btn v-if="!readonly" class="mr-4 ot_green ot_white--text" @click="submit">
               {{ $t('save') }}
             </v-btn>
           </v-col>
@@ -29,7 +29,7 @@ Formulaire générique
         <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">
+            <v-btn v-if="!readonly" class="mr-4 ot_green ot_white--text" @click="submit">
               {{ $t('save') }}
             </v-btn>
           </v-col>
@@ -94,16 +94,16 @@ export default defineComponent({
     const { $dataPersister, store, app: { router, i18n } } = useContext()
     const { markFormAsDirty, markFormAsNotDirty } = $useDirtyForm(store)
 
-    const { id, query }:ToRefs = toRefs(props)
-    const properties:UnwrapRef<AnyJson> = reactive({
+    const { id, query }: ToRefs = toRefs(props)
+    const properties: UnwrapRef<AnyJson> = reactive({
       valid: false,
       saveOk: false,
       saveKo: false
     })
 
-    const readOnly:Ref<boolean> = ref(false)
+    const readonly: Ref<boolean> = ref(false)
 
-    const entry:ComputedRef<AnyJson> = computed(() => {
+    const entry: ComputedRef<AnyJson> = computed(() => {
       return queryHelper.getFlattenEntry(query.value, id.value)
     })
 
@@ -166,7 +166,7 @@ export default defineComponent({
       submit,
       updateRepository,
       properties,
-      readOnly,
+      readonly,
       showDialog,
       entry,
       goEvenUnsavedData,

+ 1 - 1
components/Ui/Input/Autocomplete.vue

@@ -45,7 +45,7 @@ export default defineComponent({
       required: false,
       default: () => []
     },
-    readOnly: {
+    readonly: {
       type: Boolean,
       required: false
     },

+ 1 - 1
components/Ui/Input/AutocompleteWithAPI.vue

@@ -47,7 +47,7 @@ export default defineComponent({
       required: false,
       default: null
     },
-    readOnly: {
+    readonly: {
       type: Boolean,
       required: false
     },

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

@@ -12,7 +12,7 @@ Case à cocher
     <v-checkbox
       :value="data"
       :label="$t(label_field)"
-      :disabled="readOnly"
+      :disabled="readonly"
       @change="$emit('update', $event, field)"
     />
   </v-container>
@@ -37,7 +37,7 @@ export default defineComponent({
       type: Boolean,
       required: false
     },
-    readOnly: {
+    readonly: {
       type: Boolean,
       required: false
     }

+ 2 - 2
components/Ui/Input/DatePicker.vue

@@ -20,7 +20,7 @@ Sélecteur de dates
           autocomplete="off"
           :label="$t(label_field)"
           prepend-icon="mdi-calendar"
-          :disabled="readOnly"
+          :disabled="readonly"
           v-bind="attrs"
           :dense="dense"
           :single-line="singleLine"
@@ -60,7 +60,7 @@ export default defineComponent({
       required: false,
       default: null
     },
-    readOnly: {
+    readonly: {
       type: Boolean,
       required: false
     },

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

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

+ 2 - 2
components/Ui/Input/Enum.vue

@@ -20,7 +20,7 @@ Liste déroulante dédiée à l'affichage d'objets Enum
       item-text="label"
       item-value="value"
       :rules="rules"
-      :disabled="readOnly"
+      :disabled="readonly"
       @change="$emit('update', $event, field)"
     />
   </main>
@@ -52,7 +52,7 @@ export default defineComponent({
       required: false,
       default: null
     },
-    readOnly: {
+    readonly: {
       type: Boolean,
       required: false
     },

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

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

+ 2 - 2
components/Ui/Input/Text.vue

@@ -10,7 +10,7 @@ Champs de saisie de texte
     :value="data"
     :label="$t(label_field)"
     :rules="rules"
-    :disabled="readOnly"
+    :disabled="readonly"
     :type="type"
     :error="error"
     :error-messages="errorMessage"
@@ -43,7 +43,7 @@ export default defineComponent({
       required: false,
       default: null
     },
-    readOnly: {
+    readonly: {
       type: Boolean,
       required: false
     },

+ 4 - 1
config/nuxtConfig/build.js

@@ -13,13 +13,16 @@ export default {
   build: {
     extend (config, { isDev, isClient }) {
       config.node = {
-        fs: "empty"
+        fs: 'empty'
       }
     },
     plugins: [
       new webpack.ProvidePlugin({
         _: 'lodash'
       })
+    ],
+    transpile: [
+      'vue-tel-input-vuetify'
     ]
   },
 

+ 2 - 1
config/nuxtConfig/plugins.js

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

+ 2 - 1
lang/breadcrumbs/fr-FR.js

@@ -2,6 +2,7 @@ export default (context, locale) => {
   return ({
     item: 'Détails',
     organization_breadcrumbs: 'Fiche de la structure',
-    address_breadcrumbs: 'Adresse postale'
+    address_breadcrumbs: 'Adresse postale',
+    contact_points_breadcrumbs: 'Points de contact'
   })
 }

+ 5 - 1
lang/field/fr-FR.js

@@ -38,6 +38,8 @@ export default (context, locale) => {
     emailInvalid: 'E-mail invalide',
     telphone: 'Téléphone',
     telphoneInvalid: 'Téléphone invalide',
+    faxNumber: 'Fax',
+    faxNumberInvalid: 'Fax invalide',
     mobilPhone: 'Portable',
     mobilPhoneInvalid: 'Portable invalide',
     actions: 'Actions',
@@ -66,6 +68,8 @@ export default (context, locale) => {
     postalCode: 'Code postal',
     addressCity: 'Ville',
     country: 'Pays',
-    addresstype: 'Nature'
+    addresstype: 'Nature',
+    contactpoint_type: 'Type de contact',
+    phoneNumberInvalid: 'Numéro de téléphone invalide'
   })
 }

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

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

+ 16 - 0
models/Organization/OrganizationContactPoint.ts

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

+ 4 - 2
package.json

@@ -17,23 +17,25 @@
     "@casl/vue": "^1.2",
     "@fortawesome/fontawesome-free": "^5.15",
     "@nuxt/components": "^2.2",
+    "@nuxt/image": "^0.6.0",
     "@nuxt/typescript-runtime": "^2.0",
     "@nuxtjs/axios": "^5.13",
-    "@nuxtjs/vuetify": "^1.12.1",
     "@nuxtjs/composition-api": "^0.28",
     "@nuxtjs/i18n": "^7.0",
-    "@nuxt/image": "^0.6.0",
+    "@nuxtjs/vuetify": "^1.12.1",
     "@types/lodash": "^4.14",
     "@vuex-orm/core": "1.0.0-draft.14",
     "cookieparser": "^0.1",
     "core-js": "^3.17",
     "js-yaml": "^4.0",
+    "libphonenumber-js": "^1.9.39",
     "lodash": "^4.17",
     "marked": "^3.0",
     "nuxt": "^2.15",
     "nuxt-leaflet": "^0.0",
     "postcss": "^8.3.6",
     "vue-i18n-composable": "^0.2",
+    "vue-tel-input-vuetify": "^1.4.1",
     "vue-template-compiler": "^2.6",
     "webpack": "^4.46",
     "yaml-import": "^2.0"

+ 130 - 0
pages/organization/contact_points/_id.vue

@@ -0,0 +1,130 @@
+<!-- Page de détails d'un point de contact -->
+
+<template>
+  <main>
+    <v-skeleton-loader
+      v-if="loading"
+      type="text"
+    />
+    <LayoutContainer v-else>
+      <v-card class="margin-bottom-20">
+        <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">
+              fa-globe-europe
+            </v-icon>
+            {{ $t('contact_point') }}
+          </v-toolbar-title>
+        </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">
+              <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 v-slot: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 { defineComponent, useContext, useFetch, ref, Ref } from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+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'
+
+export default defineComponent({
+  name: 'ContactPoint',
+  setup () {
+    const { route, $dataProvider } = useContext()
+    const id: number = parseInt(route.value.params.id)
+    const loading: Ref<boolean> = ref(true)
+
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(ContactPoint)
+    const query: Query = repository.query()
+
+    useFetch(async () => {
+      await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: ContactPoint,
+        id
+      })
+      loading.value = false
+    })
+
+    return {
+      model: ContactPoint,
+      query: () => query,
+      id,
+      loading
+    }
+  }
+})
+</script>
+<style>
+  .toolbarForm .v-toolbar__content{
+    padding-left: 0 !important;
+  }
+  .toolbarForm .v-toolbar__title .v-icon{
+    height: 46px;
+    width: 46px;
+  }
+</style>

+ 135 - 124
pages/organization/index.vue

@@ -4,62 +4,63 @@ Contient toutes les informations sur l'organization courante
 -->
 <template>
   <LayoutContainer>
-
-    <UiForm :model="models.Organization" :id="id" :query="repositories.organizationRepository.query()">
-      <template v-slot:form.input="{entry, updateRepository}">
-
+    <UiForm :id="id" :model="models.Organization" :query="repositories.organizationRepository.query()">
+      <template #form.input="{entry, updateRepository}">
         <v-expansion-panels :value="panel" focusable accordion>
-
           <!-- Description -->
           <UiExpansionPanel id="description" icon="fa-info">
             <v-container fluid class="container">
               <v-row>
                 <v-col cols="12" sm="6">
-                  <UiInputText field="name" :data="entry['name']"  v-on:update="updateRepository"/>
+                  <UiInputText field="name" :data="entry['name']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="acronym" :data="entry['acronym']"  v-on:update="updateRepository"/>
+                  <UiInputText field="acronym" :data="entry['acronym']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="siretNumber" :data="entry['siretNumber']"  v-on:update="checkSiretHook($event, 'siretNumber', updateRepository)"
-                                  :error="siretError"
-                                  :errorMessage="siretErrorMessage"
-                                  :rules="rules.siretRule"/>
+                  <UiInputText
+                    field="siretNumber"
+                    :data="entry['siretNumber']"
+                    :error="siretError"
+                    :error-message="siretErrorMessage"
+                    :rules="rules.siretRule"
+                    @update="checkSiretHook($event, 'siretNumber', updateRepository)"
+                  />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="apeNumber" :data="entry['apeNumber']"  v-on:update="updateRepository"/>
+                  <UiInputText field="apeNumber" :data="entry['apeNumber']" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="entry['legalStatus'] === 'ASSOCIATION_LAW_1901'">
-                  <UiInputText field="waldecNumber" :data="entry['waldecNumber']"  v-on:update="updateRepository"/>
+                <v-col v-if="entry['legalStatus'] === 'ASSOCIATION_LAW_1901'" cols="12" sm="6">
+                  <UiInputText field="waldecNumber" :data="entry['waldecNumber']" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="organizationProfile.isInsideNetwork()">
-                  <UiInputText :label="organizationProfile.isCmf() ? 'identifierCmf' : 'identifierFfec'" field="identifier" :data="entry['identifier']"  v-on:update="updateRepository"/>
+                <v-col v-if="organizationProfile.isInsideNetwork()" cols="12" sm="6">
+                  <UiInputText :label="organizationProfile.isCmf() ? 'identifierCmf' : 'identifierFfec'" field="identifier" :data="entry['identifier']" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="organizationProfile.isFfec()">
-                  <UiInputText field="ffecApproval" :data="entry['ffecApproval']"  v-on:update="updateRepository"/>
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputText field="ffecApproval" :data="entry['ffecApproval']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="description" :data="entry['description']"  v-on:update="updateRepository"/>
+                  <UiInputText field="description" :data="entry['description']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="logo" :data="entry['logo']"  v-on:update="updateRepository"/>
+                  <UiInputText field="logo" :data="entry['logo']" @update="updateRepository" />
                 </v-col>
 
                 <!-- @todo: ajouter les if et transformer en select-->
                 <v-col cols="12" sm="6">
-                  <UiInputText field="typeOfPractices" :data="entry['typeOfPractices']"  v-on:update="updateRepository"/>
+                  <UiInputText field="typeOfPractices" :data="entry['typeOfPractices']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="otherPractice" :data="entry['otherPractice']"  v-on:update="updateRepository"/>
+                  <UiInputText field="otherPractice" :data="entry['otherPractice']" @update="updateRepository" />
                 </v-col>
               </v-row>
             </v-container>
@@ -74,33 +75,35 @@ Contient toutes les informations sur l'organization courante
                     :query="repositories.addressRepository.with('addressPostal', (query) => {
                       query.with('addressCountry')
                     })"
-                    :rootModel="models.Organization"
-                    :rootId="id"
+                    :root-model="models.Organization"
+                    :root-id="id"
                     :model="models.OrganizationAddressPostal"
                   >
-                    <template v-slot:list.item="{items}">
+                    <template #list.item="{items}">
                       <v-container fluid>
                         <v-row dense>
-                            <v-col
-                              v-for="item in items"
-                              :key="item.id"
-                              cols="4"
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/organization/address/${item.id}`"
+                              :model="models.OrganizationAddressPostal"
                             >
-                              <UiCard
-                                :link="`/organization/address/${item.id}`"
-                                :model="models.OrganizationAddressPostal"
-                                :id="item.id"
-                              >
-                                <template v-slot:card.title>{{$t(item.type)}}</template>
-                                <template v-slot:card.text>
-                                  {{item.addressPostal.streetAddress}} <br />
-                                  <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>
-                                </template>
-                              </UiCard>
-                            </v-col>
+                              <template #card.title>
+                                {{ $t(item.type) }}
+                              </template>
+                              <template #card.text>
+                                {{ item.addressPostal.streetAddress }} <br>
+                                <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>
+                              </template>
+                            </UiCard>
+                          </v-col>
                         </v-row>
                       </v-container>
                     </template>
@@ -117,11 +120,11 @@ Contient toutes les informations sur l'organization courante
                 <v-col cols="12" sm="12">
                   <UiSubList
                     :query="repositories.contactPointRepository.query()"
-                    :rootModel="models.Organization"
-                    :rootId="id"
+                    :root-model="models.Organization"
+                    :root-id="id"
                     :model="models.ContactPoint"
                   >
-                    <template v-slot:list.item="{items}">
+                    <template #list.item="{items}">
                       <v-container fluid>
                         <v-row dense>
                           <v-col
@@ -130,18 +133,22 @@ Contient toutes les informations sur l'organization courante
                             cols="4"
                           >
                             <UiCard
-                              :link="`/organization/contact-points/${item.id}`"
-                              :model="models.ContactPoint"
                               :id="item.id"
+                              :link="`/organization/contact_points/${item.id}`"
+                              :model="models.ContactPoint"
                             >
-                              <template v-slot:card.title>{{$t(item.contactType)}}</template>
-                              <template v-slot:card.text>
-                                <span v-if="item.email"><strong>{{$t('email')}}</strong> : {{item.email}} <br /></span>
-                                <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> : {{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.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>
+                              <template #card.title>
+                                {{ $t(item.contactType) }}
+                              </template>
+                              <template #card.text>
+                                <span v-if="item.email"><strong>{{ $t('email') }}</strong> : {{ item.email }} <br></span>
+                                <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.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>
                               </template>
                             </UiCard>
                           </v-col>
@@ -159,38 +166,38 @@ Contient toutes les informations sur l'organization courante
             <v-container fluid class="container">
               <v-row>
                 <v-col cols="12" sm="6">
-                  <UiInputDatePicker field="creationDate" :data="entry['creationDate']"  v-on:update="updateRepository"/>
+                  <UiInputDatePicker field="creationDate" :data="entry['creationDate']" @update="updateRepository" />
                 </v-col>
                 <v-col cols="12" sm="6">
-                  <UiInputText field="prefectureName" :data="entry['prefectureName']"  v-on:update="updateRepository"/>
+                  <UiInputText field="prefectureName" :data="entry['prefectureName']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="prefectureNumber" :data="entry['prefectureNumber']"  v-on:update="updateRepository"/>
+                  <UiInputText field="prefectureNumber" :data="entry['prefectureNumber']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="tvaNumber" :data="entry['tvaNumber']"  v-on:update="updateRepository"/>
+                  <UiInputText field="tvaNumber" :data="entry['tvaNumber']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputEnum field="legalStatus" :data="entry['legalStatus']"  enumType="organization_legal" v-on:update="updateRepository"/>
+                  <UiInputEnum field="legalStatus" :data="entry['legalStatus']" enum-type="organization_legal" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="!organizationProfile.isManagerProduct()">
-                  <UiInputEnum field="principalType" :data="entry['principalType']"  enumType="organization_principal_type" v-on:update="updateRepository"/>
+                <v-col v-if="!organizationProfile.isManagerProduct()" cols="12" sm="6">
+                  <UiInputEnum field="principalType" :data="entry['principalType']" enum-type="organization_principal_type" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="!organizationProfile.isFfec() && !organizationProfile.isManagerProduct() && !organizationProfile.isArtist()">
-                  <UiInputEnum field="schoolCategory" :data="entry['schoolCategory']"  enumType="organization_school_cat" v-on:update="updateRepository"/>
+                <v-col v-if="!organizationProfile.isFfec() && !organizationProfile.isManagerProduct() && !organizationProfile.isArtist()" cols="12" sm="6">
+                  <UiInputEnum field="schoolCategory" :data="entry['schoolCategory']" enum-type="organization_school_cat" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="organizationProfile.isFfec()">
-                  <UiInputEnum field="typeEstablishment" :data="entry['typeEstablishment']"  enumType="organization_type_establishment" v-on:update="updateRepository"/>
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputEnum field="typeEstablishment" :data="entry['typeEstablishment']" enum-type="organization_type_establishment" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="entry.typeEstablishment === 'MULTIPLE'">
-                  <UiInputEnum field="typeEstablishmentDetail" :data="entry['typeEstablishmentDetail']"  enumType="organization_type_establishment_detail" v-on:update="updateRepository"/>
+                <v-col v-if="entry.typeEstablishment === 'MULTIPLE'" cols="12" sm="6">
+                  <UiInputEnum field="typeEstablishmentDetail" :data="entry['typeEstablishmentDetail']" enum-type="organization_type_establishment_detail" @update="updateRepository" />
                 </v-col>
               </v-row>
             </v-container>
@@ -201,15 +208,15 @@ Contient toutes les informations sur l'organization courante
             <v-container class="container">
               <v-row>
                 <v-col cols="12" sm="6">
-                  <UiInputText field="youngApproval" :data="entry['youngApproval']"  v-on:update="updateRepository"/>
+                  <UiInputText field="youngApproval" :data="entry['youngApproval']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="trainingApproval" :data="entry['trainingApproval']"  v-on:update="updateRepository"/>
+                  <UiInputText field="trainingApproval" :data="entry['trainingApproval']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="otherApproval" :data="entry['otherApproval']"  v-on:update="updateRepository"/>
+                  <UiInputText field="otherApproval" :data="entry['otherApproval']" @update="updateRepository" />
                 </v-col>
               </v-row>
             </v-container>
@@ -220,19 +227,19 @@ Contient toutes les informations sur l'organization courante
             <v-container class="container">
               <v-row>
                 <v-col cols="12" sm="6">
-                  <UiInputText field="collectiveAgreement" :data="entry['collectiveAgreement']"  v-on:update="updateRepository"/>
+                  <UiInputText field="collectiveAgreement" :data="entry['collectiveAgreement']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputEnum field="opca" :data="entry['opca']"  enumType="organization_opca" v-on:update="updateRepository"/>
+                  <UiInputEnum field="opca" :data="entry['opca']" enum-type="organization_opca" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="icomNumber" :data="entry['icomNumber']"  v-on:update="updateRepository"/>
+                  <UiInputText field="icomNumber" :data="entry['icomNumber']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="urssafNumber" :data="entry['urssafNumber']"  v-on:update="updateRepository"/>
+                  <UiInputText field="urssafNumber" :data="entry['urssafNumber']" @update="updateRepository" />
                 </v-col>
               </v-row>
             </v-container>
@@ -242,16 +249,16 @@ Contient toutes les informations sur l'organization courante
           <UiExpansionPanel v-if="organizationProfile.isInsideNetwork()" id="network" icon="fa-share-alt">
             <v-container class="container">
               <v-row>
-                <v-col cols="12" sm="6" v-if="organizationProfile.isFfec()">
-                  <UiInputText field="budget" :data="entry['budget']"  v-on:update="updateRepository" type="number"/>
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputText field="budget" :data="entry['budget']" type="number" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="organizationProfile.isFfec()">
-                  <UiInputCheckbox field="isPedagogicIsPrincipalActivity" :data="entry['isPedagogicIsPrincipalActivity']"  v-on:update="updateRepository"/>
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputCheckbox field="isPedagogicIsPrincipalActivity" :data="entry['isPedagogicIsPrincipalActivity']" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="organizationProfile.isFfec()">
-                  <UiInputText field="pedagogicBudget" :data="entry['pedagogicBudget']"  v-on:update="updateRepository" type="number"/>
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputText field="pedagogicBudget" :data="entry['pedagogicBudget']" type="number" @update="updateRepository" />
                 </v-col>
               </v-row>
             </v-container>
@@ -262,23 +269,23 @@ Contient toutes les informations sur l'organization courante
             <v-container class="container">
               <v-row>
                 <v-col cols="12" sm="6">
-                  <UiInputText field="twitter" :data="entry['twitter']"  v-on:update="updateRepository"/>
+                  <UiInputText field="twitter" :data="entry['twitter']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="facebook" :data="entry['facebook']"  v-on:update="updateRepository"/>
+                  <UiInputText field="facebook" :data="entry['facebook']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="instagram" :data="entry['instagram']"  v-on:update="updateRepository"/>
+                  <UiInputText field="instagram" :data="entry['instagram']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputCheckbox field="portailVisibility" :data="entry['portailVisibility']"  v-on:update="updateRepository"/>
+                  <UiInputCheckbox field="portailVisibility" :data="entry['portailVisibility']" @update="updateRepository" />
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="image" :data="entry['image']"  v-on:update="updateRepository"/>
+                  <UiInputText field="image" :data="entry['image']" @update="updateRepository" />
                 </v-col>
               </v-row>
             </v-container>
@@ -291,17 +298,15 @@ Contient toutes les informations sur l'organization courante
                 <v-col cols="12" sm="12">
                   <UiDataTable
                     :query="repositories.bankAccountRepository.query()"
-                    :rootModel="models.Organization"
-                    :rootId="id"
+                    :root-model="models.Organization"
+                    :root-id="id"
                     :model="models.BankAccount"
                     :headers="datatableHeaders.headersBankAccount"
-                  >
-                  </UiDataTable>
+                  />
                 </v-col>
               </v-row>
             </v-container>
           </UiExpansionPanel>
-
         </v-expansion-panels>
       </template>
     </UiForm>
@@ -309,39 +314,40 @@ Contient toutes les informations sur l'organization courante
 </template>
 
 <script lang="ts">
-import {defineComponent, useContext} from '@nuxtjs/composition-api'
-import {$organizationProfile} from "~/services/profile/organizationProfile";
-import {Organization} from '@/models/Organization/Organization'
-import {OrganizationAddressPostal} from "~/models/Organization/OrganizationAddressPostal";
-import {ContactPoint} from "~/models/Core/ContactPoint";
-import {BankAccount} from "~/models/Core/BankAccount";
-import {repositoryHelper} from "~/services/store/repository";
-import UseValidator from "~/use/form/useValidator";
-import {UseNavigationHelpers} from "~/use/form/useNavigationHelpers";
+import { defineComponent, useContext } from '@nuxtjs/composition-api'
+import { $organizationProfile } from '~/services/profile/organizationProfile'
+import { Organization } from '@/models/Organization/Organization'
+import { OrganizationAddressPostal } from '~/models/Organization/OrganizationAddressPostal'
+import { ContactPoint } from '~/models/Core/ContactPoint'
+import { BankAccount } from '~/models/Core/BankAccount'
+import { repositoryHelper } from '~/services/store/repository'
+import UseValidator from '~/use/form/useValidator'
+import { UseNavigationHelpers } from '~/use/form/useNavigationHelpers'
+import I18N from '~/services/utils/i18n'
 
 export default defineComponent({
   name: 'OrganizationParent',
-  setup() {
-    const {store, app: {i18n}, $dataProvider} = useContext()
+  setup () {
+    const { store, app: { i18n }, $dataProvider } = useContext()
     const organizationProfile = $organizationProfile(store)
-    const id: number = store.state.profile.organization.id;
+    const id: number = store.state.profile.organization.id
 
     const repositories = getRepositories()
 
-    const {siretError, siretErrorMessage, checkSiret} = UseValidator.useHandleSiret(i18n, $dataProvider)
+    const { siretError, siretErrorMessage, checkSiret } = UseValidator.useHandleSiret(i18n, $dataProvider)
+
     const checkSiretHook = async (siret: string, field: string, updateRepository: any) => {
       await checkSiret(siret)
-      if(!siretError.value)
-        updateRepository(siret, field);
+      if (!siretError.value) { updateRepository(siret, field) }
     }
 
-    const {panel} = UseNavigationHelpers.expansionPanels()
+    const { panel } = UseNavigationHelpers.expansionPanels()
 
     return {
       repositories,
       id,
       organizationProfile,
-      models: {Organization, ContactPoint, BankAccount, OrganizationAddressPostal},
+      models: { Organization, ContactPoint, BankAccount, OrganizationAddressPostal },
       datatableHeaders: getDataTablesHeaders(i18n),
       rules: getRules(i18n),
       siretError,
@@ -350,9 +356,14 @@ export default defineComponent({
       panel
     }
   },
+  methods: {
+    formatPhoneNumber (number: string): string {
+      return I18N.formatPhoneNumber(number)
+    }
+  }
 })
 
-function getRules(i18n: any) {
+function getRules (i18n: any) {
   return {
     nameRules: [
       (nameValue: string) => !!nameValue || i18n.t('required'),
@@ -364,29 +375,29 @@ function getRules(i18n: any) {
   }
 }
 
-function getDataTablesHeaders(i18n: any) {
+function getDataTablesHeaders (i18n: any) {
   return {
     headersContactPoint: [
-      {text: i18n.t('email'), value: 'email'},
-      {text: i18n.t('telphone'), value: 'telphone'},
-      {text: i18n.t('mobilPhone'), value: 'mobilPhone'},
-      {text: i18n.t('actions'), value: 'actions', sortable: false},
+      { text: i18n.t('email'), value: 'email' },
+      { text: i18n.t('telphone'), value: 'telphone' },
+      { text: i18n.t('mobilPhone'), value: 'mobilPhone' },
+      { text: i18n.t('actions'), value: 'actions', sortable: false }
     ],
     headersBankAccount: [
-      {text: i18n.t('bankName'), value: 'bankName'},
-      {text: i18n.t('iban'), value: 'iban'},
-      {text: i18n.t('bic'), value: 'bic'},
-      {text: i18n.t('actions'), value: 'actions', sortable: false},
+      { text: i18n.t('bankName'), value: 'bankName' },
+      { text: i18n.t('iban'), value: 'iban' },
+      { text: i18n.t('bic'), value: 'bic' },
+      { text: i18n.t('actions'), value: 'actions', sortable: false }
     ],
     headersAddressPostal: [
-      {text: i18n.t('address_postal_type'), value: 'type'},
-      {text: i18n.t('address'), value: 'address'},
-      {text: i18n.t('actions'), value: 'actions', sortable: false},
+      { text: i18n.t('address_postal_type'), value: 'type' },
+      { text: i18n.t('address'), value: 'address' },
+      { text: i18n.t('actions'), value: 'actions', sortable: false }
     ]
   }
 }
 
-function getRepositories() {
+function getRepositories () {
   return {
     organizationRepository: repositoryHelper.getRepository(Organization),
     contactPointRepository: repositoryHelper.getRepository(ContactPoint),

+ 17 - 0
plugins/phone-input.ts

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import vuetify from 'vuetify'
+// @ts-ignore
+import VueTelInputVuetify from 'vue-tel-input-vuetify/lib'
+import { NuxtAppOptions } from '@nuxt/types/app'
+
+// -- vue-tel-input-vuetify --
+// @see https://github.com/yogakurniawan/vue-tel-input-vuetify
+export default ({ app } : { app: NuxtAppOptions }) => {
+  Vue.use(VueTelInputVuetify, {
+    vuetify,
+    preferredCountries: ['FR', 'DE', 'CH', 'BE', 'GB'],
+    defaultCountry: 'FR',
+    mode: 'international',
+    invalidMsg: app.i18n.t('phoneNumberInvalid')
+  })
+}

+ 3 - 1
services/rights/abilitiesUtils.ts

@@ -78,7 +78,9 @@ class AbilitiesUtils {
     const abilitiesByRoles = this.getAbilitiesByRoles(this.$store.state.profile.access.roles)
     this.$ability.update(abilitiesByRoles)
     this.initFactory()
-    return abilitiesByRoles.concat(this.getAbilitiesByConfig('./config/abilities/config.yaml'))
+    return abilitiesByRoles.concat(
+      this.getAbilitiesByConfig('./config/abilities/config.yaml')
+    )
   }
 
   /**

+ 0 - 1
services/serializer/denormalizer/yaml.ts

@@ -20,7 +20,6 @@ class Yaml extends BaseDenormalizer {
    */
   public static denormalize (data: AnyJson): AnyJson {
     try {
-      // noinspection TypeScriptValidateJSTypes,JSVoidFunctionReturnValueUsed
       return yaml.load(yaml.dump(read(data.path)))
     } catch (e) {
       throw new Error(e.message)

+ 8 - 0
services/utils/i18n.ts

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

+ 7 - 6
types/interfaces.d.ts

@@ -131,12 +131,6 @@ interface EnumChoice {
   value: string,
   label: string
 }
-interface EnumChoices extends Array<EnumChoice> {}
-
-interface DataManager {
-  initCtx(ctx: Context): void,
-  invoke(args: UrlArgs): Promise<any>,
-}
 
 interface UrlArgs {
   readonly type: QUERY_TYPE,
@@ -157,6 +151,13 @@ interface DataPersisterArgs extends UrlArgs {
 }
 interface DataDeleterArgs extends UrlArgs {}
 
+interface EnumChoices extends Array<EnumChoice> {}
+
+interface DataManager {
+  initCtx(ctx: Context): void,
+  invoke(args: UrlArgs): Promise<any>,
+}
+
 interface HookProvider {
   invoke(args: DataProviderArgs): Promise<any>,
 }

+ 25 - 1
yarn.lock

@@ -3105,6 +3105,11 @@ autoprefixer@^9.6.1:
     postcss "^7.0.32"
     postcss-value-parser "^4.1.0"
 
+awesome-phonenumber@^2.15.0:
+  version "2.59.0"
+  resolved "https://registry.yarnpkg.com/awesome-phonenumber/-/awesome-phonenumber-2.59.0.tgz#9a45f08167d92b1c177cbe2a3df313f8b19d8ebe"
+  integrity sha512-ySAF+Xe0M1NWvj8FMYcgsMMWGuFJAH1DJL9T/juFfozV6bOe0IifP50W+MdyBMQI2rQsNm5mNXBwdVGuwATkmQ==
+
 axios-retry@^3.1.9:
   version "3.1.9"
   resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.1.9.tgz#6c30fc9aeb4519aebaec758b90ef56fa03fe72e8"
@@ -7961,6 +7966,11 @@ levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+libphonenumber-js@^1.9.39:
+  version "1.9.39"
+  resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.39.tgz#af6407167d5446758da795412d082eeb3e005ba2"
+  integrity sha512-TxYz/Ii7mjkocKGKmFHhsTAvvcxr4AY3yUlZzZ2z7HC4DPRrNlzJ9n32/SMogqsyFOXLMXQPCkCInNRbiVaEPA==
+
 lines-and-columns@^1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@@ -12848,6 +12858,15 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.3:
     hash-sum "^1.0.2"
     loader-utils "^1.0.2"
 
+vue-tel-input-vuetify@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/vue-tel-input-vuetify/-/vue-tel-input-vuetify-1.4.1.tgz#9ca62c9c9feb5d96adce59467885e79c285a4a39"
+  integrity sha512-oOswIiWhnkiVoNC4eCJJef4GG0mVkFsiVXTSquTio+oPn4GfMwm5f2OUv9qvuBE5IxdvW2X5WQZOF8IKhN7kHA==
+  dependencies:
+    awesome-phonenumber "^2.15.0"
+    vuetify "^2.2.11"
+    whatwg-fetch "^3.2.0"
+
 vue-template-compiler@^2.0.0, vue-template-compiler@^2.6, vue-template-compiler@^2.6.12, vue-template-compiler@^2.6.14:
   version "2.6.14"
   resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz#a2f0e7d985670d42c9c9ee0d044fed7690f4f763"
@@ -12887,7 +12906,7 @@ vuetify-loader@^1.7.2:
     file-loader "^6.2.0"
     loader-utils "^2.0.0"
 
-vuetify@^2.5.3:
+vuetify@^2.2.11, vuetify@^2.5.3:
   version "2.5.8"
   resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.5.8.tgz#a23e6a7e77b77de0744b53cf1e7c9d40efac0635"
   integrity sha512-paLmNhKTYFD41+14rIHnCo+P1jHbUzwBiMowxs5qXVq8RdRMqRmcy05Sfse1WUu90amPGK2fIFQq5rL2N8zqZg==
@@ -13051,6 +13070,11 @@ whatwg-encoding@^1.0.5:
   dependencies:
     iconv-lite "0.4.24"
 
+whatwg-fetch@^3.2.0:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
+  integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
+
 whatwg-mimetype@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"