Browse Source

Création des Notifications

Vincent GUFFON 4 years ago
parent
commit
7e299c4b16

+ 6 - 0
assets/css/global.scss

@@ -22,4 +22,10 @@ header .v-toolbar__content{
   color: var(--v-ot_green-base, white)
 }
 
+.header_menu{
+  max-height: 300px;
+  min-width: 300px;
+  overflow-y: scroll
+}
+
 

+ 5 - 8
components/Layout/Header/Menu.vue

@@ -9,7 +9,6 @@ header principal (configuration, paramètres du compte...)
       <v-tooltip bottom>
         <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
           <v-btn
-
             icon
             v-bind="[attrs, attrs_tooltips]"
             color=""
@@ -31,16 +30,15 @@ header principal (configuration, paramètres du compte...)
       </v-tooltip>
     </template>
     <v-card scrollable>
-      <v-card-title class="ot_super_light_grey text-body-2 font-weight-bold">
+      <v-card-title class="ot_header_menu text-body-2 font-weight-bold">
         {{$t(menu.title)}}
       </v-card-title>
-      <v-card-text style="max-height: 300px; overflow-y: scroll" class="ma-0 pa-0">
+      <v-card-text class="ma-0 pa-0 header_menu">
         <v-list dense :subheader="true">
           <template v-for="(item, index) in menu.children">
             <v-list-item
-
               :id="item.title"
-              :key="item.title"
+              :key="index"
               :href="item.isExternalLink ? item.to : undefined"
               :to="!item.isExternalLink ? item.to : undefined"
               router
@@ -54,14 +52,13 @@ header principal (configuration, paramètres du compte...)
       <v-card-actions class="ma-0 pa-0">
         <template v-for="(item, index) in menu.actions">
           <v-list-item
-            class="text-body-2"
             :id="item.title"
-            :key="item.title"
+            :key="index"
             :href="item.isExternalLink ? item.to : undefined"
             :to="!item.isExternalLink ? item.to : undefined"
             router
           >
-            <v-list-item-title v-text="$t(item.title)"/>
+            <v-list-item-title class="text-body-2 ot_white--text" v-text="$t(item.title)"/>
           </v-list-item>
         </template>
       </v-card-actions>

+ 224 - 32
components/Layout/Header/Notification.vue

@@ -1,59 +1,251 @@
-<!--
-Notification
--->
-
 <template>
-  <v-menu offset-y>
-    <template v-slot:activator="{ on, attrs }">
+  <v-menu offset-y v-model="isOpen">
+    <template v-slot:activator="{ on: { click }, attrs }">
       <v-tooltip bottom>
-        <template v-slot:activator="{ on, attrs }">
+        <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
           <v-btn
             icon
-            v-bind="attrs"
-            v-on="on"
+            v-bind="[attrs, attrs_tooltips]"
+            color=""
+            v-on="on_tooltips"
+            @click="click"
           >
-            <v-icon class="ot_white--text" small>
-              fa-bell
-            </v-icon>
+            <v-badge
+              color="orange"
+              offset-y="10"
+              :value="unreadNotification.length > 0"
+              :content="unreadNotification.length"
+            >
+              <v-icon class="ot_white--text" small>
+                fa-bell
+              </v-icon>
+            </v-badge>
           </v-btn>
         </template>
         <span>{{ $t('notification') }}</span>
       </v-tooltip>
     </template>
