浏览代码

Merge branch 'feature/BTTF-50' into develop

Vincent GUFFON 4 年之前
父节点
当前提交
6c09b8427f

+ 6 - 6
components/Layout/ActivityYear.vue

@@ -1,6 +1,6 @@
 <template>
   <main class="d-flex">
-    <span class="mr-2 ot_dark_grey--text font-weight-bold">{{$t(label)}}</span>
+    <span class="mr-2 ot_dark_grey--text font-weight-bold">{{$t(label)}} : </span>
     <UiXeditableText
       class="activity-year-input"
       type="number"
@@ -16,14 +16,14 @@
 
 <script lang="ts">
   import {defineComponent, useContext} from '@nuxtjs/composition-api'
-  import {$useActivityYearUpdater} from "~/use/updater/useActivityYearUpdater";
+  import {$useMyProfileUpdater} from "~/use/updater/useMyProfileUpdater";
   import {$organizationProfile} from "~/services/profile/organizationProfile";
   import {$useDirtyForm} from "~/use/form/useDirtyForm";
 
   export default defineComponent({
     setup() {
       const {store, $dataPersister} = useContext()
-      const {updater, activityYear} = $useActivityYearUpdater(store, $dataPersister)
+      const {updateMyProfile, setActivityYear, activityYear} = $useMyProfileUpdater(store, $dataPersister)
       const {markFormAsNotDirty} = $useDirtyForm(store)
 
       const organizationProfile = $organizationProfile(store);
@@ -33,9 +33,9 @@
 
       const updateActivityYear = async (newDate:number) =>{
         markFormAsNotDirty()
-        await updater(newDate)
-        console.log('1')
-        setTimeout(()=>window.location.reload(), 0)
+        setActivityYear(newDate)
+        await updateMyProfile()
+        window.location.reload()
       }
 
       return {

+ 77 - 0
components/Layout/DataTiming.vue

@@ -0,0 +1,77 @@
+<template>
+  <main class="d-flex align-baseline">
+
+    <span class="mr-2 ot_dark_grey--text font-weight-bold">{{$t('display_data')}} : </span>
+
+    <v-btn-toggle
+      v-model="timeChoice"
+      dense
+      class="ot_light_grey toggle-btn"
+      active-class="ot_green ot_white--text"
+      multiple
+    >
+      <v-btn max-height="25" class="font-weight-normal text-caption">
+        {{$t('past')}}
+      </v-btn>
+
+      <v-btn max-height="25" class="font-weight-normal text-caption">
+        {{$t('present')}}
+      </v-btn>
+
+      <v-btn max-height="25" class="font-weight-normal text-caption">
+        {{$t('future')}}
+      </v-btn>
+
+    </v-btn-toggle>
+
+  </main>
+</template>
+
+<script lang="ts">
+  import {defineComponent, onUnmounted, ref, useContext, watch} from '@nuxtjs/composition-api'
+  import {$useDirtyForm} from "~/use/form/useDirtyForm";
+  import {$useMyProfileUpdater} from "~/use/updater/useMyProfileUpdater";
+
+  export default defineComponent({
+    setup() {
+      const {store, $dataPersister} = useContext()
+      const {markFormAsNotDirty} = $useDirtyForm(store)
+      const {updateMyProfile, setHistorical, historical} = $useMyProfileUpdater(store, $dataPersister)
+
+      const timeChoice = ref(Array<number>())
+
+      const historicalArray = ['past', 'present', 'future']
+      for(const key in historicalArray){
+        if (historical.value[historicalArray[key]])
+          timeChoice.value.push(parseInt(key))
+      }
+
+      const unwatch = watch(timeChoice, async (newValue) => {
+        const historicalChoice:Array<string> = []
+        for(const key in newValue){
+          historicalChoice.push(historicalArray[newValue[key]])
+        }
+
+        setHistorical(historicalChoice)
+        markFormAsNotDirty()
+        await updateMyProfile()
+        window.location.reload()
+      })
+
+      onUnmounted(()=>{
+        unwatch()
+      })
+
+      return {
+        timeChoice
+      }
+    }
+  })
+</script>
+
+<style scoped lang="scss">
+  .toggle-btn{
+    z-index: 1;
+    border-radius: 4px 0px 0px 4px;
+  }
+</style>

+ 106 - 0
components/Layout/DataTimingRange.vue

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

+ 18 - 5
components/Layout/Header.vue

@@ -25,9 +25,19 @@
       color="ot_warning ot_white--text"
     >{{$t('create')}}</v-btn>
 
-    <v-btn icon>
-      <a class="no-decoration" :href="properties.homeUrl + '/'"><v-icon class="ot_white--text" small>fa-home</v-icon></a>
-    </v-btn>
+    <v-tooltip bottom>
+      <template v-slot:activator="{ on, attrs }">
+        <v-btn
+          icon
+          v-bind="attrs"
+          v-on="on"
+        >
+          <a class="no-decoration" :href="properties.homeUrl + '/'"><v-icon class="ot_white--text" small>fa-home</v-icon></a>
+        </v-btn>
+      </template>
+      <span>{{$t('welcome')}}</span>
+    </v-tooltip>
+
 
     <LayoutHeaderMenu :menu="webSiteMenu"></LayoutHeaderMenu>
 
@@ -39,9 +49,12 @@
 
     <LayoutHeaderMenu :menu="configurationMenu" v-if="hasConfigurationMenu"></LayoutHeaderMenu>
 
-<!--    <LayoutHeaderMenu :menu="accountMenu" :avatar="true"></LayoutHeaderMenu>-->
+    <LayoutHeaderMenu :menu="accountMenu" :avatar="true"></LayoutHeaderMenu>
 
-    <a class="help ot_dark_grey ot_menu_color--text" href="https://support.opentalent.fr/" target="_blank">{{$t('help_access')}}</a>
+    <a class="text-body pa-3 ml-2 ot_dark_grey ot_white--text text-decoration-none" href="https://support.opentalent.fr/" target="_blank">
+      <span class="d-none d-sm-none d-md-flex">{{$t('help_access')}}</span>
+      <v-icon class="d-sm-flex d-md-none" color="white">fas fa-question-circle</v-icon>
+    </a>
 
   </v-app-bar>
 </template>

+ 27 - 19
components/Layout/HeaderMenu.vue

@@ -1,24 +1,32 @@
 <template>
   <v-menu offset-y left max-height="300">
-    <template v-slot:activator="{ on, attrs }">
-      <v-avatar v-if="avatar"
-             size="30"
-             v-bind="attrs"
-             v-on="on"
-      >
-        <img
-          src="https://cdn.vuetifyjs.com/images/john.jpg"
-          alt="John"
-        >
-      </v-avatar>
-      <v-btn v-else
-             icon
-             v-bind="attrs"
-             v-on="on"
-             color=""
-      >
-        <v-icon class="ot_white--text" small>{{menu.icon}}</v-icon>
-      </v-btn>
+    <template v-slot:activator="{ on: { click }, attrs }">
+      <v-tooltip bottom>
+        <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
+          <v-avatar v-if="avatar"
+                 size="30"
+                 v-bind="[attrs, attrs_tooltips]"
+                 v-on="on_tooltips"
+                 v-on:click="click"
+          >
+            <img
+              src="https://cdn.vuetifyjs.com/images/john.jpg"
+              alt="John"
+            >
+          </v-avatar>
+
+          <v-btn v-else
+                 icon
+                 v-bind="[attrs, attrs_tooltips]"
+                 v-on="on_tooltips"
+                 v-on:click="click"
+                 color=""
+          >
+            <v-icon class="ot_white--text" small>{{menu.icon}}</v-icon>
+          </v-btn>
+        </template>
+        <span>{{$t(menu.title)}}</span>
+      </v-tooltip>
     </template>
     <v-list dense :subheader="true">
       <v-list-item dense class="ot_light_grey">

+ 46 - 0
components/Layout/Loading.vue

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

+ 11 - 6
components/Layout/Notification.vue

@@ -1,12 +1,17 @@
 <template>
   <v-menu offset-y>
     <template v-slot:activator="{ on, attrs }">
-      <v-btn icon
-             v-bind="attrs"
-             v-on="on"
-      >
-        <v-icon class="ot_white--text" small>fa-bell</v-icon>
-      </v-btn>
+      <v-tooltip bottom>
+        <template v-slot:activator="{ on, attrs }">
+          <v-btn icon
+                 v-bind="attrs"
+                 v-on="on"
+          >
+            <v-icon class="ot_white--text" small>fa-bell</v-icon>
+          </v-btn>
+        </template>
+        <span>{{$t('notification')}}</span>
+      </v-tooltip>
     </template>
     <v-list dense>
       <template v-for="(item, index) in properties.menu">

+ 24 - 4
components/Layout/Subheader.vue

@@ -1,22 +1,42 @@
 <template>
   <main>
     <v-card
-      class="d-flex ot_super_light_grey text-body-2"
+      v-if="displayedSubHeader"
+      class="d-none d-sm-none d-md-flex ot_super_light_grey text-body-2"
       flat
       tile
     >
-      <LayoutBreadcrumbs class="d-none sd-sm-none d-md-flex mr-auto"></LayoutBreadcrumbs>
+      <LayoutBreadcrumbs class="mr-auto"></LayoutBreadcrumbs>
       <v-card
-        class="d-md-flex ot_super_light_grey pt-2 mr-6"
+        class="d-md-flex ot_super_light_grey pt-2 mr-6  align-baseline"
         flat
         tile
       >
-        <LayoutActivityYear></LayoutActivityYear>
+        <LayoutActivityYear v-if="!showDateTimeRange"></LayoutActivityYear>
+        <LayoutDataTiming v-if="!showDateTimeRange" class="ml-2"></LayoutDataTiming>
+        <LayoutDataTimingRange class="ml-n1" v-on:showDateTimeRange="showDateTimeRange=$event"></LayoutDataTimingRange>
       </v-card>
     </v-card>
   </main>
 </template>
 
+<script lang="ts">
+  import {computed, defineComponent, useContext, ref} from '@nuxtjs/composition-api'
+
+  export default defineComponent({
+    setup() {
+      const {store} = useContext()
+      const showDateTimeRange = ref(false)
+      const displayedSubHeader = computed(()=>store.state.profile.access.hasLateralMenu || store.state.profile.access.isTeacher)
+      return {
+        displayedSubHeader,
+        showDateTimeRange
+      }
+    }
+  })
+</script>
+
+
 <style scoped>
 
 </style>

+ 69 - 36
components/Ui/Input/DatePicker.vue

@@ -1,34 +1,41 @@
 <template>
-  <v-menu
-    v-model="dateOpen"
-    :close-on-content-click="false"
-    :nudge-right="40"
-    transition="scale-transition"
-    offset-y
-    min-width="auto"
-  >
-    <template v-slot:activator="{ on, attrs }">
-      <v-text-field
-        v-model="dateFormatted"
-        :label="$t(label_field)"
-        prepend-icon="mdi-calendar"
-        :disabled="readOnly"
-        v-bind="attrs"
-        v-on="on"
-      ></v-text-field>
-    </template>
-    <v-date-picker
-      v-model="dateParsed"
-      @input="dateOpen = false"
-      locale="fr"
-      color="ot_green lighten-1"
-    ></v-date-picker>
-  </v-menu>
+  <main>
+    <v-menu
+      v-model="dateOpen"
+      :close-on-content-click="false"
+      :nudge-right="40"
+      transition="scale-transition"
+      offset-y
+      min-width="auto"
+    >
+      <template v-slot:activator="{ on, attrs }">
+        <v-text-field
+          v-model="datesFormatted"
+          autocomplete="off"
+          :label="$t(label_field)"
+          prepend-icon="mdi-calendar"
+          :disabled="readOnly"
+          v-bind="attrs"
+          v-on="on"
+          :dense="dense"
+          :single-line="singleLine"
+        ></v-text-field>
+      </template>
+      <v-date-picker
+        v-model="datesParsed"
+        @input="dateOpen = range && datesParsed.length < 2"
+        locale="fr"
+        :range="range"
+        color="ot_green lighten-1"
+      ></v-date-picker>
+    </v-menu>
+  </main>
 </template>
 
 
 <script lang="ts">
-  import {defineComponent, watch, ref, useContext, onUnmounted} from '@nuxtjs/composition-api'
+  import {defineComponent, watch, ref, useContext, onUnmounted, computed} from '@nuxtjs/composition-api'
+  import DatesUtils from "~/services/utils/datesUtils";
 
   export default defineComponent({
     props: {
@@ -41,24 +48,50 @@
         required: false
       },
       data: {
-        type: String,
+        type: [String, Array],
         required: false
       },
       readOnly: {
         type: Boolean,
         required: false
       },
+      range: {
+        type: Boolean,
+        required: false
+      },
+      dense: {
+        type: Boolean,
+        required: false
+      },
+      singleLine: {
+        type: Boolean,
+        required: false
+      }
     },
     setup(props, {emit}){
-      const {data, field} = props
+      const {data, field, range} = props
       const {$moment} = useContext()
+      const dateUtils = new DatesUtils($moment)
+
+      const datesParsed = range ? ref(Array<string>()) : ref()
 
-      const dateFormatted = ref($moment(data).format('DD/MM/YYYY'))
-      const dateParsed = ref($moment(data).format('YYYY-MM-DD'))
+      if(range){
+        for(const date of data as Array<string>){
+          if(date)
+            datesParsed.value.push($moment(date).format('YYYY-MM-DD'))
+        }
+      }else{
+        datesParsed.value = $moment(data as string).format('YYYY-MM-DD')
+      }
+
+      const datesFormatted = computed(()=>{
+       return dateUtils.formattedDate(datesParsed.value, 'DD/MM/YYYY')
+      })
 
-      const unwatch = watch(dateParsed, (newValue) => {
-        dateFormatted.value = $moment(newValue).format('DD/MM/YYYY')
-        emit('update', newValue, field)
+      const unwatch = watch(datesParsed, (newValue, oldValue) => {
+        if(newValue === oldValue) return
+        if(range && newValue.length < 2) return
+        emit('update', range ? dateUtils.sortDate(newValue) : newValue, field)
       })
 
       onUnmounted(()=>{
@@ -67,9 +100,9 @@
 
       return {
         label_field : props.label ?? props.field,
-        dateParsed,
-        dateFormatted,
-        dateOpen: false
+        datesParsed,
+        datesFormatted,
+        dateOpen: ref(false)
       }
     }
   })

+ 1 - 0
components/Ui/Input/Text.vue

@@ -1,5 +1,6 @@
 <template>
     <v-text-field
+      autocomplete="off"
       :value="data"
       :label="$t(label_field)"
       @change="$emit('update', $event, field)"

+ 2 - 0
config/nuxtConfig/build.js

@@ -7,6 +7,8 @@ export default {
   // // Auto import components (https://go.nuxtjs.dev/config-components)
   components: true,
 
+  loading: '~/components/Layout/Loading.vue',
+
   // Build Configuration (https://go.nuxtjs.dev/config-build)
   build: {
     extend (config, { isDev, isClient }) {

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

@@ -92,5 +92,13 @@ export default (context, locale) => {
     cotisation_year: 'Année de cotisation',
     multiAccesses: 'Mes structures',
     familyAccesses: 'Changement d\'utilisateur',
+    display_data: 'Afficher les données',
+    past: 'Passée',
+    present: 'Présent',
+    future: 'Future',
+    notification: 'Notifications',
+    history_help: 'Personnaliser la pédiode d\'affichage',
+    period_choose: 'Période à afficher',
+    date_choose: 'Choix de la période',
   })
 }

+ 5 - 1
models/Access/Access.ts → models/Access/MyProfile.ts

@@ -1,6 +1,7 @@
 import {Attr, Num, Model} from '@vuex-orm/core'
+import {Historical} from "~/types/interfaces";
 
-export class Access extends Model{
+export class MyProfile extends Model{
   static entity = 'accesses'
 
   @Attr(null)
@@ -8,4 +9,7 @@ export class Access extends Model{
 
   @Num(0, {nullable: true})
   activityYear!: number
+
+  @Attr({})
+  historical!: Historical
 }

+ 3 - 0
plugins/route.ts

@@ -10,6 +10,9 @@ const routePlugin: Plugin = (ctx) => {
         next()
       }
     })
+
+    ctx.app.router.afterEach(()=>{
+    })
   }
 }
 

+ 17 - 9
services/connection/connection.ts

@@ -30,21 +30,21 @@ class Connection{
     switch (method) {
       case HTTP_METHOD.GET:
         if(args.id)
-          return this.getItem(url, args.id)
+          return this.getItem(url, args.id, args.progress)
         else
-          return this.getCollection(url)
+          return this.getCollection(url, args.progress)
 
       case HTTP_METHOD.PUT:
         if(this.isArgsIsDataPersisterArgs(args)){
           if(!args.data)
             throw new Error('data not found')
 
-          return this.put(url, args.id, args.data)
+          return this.put(url, args.id, args.data, args.progress)
         }
         else throw new Error('args not a dataPersisterArgs')
 
       case HTTP_METHOD.DELETE:
-        return this.deleteItem(url, args.id)
+        return this.deleteItem(url, args.id, args.progress)
     }
 
     throw new Error('Method unknown')
@@ -54,12 +54,14 @@ class Connection{
    * GET Item : préparation de la config pour la récupération d'un item
    * @param {string} url
    * @param {number} id
+   * @param {boolean} progress
    * @return {Promise<any>}
    */
-  private getItem(url: string, id: number): Promise<any>{
+  private getItem(url: string, id: number, progress: boolean = true): Promise<any>{
     const config:AxiosRequestConfig = {
       url: `${url}/${id}`,
       method: HTTP_METHOD.GET,
+      progress: progress
     }
     return this.request(config)
   }
@@ -67,12 +69,14 @@ class Connection{
   /**
    * Get collection : préparation de la config pour la récupération d'une collection d'items
    * @param {string} url
+   * @param {boolean} progress
    * @return {Promise<any>}
    */
-  private getCollection(url: string): Promise<any>{
+  private getCollection(url: string, progress: boolean = true): Promise<any>{
     const config:AxiosRequestConfig = {
       url: `${url}`,
       method: HTTP_METHOD.GET,
+      progress: progress
     }
     return this.request(config)
   }
@@ -82,13 +86,15 @@ class Connection{
    * @param {string} url
    * @param {number} id
    * @param {AnyJson} data
+   * @param {boolean} progress
    * @return {Promise<any>}
    */
-  private put(url: string, id: number, data: AnyJson): Promise<any>{
+  private put(url: string, id: number, data: AnyJson, progress: boolean = true): Promise<any>{
     const config:AxiosRequestConfig = {
       url: `${url}/${id}`,
       method: HTTP_METHOD.PUT,
-      data: data
+      data: data,
+      progress: progress
     }
     return this.request(config)
   }
@@ -97,12 +103,14 @@ class Connection{
    * DELETE Item : préparation de la config pour la suppression d'un item
    * @param {string} url
    * @param {number} id
+   * @param {boolean} progress
    * @return {Promise<any>}
    */
-  private deleteItem(url: string, id: number): Promise<any>{
+  private deleteItem(url: string, id: number, progress: boolean = true): Promise<any>{
     const config:AxiosRequestConfig = {
       url: `${url}/${id}`,
       method: HTTP_METHOD.DELETE,
+      progress: progress
     }
     return this.request(config)
   }

+ 47 - 29
services/dataPersister/dataPersister.ts

@@ -1,19 +1,23 @@
 import {hooks} from "~/services/dataPersister/hook/_import";
-import {AnyJson, DataPersisterArgs} from "~/types/interfaces";
+import {AnyJson, DataPersisterArgs, DataProviderArgs} from "~/types/interfaces";
 import {Context} from "@nuxt/types/app";
 import Connection from "~/services/connection/connection";
 import ConstructUrl from "~/services/connection/constructUrl";
-import {HTTP_METHOD} from "~/types/enums";
+import {HTTP_METHOD, QUERY_TYPE} from "~/types/enums";
 import Serializer from "~/services/serializer/serializer";
 import ApiError from "~/services/utils/apiError";
 import DataProvider from "~/services/dataProvider/dataProvider";
 
 class DataPersister{
   private ctx !: Context
-  private arguments!: DataPersisterArgs
+  private defaultArguments: DataPersisterArgs
 
   constructor() {
     this.sort()
+    this.defaultArguments = {
+      type: QUERY_TYPE.MODEL,
+      progress:true
+    }
   }
 
   initCtx(ctx:Context){
@@ -21,59 +25,73 @@ class DataPersister{
     this.ctx = ctx
   }
 
+  getArguments(args: DataPersisterArgs):DataPersisterArgs{
+    return { ...this.defaultArguments, ...args }
+  }
+
   async invoke(args:DataPersisterArgs): Promise<any>{
-    this.arguments = args
     try{
-      this.preHook()
+      const dpArgs = this.getArguments(args)
+
+      this.startLoading(dpArgs)
+
+      this.preHook(dpArgs)
 
-      this.arguments.data = this.serialization()
+      dpArgs.data = this.serialization(dpArgs)
 
-      const url = this.constructUrl()
+      const url = this.constructUrl(dpArgs)
 
-      const response = await this.connection(url)
+      const response = await this.connection(url, dpArgs)
 
-      this.provideResponse(response)
+      this.provideResponse(response, dpArgs)
     }catch(error){
      throw new ApiError(error.response.status, error.response.data.detail)
     }
   }
 
-  async preHook(){
+  startLoading(args: DataPersisterArgs){
+    if(args.progress){
+      const $nuxt = window['$nuxt']
+      $nuxt.$loading.start()
+    }
+  }
+
+  async preHook(args: DataPersisterArgs){
     for(const hook of hooks){
-      if(hook.support(this.arguments)){
-        await new hook().invoke(this.arguments)
+      if(hook.support(args)){
+        await new hook().invoke(args)
       }
     }
   }
 
-  serialization(){
+  serialization(args: DataPersisterArgs){
     const serializer = new Serializer()
-    return serializer.normalize(this.arguments)
+    return serializer.normalize(args)
   }
 
-  constructUrl(): string{
+  constructUrl(args: DataPersisterArgs): string{
     const constructUrl = new ConstructUrl()
-    return constructUrl.invoke(this.arguments)
+    return constructUrl.invoke(args)
   }
 
-  connection(url: string): Promise<any>{
+  connection(url: string, args: DataPersisterArgs): Promise<any>{
     const connection = new Connection()
-    return connection.invoke(this.arguments.id ? HTTP_METHOD.PUT : HTTP_METHOD.POST, url, this.arguments)
+    return connection.invoke(args.id ? HTTP_METHOD.PUT : HTTP_METHOD.POST, url, args)
   }
 
-  async provideResponse(response: AnyJson){
+  async provideResponse(response: AnyJson, args: DataPersisterArgs){
     const dataProvider = new DataProvider()
-    dataProvider.setArguments({
-      type: this.arguments.type,
-      url: this.arguments.url,
-      enumType: this.arguments.enumType,
-      model: this.arguments.model,
-      root_model: this.arguments.root_model,
-      id: this.arguments.id,
-      root_id: this.arguments.root_id
-    })
+    const dataProviderArgs: DataProviderArgs = {
+      type: args.type,
+      url: args.url,
+      enumType: args.enumType,
+      model: args.model,
+      root_model: args.root_model,
+      id: args.id,
+      root_id: args.root_id
+    }
     const deserializeResponse = dataProvider.deserialization(response)
-    return await dataProvider.provide(deserializeResponse)
+    return await dataProvider.provide(deserializeResponse, dataProviderArgs)
   }
 
   sort(){

+ 32 - 18
services/dataProvider/dataProvider.ts

@@ -1,5 +1,5 @@
-import {AnyJson, DataProviderArgs, DataProviders} from "~/types/interfaces";
-import {DENORMALIZER_TYPE, HTTP_METHOD} from "~/types/enums";
+import {AnyJson, DataProviderArgs} from "~/types/interfaces";
+import {DENORMALIZER_TYPE, HTTP_METHOD, QUERY_TYPE} from "~/types/enums";
 import {providers} from "~/services/dataProvider/provider/_import";
 import ConstructUrl from "~/services/connection/constructUrl";
 import Connection from "~/services/connection/connection";
@@ -9,13 +9,13 @@ import ApiError from "~/services/utils/apiError";
 
 class DataProvider{
   private ctx !: Context;
-  private arguments!: DataProviderArgs;
+  private defaultArguments!: DataProviderArgs;
 
   constructor() {
-  }
-
-  setArguments(args: DataProviderArgs){
-    this.arguments = args
+    this.defaultArguments = {
+      type: QUERY_TYPE.MODEL,
+      progress:false
+    }
   }
 
   initCtx(ctx:Context){
@@ -23,36 +23,50 @@ class DataProvider{
     this.ctx = ctx
   }
 
+  getArguments(args: DataProviderArgs): DataProviderArgs{
+    return { ...this.defaultArguments, ...args }
+  }
+
   async invoke(args:DataProviderArgs): Promise<any>{
-    this.setArguments(args)
 
     try{
-      const url = this.constructUrl()
+      const dpArgs = this.getArguments(args)
 
-      const response = await this.connection(url)
+      this.startLoading(dpArgs)
+
+      const url = this.constructUrl(dpArgs)
+
+      const response = await this.connection(url, dpArgs)
 
       const deserializeResponse = await this.deserialization(response)
 
-      return await this.provide(deserializeResponse)
+      return await this.provide(deserializeResponse, dpArgs)
     }catch(error){
       throw new ApiError(500, error)
     }
   }
 
-  constructUrl(): string{
+  startLoading(args: DataProviderArgs){
+    if(args.progress){
+      const $nuxt = window['$nuxt']
+      $nuxt.$loading.start()
+    }
+  }
+
+  constructUrl(args: DataProviderArgs): string{
     const constructUrl = new ConstructUrl();
-    return constructUrl.invoke(this.arguments)
+    return constructUrl.invoke(args)
   }
 
-  connection(url: string): Promise<any>{
+  connection(url: string, args: DataProviderArgs): Promise<any>{
     const connection = new Connection()
-    return connection.invoke(HTTP_METHOD.GET, url, this.arguments)
+    return connection.invoke(HTTP_METHOD.GET, url, args)
   }
 
-  provide(data: AnyJson): any{
+  provide(data: AnyJson, args: DataProviderArgs): any{
     for(const provider of providers){
-      if(provider.support(this.arguments)){
-         return new provider(this.ctx, this.arguments).invoke(data);
+      if(provider.support(args)){
+         return new provider(this.ctx, args).invoke(data);
       }
     }
   }

+ 31 - 0
services/utils/datesUtils.ts

@@ -0,0 +1,31 @@
+import moment from 'moment'
+
+export default class DatesUtils {
+  private $moment:typeof moment;
+
+  constructor(momentInstance:any) {
+    this.$moment = momentInstance
+  }
+
+  formattedDate(dates:any, format:string): string{
+    const d_format:Array<string> = []
+    if(dates instanceof Array){
+      for(const date of dates){
+        d_format.push(this.$moment(date).format(format))
+      }
+    }else{
+      d_format.push(this.$moment(dates as string).format(format))
+    }
+    return d_format.join(' - ')
+  }
+
+  sortDate(dates:Array<string>): Array<string>{
+    return dates.sort( (a, b) => {
+      if (a > b)
+        return 1;
+      if (a < b)
+        return -1;
+      return 0;
+    });
+  }
+}

+ 6 - 1
store/profile/access.ts

@@ -1,5 +1,5 @@
 import {$roleUtils} from '~/services/rights/roleUtils'
-import {AbilitiesType, accessState, baseAccessState, baseOrganizationState} from "~/types/interfaces";
+import {AbilitiesType, accessState, AnyJson, baseAccessState, baseOrganizationState, Historical} from "~/types/interfaces";
 import * as _ from "lodash";
 import {GENDER} from "~/types/enums";
 
@@ -12,6 +12,7 @@ export const state = () => ({
   gender: null,
   avatarId: null,
   activityYear: null,
+  historical: [],
   roles: [],
   abilities: [],
   isAdmin: false,
@@ -57,6 +58,9 @@ export const mutations = {
   setActivityYear(state:accessState, activityYear:number){
     state.activityYear = activityYear
   },
+  setHistorical(state:accessState, historical:Historical){
+    state.historical = historical
+  },
   setRoles(state:accessState, roles:Array<string>){
     state.roles = roles
   },
@@ -125,6 +129,7 @@ export const actions = {
     context.commit('setGender', profile.gender)
     context.commit('setAvatarId', profile.avatarId)
     context.commit('setActivityYear', profile.activityYear)
+    context.commit('setHistorical', profile.historical)
     context.commit('setIsAdminAccess', profile.isAdminAccess)
     context.commit('setIsAdmin', $roleUtils.isA('ADMIN', roles_to_array))
     context.commit('setIsAdministratifManager', $roleUtils.isA('ADMINISTRATIF_MANAGER', roles_to_array))

+ 0 - 39
tests/unit/use/updater/useActivityYearUpdater.spec.ts

@@ -1,39 +0,0 @@
-import {createStore} from "~/tests/unit/Helpers";
-import {AnyStore} from "~/types/interfaces";
-import DataPersister from "~/services/dataPersister/dataPersister";
-import {UseActivityYearUpdater} from "~/use/updater/useActivityYearUpdater";
-import {repositoryHelper} from "~/services/store/repository";
-
-var store:AnyStore
-var dataPersister:DataPersister
-var useActivityYearUpdater:any
-
-beforeAll(()=>{
-  store = createStore();
-  dataPersister = new DataPersister()
-  repositoryHelper.setStore(store)
-  useActivityYearUpdater = new UseActivityYearUpdater(store, dataPersister) as any
-})
-
-describe('update()', () => {
-  it('should throw an error if year is negative nor eq to 0', () => {
-     expect(() => useActivityYearUpdater.update(-1)).rejects.toThrow();
-  })
-})
-
-describe('createNewAccessInstance()', () => {
-  it('should create an Access instance with id, and activityYear', () => {
-    const access = useActivityYearUpdater.createNewAccessInstance(1, 2020)
-    expect(access.id).toStrictEqual(1)
-    expect(access.activityYear).toStrictEqual(2020)
-  })
-
-  it('should throw an error if accessID is negative nor eq to 0', () => {
-    expect( () => useActivityYearUpdater.createNewAccessInstance(-1, 2020)).toThrow()
-  })
-
-  it('should throw an error if year is negative nor eq to 0', () => {
-    expect( () => useActivityYearUpdater.createNewAccessInstance(1, 0)).toThrow()
-  })
-})
-

+ 64 - 0
tests/unit/use/updater/useMyProfileUpdater.spec.ts

@@ -0,0 +1,64 @@
+import {createStore} from "~/tests/unit/Helpers";
+import {accessState, AnyStore} from "~/types/interfaces";
+import DataPersister from "~/services/dataPersister/dataPersister";
+import {UseMyProfileUpdater} from "~/use/updater/useMyProfileUpdater";
+import {repositoryHelper} from "~/services/store/repository";
+
+var store:AnyStore
+var dataPersister:DataPersister
+var useMyProfileUpdater:any
+
+beforeAll(()=>{
+  store = createStore();
+  dataPersister = new DataPersister()
+  repositoryHelper.setStore(store)
+  useMyProfileUpdater = new UseMyProfileUpdater(store, dataPersister) as any
+})
+
+describe('setActivityYear()', () => {
+  it('should throw an error if year is negative nor eq to 0', () => {
+     expect(() => useMyProfileUpdater.setActivityYear(-1)).toThrow();
+  })
+  it('should call updateStoreFromField', () => {
+    repositoryHelper.updateStoreFromField  =  jest.fn()
+    useMyProfileUpdater.setActivityYear(2020)
+    expect(repositoryHelper).toHaveBeenCalled
+  })
+})
+
+describe('setHistorical()', () => {
+  it('should call updateStoreFromField', () => {
+    repositoryHelper.updateStoreFromField  =  jest.fn()
+    useMyProfileUpdater.setHistorical(['present', 'future'])
+    expect(repositoryHelper).toHaveBeenCalled
+  })
+})
+
+describe('getHistoricalEntry()', () => {
+  it('should return an json object', () => {
+    const historical = useMyProfileUpdater.getHistoricalEntry(['present', 'future'])
+    expect(historical).toStrictEqual({past: false, present: true, future: true})
+  })
+})
+
+describe('getHistoricalRangeEntry()', () => {
+  it('should return an json object', () => {
+    const historical = useMyProfileUpdater.getHistoricalRangeEntry(['2020/01/01', '2020/01/31'])
+    expect(historical).toStrictEqual({past: false, present: false, future: false, dateStart: '2020/01/01', dateEnd: '2020/01/31'})
+  })
+})
+
+describe('createNewMyProfileInstance()', () => {
+  it('should create an My Profile instance', () => {
+    const state_my_profile = {
+      id: 1,
+      activityYear: 2020,
+      historical: {past: false, present: true, future: false}
+    } as accessState
+    const myProfile = useMyProfileUpdater.createNewMyProfileInstance(state_my_profile)
+    expect(myProfile.id).toStrictEqual(1)
+    expect(myProfile.activityYear).toStrictEqual(2020)
+    expect(myProfile.historical).toStrictEqual({past: false, present: true, future: false})
+  })
+})
+

+ 10 - 0
types/interfaces.d.ts

@@ -70,10 +70,19 @@ interface baseAccessState {
   avatarId: number
 }
 
+interface Historical {
+  future?: boolean,
+  past?: boolean,
+  present?: boolean,
+  dateStart?: string,
+  dateEnd?: string
+}
+
 interface accessState extends baseAccessState{
   bearer: string,
   switchId: number,
   activityYear: number,
+  historical: Historical,
   roles: Array<string>,
   abilities: Array<AbilitiesType>,
   isAdminAccess: boolean,
@@ -132,6 +141,7 @@ interface UrlArgs {
   readonly root_model?: typeof Model,
   readonly id?:any,
   readonly root_id?:number
+  readonly progress?:boolean
 }
 
 interface DataPersisterArgs extends UrlArgs{

+ 0 - 69
use/updater/useActivityYearUpdater.ts

@@ -1,69 +0,0 @@
-import {repositoryHelper} from "~/services/store/repository";
-import {Access} from "~/models/Access/Access";
-import {QUERY_TYPE} from "~/types/enums";
-import {computed} from '@nuxtjs/composition-api'
-import {AnyJson, AnyStore} from "~/types/interfaces";
-import DataPersister from "~/services/dataPersister/dataPersister";
-
-/**
- * @category Use/updater
- * @class UseActivityYearUpdater
- * Use Classe pour la mise à jour de l'activity year
- */
-export class UseActivityYearUpdater{
-  private store!:AnyStore
-  private $dataPersister!:DataPersister
-
-  constructor(store:AnyStore, $dataPersister:DataPersister){
-    this.store = store
-    this.$dataPersister = $dataPersister
-  }
-  /**
-   * Composition function
-   */
-  public invoke(): AnyJson{
-    const activityYear = computed(()=>this.store.state.profile.access.activityYear)
-
-    return {
-      updater: (activityYear:number) => this.update(activityYear),
-      activityYear
-    }
-  }
-
-  /**
-   * Mets à jour le store, puis créer une instance de Access avec la nouvelle activityYear et là met à jour
-   * @param activityYear
-   */
-  private async update(activityYear:number): Promise<any>{
-    if(activityYear <= 0)
-      throw new Error('year must be positive')
-
-    this.store.commit('profile/access/setActivityYear', activityYear)
-
-    const accessInstance:Access = this.createNewAccessInstance(this.store.state.profile.access.id, this.store.state.profile.access.activityYear)
-
-    await this.$dataPersister.invoke({
-      type: QUERY_TYPE.MODEL,
-      model: Access,
-      id: accessInstance.id
-    })
-  }
-
-  /**
-   * Créer une nouvelle instance d'Access (== nouveau state dans le store) avec une date d'activité et la retourne
-   * @param accessId
-   * @param activityYear
-   */
-  private createNewAccessInstance(accessId: number, activityYear:number): Access{
-    if(accessId <= 0 || activityYear <= 0)
-      throw new Error('id and activity year must be positive')
-
-    const accessInstance = repositoryHelper.createNewModelInstance(Access) as Access
-    accessInstance.id = accessId
-    accessInstance.activityYear = activityYear
-    repositoryHelper.persist(Access, accessInstance)
-    return accessInstance
-  }
-}
-
-export const $useActivityYearUpdater = (store:AnyStore, $dataPersister:DataPersister) => new UseActivityYearUpdater(store, $dataPersister).invoke()

+ 112 - 0
use/updater/useMyProfileUpdater.ts

@@ -0,0 +1,112 @@
+import {repositoryHelper} from "~/services/store/repository";
+import {QUERY_TYPE} from "~/types/enums";
+import {computed} from '@nuxtjs/composition-api'
+import {accessState, AnyJson, AnyStore} from "~/types/interfaces";
+import DataPersister from "~/services/dataPersister/dataPersister";
+import {MyProfile} from "~/models/Access/MyProfile";
+
+/**
+ * @category Use/updater
+ * @class UseMyProfileUpdater
+ * Use Classe pour la gestion REST de MyProfile
+ */
+export class UseMyProfileUpdater{
+  private store!:AnyStore
+  private $dataPersister!:DataPersister
+  private myProfile!:MyProfile
+
+  constructor(store:AnyStore, $dataPersister:DataPersister){
+    this.store = store
+    this.$dataPersister = $dataPersister
+  }
+  /**
+   * Composition function
+   */
+  public invoke(): AnyJson{
+    this.myProfile = this.createNewMyProfileInstance(this.store.state.profile.access)
+    const activityYear = computed(()=>this.myProfile.activityYear)
+    const historical = computed(()=>this.myProfile.historical)
+
+    return {
+      updateMyProfile: () => this.updateMyProfile(),
+      setActivityYear: (activityYear:number) => this.setActivityYear(activityYear),
+      setHistorical: (historicalChoices:Array<string>) => this.setHistorical(historicalChoices),
+      setHistoricalRange: (dates:Array<string>) => this.setHistoricalRange(dates),
+      activityYear,
+      historical
+    }
+  }
+
+  /**
+   * Créer une nouvelle instance MyProfile
+   * @param myProfile
+   */
+  private createNewMyProfileInstance(myProfile:accessState): MyProfile{
+    const myProfileInstance = repositoryHelper.createNewModelInstance(MyProfile) as MyProfile
+    myProfileInstance.id = myProfile.id
+    myProfileInstance.activityYear = myProfile.activityYear
+    myProfileInstance.historical = myProfile.historical
+    repositoryHelper.persist(MyProfile, myProfileInstance)
+    return myProfileInstance
+  }
+  /**
+   * Mets à jour l'activity de my profile
+   * @param activityYear
+   */
+  private setActivityYear(activityYear:number){
+    if(activityYear <= 0)
+      throw new Error('year must be positive')
+
+    repositoryHelper.updateStoreFromField(MyProfile, this.myProfile, activityYear, 'activityYear')
+  }
+
+  /**
+   * Mets à jour l'historical de my profile
+   * @param historicalChoices
+   */
+  private setHistorical(historicalChoices:Array<string>){
+    repositoryHelper.updateStoreFromField(MyProfile, this.myProfile, this.getHistoricalEntry(historicalChoices), 'historical')
+  }
+
+  /**
+   * Mets à jour l'historical de my profile
+   * @param dates
+   */
+  private setHistoricalRange(dates:Array<string>){
+    repositoryHelper.updateStoreFromField(MyProfile, this.myProfile, this.getHistoricalRangeEntry(dates), 'historical')
+  }
+
+  /**
+   * Transform les choix de l'historique en objet JSON reconnaissable coté API
+   * @param historicalChoices
+   */
+  private getHistoricalEntry(historicalChoices:Array<string>){
+    const historicalDefault:any = {'past':false, 'future':false, 'present':false}
+
+    for(const historicalChoice of historicalChoices){
+      historicalDefault[historicalChoice] = true
+    }
+    return historicalDefault;
+  }
+
+  /**
+   * Trasnforme le choix des période en Objet JSON reconnaissable coté API
+   * @param dates
+   */
+  private getHistoricalRangeEntry(dates:Array<string>){
+    return {'past':false, 'future':false, 'present':false, dateStart:dates[0], dateEnd:dates[1]}
+  }
+
+  /**
+   * Effectue la mise à jour (coté API) de MyProfile
+   */
+  private async updateMyProfile(): Promise<any>{
+    await this.$dataPersister.invoke({
+      type: QUERY_TYPE.MODEL,
+      model: MyProfile,
+      id: this.myProfile.id
+    })
+  }
+}
+
+export const $useMyProfileUpdater = (store:AnyStore, $dataPersister:DataPersister) => new UseMyProfileUpdater(store, $dataPersister).invoke()