Przeglądaj źródła

Merge branch 'feature/parameters' into develop

# Conflicts:
#	lang/layout/fr-FR.js
#	pages/organization.vue
#	pages/organization/address/_id.vue
#	pages/organization/bank_account/_id.vue
#	pages/organization/contact_points/_id.vue
#	pages/organization/index.vue
#	services/profile/organizationProfile.ts
Vincent GUFFON 3 lat temu
rodzic
commit
46b79b7483
78 zmienionych plików z 2611 dodań i 132 usunięć
  1. 2 0
      .gitignore
  2. 1 1
      components/Form/Organization/Address.vue
  3. 1 0
      components/Form/Organization/BankAccount.vue
  4. 1 0
      components/Form/Organization/ContactPoint.vue
  5. 80 0
      components/Form/Parameters/Cycle.vue
  6. 97 0
      components/Form/Parameters/EducationTiming.vue
  7. 80 0
      components/Form/Parameters/ResidenceArea.vue
  8. 2 2
      components/Layout/Alert/Content.vue
  9. 2 2
      components/Layout/Header.vue
  10. 17 3
      components/Layout/Menu.vue
  11. 3 5
      components/Layout/Subheader.vue
  12. 2 2
      components/Ui/Button/Delete.vue
  13. 6 1
      components/Ui/Card.vue
  14. 9 15
      components/Ui/Collection.vue
  15. 10 8
      components/Ui/Form.vue
  16. 18 8
      components/Ui/Input/Autocomplete.vue
  17. 55 7
      components/Ui/Input/AutocompleteWithAPI.vue
  18. 13 3
      components/Ui/Input/Checkbox.vue
  19. 19 4
      components/Ui/Input/DatePicker.vue
  20. 16 5
      components/Ui/Input/Email.vue
  21. 13 3
      components/Ui/Input/Enum.vue
  22. 13 3
      components/Ui/Input/Phone.vue
  23. 10 6
      components/Ui/Input/Text.vue
  24. 4 4
      components/Ui/Input/TextArea.vue
  25. 1 1
      components/Ui/Map.vue
  26. 25 6
      composables/data/useDataUtils.ts
  27. 5 5
      composables/form/useError.ts
  28. 5 1
      composables/layout/Menus/baseMenu.ts
  29. 4 2
      composables/layout/Menus/configurationMenu.ts
  30. 59 0
      composables/layout/Menus/parametersMenu.ts
  31. 13 0
      composables/layout/menu.ts
  32. 12 0
      composables/utils/useAccess.ts
  33. 43 0
      config/abilities/pages/parameters.yaml
  34. 2 1
      config/nuxtConfig/env.js
  35. 5 0
      lang/content/parameters/fr-FR.js
  36. 9 0
      lang/enum/fr-FR.js
  37. 47 0
      lang/field/fr-FR.js
  38. 3 1
      lang/fr-FR.js
  39. 3 1
      lang/help/fr-FR.js
  40. 7 0
      lang/layout/fr-FR.js
  41. 8 2
      lang/rulesAndErrors/fr-FR.js
  42. 1 1
      layouts/default.vue
  43. 81 0
      layouts/parameters.vue
  44. 19 0
      models/Access/Access.ts
  45. 14 0
      models/Access/AdminAccess.ts
  46. 11 0
      models/Billing/ResidenceArea.ts
  47. 14 0
      models/Education/Cycle.ts
  48. 11 0
      models/Education/EducationTiming.ts
  49. 116 0
      models/Organization/Parameters.ts
  50. 14 0
      models/Person/Person.ts
  51. 80 0
      pages/organization.vue
  52. 26 0
      pages/organization/address/_id.vue
  53. 30 0
      pages/organization/bank_account/_id.vue
  54. 31 0
      pages/organization/contact_points/_id.vue
  55. 551 0
      pages/organization/index.vue
  56. 43 0
      pages/parameters.vue
  57. 65 0
      pages/parameters/billing/index.vue
  58. 26 0
      pages/parameters/billing/residence_area/_id.vue
  59. 35 0
      pages/parameters/billing/residence_area/new.vue
  60. 127 0
      pages/parameters/communication.vue
  61. 26 0
      pages/parameters/education/cycle/_id.vue
  62. 26 0
      pages/parameters/education/education_timing/_id.vue
  63. 35 0
      pages/parameters/education/education_timing/new.vue
  64. 108 0
      pages/parameters/education/index.vue
  65. 100 0
      pages/parameters/index.vue
  66. 60 0
      pages/parameters/secure.vue
  67. 163 0
      pages/parameters/student.vue
  68. 1 1
      plugins/Data/axios.js
  69. 1 6
      services/data/baseDataManager.ts
  70. 0 13
      services/exception/apiError.ts
  71. 12 1
      services/profile/organizationProfile.ts
  72. 2 1
      services/store/form.ts
  73. 5 0
      store/profile/organization.ts
  74. 5 0
      tests/unit/component/Layout/SubHeader.spec.js
  75. 5 3
      tests/unit/composables/data/useDataUtils.spec.ts
  76. 4 4
      tests/unit/composables/form/useError.spec.ts
  77. 37 0
      tests/unit/composables/utils/useAccess.spec.ts
  78. 1 0
      types/interfaces.d.ts

+ 2 - 0
.gitignore

@@ -93,3 +93,5 @@ sw.*
 *.swp
 /.project
 /todo.md
+
+package-lock.json

+ 1 - 1
components/Form/Organization/Address.vue

@@ -165,7 +165,7 @@ export default defineComponent({
       return actions
     })
 
