orders list native with status filter + seller

This commit is contained in:
hamid zarghami
2025-09-24 12:03:26 +03:30
parent c0b6f1577c
commit edf6bcfc3d
11 changed files with 864 additions and 10 deletions
+11
View File
@@ -140,4 +140,15 @@ export const Pages = {
update: "/products/warranty/update/",
},
},
orders: {
native: "/orders/native",
sellers: "/orders/seller",
admin: "/orders/native",
completed: "/orders/native?status=Completed",
cancelled: "/orders/native?status=Cancelled",
pending: "/orders/native?status=Pending",
detail: "/orders/native/",
cancellationCategories: "/orders/cancellation-categories",
returnCategories: "/orders/return-categories",
},
};
+116
View File
@@ -0,0 +1,116 @@
import { type FC, useState } from 'react'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import PaginationUi from '../../components/PaginationUi'
import Td from '../../components/Td'
import TrashWithConfrim from '../../components/TrashWithConfrim'
import { Edit } from 'iconsax-react'
import { Link } from 'react-router-dom'
import Button from '@/components/Button'
import { useNavigate } from 'react-router-dom'
import { type CancellationCategoryType, type PagerType } from './types/Types'
const CancellationCategories: FC = () => {
const [currentPage, setCurrentPage] = useState(1);
// TODO: Add category data fetching hook
const isLoading = false;
const error = null;
const refetch = () => { };
// TODO: Add delete mutation
const deleteCategoryMutation = { isPending: false, mutateAsync: async (_id: string) => { } };
const navigate = useNavigate();
// Mock data for now
const categories: CancellationCategoryType[] = [];
const pager: PagerType = { page: currentPage, limit: 10, totalPages: 1, totalItems: 0, prevPage: null, nextPage: null };
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
);
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری دسته‌بندی کنسلی‌ها" />
</div>
);
}
const handleDeleteCategory = async (categoryId: string) => {
await deleteCategoryMutation.mutateAsync(categoryId);
refetch()
};
return (
<div>
<div className='flex justify-end mt-5'>
<Button
label='افزودن دسته‌بندی کنسلی'
onClick={() => navigate('/orders/cancellation-categories/create')}
className='w-fit'
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text={'عنوان'} />
<Td text={'توضیحات'} />
<Td text={'وضعیت'} />
<Td text={'عملیات'} />
</tr>
</thead>
<tbody>
{categories.length === 0 ? (
<tr className='tr'>
<td colSpan={4} className="text-center py-8 text-gray-500">
هیچ دستهبندی کنسلی یافت نشد
</td>
</tr>
) : (
categories.map((category: any) => (
<tr key={category.id} className='tr'>
<Td text={category.title} />
<Td text={category.description} />
<Td text="">
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded-full text-xs`}>
{category.status}
</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Link to={`/orders/cancellation-categories/${category.id}`}>
<Edit color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500" />
</Link>
<TrashWithConfrim
onDelete={() => handleDeleteCategory(category.id)}
isLoading={deleteCategoryMutation.isPending}
/>
</div>
</Td>
</tr>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
</div>
)
}
export default CancellationCategories
+141
View File
@@ -0,0 +1,141 @@
import { type FC, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import PaginationUi from '../../components/PaginationUi'
import Td from '../../components/Td'
import { Eye } from 'iconsax-react'
import { Link } from 'react-router-dom'
import { type NativeOrderType } from '@/pages/orders/types/Types'
import { useGetNativeOrders } from '@/pages/orders/hooks/useOrderData'
import { Pages } from '@/config/Pages'
const STATUS_CONFIG = {
Cancelled: { label: 'کنسل شده', color: 'bg-red-100 text-red-800' },
wait_payment: { label: 'در انتظار پرداخت', color: 'bg-orange-100 text-orange-800' },
process_by_sellers: { label: 'در حال پردازش توسط فروشنده', color: 'bg-blue-100 text-blue-800' },
cancelled_system: { label: 'کنسل شده توسط سیستم', color: 'bg-red-100 text-red-800' },
Delivered: { label: 'تحویل داده شده', color: 'bg-green-100 text-green-800' },
} as const
const getStatusLabel = (status: string): string => STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status
const getStatusColor = (status: string): string => {
const statusConfig = STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]
return statusConfig && 'color' in statusConfig ? statusConfig.color : 'bg-gray-100 text-gray-800'
}
const Native: FC = () => {
const [searchParams] = useSearchParams()
const status = searchParams.get('status') || ''
const [currentPage, setCurrentPage] = useState(1)
const { data, isLoading, error } = useGetNativeOrders(currentPage, status)
const orders = data?.results?.orders || []
const pager = data?.results?.pager ? {
...data.results.pager,
prevPage: data.results.pager.prevPage ? true : null,
nextPage: data.results.pager.nextPage ? 'next' : null
} : {
page: 1,
limit: 10,
totalPages: 1,
totalItems: 0,
prevPage: null,
nextPage: null
}
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText={`خطا در بارگذاری ${getStatusLabel(status)}`} />
</div>
)
}
const getTableHeaders = () => {
const headers = [
'شماره سفارش',
'مشتری',
'مبلغ',
'وضعیت'
]
headers.push('تاریخ')
headers.push('عملیات')
return headers.map(header => <Td key={header} text={header} />)
}
const getTableRow = (order: NativeOrderType) => {
const statusColor = getStatusColor(order.orderStatus)
return (
<tr key={order._id} className='tr'>
<Td text={`#${order._id}`} />
<Td text={order.user?.fullName || 'نامشخص'} />
<Td text={(order.payment?.totalPrice || 0).toLocaleString('fa-IR')} />
<Td text="">
<span className={`px-2 py-1 rounded-full text-xs ${statusColor}`}>
{getStatusLabel(order.orderStatus)}
</span>
</Td>
<Td text={order.createdAt || 'نامشخص'} />
<Td text="">
<div className="flex items-center gap-2">
<Link to={`${Pages.orders.detail}${order._id}?status=${status}`}>
<Eye color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500 transition-colors" />
</Link>
</div>
</Td>
</tr>
)
}
const columnCount = 6
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">{getStatusLabel(status)}</h1>
<div className='relative overflow-x-auto rounded-3xl bg-white shadow-sm'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
{getTableHeaders()}
</tr>
</thead>
<tbody>
{orders.length === 0 ? (
<tr className='tr'>
<td colSpan={columnCount} className="text-center py-12 text-gray-500">
هیچ سفارشی یافت نشد
</td>
</tr>
) : (
orders.map(order => getTableRow(order))
)}
</tbody>
</table>
</div>
{orders.length > 0 && (
<PaginationUi
pager={pager}
onPageChange={setCurrentPage}
/>
)}
</div>
)
}
export default Native
+116
View File
@@ -0,0 +1,116 @@
import { type FC, useState } from 'react'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import PaginationUi from '../../components/PaginationUi'
import Td from '../../components/Td'
import TrashWithConfrim from '../../components/TrashWithConfrim'
import { Edit } from 'iconsax-react'
import { Link } from 'react-router-dom'
import Button from '@/components/Button'
import { useNavigate } from 'react-router-dom'
import { type ReturnCategoryType, type PagerType } from './types/Types'
const ReturnCategories: FC = () => {
const [currentPage, setCurrentPage] = useState(1);
// TODO: Add category data fetching hook
const isLoading = false;
const error = null;
const refetch = () => { };
// TODO: Add delete mutation
const deleteCategoryMutation = { isPending: false, mutateAsync: async (_id: string) => { } };
const navigate = useNavigate();
// Mock data for now
const categories: ReturnCategoryType[] = [];
const pager: PagerType = { page: currentPage, limit: 10, totalPages: 1, totalItems: 0, prevPage: null, nextPage: null };
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
);
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText="خطا در بارگذاری دسته‌بندی مرجوعی‌ها" />
</div>
);
}
const handleDeleteCategory = async (categoryId: string) => {
await deleteCategoryMutation.mutateAsync(categoryId);
refetch()
};
return (
<div>
<div className='flex justify-end mt-5'>
<Button
label='افزودن دسته‌بندی مرجوعی'
onClick={() => navigate('/orders/return-categories/create')}
className='w-fit'
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
<Td text={'عنوان'} />
<Td text={'توضیحات'} />
<Td text={'وضعیت'} />
<Td text={'عملیات'} />
</tr>
</thead>
<tbody>
{categories.length === 0 ? (
<tr className='tr'>
<td colSpan={4} className="text-center py-8 text-gray-500">
هیچ دستهبندی مرجوعی یافت نشد
</td>
</tr>
) : (
categories.map((category: any) => (
<tr key={category.id} className='tr'>
<Td text={category.title} />
<Td text={category.description} />
<Td text="">
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded-full text-xs`}>
{category.status}
</span>
</div>
</Td>
<Td text="">
<div className="flex items-center gap-2">
<Link to={`/orders/return-categories/${category.id}`}>
<Edit color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500" />
</Link>
<TrashWithConfrim
onDelete={() => handleDeleteCategory(category.id)}
isLoading={deleteCategoryMutation.isPending}
/>
</div>
</Td>
</tr>
))
)}
</tbody>
</table>
</div>
<PaginationUi
pager={pager}
onPageChange={(page) => {
setCurrentPage(page);
}}
/>
</div>
)
}
export default ReturnCategories
+141
View File
@@ -0,0 +1,141 @@
import { type FC, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import PageLoading from '../../components/PageLoading'
import Error from '../../components/Error'
import PaginationUi from '../../components/PaginationUi'
import Td from '../../components/Td'
import { Eye } from 'iconsax-react'
import { Link } from 'react-router-dom'
import { type NativeOrderType } from '@/pages/orders/types/Types'
import { useGetSellerOrders } from '@/pages/orders/hooks/useOrderData'
import { Pages } from '@/config/Pages'
const STATUS_CONFIG = {
Cancelled: { label: 'کنسل شده', color: 'bg-red-100 text-red-800' },
wait_payment: { label: 'در انتظار پرداخت', color: 'bg-orange-100 text-orange-800' },
process_by_sellers: { label: 'در حال پردازش توسط فروشنده', color: 'bg-blue-100 text-blue-800' },
cancelled_system: { label: 'کنسل شده توسط سیستم', color: 'bg-red-100 text-red-800' },
Delivered: { label: 'تحویل داده شده', color: 'bg-green-100 text-green-800' },
} as const
const getStatusLabel = (status: string): string => STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]?.label || status
const getStatusColor = (status: string): string => {
const statusConfig = STATUS_CONFIG[status as keyof typeof STATUS_CONFIG]
return statusConfig && 'color' in statusConfig ? statusConfig.color : 'bg-gray-100 text-gray-800'
}
const OrdersSeller: FC = () => {
const [searchParams] = useSearchParams()
const status = searchParams.get('status') || ''
const [currentPage, setCurrentPage] = useState(1)
const { data, isLoading, error } = useGetSellerOrders(currentPage, status)
const orders = data?.results?.orders || []
const pager = data?.results?.pager ? {
...data.results.pager,
prevPage: data.results.pager.prevPage ? true : null,
nextPage: data.results.pager.nextPage ? 'next' : null
} : {
page: 1,
limit: 10,
totalPages: 1,
totalItems: 0,
prevPage: null,
nextPage: null
}
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<PageLoading />
</div>
)
}
if (error) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<Error errorText={`خطا در بارگذاری ${getStatusLabel(status)}`} />
</div>
)
}
const getTableHeaders = () => {
const headers = [
'شماره سفارش',
'مشتری',
'مبلغ',
'وضعیت'
]
headers.push('تاریخ')
headers.push('عملیات')
return headers.map(header => <Td key={header} text={header} />)
}
const getTableRow = (order: NativeOrderType) => {
const statusColor = getStatusColor(order.orderStatus)
return (
<tr key={order._id} className='tr'>
<Td text={`#${order._id}`} />
<Td text={order.user?.fullName || 'نامشخص'} />
<Td text={(order.payment?.totalPrice || 0).toLocaleString('fa-IR')} />
<Td text="">
<span className={`px-2 py-1 rounded-full text-xs ${statusColor}`}>
{getStatusLabel(order.orderStatus)}
</span>
</Td>
<Td text={order.createdAt || 'نامشخص'} />
<Td text="">
<div className="flex items-center gap-2">
<Link to={`${Pages.orders.detail}${order._id}?status=${status}`}>
<Eye color='#8C90A3' size={16} className="cursor-pointer hover:text-blue-500 transition-colors" />
</Link>
</div>
</Td>
</tr>
)
}
const columnCount = 6
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">{getStatusLabel(status)}</h1>
<div className='relative overflow-x-auto rounded-3xl bg-white shadow-sm'>
<table className='w-full text-sm'>
<thead className='thead'>
<tr>
{getTableHeaders()}
</tr>
</thead>
<tbody>
{orders.length === 0 ? (
<tr className='tr'>
<td colSpan={columnCount} className="text-center py-12 text-gray-500">
هیچ سفارشی یافت نشد
</td>
</tr>
) : (
orders.map(order => getTableRow(order))
)}
</tbody>
</table>
</div>
{orders.length > 0 && (
<PaginationUi
pager={pager}
onPageChange={setCurrentPage}
/>
)}
</div>
)
}
export default OrdersSeller
+22
View File
@@ -0,0 +1,22 @@
export const enum PaymentStatus {
Pending = "Pending",
Cancelled = "Cancelled",
Completed = "Completed",
}
export const enum OrdersStatus {
wait_payment = "wait_payment",
process_by_seller = "process_by_sellers",
cancelled_system = "cancelled_system",
Cancelled = "cancelled",
Delivered = "Delivered",
}
export const enum OrderItemsStatus {
Processing = "Processing",
Shipped = "Shipped",
Delivered = "Delivered",
cancelled_shop = "cancelled_shop",
cancelled_system = "cancelled_system",
cancelled_user = "cancelled_user",
Returned = "Returned",
}
+24
View File
@@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/OrderService";
export const useGetNativeOrders = (
page: number = 1,
status: string = "all"
) => {
const { data, isLoading, error } = useQuery({
queryKey: ["orders", page, status],
queryFn: () => api.getNativeOrders(page, status),
});
return { data, isLoading, error };
};
export const useGetSellerOrders = (
page: number = 1,
status: string = "all"
) => {
const { data, isLoading, error } = useQuery({
queryKey: ["orders", page, status],
queryFn: () => api.getSellerOrders(page, status),
});
return { data, isLoading, error };
};
+22
View File
@@ -0,0 +1,22 @@
import axios from "../../../config/axios";
import type { NativeOrdersResponseType } from "../types/Types";
export const getNativeOrders = async (
page: number = 1,
status: string
): Promise<NativeOrdersResponseType> => {
const { data } = await axios.get("/admin/orders/native", {
params: { page, status },
});
return data;
};
export const getSellerOrders = async (
page: number = 1,
status: string
): Promise<NativeOrdersResponseType> => {
const { data } = await axios.get("/admin/orders/seller", {
params: { page, status },
});
return data;
};
+245
View File
@@ -0,0 +1,245 @@
// تایپ‌های مربوط به سفارشات
export type OrderType = {
_id: string;
orderNumber: string;
customer: {
name: string;
};
seller?: {
name: string;
};
amount: number;
status: "Completed" | "Cancelled" | "Pending" | string;
date: string;
cancellationReason?: string;
cancelledDate?: string;
};
export type PaymentType = {
_id: string;
transaction_id: string | null;
payment_method: PaymentMethodType;
priceDetails: PriceDetailsType;
totalPrice: number;
paymentStatus: string;
createdAt: string;
};
export type PaymentMethodType = {
_id: string;
title_fa: string;
title_en: string;
description: string;
provider: string;
isActive: boolean;
type: string;
};
export type PriceDetailsType = {
shipping_cost: number;
process_cost: number;
total_retail_price: number;
total_payable_price: number;
total_discount: number;
coupon_discount: number;
};
export type OrderItemsType = {
_id: string;
order_id: number;
shop: ShopType;
shipmentItems: ShipmentItemType[];
shipper: ShipperType;
trackCode: string | null;
itemsCount: number;
totalPaymentPrice: number;
totalRetailPrice: number;
totalSellingPrice: number;
totalShipmentCost: number;
postingDate: string | null;
postingReceipt: string | null;
status: string;
};
export type ShipmentItemType = {
_id: string;
product: ProductType;
variant: VariantType;
quantity: number;
cancelled_quantity: number;
returned_quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shipmentCost: number;
};
export type ProductType = {
_id: number;
title_fa: string;
title_en: string;
seoTitle: string | null;
seoDescription: string | null;
source: string;
description: string;
metaDescription: string;
tags: string[];
advantages: string[];
disAdvantages: string[];
imagesUrl: ImagesUrlType;
isFake: string;
voice: string | null;
specifications: SpecificationType[];
brand: BrandType;
category: CategoryType;
variants: string[];
};
export type ImagesUrlType = {
cover: string;
list: string[];
};
export type SpecificationType = {
title: string;
values: string[];
};
export type BrandType = {
_id: string;
};
export type CategoryType = {
_id: string;
};
export type VariantType = {
_id: string;
market_status: string;
price: PriceType;
stock: number;
postingTime: number;
isFreeShip: boolean;
isWholeSale: boolean;
shop: ShopType;
saleFormat: SaleFormatType;
warranty: number;
size?: number;
meterage?: number;
};
export type PriceType = {
order_limit: number;
retailPrice: number;
selling_price: number;
is_specialSale: boolean;
discount_percent: number;
specialSale_order_limit: number | null;
specialSale_quantity: number | null;
specialSale_endDate: string | null;
};
export type SaleFormatType = {
wholeSale: unknown[];
};
export type ShopType = {
_id: string;
};
export type ShipperType = {
_id: number;
name: string;
description: string;
deliveryTime: number;
deliveryType: string;
};
export type ShipmentAddressType = {
address: string;
city: string;
province: string;
plaque: string;
postalCode: string;
phone: string;
};
export type CancellationCategoryType = {
_id: string;
title: string;
description: string;
status: string;
deleted: boolean;
};
export type ReturnCategoryType = {
_id: string;
title: string;
description: string;
status: string;
deleted: boolean;
};
// تایپ‌های صفحه‌بندی
export type PagerType = {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
};
export type OrdersResponseType = {
status: number;
success: boolean;
results: {
orders: OrderType[];
pager: PagerType;
};
};
export type CategoriesResponseType = {
status: number;
success: boolean;
results: {
categories: CancellationCategoryType[] | ReturnCategoryType[];
pager: PagerType;
};
};
export type CreateCategoryType = {
title: string;
description: string;
};
// تایپ‌های مربوط به سفارشات بومی
export type PriceRangeType = {
minPrice: number;
maxPrice: number;
};
export type UserType = {
fullName: string;
};
export type NativeOrderType = {
_id: number;
user: UserType;
payment: PaymentType;
orderItems: OrderItemsType;
shipmentAddress: ShipmentAddressType;
orderStatus: string;
createdAt: string;
};
export type NativeOrdersResponseType = {
status: number;
success: boolean;
results: {
pager: PagerType;
orders: NativeOrderType[];
priceRange: PriceRangeType;
};
};
+11
View File
@@ -19,6 +19,10 @@ import BrandList from '@/pages/brand/List'
import BrandCreate from '@/pages/brand/Create'
import WarrantyList from '@/pages/warranty/List'
import CreateWarranty from '@/pages/warranty/Create'
import Native from '@/pages/orders/Native'
import OrdersSeller from '@/pages/orders/Seller'
import CancellationCategories from '@/pages/orders/CancellationCategories'
import ReturnCategories from '@/pages/orders/ReturnCategories'
const MainRouter: FC = () => {
@@ -50,6 +54,13 @@ const MainRouter: FC = () => {
<Route path={Pages.products.brands.create} element={<BrandCreate />} />
<Route path={Pages.products.warranty.list} element={<WarrantyList />} />
<Route path={Pages.products.warranty.create} element={<CreateWarranty />} />
<Route path={Pages.orders.native} element={<Native />} />
<Route path={Pages.orders.sellers} element={<OrdersSeller />} />
<Route path={Pages.orders.completed} element={<Native />} />
<Route path={Pages.orders.cancelled} element={<Native />} />
<Route path={Pages.orders.pending} element={<Native />} />
<Route path={Pages.orders.cancellationCategories} element={<CancellationCategories />} />
<Route path={Pages.orders.returnCategories} element={<ReturnCategories />} />
</Routes>
</div>
</div>
+15 -10
View File
@@ -37,7 +37,7 @@ const SideBar: FC = () => {
const location = useLocation()
const isActive = (name: string) => {
return location.pathname.includes(name)
return window.location.href.includes(name)
}
useEffect(() => {
@@ -389,7 +389,7 @@ const ProductsSubMenu: FC = () => {
const OrdersSubMenu: FC = () => {
const isActive = (name: string) => {
return location.pathname.includes(name)
return window.location.href.includes(name)
}
return (
@@ -408,28 +408,33 @@ const OrdersSubMenu: FC = () => {
<div className='flex flex-col mt-10 gap-4'>
<SubMenuItem
title={'لیست سفارشات (فروشندگان)'}
isActive={isActive('sellers')}
link="/orders/sellers"
isActive={isActive('/seller')}
link={Pages.orders.sellers}
/>
<SubMenuItem
title={'لیست سفارشات (مدیر)'}
isActive={isActive('admin')}
link="/orders/admin"
isActive={isActive('/native')}
link={Pages.orders.admin}
/>
<SubMenuItem
title={'سفارشات کنسل شده'}
isActive={isActive('cancelled')}
link="/orders/cancelled"
isActive={isActive('Cancelled')}
link={Pages.orders.cancelled}
/>
<SubMenuItem
title={'سفارشات تکمیل شده'}
isActive={isActive('Completed')}
link={Pages.orders.completed}
/>
<SubMenuItem
title={'دسته بندی کنسلی ها'}
isActive={isActive('cancellation-categories')}
link="/orders/cancellation-categories"
link={Pages.orders.cancellationCategories}
/>
<SubMenuItem
title={'دسته بندی مرجوعی ها'}
isActive={isActive('return-categories')}
link="/orders/return-categories"
link={Pages.orders.returnCategories}
/>
</div>
</div>