fix
Deploy banu-front / deploy (push) Successful in 1m27s

This commit is contained in:
sajjadtalkhabi
2026-07-07 17:36:43 +03:30
parent b9ef684b51
commit 38a396ba21
89 changed files with 1815 additions and 478 deletions
@@ -14,7 +14,17 @@
</div>
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
<div v-else-if="messages.length > 0" class="ticket-details__thread" ref="thread">
<div
v-else-if="messages.length > 0 || ticketMedia.length > 0"
class="ticket-details__thread"
ref="thread"
>
<div v-if="ticketMedia.length > 0" class="ticket-details__row ticket-details__row--user">
<div class="ticket-details__bubble">
<MessageAttachments :media="ticketMedia" />
<span class="ticket-details__time">{{ messageTime(ticket) }}</span>
</div>
</div>
<div
v-for="message in messages"
:key="message.id"
@@ -22,6 +32,7 @@
:class="`ticket-details__row--${senderClass(message)}`"
>
<div class="ticket-details__bubble">
<MessageAttachments v-if="message.media?.length" :media="message.media" />
<p class="ticket-details__text">{{ message.message }}</p>
<span class="ticket-details__time">{{ messageTime(message) }}</span>
</div>
@@ -31,6 +42,25 @@
<div class="ticket-details__divider" />
<div v-if="attachments.length > 0" class="ticket-details__attachments">
<span
v-for="(file, index) in attachments"
:key="file.id ?? index"
class="ticket-details__attachment"
>
<SvgIcon name="file" :size="14" color="currentColor" />
<span class="ticket-details__attachment-name">{{ file.name }}</span>
<button
type="button"
class="ticket-details__attachment-remove"
aria-label="حذف فایل"
@click="removeAttachment(index)"
>
<SvgIcon name="close" :size="12" color="var(--color-error)" />
</button>
</span>
</div>
<form class="ticket-details__compose" @submit.prevent="onSend">
<button
type="submit"
@@ -57,6 +87,23 @@
>
<SvgIcon name="mood" :size="30" color="currentColor" />
</button>
<button
type="button"
class="ticket-details__compose-btn"
:disabled="uploadMutation.isPending.value"
aria-label="پیوست فایل"
@click="fileInput?.click()"
>
<SvgIcon name="attach-file" :size="30" color="currentColor" />
</button>
<input
ref="fileInput"
type="file"
class="ticket-details__file-input"
accept="image/*,application/pdf"
multiple
@change="onFilesPicked"
/>
</div>
</form>
</div>
@@ -81,13 +128,17 @@
</template>
<script setup>
import { toast } from 'vue3-toastify'
import useModal from '@/composables/useModal'
import { useQueryClient } from '@tanstack/vue-query'
import BasicModal from '@/components/BasicModal.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import { objectToFormData } from '@/utils/object-to-formdata'
import { useUploadMediaMutation } from '@/services/query/auth'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import MessageAttachments from '@/components/blocks/MessageAttachments.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
autoUpdate,
@@ -143,6 +194,8 @@ const { data: ticket, isLoading } = useAdminTicketQuery(ticketId, {
const messages = computed(() => ticket.value?.messages ?? [])
const ticketMedia = computed(() => ticket.value?.media ?? [])
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
// Anyone whose id matches the ticket's student is "the student"; everyone else
@@ -157,7 +210,49 @@ const messageTime = (message) => {
}
const text = ref('')
const canSend = computed(() => text.value.trim().length > 0)
const MAX_ATTACHMENTS = 5
const fileInput = ref(null)
const attachments = ref([])
const uploadMutation = useUploadMediaMutation()
const onFilesPicked = async (event) => {
const picked = [...event.target.files]
event.target.value = ''
if (attachments.value.length + picked.length > MAX_ATTACHMENTS) {
toast.warning(`حداکثر تعداد فایل مجاز ${MAX_ATTACHMENTS} عدد است.`)
return
}
for (const file of picked) {
try {
const fd = objectToFormData({ file, purpose: 'attachment', context: 'ticket' })
const response = await uploadMutation.mutateAsync(fd)
const payload = response?.data ?? response
const id = payload?.id ?? payload?.uploadId
if (id == null) continue
attachments.value = [
...attachments.value,
{ id, name: file.name, size: file.size, url: payload?.url ?? '' },
]
} catch {
toast.error(`بارگذاری فایل "${file.name}" با خطا مواجه شد.`)
}
}
}
const removeAttachment = (index) => {
const next = [...attachments.value]
next.splice(index, 1)
attachments.value = next
}
const canSend = computed(
() =>
(text.value.trim().length > 0 || attachments.value.length > 0) &&
!uploadMutation.isPending.value
)
const emojiOpen = ref(false)
const emojiButton = ref(null)
@@ -213,10 +308,15 @@ const sendMutation = useSendAdminTicketMessageMutation()
const onSend = async () => {
if (!canSend.value || !ticketId.value) return
const value = text.value.trim()
const mediaIds = attachments.value.map((f) => f.id).filter((id) => id != null)
text.value = ''
attachments.value = []
emojiOpen.value = false
// Backend POST /admin/tickets/:id/messages — body is { message: string }.
await sendMutation.mutateAsync({ id: ticketId.value, payload: { message: value } })
// Backend POST /admin/tickets/:id/messages — body is { message: string,
// mediaIds?: number[] } where mediaIds reference previously uploaded media.
const payload = { message: value }
if (mediaIds.length > 0) payload.mediaIds = mediaIds
await sendMutation.mutateAsync({ id: ticketId.value, payload })
await queryClient.invalidateQueries({ queryKey: adminTicketsKeys.all })
}
@@ -405,6 +505,12 @@ watch(messages, async () => {
display: none;
}
&__attachments {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
&__attachment {
display: inline-flex;
align-items: center;