Skip to content

Commit 495cac9

Browse files
authored
feat: show tickets page (#170)
* feat: show tickets page * chore: update
1 parent 80c8ffe commit 495cac9

12 files changed

Lines changed: 354 additions & 10 deletions

File tree

apps/atrium-telegram/app/app.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ watch(colorMode, () => {
6060
// Init Stores
6161
const user = useUserStore()
6262
const task = useTaskStore()
63-
const epic = useEpicStore()
63+
const ticket = useTicketStore()
6464
const kitchen = useKitchenStore()
6565
const flow = useFlowStore()
6666
@@ -79,7 +79,7 @@ onMounted(async () => {
7979
user.updateOnline(),
8080
user.update(),
8181
task.update(),
82-
epic.update(),
82+
ticket.update(),
8383
kitchen.update(),
8484
flow.update(),
8585
])
@@ -89,11 +89,11 @@ onMounted(async () => {
8989
user.updateOnline(),
9090
user.update(),
9191
task.update(),
92-
epic.update(),
92+
ticket.update(),
9393
kitchen.update(),
9494
flow.update(),
9595
])
96-
}, 30000)
96+
}, 20000)
9797
})
9898
9999
onUnmounted(() => {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<template>
2+
<ActiveCard>
3+
<UIcon name="i-lucide-mail-question-mark" class="size-8 text-primary" />
4+
5+
<h3 class="text-xl/5 font-bold">
6+
{{ ticket.title }}
7+
</h3>
8+
9+
<div class="w-full text-base/5 font-normal whitespace-pre-wrap break-words line-clamp-5">
10+
{{ ticket.description }}
11+
</div>
12+
13+
<div class="flex justify-between items-center">
14+
<div class="flex flex-row gap-4">
15+
<div class="flex flex-row gap-1.5 items-center text-muted">
16+
<UIcon name="i-lucide-message-circle" class="size-5" />
17+
<p>{{ ticket?.messages.length }}</p>
18+
</div>
19+
</div>
20+
21+
<time
22+
:datetime="ticket.updatedAt"
23+
class="text-sm text-muted"
24+
v-text="format(new Date(ticket.updatedAt), 'обновлен d MMMM yyyy', { locale: ru })"
25+
/>
26+
</div>
27+
</ActiveCard>
28+
</template>
29+
30+
<script setup lang="ts">
31+
import type { TicketWithData } from '~/stores/ticket'
32+
import { format } from 'date-fns'
33+
import { ru } from 'date-fns/locale/ru'
34+
35+
defineProps<{
36+
ticket: TicketWithData
37+
}>()
38+
</script>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<template>
2+
<div class="flex flex-row gap-2 items-start">
3+
<div class="mt-2.5">
4+
<UAvatar :src="user?.avatarUrl ?? undefined" />
5+
</div>
6+
<div class="w-full flex flex-col gap-1.5">
7+
<UDropdownMenu
8+
:items="items"
9+
:ui="{
10+
content: 'w-56',
11+
item: 'p-2 motion-preset-slide-left motion-duration-200',
12+
}"
13+
:content="{
14+
sideOffset: -32,
15+
}"
16+
>
17+
<ActiveCard>
18+
<div class="w-full relative flex flex-col justify-between gap-2">
19+
<div class="flex flex-col gap-1">
20+
<div class="text-base/5 whitespace-break-spaces text-default font-medium">
21+
{{ message?.text }}
22+
</div>
23+
24+
<div v-if="message?.fileUrl && message.fileType !== 'image'">
25+
<UButton
26+
variant="solid"
27+
color="secondary"
28+
:icon="getFileIcon(message.fileType)"
29+
@click="handleFileClick(message.fileUrl)"
30+
>
31+
Прикрепленный файл
32+
</UButton>
33+
</div>
34+
<div v-else-if="message?.fileUrl && message.fileType === 'image'">
35+
<img
36+
:src="message.fileUrl"
37+
alt=""
38+
class="w-full h-full object-contain rounded-lg"
39+
@click="handleFileClick(message.fileUrl)"
40+
>
41+
</div>
42+
43+
<div v-if="message?.createdAt" class="mt-1 flex justify-end text-xs text-muted">
44+
{{ format(new Date(message.createdAt), 'dd MMMM в HH:mm', { locale: ru }) }}
45+
</div>
46+
</div>
47+
</div>
48+
</ActiveCard>
49+
</UDropdownMenu>
50+
51+
<!-- <div v-if="message?.notifications?.length" class="-mt-4 ml-4 flex flex-row flex-wrap gap-1">
52+
<UserBeacon
53+
v-for="notification in comment.notifications"
54+
:key="notification.id"
55+
:notification="notification"
56+
/>
57+
</div> -->
58+
</div>
59+
</div>
60+
</template>
61+
62+
<script setup lang="ts">
63+
import type { DropdownMenuItem } from '@nuxt/ui'
64+
import type { TicketMessage } from '@roll-stack/database'
65+
import { format } from 'date-fns'
66+
import { ru } from 'date-fns/locale/ru'
67+
68+
const { ticketId, messageId } = defineProps<{
69+
ticketId: string
70+
messageId: string
71+
}>()
72+
73+
// const overlay = useOverlay()
74+
// const modalCreateEpicCommentBeacon = overlay.create(ModalCreateEpicCommentBeacon)
75+
76+
const ticketStore = useTicketStore()
77+
const userStore = useUserStore()
78+
79+
const ticket = computed(() => ticketStore.tickets.find((t) => t.id === ticketId))
80+
const message = computed(() => ticket.value?.messages.find((m) => m.id === messageId))
81+
const user = computed(() => userStore.find(message.value?.userId ?? ''))
82+
83+
const items = computed<DropdownMenuItem[]>(() => {
84+
const menuItems: DropdownMenuItem[] = [
85+
{
86+
label: 'Скопировать сообщение',
87+
icon: 'i-lucide-copy',
88+
color: 'neutral',
89+
disabled: false,
90+
onSelect: () => navigator.clipboard.writeText(message.value?.text ?? ''),
91+
condition: true,
92+
},
93+
// {
94+
// label: 'Маякнуть (будет позже)',
95+
// icon: 'i-lucide-users-round',
96+
// color: 'neutral',
97+
// disabled: true,
98+
// onSelect: () => modalCreateEpicCommentBeacon.open({ messageId }),
99+
// condition: true,
100+
// },
101+
{
102+
label: 'Лайкнуть (будет позже)',
103+
icon: 'i-lucide-thumbs-up',
104+
color: 'neutral',
105+
disabled: true,
106+
onSelect: () => {},
107+
condition: user.value?.id !== userStore.id,
108+
},
109+
{
110+
label: 'Редактировать',
111+
icon: 'i-lucide-edit',
112+
disabled: true,
113+
onSelect: () => {},
114+
condition: user.value?.id === userStore.id,
115+
},
116+
{
117+
label: 'Удалить',
118+
icon: 'i-lucide-trash-2',
119+
disabled: true,
120+
onSelect: () => {},
121+
condition: user.value?.id === userStore.id,
122+
},
123+
]
124+
125+
return menuItems.filter((item) => item.condition)
126+
})
127+
128+
function getFileIcon(type: TicketMessage['fileType']) {
129+
switch (type) {
130+
case 'image':
131+
return 'i-lucide-image'
132+
case 'video':
133+
return 'i-lucide-video'
134+
case 'document':
135+
return 'i-lucide-file'
136+
default:
137+
return 'i-lucide-file'
138+
}
139+
}
140+
141+
function handleFileClick(fileUrl: string) {
142+
window.open(fileUrl, '_blank')
143+
}
144+
</script>