-    <v-list dense>
-      <template v-for="(item, index) in properties.menu">
-        <v-list-item :key="item.title">
-          <v-list-item-title v-text="$t(item.title)" />
-        </v-list-item>
-        <v-divider
-          v-if="index < properties.menu.length - 1"
-          :key="index"
-        />
-      </template>
-    </v-list>
+    <v-card scrollable max-width="400">
+      <v-card-title class="ot_header_menu text-body-2 font-weight-bold">
+        {{ $t('notification') }}
+      </v-card-title>
+      <v-card-text class="ma-0 pa-0 header_menu">
+        <v-list dense :subheader="true">
+          <template v-for="(notification, index) in notifications">
+            <v-list-item :key="index" :class="`${notification.notificationUsers.length === 0 ? 'unread' : ''}`">
+              <v-list-item-content>
+                <v-list-item-title class="list_item mt-2 mb-2" v-text="getMessage(notification)"/>
+              </v-list-item-content>
+              <v-list-item-icon v-if="notification.link" class="pt-4">
+                <v-icon @click="download(notification.link)">mdi-download</v-icon>
+              </v-list-item-icon>
+            </v-list-item>
+            <v-divider></v-divider>
+          </template>
+        </v-list>
+        <v-card v-intersect="update"></v-card>
+        <v-row
+          v-if="loading"
+          class="fill-height mt-3 mb-3"
+          align="center"
+          justify="center"
+        >
+          <v-progress-circular
+            indeterminate
+            color="grey lighten-1"
+          ></v-progress-circular>
+        </v-row>
+      </v-card-text>
+      <v-card-actions class="ma-0 pa-0">
+        <template>
+          <v-list-item
+            id="all_notifications"
+            :key="$t('all_notification')"
+            :href="notificationUrl"
+            router
+          >
+            <v-list-item-title class="text-body-2 ot_white--text" v-text="$t('all_notification')"/>
+          </v-list-item>
+        </template>
+      </v-card-actions>
+    </v-card>
   </v-menu>
 </template>
 
 <script lang="ts">
