TicketOverview.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { useRouteQuery } from '@vueuse/router'
  4. import { storeToRefs } from 'pinia'
  5. import { computed, ref, watch } from 'vue'
  6. import { useRoute, useRouter } from 'vue-router'
  7. import { useStickyHeader } from '#shared/composables/useStickyHeader.ts'
  8. import { EnumOrderDirection } from '#shared/graphql/types.ts'
  9. import { i18n } from '#shared/i18n.ts'
  10. import { useApplicationStore } from '#shared/stores/application.ts'
  11. import { useSessionStore } from '#shared/stores/session.ts'
  12. import CommonLoader from '#mobile/components/CommonLoader/CommonLoader.vue'
  13. import CommonSelectPill from '#mobile/components/CommonSelectPill/CommonSelectPill.vue'
  14. import CommonTicketCreateLink from '#mobile/components/CommonTicketCreateLink/CommonTicketCreateLink.vue'
  15. import LayoutHeader from '#mobile/components/layout/LayoutHeader.vue'
  16. import { useTicketOverviews } from '#mobile/entities/ticket/composables/useTicketOverviews.ts'
  17. import TicketList from '../components/TicketList/TicketList.vue'
  18. import TicketOrderBySelector from '../components/TicketList/TicketOrderBySelector.vue'
  19. const application = useApplicationStore()
  20. const props = defineProps<{
  21. overviewLink: string
  22. }>()
  23. const router = useRouter()
  24. const route = useRoute()
  25. const { overviews, loading: loadingOverviews } =
  26. storeToRefs(useTicketOverviews())
  27. const optionsOverviews = computed(() => {
  28. return overviews.value.map((overview) => ({
  29. value: overview.link,
  30. label: `${i18n.t(overview.name)} (${overview.ticketCount})`,
  31. }))
  32. })
  33. const selectedOverview = computed(() => {
  34. return (
  35. overviews.value.find((overview) => overview.link === props.overviewLink) ||
  36. null
  37. )
  38. })
  39. const selectedOverviewLink = computed(() => {
  40. return selectedOverview.value?.link || null
  41. })
  42. const session = useSessionStore()
  43. const hiddenColumns = computed(() => {
  44. if (session.hasPermission(['ticket.agent'])) return []
  45. const viewColumns =
  46. selectedOverview.value?.viewColumns.map((column) => column.key) || []
  47. // show priority only if it is specified in overview
  48. return ['priority', 'updated_by'].filter(
  49. (name) => !viewColumns.includes(name),
  50. )
  51. })
  52. const selectOverview = (link: string) => {
  53. const { query } = route
  54. return router.replace({ path: `/tickets/view/${link}`, query })
  55. }
  56. watch(
  57. [selectedOverview, overviews],
  58. async ([overview]) => {
  59. if (!overview && overviews.value.length) {
  60. const [firstOverview] = overviews.value
  61. await selectOverview(firstOverview.link)
  62. }
  63. },
  64. { immediate: true },
  65. )
  66. const userOrderBy = useRouteQuery<string | undefined>('column', undefined)
  67. const orderColumnsOptions = computed(() => {
  68. return (
  69. selectedOverview.value?.orderColumns.map((entry) => {
  70. return { value: entry.key, label: entry.value || entry.key }
  71. }) || []
  72. )
  73. })
  74. const orderColumnLabels = computed(() => {
  75. return (
  76. selectedOverview.value?.orderColumns.reduce(
  77. (map, entry) => {
  78. map[entry.key] = entry.value || entry.key
  79. return map
  80. },
  81. {} as Record<string, string>,
  82. ) || {}
  83. )
  84. })
  85. // Check that the given order by column is really a valid column and otherwise
  86. // reset query parameter.
  87. watch(selectedOverview, () => {
  88. if (userOrderBy.value && !orderColumnLabels.value[userOrderBy.value]) {
  89. userOrderBy.value = undefined
  90. }
  91. })
  92. const orderBy = computed({
  93. get: () => {
  94. if (userOrderBy.value && orderColumnLabels.value[userOrderBy.value])
  95. return userOrderBy.value
  96. return selectedOverview.value?.orderBy
  97. },
  98. set: (column) => {
  99. userOrderBy.value =
  100. column !== selectedOverview.value?.orderBy ? column : undefined
  101. },
  102. })
  103. const columnLabel = computed(() => {
  104. return orderColumnLabels.value[orderBy.value || ''] || ''
  105. })
  106. const userOrderDirection = useRouteQuery<EnumOrderDirection | undefined>(
  107. 'direction',
  108. undefined,
  109. )
  110. // Check that the given order direction is a valid direction, otherwise
  111. // reset the query parameter.
  112. if (
  113. userOrderDirection.value &&
  114. !Object.values(EnumOrderDirection).includes(userOrderDirection.value)
  115. ) {
  116. userOrderDirection.value = undefined
  117. }
  118. const orderDirection = computed({
  119. get: () => {
  120. if (userOrderDirection.value) return userOrderDirection.value
  121. return selectedOverview.value?.orderDirection
  122. },
  123. set: (direction) => {
  124. userOrderDirection.value =
  125. direction !== selectedOverview.value?.orderDirection
  126. ? direction
  127. : undefined
  128. },
  129. })
  130. const { stickyStyles, headerElement } = useStickyHeader([loadingOverviews])
  131. const showRefetch = ref(false)
  132. </script>
  133. <template>
  134. <div>
  135. <header
  136. ref="headerElement"
  137. class="border-b-[0.5px] border-white/10 bg-black px-4"
  138. :style="stickyStyles.header"
  139. >
  140. <LayoutHeader
  141. back-url="/"
  142. container-tag="div"
  143. class="h-16 border-none first:px-0"
  144. back-avoid-home-button
  145. :refetch="showRefetch"
  146. :title="__('Tickets')"
  147. >
  148. <template #after>
  149. <CommonTicketCreateLink class="justify-self-end text-base" />
  150. </template>
  151. </LayoutHeader>
  152. <div
  153. v-if="optionsOverviews.length"
  154. class="mb-3 flex items-center justify-between gap-2"
  155. data-test-id="overview"
  156. >
  157. <CommonSelectPill
  158. :model-value="selectedOverviewLink"
  159. :options="optionsOverviews"
  160. no-options-label-translation
  161. @update:model-value="selectOverview($event as string)"
  162. >
  163. <span class="max-w-[55vw] truncate">
  164. {{ $t(selectedOverview?.name) }}
  165. </span>
  166. <span class="px-1"> ({{ selectedOverview?.ticketCount }}) </span>
  167. </CommonSelectPill>
  168. <TicketOrderBySelector
  169. v-model:order-by="orderBy"
  170. v-model:direction="orderDirection"
  171. :options="orderColumnsOptions"
  172. :label="columnLabel"
  173. />
  174. </div>
  175. </header>
  176. <div :style="stickyStyles.body">
  177. <CommonLoader
  178. v-if="loadingOverviews || overviews.length"
  179. :loading="loadingOverviews"
  180. >
  181. <TicketList
  182. v-if="selectedOverview && orderBy && orderDirection"
  183. :overview-id="selectedOverview.id"
  184. :overview-ticket-count="selectedOverview.ticketCount"
  185. :order-by="orderBy"
  186. :order-direction="orderDirection"
  187. :max-count="application.config.ui_ticket_overview_ticket_limit"
  188. :hidden-columns="hiddenColumns"
  189. @refetch="showRefetch = $event"
  190. />
  191. </CommonLoader>
  192. <div
  193. v-else
  194. class="flex items-center justify-center gap-2 p-4 text-center"
  195. >
  196. <CommonIcon class="text-red" name="close-small" />
  197. {{ $t('Currently no overview is assigned to your roles.') }}
  198. </div>
  199. </div>
  200. </div>
  201. </template>