Browse Source

complete subheader (historical, lists) and various fixes

Olivier Massot 3 years ago
parent
commit
7926f629a1

+ 28 - 0
.env.test

@@ -0,0 +1,28 @@
+## LOCAL ENVIRONMENT FILE
+ENV=dev
+DEBUG=1
+
+HOST=0
+PORT=3000
+
+## API Base Url
+NUXT_BASE_URL=https://ap2i.test.opentalent.fr
+NUXT_PUBLIC_BASE_URL=https://ap2i.test.opentalent.fr
+
+# Legacy API Base Url
+NUXT_BASE_URL_LEGACY=http://nginx
+NUXT_PUBLIC_BASE_URL_LEGACY=https://api.test.opentalent.fr
+
+# Legacy Admin Base Url
+NUXT_BASE_URL_ADMIN_LEGACY=https://admin.test.opentalent.fr/#
+NUXT_PUBLIC_BASE_URL_ADMIN_LEGACY=https://admin.test.opentalent.fr/#
+
+# Typo3 Base Url
+NUXT_BASE_URL_TYPO3=https://test.opentalent.fr/###subDomain###
+NUXT_PUBLIC_BASE_URL_TYPO3=https://test.opentalent.fr/###subDomain###
+
+# Mercure push events
+NUXT_BASE_URL_MERCURE=https://mercure.test.opentalent.fr/.well-known/mercure
+NUXT_PUBLIC_BASE_URL_MERCURE=https://mercure.test.opentalent.fr/.well-known/mercure
+
+MERCURE_SUBSCRIBER_JWT_KEY=NQEupdREijrfYvCmF2mnvZQFL9zLKDH9RCYter6tUWzjemPqzicffhc2fSf0yEmM

+ 96 - 7
README.md

@@ -1,22 +1,111 @@
 # App - Migration Nuxt 3
 
-Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
+Frontend développé avec NuxtJs 3
 
-## Setup
+A voir :
 
