Notification.vue 7.8 KB

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