-import { defineComponent, reactive, Ref, UnwrapRef } from '@nuxtjs/composition-api'
-import { $useMenu } from '@/use/layout/menu'
-import { AnyJson } from '~/types/interfaces'
+import {computed, ComputedRef, defineComponent, Ref, ref, useContext, useFetch, useStore, watch} from '@nuxtjs/composition-api'
+import {NOTIFICATION_TYPE, QUERY_TYPE} from "~/types/enums";
+import {Notification} from "~/models/Core/Notification";
+import {repositoryHelper} from "~/services/store/repository";
+import {AnyStore, ApiResponse, HydraMetadata} from "~/types/interfaces";
+import {queryHelper} from "~/services/store/query";
+import {NotificationUsers} from "~/models/Core/NotificationUsers";
+import {State} from "@vuex-orm/core";
 
 export default defineComponent({
-  setup () {
-    const menu: Ref<any> = $useMenu.setupContext().useConfigurationMenuConstruct()
+  setup: function () {
+    const {$dataProvider, $dataPersister, $config, app: { i18n }} = useContext()
+    const store:AnyStore = useStore<State>()
+    const profileAccess = store.state.profile.access
+    const loading: Ref<Boolean> = ref(true)
+    const isOpen: Ref<Boolean> = ref(false)
+    const page: Ref<number> = ref(1)
+    const data: Ref<ApiResponse> = ref({} as ApiResponse)
+
+    /**
+     * On récupère les notifications via l'API qui seront stockées dans le store
+     */
+    const {fetch, fetchState} = useFetch(async () => {
+      data.value = await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: Notification,
+        listArgs: {
+          itemsPerPage: 10,
+          page: page.value
+        }
+      })
+      loading.value = false
+    })
+
+    /**
+     * On récupère les Notifications via le store
+     */
+    const notifications: ComputedRef = computed(() => {
+      const query = repositoryHelper.getRepository(Notification).with('message').orderBy('id', 'desc')
+      return queryHelper.getCollection(query)
+    })
+
+    /**
+     * on calcul le nombre de notification non lues
+     */
+    const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
+      return notifications.value.filter((notification: Notification) => {
+        return notification.notificationUsers.length === 0
+      })
+    })
+
+    /**
+     * Les metadata dépendront de la dernière valeur du GET lancé
+     */
+    const metadata: ComputedRef<HydraMetadata> = computed(() => {
+      return data.value.metadata
+    })
+
+    /**
+     * Lorsque l'utilisateur scroll on regarde la nextPage a charger et on le fait que si le pending du fetch est false
+     * (si on a fini de télécharger les éléments précédents)
+     */
+    const update = async () => {
+      if (!fetchState.pending && metadata.value?.nextPage && metadata.value.nextPage > 0) {
+        loading.value = true
+        page.value = metadata.value.nextPage
+        await fetch()
+        //Si des notifications n'avaient pas été marquées comme lues, on le fait immédiatement.
+        markNotificationsAsRead()
+      }
+    }
+
+    /**
+     * On construit le message qui va devoir s'afficher pour une notification
+     * @param notification
+     */
+    const getMessage = (notification:Notification) => {
+      switch (notification.type){
+        case NOTIFICATION_TYPE.FILE :
+         return `${i18n.t('your_file')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be_downloaded')}`
+          break;
+
+         case NOTIFICATION_TYPE.MESSAGE:
+           if(notification.message?.action)
+             return `${i18n.t('your_message')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be')} ${notification.message.action}`
 
-    const properties: UnwrapRef<AnyJson> = reactive({
-      menu
+           return `${i18n.t('your_message')} ${notification.message?.about} ${i18n.t('has_been_sent')} `
+           break;
+
+        default:
+          return i18n.t(notification.name)
+      }
+    }
+
+    /**
+     * Dès l'ouverture du menu, on indique que les notifications non lues, le sont.
+     */
+    watch(isOpen, (newValue, oldValue) => {
+      if(newValue){
+        markNotificationsAsRead()
+      }
     })
 
+    /**
+     * Marque les notification non lues comme lues
+     */
+    const markNotificationsAsRead = () => {
+      unreadNotification.value.map((notification:Notification)=>{
+        notification.notificationUsers = ['read']
+        repositoryHelper.persist(Notification, notification)
+        createNewNotificationUsers(notification)
+      })
+    }
+
+    /**
+     * Créer une nouvelle notification users coté back.
+     * @param notification
+     */
+    const createNewNotificationUsers = (notification: Notification) =>{
+      const newNotificationUsers = repositoryHelper.persist(NotificationUsers, new NotificationUsers(
+        {
+          access:`/api/accesses/${profileAccess.id}`,
+          notification:`/api/notifications/${notification.id}`,
+          isRead: true
+        }
+      )) as NotificationUsers
+
+      $dataPersister.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: NotificationUsers,
+        idTemp: newNotificationUsers.id,
+        showProgress: false
+      })
+    }
+
+    /**
+     * Download le lien
+     * @param link
+     */
+    const download = (link: string) => {
+      const url_parts: Array<string> = link.split('/api');
+      if(profileAccess.originalAccess)
+        url_parts[0] = `api/${profileAccess.originalAccess}/${profileAccess.id}`
+      else
+        url_parts[0] = `api/${profileAccess.id}`
+
+      window.open(`${$config.baseURL_Legacy}/${url_parts.join('')}`);
+    }
+
     return {
-      properties
+      data,
+      getMessage,
+      notificationUrl: `${$config.baseURL_adminLegacy}/notifications/list/`,
+      loading,
+      notifications,
+      update,
+      unreadNotification,
+      isOpen,
+      download
     }
   }
 })
 </script>
 
 <style scoped>
-
+  #all_notifications{
+    background: var(--v-ot_green-base, white);
+    color: white;
+  }
+  .list_item{
+    white-space: normal;
+  }
+  .unread{
+    background: #ecf0f5;
+  }
 </style>

+ 1 - 1
components/Layout/SubHeader/PersonnalizedList.vue

