refresh token + base orders list and detail order

This commit is contained in:
hamid zarghami
2025-09-15 11:30:41 +03:30
parent 21319eef94
commit 220c8be526
18 changed files with 1316 additions and 534 deletions
@@ -0,0 +1,132 @@
'use client'
import React from 'react'
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'
import Modal from '@/components/Modal'
import Select from '@/components/Select'
import Input from '@/components/Input'
import { Button } from '@/components/ui/button'
interface CancelOrderFormData {
reason: string
count: string
description: string
}
interface CancelReason {
_id: string
title: string
}
interface CancelOrderModalProps {
showModalCancel: boolean
setShowModalCancel: (show: boolean) => void
onSubmit: (data: CancelOrderFormData) => void
isLoading: boolean
cancelReasons?: Array<{ _id: string; title: string }>
}
const cancelOrderSchema = yup.object({
reason: yup.string().required('دلیل لغو سفارش را انتخاب کنید'),
count: yup.string().required('تعداد را وارد کنید'),
description: yup.string().required('توضیحات را وارد کنید')
})
const CancelOrderModal: React.FC<CancelOrderModalProps> = ({
showModalCancel,
setShowModalCancel,
onSubmit,
isLoading,
cancelReasons = []
}) => {
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors },
reset
} = useForm<CancelOrderFormData>({
resolver: yupResolver(cancelOrderSchema)
})
const handleFormSubmit = (data: CancelOrderFormData) => {
onSubmit(data)
reset()
}
return (
<Modal
open={showModalCancel}
close={() => setShowModalCancel(false)}
isHeader
title='درخواست لغو سفارش'
>
<form onSubmit={handleSubmit(handleFormSubmit)} className='mt-6'>
<Select
placeholder='دلیل لغو سفارش'
items={cancelReasons.map((item) => ({
label: item.title,
value: item._id
}))}
className='bg-white border'
value={watch('reason')}
name='reason'
onChange={(e) => setValue('reason', e.target.value)}
error_text={errors.reason?.message}
/>
<Input
variant='floating_outlined'
label='تعداد'
className='mt-6'
{...register('count')}
error_text={errors.count?.message}
/>
<div className='mt-6'>
<div className="text-sm font-medium mb-2">توضیحات :</div>
<textarea
{...register('description')}
className='h-[123px] rounded-xl p-3 text-sm border w-full resize-none focus:outline-none focus:ring-2 focus:ring-primary/20'
placeholder='متن توضیحات خود را بنویسید'
/>
{errors.description && (
<div className='text-xs text-red-500 mt-1'>
{errors.description.message}
</div>
)}
</div>
<div className='flex gap-2 text-xs text-[#7F7F7F] mt-4 p-3 bg-blue-50 rounded-lg'>
<div className="size-5 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 text-xs">!</span>
</div>
<span>
توجه داشته باشید که بعد از ثبت لغو سفارش طبق
</span>
<span className='text-primary font-medium'>
جدول جرائم
</span>
<span>
جریمه میشود
</span>
</div>
<div className='mt-6'>
<Button
type="submit"
className='w-full'
disabled={isLoading}
isLoading={isLoading}
>
لغو سفارش
</Button>
</div>
</form>
</Modal>
)
}
export default CancelOrderModal
@@ -0,0 +1,96 @@
'use client'
import React from 'react'
import OrderProductItem from './OrderProductItem'
import { OrderItemDetail } from '../../types/Types'
interface OrderMainContentProps {
orderItems: OrderItemDetail[]
orderId: string
getStatusText: (status: string) => string
onCancelClick: () => void
}
const OrderMainContent: React.FC<OrderMainContentProps> = ({
orderItems,
orderId,
getStatusText,
onCancelClick
}) => {
// محاسبه تعداد مرسوله (length of orderItems)
const shipmentCount = orderItems.length
// محاسبه تعداد اقلام (sum of all itemsCount)
const totalItemsCount = orderItems.reduce((sum, item) => sum + item.itemsCount, 0)
// گرفتن اولین آیتم برای نمایش اطلاعات (در صورت نیاز)
const firstOrderItem = orderItems[0]
// محاسبه مجموع هزینه ارسال و مبلغ مرسوله
const totalShipmentCost = orderItems.reduce((sum, item) => sum + item.totalShipmentCost, 0)
const totalPaymentPrice = orderItems.reduce((sum, item) => sum + item.totalPaymentPrice, 0)
return (
<div className='flex-1 w-full bg-white shadow-lg p-6 rounded-2xl'>
<div className='flex md:flex-row flex-col gap-4 justify-between md:items-center'>
<div className='flex md:flex-row flex-col gap-4'>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>تعداد مرسوله :</div>
<div>{shipmentCount}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>تعداد اقلام :</div>
<div>
{totalItemsCount}
</div>
</div>
</div>
<div className='text-primary font-semibold'>
{firstOrderItem ? getStatusText(firstOrderItem.status) : ''}
</div>
</div>
<div className='flex items-center gap-2 mt-6'>
<div className='text-[#7F7F7F]'>حداکثر مهلت ارسال :</div>
<div>
{firstOrderItem?.shipper.deliveryTime} روز
</div>
</div>
<div className='flex md:flex-row flex-col gap-3 justify-between mt-6'>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>هزینه ارسال :</div>
<div>
{totalShipmentCost.toLocaleString()} تومان
</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>مبلغ مرسوله :</div>
<div>
{totalPaymentPrice.toLocaleString()} تومان
</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>شناسه سفارش :</div>
<div className="font-mono">
{orderId}
</div>
</div>
</div>
{/* Product Items */}
{orderItems.map((orderItem) =>
orderItem.shipmentItems.map((item) => (
<OrderProductItem
key={item._id}
item={item}
shipperName={orderItem.shipper.name}
onCancelClick={onCancelClick}
/>
))
)}
</div>
)
}
export default OrderMainContent
@@ -0,0 +1,97 @@
'use client'
import React from 'react'
import Image from 'next/image'
import { MessageText, ShieldTick, Truck, CloseCircle } from 'iconsax-react'
import { ShipmentItemDetail } from '../../types/Types'
interface OrderProductItemProps {
item: ShipmentItemDetail
shipperName: string
onCancelClick: () => void
}
const OrderProductItem: React.FC<OrderProductItemProps> = ({ item, shipperName, onCancelClick }) => {
return (
<div className='mt-8 py-7 border-t items-start flex gap-6 sm:flex-row flex-col'>
<Image
src={item.product.imagesUrl.cover}
width={80}
height={80}
className='w-20 h-20 object-cover rounded-lg border'
alt={item.product.title_fa}
/>
<div className='flex-1 w-full'>
<div className='truncate max-w-[90%] font-medium'>
{item.product.title_fa}
</div>
<div className='mt-4 text-[#999999] space-y-3'>
<div className='flex gap-2'>
<div>تعداد اقلام :</div>
<div>{item.quantity}</div>
</div>
<div className='flex gap-2'>
<div>تعداد کنسلی :</div>
<div>{item.cancelled_quantity}</div>
</div>
<div className='flex gap-2'>
<div>تعداد مرجوعی :</div>
<div>{item.returned_quantity}</div>
</div>
<div className='flex gap-3 items-center'>
<div className='size-3 bg-black rounded-full'></div>
<div>
{item.variant.meterage?.value ||
item.variant.size?.value ||
'ندارد'}
</div>
</div>
<div className='flex gap-3 items-center'>
<MessageText size={20} className="text-gray-400" />
<div className="font-mono">
SKP_{item.product._id}
</div>
</div>
<div className='flex gap-3 items-center'>
<ShieldTick size={20} className="text-green-500" />
<div>
گارانتی {item.variant.warranty.duration} {item.variant.warranty.name}
</div>
</div>
<div className='flex gap-3 items-center'>
<Truck size={20} className="text-blue-500" />
<div>
{shipperName}
</div>
</div>
</div>
<div className='mt-8 flex justify-between items-center'>
<div>
<div className='text-lg font-semibold'>
{Intl.NumberFormat('fa-IR').format(item.selling_price)}
</div>
<div className='text-xs mt-1 text-gray-500'>تومان</div>
</div>
{item.quantity !== item.cancelled_quantity && (
<button
onClick={onCancelClick}
className='flex cursor-pointer gap-2 text-[#B3261E] hover:bg-red-50 p-2 rounded-lg transition-colors'
>
<div>لغو سفارش</div>
<CloseCircle size={16} />
</button>
)}
</div>
</div>
</div>
)
}
export default OrderProductItem
@@ -0,0 +1,104 @@
'use client'
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import TrackCode from './TrackCode'
import { Payment, User, ShipmentAddress } from '../../types/Types'
import * as api from '../../service/Service'
import { toast } from '@/components/Toast'
interface OrderSidebarProps {
orderData: {
payment: Payment
order: {
_id: string
user: User
shipmentAddress: ShipmentAddress
createdAt: string
}
}
orderId: number
}
const OrderSidebar: React.FC<OrderSidebarProps> = ({ orderData, orderId }) => {
const [isLoadingInvoice, setIsLoadingInvoice] = useState(false)
const handleInvoice = async () => {
try {
setIsLoadingInvoice(true)
const blob = await api.getInvoice(orderId.toString())
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `invoice-${orderId}.pdf`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
toast("فاکتور با موفقیت دانلود شد", "success")
} catch (error) {
toast("مشکلی در دانلود فاکتور رخ داد", "error")
} finally {
setIsLoadingInvoice(false)
}
}
return (
<div className='lg:w-[280px] w-full p-5 rounded-2xl shadow-lg'>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>شناسه تراکنش :</div>
<div className="font-mono">
{orderData.payment.transaction_id}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F]'>گیرنده :</div>
<div>
{orderData.order.user.fullName}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F]'>کدپستی :</div>
<div>
{orderData.order.shipmentAddress.postalCode}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F] whitespace-nowrap'>تاریخ ثبت سفارش :</div>
<div>
{orderData.order.createdAt}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F]'>شماره موبایل :</div>
<div>
{orderData.order.shipmentAddress.phone}
</div>
</div>
<div className='flex items-start gap-2 mt-4'>
<div className='text-[#7F7F7F] whitespace-nowrap'>آدرس :</div>
<div>
{orderData.order.shipmentAddress.address}
</div>
</div>
<TrackCode orderId={orderId} />
<Button
className='mt-3 w-full'
variant="outline"
onClick={handleInvoice}
disabled={isLoadingInvoice}
>
{isLoadingInvoice ? 'در حال دانلود...' : 'مشاهده فاکتور'}
</Button>
</div>
)
}
export default OrderSidebar
+51 -363
View File
@@ -1,115 +1,25 @@
'use client'
import { FC, useState } from 'react'
import { ArrowRight2, ShieldTick, Truck, MessageText, CloseCircle } from 'iconsax-react'
import { ArrowRight2 } from 'iconsax-react'
import { useRouter, useParams } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'
import PageLoading from '@/components/PageLoading'
import TrackCode from './components/TrackCode'
import { Button } from '@/components/ui/button'
import Modal from '@/components/Modal'
import Select from '@/components/Select'
import Input from '@/components/Input'
import Layout from '@/hoc/Layout'
import Image from 'next/image'
import Menu from '../../components/Menu'
import OrderSidebar from './components/OrderSidebar'
import OrderMainContent from './components/OrderMainContent'
import CancelOrderModal from './components/CancelOrderModal'
import { useGetOrderDetail } from '../hooks/useOrdersData'
import * as api from '../service/Service'
import { toast } from '@/components/Toast'
// Mock data
const mockOrderDetail = {
payment: {
transaction_id: 'TRX123456789'
},
order: {
_id: 'ORD001',
user: {
fullName: 'احمد محمدی'
},
shipmentAddress: {
postalCode: '1234567890',
phone: '09123456789',
address: 'تهران، خیابان ولیعصر، پلاک 123'
},
createdAt: '1402/12/15',
orderItems: {
_id: 'ITEM001',
itemsCount: 3,
status: 'processing',
totalShipmentCost: '25000 تومان',
totalPaymentPrice: '450000',
shipper: {
name: 'پست ایران',
deliveryTime: 3
},
shipmentItems: [
{
_id: 'SHIP001',
product: {
_id: 'PROD001',
title_fa: 'لپ‌تاپ اپل مک‌بوک پرو 13 اینچی',
imagesUrl: {
cover: 'https://via.placeholder.com/80x80'
}
},
quantity: 1,
cancelled_quantity: 0,
returned_quantity: 0,
selling_price: 450000,
variant: {
color: {
name: 'نقره‌ای'
},
size: {
name: '13 اینچ'
},
meterages: {
name: '256GB'
},
warranty: {
duration: '2 سال',
name: 'گارانتی'
}
}
}
]
}
}
}
const mockCancelReasons = [
{ _id: 'REASON1', title: 'تغییر نظر در خرید' },
{ _id: 'REASON2', title: 'یافتن قیمت بهتر' },
{ _id: 'REASON3', title: 'مشکل در محصول' },
{ _id: 'REASON4', title: 'تأخیر در ارسال' }
]
// Form validation schema
const cancelOrderSchema = yup.object({
reason: yup.string().required('لطفا دلیل لغو سفارش را انتخاب کنید'),
description: yup.string().required('لطفا توضیحات لغو سفارش را وارد کنید'),
count: yup.string().required('تعداد لغو سفارش را وارد کنید')
})
type CancelOrderFormData = yup.InferType<typeof cancelOrderSchema>
const OrderDetail: FC = () => {
const { id } = useParams()
const { data: orderDetail } = useGetOrderDetail(id as string)
const [showModalCancel, setShowModalCancel] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState<boolean>(false)
const router = useRouter()
// Form with react-hook-form
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors },
reset
} = useForm<CancelOrderFormData>({
resolver: yupResolver(cancelOrderSchema)
})
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'processing': 'در حال پردازش',
@@ -120,17 +30,24 @@ const OrderDetail: FC = () => {
return statusMap[status] || status
}
const onSubmit = async () => {
setIsLoading(true)
const onSubmit = async (cancelData: { reason: string; count: string; description: string }) => {
try {
setIsLoading(true)
await api.cancelOrder(id as string, {
reason: cancelData.reason,
count: parseInt(cancelData.count),
description: cancelData.description
})
toast("درخواست لغو سفارش با موفقیت ثبت شد", "success")
// Simulate API call
setTimeout(() => {
setIsLoading(false)
setShowModalCancel(false)
reset() // Reset form
alert('درخواست لغو سفارش با موفقیت ثبت شد')
router.push('/profile/orders')
}, 1000)
} catch {
toast("مشکلی در ثبت درخواست لغو سفارش رخ داد", "error")
} finally {
setIsLoading(false)
}
}
return (
@@ -151,269 +68,40 @@ const OrderDetail: FC = () => {
{isLoading ? (
<PageLoading />
) : (
) : orderDetail?.results.order ? (
<div className='mt-6 lg:flex-row flex-col flex gap-6 items-start'>
{/* Sidebar */}
<div className='lg:w-[280px] w-full p-5 rounded-2xl shadow-lg'>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>شناسه تراکنش :</div>
<div className="font-mono">
{mockOrderDetail.payment.transaction_id}
</div>
</div>
<OrderSidebar
orderData={{
payment: orderDetail.results.order.payment,
order: {
_id: orderDetail.results.order._id.toString(),
user: orderDetail.results.order.user,
shipmentAddress: orderDetail.results.order.shipmentAddress,
createdAt: orderDetail.results.order.createdAt
}
}}
orderId={orderDetail.results.order._id}
/>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F]'>گیرنده :</div>
<div>
{mockOrderDetail.order.user.fullName}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F]'>کدپستی :</div>
<div>
{mockOrderDetail.order.shipmentAddress.postalCode}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F] whitespace-nowrap'>تاریخ ثبت سفارش :</div>
<div>
{mockOrderDetail.order.createdAt}
</div>
</div>
<div className='flex items-center gap-2 mt-4'>
<div className='text-[#7F7F7F]'>شماره موبایل :</div>
<div>
{mockOrderDetail.order.shipmentAddress.phone}
</div>
</div>
<div className='flex items-start gap-2 mt-4'>
<div className='text-[#7F7F7F] whitespace-nowrap'>آدرس :</div>
<div>
{mockOrderDetail.order.shipmentAddress.address}
</div>
</div>
<TrackCode orderId={Number(id) || 0} />
<Button
className='mt-3 w-full'
variant="outline"
>
مشاهده فاکتور
</Button>
</div>
{/* Main Content */}
<div className='flex-1 w-full bg-white shadow-lg p-6 rounded-2xl'>
<div className='flex md:flex-row flex-col gap-4 justify-between md:items-center'>
<div className='flex md:flex-row flex-col gap-4'>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>تعداد مرسوله :</div>
<div>1</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>تعداد اقلام :</div>
<div>
{mockOrderDetail.order.orderItems.itemsCount}
</div>
</div>
</div>
<div className='text-primary font-semibold'>
{getStatusText(mockOrderDetail.order.orderItems.status)}
</div>
</div>
<div className='flex items-center gap-2 mt-6'>
<div className='text-[#7F7F7F]'>حداکثر مهلت ارسال :</div>
<div>
{mockOrderDetail.order.orderItems.shipper.deliveryTime} روز
</div>
</div>
<div className='flex md:flex-row flex-col gap-3 justify-between mt-6'>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>هزینه ارسال :</div>
<div>
{mockOrderDetail.order.orderItems.totalShipmentCost}
</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>مبلغ مرسوله :</div>
<div>
{mockOrderDetail.order.orderItems.totalPaymentPrice} تومان
</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>شناسه سفارش :</div>
<div className="font-mono">
{mockOrderDetail.order._id}
</div>
</div>
</div>
{/* Product Items */}
{mockOrderDetail.order.orderItems.shipmentItems.map((item) => (
<div key={item.product._id} className='mt-8 py-7 border-t items-start flex gap-6 sm:flex-row flex-col'>
<Image
src={item.product.imagesUrl.cover}
width={80}
height={80}
className='w-20 h-20 object-cover rounded-lg border'
alt={item.product.title_fa}
/>
<div className='flex-1 w-full'>
<div className='truncate max-w-[90%] font-medium'>
{item.product.title_fa}
</div>
<div className='mt-4 text-[#999999] space-y-3'>
<div className='flex gap-2'>
<div>تعداد اقلام :</div>
<div>{item.quantity}</div>
</div>
<div className='flex gap-2'>
<div>تعداد کنسلی :</div>
<div>{item.cancelled_quantity}</div>
</div>
<div className='flex gap-2'>
<div>تعداد مرجوعی :</div>
<div>{item.returned_quantity}</div>
</div>
<div className='flex gap-3 items-center'>
<div className='size-3 bg-black rounded-full'></div>
<div>
{item.variant.color?.name ||
item.variant.size?.name ||
item.variant.meterages?.name}
</div>
</div>
<div className='flex gap-3 items-center'>
<MessageText size={20} className="text-gray-400" />
<div className="font-mono">
SKP_{item.product._id}
</div>
</div>
<div className='flex gap-3 items-center'>
<ShieldTick size={20} className="text-green-500" />
<div>
گارانتی {item.variant.warranty.duration} {item.variant.warranty.name}
</div>
</div>
<div className='flex gap-3 items-center'>
<Truck size={20} className="text-blue-500" />
<div>
{mockOrderDetail.order.orderItems.shipper.name}
</div>
</div>
</div>
<div className='mt-8 flex justify-between items-center'>
<div>
<div className='text-lg font-semibold'>
{Intl.NumberFormat('fa-IR').format(item.selling_price)}
</div>
<div className='text-xs mt-1 text-gray-500'>تومان</div>
</div>
{item.quantity !== item.cancelled_quantity && (
<button
onClick={() => setShowModalCancel(true)}
className='flex cursor-pointer gap-2 text-[#B3261E] hover:bg-red-50 p-2 rounded-lg transition-colors'
>
<div>لغو سفارش</div>
<CloseCircle size={16} />
</button>
)}
</div>
</div>
</div>
))}
</div>
<OrderMainContent
orderItems={orderDetail.results.order.orderItems}
orderId={orderDetail.results.order._id.toString()}
getStatusText={getStatusText}
onCancelClick={() => setShowModalCancel(true)}
/>
</div>
) : (
<div className='mt-6 text-center'>در حال بارگذاری...</div>
)}
</div>
{/* Cancel Order Modal */}
<Modal
open={showModalCancel}
close={() => setShowModalCancel(false)}
isHeader
title='درخواست لغو سفارش'
>
<form onSubmit={handleSubmit(onSubmit)} className='mt-6'>
<Select
placeholder='دلیل لغو سفارش'
items={mockCancelReasons.map((item) => ({
label: item.title,
value: item._id
}))}
className='bg-white border'
value={watch('reason')}
name='reason'
onChange={(e) => setValue('reason', e.target.value)}
error_text={errors.reason?.message}
/>
<Input
variant='floating_outlined'
label='تعداد'
className='mt-6'
{...register('count')}
error_text={errors.count?.message}
/>
<div className='mt-6'>
<div className="text-sm font-medium mb-2">توضیحات :</div>
<textarea
{...register('description')}
className='h-[123px] rounded-xl p-3 text-sm border w-full resize-none focus:outline-none focus:ring-2 focus:ring-primary/20'
placeholder='متن توضیحات خود را بنویسید'
/>
{errors.description && (
<div className='text-xs text-red-500 mt-1'>
{errors.description.message}
</div>
)}
</div>
<div className='flex gap-2 text-xs text-[#7F7F7F] mt-4 p-3 bg-blue-50 rounded-lg'>
<div className="size-5 bg-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 text-xs">!</span>
</div>
<span>
توجه داشته باشید که بعد از ثبت لغو سفارش طبق
</span>
<span className='text-primary font-medium'>
جدول جرائم
</span>
<span>
جریمه میشود
</span>
</div>
<div className='mt-6'>
<Button
type="submit"
className='w-full'
disabled={isLoading}
isLoading={isLoading}
>
لغو سفارش
</Button>
</div>
</form>
</Modal>
<CancelOrderModal
showModalCancel={showModalCancel}
setShowModalCancel={setShowModalCancel}
onSubmit={onSubmit}
isLoading={isLoading}
cancelReasons={orderDetail?.results.order.cancelReasons}
/>
</div>
)
}
@@ -0,0 +1,103 @@
'use client'
import React from 'react'
import { Button } from '@/components/ui/button'
import { useRouter } from 'next/navigation'
interface OrderButtonsProps {
status: string
orderId: string
}
const OrderButtons: React.FC<OrderButtonsProps> = ({ status, orderId }) => {
const statusLower = status.toLowerCase();
const router = useRouter();
switch (statusLower) {
case 'pending':
return (
<>
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
onClick={() => router.push(`/profile/orders/${orderId}`)}
>
جزئیات سفارش
</Button>
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
پرداخت
</Button>
</>
);
case 'processing':
case 'shipped':
return (
<>
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
>
پیگیری سفارش
</Button>
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
onClick={() => router.push(`/profile/orders/${orderId}`)}
>
جزئیات سفارش
</Button>
</>
);
case 'delivered':
return (
<>
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
>
امتیازدهی
</Button>
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
خرید مجدد
</Button>
</>
);
case 'returned':
return (
<>
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
onClick={() => router.push(`/profile/orders/${orderId}`)}
>
جزئیات سفارش
</Button>
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
خرید مجدد
</Button>
</>
);
case 'cancelled':
return (
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
سفارش مجدد
</Button>
);
default:
return (
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
onClick={() => router.push(`/profile/orders/${orderId}`)}
>
جزئیات سفارش
</Button>
);
}
};
export default OrderButtons
@@ -0,0 +1,53 @@
'use client'
import React from 'react'
import OrderButtons from './OrderButtons'
import OrderProducts from './OrderProducts'
import { Order } from '../types/Types'
import moment from 'moment-jalaali'
interface OrderItemProps {
order: Order
getStatusText: (status: string) => string
}
const OrderItem: React.FC<OrderItemProps> = ({ order, getStatusText }) => {
return (
<div className='bg-white border border-[#E0E0E0] rounded-[8px] p-3 sm:p-4 md:p-5'>
<div className='flex flex-col lg:flex-row gap-4 sm:gap-6'>
<div className='lg:max-w-[234px] w-full'>
<div className=''>
<div className='text-xs sm:text-sm text-[#7F7F7F] mb-1'>
شماره سفارش: <span className='text-black'> {order._id.toString()}</span>
</div>
<div className='text-xs sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
مبلغ: <span className='text-black'> {(order.payment?.totalPrice || 0).toLocaleString('fa-IR')} تومان</span>
</div>
</div>
<div className='mb-4 sm:mb-6 mt-3 sm:mt-4'>
<div className='text-xs sm:text-sm text-[#7F7F7F]'>
وضعیت سفارش : <span className='text-black'>{getStatusText(order.status || 'pending')}</span>
</div>
<div className='text-xs dltr text-right sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
تاریخ ثبت سفارش: <span className='text-black'> {moment(order.payment?.createdAt, 'jYYYY/jMM/jDD HH:mm:ss').format('jYYYY-jMM-jDD')}</span>
</div>
</div>
<div className='flex flex-col sm:flex-row gap-2 sm:gap-3'>
<OrderButtons status={order.status || 'pending'} orderId={order._id.toString()} />
</div>
</div>
<OrderProducts products={order.orderItems.flatMap(item =>
item.shipmentItems.map(shipmentItem => ({
id: shipmentItem.product._id,
image: shipmentItem.product.imagesUrl?.cover || '',
name: shipmentItem.product.title_fa
}))
)} />
</div>
</div>
)
}
export default OrderItem
@@ -0,0 +1,47 @@
'use client'
import React from 'react'
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import Image from 'next/image'
interface Product {
id: number
image: string
name: string
}
interface OrderProductsProps {
products: Product[]
}
const OrderProducts: React.FC<OrderProductsProps> = ({ products }) => {
return (
<div className='flex-1 flex gap-3 sm:gap-4 lg:border-r lg:border-border lg:pr-5 overflow-hidden min-h-[200px] sm:min-h-[225px]'>
<Swiper
spaceBetween={12}
slidesPerView={'auto'}
className='h-full w-full'
grabCursor={true}
>
{products.map((product) => (
<SwiperSlide key={product.id} className='!w-[180px] sm:!w-[200px] lg:!w-[234px]'>
<div className='flex w-[180px] sm:w-[200px] lg:w-[234px] h-[180px] sm:h-[200px] lg:h-[234px] flex-col p-4 sm:p-5 lg:p-7 items-center rounded-xl gap-2 border border-border'>
<Image
src={product.image}
className='max-w-[100px] sm:max-w-[120px] lg:max-w-[140px] max-h-[100px] sm:max-h-[120px] lg:max-h-[140px] object-center'
alt={product.name}
width={100}
height={100}
/>
<div className='max-w-[160px] truncate h-full sm:max-w-[180px] lg:max-w-[200px] text-center text-xs sm:text-sm font-light mt-2 sm:mt-4'>
{product.name}
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
)
}
export default OrderProducts
@@ -0,0 +1,55 @@
'use client'
import React from 'react'
interface Tab {
key: 'Processing' | 'Delivered' | 'Cancelled'
label: string
count: number
}
interface OrderTabsProps {
activeTab: 'Processing' | 'Delivered' | 'Cancelled'
setActiveTab: (tab: 'Processing' | 'Delivered' | 'Cancelled') => void
tabCounts: {
current: number
delivered: number
returned: number
cancelled: number
}
}
const OrderTabs: React.FC<OrderTabsProps> = ({ activeTab, setActiveTab, tabCounts }) => {
const tabs: Tab[] = [
{ key: 'Processing', label: 'جاری', count: tabCounts.current },
{ key: 'Delivered', label: 'تحویل شده', count: tabCounts.delivered },
// { key: 'Returned', label: 'مرجوع شده', count: tabCounts.returned },
{ key: 'Cancelled', label: 'لغو شده', count: tabCounts.cancelled }
]
return (
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14 border-b border-gray-200'>
<div className='flex gap-2 sm:gap-4 lg:gap-8 overflow-x-auto'>
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`pb-3 sm:pb-4 px-1 sm:px-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === tab.key
? 'border-primary text-primary'
: 'border-transparent text-gray-600 hover:text-gray-800'
}`}
>
<span className='ml-1 sm:ml-2 text-xs sm:text-sm'>{tab.label}</span>
<span className={`inline-flex items-center justify-center min-w-[16px] sm:min-w-[20px] h-4 sm:h-5 px-1 sm:px-1.5 text-xs rounded-full ${activeTab === tab.key
? 'bg-primary text-white'
: 'bg-gray-200 text-gray-600'
}`}>
{tab.count}
</span>
</button>
))}
</div>
</div>
)
}
export default OrderTabs
@@ -0,0 +1,19 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { OrdersResponse, OrderDetailResponse } from "../types/Types";
export const useGetOrders = (activeTab: string) => {
return useQuery<OrdersResponse>({
queryKey: ["orders", activeTab],
queryFn: () => api.getOrders(activeTab),
enabled: !!activeTab,
});
};
export const useGetOrderDetail = (orderId: string) => {
return useQuery<OrderDetailResponse>({
queryKey: ["orderDetail", orderId],
queryFn: () => api.getOrderDetail(orderId),
enabled: !!orderId,
});
};
+51 -118
View File
@@ -1,139 +1,72 @@
'use client'
import Layout from '@/hoc/Layout'
import { NextPage } from 'next'
import React, { useState } from 'react'
import React, { useState, useMemo } from 'react'
import Menu from '../components/Menu'
import { Button } from '@/components/ui/button'
import Image from 'next/image'
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import { useGetOrders } from './hooks/useOrdersData'
import { Order } from './types/Types'
import OrderTabs from './components/OrderTabs'
import OrderItem from './components/OrderItem'
const Orders: NextPage = () => {
const [activeTab, setActiveTab] = useState<'current' | 'delivered' | 'returned' | 'cancelled'>('current')
const mockOrders = [
{
id: '656151',
amount: '18,500,000',
status: 'در انتظار پرداخت',
date: '1401 / 10 / 12',
products: [
{ id: 1, image: 'https://picsum.photos/80/80?random=1', name: 'گوشی موبایل سامسونگ مدل Galaxy S24 Ultra' },
{ id: 2, image: 'https://picsum.photos/80/80?random=2', name: 'لحاف کرسی ستین مدل ساده' },
{ id: 3, image: 'https://picsum.photos/80/80?random=3', name: 'روغنی ستین کد T-2112 طرح گلریز' },
{ id: 4, image: 'https://picsum.photos/80/80?random=4', name: 'شارژر دیواری 65 وات ماگسیونگ' },
{ id: 5, image: 'https://picsum.photos/80/80?random=5', name: 'گوشی موبایل سامسونگ مدل Galaxy S24 Ultra' },
{ id: 6, image: 'https://picsum.photos/80/80?random=6', name: 'لحاف کرسی ستین مدل ساده' },
{ id: 7, image: 'https://picsum.photos/80/80?random=7', name: 'روغنی ستین کد T-2112 طرح گلریز' },
{ id: 8, image: 'https://picsum.photos/80/80?random=8', name: 'شارژر دیواری 65 وات ماگسیونگ' },
]
},
{
id: '846511',
amount: '254,500,000',
status: 'وضعیت سفارش: در حال ارسال',
date: '1401 / 8 / 10',
products: [
{ id: 1, image: 'https://picsum.photos/80/80?random=5', name: 'گوشی موبایل سامسونگ مدل Galaxy S24 Ultra' },
{ id: 2, image: 'https://picsum.photos/80/80?random=6', name: 'لحاف کرسی ستین مدل ساده' },
{ id: 3, image: 'https://picsum.photos/80/80?random=7', name: 'روغنی ستین کد T-2112 طرح گلریز' },
{ id: 4, image: 'https://picsum.photos/80/80?random=8', name: 'شارژر دیواری 65 وات ماگسیونگ' }
]
}
]
const [activeTab, setActiveTab] = useState<'Processing' | 'Delivered' | 'Cancelled'>('Processing')
const { data: ordersData } = useGetOrders(activeTab);
// محاسبه تعداد سفارشات در هر تب از داده‌های اصلی
const tabCounts = useMemo(() => {
if (!ordersData?.results?.orders) return { current: 0, delivered: 0, returned: 0, cancelled: 0 };
return ordersData.results.orders.reduce((acc, order: Order) => {
const status = (order.status || 'pending').toLowerCase();
if (['pending', 'processing', 'shipped'].includes(status)) {
acc.current++;
} else if (status === 'delivered') {
acc.delivered++;
} else if (status === 'returned') {
acc.returned++;
} else if (status === 'cancelled') {
acc.cancelled++;
}
return acc;
}, { current: 0, delivered: 0, returned: 0, cancelled: 0 });
}, [ordersData]);
// تابع تبدیل وضعیت به فارسی
const getStatusText = (status: string) => {
const statusMap: { [key: string]: string } = {
'pending': 'در انتظار پرداخت',
'processing': 'در حال پردازش',
'shipped': 'در حال ارسال',
'delivered': 'تحویل شده',
'returned': 'مرجوع شده',
'cancelled': 'لغو شده'
};
return statusMap[status.toLowerCase()] || status;
};
const tabs = [
{ key: 'current' as const, label: 'جاری', count: 10 },
{ key: 'delivered' as const, label: 'تحویل شده', count: 3 },
{ key: 'returned' as const, label: 'مرجوع شده', count: 3 },
{ key: 'cancelled' as const, label: 'لغو شده', count: 3 }
]
return (
<div className='p-3 sm:p-4 md:p-6'>
<Menu pageActive='orders' />
{/* Tabs */}
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14 border-b border-gray-200'>
<div className='flex gap-2 sm:gap-4 lg:gap-8 overflow-x-auto'>
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`pb-3 sm:pb-4 px-1 sm:px-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === tab.key
? 'border-primary text-primary'
: 'border-transparent text-gray-600 hover:text-gray-800'
}`}
>
<span className='ml-1 sm:ml-2 text-xs sm:text-sm'>{tab.label}</span>
<span className={`inline-flex items-center justify-center min-w-[16px] sm:min-w-[20px] h-4 sm:h-5 px-1 sm:px-1.5 text-xs rounded-full ${activeTab === tab.key
? 'bg-primary text-white'
: 'bg-gray-200 text-gray-600'
}`}>
{tab.count}
</span>
</button>
))}
</div>
</div>
<OrderTabs
activeTab={activeTab}
setActiveTab={setActiveTab}
tabCounts={tabCounts}
/>
{/* Orders List */}
<div className='mt-4 sm:mt-6 space-y-3 sm:space-y-4'>
{mockOrders.map((order) => (
<div key={order.id} className='bg-white border border-[#E0E0E0] rounded-[8px] p-3 sm:p-4 md:p-5'>
<div className='flex flex-col lg:flex-row gap-4 sm:gap-6'>
<div className='lg:max-w-[234px] w-full'>
<div className=''>
<div className='text-xs sm:text-sm text-[#7F7F7F] mb-1'>
شماره سفارش: <span className='text-black'> {order.id}</span>
</div>
<div className='text-xs sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
مبلغ: <span className='text-black'> {order.amount} تومان</span>
</div>
</div>
<div className='mb-4 sm:mb-6 mt-3 sm:mt-4'>
<div className='text-xs sm:text-sm text-[#7F7F7F]'>
وضعیت سفارش : <span className='text-black'>{order.status}</span>
</div>
<div className='text-xs sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
تاریخ ثبت سفارش: <span className='text-black'> {order.date}</span>
</div>
</div>
<div className='flex flex-col sm:flex-row gap-2 sm:gap-3'>
<Button
variant="outline"
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
>
جزئیات سفارش
</Button>
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
پرداخت
</Button>
</div>
</div>
<div className='flex-1 flex gap-3 sm:gap-4 lg:border-r lg:border-border lg:pr-5 overflow-hidden min-h-[200px] sm:min-h-[225px]'>
<Swiper
spaceBetween={12}
slidesPerView={'auto'}
className='h-full w-full'
grabCursor={true}
>
{order.products.map((product) => (
<SwiperSlide key={product.id} className='!w-[180px] sm:!w-[200px] lg:!w-[234px]'>
<div className='flex w-[180px] sm:w-[200px] lg:w-[234px] h-[180px] sm:h-[200px] lg:h-[224px] flex-col p-4 sm:p-5 lg:p-7 items-center rounded-xl gap-2 border border-border'>
<Image src={product.image} className='max-w-[100px] sm:max-w-[120px] lg:max-w-[140px] max-h-[100px] sm:max-h-[120px] lg:max-h-[140px] object-center' alt={product.name} width={100} height={100} />
<div className='max-w-[160px] sm:max-w-[180px] lg:max-w-[200px] text-center text-xs sm:text-sm font-light mt-2 sm:mt-4'>{product.name}</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
{ordersData?.results?.orders.map((order) => (
<OrderItem
key={order._id.toString()}
order={order}
getStatusText={getStatusText}
/>
))}
</div>
</div>
+33
View File
@@ -0,0 +1,33 @@
import axios from "@/config/axios";
import { OrdersResponse, OrderDetailResponse } from "../types/Types";
export const getOrders = async (activeTab: string): Promise<OrdersResponse> => {
const { data } = await axios.get(`/user/profile/orders?status=${activeTab}`);
return data;
};
export const getOrderDetail = async (
orderId: string
): Promise<OrderDetailResponse> => {
const { data } = await axios.get(`/order/${orderId}/user`);
return data;
};
export const cancelOrder = async (
orderId: string,
cancelData: {
reason: string;
count: number;
description: string;
}
) => {
const { data } = await axios.post(`/order/${orderId}/cancel`, cancelData);
return data;
};
export const getInvoice = async (orderId: string) => {
const { data } = await axios.get(`/order/${orderId}/invoice`, {
responseType: "blob",
});
return data;
};
+293
View File
@@ -0,0 +1,293 @@
export interface ProductImage {
cover: string;
list: string[];
}
export interface ProductSpecification {
title: string;
values: string[];
}
export interface Brand {
_id: string;
}
export interface Category {
_id: string;
}
export interface Variant {
_id: string;
}
export interface Product {
_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: ProductImage;
isFake: string;
voice: string | null;
specifications: ProductSpecification[];
brand: Brand;
category: Category;
variants: Variant[];
}
export interface Price {
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 interface Shop {
_id: string;
}
export interface ProductVariant {
_id: string;
market_status: string;
price: Price;
stock: number;
postingTime: number;
isFreeShip: boolean;
isWholeSale: boolean;
shop: Shop;
warranty: number;
meterage?: number;
size?: number;
}
export interface OrderItem {
product: Product;
variant: ProductVariant;
quantity: number;
cancelled_quantity: number;
returned_quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shipmentCost: number;
}
export interface PaymentMethod {
_id: string;
title_fa: string;
title_en: string;
description: string;
provider: string;
isActive: boolean;
type: string;
}
export interface PriceDetails {
shipping_cost: number;
process_cost: number;
total_retail_price: number;
total_payable_price: number;
total_discount: number;
coupon_discount: number;
}
export interface Payment {
_id: string;
transaction_id: string | null;
payment_method: PaymentMethod;
priceDetails: PriceDetails;
totalPrice: number;
paymentStatus: string;
createdAt: string;
}
export interface ShipmentAddress {
address: string;
city: string;
province: string;
plaque: string;
postalCode: string;
phone: string;
}
export interface MappedOrder {
orderId: number;
orderItems: OrderItem[];
payment: Payment;
shipmentAddress: ShipmentAddress;
orderStatus: string;
createdAt: string;
}
export interface ShopDetails {
_id: string;
shopName: string;
shopCode: number;
owner: string;
shopDescription: string;
telephoneNumber: string;
isChatActive: boolean;
shopPostalCode: string;
logo: string;
}
export interface ShipmentItem {
product: Product;
variant: ProductVariant;
quantity: number;
cancelled_quantity: number;
returned_quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shipmentCost: number;
}
export interface OrderItemDetails {
_id: string;
shop: ShopDetails;
shipmentItems: ShipmentItem[];
}
export interface Shipper {
_id: number;
name: string;
description: string;
deliveryTime: number;
deliveryType: string;
}
export interface Order {
_id: number;
payment: Payment;
orderItems: OrderItemDetails[];
shipper: Shipper;
trackCode: string | null;
itemsCount: number;
totalPaymentPrice: number;
totalRetailPrice: number;
totalSellingPrice: number;
totalShipmentCost: number;
postingDate: string | null;
postingReceipt: string | null;
status: string;
}
export interface User {
_id: string;
fullName: string;
phoneNumber: string;
dateOfBirth: string | null;
address: {
_id: string;
};
nationalCode: string | null;
homeNumber: string | null;
}
export interface Warranty {
_id: number;
duration: string;
logoUrl: string;
name: string;
}
export interface Meterage {
_id: number;
value: string;
}
export interface Size {
_id: number;
value: string;
}
export interface ProductVariantDetail {
_id: string;
market_status: string;
price: Price;
stock: number;
postingTime: number;
isFreeShip: boolean;
isWholeSale: boolean;
shop: ShopDetails;
warranty: Warranty;
meterage?: Meterage;
size?: Size;
}
export interface ShipmentItemDetail {
_id: string;
product: Product;
variant: ProductVariantDetail;
quantity: number;
cancelled_quantity: number;
returned_quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shipmentCost: number;
}
export interface OrderItemDetail {
_id: string;
order_id: number;
shop: ShopDetails;
shipmentItems: ShipmentItemDetail[];
shipper: Shipper;
trackCode: string | null;
itemsCount: number;
totalPaymentPrice: number;
totalRetailPrice: number;
totalSellingPrice: number;
totalShipmentCost: number;
postingDate: string | null;
postingReceipt: string | null;
status: string;
}
export interface CancelReason {
_id: string;
title: string;
}
export interface OrderDetail {
_id: number;
user: User;
payment: Payment;
orderItems: OrderItemDetail[];
shipmentAddress: ShipmentAddress;
orderStatus: string;
createdAt: string;
cancelReasons?: CancelReason[];
}
export interface OrderDetailResponse {
status: number;
success: boolean;
results: {
order: OrderDetail;
};
}
export interface OrdersResponse {
status: number;
success: boolean;
results: {
mappedOrders: MappedOrder[];
orders: Order[];
shipmentAddress: ShipmentAddress;
orderStatus: string;
createdAt: string;
};
}