Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -105,7 +105,7 @@ const handleDeleteAddress = (id: number) => {
|
||||
>
|
||||
<div class="w-full lg:w-9/12 overflow-hidden">
|
||||
<div
|
||||
class="w-full overflow-hidden overflow-ellipsis gap-5 text-start whitespace-pre text-sm lg:text-[1rem] text-slate-700"
|
||||
class="w-full overflow-hidden overflow-ellipsis gap-5 text-start whitespace-pre text-sm text-slate-700"
|
||||
>
|
||||
{{
|
||||
!!address
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
max: number;
|
||||
size: string;
|
||||
items: string[];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { items, max } = toRefs(props);
|
||||
|
||||
// computed
|
||||
|
||||
const displayedAvatars = computed(() => items.value.slice(0, max.value));
|
||||
|
||||
const remaining = computed(() => items.value.length - max.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
v-if="remaining > 0"
|
||||
class="flex-col-center bg-black text-white font-semibold rounded-full text-[10px] z-[1]"
|
||||
:style="{ width: size, height: size }"
|
||||
>
|
||||
+{{ remaining }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in displayedAvatars"
|
||||
:key="index"
|
||||
class="relative bg-gray-400 border border-black rounded-full -ms-2"
|
||||
:class="index < 0 ? '' : ''"
|
||||
:style="{ width: size, height: size, zIndex: index + 2 }"
|
||||
>
|
||||
<img
|
||||
:src="item"
|
||||
alt="avatar"
|
||||
class="rounded-full object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,13 +6,7 @@ import { useToast } from "~/composables/global/useToast";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
modelValue: {
|
||||
id: number;
|
||||
file_link: string;
|
||||
date: string;
|
||||
size: number;
|
||||
name: string;
|
||||
}[];
|
||||
modelValue: ServerFile[];
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ const page = computed({
|
||||
v-model:page="page"
|
||||
>
|
||||
<PaginationList v-slot="{ items }" class="flex items-center gap-2">
|
||||
<PaginationLast
|
||||
class="px-2 h-9 font-light whitespace-nowrap flex items-center justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
<PaginationFirst
|
||||
class="px-2 h-9 font-light flex items-center whitespace-nowrap justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
>
|
||||
برو آخر
|
||||
</PaginationLast>
|
||||
برو اول
|
||||
</PaginationFirst>
|
||||
<PaginationNext
|
||||
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
>
|
||||
@@ -73,11 +73,12 @@ const page = computed({
|
||||
>
|
||||
<Icon name="ci:chevron-left" class="**:fill-back" size="18px" />
|
||||
</PaginationPrev>
|
||||
<PaginationFirst
|
||||
class="px-2 h-9 font-light flex items-center whitespace-nowrap justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
|
||||
<PaginationLast
|
||||
class="px-2 h-9 font-light whitespace-nowrap flex items-center justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
|
||||
>
|
||||
برو اول
|
||||
</PaginationFirst>
|
||||
برو آخر
|
||||
</PaginationLast>
|
||||
</PaginationList>
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,7 @@ type Props = {
|
||||
options?: string[];
|
||||
placeholder?: string;
|
||||
triggerRootClass?: string;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
@@ -21,6 +22,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
variant: "solid",
|
||||
disabled: false,
|
||||
placeholder: "وارد نشده",
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { modelValue, variant, error, triggerRootClass } = toRefs(props);
|
||||
@@ -53,15 +55,25 @@ const classes = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-model="selectedValue" dir="rtl">
|
||||
<SelectRoot
|
||||
v-model="selectedValue"
|
||||
dir="rtl"
|
||||
:disabled="disabled || loading"
|
||||
>
|
||||
<SelectTrigger :class="classes">
|
||||
<slot v-if="!!$slots.trigger" name="trigger" />
|
||||
|
||||
<SelectValue
|
||||
v-else
|
||||
:placeholder="placeholder"
|
||||
v-bind="$attrs"
|
||||
:class="selectedValue ? 'text-black' : 'text-slate-400'"
|
||||
:class="selectedValue ? '!text-black' : 'text-slate-400'"
|
||||
class="font-iran-yekan-x text-sm text-start placeholder-slate-400"
|
||||
/>
|
||||
<Icon name="bi:chevron-down" size="16" />
|
||||
<Icon
|
||||
:name="loading ? 'svg-spinners:3-dots-fade' : 'bi:chevron-down'"
|
||||
size="16"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectPortal>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
variant?: "solid" | "outlined";
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
message?: string;
|
||||
placeholder?: string;
|
||||
modelValue: string;
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: "solid",
|
||||
disabled: false,
|
||||
placeholder: "وارد نشده",
|
||||
});
|
||||
const { variant, message, error, disabled, modelValue } = toRefs(props);
|
||||
|
||||
// emits
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// state
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// computed
|
||||
|
||||
const value = computed({
|
||||
get: () => modelValue.value ?? "",
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const classes = computed(() => {
|
||||
return [
|
||||
"flex items-start text-black justify-between cursor-text resize-none transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 transition-all !text-sm",
|
||||
{
|
||||
"input-solid": variant.value === "solid",
|
||||
"input-outlined": variant.value === "outlined",
|
||||
"input-effects": !error.value,
|
||||
[variant.value === "solid"
|
||||
? "input-solid-error"
|
||||
: "input-outlined-error"]: error.value,
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
v-model="value"
|
||||
ref="inputRef"
|
||||
v-bind="$attrs"
|
||||
:class="classes"
|
||||
class="size-full outline-none placeholder-slate-400"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetOrdersListResponse = Order[];
|
||||
|
||||
const useGetOrdersList = () => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetOrdersList = async () => {
|
||||
const { data } = await axios.get<GetOrdersListResponse>(
|
||||
API_ENDPOINTS.orders.get_all
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.tickets],
|
||||
queryFn: () => handleGetOrdersList(),
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetOrdersList;
|
||||
@@ -10,13 +10,7 @@ export type CreateTicketRequest = {
|
||||
order: number | undefined;
|
||||
subject: string;
|
||||
content: string;
|
||||
attachments: {
|
||||
id: number;
|
||||
file_link: string;
|
||||
date: string;
|
||||
size: number;
|
||||
name: string;
|
||||
}[];
|
||||
attachments: ServerFile[];
|
||||
};
|
||||
|
||||
const useCreateTicket = () => {
|
||||
@@ -28,9 +22,15 @@ const useCreateTicket = () => {
|
||||
|
||||
const handleCreateTicket = async (params: CreateTicketRequest) => {
|
||||
const { data } = await axios.post(
|
||||
API_ENDPOINTS.account.address.update,
|
||||
API_ENDPOINTS.tickets.create,
|
||||
{
|
||||
...params,
|
||||
message: {
|
||||
content: params.content,
|
||||
attachments: params.attachments,
|
||||
},
|
||||
subject: params.subject,
|
||||
ticket_category: params.ticket_category,
|
||||
order: params.order,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -5,11 +5,11 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetAllTicketsResponse = ApiPaginated<Ticket>;
|
||||
export type GetAllTicketsResponse = ApiPaginated<Omit<Ticket, "messages">>;
|
||||
|
||||
export type GetAllTicketsFilters = {
|
||||
sort: string | undefined;
|
||||
filter: string | undefined;
|
||||
status: string | undefined;
|
||||
page: string | string[];
|
||||
};
|
||||
|
||||
@@ -26,9 +26,9 @@ const useGetAllTickets = (params: Ref<GetAllTicketsFilters>) => {
|
||||
{
|
||||
params: {
|
||||
sort: params.sort,
|
||||
filter: params.filter,
|
||||
offset: Number(params.page) * 10 - 10,
|
||||
limit: 10,
|
||||
filter: params.status,
|
||||
offset: Number(params.page) * 7 - 7,
|
||||
limit: 7,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// imports
|
||||
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type GetTicketResponse = Ticket;
|
||||
|
||||
const useGetTicket = (id: ComputedRef<number | string>) => {
|
||||
// state
|
||||
|
||||
const { $axios: axios } = useNuxtApp();
|
||||
|
||||
// methods
|
||||
|
||||
const handleGetTicket = async (id: string | number | undefined) => {
|
||||
const { data } = await axios.get<GetTicketResponse>(
|
||||
`${API_ENDPOINTS.tickets.get_one}/${id}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEYS.ticket, id],
|
||||
queryFn: () => handleGetTicket(id.value),
|
||||
});
|
||||
};
|
||||
|
||||
export default useGetTicket;
|
||||
@@ -39,6 +39,10 @@ export const API_ENDPOINTS = {
|
||||
create: "/tickets/create",
|
||||
upload_attachment: "/tickets/attachment/create",
|
||||
delete_attachment: "/tickets/attachment/delete",
|
||||
get_one: "/tickets",
|
||||
},
|
||||
orders: {
|
||||
get_all: "/order/list",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -54,6 +58,8 @@ export const QUERY_KEYS = {
|
||||
categories: "categories",
|
||||
addresses: "addresses",
|
||||
tickets: "tickets",
|
||||
ticket: "ticket",
|
||||
orders: "orders",
|
||||
};
|
||||
|
||||
export const MUTATION_KEYS = {
|
||||
|
||||
@@ -128,7 +128,7 @@ watch(
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="products!.length > 10" class="w-full flex-center py-10">
|
||||
<div v-if="data?.count > 10" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import useGetTicket from "~/composables/api/tickets/useGetTicket";
|
||||
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
@@ -15,74 +17,13 @@ const ticketData = ref({
|
||||
files: [],
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{
|
||||
is_user: true,
|
||||
date: "2023-10-15T14:30:00Z",
|
||||
username: "JohnDoe",
|
||||
message: "Hey, did you get a chance to review the design files I sent?",
|
||||
profile: "https://example.com/profiles/johndoe.jpg",
|
||||
files: ["design-draft-1.pdf", "design-draft-2.pdf"],
|
||||
},
|
||||
{
|
||||
is_user: false,
|
||||
date: "2023-10-15T14:35:00Z",
|
||||
username: "JaneSmith",
|
||||
message:
|
||||
"Yes, I just finished reviewing them. The second draft looks great!",
|
||||
profile: "https://example.com/profiles/janesmith.jpg",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
is_user: true,
|
||||
date: "2023-10-15T14:40:00Z",
|
||||
username: "JohnDoe",
|
||||
message:
|
||||
"Awesome! Let me know if you need any changes before we finalize it.",
|
||||
profile: "https://example.com/profiles/johndoe.jpg",
|
||||
files: ["final-design-notes.txt"],
|
||||
},
|
||||
{
|
||||
is_user: false,
|
||||
date: "2023-10-15T14:35:00Z",
|
||||
username: "JaneSmith",
|
||||
message:
|
||||
"Yes, I just finished reviewing them. The second draft looks great!",
|
||||
profile: "https://example.com/profiles/janesmith.jpg",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
is_user: true,
|
||||
date: "2023-10-15T14:40:00Z",
|
||||
username: "JohnDoe",
|
||||
message:
|
||||
"Awesome! Let me know if you need any changes before we finalize it.",
|
||||
profile: "https://example.com/profiles/johndoe.jpg",
|
||||
files: ["final-design-notes.txt"],
|
||||
},
|
||||
{
|
||||
is_user: false,
|
||||
date: "2023-10-15T14:35:00Z",
|
||||
username: "JaneSmith",
|
||||
message:
|
||||
"Yes, I just finished reviewing them. The second draft looks great!",
|
||||
profile: "https://example.com/profiles/janesmith.jpg",
|
||||
files: [],
|
||||
},
|
||||
{
|
||||
is_user: true,
|
||||
date: "2023-10-15T14:40:00Z",
|
||||
username: "JohnDoe",
|
||||
message:
|
||||
"Awesome! Let me know if you need any changes before we finalize it.",
|
||||
profile: "https://example.com/profiles/johndoe.jpg",
|
||||
files: ["final-design-notes.txt"],
|
||||
},
|
||||
];
|
||||
|
||||
// computed
|
||||
|
||||
const ticketId = computed(() => route.params.id);
|
||||
const ticketId = computed(() => route.params.id as string);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: ticket, isLoading: ticketIsLoading } = useGetTicket(ticketId);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -95,16 +36,6 @@ const ticketId = computed(() => route.params.id);
|
||||
<div
|
||||
class="flex flex-col items-start w-1/2 gap-4 lg:gap-5 lg:w-full lg:items-center 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">
|
||||
634932
|
||||
</p>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
@@ -112,7 +43,7 @@ const ticketId = computed(() => route.params.id);
|
||||
دسته‌بندی :
|
||||
</p>
|
||||
<p class="text-xs font-semibold lg:text-sm text-black">
|
||||
گیفت‌کارت
|
||||
{{ ticket?.ticket_category }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
@@ -124,7 +55,7 @@ const ticketId = computed(() => route.params.id);
|
||||
<p
|
||||
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">
|
||||
@@ -132,8 +63,7 @@ const ticketId = computed(() => route.params.id);
|
||||
موضوع :
|
||||
</p>
|
||||
<p class="text-xs font-semibold lg:text-sm text-black">
|
||||
نمی‌دانم کد ارسال گیفت کارت خریداری شده را از کجا
|
||||
باید ببینم
|
||||
{{ ticket?.subject }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="items-center justify-end hidden w-1/4 lg:flex">
|
||||
@@ -153,12 +83,12 @@ const ticketId = computed(() => route.params.id);
|
||||
<div
|
||||
class="hidden w-full text-sm px-5 pb-8 mt-3 border-b lg:block text-md border-slate-200"
|
||||
>
|
||||
<span class="text-black/50"> موضوع : </span> نمی‌دانم کد ارسال
|
||||
گیفت کارت خریداری شده را از کجا باید ببینم
|
||||
<span class="text-black/50"> موضوع : </span>
|
||||
{{ ticket?.subject }}
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-5 h-[32rem] overflow-y-auto">
|
||||
<TicketBubble
|
||||
<!-- <TicketBubble
|
||||
v-for="(message, index) in messages"
|
||||
:key="index"
|
||||
:is_user="message.is_user"
|
||||
@@ -167,7 +97,7 @@ const ticketId = computed(() => route.params.id);
|
||||
:profile="message.profile"
|
||||
:username="message.username"
|
||||
:files="message.files"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -14,12 +14,14 @@ definePageMeta({
|
||||
|
||||
// state
|
||||
|
||||
const params = useUrlSearchParams();
|
||||
const params: GetAllTicketsFilters = useUrlSearchParams("history");
|
||||
|
||||
const filters = ref<GetAllTicketsFilters>({
|
||||
sort: undefined,
|
||||
filter: undefined,
|
||||
page: params.page ?? 1,
|
||||
const filters = computed(() => {
|
||||
return {
|
||||
sort: params.sort ?? "created_at",
|
||||
status: params.status ?? "",
|
||||
page: params.page ?? 1,
|
||||
};
|
||||
});
|
||||
|
||||
const tableHeads = ref([
|
||||
@@ -30,9 +32,39 @@ const tableHeads = ref([
|
||||
"عملیات",
|
||||
]);
|
||||
|
||||
const sortFilters = ref([
|
||||
{
|
||||
title: "جدید ترین",
|
||||
value: "created_at",
|
||||
},
|
||||
{
|
||||
title: "قدیمی ترین",
|
||||
value: "-created_at",
|
||||
},
|
||||
]);
|
||||
|
||||
const statusFilters = ref([
|
||||
{
|
||||
title: "پاسخ داده شده",
|
||||
value: "resolved",
|
||||
},
|
||||
{
|
||||
title: "در حال پردازش",
|
||||
value: "in_progress",
|
||||
},
|
||||
{
|
||||
title: "بسته شده",
|
||||
value: "closed",
|
||||
},
|
||||
]);
|
||||
|
||||
// provide / inject
|
||||
|
||||
provide("params", params);
|
||||
|
||||
// queries
|
||||
|
||||
const { data, isLoading: ticketsIsLoading } = useGetAllTickets(filters);
|
||||
const { data, isPending: ticketsIsPending } = useGetAllTickets(filters);
|
||||
|
||||
// computed
|
||||
|
||||
@@ -40,10 +72,10 @@ const tickets = computed(() => {
|
||||
return data.value?.results.flat();
|
||||
});
|
||||
|
||||
const hasTickets = computed(() => tickets.value?.length > 0);
|
||||
const hasTickets = computed(() => data.value?.count > 0);
|
||||
|
||||
const paginationData = computed(() => {
|
||||
return tickets!.value?.results.map((_, i: number) => {
|
||||
return data.value?.results.map((_, i: number) => {
|
||||
return { type: "page", value: i };
|
||||
});
|
||||
});
|
||||
@@ -56,33 +88,67 @@ const paginationData = computed(() => {
|
||||
<div class="w-full flex flex-col gap-5">
|
||||
<div class="w-full flex items-center justify-between px-5">
|
||||
<div class="flex items-center justify-start gap-8">
|
||||
<div
|
||||
v-if="hasTickets"
|
||||
class="flex items-center justify-start gap-3"
|
||||
>
|
||||
<div class="flex items-center justify-start gap-3">
|
||||
<span class="text-sm">ترتیب بر اساس</span>
|
||||
<Select
|
||||
:options="['جدید ترین', 'قدیمی ترین']"
|
||||
v-model="filters.sort!"
|
||||
v-model="params.sort!"
|
||||
triggerRootClass="!py-2.5"
|
||||
class="w-[5rem]"
|
||||
/>
|
||||
class="w-[6rem]"
|
||||
>
|
||||
<template #content>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="(category, index) in sortFilters"
|
||||
:key="index"
|
||||
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
|
||||
:value="category.value"
|
||||
>
|
||||
<SelectItemIndicator
|
||||
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
|
||||
>
|
||||
<Icon name="bi:check" size="20" />
|
||||
</SelectItemIndicator>
|
||||
<SelectItemText
|
||||
class="text-end font-iran-yekan-x text-sm"
|
||||
>
|
||||
{{ category.title }}
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasTickets"
|
||||
class="flex items-center justify-start gap-3"
|
||||
>
|
||||
<span class="text-sm">وضعیت پرداخت</span>
|
||||
<div class="flex items-center justify-start gap-3">
|
||||
<span class="text-sm">وضعیت</span>
|
||||
<Select
|
||||
:options="[
|
||||
'پرداخت شده',
|
||||
'در حال پردازش',
|
||||
'لغو شده',
|
||||
]"
|
||||
v-model="filters.filter!"
|
||||
v-model="params.status!"
|
||||
triggerRootClass="!py-2.5"
|
||||
class="w-[5rem]"
|
||||
/>
|
||||
class="w-[6rem]"
|
||||
>
|
||||
<template #content>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="(
|
||||
category, index
|
||||
) in statusFilters"
|
||||
:key="index"
|
||||
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
|
||||
:value="category.value"
|
||||
>
|
||||
<SelectItemIndicator
|
||||
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
|
||||
>
|
||||
<Icon name="bi:check" size="20" />
|
||||
</SelectItemIndicator>
|
||||
<SelectItemText
|
||||
class="text-end font-iran-yekan-x text-sm"
|
||||
>
|
||||
{{ category.title }}
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +162,7 @@ const paginationData = computed(() => {
|
||||
</div>
|
||||
|
||||
<Placeholder
|
||||
v-if="!hasTickets && !ticketsIsLoading"
|
||||
v-if="!hasTickets && !ticketsIsPending"
|
||||
class="!w-full !py-[5rem]"
|
||||
icon="bi:ticket"
|
||||
title="تیکتی یافت نشد"
|
||||
@@ -121,7 +187,7 @@ const paginationData = computed(() => {
|
||||
</th>
|
||||
</template>
|
||||
<template #tbody>
|
||||
<template v-if="ticketsIsLoading">
|
||||
<template v-if="ticketsIsPending">
|
||||
<TicketsTableRowLoading v-for="i in 5" />
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -134,7 +200,7 @@ const paginationData = computed(() => {
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div v-if="tickets?.length > 10" class="w-full flex-center py-10">
|
||||
<div v-if="data?.count > 7" class="w-full flex-center py-10">
|
||||
<Pagination :items="paginationData" :total="data?.count" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetOrdersList from "~/composables/api/orders/useGetOrdersList";
|
||||
import useCreateTicket, {
|
||||
type CreateTicketRequest,
|
||||
} from "~/composables/api/tickets/useCreateTicket";
|
||||
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";
|
||||
|
||||
// meta
|
||||
|
||||
@@ -71,12 +74,49 @@ const ticketData = ref<CreateTicketRequest>({
|
||||
|
||||
// queries
|
||||
|
||||
const { data: orders, isLoading: ordersIsLoading } = useGetOrdersList();
|
||||
|
||||
const { mutateAsync: createTicket, isPending: createTicketIsPending } =
|
||||
useCreateTicket();
|
||||
|
||||
const { mutate: uploadAttachment, isPending: uploadAttachmentIsPending } =
|
||||
const { mutateAsync: uploadAttachment, isPending: uploadAttachmentIsPending } =
|
||||
useUploadAttachment();
|
||||
|
||||
// computed
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
ticket_category: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد دسته بندی الزامی می باشد",
|
||||
required
|
||||
),
|
||||
},
|
||||
subject: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد عنوان تیکت الزامی می باشد",
|
||||
required
|
||||
),
|
||||
minLength: helpers.withMessage(
|
||||
"فیلد عنوان تیکت حداقل ۵ کرکتر می باشد",
|
||||
minLength(5)
|
||||
),
|
||||
},
|
||||
content: {
|
||||
required: helpers.withMessage(
|
||||
"فیلد متن تیکت الزامی می باشد",
|
||||
required
|
||||
),
|
||||
minLength: helpers.withMessage(
|
||||
"فیلد متن تیکت حداقل ۵ کرکتر می باشد",
|
||||
minLength(5)
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const formValidator$ = useVuelidate(formRules, ticketData);
|
||||
|
||||
// methods
|
||||
|
||||
const handleUploadAttachment = (file: File) => {
|
||||
@@ -101,35 +141,38 @@ const handleUploadAttachment = (file: File) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
createTicket(
|
||||
{ ...ticketData.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.push({ name: "profile-tickets" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEYS.tickets],
|
||||
});
|
||||
addToast({
|
||||
message: "تیکت شما با موفقیت ثبت شد",
|
||||
options: {
|
||||
status: "success",
|
||||
description:
|
||||
"پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در ثبت تیکت رخ داد",
|
||||
options: {
|
||||
status: "success",
|
||||
description: "لطفا مجدد تلاش کنید",
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
const handleSubmit = async () => {
|
||||
await formValidator$.value.$validate();
|
||||
if (!formValidator$.value.$errors.length) {
|
||||
createTicket(
|
||||
{ ...ticketData.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
router.push({ name: "profile-tickets" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEYS.tickets],
|
||||
});
|
||||
addToast({
|
||||
message: "تیکت شما با موفقیت ثبت شد",
|
||||
options: {
|
||||
status: "success",
|
||||
description:
|
||||
"پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
addToast({
|
||||
message: "خطایی در ثبت تیکت رخ داد",
|
||||
options: {
|
||||
status: "success",
|
||||
description: "لطفا مجدد تلاش کنید",
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -150,10 +193,16 @@ const handleSubmit = () => {
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<DataField id="category" :required="true" label="دسته بندی">
|
||||
<DataField
|
||||
id="category"
|
||||
:required="true"
|
||||
label="دسته بندی"
|
||||
:error="formValidator$.ticket_category"
|
||||
>
|
||||
<Select
|
||||
placeholder="انتخاب کنید"
|
||||
variant="outlined"
|
||||
:error="formValidator$.ticket_category.$error"
|
||||
v-model="ticketData.ticket_category"
|
||||
>
|
||||
<template #content>
|
||||
@@ -186,26 +235,65 @@ const handleSubmit = () => {
|
||||
placeholder="انتخاب کنید"
|
||||
variant="outlined"
|
||||
v-model="ticketData.order"
|
||||
:loading="ordersIsLoading"
|
||||
>
|
||||
<template #trigger>
|
||||
<SelectValue
|
||||
:class="
|
||||
ticketData.order
|
||||
? 'text-black'
|
||||
: 'text-slate-400'
|
||||
"
|
||||
class="font-iran-yekan-x text-sm text-start placeholder-slate-400"
|
||||
>
|
||||
{{
|
||||
ticketData.order
|
||||
? `شماره سفارش : ${ticketData.order}`
|
||||
: "وارد نشده"
|
||||
}}
|
||||
</SelectValue>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="(
|
||||
category, index
|
||||
) in ticketCategories"
|
||||
v-for="(order, index) in orders"
|
||||
:key="index"
|
||||
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
|
||||
:value="category.value"
|
||||
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] px-[12px] shrink-0 relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
|
||||
:value="order.id"
|
||||
>
|
||||
<SelectItemIndicator
|
||||
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
|
||||
>
|
||||
<Icon name="bi:check" size="20" />
|
||||
</SelectItemIndicator>
|
||||
<SelectItemText
|
||||
class="text-end font-iran-yekan-x text-sm"
|
||||
class="w-full text-end font-iran-yekan-x text-sm flex items-center justify-between"
|
||||
>
|
||||
{{ category.title }}
|
||||
<div class="flex items-center gap-4">
|
||||
<AvatarGroup
|
||||
:items="[
|
||||
'https://c262408.parspack.net/media/profile_photos/Jackie_Robinson_NPG_97_135.jpg?AWSAccessKeyId=mtiSN2JWjWgyfr2u&Signature=mlUzygzyg2gQD7B5STTlgM2N%2FUM%3D&Expires=1740517316',
|
||||
'https://c262408.parspack.net/media/profile_photos/Jackie_Robinson_NPG_97_135.jpg?AWSAccessKeyId=mtiSN2JWjWgyfr2u&Signature=mlUzygzyg2gQD7B5STTlgM2N%2FUM%3D&Expires=1740517316',
|
||||
'https://c262408.parspack.net/media/profile_photos/Jackie_Robinson_NPG_97_135.jpg?AWSAccessKeyId=mtiSN2JWjWgyfr2u&Signature=mlUzygzyg2gQD7B5STTlgM2N%2FUM%3D&Expires=1740517316',
|
||||
]"
|
||||
:max="2"
|
||||
size="32px"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-start gap-1 text-[10px]"
|
||||
>
|
||||
<span
|
||||
>{{
|
||||
order.count
|
||||
}}
|
||||
محصول</span
|
||||
>
|
||||
<span>
|
||||
شماره سفارش : {{ order.id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ order.status }}
|
||||
</span>
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
@@ -217,17 +305,26 @@ const handleSubmit = () => {
|
||||
:required="true"
|
||||
label="عنوان تیکت"
|
||||
class="col-span-full"
|
||||
:error="formValidator$.subject"
|
||||
>
|
||||
<Input
|
||||
v-model="ticketData.subject"
|
||||
placeholder="عنوان تیکت را اینجا بنویسید ..."
|
||||
variant="outlined"
|
||||
:error="formValidator$.subject.$error"
|
||||
/>
|
||||
</DataField>
|
||||
<DataField id="message" :required="true" label="متن تیکت">
|
||||
<textarea
|
||||
<DataField
|
||||
id="message"
|
||||
:required="true"
|
||||
label="متن تیکت"
|
||||
:error="formValidator$.content"
|
||||
>
|
||||
<Textarea
|
||||
v-model="ticketData.content"
|
||||
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"
|
||||
:error="formValidator$.content.$error"
|
||||
class="h-[10rem] lg:h-[20rem]"
|
||||
variant="outlined"
|
||||
placeholder="متن تیکت را اینجا بنویسید ..."
|
||||
/>
|
||||
</DataField>
|
||||
@@ -239,10 +336,18 @@ const handleSubmit = () => {
|
||||
</div>
|
||||
</ProfileSection>
|
||||
<div class="w-full flex-center py-5">
|
||||
<Button class="rounded-full px-20" end-icon="bi:send" size="md">
|
||||
<Button
|
||||
@click="handleSubmit"
|
||||
class="rounded-full w-[14rem] h-11"
|
||||
size="md"
|
||||
>
|
||||
<Icon
|
||||
v-if="createTicketIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
:name="
|
||||
createTicketIsPending
|
||||
? 'svg-spinners:3-dots-bounce'
|
||||
: 'bi:send'
|
||||
"
|
||||
/>
|
||||
<span v-else>ارسال تیکت</span>
|
||||
</Button>
|
||||
|
||||
Vendored
+81
-51
@@ -25,71 +25,73 @@ declare global {
|
||||
};
|
||||
|
||||
type ProductVariantAttribute = {
|
||||
"id": number,
|
||||
"attribute_type": {
|
||||
"id": number,
|
||||
"name": string
|
||||
},
|
||||
"value": string
|
||||
}
|
||||
id: number;
|
||||
attribute_type: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ProductImage = {
|
||||
"id": number,
|
||||
"name": string,
|
||||
"image": string
|
||||
}
|
||||
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
type ProductDetailItem = {
|
||||
"id": number,
|
||||
"title": string,
|
||||
"detail_text1": string,
|
||||
"detail_text2": string,
|
||||
"detail_text3": string,
|
||||
"detail_text4": string
|
||||
}
|
||||
id: number;
|
||||
title: string;
|
||||
detail_text1: string;
|
||||
detail_text2: string;
|
||||
detail_text3: string;
|
||||
detail_text4: string;
|
||||
};
|
||||
|
||||
type ProductDetail = {
|
||||
"id": number,
|
||||
"detail": ProductDetailItem[],
|
||||
"detail_category": number
|
||||
}
|
||||
id: number;
|
||||
detail: ProductDetailItem[];
|
||||
detail_category: number;
|
||||
};
|
||||
|
||||
type ProductInPackItem = {
|
||||
"id": number,
|
||||
"item_title": string,
|
||||
"cover": string
|
||||
}
|
||||
id: number;
|
||||
item_title: string;
|
||||
cover: string;
|
||||
};
|
||||
|
||||
type ProductVariant = {
|
||||
"id": number,
|
||||
"product_attributes": ProductVariantAttribute[],
|
||||
"price": string,
|
||||
"in_pack_items": ProductInPackItem[],
|
||||
"details": ProductDetail[],
|
||||
"images": ProductImage[],
|
||||
"in_stock": number,
|
||||
"discount": number,
|
||||
"color": string,
|
||||
"video": string | null
|
||||
id: number;
|
||||
product_attributes: ProductVariantAttribute[];
|
||||
price: string;
|
||||
in_pack_items: ProductInPackItem[];
|
||||
details: ProductDetail[];
|
||||
images: ProductImage[];
|
||||
in_stock: number;
|
||||
discount: number;
|
||||
color: string;
|
||||
video: string | null;
|
||||
};
|
||||
|
||||
type Product = {
|
||||
"id": number,
|
||||
"variants": ProductVariant[],
|
||||
"related_products": ProductListItem[],
|
||||
"name": string,
|
||||
"description": string,
|
||||
"rating": number,
|
||||
"slug": string,
|
||||
"meta_description": string | null,
|
||||
"meta_keywords": string | null,
|
||||
"meta_rating": number,
|
||||
"category": number,
|
||||
"colors": string[]
|
||||
}
|
||||
id: number;
|
||||
variants: ProductVariant[];
|
||||
related_products: ProductListItem[];
|
||||
name: string;
|
||||
description: string;
|
||||
rating: number;
|
||||
slug: string;
|
||||
meta_description: string | null;
|
||||
meta_keywords: string | null;
|
||||
meta_rating: number;
|
||||
category: number;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">
|
||||
type ProductListItem = Pick<
|
||||
Product,
|
||||
"id" | "variants" | "name" | "rating" | "slug" | "category" | "colors"
|
||||
>;
|
||||
|
||||
type Article = {
|
||||
id: number;
|
||||
@@ -166,12 +168,40 @@ declare global {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type Order = {
|
||||
id: number;
|
||||
count: number;
|
||||
images: string[];
|
||||
discount_code: string | null;
|
||||
status: string;
|
||||
is_paid: boolean;
|
||||
created_at: string | null;
|
||||
address: Address | null;
|
||||
};
|
||||
|
||||
type ServerFile = {
|
||||
id: number;
|
||||
file_link: string;
|
||||
date: string;
|
||||
size: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type TicketMessage = {
|
||||
id: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
attachments: ServerFile[];
|
||||
};
|
||||
|
||||
type Ticket = {
|
||||
id: number;
|
||||
messages: TicketMessage[];
|
||||
subject: string;
|
||||
ticket_category: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
order: Order;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user