apps/atrium-telegram/app/composables/useNavigation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ function _useNavigation() {
1616
badge: flowStore.nowViewedItemsCount.toString(),
1717
},
1818
{
19-
path: '/epic',
20-
names: ['epic', 'epic-epicId'],
21-
title: t('app.epics'),
22-
icon: 'i-lucide-crown',
19+
path: '/ticket',
20+
names: ['ticket', 'ticket-ticketId'],
21+
title: t('app.tickets'),
22+
icon: 'i-lucide-mail-question-mark',
2323
},
2424
{
2525
path: '/tasks',

apps/atrium-telegram/app/pages/index.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
</NuxtLink>
3535
</div>
3636
</div>
37+
38+
<div class="mt-16 flex flex-row justify-center">
39+
<UIcon name="i-lucide-route" class="size-8 text-dimmed/25" />
40+
</div>
3741
</PageContainer>
3842
</template>
3943

apps/atrium-telegram/app/pages/startapp.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ if (tgWebAppStartParam?.length && tgWebAppStartParam.includes(separator)) {
2727
case 'epic':
2828
await navigateTo({ path: `/epic/${value}`, query })
2929
break
30+
case 'ticket':
31+
await navigateTo({ path: `/ticket/${value}`, query })
32+
break
3033
default:
3134
await navigateTo('/')
3235
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<template>
2+
<PageContainer>
3+
<Section>
4+
<div class="flex flex-row items-start justify-between gap-2.5">
5+
<UIcon name="i-lucide-mail-question-mark" class="size-10 text-primary" />
6+
</div>
7+
8+
<h1 class="text-2xl/6 font-bold">
9+
{{ ticket?.title }}
10+
</h1>
11+
12+
<div class="w-full text-base/5 whitespace-pre-wrap break-words">
13+
{{ ticket?.description }}
14+
</div>
15+
</Section>
16+
17+
<Section class="flex flex-row justify-between items-center">
18+
<div class="flex flex-row items-center gap-2">
19+
<UIcon name="i-lucide-message-circle" class="size-5" />
20+
{{ ticket?.messages.length }} {{ pluralizationRu(ticket?.messages.length ?? 0, ['сообщение', 'сообщения', 'сообщений']) }}
21+
</div>
22+
</Section>
23+
24+
<div class="w-full flex flex-col gap-3.5 flex-1 last-of-type:mb-20">
25+
<TicketMessage
26+
v-for="message in messages"
27+
:key="message.id"
28+
:ticket-id="message.ticketId"
29+
:message-id="message.id"
30+
/>
31+
32+
<UButton
33+
v-if="isShowMore"
34+
variant="solid"
35+
color="secondary"
36+
size="xl"
37+
class="w-full items-center justify-center"
38+
icon="i-lucide-message-circle"
39+
:label="$t('common.show-more')"
40+
@click="shownMessages += 10"
41+
/>
42+
</div>
43+
44+
<!-- <UDrawer v-model:open="isDrawerOpened">
45+
<CreateCard
46+
v-if="epic?.id"
47+
:label="$t('app.create.epic-comment.button')"
48+
icon="i-lucide-message-circle"
49+
/>
50+
51+
<template #body>
52+
<FormCreateEpicComment
53+
:epic-id="epic?.id ?? ''"
54+
@submitted="isDrawerOpened = false"
55+
@success="isDrawerOpened = false"
56+
/>
57+
</template>
58+
</UDrawer> -->
59+
</PageContainer>
60+
</template>
61+
62+
<script setup lang="ts">
63+
definePageMeta({
64+
name: 'ticket-ticketId',
65+
canReturn: true,
66+
})
67+
68+
const { params } = useRoute('ticket-ticketId')
69+
70+
const ticketStore = useTicketStore()
71+
const ticket = computed(() => ticketStore.tickets.find((e) => e.id === params.ticketId))
72+
73+
// On load show last 10 messages. On button click = show more 10 messages
74+
const shownMessages = ref(10)
75+
const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value))
76+
const isShowMore = computed<boolean>(() => messages.value?.length && ticket.value?.messages.length ? messages.value.length < ticket.value.messages.length : false)
77+
78+
// const isDrawerOpened = ref(false)
79+
</script>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<PageContainer>
3+
<NuxtLink
4+
v-for="ticket of ticketStore.tickets"
5+
:key="ticket.id"
6+
:to="`/ticket/${ticket.id}`"
7+
class="motion-preset-slide-left"
8+
>
9+
<TicketCard :ticket="ticket">
10+
{{ ticket.title }}
11+
</TicketCard>
12+
</NuxtLink>
13+
14+
<div class="mt-16 flex flex-row justify-center">
15+
<UIcon name="i-lucide-route" class="size-8 text-dimmed/25" />
16+
</div>
17+
</PageContainer>
18+
</template>
19+
20+
<script setup lang="ts">
21+
const ticketStore = useTicketStore()
22+
</script>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Ticket, TicketMessage, User } from '@roll-stack/database'
2+
import { initDataRaw as _initDataRaw, useSignal } from '@telegram-apps/sdk-vue'
3+
4+
export type TicketWithData = Ticket & {
5+
messages: TicketMessage[]
6+
lastMessage: TicketMessage | null
7+
user: User
8+
}
9+
10+
export const useTicketStore = defineStore('ticket', () => {
11+
const tickets = ref<TicketWithData[]>([])
12+
13+
const initDataRaw = useSignal(_initDataRaw)
14+
15+
async function update() {
16+
try {
17+
const data = await $fetch('/api/ticket/list', {
18+
headers: {
19+
Authorization: `tma ${initDataRaw.value}`,
20+
},
21+
})
22+
if (!data) {
23+
return
24+
}
25+
26+
tickets.value = data
27+
} catch (error) {
28+
if (error instanceof Error) {
29+
if (error.message.includes('401')) {
30+
// No session
31+
}
32+
if (error.message.includes('404')) {
33+
// Not found
34+
}
35+
}
36+
}
37+
}
38+
39+
return {
40+
tickets,
41+
42+
update,
43+
}
44+
})

0 commit comments

Comments
 (0)