-Make sure to install the dependencies:
+* [vuejs.org](https://vuejs.org/guide/introduction.html) : Vue est le framework de base de l'application
+* [nuxtjs.org](https://nuxt.com/docs/getting-started/introduction) : Nuxt est une surcouche à Vue qui automatise et simplifie beaucoup de choses
+* [pinia.vuejs.org](https://pinia.vuejs.org/introduction.html) : Store library that allow you to share a state accross your components / pages
+* [pinia-orm.codedredd.de](https://pinia-orm.codedredd.de/guide/getting-started/quick-start) : Ajoute une gestion par modèles / repos au store Pinia
+* [vuetifyjs.com](https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg) : Composants graphiques préconstruits
+* [typescriptlang.org](https://www.typescriptlang.org/) : Typescript
+* [jestjs.io](https://jestjs.io/docs/getting-started) : Tests unitaires pour JS
+* [cypress.io](https://docs.cypress.io/guides/getting-started/installing-cypress) : Tests end-to-end pour JS
+
+
+## Installation (mode dev)
+
+Cloner le projet :
+
+    git clone git@gitlab.2iopenservice.com:opentalent/app_nuxt3.git
+
+
+Installer les dépendances :
 
     yarn install
 
-Create the .env symlink :
+Créer le symlink vers le bon fichier env (remplacer `<environnement>` par l'env courant):
+
+    ln -s .env.<environnement> .env
 
-    ln -s .env.local .env
 
-Copy the cert files in the root-dir of this project :
+Copier les certificats à la racine de ce projet :
 
 * local.app-v3.opentalent.fr.crt
 * local.app-v3.opentalent.fr.key
 
-Start the development server :
+
+Lancer le serveur de développement :
 
     yarn dev
+
+
+
+## Déploiement en prod
+
+### Premier déploiement en tant que service
+
+On commence par cloner le projet app, puis par se placer dans le répertoire ainsi créé.
+
+On créé un symlink vers le fichier .env.xxx voulu sous le nom de .env (selon l'environnement)
+
+    ln -s .env.xxx .env
+
+Pour déployer le projet en mode SSR, on commence par mettre à jour et compiler avec la commande custom :
+
+    yarn deploy
+
+Cette commande est un alias qui équivaut à lancer:
+
+    git pull
+    yarn install
+    yarn build
+
+### Mettre à jour
+
+Se placer dans le répertoire de l'application, puis lancer :
+
+    yarn deploy
+
+
+
+## Autres
+
+### Lancer les tests
+
+Pour lancer les tests unitaires :
+
+    jest
+
+### Générer la doc
+
+Pour régénérer la documentation automatique :
+
+    yarn docs
+
+
+## Plus d'infos
+
+## Structure du projet
+
+| Répertoire     | Rôle                                                                                                |
+|----------------|-----------------------------------------------------------------------------------------------------|
+| `assets`       | Contient les fichiers style et medias                                                               |
+| `components`   | Les différents composants graphiques qui composent l'application                                    |
+| `composables`  | Des fonctions conscientes du contexte applicatif, qui font le lien entre les pages et les services  |
+| `config`       | La configuration de l'application                                                                   |
+| `doc`          | Documentation du projet                                                                             |
+| `lang`         | Les fichiers de traduction                                                                          |
+| `layouts`      | Layouts des pages                                                                                   |
+| `middleware`   | Code exécuté avant le rendu des pages (ex: pour vérifier l'authentification)                        |
+| `models`       | Définition des entités (ou modèles)                                                                 |
+| `node_modules` | Modules node installés via npm                                                                      |
+| `pages`        | Définition des pages qui composent l'application                                                    |
+| `plugins`      | Configuration des modules                                                                           |
+| `public`       | Ressources statiques et publiques                                                                   |
+| `services`     | Rassemble les classes utilitaires non graphiques et indépendantes du contexte applicatif            |
+| `stores`       | Le store et ses composants servent d'entrepôt de donnés, et s'assurent de la cohérence de celles-ci |
+| `tests`        | Regroupe les tests (unitaires, end-to-end...)                                                       |
+| `types`        | Types Typescript (interfaces, enums...)                                                             |
+

+ 1 - 1
components/Layout/SubHeader/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 font-weight-bold">{{ $t(label) }} : </span>
     <UiXeditableText
       class="activity-year-input bg-ot-light-grey"
       type="number"

+ 64 - 56
components/Layout/SubHeader/DataTiming.vue

@@ -1,85 +1,93 @@
 <template>
   <main class="d-flex align-baseline">
-    <span class="mr-2 ot-dark_grey--text font-weight-bold">{{ $t('display_data') }} : </span>
+    <span class="mr-2 text-ot-dark_grey font-weight-bold">{{ $t('display_data') }} : </span>
 
     <v-btn-toggle
-      v-model="historicalBtn"
-      dense
-      class="ot-light_grey toggle-btn"
-      active-class="ot-green ot-white--text"
+      ref="toggle"
+      :model-value="historicalValue"
+      density="compact"
+      :color="color"
       multiple
+      divider
+      border
+      :rounded="true"
+      class="bg-ot-light-grey toggle-btn"
+      @update:modelValue="onUpdate"
     >
-      <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-for="choice in historicalChoices"
+          :value="choice"
+          max-height="25"
+          :class="'font-weight-normal text-caption' + (historicalValue.includes(choice) ? ' btn-selected' : '')"
+      >
+        <!-- TODO: on ne devrait pas avoir besoin du if et de la classe 'btn-selected' dans v-btn, mais à l'heure
+         qu'il est, le component ne fonctionne pas comme attendu. A revoir quand vuetify 3 sera plus stable -->
+        {{ $t(choice) }}
       </v-btn>
     </v-btn-toggle>
   </main>
 </template>
 
-<script lang="ts">
+<script setup lang="ts">
 import {useFormStore} from "~/stores/form";
 import {useAccessProfileStore} from "~/stores/accessProfile";
 import {Ref} from "@vue/reactivity";
-import {WatchStopHandle} from "@vue/runtime-core";
+import {useEntityManager} from "~/composables/data/useEntityManager";
+import {useTheme} from "vuetify";
+import {Access} from "~/models/Access/Access";
+
+// TODO: en v3.0.5, pas de solution documentée pour renseigner directement la couler dans le template, à revoir
+const color = useTheme().current.value.colors['ot-green']
 
 const { setDirty } = useFormStore()
 const accessProfileStore = useAccessProfileStore()
+const { em } = useEntityManager()
 
-const historicalBtn: Ref<Array<number>> = initHistoricalBtn(accessProfileStore.historical as Array<any>)
+const toggle = ref(null)
 
-const unwatch: WatchStopHandle = watch(historicalBtn, async (newValue) => {
-  const historicalChoice: Array<string> = initHistoricalChoice(newValue)
+const historicalChoices: Array<'past' | 'present' | 'future'> = ['past', 'present', 'future']
 
-  accessProfileStore.setHistorical(historicalChoice)
-  setDirty(false)
-  await updateMyProfile()
-  window.location.reload()
-})
-
-onUnmounted(() => {
-  unwatch()
-})
-
-/**
-   * Prépare le tableau de valeur numéraire devant être passé au component v-btn-toggle
-   * @param historical
-   */
-function initHistoricalBtn (historical: Array<any>) {
-  const timeChoice:Ref<Array<number>> = ref(Array<number>())
-  const historicalArray:Array<any> = ['past', 'present', 'future']
-
-  for (const key in historicalArray) {
-    if (historical[historicalArray[key]]) { timeChoice.value.push(parseInt(key)) }
-  }
-  return timeChoice
-}
+const historicalValue: Ref<Array<string>> = ref(historicalChoices.filter((item) => accessProfileStore.historical[item]))
 
-/**
-   * Transforme le résultat renvoyé par le component v-btn-toggle pour l'enregistrer coté AccessProfile
-   * @param historical
-   */
-function initHistoricalChoice (historical: Array<any>) {
-  const historicalArray:Array<any> = ['past', 'present', 'future']
+const onUpdate = async (newValue: Array<string>) => {
+  historicalValue.value = newValue
 
-  const historicalChoice:Array<string> = []
-  for (const key in historical) {
-    historicalChoice.push(historicalArray[historical[key]])
+  console.log(historicalValue.value)
+
+  if (accessProfileStore.id === null) {
+    throw new Error('Invalid profile id')
   }
-  return historicalChoice
+
+  accessProfileStore.setHistorical(
+      historicalValue.value.includes('past'),
+      historicalValue.value.includes('present'),
+      historicalValue.value.includes('future')
+  )
+
+  setDirty(false)
+
+  await em.patch(
+      Access,
+      accessProfileStore.id,
+      {'historical': accessProfileStore.historical}
+  )
+
+  // TODO: voir si c'est indispensable de reload la page entière
+  window.location.reload()
 }
+
 </script>
 
 <style scoped lang="scss">
-  .toggle-btn{
-    z-index: 1;
-    border-radius: 4px 0 0 4px;
+  .v-btn-group {
+    max-height: 22px;
+  }
+
+  .v-btn {
+    padding: 0 8px;
+  }
+
+  .v-btn.btn-selected {
+    background-color: rgb(var(--v-theme-ot-green)) !important;
   }
 </style>

+ 101 - 62
components/Layout/SubHeader/DataTimingRange.vue

@@ -1,65 +1,81 @@
 <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>
+  <main class="d-flex align-center data-timing-range">
+    <div v-if="show" class="d-flex align-center" style="max-height: 100%">
+      <span class="pl-2 mr-2 font-weight-bold">
+        {{ $t('period_choose') }}
+      </span>
+
       <UiInputDatePicker
-        class="time-range"
         label="date_choose"
         :data="datesRange"
-        :range="true"
-        :dense="true"
-        :single-line="true"
+        range
+        dense
+        single-line
+        height="22"
         @update="updateDateTimeRange"
       />
     </div>
 
-    <v-tooltip bottom>
-      <template #activator="{ on, attrs }">
-        <v-btn
-          class="time-btn"
-          max-height="25"
-          v-bind="attrs"
-          elevation="0"
-          max-width="10px"
-          min-width="10px"
-          v-on="on"
-          @click="show=!show"
-        >
-          <v-icon color="ot-grey" class="font-weight-normal" x-small>
-            fas fa-history
-          </v-icon>
-        </v-btn>
-      </template>
+    <v-btn
+        ref="btn"
+        class="time-btn ml-1"
+        height="22" min-height="22" max-height="22"
+        width="25" min-width="25" max-width="25"
+        elevation="0"
+        @click="show = !show"
+    >
+      <v-icon icon="fas fa-history" color="ot-grey" class="font-weight-normal" style="font-size: 14px;" />
+    </v-btn>
+
+    <v-tooltip location="bottom" :activator="btn">
       <span>{{ $t('history_help') }}</span>
     </v-tooltip>
   </main>
 </template>
 
 <script setup lang="ts">
-import {ComputedRef, Ref} from "@vue/reactivity";
-    import {useAccessProfileStore} from "~/stores/accessProfile";
-    import {useFormStore} from "~/stores/form";
+import {Ref} from "@vue/reactivity";
+import {useAccessProfileStore} from "~/stores/accessProfile";
+import {useFormStore} from "~/stores/form";
 import {WatchStopHandle} from "@vue/runtime-core";
+import {useEntityManager} from "~/composables/data/useEntityManager";
+import {Access} from "~/models/Access/Access";
 
-const emit = defineEmits(['showDateTimeRange'])
+const btn: Ref = ref(null)
+const show: Ref<boolean> = ref(false)
 
 const { setDirty } = useFormStore()
-
 const accessProfileStore = useAccessProfileStore()
+const { em } = useEntityManager()
 
-const { updateMyProfile, setHistoricalRange, historical } = useAccessProfileStore()
+const datesRange: Ref<Array<string | null | undefined>> = ref([
+  accessProfileStore.historical.dateStart,
+  accessProfileStore.historical.dateEnd
+])
 
-const datesRange: ComputedRef<Array<any>> = computed(() => {
-  return [accessProfileStore.historical.dateStart, accessProfileStore.historical.dateEnd]
-})
+const updateDateTimeRange = async (dates:Array<string>): Promise<any> => {
+  if (accessProfileStore.id === null) {
+    throw new Error('Invalid profile id')
+  }
 
-const show: Ref<boolean> = ref(false)
+  accessProfileStore.setHistoricalRange(dates[0], dates[1])
+  setDirty(false)
 
-if (accessProfileStore.historical.dateStart || accessProfileStore.historical.dateEnd) {
-  show.value = true
-  emit('showDateTimeRange', true)
+  await em.patch(
+      Access,
+      accessProfileStore.id,
+      {'historical': accessProfileStore.historical}
+  )
+
+  // TODO: voir si c'est indispensable de reload la page entière
+  window.location.reload()
 }
 
+/**
+ * Emit event when component is hidden / shown
+  */
+const emit = defineEmits(['showDateTimeRange'])
+
 const unwatch: WatchStopHandle = watch(show, (newValue) => {
   emit('showDateTimeRange', newValue)
 })
@@ -68,40 +84,63 @@ onUnmounted(() => {
   unwatch()
 })
 
-const updateDateTimeRange = async (dates:Array<string>): Promise<any> => {
-  setHistoricalRange(dates)
-  setDirty(false)
-  await updateMyProfile()
-  window.location.reload()
+// Show by default if a date range is defined in store
+if (accessProfileStore.historical.dateStart || accessProfileStore.historical.dateEnd) {
+  show.value = true
+  emit('showDateTimeRange', true)
 }
 </script>
 
-<style lang="scss">
-  .v-btn--active .v-icon {
-    color: #FFF !important;
+<style scoped lang="scss">
+.v-btn--active .v-icon {
+  color: #FFF !important;
+}
+
+.time-btn {
+  border-width: 1px 1px 1px 0;
+  border-style: solid;
+  border-color: rgba(0, 0, 0, 0.12) !important;
+}
+
+.data-timing-range {
+  max-height: 22px;
+
+  :deep(.v-text-field) {
+    padding-top: 0 !important;
+    margin-top: 0 !important;
   }
 
-  .time-btn {
-    border-width: 1px 1px 1px 0;
-    border-style: solid;
-    border-color: rgba(0, 0, 0, 0.12) !important;
+  :deep(.v-input) {
+    max-height: 22px;
+    min-height: 22px;
+    height: 22px;
   }
 
-  .time-range {
-    max-height: 20px;
+  :deep(.v-icon) {
+    max-height: 22px;
+  }
 
-    .v-text-field {
-      padding-top: 0 !important;
-      margin-top: 0 !important;
-    }
+  :deep(.v-field__input) {
+    font-size: 14px;
+    width: 400px !important;
+    padding: 0;
+    max-height: 22px;
+    min-height: 22px;
+    height: 22px;
+  }
 
-    .v-icon {
-      font-size: 20px;
-    }
+  :deep(.v-field-label) {
+    top: 0;
+    font-size: 14px;
+    max-height: 22px;
+    min-height: 22px;
+    height: 22px;
+  }
 
-    input {
-      font-size: 14px;
-      width: 400px !important;
-    }
+  :deep(.v-input__prepend) {
+    padding-top: 0;
+    font-size: 14px;
   }
+}
+
 </style>

+ 57 - 43
components/Layout/SubHeader/PersonnalizedList.vue

@@ -1,47 +1,52 @@
 <template>
   <main>
+    <span
+        ref="btn"
+        class="text-ot-green"
+        style="cursor: pointer;"
+    >
+      {{ $t('my_list') }}
+    </span>
+
     <v-menu
-      bottom
-      left
-      transition="slide-y-transition"
-      :close-on-content-click="false"
-      min-width="500"
+        :activator="btn"
+        :model-value="showMenu"
+        location="start"
+        :close-on-content-click="false"
+        min-width="500"
+        @update:modelValue="onMenuToggled($event)"
     >
-      <template #activator="{ on, attrs }">
-        <span
-          v-bind="attrs"
-          class="ot-green--text"
-          v-on="on"
-        >
-          {{ $t('my_list') }}
-        </span>
-      </template>
-      <v-card scrollable>
-        <v-card-title class="text-body-2 header-personnalized">
+
+      <v-card v-if="collection.totalItems === 0" height="80" class="pa-4">
+        <v-card-text class="ma-0 pa-0 header_menu">
+          {{ $t('nothing_to_show') }}
+        </v-card-text>
+      </v-card>
+
+      <v-card v-else>
+        <v-card-title class="text-body-2 header-personalized">
           <v-text-field
-            dense
-            clear-icon="header-personnalized"
-            :loading="fetchState.pending"
-            v-model="search"
-            :label="$t('searchList')"
+              v-model="search"
+              :label="$t('searchList')"
+              :loading="pending"
+              density="compact"
+              clear-icon="header-personalized"
           />
         </v-card-title>
-        <v-card-text class="ma-0 pa-0 mt-n3 header_menu">
-          <v-list dense :subheader="true">
-            <template v-for="(item, index) in filteredItems">
-              <v-list-item
-                :id="item.id"
-                :key="index"
-                :href="goOn(item)"
-                router
-                exact
-              >
-                <v-list-item-title>
-                  {{item.label}} - <strong>{{item.menuKey}}</strong>
-                </v-list-item-title>
-              </v-list-item>
-            </template>
 
+        <v-card-text class="ma-0 pa-0 mt-n3 header_menu">
+          <v-list density="compact" :subheader="true">
+            <v-list-item
+              v-for="(item, index) in filteredItems"
+              :key="index"
+              :value="item"
+              :href="getListURL(item)"
+              exact
+            >
+              <v-list-item-title>
+                {{item.label}} - <strong>{{item.menuKey}}</strong>
+              </v-list-item-title>
+            </v-list-item>
           </v-list>
         </v-card-text>
       </v-card>
@@ -53,11 +58,16 @@
 <script setup lang="ts">
 import {PersonalizedList} from '~/models/Access/PersonalizedList'
 import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import {ComputedRef, ref} from "@vue/reactivity";
+import {ComputedRef, Ref, ref} from "@vue/reactivity";
 import {AnyJson} from "~/types/data";
 import ApiResource from "~/models/ApiResource";
 
-// fetchOnServer: false,
+const btn: Ref = ref(null)
+const showMenu: Ref<boolean> = ref(false)
+
+const onMenuToggled = (event: any) => {
+  showMenu.value = event
+}
 
 const { fetch, fetchCollection } = useEntityFetch()
 
@@ -71,6 +81,7 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
   lists.map(item => {
     item.menuKey = i18n.t(item.menuKey) as string
   })
+
   return lists
 })
 
@@ -78,22 +89,25 @@ const search = ref('');
 
 const filteredItems = computed(() => {
   return items.value.filter( item => {
-      return item.label.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
-             ||item.menuKey.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
+      return !search.value ||
+             item.label.toLowerCase().indexOf(search.value.toLowerCase()) >= 0 ||
+             item.menuKey.toLowerCase().indexOf(search.value.toLowerCase()) >= 0
     }
   )
 })
 
+console.log(filteredItems)
+
 const runtimeConfig = useRuntimeConfig()
-const homeUrl: string = runtimeConfig.baseURL_adminLegacy
+const homeUrl: string = runtimeConfig.baseUrlAdminLegacy
 
-const goOn = (list: PersonalizedList) => {
+const getListURL = (list: PersonalizedList) => {
   return `${homeUrl}/${list.entity}/list/${list.id}`
 }
 </script>
 
 <style lang="scss">
-  .header-personnalized {
+  .header-personalized {
     margin-bottom: 0;
     padding-bottom: 0;
   }

+ 14 - 7
components/Layout/Subheader.vue

@@ -12,17 +12,17 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
       <LayoutSubHeaderBreadcrumbs class="mr-auto d-sm-none d-md-flex d-none d-sm-flex" />
 
       <v-card
-        class="d-md-flex ot-light_grey pt-2 mr-6 align-baseline"
+        class="d-md-flex pt-2 mr-6 align-baseline"
         flat
         tile
       >
         <LayoutSubHeaderActivityYear v-if="!showDateTimeRange" class="activity-year" />
 
-<!--        <div v-if="hasMenuOrIsTeacher" class="d-sm-none d-md-flex d-none d-sm-flex">-->
-<!--          <LayoutSubHeaderDataTiming v-if="!showDateTimeRange" class="data-timing ml-2" />-->
-<!--          <LayoutSubHeaderDataTimingRange class="data-timing-range ml-n1" @showDateTimeRange="showDateTimeRange=$event" />-->
-<!--          <LayoutSubHeaderPersonnalizedList class="personalized-list ml-2" />-->
-<!--        </div>-->
+        <div v-if="hasMenuOrIsTeacher" class="d-sm-none d-md-flex d-none d-sm-flex">
+          <LayoutSubHeaderDataTiming v-if="!showDateTimeRange" class="data-timing ml-2" />
+          <LayoutSubHeaderDataTimingRange class="data-timing-range ml-n1" @showDateTimeRange="showDateTimeRange=$event" />
+          <LayoutSubHeaderPersonnalizedList class="personalized-list ml-2" />
+        </div>
       </v-card>
     </v-card>
   </main>
@@ -45,8 +45,15 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+
+main {
+  color: rgb(var(--v-theme-ot-grey));
+  font-size: 12px;
+}
+
 .v-card {
   max-height: 33px;
+  background: transparent;
 }
 </style>

+ 39 - 22
components/Ui/Input/DatePicker.vue

@@ -6,34 +6,37 @@ Sélecteur de dates
 
 <template>
   <main>
+    <v-text-field
+      ref="input"
+      v-model="datesFormatted"
+      autocomplete="off"
+      :label="$t(fieldLabel)"
+      prepend-icon="mdi:mdi-calendar"
+      :disabled="readonly"
+      :density="dense ? 'compact' : 'default'"
+      :single-line="singleLine"
+      :error="error || !!fieldViolations"
+      :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
+      @update:focused=""
+      @focus="onInputFocused($event); $emit('focus', $event)"
+      @blur="onInputBlured($event); $emit('blur', $event)"
+    />
+
     <v-menu
-      v-model="dateOpen"
+      activator="input"
+      :model-value="dateOpen"
       :close-on-content-click="false"
       :nudge-right="40"
       transition="scale-transition"
       offset-y
       min-width="auto"
     >
-      <template #activator="{ on, attrs }">
-        <v-text-field
-          v-model="datesFormatted"
-          autocomplete="off"
-          :label="$t(fieldLabel)"
-          prepend-icon="mdi-calendar"
-          :disabled="readonly"
-          v-bind="attrs"
-          :dense="dense"
-          :single-line="singleLine"
-          v-on="on"
-          :error="error || !!violation"
-          :error-messages="errorMessage || violation ? $t(violation) : ''"
-        />
-      </template>
+      <!-- TODO: terminer une fois v-date-picker implémenté dans vuetify 3 -->
       <v-date-picker
-        v-model="datesParsed"
-        :range="range"
-        color="ot-green lighten-1"
-        @input="dateOpen = range && datesParsed.length < 2"
+          v-model="datesParsed"
+          :range="range"
+          color="ot-green lighten-1"
+          @input="dateOpen = range && datesParsed.length < 2"
       />
     </v-menu>
   </main>
@@ -95,11 +98,13 @@ const props = defineProps({
   }
 })
 
+const input = ref(null)
+
 const { emit, $moment } = useNuxtApp()
 
 const { data, range } = props
 
-const {violation, onChange} = useFieldViolation(props.field, emit)
+const {fieldViolations, updateViolationState} = useFieldViolation(props.field)
 
 const dateUtils = useDateUtils()
 
@@ -109,6 +114,16 @@ const fieldLabel = props.label ?? props.field
 
 const dateOpen: Ref<boolean> = ref(false)
 
+const onInputFocused = (event: any) => {
+  dateOpen.value = true
+}
+
+const onInputBlured = (event: any) => {
+  dateOpen.value = false
+}
+
+
+
 if (Array.isArray(datesParsed.value)) {
   for (const date of data as Array<string>) {
     if (date) {
@@ -129,13 +144,15 @@ const datesFormatted: ComputedRef<string|null> = computed(() => {
 const unwatch: WatchStopHandle = watch(datesParsed, (newValue, oldValue) => {
   if (newValue === oldValue) { return }
   if (props.range && newValue && newValue.length < 2) { return }
-  onChange(Array.isArray(newValue) ? dateUtils.sortDate(newValue) : newValue)
+  updateViolationState(Array.isArray(newValue) ? dateUtils.sortDate(newValue) : newValue)
 })
 
 onUnmounted(() => {
   unwatch()
 })
+
 </script>
 
 <style scoped>
+
 </style>

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

@@ -19,7 +19,7 @@ Liste déroulante dédiée à l'affichage d'objets Enum
       :items="items"
       item-value="value"
       item-title="label"
-      :no-data-text="$t('nothing-to-show') + '...'"
+      :no-data-text="$t('nothing_to_show') + '...'"
       :rules="rules"
       :disabled="readonly"
       :error="error || !!fieldViolations"

+ 0 - 4
doc/Débugger en production.md

@@ -1,4 +0,0 @@
-
-
-1. Tester l'api v1 / v2 avec postman
-2. Vérifier les lots avec pm2 : `pm2 logs app` 

+ 4 - 4
lang/fr.json

@@ -490,9 +490,9 @@
   "multiAccesses": "Mes structures",
   "familyAccesses": "Changement d'utilisateur",
   "display_data": "Afficher les données",
-  "past": "Passée",
-  "present": "Présent",
-  "future": "Future",
+  "past": "Passées",
+  "present": "Présentes",
+  "future": "Futures",
   "notification": "Notifications",
   "history_help": "Personnaliser la période d'affichage",
   "period_choose": "Période à afficher",
@@ -566,7 +566,7 @@
   "BILL_non_unique": "Vous ne pouvez pas avoir 2 points de contact de facturation",
   "CONTACT_non_unique": "Vous ne pouvez pas avoir 2 points de contact",
   "could_not_contact_server_please_try_again": "Le serveur n'a pas pu être contacté, veuillez réessayer un peu plus tard",
-  "nothing-to-show": "Rien à afficher",
+  "nothing_to_show": "Rien à afficher",
   "PENDING": "En cours de traitement",
   "READY": "Prêt",
   "DELETED": "Supprimé",

+ 4 - 0
layouts/default.vue

@@ -29,6 +29,10 @@
 </script>
 
 <style scoped>
+body {
+  font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
 client-only-placeholder {
   height: 100%;
   width: 100%;

+ 1 - 1
package.json

@@ -72,7 +72,7 @@
     "uuid": "^9.0.0",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
-    "vuetify": "^3.0.4",
+    "vuetify": "^3.0.5",
     "yaml-import": "^2.0.0"
   }
 }

+ 1 - 1
plugins/ability.ts

@@ -12,5 +12,5 @@ export default defineNuxtPlugin(() => {
 
     const abilityUtils = new AbilityUtils(ability, accessProfile, organizationProfile)
 
-    abilityUtils.defineAbilities()
+    abilityUtils.setupAbilities()
 })

+ 1 - 1
plugins/casl.ts

@@ -1,5 +1,5 @@
 import { abilitiesPlugin } from '@casl/vue';
-import { ability } from '@/plugins/ability'
+import { ability } from '~/plugins/ability'
 import { defineNuxtPlugin } from "nuxt/app";
 
 export default defineNuxtPlugin((nuxtApp) => {

+ 29 - 13
services/data/entityManager.ts

@@ -6,7 +6,7 @@ import HydraDenormalizer from "./serializer/denormalizer/hydraDenormalizer";
 import ApiModel from "~/models/ApiModel";
 import ApiResource from "~/models/ApiResource";
 import {MyProfile} from "~/models/Access/MyProfile";
-import { v4 as uuid4 } from 'uuid';
+import {v4 as uuid4} from 'uuid';
 import {AssociativeArray, Collection} from "~/types/data.d";
 import {useCloneDeep} from "#imports";
 import models from "~/models/models";
@@ -166,6 +166,20 @@ class EntityManager {
         }
     }
 
+    private async saveResponseAsEntity(model: typeof ApiModel, response: Response) {
+        const repository = this.getRepository(model)
+
+        const hydraResponse = await HydraDenormalizer.denormalize(response)
+        const returnedEntity = this.newInstance(model, hydraResponse.data)
+
+        this.saveInitialState(model, returnedEntity)
+
+        // Save data into the store
+        repository.save(returnedEntity)
+
+        return returnedEntity
+    }
+
     /**
      * Persist the entity as it is in the store into the data source via the API
      *
@@ -173,8 +187,6 @@ class EntityManager {
      * @param entity
      */
     public async persist(model: typeof ApiModel, entity: ApiModel) {
-        const repository = this.getRepository(model)
-
         // Recast in case class definition has been "lost"
         entity = this.cast(model, entity)
 
@@ -191,19 +203,23 @@ class EntityManager {
             response = await this.apiRequestService.post(url, data)
         }
 
-        const hydraResponse = await HydraDenormalizer.denormalize(response)
-        const returnedEntity = this.newInstance(model, hydraResponse.data)
-
-        this.saveInitialState(model, returnedEntity)
+        return this.saveResponseAsEntity(model, response)
+    }
 
-        // Save data into the store
-        repository.save(returnedEntity)
+    /**
+     * Send an update request (PUT) to the API with the given data on an existing entity
+     *
+     * @param model
+     * @param id
+     * @param data
+     */
+    public async patch(model: typeof ApiModel, id: number, data: AssociativeArray) {
+        let url = Url.join('api', model.entity, ''+id)
 
-        if (['accesses', 'organizations', 'parameters', 'subdomains'].includes(model.entity)) {
-            await this.refreshProfile()
-        }
+        const body = JSON.stringify(data)
+        const response = await this.apiRequestService.put(url, body)
 
-        return returnedEntity
+        return this.saveResponseAsEntity(model, response)
     }
 
     /**

+ 3 - 3
services/menuBuilder/abstractMenuBuilder.ts

@@ -3,7 +3,7 @@ import {MENU_LINK_TYPE} from "~/types/enum/layout";
 import {RuntimeConfig} from "@nuxt/schema";
 import Url from "~/services/utils/url";
 import {AnyAbility} from "@casl/ability";
-import {accessState, organizationState} from "~/types/interfaces";
+import {AccessProfile, organizationState} from "~/types/interfaces";
 
 /**
  * Classe de base des menus et sous-menus.
@@ -14,7 +14,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
   protected runtimeConfig: RuntimeConfig;
   protected ability: AnyAbility;
   protected organizationProfile: organizationState;
-  protected accessProfile: accessState;
+  protected accessProfile: AccessProfile;
 
   /**
    * Nom court désignant le menu que construit ce builder
@@ -25,7 +25,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
       runtimeConfig: RuntimeConfig,
       ability: AnyAbility,
       organizationProfile: organizationState,
-      accessProfile: accessState,
+      accessProfile: AccessProfile,
   ) {
     this.runtimeConfig = runtimeConfig
     this.ability = ability

+ 6 - 2
services/rights/abilityUtils.ts

@@ -30,10 +30,14 @@ class AbilityUtils {
     /**
      * Définit les abilities de l'utilisateur selon son profil
      */
-    defineAbilities() {
+    setupAbilities() {
         // Nécessaire pour que l'update des habilités soit correcte après la phase SSR
         this.ability.update(this.accessProfile.abilities)
 
+        // const abilities: Array<AbilitiesType> = this.buildAbilities();
+        // this.accessProfile.abilities = abilities
+        // this.ability.update(abilities)
+
         // Au moment où l'on effectue une action organizationProfileStore.setProfile, il faut aller récupérer
         // les différentes habilités que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
         const unsubscribe = this.organizationProfile.$onAction(({
@@ -44,7 +48,7 @@ class AbilityUtils {
                                                                     onError, // hook if the action throws or rejects
                                                                 }: any) => {
             after((result: any)=>{
-                if(name === 'setProfile'){
+                if (name === 'setProfile'){
                     //On récupère les habilités
                     const abilities: Array<AbilitiesType> = this.buildAbilities();
 

+ 38 - 14
stores/accessProfile.ts

@@ -1,14 +1,14 @@
 import { defineStore } from 'pinia'
 import {
   AbilitiesType,
-  baseAccessState,
-  baseOrganizationState,
-  OrignalAccessState
+  Historical,
+  BaseOrganizationProfile,
+  BaseAccessProfile,
+  OrignalAccessProfile,
 } from "~/types/interfaces";
 
 import {computed, ref, Ref} from "@vue/reactivity";
 import {useEach} from "#imports";
-import {useAbility} from "@casl/vue";
 import {useOrganizationProfileStore} from "~/stores/organizationProfile";
 import RoleUtils from "~/services/rights/roleUtils";
 
@@ -23,7 +23,11 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
   const gender: Ref<string | null> = ref(null)
   const avatarId: Ref<number | null> = ref(null)
   const activityYear: Ref<number | null> = ref(null)
-  const historical = ref([])
+  const historical: Ref<Historical> = ref({
+    "future": false,
+    "past": false,
+    "present": true,
+  })
   const roles: Ref<Array<string>> = ref([])
   const abilities: Ref<Array<AbilitiesType>> = ref([])
   const isAdminAccess: Ref<boolean | null> = ref(false)
@@ -39,9 +43,9 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
   const isOther: Ref<boolean | null> = ref(false)
   const isGuardian: Ref<boolean | null> = ref(false)
   const isPayer: Ref<boolean | null> = ref(false)
-  const multiAccesses: Ref<Array<baseOrganizationState>> = ref([])
-  const familyAccesses: Ref<Array<baseAccessState>> = ref([])
-  const originalAccess = ref(null)
+  const multiAccesses: Ref<Array<BaseOrganizationProfile>> = ref([])
+  const familyAccesses: Ref<Array<BaseAccessProfile>> = ref([])
+  const originalAccess: Ref<OrignalAccessProfile | null> = ref(null)
 
   // Getters
   /**
@@ -63,7 +67,7 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
 
   // Actions
   const setMultiAccesses = (organizations: any) => {
-    useEach(organizations, (organization: baseOrganizationState) => {
+    useEach(organizations, (organization: BaseOrganizationProfile) => {
       multiAccesses.value.push({
         id: organization.id,
         name: organization.name
@@ -72,8 +76,8 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
   }
 
   const setFamilyAccesses = (accesses: any) => {
-    useEach(accesses, (access: baseAccessState) => {
-      const a: baseAccessState = {
+    useEach(accesses, (access: BaseAccessProfile) => {
+      const a: BaseAccessProfile = {
         id: access.id,
         name: access.name,
         givenName: access.givenName,
@@ -135,12 +139,12 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
 
   const setOriginalAccess = (access: any) => {
     if (access) {
-      const organization: baseOrganizationState = {
+      const organization: BaseOrganizationProfile = {
         id: access.organization.id,
         name: access.organization.name
       }
 
-      const originalAccess: OrignalAccessState = {
+      const originalAccess: OrignalAccessProfile = {
         id: access.id,
         name: access.name,
         givenName: access.givenName,
@@ -153,6 +157,24 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     }
   }
 
+  const setHistorical = (past: boolean, present: boolean, future: boolean) => {
+    historical.value = <Historical> {
+      past,
+      present,
+      future
+    }
+  }
+
+  const setHistoricalRange = (dateStart: string, dateEnd: string) => {
+    historical.value = <Historical> {
+      past: false,
+      present: false,
+      future: false,
+      dateStart,
+      dateEnd
+    }
+  }
+
   return {
     bearer,
     id,
@@ -187,6 +209,8 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     setFamilyAccesses,
     setProfile,
     refreshProfile,
-    setOriginalAccess
+    setOriginalAccess,
+    setHistorical,
+    setHistoricalRange
   }
 })

+ 5 - 5
stores/form.ts

@@ -4,11 +4,11 @@ import {Ref, ref} from "@vue/reactivity";
 import {AnyJson} from "~/types/data";
 
 export const useFormStore = defineStore('form', () => {
-  const formFunction = ref(FORM_FUNCTION.EDIT)
-  const violations = ref({})
-  const readonly = ref(false)
-  const dirty = ref(false)
-  const showConfirmToLeave = ref(false)
+  const formFunction: Ref<FORM_FUNCTION> = ref(FORM_FUNCTION.EDIT)
+  const violations: Ref<AnyJson> = ref({})
+  const readonly: Ref<boolean> = ref(false)
+  const dirty: Ref<boolean> = ref(false)
+  const showConfirmToLeave: Ref<boolean> = ref(false)
   const goAfterLeave: Ref<string | null> = ref(null)
 
   const setViolations = (newViolations: Array<string>) => {

+ 2 - 2
stores/organizationProfile.ts

@@ -1,4 +1,4 @@
-import { baseOrganizationState } from '~/types/interfaces'
+import { BaseOrganizationProfile } from '~/types/interfaces'
 import { defineStore } from "pinia";
 import {computed, Ref} from "@vue/reactivity";
 import {useEach} from "#imports";
@@ -17,7 +17,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
   const showAdherentList = ref(false)
   const networks = ref([])
   const website = ref(null)
-  const parents: Ref<Array<baseOrganizationState>> = ref([])
+  const parents: Ref<Array<BaseOrganizationProfile>> = ref([])
 
   // Getters
   /**

+ 6 - 7
stores/sse.ts

@@ -1,14 +1,13 @@
-import {MercureEntityUpdate, sseState} from "~/types/interfaces";
 import {defineStore} from "pinia";
-import {ref} from "@vue/reactivity";
+import {Ref, ref} from "@vue/reactivity";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 
 export const useSseStore = defineStore('sse', () => {
-  const connected = ref(false)
-  const events = ref([])
+  const connected: Ref<boolean> = ref(false)
+  const events: Ref<Array<MercureEntityUpdate>> = ref([])
 
-  const addEvent = (state: sseState, event: MercureEntityUpdate) => {
-    const { em } = useEntityManager()
+  const addEvent = async (event: MercureEntityUpdate) => {
+    const {em} = useEntityManager()
 
     // TODO: voir à refactorer le "get model from iri"
     const matches = event.iri.match(/^\/api\/(\w+)\/.*/)
@@ -22,7 +21,7 @@ export const useSseStore = defineStore('sse', () => {
     switch (event.operation) {
       case "update":
       case "create":
-        em.persist(model, JSON.parse(event.data))
+        await em.persist(model, JSON.parse(event.data))
         break
 
       case "delete":

+ 0 - 0
tests/.gitkeep


+ 53 - 82
types/interfaces.d.ts

@@ -13,18 +13,6 @@ import ApiResource from "~/models/ApiResource";
 import {AnyJson} from "~/types/enum/data";
 import {Record} from "immutable";
 
-/**
- * Upgrade du @nuxt/types pour TypeScript
- */
-declare module '@nuxt/types' {
-  interface Context {
-    $ability: Ability
-    $dataPersister: DataPersister
-    $dataProvider: DataProvider
-    $dataDeleter: DataDeleter
-  }
-}
-
 declare module '@vuex-orm/core' {
   interface Query {
     getAllRelations: () => Array<string>
@@ -45,38 +33,55 @@ interface AbilitiesType {
   reason?: string
 }
 
-interface formState {
-  violations: AnyJson
-  readonly: boolean
-  formFunction: FORM_FUNCTION
-  dirty: boolean
-  showConfirmToLeave: boolean
-  goAfterLeave: string | null
-}
-
 interface Alert {
   type: TYPE_ALERT
   messages: Array<string>
 }
 
-interface pageState {
-  alerts: Array<Alert>,
-  menusOpened: Record<string, boolean>
+type AnyStore = Store<any>
+
+interface EnumChoice {
+  value: string
+  label: string
+}
+
+interface Filter {
+  readonly key: string
+  readonly value: string | boolean | number
+}
+
+type EnumChoices = Array<EnumChoice>
+
+interface HookProvider {
+  invoke(args: DataProviderArgs): Promise<any>
+}
+interface HookPersister {
+  invoke(args: DataPersisterArgs): Promise<any>
+}
+interface HookDeleter {
+  invoke(args: DataDeleterArgs): Promise<any>
+}
+
+interface Processor {
+  process(data: AnyJson): Promise<any>
 }
 
-interface ormState {
-  initialValues: Map<string, Map<number, ApiResource>>
+interface Normalizer {
+  normalize(args: DataPersisterArgs): any
+}
+interface Denormalizer {
+  denormalize(data: any): any
 }
 
 interface Historical {
-  future?: boolean
-  past?: boolean
-  present?: boolean
-  dateStart?: string
-  dateEnd?: string
+  future: boolean
+  past: boolean
+  present: boolean
+  dateStart?: string | null
+  dateEnd?: string | null
 }
 
-interface baseAccessState {
+interface BaseAccessProfile {
   id: number | null
   name: string | null
   givenName: string | null
@@ -84,18 +89,18 @@ interface baseAccessState {
   avatarId: number | null
 }
 
-interface baseOrganizationState {
+interface BaseOrganizationProfile {
   id: number | null
   name: string | null
   website?: string | null
 }
 
-interface OrignalAccessState extends baseAccessState {
+interface OrignalAccessProfile extends BaseAccessProfile {
   isSuperAdminAccess: boolean
-  organization: baseOrganizationState
+  organization: BaseOrganizationProfile
 }
 
-interface accessState extends baseAccessState {
+interface AccessProfile extends BaseAccessProfile {
   bearer: string | null
   switchId: number | null
   activityYear: number | null
@@ -115,12 +120,12 @@ interface accessState extends baseAccessState {
   isOther: boolean | null
   isGuardian: boolean | null
   isPayer: boolean | null
-  multiAccesses: Array<baseOrganizationState>
-  familyAccesses: Array<baseAccessState>
-  originalAccess: OrignalAccessState | null
+  multiAccesses: Array<BaseOrganizationProfile>
+  familyAccesses: Array<BaseAccessProfile>
+  originalAccess: OrignalAccessProfile | null
 }
 
-interface organizationState extends baseOrganizationState {
+interface organizationState extends BaseOrganizationProfile {
   id: number | null
   parametersId: number | null
   name: string | null
@@ -131,42 +136,7 @@ interface organizationState extends baseOrganizationState {
   showAdherentList?: boolean | null
   legalStatus?: string | null
   networks: Array<string>
-  parents: Array<baseOrganizationState>
-}
-
-type AnyStore = Store<any>
-
-interface EnumChoice {
-  value: string
-  label: string
-}
-
-interface Filter {
-  readonly key: string
-  readonly value: string | boolean | number
-}
-
-type EnumChoices = Array<EnumChoice>
-
-interface HookProvider {
-  invoke(args: DataProviderArgs): Promise<any>
-}
-interface HookPersister {
-  invoke(args: DataPersisterArgs): Promise<any>
-}
-interface HookDeleter {
-  invoke(args: DataDeleterArgs): Promise<any>
-}
-
-interface Processor {
-  process(data: AnyJson): Promise<any>
-}
-
-interface Normalizer {
-  normalize(args: DataPersisterArgs): any
-}
-interface Denormalizer {
-  denormalize(data: any): any
+  parents: Array<BaseOrganizationProfile>
 }
 
 interface DolibarrContractLine {
@@ -199,11 +169,11 @@ interface DolibarrAccount {
   socId: number
   clientNumber: string
   product:
-    | 'PRODUCT_ARTIST'
-    | 'PRODUCT_ARTIST_PREMIUM'
-    | 'PRODUCT_SCHOOL'
-    | 'PRODUCT_SCHOOL_PREMIUM'
-    | 'PRODUCT_MANAGER'
+      | 'PRODUCT_ARTIST'
+      | 'PRODUCT_ARTIST_PREMIUM'
+      | 'PRODUCT_SCHOOL'
+      | 'PRODUCT_SCHOOL_PREMIUM'
+      | 'PRODUCT_MANAGER'
   contract: DolibarrContract
   bills: Array<DolibarrBill>
 }
@@ -221,7 +191,8 @@ interface MercureEntityUpdate {
   data: any
 }
 
-interface sseState {
+interface SseState {
   connected: boolean
   events: Array<MercureEntityUpdate>
 }
+