This commit is contained in:
Parsa Nazer
2025-02-28 18:57:56 +03:30
16 changed files with 558 additions and 249 deletions
+1 -1
View File
@@ -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>
+1 -7
View File
@@ -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;
};
+9 -8
View File
@@ -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>
+15 -3
View File
@@ -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>
+59
View File
@@ -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;
+6
View File
@@ -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 = {
+1 -1
View File
@@ -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>
+14 -84
View File
@@ -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);
دسته&zwnj;بندی :
</p>
<p class="text-xs font-semibold lg:text-sm text-black">
گیفت&zwnj;کارت
{{ 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">
نمی&zwnj;دانم کد ارسال گیفت کارت خریداری شده را از کجا
باید ببینم
{{ 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> نمی&zwnj;دانم کد ارسال
گیفت کارت خریداری شده را از کجا باید ببینم
<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
+98 -32
View File
@@ -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>
+153 -48
View File
@@ -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>
+81 -51
View File
@@ -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;
};
}