-    /** Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
     return {
       model: OrganizationAddressPostal,
       query: () => query,

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

@@ -84,6 +84,7 @@ export default defineComponent({
       return actions
     })
 
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
     return {
       model: BankAccount,
       query: () => query,

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

@@ -98,6 +98,7 @@ export default defineComponent({
       return actions
     })
 
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
     return {
       model: ContactPoint,
       query: () => query,

+ 80 - 0
components/Form/Parameters/Cycle.vue

@@ -0,0 +1,80 @@
+<!-- Component d'un formulaire d'un cycle -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5 mt-4">
+        <FormToolbar title="cycle" icon="fa-bars"/>
+
+        <UiForm
+          :id="id"
+          :model="model"
+          :query="query()"
+          :submitActions="submitActions">
+          <template #form.input="{entry, updateRepository}">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="label" label="title" :data="entry['label']" @update="updateRepository" />
+                </v-col>
+
+              </v-row>
+            </v-container>
+
+          </template>
+
+          <template #form.button>
+            <NuxtLink :to="{ path: '/parameters/education'}" class="no-decoration">
+              <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                {{ $t('back') }}
+              </v-btn>
+            </NuxtLink>
+          </template>
+
+        </UiForm>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed} from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import { Query, Model } from '@vuex-orm/core'
+import { SUBMIT_TYPE} from '~/types/enums'
+import { repositoryHelper } from '~/services/store/repository'
+import {AnyJson} from "~/types/interfaces";
+import {Cycle} from "~/models/Education/Cycle";
+
+export default defineComponent({
+  props: {
+    id:{
+      type: [Number, String],
+      required: true
+    }
+  },
+  setup () {
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(Cycle)
+    const query: Query = repository.query()
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/parameters/education` }
+      actions[SUBMIT_TYPE.SAVE] = { path: `/parameters/education/cycle/` }
+      return actions
+    })
+
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    return {
+      model: Cycle,
+      query: () => query,
+      panel: 0,
+      submitActions
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Cycle)
+  }
+})
+
+</script>

+ 97 - 0
components/Form/Parameters/EducationTiming.vue

@@ -0,0 +1,97 @@
+<!-- Component d'un formulaire d'une durée pour un curriculum -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5 mt-4">
+        <FormToolbar title="educationTiming" icon="fa-calendar"/>
+
+        <UiForm
+          :id="id"
+          :model="model"
+          :query="query()"
+          :submitActions="submitActions">
+          <template #form.input="{entry, updateRepository}">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText
+                    field="timing"
+                    label="timing"
+                    :data="entry['timing']"
+                    @update="updateRepository"
+                    type="number"
+                    :rules="rules().timingRules"
+                  />
+                </v-col>
+
+              </v-row>
+            </v-container>
+
+          </template>
+
+          <template #form.button>
+            <NuxtLink :to="{ path: '/parameters/education'}" class="no-decoration">
+              <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                {{ $t('back') }}
+              </v-btn>
+            </NuxtLink>
+          </template>
+
+        </UiForm>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, computed, useContext} from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import { Query, Model } from '@vuex-orm/core'
+import { SUBMIT_TYPE} from '~/types/enums'
+import { repositoryHelper } from '~/services/store/repository'
+import {AnyJson} from "~/types/interfaces";
+import {EducationTiming} from "~/models/Education/EducationTiming";
+
+export default defineComponent({
+  props: {
+    id:{
+      type: [Number, String],
+      required: true
+    }
+  },
+  setup () {
+    const {app:{i18n}} = useContext()
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(EducationTiming)
+    const query: Query = repository.query()
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/parameters/education` }
+      actions[SUBMIT_TYPE.SAVE] = { path: `/parameters/education/education_timing/` }
+      return actions
+    })
+
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    return {
+      model: EducationTiming,
+      query: () => query,
+      rules: () => getRules(i18n),
+      panel: 0,
+      submitActions
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(EducationTiming)
+  }
+})
+
+function getRules (i18n: any) {
+  return {
+    timingRules: [
+      (timingValue: number) => (timingValue > 0) || i18n.t('value_need_to_be_bigger_than_0')
+    ]
+  }
+}
+
+</script>

+ 80 - 0
components/Form/Parameters/ResidenceArea.vue

@@ -0,0 +1,80 @@
+<!-- Component d'un formulaire d'une zone de résidence -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5 mt-4">
+        <FormToolbar title="residenceArea" icon="fa-globe-europe"/>
+
+        <UiForm
+          :id="id"
+          :model="model"
+          :query="query()"
+          :submitActions="submitActions">
+          <template #form.input="{entry, updateRepository}">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="label" label="title" :data="entry['label']" @update="updateRepository" />
+                </v-col>
+
+              </v-row>
+            </v-container>
+
+          </template>
+
+          <template #form.button>
+            <NuxtLink :to="{ path: '/parameters/billing'}" class="no-decoration">
+              <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                {{ $t('back') }}
+              </v-btn>
+            </NuxtLink>
+          </template>
+
+        </UiForm>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed} from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import { Query, Model } from '@vuex-orm/core'
+import { SUBMIT_TYPE} from '~/types/enums'
+import { repositoryHelper } from '~/services/store/repository'
+import {AnyJson} from "~/types/interfaces";
+import {ResidenceArea} from "~/models/Billing/ResidenceArea";
+
+export default defineComponent({
+  props: {
+    id:{
+      type: [Number, String],
+      required: true
+    }
+  },
+  setup () {
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(ResidenceArea)
+    const query: Query = repository.query()
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/parameters/billing` }
+      actions[SUBMIT_TYPE.SAVE] = { path: `/parameters/billing/residence_area/` }
+      return actions
+    })
+
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    return {
+      model: ResidenceArea,
+      query: () => query,
+      panel: 0,
+      submitActions
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(ResidenceArea)
+  }
+})
+
+</script>

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

@@ -14,10 +14,10 @@
   >
     <ul v-if="alert.messages.length > 1">
        <li v-for="message in alert.messages">
-        {{ message }}
+        {{ $t(message) }}
       </li>
     </ul>
-    <span v-else>{{alert.messages[0]}}</span>
+    <span v-else>{{$t(alert.messages[0])}}</span>
   </v-alert>
 </template>
 

+ 2 - 2
components/Layout/Header.vue

@@ -14,14 +14,14 @@ et aux préférences de l'utilisateur
   >
     <v-btn
       v-if="displayedMiniVariant"
-      class="menu-btn d-none d-sm-none d-md-flex" icon @click.stop="displayedMiniMenu()"
+      class="menu-btn d-none d-sm-none d-sm-none d-md-none d-lg-flex" icon @click.stop="displayedMiniMenu()"
     >
       <v-icon class="ot_white--text">
         mdi-menu{{ `${properties.miniVariant ? '' : '-open'}` }}
       </v-icon>
     </v-btn>
 
-    <v-btn class="menu-btn d-sm-flex d-md-none" icon @click.stop="displayedMenu()">
+    <v-btn class="menu-btn d-sm-flex d-md-flex d-lg-none" icon @click.stop="displayedMenu()">
       <v-icon class="ot_white--text">
         mdi-menu
       </v-icon>

+ 17 - 3
components/Layout/Menu.vue

@@ -12,6 +12,10 @@ Prend en paramètre une liste de ItemMenu et les met en forme
     class="ot_dark_grey ot_menu_color--text"
     app
   >
+    <template #prepend>
+      <slot name="title"></slot>
+    </template>
+
     <v-list class="left-menu">
       <div v-for="(item, i) in menu" :key="i">
         <v-list-item
@@ -71,7 +75,12 @@ Prend en paramètre une liste de ItemMenu et les met en forme
         </v-list-group>
       </div>
     </v-list>
+
+    <template #append>
+      <slot name="foot"></slot>
+    </template>
   </v-navigation-drawer>
+
 </template>
 
 <script lang="ts">
@@ -96,13 +105,18 @@ export default defineComponent({
   },
   setup(props){
     const {openMenu} = toRefs(props)
-    const open = ref(false)
+    const open = ref(true)
+
+    //Par défaut si l'écran est trop petit au chargement de la page, le menu doit être fermé.
+    if(process.client)
+      open.value = window.innerWidth >= 1264
+
     const unwatch: WatchStopHandle = watch(openMenu, (newValue, oldValue) => {
+      if(newValue !== oldValue)
       open.value = true
     })
-
     onUnmounted(() => {
-      unwatch()
+     unwatch()
     })
 
     return {

+ 3 - 5
components/Layout/Subheader.vue

@@ -29,17 +29,15 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 </template>
 
 <script lang="ts">
-import {computed, ComputedRef, defineComponent, ref, Ref, useContext} from '@nuxtjs/composition-api'
+import {defineComponent, ref, Ref, useContext} from '@nuxtjs/composition-api'
+import {UseAccess} from "~/composables/utils/useAccess";
 
 export default defineComponent({
   setup () {
     const {store} = useContext()
+    const {hasMenuOrIsTeacher} = UseAccess(store)
     const showDateTimeRange: Ref<boolean> = ref(false)
 
-    const hasMenuOrIsTeacher: ComputedRef<boolean> = computed(
-      () => store.state.profile.access.hasLateralMenu || store.state.profile.access.isTeacher
-    )
-
     return {
       showDateTimeRange,
       hasMenuOrIsTeacher

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

@@ -44,14 +44,14 @@ export default defineComponent({
     }
   },
   setup (props) {
-    const { $dataDeleter, store, app: { i18n } } = useContext()
+    const { $dataDeleter, store } = useContext()
     const showDialog: Ref<boolean> = ref(false)
     const page = new Page(store)
 
     const deleteItem = async () => {
       try {
         await $dataDeleter.invoke(props.deleteArgs)
-        page.addAlerts(TYPE_ALERT.SUCCESS, [i18n.t('deleteSuccess') as string])
+        page.addAlerts(TYPE_ALERT.SUCCESS, ['deleteSuccess'])
       } catch (error) {
         page.addAlerts(TYPE_ALERT.ALERT, [error.message])
       }

+ 6 - 1
components/Ui/Card.vue

@@ -22,7 +22,7 @@ Container de type Card
           <v-icon>mdi-pencil</v-icon>
         </NuxtLink>
       </v-btn>
-      <UiButtonDelete :delete-args="args" />
+      <UiButtonDelete v-if="canDelete" :delete-args="args" />
       <slot name="card.action" />
     </v-card-actions>
   </v-card>
@@ -46,6 +46,11 @@ export default defineComponent({
     id: {
       type: Number,
       required: true
+    },
+    canDelete:{
+      type: Boolean,
+      required: false,
+      default: true
     }
   },
   setup (props) {

+ 9 - 15
components/Ui/SubResource.vue → components/Ui/Collection.vue

@@ -3,7 +3,7 @@
 <template>
   <main>
     <v-skeleton-loader
-      v-if="$fetchState.pending"
+      v-if="fetchState.pending"
       :type="loaderType"
     />
     <div v-else>
@@ -22,22 +22,22 @@
 
 <script lang="ts">
 import {
-  defineComponent, computed, useContext, useFetch, toRefs, ToRefs, ComputedRef
+  defineComponent, computed, useContext, toRefs, ToRefs, ComputedRef
 } from '@nuxtjs/composition-api'
 import { Query } from '@vuex-orm/core'
 import { Collection } from '@vuex-orm/core/dist/src/data/Data'
 import { queryHelper } from '~/services/store/query'
-import { QUERY_TYPE } from '~/types/enums'
+import {useDataUtils} from "~/composables/data/useDataUtils";
 
 export default defineComponent({
   props: {
     rootModel: {
       type: Function,
-      required: true
+      required: false
     },
     rootId: {
       type: Number,
-      required: true
+      required: false
     },
     model: {
       type: Function,
@@ -60,19 +60,13 @@ export default defineComponent({
   setup (props) {
     const { rootModel, rootId, model, query }: ToRefs = toRefs(props)
     const { $dataProvider } = useContext()
-    useFetch(async () => {
-      await $dataProvider.invoke({
-        type: QUERY_TYPE.MODEL,
-        model: model.value,
-        rootModel: rootModel.value,
-        rootId: rootId.value
-      })
-    })
-
+    const { getCollection } = useDataUtils($dataProvider)
+    const {fetchState} = getCollection(model.value, rootModel.value, rootId.value)
     const items: ComputedRef<Collection> = computed(() => queryHelper.getCollection(query.value))
 
     return {
-      items
+      items,
+      fetchState
     }
   }
 })

+ 10 - 8
components/Ui/Form.vue

@@ -68,7 +68,7 @@ Formulaire générique
 </template>
 
 <script lang="ts">
-import {computed, ComputedRef, defineComponent, ref, Ref, toRefs, ToRefs, useContext} from '@nuxtjs/composition-api'
+import {computed, ComputedRef, defineComponent, ref, Ref, toRefs, ToRefs, useContext, useRouter} from '@nuxtjs/composition-api'
 import {Query} from '@vuex-orm/core'
 import {repositoryHelper} from '~/services/store/repository'
 import {queryHelper} from '~/services/store/query'
@@ -104,7 +104,8 @@ export default defineComponent({
     }
   },
   setup(props) {
-    const {$dataPersister, store, app: {router, i18n}} = useContext()
+    const {$dataPersister, store, app: {i18n}} = useContext()
+    const router = useRouter()
     const {markAsDirty, markAsNotDirty, readonly, nextStepFactory} = useForm(store)
     const {id, query}: ToRefs = toRefs(props)
     const page = new Page(store)
@@ -132,25 +133,26 @@ export default defineComponent({
             query: props.query
           })
 
-          page.addAlerts(TYPE_ALERT.SUCCESS, [i18n.t('saveSuccess') as string])
+          page.addAlerts(TYPE_ALERT.SUCCESS, ['saveSuccess'])
           nextStep(next, response.data)
-        } catch (error) {
+        } catch (error: any) {
+
           if (error.response.status === 422) {
             if(error.response.data['violations']){
               const violations:Array<string> = []
-              const fields:Array<string> = []
+              let fields:AnyJson = {}
               for(const violation of error.response.data['violations']){
                 violations.push(i18n.t(violation['message']) as string)
-                fields.push(violation['propertyPath'])
+                fields = Object.assign(fields, {[violation['propertyPath']] : violation['message']})
               }
 
               new Form(store).addViolations(fields)
-              page.addAlerts(TYPE_ALERT.ALERT, violations)
+              page.addAlerts(TYPE_ALERT.ALERT, ['invalid_form'])
             }
           }
         }
       }else{
-        page.addAlerts(TYPE_ALERT.ALERT, [i18n.t('invalide_form') as string])
+        page.addAlerts(TYPE_ALERT.ALERT, ['invalid_form'])
       }
     }
 

+ 18 - 8
components/Ui/Input/Autocomplete.vue

@@ -21,9 +21,10 @@ Liste déroulante avec autocompletion
       :return-object="returnObject"
       :search-input.sync="search"
       :prepend-icon="prependIcon"
-      :error="error"
+      :error="error || !!violation"
+      :error-messages="errorMessage || violation ? $t(violation) : ''"
       :rules="rules"
-      :menu-props="menuProps"
+      :chips="chips"
       @input="onChange($event)"
     >
       <template v-if="slotText" #item="data">
@@ -107,20 +108,29 @@ export default defineComponent({
       type: Boolean,
       default: false
     },
-    menuProps: {
-      type: Object,
-      default: false
-    },
     rules: {
       type: Array,
       required: false,
       default: () => []
     },
+    chips: {
+      type: Boolean,
+      default: false
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
+    }
   },
   setup (props, { emit }) {
     const {app:{i18n}, store} = useContext()
     const search:Ref<string|null> = ref(null)
-    const {error, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     // On reconstruit les items à afficher...
     const itemsToDisplayed: ComputedRef<Array<AnyJson>> = computed(() => {
@@ -199,7 +209,7 @@ export default defineComponent({
       label_field: props.label ?? props.field,
       itemsToDisplayed,
       search,
-      error,
+      violation,
       onChange
     }
   }

+ 55 - 7
components/Ui/Input/AutocompleteWithAPI.vue

@@ -9,12 +9,14 @@ d'une api)
     <UiInputAutocomplete
       :field="field"
       :label="label"
-      :data="data"
+      :data="remoteData ? remoteData : data"
       :items="items"
       :isLoading="isLoading"
       :item-text="itemText"
       :slotText="slotText"
       :item-value="itemValue"
+      :multiple="multiple"
+      :chips="chips"
       prependIcon="mdi-magnify"
       :return-object="returnObject"
       @research="search"
@@ -25,7 +27,9 @@ d'une api)
 </template>
 
 <script lang="ts">
-import {defineComponent, ref, Ref, watch, onUnmounted, toRefs} from '@nuxtjs/composition-api'
+import {defineComponent, ref, Ref, watch, onUnmounted, toRefs, useContext, useFetch} from '@nuxtjs/composition-api'
+import {QUERY_TYPE} from "~/types/enums";
+import ModelsUtils from "~/services/utils/modelsUtils";
 
 export default defineComponent({
   props: {
@@ -48,6 +52,16 @@ export default defineComponent({
       required: false,
       default: null
     },
+    remoteUri: {
+      type: [Array],
+      required: false,
+      default: null
+    },
+    remoteUrl: {
+      type: String,
+      required: false,
+      default: null
+    },
     readonly: {
       type: Boolean,
       required: false
@@ -71,24 +85,57 @@ export default defineComponent({
     noFilter: {
       type: Boolean,
       default: false
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    chips: {
+      type: Boolean,
+      default: false
     }
   },
   setup(props) {
     const {data} = toRefs(props)
-    const items:Ref<Array<any>> = ref([data.value])
+    const items = ref([])
+    const remoteData:Ref<Array<string> | null> = ref(null)
     const isLoading = ref(false)
+    const {$dataProvider} = useContext()
+
+    if(props.data){
+      items.value = props.multiple ? (data.value ?? []) : [data.value]
+    }else if(props.remoteUri){
+      const ids:Array<any> = []
+      for(const uri of props.remoteUri){
+        ids.push(ModelsUtils.extractIdFromUri(uri as string))
+      }
+
+      useFetch(async () => {
+        isLoading.value = true
+        const r = await $dataProvider.invoke({
+          type: QUERY_TYPE.DEFAULT,
+          url: props.remoteUrl,
+          listArgs: {
+            filters:[
+              {key: 'id', value: ids.join(',')}
+            ]
+          }
+        })
+        isLoading.value = false
+        remoteData.value = r.data
+        items.value = r.data
+      })
+    }
 
     const search = async (research:string) => {
       isLoading.value = true
-
       const func: Function = props.searchFunction
-      items.value = await func(research, props.field)
-
+      items.value = items.value.concat(await func(research, props.field))
       isLoading.value = false
     }
 
     const unwatch = watch(data,(d) => {
-      items.value = [d]
+      items.value = props.multiple ? d : [d]
     })
 
     onUnmounted(() => {
@@ -99,6 +146,7 @@ export default defineComponent({
       label_field: props.label ?? props.field,
       isLoading,
       items,
+      remoteData,
       search
     }
   }

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

@@ -14,7 +14,8 @@ Case à cocher
       :value="data"
       :label="$t(label_field)"
       :disabled="readonly"
-      :error="error"
+      :error="error || !!violation"
+      :error-messages="errorMessage || violation ? $t(violation) : ''"
       @change="onChange($event)"
     />
   </v-container>
@@ -43,15 +44,24 @@ export default defineComponent({
     readonly: {
       type: Boolean,
       required: false
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
     }
   },
   setup (props, {emit}) {
     const {store} = useContext()
-    const {error, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     return {
       label_field: props.label ?? props.field,
-      error,
+      violation,
       onChange
     }
   }

+ 19 - 4
components/Ui/Input/DatePicker.vue

@@ -25,7 +25,8 @@ Sélecteur de dates
           :dense="dense"
           :single-line="singleLine"
           v-on="on"
-          :error="error"
+          :error="error || !!violation"
+          :error-messages="errorMessage || violation ? $t(violation) : ''"
         />
       </template>
       <v-date-picker
@@ -76,12 +77,26 @@ export default defineComponent({
     singleLine: {
       type: Boolean,
       required: false
+    },
+    format: {
+      type: String,
+      required: false,
+      default: 'DD/MM/YYYY'
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
     }
   },
   setup (props, { emit }) {
     const { data, range } = props
     const { $moment, store } = useContext()
-    const {error, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
     const dateUtils = new DatesUtils($moment)
 
     const datesParsed: Ref<Array<string>|string|null> = range ? ref(Array<string>()) : ref(null)
@@ -96,7 +111,7 @@ export default defineComponent({
 
     const datesFormatted: ComputedRef<string|null> = computed(() => {
       if (props.range && datesParsed.value && datesParsed.value.length < 2) { return null }
-      return datesParsed.value ? dateUtils.formattedDate(datesParsed.value, 'DD/MM/YYYY') :  null
+      return datesParsed.value ? dateUtils.formattedDate(datesParsed.value, props.format) :  null
     })
 
     const unwatch: WatchStopHandle = watch(datesParsed, (newValue, oldValue) => {
@@ -114,7 +129,7 @@ export default defineComponent({
       datesParsed,
       datesFormatted,
       dateOpen: ref(false),
-      error
+      violation
     }
   }
 })

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

@@ -5,9 +5,10 @@ Champs de saisie de type Text dédié à la saisie d'emails
 <template>
   <UiInputText
     :data="data"
-    :label="label"
+    :label="$t(label_field)"
     :readonly="readonly"
-    :error="error"
+    :error="error || !!violation"
+    :error-messages="errorMessage || violation ? $t(violation) : ''"
     :rules="rules"
     @update="onChange"
   />
@@ -22,7 +23,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     field: {
       type: String,
@@ -43,11 +44,20 @@ export default defineComponent({
       type: Boolean,
       required: false,
       default: false
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
     }
   },
   setup (props, {emit}) {
     const { app: { i18n }, store } = useContext()
-    const {error, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     const rules = [
       (email: string) => validEmail(email) || i18n.t('email_error')
@@ -60,8 +70,9 @@ export default defineComponent({
     }
 
     return {
+      label_field: props.label ?? props.field,
       rules,
-      error,
+      violation,
       onChange
     }
   }

+ 13 - 3
components/Ui/Input/Enum.vue

@@ -21,7 +21,8 @@ Liste déroulante dédiée à l'affichage d'objets Enum
       item-value="value"
       :rules="rules"
       :disabled="readonly"
-      :error="error"
+      :error="error || !!violation"
+      :error-messages="errorMessage || violation ? $t(violation) : ''"
       @change="onChange($event)"
     />
   </main>
@@ -62,6 +63,15 @@ export default defineComponent({
       type: Array,
       required: false,
       default: () => []
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
     }
   },
   setup (props, {emit}) {
@@ -69,7 +79,7 @@ export default defineComponent({
 
     const { enumType } = props
     const { $dataProvider, store } = useContext()
-    const {error, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     const items: Ref<Array<EnumChoices>> = ref([])
     useFetch(async () => {
@@ -83,7 +93,7 @@ export default defineComponent({
     return {
       items,
       label_field: labelField,
-      error,
+      violation,
       onChange
     }
   }

+ 13 - 3
components/Ui/Input/Phone.vue

@@ -7,7 +7,8 @@ Champs de saisie d'un numéro de téléphone
 <template>
   <client-only>
     <vue-tel-input-vuetify
-      :error="error"
+      :error="error || !!violation"
+      :error-messages="errorMessage || violation ? $t(violation) : ''"
       :field="field"
       :label="label"
       v-model="myPhone"
@@ -46,11 +47,20 @@ export default defineComponent({
     readonly: {
       type: Boolean,
       required: false
+    },
+    error: {
+      type: Boolean,
+      required: false
+    },
+    errorMessage: {
+      type: String,
+      required: false,
+      default: null
     }
   },
   setup (props, {emit}) {
     const { app: { i18n }, store } = useContext()
-    const {error, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     const nationalNumber: Ref<string | number> = ref('')
     const internationalNumber: Ref<string | number> = ref('')
@@ -83,7 +93,7 @@ export default defineComponent({
 
     return {
       myPhone,
-      error,
+      violation,
       onInput,
       onChangeValue,
       rules: [

+ 10 - 6
components/Ui/Input/Text.vue

@@ -11,18 +11,21 @@ Champs de saisie de texte
       :label="$t(label_field)"
       :rules="rules"
       :disabled="readonly"
-      :type="type"
-      :error="error || violations"
-      :error-messages="errorMessage"
+      :type="type === 'password' ? (show ? 'text' : type) : type"
+      :error="error || !!violation"
+      :error-messages="errorMessage || violation ? $t(violation) : ''"
       @change="onChange($event)"
       v-mask="mask"
+      :append-icon="type === 'password' ? (show ? 'mdi-eye' : 'mdi-eye-off') : ''"
+      @click:append="show = !show"
     />
 </template>
 
 <script lang="ts">
-import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {defineComponent, ref, useContext} from '@nuxtjs/composition-api'
 import {useError} from "~/composables/form/useError";
 import {mask} from 'vue-the-mask';
+import {Num} from "@vuex-orm/core";
 
 export default defineComponent({
   props: {
@@ -72,11 +75,12 @@ export default defineComponent({
   },
   setup (props, {emit}) {
     const {store} = useContext()
-    const {error: violations, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     return {
       label_field: props.label ?? props.field,
-      violations,
+      violation,
+      show: ref(false),
       onChange
     }
   },

+ 4 - 4
components/Ui/Input/TextArea.vue

@@ -11,8 +11,8 @@ Champs de saisie de bloc texte
       :label="$t(label_field)"
       :rules="rules"
       :disabled="readonly"
-      :error="error || violations"
-      :error-messages="errorMessage"
+      :error="error || !!violation"
+      :error-messages="errorMessage || violation ? $t(violation) : ''"
       @change="onChange($event)"
     />
 </template>
@@ -59,11 +59,11 @@ export default defineComponent({
   },
   setup (props, {emit}) {
     const {store} = useContext()
-    const {error: violations, onChange} = useError(props.field, emit, store)
+    const {violation, onChange} = useError(props.field, emit, store)
 
     return {
       label_field: props.label ?? props.field,
-      violations,
+      violation,
       onChange
     }
   }

+ 1 - 1
components/Ui/Map.vue

@@ -66,7 +66,7 @@ export default defineComponent({
         address.value.longitude = data[0].longitude
         emit('updateAddress', address.value)
       } else {
-        new Page(store).addAlerts(TYPE_ALERT.ALERT, [i18n.t('no_coordinate_corresponding') as string])
+        new Page(store).addAlerts(TYPE_ALERT.ALERT, ['no_coordinate_corresponding'])
       }
     }
 

+ 25 - 6
composables/data/useDataUtils.ts

@@ -12,14 +12,32 @@ import {Store} from "vuex";
  * Composable Classe qui va récupérer les Accesses suivant des critères de recherche
  */
 export function useDataUtils($dataProvider: DataProvider){
+  /**
+   * recherche la collection d'item et alimente le dataprovider
+   * @param model
+   * @param rootModel
+   * @param rootId
+   */
+  function getCollection(model: typeof Model, rootModel?: typeof Model, rootId?: number){
+    const {fetchState} = useFetch(async () => {
+      await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: model,
+        rootModel: rootModel,
+        rootId: rootId
+      })
+    })
+    return {
+      fetchState
+    }
+  }
 
   /**
    * recherche l'item a éditer et alimente le dataprovider
-   * @param route
+   * @param id
    * @param model
    */
