return detail

This commit is contained in:
hamid zarghami
2025-11-01 15:33:54 +03:30
parent 57c5ac8bad
commit c834d2206d
7 changed files with 491 additions and 6 deletions
+1
View File
@@ -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",
+319
View File
@@ -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
+5 -6
View File
@@ -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>
))
+20
View File
@@ -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,
});
};
+18
View File
@@ -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;
};
+126
View File
@@ -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;
};
};
+2
View File
@@ -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>