Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -17,11 +17,16 @@ defineProps<Props>();
|
||||
|
||||
const params: any = inject("params");
|
||||
|
||||
const { y } = useWindowScroll({ behavior: "smooth" });
|
||||
|
||||
// computed
|
||||
|
||||
const page = computed({
|
||||
get: () => (params?.page ? Number(params.page) : 1),
|
||||
set: (value: number) => (params.page = value),
|
||||
set: (value: number) => {
|
||||
params.page = value;
|
||||
y.value = 0;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
size: number;
|
||||
link: string;
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
:to="link"
|
||||
target="_blank"
|
||||
class="w-full flex items-center cursor-pointer p-2 gap-2 rounded-100 bg-slate-100 border border-slate-300"
|
||||
>
|
||||
<div class="size-10 flex-center border border-slate-300 rounded-md">
|
||||
<Icon name="bi:pin-angle" class="**:fill-black" size="20" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm line-clamp-1">
|
||||
{{ `فایل ضمیمه ${index}` }}
|
||||
</span>
|
||||
<span class="text-xs">{{ (size / 1024).toFixed(2) }}KB</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetAccount from "~/composables/api/account/useGetAccount";
|
||||
import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
is_user: boolean;
|
||||
date: string;
|
||||
message: string;
|
||||
files: ServerFile[];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { is_user, files, date } = toRefs(props);
|
||||
|
||||
// state
|
||||
|
||||
const timeAgo = usePersianTimeAgo(new Date(date.value));
|
||||
|
||||
// queries
|
||||
|
||||
const { data: account } = useGetAccount();
|
||||
|
||||
// computed
|
||||
|
||||
const profile = computed(() => {
|
||||
return is_user.value
|
||||
? account.value?.profile_photo
|
||||
? account.value?.profile_photo
|
||||
: "/avatars/1.jpg"
|
||||
: "/avatars/3.jpg";
|
||||
});
|
||||
|
||||
const username = computed(() => {
|
||||
return is_user.value
|
||||
? `${account.value?.first_name} ${account.value?.last_name}`
|
||||
: "ادمین پشتیبانی هی ملز";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full flex flex-col justify-center gap-2"
|
||||
:class="is_user ? 'items-start' : 'items-end'"
|
||||
>
|
||||
<div
|
||||
class="w-full lg:w-1/2 bg-slate-50 border border-slate-200 rounded-xl p-5 flex items-start"
|
||||
:class="is_user ? 'rounded-br-none' : 'rounded-bl-none'"
|
||||
>
|
||||
<div class="w-2/12 flex items-start justify-start">
|
||||
<img :src="profile" class="size-16 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div class="w-10/12 flex flex-col items-start pt-2">
|
||||
<div class="flex flex-col gap-0.5 w-full">
|
||||
<p
|
||||
class="text-[10px] font-semibold text-[#1677FF] line-clamp-1 text-right"
|
||||
style="direction: ltr"
|
||||
>
|
||||
{{ timeAgo }}
|
||||
</p>
|
||||
<p
|
||||
class="text-xs font-semibold text-dynamic-secondary line-clamp-1"
|
||||
>
|
||||
{{ username }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="py-2 text-start">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-1/2 grid grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<Attachment
|
||||
v-for="(attachment, index) in files"
|
||||
:index="index + 1"
|
||||
:link="attachment.file_link"
|
||||
:size="attachment.size"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
is_user: boolean;
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full flex items-center"
|
||||
:class="is_user ? 'justify-start' : 'justify-end'"
|
||||
>
|
||||
<Skeleton
|
||||
class="!w-full lg:!w-1/2 !h-32 border border-slate-200 !rounded-xl p-5 flex items-start"
|
||||
:class="is_user ? '!rounded-br-none' : '!rounded-bl-none'"
|
||||
></Skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
|
||||
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
@@ -7,7 +11,13 @@ type Props = {
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { data } = toRefs(props);
|
||||
|
||||
// computed
|
||||
|
||||
const timeAgo = usePersianTimeAgo(new Date(data.value.created_at));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,11 +28,29 @@ defineProps<Props>();
|
||||
scope="row"
|
||||
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
|
||||
>
|
||||
{{ data.ticket_category }}
|
||||
{{ data.ticket_category ? data.ticket_category : "--" }}
|
||||
</td>
|
||||
<td class="w-3/12 px-6 py-6">
|
||||
{{ data.subject ? data.subject : "--" }}
|
||||
</td>
|
||||
<td class="w-3/12 px-6 py-6">
|
||||
{{ data.created_at ? timeAgo : "--" }}
|
||||
</td>
|
||||
<td class="w-2/12 px-6 py-6">
|
||||
<div
|
||||
class="w-max rounded-full py-1.5 px-3 text-xs border"
|
||||
:class="{
|
||||
'text-warning-600 bg-warning-100 border-warning-600':
|
||||
data.status == 'در انتظار پاسخ',
|
||||
'text-success-600 bg-success-100 border-success-600':
|
||||
data.status == 'پاسخ داده شده',
|
||||
'text-danger-600 bg-danger-100 border-danger-600':
|
||||
data.status == 'بسته شده',
|
||||
}"
|
||||
>
|
||||
{{ data.status ? data.status : "--" }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="w-3/12 px-6 py-6">{{ data.subject }}</td>
|
||||
<td class="w-3/12 px-6 py-6">{{ data.created_at }}</td>
|
||||
<td class="w-2/12 px-6 py-6">{{ data.status }}</td>
|
||||
<td class="w-1/12 px-6 py-6">
|
||||
<NuxtLink
|
||||
:to="{ name: 'profile-tickets-id', params: { id: data.id } }"
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
is_user: boolean;
|
||||
date: string;
|
||||
username: string;
|
||||
message: string;
|
||||
profile: string;
|
||||
files: string[];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full flex items-center"
|
||||
:class="is_user ? 'justify-start' : 'justify-end'"
|
||||
>
|
||||
<div
|
||||
class="w-full lg:w-1/2 bg-slate-50 border border-slate-200 rounded-xl p-5 flex items-start"
|
||||
:class="is_user ? 'rounded-br-none' : 'rounded-bl-none'"
|
||||
>
|
||||
<div class="w-2/12 flex items-start justify-start">
|
||||
<img :src="profile" class="size-16 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div class="w-10/12 flex flex-col items-start pt-8">
|
||||
<div class="flex flex-col gap-0.5 w-full">
|
||||
<p
|
||||
class="text-[10px] font-semibold text-[#1677FF] line-clamp-1 text-right"
|
||||
style="direction: ltr"
|
||||
>
|
||||
{{ date }}
|
||||
</p>
|
||||
<p
|
||||
class="text-xs font-semibold text-dynamic-secondary line-clamp-1"
|
||||
>
|
||||
{{ username }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="py-2 text-start">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,39 @@
|
||||
// imports
|
||||
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type CreateMessageRequest = {
|
||||
content: string;
|
||||
attachments: ServerFile[];
|
||||
ticket_id: number | string;
|
||||
};
|
||||
|
||||
const useCreateMessage = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleCreateMessage = async (params: CreateMessageRequest) => {
|
||||
const { data } = await axios.post(
|
||||
API_ENDPOINTS.tickets.create_message,
|
||||
{
|
||||
content: params.content,
|
||||
attachments: params.attachments.map((i) => i.id),
|
||||
ticket: params.ticket_id,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (messageData: CreateMessageRequest) =>
|
||||
handleCreateMessage(messageData),
|
||||
});
|
||||
};
|
||||
|
||||
export default useCreateMessage;
|
||||
@@ -7,7 +7,7 @@ import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
export type CreateTicketRequest = {
|
||||
ticket_category: string | undefined;
|
||||
order: number | undefined;
|
||||
order_id: number | undefined;
|
||||
subject: string;
|
||||
content: string;
|
||||
attachments: ServerFile[];
|
||||
@@ -21,23 +21,15 @@ const useCreateTicket = () => {
|
||||
// methods
|
||||
|
||||
const handleCreateTicket = async (params: CreateTicketRequest) => {
|
||||
const { data } = await axios.post(
|
||||
API_ENDPOINTS.tickets.create,
|
||||
{
|
||||
message: {
|
||||
content: params.content,
|
||||
attachments: params.attachments,
|
||||
},
|
||||
subject: params.subject,
|
||||
ticket_category: params.ticket_category,
|
||||
order: params.order,
|
||||
const { data } = await axios.post(API_ENDPOINTS.tickets.create, {
|
||||
message: {
|
||||
content: params.content,
|
||||
attachments: params.attachments.map((i) => i.id),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
subject: params.subject,
|
||||
ticket_category: params.ticket_category,
|
||||
order_id: params.order_id,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// composables/usePersianTimeAgo.ts
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { formatDistance, toDate } from "date-fns-jalali";
|
||||
import { faIR } from "date-fns-jalali/locale";
|
||||
|
||||
export function usePersianTimeAgo(date: Date) {
|
||||
const timeAgo = ref("");
|
||||
|
||||
const updateTimeAgo = () => {
|
||||
timeAgo.value = formatDistance(toDate(date), new Date(), {
|
||||
addSuffix: true,
|
||||
locale: faIR,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateTimeAgo();
|
||||
|
||||
const interval = setInterval(updateTimeAgo, 60000);
|
||||
|
||||
onUnmounted(() => clearInterval(interval));
|
||||
});
|
||||
|
||||
return timeAgo;
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export const API_ENDPOINTS = {
|
||||
upload_attachment: "/tickets/attachment/create",
|
||||
delete_attachment: "/tickets/attachment/delete",
|
||||
get_one: "/tickets",
|
||||
create_message: "/tickets/message/create",
|
||||
},
|
||||
orders: {
|
||||
get_all: "/order/list",
|
||||
|
||||
Generated
+14
@@ -18,9 +18,11 @@
|
||||
"@vueuse/nuxt": "^12.3.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.9",
|
||||
"date-fns-jalali": "^4.1.0-0",
|
||||
"fast-average-color": "^9.4.0",
|
||||
"gsap": "^3.12.5",
|
||||
"isomorphic-dompurify": "^2.21.0",
|
||||
"jalali-ts": "^8.0.0",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"nuxt": "^3.14.1592",
|
||||
"reka-ui": "^1.0.0-alpha.6",
|
||||
@@ -5860,6 +5862,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-jalali": {
|
||||
"version": "4.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/db0": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/db0/-/db0-0.2.1.tgz",
|
||||
@@ -7536,6 +7544,12 @@
|
||||
"integrity": "sha512-gE+YHWSbygYAoJa+Xg8LWxGILqFOxZSBQQw39ghel01fVFUxV7bjL0x1JFsHcLQ3uPjvn81HQMa+kxwyPWnxGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jalali-ts": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jalali-ts/-/jalali-ts-8.0.0.tgz",
|
||||
"integrity": "sha512-XZmEjaw56w47ZjJUnC/18juoJta4BcpKRE3cFZpw07+gy+nt3b9e+KGqlcRFph8Xn4LRtyx6l5QpEZftbtDZ3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
"@vueuse/nuxt": "^12.3.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.7.9",
|
||||
"date-fns-jalali": "^4.1.0-0",
|
||||
"fast-average-color": "^9.4.0",
|
||||
"gsap": "^3.12.5",
|
||||
"isomorphic-dompurify": "^2.21.0",
|
||||
"jalali-ts": "^8.0.0",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"nuxt": "^3.14.1592",
|
||||
"reka-ui": "^1.0.0-alpha.6",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useCreateMessage, {
|
||||
type CreateMessageRequest,
|
||||
} from "~/composables/api/tickets/useCreateMessage";
|
||||
import useGetTicket from "~/composables/api/tickets/useGetTicket";
|
||||
import useUploadAttachment from "~/composables/api/tickets/useUploadAttachment";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import useVuelidate from "@vuelidate/core";
|
||||
import { helpers, required, minLength } from "@vuelidate/validators";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
// meta
|
||||
|
||||
@@ -8,22 +18,140 @@ definePageMeta({
|
||||
layout: "profile",
|
||||
});
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const ticketData = ref({
|
||||
message: "",
|
||||
files: [],
|
||||
});
|
||||
|
||||
// computed
|
||||
|
||||
const ticketId = computed(() => route.params.id as string);
|
||||
|
||||
// state
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const messagesContainerRefEl = ref<HTMLElement | null>(null);
|
||||
|
||||
const { y: messagesContainerRefElY } = useScroll(messagesContainerRefEl, {
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
const messageData = ref<CreateMessageRequest>({
|
||||
content: "",
|
||||
attachments: [],
|
||||
ticket_id: ticketId.value,
|
||||
});
|
||||
|
||||
// computed
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
content: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد متن تیکت الزامی می باشد",
|
||||
required
|
||||
),
|
||||
minLength: helpers.withMessage(
|
||||
"فیلد متن تیکت حداقل ۵ کرکتر می باشد",
|
||||
minLength(5)
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const formValidator$ = useVuelidate(formRules, messageData);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: ticket, isLoading: ticketIsLoading } = useGetTicket(ticketId);
|
||||
|
||||
const { mutateAsync: uploadAttachment, isPending: uploadAttachmentIsPending } =
|
||||
useUploadAttachment();
|
||||
|
||||
const { mutateAsync: createMessage, isPending: createMessageIsPending } =
|
||||
useCreateMessage();
|
||||
|
||||
// methods
|
||||
|
||||
const handleAddNewMessageToMessages = async () => {
|
||||
await ticket.value?.messages.push({
|
||||
id: 0,
|
||||
content: messageData.value.content,
|
||||
is_user: true,
|
||||
attachments: messageData.value.attachments,
|
||||
created_at: Date.now().toString(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadAttachment = (file: File) => {
|
||||
uploadAttachment(
|
||||
{ file },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
messageData.value.attachments.push({ ...data });
|
||||
},
|
||||
onError: (error) => {
|
||||
addToast({
|
||||
message: error.message
|
||||
? error.message
|
||||
: "خطایی در آپلود پیوست رخ داد",
|
||||
options: {
|
||||
status: "error",
|
||||
description: "لطفا مجدد تلاش کنید",
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formValidator$.value.$validate();
|
||||
if (!formValidator$.value.$errors.length) {
|
||||
handleAddNewMessageToMessages().then(() => {
|
||||
setTimeout(() => {
|
||||
messagesContainerRefElY.value =
|
||||
messagesContainerRefEl.value?.scrollHeight!;
|
||||
}, 500);
|
||||
});
|
||||
createMessage(
|
||||
{ ...messageData.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEYS.ticket],
|
||||
});
|
||||
addToast({
|
||||
message: "پیام شما با موفقیت ثبت شد",
|
||||
options: {
|
||||
status: "success",
|
||||
description:
|
||||
"پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
|
||||
},
|
||||
});
|
||||
messageData.value = {
|
||||
content: "",
|
||||
attachments: [],
|
||||
ticket_id: ticketId.value,
|
||||
};
|
||||
formValidator$.value.$reset();
|
||||
},
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEYS.ticket],
|
||||
});
|
||||
addToast({
|
||||
message: "خطایی در ثبت پیام رخ داد",
|
||||
options: {
|
||||
status: "success",
|
||||
description: "لطفا مجدد تلاش کنید",
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,38 +162,55 @@ const { data: ticket, isLoading: ticketIsLoading } = useGetTicket(ticketId);
|
||||
class="flex flex-wrap items-center justify-between w-full py-4 border-b lg:px-5 border-slate-200"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-start w-1/2 gap-4 lg:gap-5 lg:w-full lg:items-center lg:flex-row"
|
||||
class="flex flex-col items-start w-1/2 gap-4 lg:gap-5 lg:w-full lg:items-center lg:justify-between lg:flex-row"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3 lg:border-e border-slate-200"
|
||||
>
|
||||
<p class="text-xs lg:text-sm text-dynamic-secondary">
|
||||
دسته‌بندی :
|
||||
</p>
|
||||
<p class="text-xs font-semibold lg:text-sm text-black">
|
||||
{{ ticket?.ticket_category }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3"
|
||||
>
|
||||
<p class="text-xs lg:text-sm text-dynamic-secondary">
|
||||
وضعیت :
|
||||
</p>
|
||||
<p
|
||||
class="text-xs font-semibold lg:text-sm text-black text-dynamic-secondary"
|
||||
<div class="flex items-center w-full gap-4">
|
||||
<div
|
||||
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3 lg:border-e border-slate-200"
|
||||
>
|
||||
{{ ticket?.status }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2 lg:hidden border-black">
|
||||
<p class="text-xs lg:text-sm text-dynamic-secondary">
|
||||
موضوع :
|
||||
</p>
|
||||
<p class="text-xs font-semibold lg:text-sm text-black">
|
||||
{{ ticket?.subject }}
|
||||
</p>
|
||||
<p class="text-xs lg:text-sm text-dynamic-secondary">
|
||||
دسته‌بندی :
|
||||
</p>
|
||||
<Skeleton
|
||||
v-if="ticketIsLoading"
|
||||
class="!w-1/2 !h-5 !rounded-sm"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
class="text-xs font-semibold lg:text-sm text-black"
|
||||
>
|
||||
{{ ticket?.ticket_category }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3"
|
||||
>
|
||||
<p class="text-xs lg:text-sm text-dynamic-secondary">
|
||||
وضعیت :
|
||||
</p>
|
||||
<Skeleton
|
||||
v-if="ticketIsLoading"
|
||||
class="!w-1/2 !h-5 !rounded-sm"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
class="text-xs font-semibold lg:text-sm text-black text-dynamic-secondary"
|
||||
>
|
||||
{{ ticket?.status }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col w-full gap-2 lg:hidden border-black"
|
||||
>
|
||||
<p class="text-xs lg:text-sm text-dynamic-secondary">
|
||||
موضوع :
|
||||
</p>
|
||||
<p class="text-xs font-semibold lg:text-sm text-black">
|
||||
{{ ticket?.subject }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="items-center justify-end hidden w-1/4 lg:flex">
|
||||
<NuxtLink :to="{ name: 'profile-tickets' }">
|
||||
<Button
|
||||
@@ -81,46 +226,86 @@ const { data: ticket, isLoading: ticketIsLoading } = useGetTicket(ticketId);
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden w-full text-sm px-5 pb-8 mt-3 border-b lg:block text-md border-slate-200"
|
||||
class="hidden w-full text-sm px-5 pb-6 mt-1 border-b lg:block text-md border-slate-200"
|
||||
>
|
||||
<span class="text-black/50"> موضوع : </span>
|
||||
{{ ticket?.subject }}
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-5 h-[32rem] overflow-y-auto">
|
||||
<!-- <TicketBubble
|
||||
v-for="(message, index) in messages"
|
||||
:key="index"
|
||||
:is_user="message.is_user"
|
||||
:date="message.date"
|
||||
:message="message.message"
|
||||
:profile="message.profile"
|
||||
:username="message.username"
|
||||
:files="message.files"
|
||||
/> -->
|
||||
<Skeleton v-if="ticketIsLoading" class="!w-1/2 !h-5 !rounded-sm" />
|
||||
<span v-else>
|
||||
{{ ticket?.subject }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-2 gap-5 mt-5 pt-5 border-t border-slate-200"
|
||||
ref="messagesContainerRefEl"
|
||||
class="w-full flex flex-col gap-5 h-[32rem] overflow-y-auto"
|
||||
>
|
||||
<DataField id="message" :required="true" label="متن پیام">
|
||||
<textarea
|
||||
v-model="ticketData.message"
|
||||
class="w-full bg-white border-[1.5px] border-slate-200 rounded-xl text-xs lg:text-sm p-5 text-black h-[10rem] lg:h-[20rem] transition resize-none outline-none focus:!border-black"
|
||||
placeholder="متن تیکت را اینجا بنویسید ..."
|
||||
<template v-if="ticketIsLoading">
|
||||
<TicketBubbleLoading
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
:is_user="i % 2 != 0 ? true : false"
|
||||
/>
|
||||
</DataField>
|
||||
<FileInput v-model="ticketData.files" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<TicketBubble
|
||||
v-for="(message, index) in ticket?.messages"
|
||||
:key="index"
|
||||
:is_user="message.is_user"
|
||||
:date="message.created_at"
|
||||
:message="message.content"
|
||||
:files="message.attachments"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex-center py-5">
|
||||
<Button class="rounded-full px-20" end-icon="bi:send" size="md">
|
||||
<!-- <Icon
|
||||
v-if="createAddressIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/> -->
|
||||
<span>ارسال پیام</span>
|
||||
</Button>
|
||||
<div
|
||||
v-if="ticket?.status !== 'بسته شده' && !ticketIsLoading"
|
||||
class="w-full flex flex-col gap-5"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-2 gap-5 pt-5 border-t border-slate-200"
|
||||
>
|
||||
<DataField
|
||||
id="message"
|
||||
:required="true"
|
||||
label="متن پیام"
|
||||
:error="formValidator$.content"
|
||||
>
|
||||
<Textarea
|
||||
v-model="messageData.content"
|
||||
:error="formValidator$.content.$error"
|
||||
class="h-[10rem] lg:h-[20rem]"
|
||||
variant="outlined"
|
||||
placeholder="متن پیام را اینجا بنویسید ..."
|
||||
/>
|
||||
</DataField>
|
||||
<FileInput
|
||||
v-model="messageData.attachments"
|
||||
@change="handleUploadAttachment"
|
||||
:loading="uploadAttachmentIsPending"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex-center py-5">
|
||||
<Button
|
||||
@click="handleSubmit"
|
||||
:loading="
|
||||
createMessageIsPending || uploadAttachmentIsPending
|
||||
"
|
||||
class="rounded-full w-[14rem] h-11"
|
||||
size="md"
|
||||
>
|
||||
<Icon
|
||||
v-if="createMessageIsPending"
|
||||
:name="
|
||||
createMessageIsPending
|
||||
? 'svg-spinners:3-dots-bounce'
|
||||
: 'bi:send'
|
||||
"
|
||||
/>
|
||||
<span v-else>ارسال پیام</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ import useUploadAttachment from "~/composables/api/tickets/useUploadAttachment";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
import useVuelidate from "@vuelidate/core";
|
||||
import { helpers, required, minLength, email } from "@vuelidate/validators";
|
||||
import { helpers, required, minLength } from "@vuelidate/validators";
|
||||
|
||||
// meta
|
||||
|
||||
@@ -66,7 +66,7 @@ const ticketCategories: TicketCategory[] = [
|
||||
|
||||
const ticketData = ref<CreateTicketRequest>({
|
||||
ticket_category: undefined,
|
||||
order: undefined,
|
||||
order_id: undefined,
|
||||
subject: "",
|
||||
content: "",
|
||||
attachments: [],
|
||||
@@ -234,21 +234,21 @@ const handleSubmit = async () => {
|
||||
<Select
|
||||
placeholder="انتخاب کنید"
|
||||
variant="outlined"
|
||||
v-model="ticketData.order"
|
||||
v-model="ticketData.order_id"
|
||||
:loading="ordersIsLoading"
|
||||
>
|
||||
<template #trigger>
|
||||
<SelectValue
|
||||
:class="
|
||||
ticketData.order
|
||||
ticketData.order_id
|
||||
? 'text-black'
|
||||
: 'text-slate-400'
|
||||
"
|
||||
class="font-iran-yekan-x text-sm text-start placeholder-slate-400"
|
||||
>
|
||||
{{
|
||||
ticketData.order
|
||||
? `شماره سفارش : ${ticketData.order}`
|
||||
ticketData.order_id
|
||||
? `شماره سفارش : ${ticketData.order_id}`
|
||||
: "وارد نشده"
|
||||
}}
|
||||
</SelectValue>
|
||||
@@ -338,6 +338,7 @@ const handleSubmit = async () => {
|
||||
<div class="w-full flex-center py-5">
|
||||
<Button
|
||||
@click="handleSubmit"
|
||||
:loading="createTicketIsPending || uploadAttachmentIsPending"
|
||||
class="rounded-full w-[14rem] h-11"
|
||||
size="md"
|
||||
>
|
||||
|
||||
Vendored
+2
-1
@@ -189,6 +189,7 @@ declare global {
|
||||
|
||||
type TicketMessage = {
|
||||
id: number;
|
||||
is_user: boolean;
|
||||
content: string;
|
||||
created_at: string;
|
||||
attachments: ServerFile[];
|
||||
@@ -199,7 +200,7 @@ declare global {
|
||||
messages: TicketMessage[];
|
||||
subject: string;
|
||||
ticket_category: string;
|
||||
status: string;
|
||||
status: "در انتظار پاسخ" | "پاسخ داده شده" | "بسته شده";
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
order: Order;
|
||||
|
||||
Reference in New Issue
Block a user