@@ -62,7 +62,7 @@ export default defineComponent({
     })
 
     const items:ComputedRef<Array<AnyJson>> = computed(() => {
-      const lists = repositoryHelper.findCollectionFromModel(PersonalizedList) as Collection<PersonalizedList>
+      const lists = repositoryHelper.findCollectionFromModel(PersonalizedList, {'id':'desc'}) as Collection<PersonalizedList>
 
       let listsGroupByKeyMenu:Array<AnyJson> = groupListByKey(lists)
 

+ 1 - 0
config/nuxtConfig/vuetify.js

@@ -29,6 +29,7 @@ export default {
           ot_light_green: '#a9e0d6',
           ot_dark_grey: '#2c3a48',
           ot_grey: '#777777',
+          ot_header_menu: '#ECE7E5',
           ot_light_grey: '#f5f5f5',
           ot_super_light_grey: '#ecf0f5',
           ot_danger: '#f56954',

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

@@ -126,5 +126,11 @@ export default (context, locale) => {
     my_bills: "Mes factures",
     print_my_licence: "Imprimer ma licence CMF",
     logout: "Se déconnecter",
+    all_notification: "Toutes les notifications",
+    your_file: "Votre fichier",
+    is_ready_to_be_downloaded: "est près à être téléchargé",
+    your_message: "Votre message",
+    has_been_sent: "a été envoyé",
+    ready_to_be: "est prêt à être",
   })
 }

+ 1 - 1
store/index.js

@@ -50,6 +50,6 @@ export const actions = {
       type: QUERY_TYPE.DEFAULT,
       url: 'my_profile'
     })
-    await dispatch('profile/access/setProfile', myProfile)
+    await dispatch('profile/access/setProfile', myProfile.data)
   }
 }

+ 11 - 0
types/enums.ts

@@ -33,3 +33,14 @@ export const enum GENDER {
   MISTER = 'MISTER',
   MISS = 'MISS'
 }
+
+export const enum METADATA_TYPE {
+  ITEM,
+  COLLECTION
+}
+
+export const enum NOTIFICATION_TYPE {
+  MESSAGE= 'MESSAGE',
+  FILE= 'FILE'
+}
+

+ 24 - 5
types/interfaces.d.ts

@@ -5,7 +5,7 @@ import { Context } from '@nuxt/types/app'
 import DataPersister from '~/services/data/dataPersister'
 import DataProvider from '~/services/data/dataProvider'
 import DataDeleter from '~/services/data/dataDeleter'
-import { ABILITIES, GENDER, QUERY_TYPE, TYPE_ALERT } from '~/types/enums'
+import {ABILITIES, GENDER, METADATA_TYPE, QUERY_TYPE, TYPE_ALERT} from '~/types/enums'
 
 /**
  * Upgrade du @nuxt/types pour TypeScript
@@ -150,9 +150,9 @@ interface UrlArgs {
   readonly model?: typeof Model,
   readonly rootModel?: typeof Model,
   readonly id?: any,
-  readonly rootId?: number
-  readonly showProgress?: boolean
-
+  readonly idTemp?: any,
+  readonly rootId?: number,
+  readonly showProgress?: boolean,
   readonly hook?: string
 }
 
@@ -162,8 +162,14 @@ interface ImageArgs {
   readonly width: number
 }
 
+interface ListArgs {
+  readonly itemsPerPage: number,
+  readonly page: number
+}
+
 interface DataProviderArgs extends UrlArgs {
-  imgArgs?: ImageArgs
+  imgArgs?: ImageArgs,
+  listArgs?: ListArgs,
 }
 interface DataPersisterArgs extends UrlArgs {
   data?: AnyJson
@@ -191,3 +197,16 @@ interface Denormalizer {
   denormalize(data: any): any,
 }
 
+interface ApiResponse{
+  data: AnyJson,
+  metadata: HydraMetadata
+}
+
+interface HydraMetadata {
+  readonly totalItems?: number,
+  firstPage?: number,
+  lastPage?: number,
+  nextPage?: number,
+  previousPage?: number,
+  type?: METADATA_TYPE
+}

+ 5 - 1
types/types.d.ts

@@ -1,3 +1,7 @@
-import { Query, Repository } from '@vuex-orm/core'
+import {OrderBy, OrderDirection, Query, Repository} from '@vuex-orm/core'
 
 export type RepositoryOrQuery<R extends Repository = Repository, Q extends Query = Query> = R | Q;
+
+interface OrderByVuexOrm {
+  [key: string]: OrderDirection;
+}