Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/atrium-telegram/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Main database
DATABASE_URL=

# Queue
QUEUE_URL=

# Main API
NUXT_PUBLIC_CORE_API_URL=

Expand Down
8 changes: 8 additions & 0 deletions apps/atrium-telegram/app/components/PageContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
<div class="z-30 overflow-hidden h-full min-h-dvh w-full tg-content-safe-area">
<div class="px-4 py-2 max-w-[28rem] mx-auto mb-20 flex flex-col gap-y-6" :class="className">
<slot />

<div class="mt-16 flex flex-row justify-center">
<img
src="/sushi-heart.svg"
alt=""
class="w-10 opacity-25 invert-50"
>
</div>
</div>
</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template>
<UForm
:validate="createValidator(createTicketMessageSchema)"
:state="state"
class="flex flex-col gap-3"
@submit="onSubmit"
>
<UFormField label="Ваше сообщение" name="text">
<UTextarea
v-model="state.text"
placeholder="Не торопись, осмотрись..."
autoresize
size="xl"
class="w-full"
/>
</UFormField>

<UButton
type="submit"
variant="solid"
color="secondary"
size="xl"
icon="i-lucide-send"
block
class="mt-3"
:disabled="!state.text"
:label="$t('common.send')"
/>
</UForm>
</template>

<script setup lang="ts">
import type { CreateTicketMessage } from '#shared/services/ticket'
import type { FormSubmitEvent } from '@nuxt/ui'
import { createTicketMessageSchema } from '#shared/services/ticket'

const { ticketId } = defineProps<{ ticketId: string }>()

const emit = defineEmits(['success', 'submitted'])

const { vibrate } = useFeedback()
const userStore = useUserStore()
const ticketStore = useTicketStore()

const state = ref<Partial<CreateTicketMessage>>({
text: undefined,
})

async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) {
emit('submitted')

try {
await $fetch(`/api/ticket/id/${ticketId}/message`, {
method: 'POST',
headers: {
Authorization: `tma ${userStore.initDataRaw}`,
},
body: event.data,
})

await Promise.all([
ticketStore.update(),
userStore.update(),
])

vibrate('success')
emit('success')
} catch (error) {
console.error(error)
vibrate('error')
}
}
Comment on lines +49 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Trim input, guard whitespace, reset state, and add loading flag.

Prevents whitespace‑only posts and cleans up after success.

