return detail
This commit is contained in:
@@ -188,6 +188,7 @@ export const Pages = {
|
||||
detailUser: "/orders/detail-user/",
|
||||
detailSeller: "/orders/detail-seller/",
|
||||
return: "/orders/returns",
|
||||
returnDetails: "/orders/return-details/",
|
||||
},
|
||||
shipment: {
|
||||
list: "/shipment/list",
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useGetReturnDetails, useApproveReturn, useRejectReturn } from './hooks/useOrderData'
|
||||
import PageTitle from '@/components/PageTitle'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import { formatPrice } from '@/helpers/func'
|
||||
import StatusWithText from '@/components/StatusWithText'
|
||||
import { Calendar, Money3, Box, Location, CloseCircle, TickCircle, Shop } from 'iconsax-react'
|
||||
import Button from '@/components/Button'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import ModalConfrim from '@/components/ModalConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
|
||||
const ReturnDetails: FC = () => {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const { data, isLoading, error } = useGetReturnDetails(id!)
|
||||
const approveReturn = useApproveReturn()
|
||||
const rejectReturn = useRejectReturn()
|
||||
|
||||
const [showApproveModal, setShowApproveModal] = useState(false)
|
||||
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||
|
||||
const handleApprove = async () => {
|
||||
try {
|
||||
await approveReturn.mutateAsync(id!)
|
||||
queryClient.invalidateQueries({ queryKey: ['return-details', id] })
|
||||
queryClient.invalidateQueries({ queryKey: ['order-returns'] })
|
||||
setShowApproveModal(false)
|
||||
navigate('/orders/returns')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
await rejectReturn.mutateAsync(id!, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['return-details', id] })
|
||||
queryClient.invalidateQueries({ queryKey: ['order-returns'] })
|
||||
setShowRejectModal(false)
|
||||
navigate('/orders/returns')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoading />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-96">
|
||||
<div className="text-center">
|
||||
<div className="text-red-500 text-lg mb-2">خطا در بارگذاری دادهها</div>
|
||||
<div className="text-gray-500">لطفا دوباره تلاش کنید</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data?.results?.returnOrder) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-96">
|
||||
<div className="text-center">
|
||||
<div className="text-gray-500 text-lg">مرجوعی یافت نشد</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const returnOrder = data.results.returnOrder
|
||||
|
||||
const getStatusVariant = (status: string): 'success' | 'error' | 'warning' => {
|
||||
switch (status) {
|
||||
case 'Approved':
|
||||
return 'success'
|
||||
case 'Rejected':
|
||||
return 'error'
|
||||
case 'Pending':
|
||||
return 'warning'
|
||||
default:
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'Approved':
|
||||
return 'تایید شده'
|
||||
case 'Rejected':
|
||||
return 'رد شده'
|
||||
case 'Pending':
|
||||
return 'در انتظار'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-4 space-y-6'>
|
||||
<PageTitle />
|
||||
|
||||
{/* هدر */}
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<h1 className='text-2xl font-bold'>جزئیات مرجوعی</h1>
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(returnOrder.status)}
|
||||
text={getStatusText(returnOrder.status)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-sm text-gray-600'>
|
||||
شماره سفارش: <span className='font-mono'>#{returnOrder.order._id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* دکمههای عملیات */}
|
||||
{data?.results?.returnOrder?.status === 'Pending' && (
|
||||
<div className='flex gap-3 justify-end'>
|
||||
<Button
|
||||
onClick={() => setShowApproveModal(true)}
|
||||
className='bg-green-600 hover:bg-green-700 text-white w-fit'
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<TickCircle color='#fff' size={20} />
|
||||
<div>تایید مرجوعی</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowRejectModal(true)}
|
||||
className='bg-red-600 hover:bg-red-700 text-white w-fit'
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<CloseCircle color='#fff' size={20} />
|
||||
<div>رد مرجوعی</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* کارتهای اطلاعاتی */}
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'>
|
||||
{/* اطلاعات فروشگاه */}
|
||||
<div className='bg-white p-6 rounded-3xl'>
|
||||
<div className='flex items-center gap-3 mb-4'>
|
||||
<div className='p-2 bg-blue-100 rounded-xl'>
|
||||
<Shop size={20} color='#3B82F6' />
|
||||
</div>
|
||||
<h3 className='font-medium'>فروشگاه</h3>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<div className='text-lg font-semibold'>{returnOrder.shopName}</div>
|
||||
<div className='text-sm text-gray-600 flex items-center gap-2'>
|
||||
<Calendar size={14} color='#6B7280' />
|
||||
<div className='mt-1 dltr'>
|
||||
{returnOrder.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* مبلغ کل */}
|
||||
<div className='bg-white p-6 rounded-3xl'>
|
||||
<div className='flex items-center gap-3 mb-4'>
|
||||
<div className='p-2 bg-green-100 rounded-xl'>
|
||||
<Money3 size={20} color='#10B981' />
|
||||
</div>
|
||||
<h3 className='font-medium'>مبلغ مرجوعی</h3>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<div className='text-lg font-bold text-green-600'>
|
||||
{formatPrice(returnOrder.total_price)} تومان
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* آدرس */}
|
||||
<div className='bg-white p-6 rounded-3xl md:col-span-2'>
|
||||
<div className='flex items-center gap-3 mb-4'>
|
||||
<div className='p-2 bg-orange-100 rounded-xl'>
|
||||
<Location size={20} color='#F59E0B' />
|
||||
</div>
|
||||
<h3 className='font-medium'>آدرس</h3>
|
||||
</div>
|
||||
<div className='space-y-3'>
|
||||
<div className='text-sm leading-relaxed'>{returnOrder.order.shipmentAddress.address}</div>
|
||||
<div className='grid grid-cols-2 gap-4 text-sm text-gray-600'>
|
||||
<div>استان: {returnOrder.order.shipmentAddress.province}</div>
|
||||
<div>شهر: {returnOrder.order.shipmentAddress.city}</div>
|
||||
<div>پلاک: {returnOrder.order.shipmentAddress.plaque}</div>
|
||||
<div>کد پستی: <span className='font-mono'>{returnOrder.order.shipmentAddress.postalCode}</span></div>
|
||||
<div className='col-span-2'>تلفن: {returnOrder.order.shipmentAddress.phone}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* آیتمهای مرجوعی */}
|
||||
<div className='bg-white py-8 xl:px-10 px-6 rounded-3xl'>
|
||||
<div className='flex items-center gap-3 mb-6'>
|
||||
<div className='p-2 bg-purple-100 rounded-xl'>
|
||||
<Box size={20} color='#8B5CF6' />
|
||||
</div>
|
||||
<h2 className='text-lg font-medium'>آیتمهای مرجوعی</h2>
|
||||
</div>
|
||||
|
||||
<div className='space-y-6'>
|
||||
{returnOrder.returnOrderItems.map((item, index) => (
|
||||
<div key={index} className='border border-gray-100 rounded-xl p-6 bg-gray-50/50'>
|
||||
{/* هدر با وضعیت */}
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<h3 className='font-medium text-base'>محصول مرجوعی</h3>
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(item.status)}
|
||||
text={getStatusText(item.status)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* اطلاعات محصول با تصویر */}
|
||||
<div className='bg-white p-4 rounded-lg border border-gray-100 mb-4'>
|
||||
<div className='flex gap-4'>
|
||||
{/* تصویر محصول */}
|
||||
<div className='flex-shrink-0'>
|
||||
<img
|
||||
src={item.shipmentItem.product.imagesUrl?.cover || '/placeholder.png'}
|
||||
alt={item.shipmentItem.product.title_fa}
|
||||
className='w-24 h-24 object-cover rounded-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* جزئیات محصول */}
|
||||
<div className='flex-1'>
|
||||
<h4 className='font-medium text-base mb-3'>{item.shipmentItem.product.title_fa}</h4>
|
||||
<div className='grid grid-cols-2 md:grid-cols-3 gap-3 text-sm'>
|
||||
<div className='text-gray-600'>
|
||||
<span className='font-medium'>تعداد مرجوعی:</span> {item.quantity}
|
||||
</div>
|
||||
<div className='text-gray-600'>
|
||||
<span className='font-medium'>قیمت واحد:</span> {formatPrice(item.shipmentItem.selling_price)} تومان
|
||||
</div>
|
||||
<div className='text-gray-600'>
|
||||
<span className='font-medium'>مبلغ کل:</span> {formatPrice(item.quantity * item.shipmentItem.selling_price)} تومان
|
||||
</div>
|
||||
{item.shipmentItem.discount_percent > 0 && (
|
||||
<div className='text-green-600'>
|
||||
<span className='font-medium'>تخفیف:</span> {item.shipmentItem.discount_percent}%
|
||||
</div>
|
||||
)}
|
||||
<div className='text-gray-600'>
|
||||
<span className='font-medium'>قیمت خردهفروشی:</span> {formatPrice(item.shipmentItem.retail_price)} تومان
|
||||
</div>
|
||||
<div className='text-gray-600'>
|
||||
<span className='font-medium'>هزینه ارسال:</span> {formatPrice(item.shipmentItem.shipmentCost)} تومان
|
||||
</div>
|
||||
{item.shipmentItem.isFreeShip && (
|
||||
<div className='text-blue-600 font-medium'>
|
||||
ارسال رایگان
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* دلیل مرجوعی */}
|
||||
<div className='bg-white p-4 rounded-lg border border-gray-100 mb-4'>
|
||||
<div className='font-medium text-sm mb-2 text-gray-700'>دلیل مرجوعی:</div>
|
||||
<div className='text-base mb-2'>{item.reason.title}</div>
|
||||
{item.reason.is_mandatory_picture && (
|
||||
<div className='text-xs text-orange-600 flex items-center gap-1'>
|
||||
* نیازمند تصویر
|
||||
</div>
|
||||
)}
|
||||
{item.reason.fineRule && (
|
||||
<div className='mt-2 text-sm text-gray-600'>
|
||||
<span className='font-medium'>جریمه:</span> {item.reason.fineRule.title} ({item.reason.fineRule.fine_percentage}%)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* توضیحات کاربر */}
|
||||
{item.comment && (
|
||||
<div className='bg-blue-50 p-4 rounded-lg border border-blue-100'>
|
||||
<div className='font-medium text-sm mb-2 text-gray-700'>توضیحات کاربر:</div>
|
||||
<div className='text-sm'>{item.comment}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* مودالها */}
|
||||
<ModalConfrim
|
||||
isOpen={showApproveModal}
|
||||
close={() => setShowApproveModal(false)}
|
||||
onConfrim={handleApprove}
|
||||
label="آیا از تایید این مرجوعی اطمینان دارید؟"
|
||||
isLoading={approveReturn.isPending}
|
||||
/>
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={showRejectModal}
|
||||
close={() => setShowRejectModal(false)}
|
||||
onConfrim={handleReject}
|
||||
label="آیا از رد این مرجوعی اطمینان دارید؟"
|
||||
isLoading={rejectReturn.isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReturnDetails
|
||||
@@ -10,6 +10,8 @@ import { formatDateShort, formatPrice } from '@/helpers/func'
|
||||
import { type ReturnOrderType } from '@/pages/orders/types/ReturnTypes'
|
||||
import { Eye } from 'iconsax-react'
|
||||
import ReturnItemsModal from '@/pages/orders/components/ReturnItemsModal'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '@/config/Pages'
|
||||
|
||||
const Returns: FC = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
@@ -93,13 +95,10 @@ const Returns: FC = () => {
|
||||
<Td text={formatDateShort(ret?.createdAt)} />
|
||||
<Td text={String(ret?.returnOrderItems?.quantity ?? 0)} />
|
||||
<Td text="">
|
||||
<button
|
||||
className='h-8 w-8 inline-flex items-center justify-center rounded-lg border border-border hover:bg-accent transition-colors'
|
||||
onClick={() => { setSelectedReturn(ret); setIsModalOpen(true); }}
|
||||
aria-label='نمایش جزئیات'
|
||||
>
|
||||
|
||||
<Link to={`${Pages.orders.returnDetails}${ret._id}`}>
|
||||
<Eye size={16} color="#4B5563" />
|
||||
</button>
|
||||
</Link>
|
||||
</Td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -84,3 +84,23 @@ export const useAddTrackCode = () => {
|
||||
mutationFn: api.addTrackCode,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetReturnDetails = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["return-details", id],
|
||||
queryFn: () => api.getReturnDetails(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useApproveReturn = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.approveReturn,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRejectReturn = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.rejectReturn,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
CreateReturnReasonType,
|
||||
NativeOrdersResponseType,
|
||||
OrderDetailResponseType,
|
||||
ReturnOrderDetailResponseType,
|
||||
ReturnReasonsResponseType,
|
||||
} from "../types/Types";
|
||||
|
||||
@@ -86,3 +87,20 @@ export const addTrackCode = async (params: AddTrackCodeType) => {
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getReturnDetails = async (
|
||||
id: string
|
||||
): Promise<ReturnOrderDetailResponseType> => {
|
||||
const { data } = await axios.get(`/admin/orders/returns/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const approveReturn = async (id: string) => {
|
||||
const { data } = await axios.post(`/admin/orders/returns/${id}/approve`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const rejectReturn = async (id: string) => {
|
||||
const { data } = await axios.post(`/admin/orders/returns/${id}/reject`);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -337,3 +337,129 @@ export type CreateReturnReasonType = {
|
||||
is_mandatory_picture: boolean;
|
||||
fineRule: string;
|
||||
};
|
||||
|
||||
// تایپهای مربوط به جزئیات مرجوعی
|
||||
export type ReturnOrderDetailShipmentItemType = {
|
||||
_id: string;
|
||||
product: number;
|
||||
variant: string;
|
||||
quantity: number;
|
||||
cancelled_quantity: number;
|
||||
returned_quantity: number;
|
||||
selling_price: number;
|
||||
retail_price: number;
|
||||
discount_percent: number;
|
||||
shipmentCost: number;
|
||||
isFreeShip: boolean;
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailOrderItemType = {
|
||||
_id: string;
|
||||
order: number;
|
||||
shop: string;
|
||||
shipmentItems: ReturnOrderDetailShipmentItemType[];
|
||||
shipper: number;
|
||||
totalSellingPrice: number;
|
||||
totalRetailPrice: number;
|
||||
totalShipmentCost: number;
|
||||
totalCouponDiscount: number;
|
||||
totalPaymentPrice: number;
|
||||
itemsCount: number;
|
||||
trackCode: string | null;
|
||||
postingDate: string | null;
|
||||
postingReceipt: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailProductType = {
|
||||
_id: number;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
seoTitle: string;
|
||||
seoDescription: string | null;
|
||||
model: string;
|
||||
source: string;
|
||||
description: string;
|
||||
metaDescription: string;
|
||||
tags: string[];
|
||||
advantages: string[];
|
||||
disAdvantages: string[];
|
||||
totalRate: number;
|
||||
category: string;
|
||||
shop: string;
|
||||
brand: string;
|
||||
status: string;
|
||||
adminComments: string | null;
|
||||
step: number;
|
||||
isFake: boolean;
|
||||
variants: string[];
|
||||
voice: string | null;
|
||||
deleted: boolean;
|
||||
specifications: unknown[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
imagesUrl: ImagesUrlType;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailReturnItemShipmentItemType = {
|
||||
_id: string;
|
||||
product: ReturnOrderDetailProductType;
|
||||
variant: string;
|
||||
quantity: number;
|
||||
cancelled_quantity: number;
|
||||
returned_quantity: number;
|
||||
selling_price: number;
|
||||
retail_price: number;
|
||||
discount_percent: number;
|
||||
shipmentCost: number;
|
||||
isFreeShip: boolean;
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailReturnItemType = {
|
||||
_id: string;
|
||||
returnOrderId: string;
|
||||
orderItem: ReturnOrderDetailOrderItemType;
|
||||
shipmentItem: ReturnOrderDetailReturnItemShipmentItemType;
|
||||
quantity: number;
|
||||
comment: string;
|
||||
reason: ReturnReasonType;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailOrderType = {
|
||||
_id: number;
|
||||
user: string;
|
||||
payment: string;
|
||||
shipmentAddress: ShipmentAddressType;
|
||||
orderStatus: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailType = {
|
||||
_id: string;
|
||||
order: ReturnOrderDetailOrderType;
|
||||
status: string;
|
||||
total_price: number;
|
||||
shopName: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
returnOrderItems: ReturnOrderDetailReturnItemType[];
|
||||
};
|
||||
|
||||
export type ReturnOrderDetailResponseType = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
returnOrder: ReturnOrderDetailType;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -81,6 +81,7 @@ import TicketMessages from '@/pages/ticket/TicketMessages'
|
||||
import Reviews from '@/pages/products/Reviews'
|
||||
import Questions from '@/pages/products/Questions'
|
||||
import Returns from '@/pages/orders/Returns'
|
||||
import ReturnDetails from '@/pages/orders/ReturnDetails'
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
@@ -199,6 +200,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.blog.category} element={<BlogCategory />} />
|
||||
|
||||
<Route path={Pages.orders.return} element={<Returns />} />
|
||||
<Route path={`${Pages.orders.returnDetails}:id`} element={<ReturnDetails />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user