This commit is contained in:
Parsa Nazer
2025-03-15 01:51:48 +03:30
18 changed files with 506 additions and 82 deletions
@@ -135,7 +135,7 @@ watch(
class="rounded-full"
>
<span class="whitespace-pre">
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
{{ !!address ? "ویرایش" : "افزودن آدرس" }}
</span>
</Button>
</template>
@@ -260,6 +260,7 @@ watch(
:disabled="createAddressIsPending"
@click="addNew"
class="rounded-full px-10"
size="md"
>
<Icon
v-if="createAddressIsPending"
@@ -268,7 +269,11 @@ watch(
<span v-else>ثبت</span>
</Button>
<DialogClose aria-label="Close">
<Button variant="outlined" class="rounded-full px-10">
<Button
variant="outlined"
class="rounded-full px-10"
size="md"
>
انصراف
</Button>
</DialogClose>
@@ -1,7 +1,7 @@
<script setup lang="ts">
// imports
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode";
import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
@@ -15,7 +15,7 @@ const discountCode = ref("");
// queries
const { data: cart, isLoading: cartIsLoading } = useGetOrdersCart();
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
const { addToast } = useToast();
+2 -2
View File
@@ -3,7 +3,7 @@
import useGetAccount from "~/composables/api/account/useGetAccount";
import { useAuth } from "~/composables/api/auth/useAuth";
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
import { NAV_LINKS } from "~/constants";
// state
@@ -16,7 +16,7 @@ const isSideDrawerOpen = ref(false);
const { data: account } = useGetAccount();
const { data: cart } = useGetOrdersCart();
const { data: cart } = useGetCartOrders();
// computed
@@ -0,0 +1,85 @@
<script setup lang="ts">
// imports
import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo";
// types
type Props = {
data: Order;
};
// props
const props = defineProps<Props>();
const { data } = toRefs(props);
// computed
const createdTimeAgo = usePersianTimeAgo(new Date(data.value.created_at));
</script>
<template>
<tr
class="odd:bg-white even:bg-gray-50 last:border-none border-b border-slate-200"
>
<td
scope="row"
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap"
:class="data.order_id ? 'text-cyan-500' : 'text-black'"
>
{{ data.order_id ? `${data.order_id}#` : "--" }}
</td>
<td class="w-3/12 px-6 py-6">
{{ data.created_at ? createdTimeAgo : "--" }}
</td>
<td class="w-2/12 px-6 py-6">
{{ data.count ? data.count : "--" }}
</td>
<td class="w-2/12 px-6 py-6">
{{ data.final_price ? data.final_price : "--" }}
</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': [
'ADMIN_PENDING',
'PENDING',
].includes(data.status),
'text-success-600 bg-success-100 border-success-600': [
'POSTED',
'RECEIVED',
].includes(data.status),
'text-danger-600 bg-danger-100 border-danger-600': [
'CANCELED',
'REFUND',
].includes(data.status),
}"
>
{{ data.verbose_status ? data.verbose_status : "--" }}
</div>
</td>
<td class="w-1/12 px-6 py-6">
<NuxtLink
:to="{
name: 'profile-purchases-and-orders-id',
params: { id: data.id },
}"
>
<button
class="size-10 flex-center border border-slate-200 rounded-md"
>
<Icon
name="ci:eye-open"
class="**:stroke-black"
size="20"
/>
</button>
</NuxtLink>
</td>
</tr>
</template>
<style scoped></style>
@@ -0,0 +1,31 @@
<script setup lang="ts"></script>
<template>
<tr
class="odd:bg-white even:bg-gray-50 last:border-none border-b border-slate-200"
>
<td
scope="row"
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
>
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-3/12 px-6 py-6">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-2/12 px-6 py-6">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-2/12 px-6 py-6">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-2/12 px-6 py-6">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-1/12 px-6 py-6">
<Skeleton class="!size-10 !rounded-sm" />
</td>
</tr>
</template>
<style scoped></style>
@@ -17,7 +17,8 @@ const { data } = toRefs(props);
// computed
const timeAgo = usePersianTimeAgo(new Date(data.value.created_at));
const createdTimeAgo = usePersianTimeAgo(new Date(data.value.created_at));
const updatedTimeAgo = usePersianTimeAgo(new Date(data.value.updated_at));
</script>
<template>
@@ -33,8 +34,13 @@ const timeAgo = usePersianTimeAgo(new Date(data.value.created_at));
<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 class="w-3/12 px-6 py-6 flex flex-col gap-3 text-sm">
<span class="w-full whitespace-pre">
ایجاد : {{ data.created_at ? createdTimeAgo : "--" }}
</span>
<span class="w-full whitespace-pre">
بروزرسانی : {{ data.updated_at ? updatedTimeAgo : "--" }}
</span>
</td>
<td class="w-2/12 px-6 py-6">
<div
@@ -0,0 +1,44 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetAllOrdersResponse = ApiPaginated<Order>;
export type GetAllOrdersRequest = {
sort: string | undefined;
status: string | undefined;
page: string | string[];
};
const useGetAllOrders = (params: ComputedRef<GetAllOrdersRequest>) => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetAllOrders = async (params: GetAllOrdersRequest) => {
const { data } = await axios.get<GetAllOrdersResponse>(
API_ENDPOINTS.orders.get_all,
{
params: {
sort: params.sort,
filter: params.status,
offset: Number(params.page) * 7 - 7,
limit: 7,
},
}
);
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.orders, params],
queryFn: () => handleGetAllOrders(params.value),
});
};
export default useGetAllOrders;
@@ -5,17 +5,17 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetOrdersCartResponse = Cart;
export type GetCartOrdersResponse = Cart;
const useGetOrdersCart = () => {
const useGetCartOrders = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetOrdersCart = async () => {
const { data } = await axios.get<GetOrdersCartResponse>(
const handleGetCartOrders = async () => {
const { data } = await axios.get<GetCartOrdersResponse>(
API_ENDPOINTS.orders.cart.get_all
);
return data;
@@ -23,8 +23,8 @@ const useGetOrdersCart = () => {
return useQuery({
queryKey: [QUERY_KEYS.cart],
queryFn: () => handleGetOrdersCart(),
queryFn: () => handleGetCartOrders(),
});
};
export default useGetOrdersCart;
export default useGetCartOrders;
@@ -1,30 +0,0 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetOrdersAllResponse = Order[];
const useGetOrdersAll = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetOrdersAll = async () => {
const { data } = await axios.get<GetOrdersAllResponse>(
API_ENDPOINTS.orders.get_all
);
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.orders],
queryFn: () => handleGetOrdersAll(),
});
};
export default useGetOrdersAll;
@@ -7,28 +7,28 @@ import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
export type GetAllTicketsResponse = ApiPaginated<Omit<Ticket, "messages">>;
export type GetAllTicketsFilters = {
export type GetAllTicketsRequest = {
sort: string | undefined;
status: string | undefined;
page: string | string[];
};
const useGetAllTickets = (params: Ref<GetAllTicketsFilters>) => {
const useGetAllTickets = (params: ComputedRef<GetAllTicketsRequest>) => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetAllTickets = async (params: GetAllTicketsFilters) => {
const handleGetAllTickets = async (params: GetAllTicketsRequest) => {
const { data } = await axios.get<GetAllTicketsResponse>(
API_ENDPOINTS.tickets.get_all,
{
params: {
sort: params.sort,
filter: params.status,
offset: Number(params.page) * 7 - 7,
limit: 7,
offset: Number(params.page) * 10 - 10,
limit: 10,
},
}
);
+2 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// state
@@ -12,7 +12,7 @@ const prevPage = computed(() => route.meta.prevPage);
// queries
const { data: cart } = useGetOrdersCart();
const { data: cart } = useGetCartOrders();
// computed
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
// imports
import useGetOrdersCart from "~/composables/api/orders/useGetOrdersCart";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta
@@ -15,7 +15,7 @@ definePageMeta({
// queries
const { data: cart, isLoading: cartIsLoading, suspense } = useGetOrdersCart();
const { data: cart, isLoading: cartIsLoading, suspense } = useGetCartOrders();
await suspense();
@@ -1,16 +0,0 @@
<script setup lang="ts">
// meta
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
});
</script>
<template>
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="خرید ها و سفارش های شما" icon="bi:cart" />
</div>
</template>
<style scoped></style>
@@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
@@ -0,0 +1,256 @@
<script setup lang="ts">
import useGetAllOrders, {
type GetAllOrdersRequest,
} from "~/composables/api/orders/useGetAllOrders";
// meta
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
});
// state
const params: GetAllOrdersRequest = useUrlSearchParams("history");
const filters = computed(() => {
return {
sort: params.sort ?? "created_at",
status: params.status ?? "",
page: params.page ?? 1,
};
});
const tableHeads = ref([
"شماره سفارش",
"تاریخ ثبت",
"تعداد اقلام",
"مبلغ",
"وضعیت",
"عملیات",
]);
const sortFilters = ref([
{
title: "جدید ترین",
value: "created_at",
},
{
title: "قدیمی ترین",
value: "-created_at",
},
{
title: "گران ترین",
value: "final_price",
},
{
title: "ارزان ترین",
value: "-final_price",
},
]);
const statusFilters = ref([
{
title: "در انتظار تایید",
value: "ADMIN_PENDING",
},
{
title: "در حال پردازش",
value: "PENDING",
},
{
title: "ارسال شده",
value: "POSTED",
},
{
title: "تحویل داده شده",
value: "RECEIVED",
},
{
title: "لغو شده",
value: "CANCELED",
},
{
title: "مرجوع شده",
value: "REFUND",
},
]);
// provide / inject
provide("params", params);
// queries
const { data, isPending: purchasesIsPending } = useGetAllOrders(filters);
// computed
const purchases = computed(() => {
return data.value?.results.flat();
});
const hasPurchases = computed(() => data.value?.count > 0);
const hasFilters = computed(() =>
Object.keys(params)
.filter((key) => key != "page")
.some((key) => params[key] != undefined)
);
const paginationData = computed(() => {
return data.value?.results.map((_, i: number) => {
return { type: "page", value: i };
});
});
// methods
const clearFilters = () => {
params.sort = undefined;
params.status = undefined;
};
</script>
<template>
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="خرید ها و سفارش های شما" icon="bi:cart" />
<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 class="flex items-center justify-start gap-3">
<span class="text-sm">ترتیب بر اساس</span>
<Select
v-model="params.sort!"
triggerRootClass="!py-2.5"
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 class="flex items-center justify-start gap-3">
<span class="text-sm">وضعیت</span>
<Select
v-model="params.status!"
triggerRootClass="!py-2.5"
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>
<div class="flex-center gap-4">
<Button
v-if="hasFilters"
end-icon="bi:x"
@click="clearFilters"
size="md"
class="rounded-full"
>
<span class="whitespace-pre"> حذف فیلتر ها </span>
</Button>
<NuxtLink :to="{ name: 'products' }">
<Button
end-icon="bi:plus"
size="md"
class="rounded-full"
>
<span class="whitespace-pre"> خرید جدید </span>
</Button>
</NuxtLink>
</div>
</div>
<Placeholder
v-if="!hasPurchases && !purchasesIsPending"
class="!w-full !py-[5rem]"
icon="bi:cart"
title="خرید یا سفارشی یافت نشد"
/>
<Table v-else>
<template #thead>
<th
v-for="(tableHead, index) in tableHeads"
:key="index"
scope="col"
:class="
[0, 1].includes(index)
? 'w-3/12'
: tableHeads.length - 1 == index
? 'w-1/2'
: 'w-2/12'
"
class="px-6 py-5 text-sm font-normal"
>
{{ tableHead }}
</th>
</template>
<template #tbody>
<template v-if="purchasesIsPending">
<PurchasesTableRowLoading v-for="i in 5" />
</template>
<template v-else>
<PurchasesTableRow
v-for="(purchase, index) in purchases"
:key="index"
:data="purchase"
/>
</template>
</template>
</Table>
<div v-if="data?.count > 10" class="w-full flex-center py-10">
<Pagination :items="paginationData" :total="data?.count" />
</div>
</div>
</div>
</template>
<style scoped></style>
+35 -6
View File
@@ -2,7 +2,7 @@
// imports
import useGetAllTickets, {
type GetAllTicketsFilters,
type GetAllTicketsRequest,
} from "~/composables/api/tickets/useGetAllTickets";
// meta
@@ -14,7 +14,7 @@ definePageMeta({
// state
const params: GetAllTicketsFilters = useUrlSearchParams("history");
const params: GetAllTicketsRequest = useUrlSearchParams("history");
const filters = computed(() => {
return {
@@ -74,11 +74,24 @@ const tickets = computed(() => {
const hasTickets = computed(() => data.value?.count > 0);
const hasFilters = computed(() =>
Object.keys(params)
.filter((key) => key != "page")
.some((key) => params[key] != undefined)
);
const paginationData = computed(() => {
return data.value?.results.map((_, i: number) => {
return { type: "page", value: i };
});
});
// methods
const clearFilters = () => {
params.sort = undefined;
params.status = undefined;
};
</script>
<template>
@@ -152,11 +165,27 @@ const paginationData = computed(() => {
</div>
</div>
<NuxtLink :to="{ name: 'profile-tickets-new' }">
<Button end-icon="bi:plus" size="md" class="rounded-full">
<span class="whitespace-pre"> تیکت جدید </span>
<div class="flex-center gap-4">
<Button
v-if="hasFilters"
end-icon="bi:x"
@click="clearFilters"
size="md"
class="rounded-full"
>
<span class="whitespace-pre"> حذف فیلتر ها </span>
</Button>
</NuxtLink>
<NuxtLink :to="{ name: 'profile-tickets-new' }">
<Button
end-icon="bi:plus"
size="md"
class="rounded-full"
>
<span class="whitespace-pre"> تیکت جدید </span>
</Button>
</NuxtLink>
</div>
</div>
<Placeholder
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
// imports
import useGetOrdersAll from "~/composables/api/orders/useGetOrdersAll";
import useGetAllOrders from "~/composables/api/orders/useGetAllOrders";
import useCreateTicket, {
type CreateTicketRequest,
} from "~/composables/api/tickets/useCreateTicket";
@@ -74,7 +74,7 @@ const ticketData = ref<CreateTicketRequest>({
// queries
const { data: orders, isLoading: ordersIsLoading } = useGetOrdersAll();
const { data: orders, isLoading: ordersIsLoading } = useGetAllOrders();
const { mutateAsync: createTicket, isPending: createTicketIsPending } =
useCreateTicket();
+11 -4
View File
@@ -172,11 +172,18 @@ declare global {
id: number;
count: number;
images: string[];
discount_code: string | null;
status: string;
status:
| "ADMIN_PENDING"
| "PENDING"
| "POSTED"
| "RECEIVED"
| "CANCELED"
| "REFUND";
verbose_status: string;
is_paid: boolean;
created_at: string | null;
address: Address | null;
created_at: string;
final_price: string;
order_id: number;
};
type DiscountCode = {