+const loading = ref(false)
 async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) {
   emit('submitted')
 
   try {
+    loading.value = true
+    const text = event.data.text.trim()
+    if (!text) {
+      vibrate('error')
+      return
+    }
     await $fetch(`/api/ticket/id/${ticketId}/message`, {
       method: 'POST',
       headers: {
         Authorization: `tma ${userStore.initDataRaw}`,
       },
-      body: event.data,
+      body: { text },
     })
 
     await Promise.all([
       ticketStore.update(),
       userStore.update(),
     ])
 
     vibrate('success')
     emit('success')
   } catch (error) {
     console.error(error)
     vibrate('error')
   }
+  finally {
+    loading.value = false
+    state.value.text = ''
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) {
emit('submitted')
try {
await $fetch(`/api/ticket/id/${ticketId}/message`, {
method: 'POST',
headers: {
Authorization: `tma ${userStore.initDataRaw}`,
},
body: event.data,
})
await Promise.all([
ticketStore.update(),
userStore.update(),
])
vibrate('success')
emit('success')
} catch (error) {
console.error(error)
vibrate('error')
}
}
const loading = ref(false)
async function onSubmit(event: FormSubmitEvent<CreateTicketMessage>) {
emit('submitted')
try {
loading.value = true
const text = event.data.text.trim()
if (!text) {
vibrate('error')
return
}
await $fetch(`/api/ticket/id/${ticketId}/message`, {
method: 'POST',
headers: {
Authorization: `tma ${userStore.initDataRaw}`,
},
body: { text },
})
await Promise.all([
ticketStore.update(),
userStore.update(),
])
vibrate('success')
emit('success')
} catch (error) {
console.error(error)
vibrate('error')
} finally {
loading.value = false
state.value.text = ''
}
}
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/form/CreateTicketMessage.vue around lines
49 to 72, the submit handler should trim the input, block whitespace-only
messages, manage a loading flag, and reset form state after success: before
sending, set a local loading=true; trim the relevant text fields on event.data
(or create a trimmed copy) and if the trimmed message is empty, set
loading=false and abort (or emit a validation error); proceed to call the API
with the trimmed payload; in a finally block set loading=false; on success
clear/reset the form model/state and emit 'success' (keep vibrate calls as-is);
ensure errors still log and vibrate('error').

</script>
20 changes: 17 additions & 3 deletions apps/atrium-telegram/app/components/ticket/Card.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
<template>
<ActiveCard>
<UIcon name="i-lucide-mail-question-mark" class="size-8 text-primary" />
<div class="flex flex-row gap-2 items-center">
<UIcon name="i-lucide-mail-question-mark" class="size-8 text-primary" />

<div v-if="hasAnswerFromUser" class="flex flex-row items-center gap-1.5 text-error">
<UIcon
name="i-lucide-pointer"
class="size-8 motion-translate-y-loop-25 motion-preset-seesaw motion-duration-2000"
/>
<p class="max-w-22 text-sm/4 font-bold">
Есть ответ от партнера
</p>
</div>
</div>
Comment on lines +6 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Wrong condition for “Ответ от партнера”.

The flag triggers when last message is by the ticket owner, not the partner. Invert the check.

Apply with the script change below (see Lines 47–52).

🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/ticket/Card.vue around lines 6–15 (and
adjust the related logic at lines 47–52), the template currently shows "Есть
ответ от партнера" when hasAnswerFromUser is true; invert that check so the UI
is shown when the last message is from the partner (e.g., use !hasAnswerFromUser
or a proper hasAnswerFromPartner flag). Update the logic at lines 47–52 where
the last-message author is evaluated to set the boolean true when the author is
the partner (or rename the computed flag to reflect partner ownership) and
adjust all usages accordingly to maintain consistent naming.


<h3 class="text-xl/5 font-bold">
{{ ticket.title }}
Expand All @@ -21,7 +33,7 @@
<time
:datetime="ticket.updatedAt"
class="text-sm text-muted"
v-text="format(new Date(ticket.updatedAt), 'обновлен d MMMM yyyy', { locale: ru })"
v-text="format(new Date(ticket.updatedAt), 'd MMMM yyyy в HH:mm', { locale: ru })"
/>
</div>
</ActiveCard>
Expand All @@ -32,7 +44,9 @@ import type { TicketWithData } from '~/stores/ticket'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale/ru'

defineProps<{
const { ticket } = defineProps<{
ticket: TicketWithData
}>()

const hasAnswerFromUser = computed(() => ticket.lastMessage?.userId === ticket.userId)
</script>
Comment on lines +47 to 52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prop destructuring drops reactivity; and the condition should be “!==”.

Destructuring defineProps without toRefs makes ticket non‑reactive. Also fix partner reply logic and name accordingly.

-const { ticket } = defineProps<{
-  ticket: TicketWithData
-}>()
-
-const hasAnswerFromUser = computed(() => ticket.lastMessage?.userId === ticket.userId)
+const props = defineProps<{ ticket: TicketWithData }>()
+const { ticket } = toRefs(props)
+
+const hasAnswerFromPartner = computed(
+  () => ticket.value.lastMessage?.userId !== ticket.value.userId
+)

And in template:

-<div v-if="hasAnswerFromUser" ...
+<div v-if="hasAnswerFromPartner" ...

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/ticket/Card.vue around lines 47–52, the
current destructuring of defineProps makes ticket non‑reactive and the computed
uses the wrong equality; stop destructuring props so reactivity is preserved
(e.g. const props = defineProps<{ ticket: TicketWithData }>(); then create a
reactive ref via toRef(props, 'ticket') or use toRefs), rename the computed to
reflect partner reply (e.g. hasAnswerFromPartner) and change the condition to
!== (ticket.lastMessage?.userId !== ticket.userId), and update template bindings
to use the ref appropriately so reactivity works.

2 changes: 1 addition & 1 deletion apps/atrium-telegram/app/components/ticket/Message.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-row gap-2 items-start">
<div class="mt-2.5">
<UAvatar :src="user?.avatarUrl ?? undefined" />
<UAvatar :src="user?.avatarUrl ?? undefined" size="lg" />
</div>
<div class="relative w-full flex flex-col gap-1.5">
<UDropdownMenu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<UIcon :name="getFileData(message.fileType).icon" class="size-10 text-primary" />

<UButton
variant="solid"
variant="soft"
color="secondary"
:label="getFileData(message.fileType).label"
/>
Expand Down
2 changes: 2 additions & 0 deletions apps/atrium-telegram/app/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ function _useNavigation() {

const taskStore = useTaskStore()
const flowStore = useFlowStore()
const ticketStore = useTicketStore()

const mainRoutes = computed<NavigationRoute[]>(() => [
{
Expand All @@ -20,6 +21,7 @@ function _useNavigation() {
names: ['ticket', 'ticket-ticketId'],
title: t('app.tickets'),
icon: 'i-lucide-mail-question-mark',
badge: ticketStore.ticketsWithoutAnswer.length.toString(),
},
{
path: '/tasks',
Expand Down
20 changes: 20 additions & 0 deletions apps/atrium-telegram/app/pages/flow/[itemId]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@
<div class="w-full text-base/5 whitespace-pre-wrap break-words">
{{ item?.description }}
</div>

<div class="mt-6 flex justify-between items-center">
<div class="flex flex-row gap-4">
<div class="flex flex-row gap-1.5 items-center text-muted text-sm">
<UIcon name="i-lucide-message-circle" class="size-5" />
<p>0</p>
</div>
</div>

<time
v-if="item?.createdAt"
:datetime="item.createdAt"
class="text-sm text-muted"
v-text="format(new Date(item.createdAt), 'd MMMM yyyy в HH:mm', { locale: ru })"
/>
</div>
</Section>

<Section class="flex flex-col">
Expand All @@ -24,13 +40,17 @@
v-for="view in item?.views"
:key="view.id"
:src="userStore.getAvatarUrl(view.userId)"
size="lg"
/>
</div>
</Section>
</PageContainer>
</template>

<script setup lang="ts">
import { format } from 'date-fns'
import { ru } from 'date-fns/locale/ru'

definePageMeta({
name: 'flow-itemId',
canReturn: true,
Expand Down
4 changes: 0 additions & 4 deletions apps/atrium-telegram/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
</NuxtLink>
</div>
</div>

<div class="mt-16 flex flex-row justify-center">
<UIcon name="i-lucide-route" class="size-8 text-dimmed/25" />
</div>
</PageContainer>
</template>

Expand Down
44 changes: 22 additions & 22 deletions apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,23 @@
</div>
</Section>

<div class="w-full flex flex-col gap-3.5 flex-1 last-of-type:mb-20">
<div class="w-full flex flex-col gap-3.5 flex-1">
<UDrawer v-model:open="isDrawerOpened">
<CreateCard
label="Написать сообщение"
icon="i-lucide-message-circle"
class="mb-4"
/>

<template #body>
<FormCreateTicketMessage
:ticket-id="ticket?.id ?? ''"
@submitted="isDrawerOpened = false"
@success="isDrawerOpened = false"
/>
</template>
</UDrawer>

<TicketMessage
v-for="message in messages"
:key="message.id"
Expand All @@ -40,31 +56,15 @@

<UButton
v-if="isShowMore"
variant="solid"
color="secondary"
variant="soft"
color="primary"
size="xl"
class="w-full items-center justify-center"
icon="i-lucide-message-circle"
class="mt-6 mx-auto w-fit items-center justify-center"
icon="i-lucide-message-circle-more"
:label="$t('common.show-more')"
@click="handleClickShowMore()"
/>
</div>

<!-- <UDrawer v-model:open="isDrawerOpened">
<CreateCard
v-if="epic?.id"
:label="$t('app.create.epic-comment.button')"
icon="i-lucide-message-circle"
/>

<template #body>
<FormCreateEpicComment
:epic-id="epic?.id ?? ''"
@submitted="isDrawerOpened = false"
@success="isDrawerOpened = false"
/>
</template>
</UDrawer> -->
</PageContainer>
</template>

Expand All @@ -88,7 +88,7 @@ const shownMessages = ref(10)
const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value))
const isShowMore = computed<boolean>(() => messages.value?.length && ticket.value?.messages.length ? messages.value.length < ticket.value.messages.length : false)

// const isDrawerOpened = ref(false)
const isDrawerOpened = ref(false)

function handleClickShowMore() {
vibrate('success')
Expand Down
4 changes: 0 additions & 4 deletions apps/atrium-telegram/app/pages/ticket/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
{{ ticket.title }}
</TicketCard>
</NuxtLink>

<div class="mt-16 flex flex-row justify-center">
<UIcon name="i-lucide-route" class="size-8 text-dimmed/25" />
</div>
</PageContainer>
</template>

Expand Down
3 changes: 3 additions & 0 deletions apps/atrium-telegram/app/stores/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type TicketWithData = Ticket & {
export const useTicketStore = defineStore('ticket', () => {
const tickets = ref<TicketWithData[]>([])

const ticketsWithoutAnswer = computed(() => tickets.value.filter((ticket) => ticket.lastMessage?.userId === ticket.userId))

const initDataRaw = useSignal(_initDataRaw)

async function update() {
Expand Down Expand Up @@ -38,6 +40,7 @@ export const useTicketStore = defineStore('ticket', () => {

return {
tickets,
ticketsWithoutAnswer,

update,
}
Expand Down
1 change: 1 addition & 0 deletions apps/atrium-telegram/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@neoconfetti/vue": "catalog:",
"@pinia/nuxt": "catalog:",
"@roll-stack/database": "workspace:*",
"@roll-stack/queue": "workspace:*",
"@roll-stack/ui": "workspace:*",
"@telegram-apps/init-data-node": "catalog:",
"@telegram-apps/sdk-vue": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createTicketMessageSchema } from '#shared/services/ticket'
import { repository } from '@roll-stack/database'
import { repository as queue } from '@roll-stack/queue'
import { type } from 'arktype'

export default defineEventHandler(async (event) => {
try {
const ticketId = getRouterParam(event, 'ticketId')
if (!ticketId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}

Comment on lines +6 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Authenticate request early.

Guard missing user; otherwise event.context.user.id will throw.

 export default defineEventHandler(async (event) => {
   try {
+    if (!event.context.user) {
+      throw createError({ statusCode: 401, message: 'Unauthorized' })
+    }
     const ticketId = getRouterParam(event, 'ticketId')
     if (!ticketId) {
       throw createError({
         statusCode: 400,
         message: 'Id is required',
       })
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default defineEventHandler(async (event) => {
try {
const ticketId = getRouterParam(event, 'ticketId')
if (!ticketId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}
export default defineEventHandler(async (event) => {
try {
if (!event.context.user) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const ticketId = getRouterParam(event, 'ticketId')
if (!ticketId) {
throw createError({
statusCode: 400,
message: 'Id is required',
})
}
🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts around
lines 6–15, the handler accesses event.context.user.id without verifying the
user exists; add an early authentication guard: check event.context &&
event.context.user (or event.context.user) immediately after retrieving ticketId
and before any usage, and if missing throw a 401/Unauthorized error via
createError; then safely read and use user.id from a local variable. Ensure the
guard runs before any other logic to prevent runtime exceptions.

const body = await readBody(event)
const data = createTicketMessageSchema(body)
if (data instanceof type.errors) {
throw data
}

Comment on lines +16 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate after trimming; reject whitespace-only.

Current schema accepts spaces. Normalize and re‑check.

-    const body = await readBody(event)
-    const data = createTicketMessageSchema(body)
+    const body = await readBody(event)
+    const data = createTicketMessageSchema(body)
+    data.text = data.text.trim()
+    if (!data.text) {
+      throw createError({ statusCode: 400, message: 'Text must not be empty' })
+    }
     if (data instanceof type.errors) {
       throw data
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts around
lines 16 to 21, the code validates the raw body but doesn't trim string fields
first so inputs of only whitespace pass; before calling
createTicketMessageSchema, normalize the incoming body by trimming all string
fields (e.g., body.message = body.message?.trim()) and then re-run validation,
and update the schema to reject empty strings (or add a post-validate check) so
that whitespace-only values fail validation and the handler throws an error as
intended.

const ticket = await repository.ticket.find(ticketId)
if (!ticket) {
throw createError({
statusCode: 404,
message: 'Ticket not found',
})
}

const message = await repository.ticket.createMessage({
ticketId,
userId: event.context.user.id,
text: data.text,
})
if (!message) {
throw createError({
statusCode: 400,
message: 'Failed to create message',
})
}

// Event
await queue.ticket.messageCreated({
ticketId: message.ticketId,
ticketOwnerId: ticket.userId,
messageId: message.id,
userId: message.userId,
userName: event.context.user.name,
userSurname: event.context.user.surname,
userText: message.text,
})

return {
ok: true,
result: message,
}
} catch (error) {
throw errorResolver(error)
}
})
Comment on lines +57 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Potential missing import for errorResolver.

Unless auto‑imported, this will throw ReferenceError.

If missing, add the import with the correct path:

+import { errorResolver } from '~/server/utils/errorResolver' // adjust path if different

🏁 Script executed:

#!/bin/bash
# Verify errorResolver is globally/auto imported somewhere
rg -n "errorResolver" -C2 apps/atrium-telegram/server | sed -n '1,200p'

Length of output: 7620


🏁 Script executed:

#!/bin/bash
# Display the top of the ticket message handler to check for errorResolver import
sed -n '1,30p' apps/atrium-telegram/server/api/ticket/id/\[ticketId\]/message.post.ts

Length of output: 836


Add missing import for errorResolver

apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts calls errorResolver in the catch but does not import it — add at the top:

import { errorResolver } from '../../../../utils/error'

🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/id/[ticketId]/message.post.ts around
lines 57-60, the catch block calls errorResolver but the module is not imported;
add the import statement at the top of the file: import { errorResolver } from
'../../../../utils/error' so the function is available for use in the catch
block.

Loading