Notification.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <template>
  2. <v-btn
  3. ref="btn"
  4. icon
  5. size="small"
  6. class="ml-2"
  7. >
  8. <v-badge
  9. color="warning"
  10. offset-x="-4"
  11. offset-y="17"
  12. :model-value="unreadNotification.length > 0"
  13. :content="unreadNotification.length">
  14. <v-icon class="on-primary">
  15. fa fa-bell
  16. </v-icon>
  17. </v-badge>
  18. </v-btn>
  19. <v-tooltip v-if="btn !== null" :activator="btn" location="bottom">
  20. <span>{{ $t('notification') }}</span>
  21. </v-tooltip>
  22. <v-menu
  23. v-if="btn !== null"
  24. :activator="btn"
  25. v-model="isOpen"
  26. location="bottom left"
  27. >
  28. <v-card max-width="400">
  29. <v-card-title class="bg-neutral text-body-2 font-weight-bold">
  30. {{ $t('notification') }}
  31. </v-card-title>
  32. <v-card-text class="ma-0 pa-0 header-menu">
  33. <v-list density="compact" :subheader="true" class="pa-0">
  34. <v-list-item
  35. v-for="(notification, index) in notifications"
  36. :key="index"
  37. :class="'list_item py-3' + `${notification.notificationUsers.length === 0 ? ' unread' : ''}`"
  38. >
  39. <span class="">{{ getMessage(notification) }}</span>
  40. <template #append>
  41. <v-icon
  42. v-if="notification.link"
  43. icon="mdi:mdi-download"
  44. @click="download(notification.link)"
  45. class="pt-4"
  46. />
  47. </template>
  48. </v-list-item>
  49. <v-divider></v-divider>
  50. <!--suppress VueUnrecognizedDirective -->
  51. <span v-intersect="onLastNotificationIntersect" />
  52. <v-row
  53. v-if="pending"
  54. class="fill-height mt-3 mb-3"
  55. align="center"
  56. justify="center"
  57. >
  58. <v-progress-circular
  59. indeterminate
  60. color="neutral"
  61. />
  62. </v-row>
  63. </v-list>
  64. </v-card-text>
  65. <v-card-actions class="ma-0 pa-0">
  66. <v-list-item
  67. :key="$t('all_notification')"
  68. :href="notificationUrl"
  69. router
  70. class="theme-primary"
  71. style="width: 100%; height: 52px;"
  72. >
  73. <v-list-item-title
  74. class="text-body-2"
  75. v-text="$t('all_notification')"
  76. />
  77. </v-list-item>
  78. </v-card-actions>
  79. </v-card>
  80. </v-menu>
  81. </template>
  82. <script setup lang="ts">
  83. import {NOTIFICATION_TYPE} from "~/types/enum/enums";
  84. import Notification from "~/models/Core/Notification";
  85. import NotificationUsers from "~/models/Core/NotificationUsers";
  86. import {useAccessProfileStore} from "~/stores/accessProfile";
  87. import {computed, ref} from "@vue/reactivity";
  88. import type {ComputedRef, Ref} from "@vue/reactivity";
  89. import {useEntityFetch} from "~/composables/data/useEntityFetch";
  90. import type {AnyJson, Pagination} from "~/types/data";
  91. import {useEntityManager} from "~/composables/data/useEntityManager";
  92. import UrlUtils from "~/services/utils/urlUtils";
  93. import {useRepo} from "pinia-orm";
  94. import NotificationRepository from "~/stores/repositories/NotificationRepository";
  95. const accessProfileStore = useAccessProfileStore()
  96. const loading: Ref<Boolean> = ref(true)
  97. const isOpen: Ref<Boolean> = ref(false)
  98. const page: Ref<number> = ref(1)
  99. const i18n = useI18n()
  100. const runtimeConfig = useRuntimeConfig()
  101. const btn = ref(null)
  102. const { em } = useEntityManager()
  103. const { fetchCollection } = useEntityFetch()
  104. const notificationRepo = useRepo(NotificationRepository)
  105. const query: ComputedRef<AnyJson> = computed(() => {
  106. return { 'page': page.value }
  107. })
  108. let { data: collection, pending, refresh } = await fetchCollection(Notification, null, query)
  109. /**
  110. * On récupère les Notifications via le store (sans ça, les mises à jour SSE ne seront pas prises en compte)
  111. */
  112. const notifications: ComputedRef<Array<Notification>> = computed(() => {
  113. return notificationRepo.getNotifications()
  114. })
  115. /**
  116. * Retourne les notifications non lues
  117. */
  118. const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
  119. return notificationRepo.getUnreadNotifications()
  120. })
  121. /**
  122. * Les metadata dépendront de la dernière valeur du GET lancé
  123. */
  124. const pagination: ComputedRef<Pagination> = computed(() => {
  125. return (!pending.value && collection.value !== null) ? collection.value.pagination : {}
  126. })
  127. const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, '#/notifications/list/')
  128. /**
  129. * L'utilisateur a fait défiler le menu jusqu'à la dernière notification affichée
  130. * @param isIntersecting
  131. */
  132. const onLastNotificationIntersect = (isIntersecting: boolean) => {
  133. if (isIntersecting) {
  134. update()
  135. }
  136. }
  137. /**
  138. * Lorsque l'utilisateur scroll on regarde la nextPage à charger et on le fait que si le pending du fetch est false
  139. * (si on a fini de télécharger les éléments précédents)
  140. */
  141. const update = async () => {
  142. if (
  143. !pending.value &&
  144. pagination.value &&
  145. pagination.value.next &&
  146. pagination.value.next > 0
  147. ) {
  148. pending.value = true
  149. page.value = pagination.value.next
  150. await refresh()
  151. // Si des notifications n'avaient pas été marquées comme lues, on le fait immédiatement.
  152. markNotificationsAsRead()
  153. }
  154. }
  155. /**
  156. * On construit le message qui va devoir s'afficher pour une notification
  157. * @param notification
  158. */
  159. const getMessage = (notification: Notification) => {
  160. switch (notification.type){
  161. case NOTIFICATION_TYPE.FILE :
  162. return `${i18n.t('your_file')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be_downloaded')}`
  163. case NOTIFICATION_TYPE.MESSAGE:
  164. if (notification.message?.action)
  165. return `${i18n.t('your_message')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be')} ${notification.message.action}`
  166. return `${i18n.t('your_message')} ${notification.message?.about ?? ''} ${i18n.t('has_been_sent')} `
  167. case NOTIFICATION_TYPE.SYSTEM :
  168. if (notification.message?.about)
  169. return `${i18n.t(notification.message.about)}`
  170. break;
  171. default:
  172. return i18n.t(notification.name)
  173. }
  174. }
  175. /**
  176. * Dès la fermeture du menu, on indique que les notifications non lues, le sont.
  177. */
  178. const unwatch = watch(isOpen, (newValue, oldValue) => {
  179. if (!newValue){
  180. markNotificationsAsRead()
  181. }
  182. })
  183. onUnmounted(() => {
  184. unwatch()
  185. })
  186. /**
  187. * Marque une notification comme lue
  188. */
  189. const markNotificationAsRead = (notification: Notification) => {
  190. if (accessProfileStore.id === null) {
  191. throw new Error('Current access id is null')
  192. }
  193. const notificationUsers = em.newInstance(NotificationUsers, {
  194. access:`/api/accesses/${accessProfileStore.currentAccessId}`,
  195. notification:`/api/notifications/${notification.id}`,
  196. isRead: true
  197. })
  198. em.persist(NotificationUsers, notificationUsers)
  199. notification.notificationUsers = ['read']
  200. em.save(Notification, notification)
  201. }
  202. /**
  203. * Marque toutes les notifications non lues comme lues
  204. */
  205. const markNotificationsAsRead = () => {
  206. unreadNotification.value.map((notification: Notification) => {
  207. markNotificationAsRead(notification)
  208. })
  209. }
  210. /**
  211. * Download la cible du lien
  212. * @param link
  213. */
  214. const download = (link: string) => {
  215. if (accessProfileStore.id === null) {
  216. throw new Error('Current access id is null')
  217. }
  218. // TODO: passer cette logique dans un service ; tester ; voir si possible de réunir avec composables/utils/useDownloadFile.ts
  219. const path: string = link.split('/api')[1];
  220. // En switch : https://api.test5.opentalent.fr/api/{accessId}/{switchId}/files/{fileId}/download
  221. // Sans switch : https://local.api.opentalent.fr/api/{accessId}/files/{fileId}/download
  222. const url = UrlUtils.join(
  223. runtimeConfig.baseUrlLegacy,
  224. 'api',
  225. String(accessProfileStore.id),
  226. String(accessProfileStore.switchId || ''),
  227. path
  228. )
  229. window.open(url);
  230. }
  231. </script>
  232. <style scoped lang="scss">
  233. .list_item{
  234. white-space: normal;
  235. }
  236. .unread{
  237. background: rgb(var(--v-theme-neutral-soft, white));
  238. }
  239. </style>