-
Notifications
You must be signed in to change notification settings - Fork 0
feat: create message action #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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= | ||
|
|
||
|
|
||
| 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') | ||
| } | ||
| } | ||
| </script> | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
|
|
||
| <h3 class="text-xl/5 font-bold"> | ||
| {{ ticket.title }} | ||
|
|
@@ -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> | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prop destructuring drops reactivity; and the condition should be “!==”. Destructuring defineProps without toRefs makes -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" ...
🤖 Prompt for AI Agents |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authenticate request early. Guard missing user; otherwise 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| const body = await readBody(event) | ||||||||||||||||||||||||||||||||||||||||||||
| const data = createTicketMessageSchema(body) | ||||||||||||||||||||||||||||||||||||||||||||
| if (data instanceof type.errors) { | ||||||||||||||||||||||||||||||||||||||||||||
| throw data | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
}
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainPotential 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.tsLength 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 |
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trim input, guard whitespace, reset state, and add loading flag.
Prevents whitespace‑only posts and cleans up after success.
📝 Committable suggestion
🤖 Prompt for AI Agents