-  function getItemToEdit(route: Ref, model: typeof Model){
-    const id = parseInt(route.value.params.id)
+  function getItemToEdit(id: string|number, model: typeof Model){
     if(!id){
       throw new Error('id must be exist')
     }
@@ -28,12 +46,12 @@ export function useDataUtils($dataProvider: DataProvider){
       await $dataProvider.invoke({
         type: QUERY_TYPE.MODEL,
         model,
-        id
+        id,
+        showProgress: process.browser
       })
     })
     return {
-      fetchState,
-      id
+      fetchState
     }
   }
 
@@ -59,6 +77,7 @@ export function useDataUtils($dataProvider: DataProvider){
   }
 
   return {
+    getCollection,
     getItemToEdit,
     createItem
   }

+ 5 - 5
composables/form/useError.ts

@@ -1,5 +1,6 @@
 import { AnyStore } from '~/types/interfaces'
 import {computed, ComputedRef} from "@nuxtjs/composition-api";
+import * as _ from 'lodash'
 
 /**
  * @category composables/form
@@ -9,8 +10,8 @@ import {computed, ComputedRef} from "@nuxtjs/composition-api";
  * @param store
  */
 export function useError(field: string, emit: any, store: AnyStore){
-  const error:ComputedRef<Boolean> = computed(()=>{
-    return store.state.form.violations.indexOf(field) >= 0
+  const violation:ComputedRef<string> = computed(()=>{
+    return _.get(store.state.form.violations, field, '')
   })
 
   /**
@@ -20,13 +21,12 @@ export function useError(field: string, emit: any, store: AnyStore){
    * @param changeField
    */
   function onChange (emit:any, fieldValue:any, changeField:string) {
-      const errors = store.state.form.violations.filter((field:string) => field !== changeField)
-      store.commit('form/setViolations', errors)
+      store.commit('form/setViolations', _.omit(store.state.form.violations, changeField))
       emit('update', fieldValue, changeField)
   }
 
   return {
     onChange: (fieldValue:any) => onChange(emit, fieldValue, field),
-    error
+    violation
   }
 }

+ 5 - 1
composables/layout/Menus/baseMenu.ts

@@ -1,5 +1,5 @@
 import { NuxtConfig } from '@nuxt/types/config'
-import {IconItem, ItemMenu} from '~/types/interfaces'
+import {ItemMenu, ItemsMenu, IconItem} from '~/types/interfaces'
 
 /**
  * @category composables/layout/Menus
@@ -45,6 +45,10 @@ class BaseMenu {
     return null
   }
 
+  getMenus (): ItemsMenu | null {
+    return null
+  }
+
   getHeaderMenu (): ItemMenu | null {
     return null
   }

+ 4 - 2
composables/layout/Menus/configurationMenu.ts

@@ -30,7 +30,8 @@ class ConfigurationMenu extends BaseMenu implements Menu {
     const children: ItemsMenu = []
 
     if (this.$ability.can('display', 'organization_page')) {
-      children.push(this.constructMenu('organization_page', undefined, `/main/organizations/${this.$store.state.profile.organization.id}/dashboard`, true))
+      // children.push(this.constructMenu('organization_page', undefined, `/main/organizations/${this.$store.state.profile.organization.id}/dashboard`, true))
+      children.push(this.constructMenu('organization_page', undefined, `/organization`, false))
     }
 
     if (this.$ability.can('display', 'cmf_licence_page')) {
@@ -38,7 +39,8 @@ class ConfigurationMenu extends BaseMenu implements Menu {
     }
 
     if (this.$ability.can('display', 'parameters_page')) {
-      children.push(this.constructMenu('parameters', undefined,`/main/edit/parameters/${this.$store.state.profile.organization.id}`, true))
+      // children.push(this.constructMenu('parameters', undefined,`/main/edit/parameters/${this.$store.state.profile.organization.id}`, true))
+      children.push(this.constructMenu('parameters', undefined,`/parameters`, false))
     }
 
     if (this.$ability.can('display', 'place_page')) {

+ 59 - 0
composables/layout/Menus/parametersMenu.ts

@@ -0,0 +1,59 @@
+import { Ability } from '@casl/ability'
+import { NuxtConfig } from '@nuxt/types/config'
+import { AnyStore, ItemMenu, ItemsMenu, Menu } from '~/types/interfaces'
+import BaseMenu from '~/composables/layout/Menus/baseMenu'
+
+/**
+ * @category composables/layout/Menus
+ * @class ConfigurationMenu
+ * Classe pour la construction du Menu Paramètres
+ */
+class ParametersMenu extends BaseMenu implements Menu {
+  private $ability: Ability;
+  private $store: AnyStore;
+
+  /**
+   * @constructor
+   * Initialisation des services issues du context
+   */
+  constructor ($config: NuxtConfig, $ability: Ability, $store: AnyStore) {
+    super($config)
+    this.$ability = $ability
+    this.$store = $store
+  }
+
+  /**
+   * Construit le menu Header Configuration ou null si aucune page accessible
+   * @return {ItemMenu | null}
+   */
+  getMenus (): ItemsMenu | null {
+    const children: ItemsMenu = []
+
+    if (this.$ability.can('display', 'parameters_page')) {
+      children.push(this.constructMenu('general_params', {name: 'fa-cogs'},`/parameters`, false))
+    }
+    if (this.$ability.can('display', 'parameters_communication_page')) {
+      children.push(this.constructMenu('communication_params', {name: 'fa-comments'},`/parameters/communication`, false))
+    }
+    if (this.$ability.can('display', 'parameters_student_page')) {
+      children.push(this.constructMenu('students_params', {name: 'fa-users'},`/parameters/student`, false))
+    }
+    if (this.$ability.can('display', 'parameters_education_page')) {
+      children.push(this.constructMenu('education_params', {name: 'fa-graduation-cap'},`/parameters/education`, false))
+    }
+    if (this.$ability.can('display', 'parameters_bills_page')) {
+      children.push(this.constructMenu('bills_params', {name: 'fa-euro-sign'},`/parameters/billing`, false))
+    }
+    if (this.$ability.can('display', 'parameters_secure_page')) {
+      children.push(this.constructMenu('secure_params', {name: 'fa-lock'},`/parameters/secure`, false))
+    }
+
+    if (children.length > 0) {
+      return children
+    } else {
+      return null
+    }
+  }
+}
+
+export const getParametersMenu = ($config: NuxtConfig, $ability: Ability, $store: AnyStore) => new ParametersMenu($config, $ability, $store).getMenus()

+ 13 - 0
composables/layout/menu.ts

@@ -17,6 +17,7 @@ import { getConfigurationMenu } from '~/composables/layout/Menus/configurationMe
 import { getMyFamilyMenu } from '~/composables/layout/Menus/myFamilyMenu'
 import { getMyAccessesMenu } from '~/composables/layout/Menus/myAccessesMenu'
 import { getAccountMenu } from '~/composables/layout/Menus/accountMenu'
+import {getParametersMenu} from "~/composables/layout/Menus/parametersMenu";
 
 /**
  * @category composables/layout
@@ -130,6 +131,18 @@ class Menu {
   useWebSiteMenuConstruct (): Ref {
     return ref(getWebsiteMenu(this.$config, this.$ability, this.$store).getHeaderMenu())
   }
+
+  /**
+   * Construit le menu Paramètres
+   */
+  useParametersMenuConstruct (): Ref {
+    const menu: ItemsMenu | null = getParametersMenu(this.$config, this.$ability, this.$store)
+
+    // Si l'utilisateur possède au moins un menu alors le menu latéral sera accessible
+    this.$store.commit('profile/access/setHasLateralMenu', true)
+
+    return ref(menu)
+  }
 }
 
 export const $useMenu = new Menu()

+ 12 - 0
composables/utils/useAccess.ts

@@ -0,0 +1,12 @@
+import {AccessStore} from "~/types/interfaces";
+import {computed, ComputedRef} from "@nuxtjs/composition-api";
+
+export function UseAccess(store: AccessStore){
+  const hasMenuOrIsTeacher: ComputedRef<boolean> = computed(
+    () => store.state.profile.access.hasLateralMenu || store.state.profile.access.isTeacher
+  )
+
+  return {
+    hasMenuOrIsTeacher
+  }
+}

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

@@ -23,6 +23,49 @@
       organization:
         - {function: hasModule, parameters: ['GeneralConfig']}
 
+  parameters_communication_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
+      organization:
+        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+
+  parameters_student_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
+      organization:
+        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+        - {function: isSchool}
+
+  parameters_education_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
+      organization:
+        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+        - { function: isSchool }
+
+  parameters_bills_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
+      organization:
+        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+        - { function: isSchool }
+
+  parameters_secure_page:
+    action: 'display'
+    services:
+      access:
+        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
+      organization:
+        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+
   place_page:
     action: 'display'
     services:

+ 2 - 1
config/nuxtConfig/env.js

@@ -8,7 +8,8 @@ export default {
     manager_product: 'manager',
     cmf_network: 'CMF',
     ffec_network: 'FFEC',
-    OPENTALENT_MANAGER_ID: 93931
+    OPENTALENT_MANAGER_ID: 93931,
+    CMF_ID: 12097
   },
   publicRuntimeConfig: {
     http: {

+ 5 - 0
lang/content/parameters/fr-FR.js

@@ -0,0 +1,5 @@
+export default (context, locale) => {
+  return ({
+    'help_super_admin': 'Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise surtout pour la gestion de votre site internet et, à la première connexion au logiciel, afin de créer des comptes pour tous membres de votre structure. Enfin, il peut également être utile en cas de dépannage dans certaines situations particulières.',
+  })
+}

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

@@ -1,5 +1,14 @@
 export default (context, locale) => {
   return ({
+    GUARDIANS: 'Tuteurs uniquement',
+    STUDENTS: 'Élèves uniquement',
+    STUDENTS_AND_THEIR_GUARDIANS: 'Élèves et leurs tuteurs',
+    ANNUAL: 'Annuel',
+    HALF: 'Semestriel',
+    QUARTERLY: 'Trimestriel',
+    MONTHLY: 'Mensuel',
+    BY_EDUCATION: 'Par enseignement',
+    BY_TEACHER: 'Par professeur',
     CATEGORY_ORCHESTRE: 'Orchestre',
     CATEGORY_AMBULATORY: 'Musique ambulatoire',
     CATEGORY_OTHER: 'Autres activités',

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

@@ -1,5 +1,52 @@
 export default (context, locale) => {
   return ({
+    parameters: 'Paramètres',
+    cycle: 'Cycle',
+    timing: 'Durée d\'un enseignement (en minutes)',
+    educationTiming: 'Durée d\'un enseignement (en minutes)',
+    superAdmin: 'Compte super-admin',
+    username: 'Login de connexion',
+    residenceArea: 'Zones de résidence',
+    desactivateOpentalentSiteWeb: 'Désactiver le site opentalent',
+    passwordSMS: 'Mot de passe SMS',
+    usernameSMS: 'Nom d\'utilisateur SMS',
+    smsSenderName: 'Personnaliser le nom de l\'expéditeur SMS',
+    attendance: 'Absences',
+    sendAttendanceEmail: 'Prévenir automatiquement la famille par mail en cas d\'absence non justifiée',
+    sendAttendanceSms: 'Prévenir automatiquement la famille par sms en cas d\'absence non justifiée',
+    bulletinReceiver: 'Adresser le bulletin à',
+    bulletinEditWithoutEvaluation: 'Editer également les bulletins ne contenant aucune évaluation',
+    bulletinShowAverages: 'Afficher les moyennes',
+    bulletinShowAbsences: 'Afficher les absences',
+    bulletinViewTestResults: 'Afficher les résultats des examens',
+    bulletinShowEducationWithoutEvaluation: 'Afficher les enseignements ne contenant aucune évaluation',
+    bulletinDisplayLevelAcquired: 'Affichage niveau acquis',
+    bulletinSignatureDirector: 'Un cadre « Tampon / Signature » pour l\'administration',
+    bulletinPrintAddress: 'L\'adresse postale de l\'élève ou son tuteur',
+    bulletinWithTeacher: 'Le nom du professeur',
+    bulletin_parameters: 'Bulletins',
+    sms: 'Sms',
+    web_parameters: 'Site internet',
+    averageMax: 'Note maximale pour les notes du suivi pédagogique (entre 1 et 100)',
+    educational_follow_up: 'Suivi pédagogique',
+    advancedEducationNotationType: 'Type de grilles d\'évaluation',
+    educationPeriodicity: 'Périodicité des évaluations',
+    editCriteriaNotationByAdminOnly: 'Autoriser uniquement l\'administration à modifier les critères d\'évaluation',
+    trackingValidation: 'Contrôle et validation du suivi pédagogique par l\'administration',
+    publicationDirectors: 'Directeur(s) de publication',
+    website: 'Autre site web',
+    newSubDomain: 'Nouveau sous domaine',
+    subDomainHistorical: 'Historique de vos sous domaine(s)',
+    otherWebsite: 'Votre site Opentalent est',
+    timezone: 'Fuseau horaire',
+    qrCode: 'QrCode pour la licence',
+    studentsAreAdherents: 'Les élèves sont également adhérents de l\'association',
+    showAdherentList: 'Afficher la liste des adhérents et leurs coordonnées',
+    endCourseDate: 'Date de fin des cours ',
+    startCourseDate: 'Date de début des cours ',
+    generalParams: 'Paramètres généraux',
+    financialDate: 'Début de la saison financière',
+    musicalDate: 'Début de la saison d\'activité',
     title: 'Titre',
     link: 'Lien',
     organizationArticle: 'Coups de projecteur',

+ 3 - 1
lang/fr-FR.js

@@ -7,6 +7,7 @@ import breadcrumbs from '@/lang/breadcrumbs/fr-FR'
 import menuKey from '@/lang/menuKey/fr-FR'
 import help from '@/lang/help/fr-FR'
 import contentSubscription from '@/lang/content/subscription/fr-FR'
+import contentParameters from '@/lang/content/parameters/fr-FR'
 
 export default (context, locale) => {
   return {
@@ -18,6 +19,7 @@ export default (context, locale) => {
     ...breadcrumbs(context, locale),
     ...menuKey(context, locale),
     ...help(context, locale),
-    ...contentSubscription(context, locale)
+    ...contentSubscription(context, locale),
+    ...contentParameters(context, locale),
   }
 }

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

@@ -7,6 +7,8 @@
  */
 export default (context, locale) => {
   return ({
+    bulletinEditWithoutEvaluationHelp:'Dans le cas où la case n\'est pas cochée, s\'il n\'y a aucune évaluation dans le bulletin, alors le bulletin n\'est pas exporté',
+    bulletinShowEducationWithoutEvaluationHelp: 'Dans le cas où la case est cochée, alors on affiche le texte "Aucune évaluation" pour chaque enseignement sans évaluation',
     type_of_practices_autocomplete: 'Sélectionnez parmi la liste des types de pratiques, une ou plusieurs pratiques correspondant aux activités de votre structure',
     logo_upload: '<div>Le logo est utilisée: </div>' +
       `\n<ul>` +
@@ -15,7 +17,7 @@ export default (context, locale) => {
       '    <li>dans la recherche d\'une structure sur le site Opentalent ou sur le site d\'une fédération si vous êtes membre de la CMF ' +
       '        (voir <a target="_blank" href="https://fmfaucigny.opentalent.fr/presentation/societes-adherentes">exemple ici</a>)</li>' +
       '</ul>',
-    communication_image_upload: 'L\'image est utilisée\n' +
+    communication_image_upload: 'L\'image est utilisé\n' +
       'dans la description détaillée d\'une structure sur le site Opentalent ou ' +
       'sur le site d\'une fédération si vous êtes membre de la CMF ' +
       '(voir <a target="_blank" href="https://fmfaucigny.opentalent.fr/presentation/societes-adherentes">exemple ici</a>)'

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

@@ -1,5 +1,12 @@
 export default (context, locale) => {
   return ({
+    general_params:'Général',
+    communication_params:'Communication',
+    students_params:'Suivi des étudiants',
+    education_params:'Enseignements',
+    bills_params:'Facturation',
+    secure_params:'Sécurité',
+    back_to_dashboard:'Quittez les paramètres',
     universal_create_title_access: 'Quel type de contact souhaitez-vous créer ?',
     universal_create_title_event: 'Que souhaitez-vous ajouter à votre planning ?',
     universal_create_title_message: 'Que souhaitez-vous envoyer ?',

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

@@ -1,8 +1,14 @@
 export default (context, locale) => {
   return ({
-    invalide_form: 'Formulaire invalide',
+    value_need_to_be_bigger_than_0: 'La valeur doit être plus grande que 0',
+    forbidden: 'Vous ne possédez pas les droits nécessaires pour effectuer cette opération',
+    wrong_mobyt_credentials: 'Identifiants SMS incorrects',
+    smsSenderName_error: 'Seuls les caractères alphanumériques sont permis, sans espaces, sans accent et sans caractères spéciaux',
+    between_1_and_10: 'La valeur doit être comprise entre 0 et 10',
+    between_0_and_100: 'La valeur doit être comprise entre 0 et 10',
+    invalid_form: 'Formulaire invalide',
     required: 'Ce champs est obligatoire',
-    name_length_rule: 'La taille du nom doit être de moins de 128 caractères',
+    name_length_rule: 'La longueur du nom doit être de moins de 128 caractères',
     siret_error: 'N° de siret non valide',
     email_error: 'Adresse email invalide',
     phone_error: 'Numéro de téléphone invalide',

+ 1 - 1
layouts/default.vue

@@ -37,7 +37,7 @@ export default defineComponent({
     const properties = reactive({
       clipped: false,
       miniVariant: false,
-      openMenu: false
+      openMenu: true
     })
 
     const displayedMenu: ComputedRef<boolean> = computed(() => store.state.profile.access.hasLateralMenu)

Plik diff jest za duży
+ 81 - 0
layouts/parameters.vue


+ 19 - 0
models/Access/Access.ts

@@ -0,0 +1,19 @@
+import {Attr, Num, Model, Uid, HasOne} from '@vuex-orm/core'
+import { Historical } from '~/types/interfaces'
+import {Person} from "~/models/Person/Person";
+
+export class Access extends Model {
+  static entity = 'accesses'
+
+  @Uid()
+  id!: number | string | null
+
+  @HasOne(() => Person, 'accessId')
+  person!: Person | null
+
+  @Num(0, { nullable: true })
+  activityYear!: number
+
+  @Attr({})
+  historical!: Historical
+}

+ 14 - 0
models/Access/AdminAccess.ts

@@ -0,0 +1,14 @@
+import {Model, Str, Uid} from "@vuex-orm/core";
+
+export class AdminAccess extends Model {
+  static entity = 'admin'
+
+  @Uid()
+  id!: number
+
+  @Str(null, { nullable: true })
+  username!: string|null
+
+  @Str(null, { nullable: true })
+  email!: string|null
+}

+ 11 - 0
models/Billing/ResidenceArea.ts

@@ -0,0 +1,11 @@
+import {Str, Model, Uid} from '@vuex-orm/core'
+
+export class ResidenceArea extends Model {
+  static entity = 'residence_areas'
+
+  @Uid()
+  id!: number | string | null
+
+  @Str(null, { nullable: true })
+  label!: string|null
+}

+ 14 - 0
models/Education/Cycle.ts

@@ -0,0 +1,14 @@
+import {Str, Model, Uid, Num} from '@vuex-orm/core'
+
+export class Cycle extends Model {
+  static entity = 'cycles'
+
+  @Uid()
+  id!: number | string | null
+
+  @Str(null, { nullable: true })
+  label!: string|null
+
+  @Num(0)
+  order!: number
+}

+ 11 - 0
models/Education/EducationTiming.ts

@@ -0,0 +1,11 @@
+import {Num, Model, Uid} from '@vuex-orm/core'
+
+export class EducationTiming extends Model {
+  static entity = 'education_timings'
+
+  @Uid()
+  id!: number | string | null
+
+  @Num(null, { nullable: true })
+  timing!: number
+}

+ 116 - 0
models/Organization/Parameters.ts

@@ -0,0 +1,116 @@
+import {Str, Model, Uid, Bool, Num, Attr} from '@vuex-orm/core'
+
+export class Parameters extends Model {
+  static entity = 'parameters'
+
+  @Uid()
+  id!: number | string | null
+
+  @Str(null, { nullable: true })
+  financialDate!: string|null
+
+  @Str(null, { nullable: true })
+  musicalDate!: string|null
+
+  @Str(null, { nullable: true })
+  startCourseDate!: string|null
+
+  @Str(null, { nullable: true })
+  endCourseDate!: string|null
+
+  @Bool(false, { nullable: false })
+  trackingValidation!: boolean
+
+  @Num(20, { nullable: true })
+  average!: number
+
+  @Bool(true, { nullable: false })
+  editCriteriaNotationByAdminOnly!: boolean
+
+  @Str(null, { nullable: true })
+  smsSenderName!: string|null
+
+  @Bool(true, { nullable: false })
+  logoDonorsMove!: boolean
+
+  @Str(null, { nullable: true })
+  subDomain!: string|null
+
+  @Str(null, { nullable: true })
+  website!: string|null
+
+  @Str(null, { nullable: true })
+  otherWebsite!: string|null
+
+  @Bool(false, { nullable: false })
+  desactivateOpentalentSiteWeb!: boolean
+
+  @Attr([])
+  publicationDirectors!: []
+
+  @Str(null, { nullable: true })
+  bulletinPeriod!: string|null
+
+  @Bool(false, { nullable: false })
+  bulletinWithTeacher!: boolean
+
+  @Bool(false, { nullable: false })
+  bulletinPrintAddress!: boolean
+
+  @Bool(true, { nullable: false })
+  bulletinSignatureDirector!: boolean
+
+  @Bool(true, { nullable: false })
+  bulletinDisplayLevelAcquired!: boolean
+
+  @Bool(false, { nullable: false })
+  bulletinShowEducationWithoutEvaluation!: boolean
+
+  @Bool(false, { nullable: false })
+  bulletinViewTestResults!: boolean
+
+  @Bool(false, { nullable: false })
+  bulletinShowAbsences!: boolean
+
+  @Bool(true, { nullable: false })
+  bulletinShowAverages!: boolean
+
+  @Str(null, { nullable: true })
+  bulletinOutput!: string|null
+
+  @Bool(true, { nullable: false })
+  bulletinEditWithoutEvaluation!: boolean
+
+  @Str('STUDENTS_AND_THEIR_GUARDIANS', { nullable: true })
+  bulletinReceiver!: string|null
+
+  @Str(null, { nullable: true })
+  usernameSMS!: string|null
+
+  @Str(null, { nullable: true })
+  passwordSMS!: string|null
+
+  @Bool(true, { nullable: false })
+  showAdherentList!: boolean
+
+  @Bool(false, { nullable: false })
+  studentsAreAdherents!: boolean
+
+  @Str(null, { nullable: true })
+  qrCode!: string|null
+
+  @Str('Europe/Paris', { nullable: true })
+  timezone!: string|null
+
+  @Str('ANNUAL', { nullable: true })
+  educationPeriodicity!: string|null
+
+  @Str('BY_EDUCATION', { nullable: true })
+  advancedEducationNotationType!: string|null
+
+  @Bool(false, { nullable: false })
+  sendAttendanceEmail!: boolean
+
+  @Bool(false, { nullable: false })
+  sendAttendanceSms!: boolean
+}

+ 14 - 0
models/Person/Person.ts

@@ -0,0 +1,14 @@
+import {Model, Uid, Str, Attr} from '@vuex-orm/core'
+
+export class Person extends Model {
+  static entity = 'people'
+
+  @Uid()
+  id!: number | string | null
+
+  @Attr(null)
+  accessId!: number | null
+
+  @Str(null, { nullable: true })
+  username!: string|null
+}

+ 80 - 0
pages/organization.vue

@@ -0,0 +1,80 @@
+<!-- Page de détails de l'organization courante -->
+
+<template>
+  <LayoutContainer v-if="!fetchState.pending">
+    <!-- Définit le contenu des trois slots du header de la page -->
+    <LayoutBannerTop>
+      <template #block1>
+        {{ entry.name }}
+      </template>
+      <template #block2>
+        N°Siret : {{ entry.siretNumber }}
+      </template>
+      <template #block3>
+        {{ entry.description }}
+      </template>
+    </LayoutBannerTop>
+
+    <!-- Rend le contenu de la page -->
+    <NuxtChild />
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, defineComponent, useContext, ComputedRef} from '@nuxtjs/composition-api'
+import { Item, Query } from '@vuex-orm/core'
+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";
+import {TypeOfPractice} from "~/models/Organization/TypeOfPractice";
+import {Network} from "~/models/Network/Network";
+import {NetworkOrganization} from "~/models/Network/NetworkOrganization";
+import {AddressPostal} from "~/models/Core/AddressPostal";
+import {OrganizationArticle} from "~/models/Organization/OrganizationArticle";
+import {File} from "~/models/Core/File"
+import {useDataUtils} from "~/composables/data/useDataUtils";
+
+export default defineComponent({
+  name: 'Organization',
+  middleware({ $ability, redirect }) {
+    if(!$ability.can('display', 'organization_page'))
+      return redirect('/error')
+  },
+  setup () {
+    const {store, $dataProvider} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = store.state.profile.organization.id
+
+    const {fetchState} = getItemToEdit(id, Organization)
+
+    const repository = repositoryHelper.getRepository(Organization)
+    const query: ComputedRef<Query> = computed(() => repository.query())
+    const entry: ComputedRef<Item> = computed(() => {
+      return queryHelper.getItem(query.value, id)
+    })
+
+    return {
+      entry,
+      fetchState
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Organization)
+    repositoryHelper.cleanRepository(ContactPoint)
+    repositoryHelper.cleanRepository(BankAccount)
+    repositoryHelper.cleanRepository(OrganizationAddressPostal)
+    repositoryHelper.cleanRepository(AddressPostal)
+    repositoryHelper.cleanRepository(Country)
+    repositoryHelper.cleanRepository(TypeOfPractice)
+    repositoryHelper.cleanRepository(Network)
+    repositoryHelper.cleanRepository(NetworkOrganization)
+    repositoryHelper.cleanRepository(OrganizationArticle)
+    repositoryHelper.cleanRepository(File)
+  }
+})
+
+</script>

+ 26 - 0
pages/organization/address/_id.vue

@@ -0,0 +1,26 @@
+<!-- Page de détails d'une adresse postale -->
+<template>
+  <main>
+    <FormOrganizationAddress :id="id" v-if="!fetchState.pending"></FormOrganizationAddress>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import { OrganizationAddressPostal } from '~/models/Organization/OrganizationAddressPostal'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+
+export default defineComponent({
+  name: 'EditOrganizationAddressEdit',
+  setup () {
+    const {$dataProvider, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, OrganizationAddressPostal)
+    return {
+      id,
+      fetchState
+    }
+  }
+})
+</script>

+ 30 - 0
pages/organization/bank_account/_id.vue

@@ -0,0 +1,30 @@
+<!-- Page de détails d'un IBAN (Edit mode) -->
+
+<template>
+  <main>
+    <FormOrganizationBankAccount :id="id" v-if="!fetchState.pending"></FormOrganizationBankAccount>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {BankAccount} from "~/models/Core/BankAccount";
+import {OrganizationAddressPostal} from "~/models/Organization/OrganizationAddressPostal";
+
+export default defineComponent({
+  name: 'EditBankAccount',
+  setup () {
+    const {$dataProvider, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, BankAccount)
+    return {
+      id,
+      fetchState
+    }
+  }
+})
+</script>
+<style>
+</style>

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

@@ -0,0 +1,31 @@
+
+<!-- Page de détails d'un point de contact (Edit mode) -->
+
+<template>
+  <main>
+    <FormOrganizationContactPoint :id="id" v-if="!fetchState.pending"></FormOrganizationContactPoint>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import { ContactPoint } from '~/models/Core/ContactPoint'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {BankAccount} from "~/models/Core/BankAccount";
+
+export default defineComponent({
+  name: 'EditContactPoint',
+  setup () {
+    const {$dataProvider, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, ContactPoint)
+    return {
+      id,
+      fetchState
+    }
+  }
+})
+</script>
+<style>
+</style>

+ 551 - 0
pages/organization/index.vue

@@ -0,0 +1,551 @@
+<!--
+Contenu de la page pages/organization.vue
+Contient toutes les informations sur l'organization courante
+-->
+<template>
+  <LayoutContainer>
+    <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']" @update="updateRepository" :rules="rules().nameRules" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="acronym" :data="entry['acronym']" @update="updateRepository" />
+                </v-col>
+
+                <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 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']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <div>
+                    <span>{{ $t('logo') }}</span>
+                    <UiHelp right>
+                      <p v-html="$t('logo_upload')" />
+                    </UiHelp>
+                  </div>
+                  <UiImage
+                    :id="getIdFromUri(entry['logo'])"
+                    :upload="true"
+                    :width="200"
+                    field="logo"
+                    :ownerId="id"
+                    @update="updateRepository"
+                  ></UiImage>
+                </v-col>
+
+                <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 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 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 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-col cols="12" sm="6" v-if="organizationProfile.isCmf()">
+                  <div class="d-flex flex-row">
+                    <UiInputAutocomplete
+                      field="typeOfPractices"
+                      :items="typeOfPractices"
+                      :isLoading="typeOfPracticesFetchingState.pending"
+                      :item-text="['name']"
+                      :data="getIdsFromUris(entry['typeOfPractices'])"
+                      :translate="true"
+                      :multiple="true"
+                      group="category"
+                      :rules="rules().typeOfPractice"
+                      @update="updateRepository($event.map((id) => `/api/type_of_practices/${id}`), 'typeOfPractices')"
+                      class="flex"
+                    />
+                    <UiHelp>
+                      {{ $t('type_of_practices_autocomplete') }}
+                    </UiHelp>
+                  </div>
+                </v-col>
+                <v-col cols="12" sm="6" v-if="getIdsFromUris(entry['typeOfPractices']).indexOf(37) >= 0">
+                  <UiInputTextArea field="otherPractice" :data="entry['otherPractice']" @update="updateRepository" />
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Adresses -->
+          <UiExpansionPanel id="address_postal" icon="fa-globe-europe">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="repositories().addressRepository.with('addressPostal')"
+                    :root-model="models().Organization"
+                    :root-id="id"
+                    :model="models().OrganizationAddressPostal"
+                    loaderType="image"
+                    newLink="/organization/address/new"
+                  >
+                    <template #list.item="{items}">
+                      <v-container fluid>
+                        <v-row dense>
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/organization/address/${item.id}`"
+                              :model="models().OrganizationAddressPostal"
+                            >
+                              <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">
+                                  <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>
+                        </v-row>
+                      </v-container>
+                    </template>
+                  </UiCollection>
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!--  Point de Contact-->
+          <UiExpansionPanel id="contact_point" icon="fa-phone">
+            <v-container class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="repositories().contactPointRepository.query()"
+                    :root-model="models().Organization"
+                    :root-id="id"
+                    :model="models().ContactPoint"
+                    loaderType="image"
+                    newLink="/organization/contact_points/new"
+                  >
+                    <template #list.item="{items}">
+                      <v-container fluid>
+                        <v-row dense>
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/organization/contact_points/${item.id}`"
+                              :model="models().ContactPoint"
+                            >
+                              <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> : {{ formatPhoneNumber(item.telphoneInvalid) }} <br></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>
+                        </v-row>
+                      </v-container>
+                    </template>
+                  </UiCollection>
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Informations légales -->
+          <UiExpansionPanel id="legalInformation" icon="fa-gavel">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="6">
+                  <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']" @update="updateRepository" />
+                </v-col>
+
+                <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">
+                  <UiInputDatePicker field="creationDate" :data="entry['creationDate']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="prefectureName" :data="entry['prefectureName']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="prefectureNumber" :data="entry['prefectureNumber']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputDatePicker field="declarationDate" :data="entry['declarationDate']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="tvaNumber" :data="entry['tvaNumber']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputEnum field="legalStatus" :data="entry['legalStatus']" enum-type="organization_legal" @update="updateRepository" />
+                </v-col>
+
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!--  Agréments -->
+          <UiExpansionPanel id="agrements" icon="fa-certificate">
+            <v-container class="container">
+              <v-row>
+                <v-col cols="12" sm="6">
+                  <UiInputText field="youngApproval" :data="entry['youngApproval']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="trainingApproval" :data="entry['trainingApproval']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="otherApproval" :data="entry['otherApproval']" @update="updateRepository" />
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Salariés -->
+          <UiExpansionPanel id="salary" icon="fa-users">
+            <v-container class="container">
+              <v-row>
+                <v-col cols="12" sm="6">
+                  <UiInputText field="collectiveAgreement" :data="entry['collectiveAgreement']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <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']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="urssafNumber" :data="entry['urssafNumber']" @update="updateRepository" />
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Réseaux -->
+          <UiExpansionPanel v-if="organizationProfile.isInsideNetwork()" id="network" icon="fa-share-alt">
+            <v-container class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="repositories().netWorkOrganizationRepository.with('network')"
+                    :root-model="models().Organization"
+                    :root-id="id"
+                    :model="models().NetworkOrganization"
+                    loaderType="text"
+                  >
+                    <template #list.item="{items}">
+                      <div v-for="item in items" :key="item.id">
+                        <span>{{ item.network.name }}</span> - <span>{{$t('first_subscription')}} : <UiTemplateDate :data="item.startDate" /></span>
+                      </div>
+                    </template>
+                  </UiCollection>
+                </v-col>
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputText field="budget" :data="entry['budget']" type="number" @update="updateRepository" />
+                </v-col>
+
+                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
+                  <UiInputCheckbox field="isPedagogicIsPrincipalActivity" :data="entry['isPedagogicIsPrincipalActivity']" @update="updateRepository" />
+                </v-col>
+
+                <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>
+          </UiExpansionPanel>
+
+          <!-- Communication -->
+          <UiExpansionPanel id="communication" icon="fa-rss">
+            <v-container class="container">
+              <v-row>
+                <v-col cols="12" sm="6">
+                  <UiInputText field="twitter" :data="entry['twitter']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="youtube" :data="entry['youtube']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="facebook" :data="entry['facebook']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="instagram" :data="entry['instagram']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="portailVisibility" :data="entry['portailVisibility']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <div class="d-flex flex-column">
+                    <UiHelp class="d-flex flex-row">
+                      <span>{{ $t('image') }}</span>
+                      <p v-html="$t('communication_image_upload')" />
+                    </UiHelp>
+                    <UiImage
+                      :id="getIdFromUri(entry['image'])"
+                      :upload="true"
+                      :width="200"
+                      field="image"
+                      :ownerId="id"
+                      @update="updateRepository"
+                    ></UiImage>
+                  </div>
+                </v-col>
+
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="repositories().organizationArticleRepository.query()"
+                    :root-model="models().Organization"
+                    :root-id="id"
+                    :model="models().OrganizationArticle"
+                    loaderType="text"
+                  >
+                    <template #list.item="{items}">
+                      <h4 class="ot_grey--text font-weight-regular">{{$t('organizationArticle')}}</h4>
+                      <UiTemplateDataTable
+                        :headers="[
+                          { text: $t('title'), value: 'title' },
+                          { text: $t('link'), value: 'link' },
+                          { text: $t('date'), value: 'date' },
+                        ]"
+                        :items="items"
+                      >
+                        <template #item.date="{item}"><UiTemplateDate :data="item.date" /></template>
+                      </UiTemplateDataTable>
+                    </template>
+                  </UiCollection>
+                </v-col>
+
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- IBAN -->
+          <UiExpansionPanel id="bank_account" icon="fa-euro-sign">
+            <v-container class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="repositories().bankAccountRepository.query()"
+                    :root-model="models().Organization"
+                    :root-id="id"
+                    :model="models().BankAccount"
+                    loaderType="image"
+                    newLink="/organization/bank_account/new"
+                  >
+                    <template #list.item="{items}">
+                      <v-container fluid>
+                        <v-row dense>
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/organization/bank_account/${item.id}`"
+                              :model="models().BankAccount"
+                            >
+                              <template #card.text>
+                                <span v-if="item.bankName"><strong>{{ $t('bankName') }}</strong> : {{ item.bankName }} <br></span>
+
+                                <span v-if="item.bic"><strong>{{ $t('bic') }}</strong> : {{ item.bic }} <br></span>
+                                <span v-if="item.bicInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('bicInvalid') }}</strong> : {{ item.bicInvalid }} <br></span>
+
+                                <span v-if="item.iban"><strong>{{ $t('iban') }}</strong> : {{ item.iban }} <br></span>
+                                <span v-if="item.ibanInvalid" class="ot_danger--text"><v-icon class="ot_danger--text">mdi-alert</v-icon> <strong>{{ $t('ibanInvalid') }}</strong> : {{ item.ibanInvalid }} <br></span>
+
+                              </template>
+                            </UiCard>
+                          </v-col>
+                        </v-row>
+                      </v-container>
+                    </template>
+                  </UiCollection>
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+        </v-expansion-panels>
+      </template>
+    </UiForm>
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext, reactive} 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 "~/composables/form/useValidator";
+import {useNavigationHelpers} from '~/composables/form/useNavigationHelpers'
+import I18N from '~/services/utils/i18n'
+import {Country} from "~/models/Core/Country";
+import {useTypeOfPracticeProvider} from "~/composables/data/useTypeOfPracticeProvider";
+import ModelsUtils from "~/services/utils/modelsUtils";
+import {NetworkOrganization} from "~/models/Network/NetworkOrganization";
+import {OrganizationArticle} from "~/models/Organization/OrganizationArticle";
+
+export default defineComponent({
+  name: 'OrganizationParent',
+  setup () {
+    const { store, app: { i18n }, $dataProvider, route } = useContext()
+    const {typeOfPractices, fetchState:typeOfPracticesFetchingState} = useTypeOfPracticeProvider($dataProvider)
+    const { panel } = useNavigationHelpers(route)
+    const { siretError, siretErrorMessage, checkSiret } = useValidator($dataProvider, i18n).useHandleSiret()
+
+    const organizationProfile = reactive($organizationProfile(store))
+    const id: number = store.state.profile.organization.id
+
+    const checkSiretHook = async (siret: string, field: string, updateRepository: any) => {
+      await checkSiret(siret)
+      if (!siretError.value) { updateRepository(siret, field) }
+    }
+
+    const formatPhoneNumber = (number: string): string => {
+      return I18N.formatPhoneNumber(number)
+    }
+
+    const getIdsFromUris = (uris: Array<string>) => {
+      const ids:Array<any> = []
+      for(const uri of uris){
+        ids.push(ModelsUtils.extractIdFromUri(uri))
+      }
+      return ids
+    }
+
+    const getIdFromUri = (uri: string) => ModelsUtils.extractIdFromUri(uri)
+
+    return {
+      repositories: () => getRepositories(),
+      id,
+      organizationProfile,
+      models: () => { return { Organization, ContactPoint, BankAccount, OrganizationAddressPostal, Country, NetworkOrganization, OrganizationArticle } },
+      rules: () => getRules(i18n, organizationProfile),
+      siretError,
+      siretErrorMessage,
+      checkSiretHook,
+      formatPhoneNumber,
+      typeOfPractices,
+      typeOfPracticesFetchingState,
+      panel,
+      getIdFromUri,
+      getIdsFromUris
+    }
+  }
+})
+
+function getRules (i18n: any, $organizationProfile:any) {
+  return {
+    nameRules: [
+      (nameValue: string) => !!nameValue || i18n.t('required'),
+      (nameValue: string) => (nameValue || '').length <= 128 || i18n.t('name_length_rule')
+    ],
+    siretRule: [
+      (siretValue: string) => /^([0-9]{9}|[0-9]{14})$/.test(siretValue) || i18n.t('siret_error')
+    ],
+    typeOfPractice: [
+      (typeOfPracticeValue: Array<number>) => {
+        if(!$organizationProfile.isManagerProduct())
+          return typeOfPracticeValue.length > 0 || i18n.t('required')
+        return true
+      }
+    ]
+  }
+}
+
+function getRepositories () {
+  return {
+    organizationRepository: repositoryHelper.getRepository(Organization),
+    contactPointRepository: repositoryHelper.getRepository(ContactPoint),
+    bankAccountRepository: repositoryHelper.getRepository(BankAccount),
+    addressRepository: repositoryHelper.getRepository(OrganizationAddressPostal),
+    countryRepository: repositoryHelper.getRepository(Country),
+    netWorkOrganizationRepository: repositoryHelper.getRepository(NetworkOrganization),
+    organizationArticleRepository: repositoryHelper.getRepository(OrganizationArticle),
+  }
+}
+</script>
+
+<style scoped>
+  .v-icon.v-icon {
+    font-size: 14px;
+  }
+</style>

+ 43 - 0
pages/parameters.vue

@@ -0,0 +1,43 @@
+<!-- Page de détails des parametres -->
+
+<template>
+  <LayoutContainer>
+
+      <v-row justify="center" align="center" class="bannerTopForm mt-5">
+        <v-col cols="12" class="ot_dark_grey ot_white--text">
+          <h4>{{ $t('parameters') }}</h4>
+        </v-col>
+      </v-row>
+
+    <!-- Rend le contenu de la page -->
+    <NuxtChild />
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import { defineComponent } from '@nuxtjs/composition-api'
+import { repositoryHelper } from '~/services/store/repository'
+import {Parameters} from "~/models/Organization/Parameters";
+import {Organization} from "~/models/Organization/Organization";
+import {Cycle} from "~/models/Education/Cycle";
+import {AdminAccess} from "~/models/Access/AdminAccess";
+
+export default defineComponent({
+  name: 'parameters',
+  layout: 'parameters',
+  middleware({ $ability, redirect }) {
+    if(!$ability.can('display', 'parameters_page'))
+      return redirect('/error')
+  },
+  setup () {
+    return {}
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Parameters)
+    repositoryHelper.cleanRepository(Organization)
+    repositoryHelper.cleanRepository(Cycle)
+    repositoryHelper.cleanRepository(AdminAccess)
+  },
+})
+
+</script>

+ 65 - 0
pages/parameters/billing/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <LayoutContainer class="mt-4">
+        <v-expansion-panels  focusable multiple :value="[0]">
+          <!-- Billing -->
+          <UiExpansionPanel id="residenceArea" icon="fa-globe-europe">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="query()"
+                    :model="model"
+                    loaderType="image"
+                    newLink="/parameters/billing/residence_area/new"
+                  >
+                    <template #list.item="{items}">
+                      <v-container fluid>
+                        <v-row dense>
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/parameters/billing/residence_area/${item.id}`"
+                              :model="model"
+                            >
+                              <template #card.text>
+                                {{ item.label }}
+                              </template>
+                            </UiCard>
+                          </v-col>
+                        </v-row>
+                      </v-container>
+                    </template>
+                  </UiCollection>
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+        </v-expansion-panels>
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent} from '@nuxtjs/composition-api'
+import { repositoryHelper } from '~/services/store/repository'
+import {Query} from "@vuex-orm/core";
+import {ResidenceArea} from "~/models/Billing/ResidenceArea";
+
+export default defineComponent({
+  name: 'residence-areas',
+  setup () {
+    const repository = repositoryHelper.getRepository(ResidenceArea)
+    const query: ComputedRef<Query> = computed(() => repository.query())
+
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    return {
+      query: () => query,
+      model: ResidenceArea
+    }
+  }
+})
+</script>

+ 26 - 0
pages/parameters/billing/residence_area/_id.vue

@@ -0,0 +1,26 @@
+<!-- Page de détails d'une zone de résidence -->
+<template>
+  <main>
+    <FormParametersResidenceArea :id="id" v-if="!fetchState.pending"></FormParametersResidenceArea>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {ResidenceArea} from "~/models/Billing/ResidenceArea";
+
+export default defineComponent({
+  name: 'EditFormParametersResidenceArea',
+  setup () {
+    const {$dataProvider, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, ResidenceArea)
+    return {
+      id,
+      fetchState
+    }
+  }
+})
+</script>

+ 35 - 0
pages/parameters/billing/residence_area/new.vue

@@ -0,0 +1,35 @@
+<!-- Page de détails d'une zone de résidence -->
+<template>
+  <main>
+    <v-skeleton-loader
+      v-if="loading"
+      type="text"
+    />
+    <FormParametersResidenceArea :id="item.id" v-else></FormParametersResidenceArea>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {ResidenceArea} from "~/models/Billing/ResidenceArea";
+
+export default defineComponent({
+  name: 'NewFormParametersResidenceArea',
+  setup () {
+    const {$dataProvider, store} = useContext()
+    const {createItem} = useDataUtils($dataProvider)
+    const {create, loading, item} = createItem(store, ResidenceArea)
+
+    if(process.client){
+      const itemToCreate: ResidenceArea = new ResidenceArea()
+      create(itemToCreate)
+    }
+
+    return {
+      loading,
+      item
+    }
+  }
+})
+</script>

+ 127 - 0
pages/parameters/communication.vue

@@ -0,0 +1,127 @@
+<template>
+  <LayoutContainer v-if="!fetchState.pending">
+    <UiForm :id="id" :model="model" :query="query()">
+      <template #form.input="{entry, updateRepository}">
+        <v-expansion-panels  focusable multiple :value="[0, 1]">
+          <!-- Site internet -->
+          <UiExpansionPanel id="web_parameters" icon="fa-desktop">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="6">
+                  <div>
+                    <span>{{ $t('otherWebsite') }} : </span>
+                    <span>{{ entry['otherWebsite'] }}</span>
+                  </div>
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <div>
+                    <span>{{ $t('subDomainHistorical') }} : </span>
+                  </div>
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="newSubDomain" :data="entry['newSubDomain']" />
+                </v-col>
+
+                <v-col cols="12" sm="6" v-if="!organizationProfile.isCmf()">
+                  <UiInputCheckbox field="desactivateOpentalentSiteWeb" :data="entry['desactivateOpentalentSiteWeb']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="website" :data="entry['website']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputAutocompleteWithAPI
+                    field="publicationDirectors"
+                    label="publicationDirectors"
+                    :multiple="true"
+                    chips
+                    :remote-uri="entry['publicationDirectors']"
+                    remote-url="api/access_people"
+                    :item-text="['person.givenName', 'person.name']"
+                    :searchFunction="accessSearch"
+                    @update="updateRepository($event.map((id) => `/api/accesses/${id}`), 'publicationDirectors')"
+                  />
+                </v-col>
+
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Sms -->
+          <UiExpansionPanel id="sms" icon="fa-mobile" v-if="organizationProfile.hasModule(['Sms'])">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="smsSenderName" :data="entry['smsSenderName']" @update="updateRepository" :rules="rules().smsSenderNameRules"  />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="usernameSMS" :data="entry['usernameSMS']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText field="passwordSMS" :data="entry['passwordSMS']" @update="updateRepository" type="password" />
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+        </v-expansion-panels>
+      </template>
+    </UiForm>
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, reactive, ref, useContext} from '@nuxtjs/composition-api'
+import { Organization } from '@/models/Organization/Organization'
+import { repositoryHelper } from '~/services/store/repository'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Parameters} from "~/models/Organization/Parameters";
+import {Query} from "@vuex-orm/core";
+import {$organizationProfile} from "~/services/profile/organizationProfile";
+import ModelsUtils from "~/services/utils/modelsUtils";
+import {useAccessesProvider} from "~/composables/data/useAccessesProvider";
+
+export default defineComponent({
+  name: 'parameters',
+  setup () {
+    const {store, $dataProvider, app: {i18n}} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const {getPhysicalByFullName: accessSearch} = useAccessesProvider($dataProvider)
+
+    const organizationProfile = reactive($organizationProfile(store))
+
+    const id = store.state.profile.organization.parametersId
+    const {fetchState} = getItemToEdit(id, Parameters)
+
+    const repository = repositoryHelper.getRepository(Parameters)
+    const query: ComputedRef<Query> = computed(() => repository.query())
+
+    return {
+      query: () => query.value,
+      rules: () => getRules(i18n),
+      organizationProfile,
+      id,
+      fetchState,
+      accessSearch,
+      model: Parameters
+    }
+  }
+})
+
+function getRules (i18n: any) {
+  return {
+    smsSenderNameRules: [
+      (smsSenderNameValue: string) => {
+        const pattern = /^[a-zA-z\d]+$/
+        return pattern.test(smsSenderNameValue) || i18n.t('smsSenderName_error')
+      }
+    ]
+  }
+}
+</script>

+ 26 - 0
pages/parameters/education/cycle/_id.vue

@@ -0,0 +1,26 @@
+<!-- Page de détails d'un cycle -->
+<template>
+  <main>
+    <FormParametersCycle :id="id" v-if="!fetchState.pending"></FormParametersCycle>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Cycle} from "~/models/Education/Cycle";
+
+export default defineComponent({
+  name: 'EditFormParametersCycle',
+  setup () {
+    const {$dataProvider, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, Cycle)
+    return {
+      id,
+      fetchState
+    }
+  }
+})
+</script>

+ 26 - 0
pages/parameters/education/education_timing/_id.vue

@@ -0,0 +1,26 @@
+<!-- Page de détails d'un temps d'enseignement -->
+<template>
+  <main>
+    <FormParametersEducationTiming :id="id" v-if="!fetchState.pending"></FormParametersEducationTiming>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {EducationTiming} from "~/models/Education/EducationTiming";
+
+export default defineComponent({
+  name: 'EditFormParametersEducationTiming',
+  setup () {
+    const {$dataProvider, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, EducationTiming)
+    return {
+      id,
+      fetchState
+    }
+  }
+})
+</script>

+ 35 - 0
pages/parameters/education/education_timing/new.vue

@@ -0,0 +1,35 @@
+<!-- Page de détails d'une zone de résidence -->
+<template>
+  <main>
+    <v-skeleton-loader
+      v-if="loading"
+      type="text"
+    />
+    <FormParametersEducationTiming :id="item.id" v-else></FormParametersEducationTiming>
+  </main>
+</template>
+
+<script lang="ts">
+import {defineComponent, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {EducationTiming} from "~/models/Education/EducationTiming";
+
+export default defineComponent({
+  name: 'NewFormParametersEducationTiming',
+  setup () {
+    const {$dataProvider, store} = useContext()
+    const {createItem} = useDataUtils($dataProvider)
+    const {create, loading, item} = createItem(store, EducationTiming)
+
+    if(process.client){
+      const itemToCreate: EducationTiming = new EducationTiming()
+      create(itemToCreate)
+    }
+
+    return {
+      loading,
+      item
+    }
+  }
+})
+</script>

+ 108 - 0
pages/parameters/education/index.vue

@@ -0,0 +1,108 @@
+<template>
+  <LayoutContainer class="mt-4">
+        <v-expansion-panels  focusable multiple :value="[0, 1]">
+          <!-- Cycle -->
+          <UiExpansionPanel id="cycle" icon="fa-bars">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="queryCycle()"
+                    :model="modelCycle"
+                    loaderType="image"
+                  >
+                    <template #list.item="{items}">
+                      <v-container fluid>
+                        <v-row dense>
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/parameters/education/cycle/${item.id}`"
+                              :model="modelCycle"
+                              :can-delete="false"
+                            >
+                              <template #card.text>
+                                {{ item.label }}
+                              </template>
+                            </UiCard>
+                          </v-col>
+                        </v-row>
+                      </v-container>
+                    </template>
+                  </UiCollection>
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- EducationTiming -->
+          <UiExpansionPanel id="educationTiming" icon="fa-calendar">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                  <UiCollection
+                    :query="queryEducationTiming()"
+                    :model="modelEducationTiming"
+                    loaderType="image"
+                    newLink="/parameters/education/education_timing/new"
+                  >
+                    <template #list.item="{items}">
+                      <v-container fluid>
+                        <v-row dense>
+                          <v-col
+                            v-for="item in items"
+                            :key="item.id"
+                            cols="4"
+                          >
+                            <UiCard
+                              :id="item.id"
+                              :link="`/parameters/education/education_timing/${item.id}`"
+                              :model="modelEducationTiming"
+                              :can-delete="false"
+                            >
+                              <template #card.text>
+                                {{ item.timing }}
+                              </template>
+                            </UiCard>
+                          </v-col>
+                        </v-row>
+                      </v-container>
+                    </template>
+                  </UiCollection>
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+        </v-expansion-panels>
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent} from '@nuxtjs/composition-api'
+import { repositoryHelper } from '~/services/store/repository'
+import {Query} from "@vuex-orm/core";
+import {Cycle} from "~/models/Education/Cycle";
+import {EducationTiming} from "~/models/Education/EducationTiming";
+
+export default defineComponent({
+  name: 'educationTimings',
+  setup () {
+    const repositoryCycle = repositoryHelper.getRepository(Cycle)
+    const repositoryEducationTiming = repositoryHelper.getRepository(EducationTiming)
+    const queryCycle: ComputedRef<Query> = computed(() => repositoryCycle.query().orderBy('order', 'asc'))
+    const queryEducationTiming: ComputedRef<Query> = computed(() => repositoryEducationTiming.query())
+
+    return {
+      queryCycle: () => queryCycle,
+      queryEducationTiming: () => queryEducationTiming,
+      modelCycle: Cycle,
+      modelEducationTiming: EducationTiming,
+    }
+  }
+})
+</script>

+ 100 - 0
pages/parameters/index.vue

@@ -0,0 +1,100 @@
+<template>
+      <LayoutContainer v-if="!fetchState.pending">
+        <UiForm :id="id" :model="model" :query="query()">
+          <template #form.input="{entry, updateRepository}">
+            <v-expansion-panels  focusable multiple :value="[0]">
+              <!-- Description -->
+              <UiExpansionPanel id="generalParams" icon="fa-info">
+                <v-container fluid class="container">
+                  <v-row>
+                    <v-col cols="12" sm="6" v-if="organizationProfile.isSchool()">
+                      <UiInputDatePicker field="financialDate" :data="entry['financialDate']" format="DD MMMM" @update="updateRepository" />
+                    </v-col>
+
+                    <v-col cols="12" sm="6">
+                      <UiInputDatePicker field="musicalDate" :data="entry['musicalDate']" format="DD MMMM" @update="updateRepository" />
+                    </v-col>
+
+                    <v-col cols="12" sm="6" v-if="organizationProfile.isSchool()">
+                      <UiInputDatePicker field="startCourseDate" :data="entry['startCourseDate']" @update="updateRepository" />
+                    </v-col>
+
+                    <v-col cols="12" sm="6" v-if="organizationProfile.isSchool()">
+                      <UiInputDatePicker field="endCourseDate" :data="entry['endCourseDate']" @update="updateRepository" />
+                    </v-col>
+
+                    <v-col cols="12" sm="6">
+                      <UiInputCheckbox field="showAdherentList" :data="entry['showAdherentList']" @update="updateRepository" />
+                    </v-col>
+
+                    <v-col cols="12" sm="6" v-if="organizationProfile.isSchool() && organization.legalStatus === 'ASSOCIATION_LAW_1901'">
+                      <UiInputCheckbox field="studentsAreAdherents" :data="entry['studentsAreAdherents']" @update="updateRepository" />
+                    </v-col>
+
+                    <v-col cols="12" sm="6" v-if="organizationProfile.isCMFCentralService()">
+                      <div>
+                        <span>{{ $t('qrCode') }}</span>
+                      </div>
+                      <UiImage
+                        :id="getIdFromUri(entry['qrCode'])"
+                        :upload="true"
+                        :width="100"
+                        field="qrCode"
+                        :ownerId="id"
+                        @update="updateRepository"
+                      ></UiImage>
+                    </v-col>
+
+                    <v-col cols="12" sm="6">
+                      <UiInputEnum field="timezone" :data="entry['timezone']" enum-type="timezone" @update="updateRepository" />
+                    </v-col>
+
+                  </v-row>
+                </v-container>
+              </UiExpansionPanel>
+
+            </v-expansion-panels>
+          </template>
+        </UiForm>
+      </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, reactive, useContext} from '@nuxtjs/composition-api'
+import { Organization } from '@/models/Organization/Organization'
+import { repositoryHelper } from '~/services/store/repository'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Parameters} from "~/models/Organization/Parameters";
+import {Model, Query, Item} from "@vuex-orm/core";
+import {$organizationProfile} from "~/services/profile/organizationProfile";
+import {Repository as VuexRepository} from "@vuex-orm/core/dist/src/repository/Repository";
+import ModelsUtils from "~/services/utils/modelsUtils";
+
+export default defineComponent({
+  name: 'parameters',
+  setup () {
+    const {store, $dataProvider} = useContext()
+    const organizationProfile = reactive($organizationProfile(store))
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = store.state.profile.organization.parametersId
+    const {fetchState} = getItemToEdit(id, Parameters)
+    const repositoryParameters: VuexRepository<Model> = repositoryHelper.getRepository(Parameters)
+    const query: ComputedRef<Query> = computed(() => repositoryParameters.query())
+
+    getItemToEdit(store.state.profile.organization.id, Organization)
+    const organization:ComputedRef<Item> = computed(() => repositoryHelper.findItemFromModel(Organization, store.state.profile.organization.id))
+
+    const getIdFromUri = (uri: string) => ModelsUtils.extractIdFromUri(uri)
+
+    return {
+      query: () => query.value,
+      id,
+      fetchState,
+      model: Parameters,
+      organizationProfile,
+      organization,
+      getIdFromUri
+    }
+  }
+})
+</script>

+ 60 - 0
pages/parameters/secure.vue

@@ -0,0 +1,60 @@
+<template>
+  <LayoutContainer v-if="!fetchState.pending">
+    <UiForm :id="id" :model="model" :query="query()">
+      <template #form.input="{entry, updateRepository}">
+        <v-expansion-panels  focusable multiple :value="[0,1]">
+          <!-- Description -->
+          <UiExpansionPanel id="superAdmin" icon="fa-desktop">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="12">
+                    {{$t('help_super_admin')}}
+                </v-col>
+                <v-col cols="12" sm="6">
+                  <UiInputText field="username" :data="entry['username']" @update="updateRepository" :readonly="true" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputEmail field="email" :data="entry['email']" @update="updateRepository" />
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+        </v-expansion-panels>
+      </template>
+    </UiForm>
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, useContext} from '@nuxtjs/composition-api'
+import { repositoryHelper } from '~/services/store/repository'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Query} from "@vuex-orm/core";
+import {AdminAccess} from "~/models/Access/AdminAccess";
+import {queryHelper} from "~/services/store/query";
+
+export default defineComponent({
+  name: 'secure',
+  setup () {
+    const {$dataProvider} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const {fetchState} = getItemToEdit(1, AdminAccess)
+
+    const repository = repositoryHelper.getRepository(AdminAccess)
+    const query: ComputedRef<Query> = computed(() => repository.query())
+    const id: ComputedRef<number> = computed(() => {
+      const item:AdminAccess = queryHelper.getFirstItem(query.value) as AdminAccess
+      return item.id
+    })
+
+    return {
+      query: () => query.value,
+      fetchState,
+      id,
+      model: AdminAccess
+    }
+  }
+})
+</script>

+ 163 - 0
pages/parameters/student.vue

@@ -0,0 +1,163 @@
+<template>
+  <LayoutContainer v-if="!fetchState.pending">
+    <UiForm :id="id" :model="model" :query="query()">
+      <template #form.input="{entry, updateRepository}">
+        <v-expansion-panels  focusable multiple :value="[0,1,2]">
+          <!-- Suivi pédagogique -->
+          <UiExpansionPanel id="educational_follow_up" icon="fa-graduation-cap">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="trackingValidation" :data="entry['trackingValidation']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="editCriteriaNotationByAdminOnly" :data="entry['editCriteriaNotationByAdminOnly']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputEnum field="educationPeriodicity" :data="entry['educationPeriodicity']" enum-type="education_periodicity" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6" v-if="organizationProfile.hasModule(['AdvancedEducationNotation'])">
+                  <UiInputEnum field="advancedEducationNotationType" :data="entry['advancedEducationNotationType']" enum-type="advanced_education_notation" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputText
+                    label="averageMax"
+                    field="average"
+                    type="number"
+                    :data="entry['average']"
+                    @update="updateRepository"
+                    :rules="rules().averageRules"
+                  />
+                </v-col>
+
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Bulletins -->
+          <UiExpansionPanel id="bulletin_parameters" icon="fa-file-alt">
+            <v-container fluid class="container">
+              <v-row>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinWithTeacher" :data="entry['bulletinWithTeacher']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinPrintAddress" :data="entry['bulletinPrintAddress']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinSignatureDirector" :data="entry['bulletinSignatureDirector']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinDisplayLevelAcquired" :data="entry['bulletinDisplayLevelAcquired']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <div class="d-flex flex-row">
+                    <UiInputCheckbox field="bulletinShowEducationWithoutEvaluation" :data="entry['bulletinShowEducationWithoutEvaluation']" @update="updateRepository" />
+                    <UiHelp>
+                      <p v-html="$t('bulletinShowEducationWithoutEvaluationHelp')" />
+                    </UiHelp>
+                  </div>
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinViewTestResults" :data="entry['bulletinViewTestResults']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinShowAbsences" :data="entry['bulletinShowAbsences']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="bulletinShowAverages" :data="entry['bulletinShowAverages']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <div class="d-flex flex-row">
+                    <UiInputCheckbox field="bulletinEditWithoutEvaluation" :data="entry['bulletinEditWithoutEvaluation']" @update="updateRepository" />
+                    <UiHelp>
+                      <p v-html="$t('bulletinEditWithoutEvaluationHelp')" />
+                    </UiHelp>
+                  </div>
+                </v-col>
+
+                <v-col cols="12" sm="6">
+                  <UiInputEnum field="bulletinReceiver" :data="entry['bulletinReceiver']" enum-type="organization_bulletin_send_to" @update="updateRepository" />
+                </v-col>
+
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+          <!-- Abscences -->
+          <UiExpansionPanel id="attendance" icon="fa-check">
+            <v-container fluid class="container">
+              <v-row>
+                <v-col cols="12" sm="6">
+                  <UiInputCheckbox field="sendAttendanceEmail" :data="entry['sendAttendanceEmail']" @update="updateRepository" />
+                </v-col>
+
+                <v-col cols="12" sm="6" v-if="organizationProfile.hasModule(['Sms'])">
+                  <UiInputCheckbox field="sendAttendanceSms" :data="entry['sendAttendanceSms']" @update="updateRepository" />
+                </v-col>
+              </v-row>
+            </v-container>
+          </UiExpansionPanel>
+
+        </v-expansion-panels>
+      </template>
+    </UiForm>
+  </LayoutContainer>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, reactive, useContext} from '@nuxtjs/composition-api'
+import { Organization } from '@/models/Organization/Organization'
+import { repositoryHelper } from '~/services/store/repository'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Parameters} from "~/models/Organization/Parameters";
+import {Model, Query, Item} from "@vuex-orm/core";
+import {$organizationProfile} from "~/services/profile/organizationProfile";
+import {Repository as VuexRepository} from "@vuex-orm/core/dist/src/repository/Repository";
+import ModelsUtils from "~/services/utils/modelsUtils";
+import i18n from "~/config/nuxtConfig/i18n";
+
+export default defineComponent({
+  name: 'parameters',
+  setup () {
+    const {store, $dataProvider, app: {i18n}} = useContext()
+    const organizationProfile = reactive($organizationProfile(store))
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = store.state.profile.organization.parametersId
+    const {fetchState} = getItemToEdit(id, Parameters)
+    const repositoryParameters: VuexRepository<Model> = repositoryHelper.getRepository(Parameters)
+    const query: ComputedRef<Query> = computed(() => repositoryParameters.query())
+
+    return {
+      query: () => query.value,
+      rules: () => getRules(i18n),
+      id,
+      fetchState,
+      model: Parameters,
+      organizationProfile,
+    }
+  }
+})
+
+function getRules (i18n: any) {
+  return {
+    averageRules: [
+      (averageValue: number) => (averageValue >= 1 && averageValue <= 100) || i18n.t('between_0_and_100')
+    ]
+  }
+}
+</script>

+ 1 - 1
plugins/Data/axios.js

@@ -29,7 +29,7 @@ export default function ({ $axios, redirect, store }) {
       redirect('/login')
     }
     if (error.response.status === 403) {
-      console.debug('forbidden')
+      new Page(store).addAlerts(TYPE_ALERT.ALERT, ['forbidden'])
     }
 
     if (error.response.status === 500) {

+ 1 - 6
services/data/baseDataManager.ts

@@ -3,7 +3,6 @@ import {DataManager, DataPersisterArgs, DataProviderArgs, UrlArgs} from '~/types
 import Connection from '~/services/connection/connection'
 import Hookable from '~/services/data/hookable'
 import { HTTP_METHOD, QUERY_TYPE } from '~/types/enums'
-import ApiError from '~/services/exception/apiError'
 
 /**
  * Base class for data providers, persisters or deleters
@@ -43,11 +42,7 @@ abstract class BaseDataManager extends Hookable implements DataManager {
 
     await this.triggerHooks(queryArguments)
 
-    try {
-      return await this._invoke(queryArguments)
-    } catch (error) {
-      throw new ApiError(error.response.status, error.response.data.detail)
-    }
+    return await this._invoke(queryArguments)
   }
 
   /**

+ 0 - 13
services/exception/apiError.ts

@@ -1,13 +0,0 @@
-class ApiError extends Error {
-  private readonly status !: number
-
-  constructor (status: number, message: string) {
-    super(message)
-    this.status = status
-  }
-
-  public getStatus (): number {
-    return this.status
-  }
-}
-export default ApiError

+ 12 - 1
services/profile/organizationProfile.ts

@@ -144,6 +144,17 @@ class OrganizationProfile {
     return this.organizationProfile.legalStatus === 'ASSOCIATION_LAW_1901';
   }
 
+  /**
+   * L'organization est-elle la CMF ?
+   *
+   * @return {boolean}
+   */
+  isCMFCentralService(): boolean {
+    if(process.env.CMF_ID)
+      return this.organizationProfile.id == parseInt(process.env.CMF_ID)
+    return false
+  }
+
   /**
    * Factory
    *
@@ -158,7 +169,7 @@ class OrganizationProfile {
       isOrganizationWithChildren: this.hasChildren.bind(this),
       isAssociation: this.isAssociation.bind(this),
       isShowAdherentList: this.isShowAdherentList.bind(this),
-      isCmf: this.isCmf.bind(this)
+      isCmf: this.isCmf.bind(this),
     }
   }
 }

+ 2 - 1
services/store/form.ts

@@ -1,6 +1,7 @@
 import {Store} from "vuex";
 import {FORM_STATUS} from "~/types/enums";
 import {Route} from "vue-router";
+import {AnyJson} from "~/types/interfaces";
 
 export default class FormStorage {
   private store:Store<any>
@@ -27,7 +28,7 @@ export default class FormStorage {
    * Ajout des violations dans le store
    * @param invalidFields
    */
-  addViolations(invalidFields: Array<any>){
+  addViolations(invalidFields: AnyJson){
     this.store.commit('form/setViolations', invalidFields)
   }
 }

+ 5 - 0
store/profile/organization.ts

@@ -3,6 +3,7 @@ import { baseOrganizationState, organizationState } from '~/types/interfaces'
 
 export const state = () => ({
   id: null,
+  parametersId: null,
   name: '',
   product: '',
   currentActivityYear: '',
@@ -20,6 +21,9 @@ export const mutations = {
   setId (state: organizationState, id: number) {
     state.id = id
   },
+  setParametersId (state: organizationState, parametersId: number) {
+    state.parametersId = parametersId
+  },
   setName (state: organizationState, name: string) {
     state.name = name
   },
@@ -61,6 +65,7 @@ export const mutations = {
 export const actions = {
   setProfile (context: any, profile: any) {
     context.commit('setId', profile.id)
+    context.commit('setParametersId', profile.parametersId)
     context.commit('setName', profile.name)
     context.commit('setProduct', profile.product)
     context.commit('setCurrentActivityYear', profile.currentYear)

+ 5 - 0
tests/unit/component/Layout/SubHeader.spec.js

@@ -1,10 +1,14 @@
 import { shallowMount } from '@vue/test-utils'
 import Vuetify from 'vuetify'
 import SubHeader from '~/components/Layout/Subheader'
+import {UseAccess} from "~/composables/utils/useAccess";
+jest.mock('~/composables/utils/useAccess', );
 
 let wrapper
 let vuetify
 beforeEach(() => {
+  UseAccess.mockReturnValue({hasMenuOrIsTeacher: true});
+
   vuetify = new Vuetify()
   wrapper = shallowMount(SubHeader, {
     stubs: [
@@ -19,6 +23,7 @@ beforeEach(() => {
 })
 
 describe('LayoutSubHeaderActivityYear', () => {
+
   it('should display by default', async () => {
     expect(wrapper.find('.activity-year').exists()).toBeTruthy()
   })

+ 5 - 3
tests/unit/composables/data/useDataUtils.spec.ts

@@ -3,6 +3,7 @@ import {useDataUtils} from "~/composables/data/useDataUtils";
 import {Organization} from "~/models/Organization/Organization";
 import {Ref, ref} from "@nuxtjs/composition-api";
 import {mountComposition} from "~/tests/unit/Helpers";
+import {useForm} from "~/composables/form/useForm";
 jest.mock('~/services/data/dataProvider')
 
 
@@ -23,8 +24,9 @@ describe('getItemToEdit()', () => {
   })
 
 
-  it('should throw an error if route id is empty', () => {
-    route.value = {params: 'foo'}
-    expect(() => useDataUtilsMount.getItemToEdit(route, Organization)).toThrowError('id must be exist')
+  it('should throw an error if id is empty', () => {
+    const component = mountComposition(() => {
+      expect(() => useDataUtilsMount.getItemToEdit(null, Organization)).toThrowError('id must be exist')
+    });
   })
 })

+ 4 - 4
tests/unit/composables/form/useError.spec.ts

@@ -10,19 +10,19 @@ const emit = jest.fn()
 beforeAll(() => {
   store = createStore()
   store.registerModule('form', formModule)
-  store.commit('form/setViolations', ['foo', 'bar'])
+  store.commit('form/setViolations', {'foo': 'bob', 'bar': 'alice'})
 
   useErrorMount = useError('foo', emit, store)
 })
 
 describe('onChange()', () => {
   it('delete foo inside store', () => {
-    useErrorMount.onChange('bob')
-    expect(store.state.form.violations).toHaveLength(1)
+    useErrorMount.onChange('event')
+    expect(store.state.form.violations).toStrictEqual({'bar': 'alice'})
   })
 
   it('emit is called', () => {
-    useErrorMount.onChange('bob')
+    useErrorMount.onChange('event')
     expect(emit).toHaveBeenCalled
   })
 })

+ 37 - 0
tests/unit/composables/utils/useAccess.spec.ts

@@ -0,0 +1,37 @@
+import {createStore, mountComposition} from "~/tests/unit/Helpers";
+import {accessProfile as accessModule} from "~/tests/unit/fixture/state/profile";
+import {AccessStore} from "~/types/interfaces";
+import {UseAccess} from "~/composables/utils/useAccess";
+
+let store:AccessStore
+let useAccessMount:any
+
+beforeAll(() => {
+  store = createStore()
+  store.registerModule('profile', {})
+  store.registerModule(['profile', 'access'], accessModule)
+
+  const component = mountComposition(() => {
+    useAccessMount = UseAccess(store)
+  });
+})
+
+describe('hasMenuOrIsTeacher()', () => {
+  it('must to return false', () => {
+    const {hasMenuOrIsTeacher} = useAccessMount
+    expect(hasMenuOrIsTeacher.value).toBeFalsy()
+  })
+
+  it('must to return true because user have a lateral menu', () => {
+    store.commit('profile/access/setHasLateralMenu', true)
+    const {hasMenuOrIsTeacher} = useAccessMount
+    expect(hasMenuOrIsTeacher.value).toBeFalsy()
+  })
+
+  it('must to return true because user is a teacher', () => {
+    store.commit('profile/access/setHasLateralMenu', false)
+    store.commit('profile/access/setIsTeacher', true)
+    const {hasMenuOrIsTeacher} = useAccessMount
+    expect(hasMenuOrIsTeacher.value).toBeFalsy()
+  })
+})

+ 1 - 0
types/interfaces.d.ts

@@ -141,6 +141,7 @@ interface baseOrganizationState {
 
 interface organizationState extends baseOrganizationState {
   id: number,
+  parametersId: number,
   name: string,
   product?: string,
   currentActivityYear?: number,

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików