order detail + change status in all state

This commit is contained in:
hamid zarghami
2025-12-20 14:33:19 +03:30
parent 566c4cb97b
commit de8815f0d8
13 changed files with 1065 additions and 300 deletions
@@ -0,0 +1,169 @@
import { Location, User, Call, Cake, Heart } from 'iconsax-react'
import type { FC } from 'react'
import type { Order } from '../types/Types'
import { formatOptionalDate, formatFaNumber } from '@/helpers/func'
interface CustomerInfoProps {
order: Order
}
const CustomerInfo: FC<CustomerInfoProps> = ({ order }) => {
const customerName = [order.user?.firstName, order.user?.lastName].filter(Boolean).join(' ') || '-'
const fullAddress = order.userAddress
? `${order.userAddress.province || ''}، ${order.userAddress.city || ''}، ${order.userAddress.address || ''}، کد پستی: ${order.userAddress.postalCode || '-'}`.trim()
: '-'
return (
<div className='w-full bg-white rounded-4xl p-8'>
<div className='flex justify-between items-center'>
<div className='text-lg font-light flex items-center gap-2'>
<User color='#000' size={20} />
اطلاعات مشتری
</div>
</div>
<div className='mt-8 flex flex-wrap gap-y-7 gap-20 text-[13px] font-light border-b border-dashed border-border pb-8'>
<div className='flex gap-2'>
<div className='text-description'>
شماره سفارش:
</div>
<div>{formatFaNumber(order.orderNumber)}</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
نام مشتری:
</div>
<div className='flex items-center gap-2'>
{order.user?.avatarUrl && (
<img
src={order.user.avatarUrl}
alt={customerName}
className='w-8 h-8 rounded-full object-cover'
/>
)}
<span>{customerName}</span>
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>شماره تماس:</div>
<div className='flex items-center gap-1'>
<Call size={14} />
{order.user?.phone || '-'}
</div>
</div>
{order.user?.birthDate && (
<div className='flex gap-2'>
<div className='text-description'>تاریخ تولد:</div>
<div className='flex items-center gap-1'>
<Cake size={14} />
{formatOptionalDate(order.user.birthDate, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})}
</div>
</div>
)}
{order.user?.marriageDate && (
<div className='flex gap-2'>
<div className='text-description'>تاریخ ازدواج:</div>
<div className='flex items-center gap-1'>
<Heart size={14} />
{formatOptionalDate(order.user.marriageDate, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})}
</div>
</div>
)}
<div className='flex gap-2'>
<div className='text-description'>جنسیت:</div>
<div>{order.user?.gender ? 'مرد' : 'زن'}</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>وضعیت:</div>
<div className={`px-2 py-1 rounded text-xs ${order.user?.isActive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{order.user?.isActive ? 'فعال' : 'غیرفعال'}
</div>
</div>
{order.user?.referrer && (
<div className='flex gap-2'>
<div className='text-description'>معرف:</div>
<div>{order.user.referrer}</div>
</div>
)}
</div>
{order.userAddress && (
<div className='mt-8 space-y-4 text-[13px] font-light'>
<div className='flex gap-2 items-start'>
<div className='text-description flex items-center gap-1'>
<Location size={14} />
آدرس کامل:
</div>
<div className='flex-1'>{fullAddress}</div>
</div>
{order.userAddress.fullName && (
<div className='flex gap-2'>
<div className='text-description'>نام گیرنده:</div>
<div>{order.userAddress.fullName}</div>
</div>
)}
<div className='flex gap-2 items-center'>
<div className='text-description'>شماره تماس گیرنده:</div>
<div>{order.userAddress.phone || '-'}</div>
</div>
{order.userAddress?.latitude && order.userAddress?.longitude && (
<a
href={`https://www.google.com/maps?q=${order.userAddress.latitude},${order.userAddress.longitude}`}
target='_blank'
rel='noopener noreferrer'
className='flex gap-1 items-center cursor-pointer text-blue-600 hover:text-blue-800'
>
<Location size={17} color='#0047FF' />
<div className='text-[13px]'>مشاهده روی نقشه</div>
</a>
)}
</div>
)}
{order.carAddress && (
<div className={`${order.userAddress ? 'mt-6 pt-6 border-t' : 'mt-8'} border-dashed border-border`}>
<div className='text-description mb-4'>اطلاعات خودرو:</div>
<div className='flex flex-wrap gap-y-4 gap-8 text-[13px]'>
<div className='flex gap-2'>
<div className='text-description'>رنگ:</div>
<div>{order.carAddress.carColor}</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>مدل:</div>
<div>{order.carAddress.carModel}</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>پلاک:</div>
<div>{order.carAddress.plateNumber}</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>شماره تماس:</div>
<div>{order.carAddress.phone}</div>
</div>
</div>
</div>
)}
</div>
)
}
export default CustomerInfo
@@ -0,0 +1,100 @@
import Button from '@/components/Button'
import type { FC } from 'react'
interface OrderActionsProps {
showVerifyCashPaymentButton: boolean
canConfirm: boolean
showSendToKitchenButton: boolean
showCancelButton: boolean
isPreparing: boolean
isCourierDelivery: boolean
isCanceled: boolean
isVerifyingPayment: boolean
isChangingStatus: boolean
onVerifyCashPayment: () => void
onConfirmOrder: () => void
onSendToKitchen: () => void
onDeliverToCourier: () => void
onOrderReady: () => void
onCancelOrder: () => void
}
const OrderActions: FC<OrderActionsProps> = ({
showVerifyCashPaymentButton,
canConfirm,
showSendToKitchenButton,
showCancelButton,
isPreparing,
isCourierDelivery,
isCanceled,
isVerifyingPayment,
isChangingStatus,
onVerifyCashPayment,
onConfirmOrder,
onSendToKitchen,
onDeliverToCourier,
onOrderReady,
onCancelOrder
}) => {
if (isCanceled) return null
return (
<div className='mt-6 flex flex-col gap-3'>
{showVerifyCashPaymentButton && (
<Button
label='تایید پرداخت'
onClick={onVerifyCashPayment}
isloading={isVerifyingPayment}
className='bg-green-600 hover:bg-green-700'
/>
)}
{canConfirm && (
<Button
label='تایید سفارش'
onClick={onConfirmOrder}
isloading={isChangingStatus}
/>
)}
{showSendToKitchenButton && (
<Button
label='ارسال به آشپزخانه'
onClick={onSendToKitchen}
isloading={isChangingStatus}
className='bg-orange-600 hover:bg-orange-700'
/>
)}
{isPreparing && isCourierDelivery && (
<Button
label='تحویل به پیک'
onClick={onDeliverToCourier}
isloading={isChangingStatus}
className='bg-blue-600 hover:bg-blue-700'
/>
)}
{isPreparing && !isCourierDelivery && (
<Button
label='غذا آماده است'
onClick={onOrderReady}
isloading={isChangingStatus}
className='bg-green-600 hover:bg-green-700'
/>
)}
{showCancelButton && (
<Button
label='لغو سفارش'
onClick={onCancelOrder}
isloading={isChangingStatus}
className='bg-red-600 hover:bg-red-700'
/>
)}
</div>
)
}
export default OrderActions
@@ -0,0 +1,217 @@
import Table from '@/components/Table'
import { ShoppingCart, Calendar, Clock, Tag } from 'iconsax-react'
import type { FC } from 'react'
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
import type { Order, OrderItem as OrderItemType } from '../types/Types'
import { formatOptionalDate, formatPrice, formatFaNumber } from '@/helpers/func'
import { useTranslation } from 'react-i18next'
import OrderSummary from './OrderSummary'
interface OrderItemTable extends RowDataType {
image: string
title: string
quantity: number
unitPrice: number
discount: number
amount: string
}
interface OrderItemsDetailsProps {
order: Order
getStatusColor: (status: string) => string
}
const OrderItemsDetails: FC<OrderItemsDetailsProps> = ({ order, getStatusColor }) => {
const { t } = useTranslation('global')
const orderItems: OrderItemTable[] = order.items.map((item: OrderItemType) => ({
id: item.id,
image: item.food?.images?.[0] || '',
title: item.food?.title || '-',
quantity: item.quantity,
unitPrice: item.unitPrice,
discount: item.discount,
amount: formatPrice(item.totalPrice)
}))
const columns: ColumnType<OrderItemTable>[] = [
{
title: 'تصویر',
key: 'image',
render: (item) => (
<div>
{item.image ? (
<img
src={item.image}
alt={item.title}
className='w-12 h-12 rounded-lg object-cover'
/>
) : (
<div className='w-12 h-12 rounded-lg bg-gray-200 flex items-center justify-center'>
<span className='text-xs text-gray-400'>بدون تصویر</span>
</div>
)}
</div>
)
},
{
title: 'عنوان',
key: 'title',
align: 'right'
},
{
title: 'تعداد',
key: 'quantity',
align: 'center',
render: (item) => formatFaNumber(item.quantity)
},
{
title: 'قیمت واحد',
key: 'unitPrice',
align: 'right',
render: (item) => formatPrice(item.unitPrice)
},
{
title: 'تخفیف',
key: 'discount',
align: 'right',
render: (item) => item.discount > 0 ? formatPrice(item.discount) : '-'
},
{
title: 'مبلغ کل',
key: 'amount',
align: 'right'
}
]
return (
<div className='w-full bg-white rounded-4xl p-8'>
<div className='text-lg font-light mb-6 flex items-center gap-2'>
<ShoppingCart color='#000' size={20} />
جزییات سفارش
</div>
<div className='mb-6 flex flex-wrap gap-4 text-[13px] font-light'>
<div className='flex gap-2'>
<div className='text-description'>زمان سفارش:</div>
<div className='flex items-center gap-1'>
<Clock size={14} />
{formatOptionalDate(order.createdAt, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>زمان بهروزرسانی:</div>
<div>
{formatOptionalDate(order.updatedAt, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>نوع تحویل:</div>
<div>{order.deliveryMethod?.description || '-'}</div>
</div>
{order.tableNumber && (
<div className='flex gap-2'>
<div className='text-description'>شماره میز:</div>
<div>{order.tableNumber}</div>
</div>
)}
{order.description && (
<div className='w-full'>
<div className='text-description mb-2'>توضیحات سفارش:</div>
<div className='text-gray-700 bg-gray-50 p-3 rounded-lg'>{order.description}</div>
</div>
)}
</div>
<Table
columns={columns}
data={orderItems}
showHeader={true}
className='mt-0'
/>
<OrderSummary />
{order.couponDetail && (
<div className='mt-6 pt-6 border-t border-dashed border-border'>
<div className='flex items-center gap-2 mb-4'>
<Tag size={16} />
<div className='text-description'>اطلاعات کوپن:</div>
</div>
<div className='bg-blue-50 p-4 rounded-lg space-y-2 text-[13px]'>
<div className='flex justify-between'>
<span className='text-description'>نام کوپن:</span>
<span className='font-medium'>{order.couponDetail.couponName}</span>
</div>
<div className='flex justify-between'>
<span className='text-description'>کد کوپن:</span>
<span className='font-mono bg-white px-2 py-1 rounded'>{order.couponDetail.couponCode}</span>
</div>
<div className='flex justify-between'>
<span className='text-description'>نوع:</span>
<span>{order.couponDetail.type === 'PERCENTAGE' ? 'درصدی' : 'مبلغی'}</span>
</div>
<div className='flex justify-between'>
<span className='text-description'>مقدار:</span>
<span>{order.couponDetail.value}{order.couponDetail.type === 'PERCENTAGE' ? '%' : ' تومان'}</span>
</div>
<div className='flex justify-between'>
<span className='text-description'>حداکثر تخفیف:</span>
<span>{formatPrice(order.couponDetail.maxDiscount)}</span>
</div>
</div>
</div>
)}
{order.history && order.history.length > 0 && (
<div className='mt-6 pt-6 border-t border-dashed border-border'>
<div className='flex items-center gap-2 mb-4'>
<Calendar color='#000' size={16} />
<div className='text-description'>تاریخچه سفارش:</div>
</div>
<div className='space-y-3'>
{order.history.map((historyItem, index) => (
<div key={index} className='flex justify-between items-center text-[13px] bg-gray-50 p-3 rounded-lg'>
<div className='flex items-center gap-2'>
<div
className='size-2 rounded-full'
style={{ backgroundColor: getStatusColor(historyItem.status) }}
></div>
<span>{t(`order_status.${historyItem.status}`) || historyItem.status}</span>
</div>
<div className='text-description'>
{formatOptionalDate(historyItem.changedAt, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
export default OrderItemsDetails
@@ -0,0 +1,90 @@
// import type { FC } from 'react'
// import type { Order } from '../types/Types'
// import { formatPrice, formatFaNumber } from '@/helpers/func'
// interface OrderSummaryProps {
// order: Order
// variant?: 'default' | 'compact'
// }
// const OrderSummary: FC<OrderSummaryProps> = ({ order, variant = 'default' }) => {
// const isCompact = variant === 'compact'
// return (
// <div className={`space-y-3 ${isCompact ? '' : 'text-[13px]'}`}>
// <div className={`flex justify-between items-center ${isCompact ? 'border-b border-border border-dashed pb-4' : 'mt-4 border-b border-border border-dashed pb-4'}`}>
// <div className='text-description'>جمع آیتم‌ها</div>
// <div>{formatPrice(order.subTotal)}</div>
// </div>
// {order.totalDiscount > 0 && (
// <div className='space-y-3'>
// {order.itemsDiscount > 0 && (
// <div className='flex justify-between items-center'>
// <div className='text-description font-medium'>تخفیف‌ها:</div>
// <div className='text-green-600 font-medium'>-{formatPrice(order.itemsDiscount)}</div>
// </div>
// )}
// {order.couponDiscount > 0 && (
// <div className='flex justify-between items-center'>
// <div className='text-description font-medium'>تخفیف کوپن:</div>
// <div className='text-green-600 font-medium'>-{formatPrice(order.couponDiscount)}</div>
// </div>
// )}
// <div className='border-t border-border border-dashed pt-3 mt-2'>
// <div className='flex justify-between items-center'>
// <div className='font-bold'>جمع تخفیف‌ها:</div>
// <div className='font-bold text-green-600'>-{formatPrice(order.totalDiscount)}</div>
// </div>
// </div>
// </div>
// )}
// {order.deliveryFee > 0 && (
// <div className='flex justify-between items-center pt-2'>
// <div className='text-description'>هزینه ارسال:</div>
// <div>{formatPrice(order.deliveryFee)}</div>
// </div>
// )}
// {order.tax > 0 && (
// <div className='flex justify-between items-center pt-2'>
// <div className='text-description'>مالیات:</div>
// <div>{formatPrice(order.tax)}</div>
// </div>
// )}
// {isCompact && (
// <>
// {order.deliveryMethod?.minOrderPrice && (
// <div className='flex justify-between items-center border-b border-border border-dashed pb-4'>
// <div className='text-description'>حداقل سفارش:</div>
// <div>{formatPrice(order.deliveryMethod.minOrderPrice)}</div>
// </div>
// )}
// <div className='flex justify-between items-center border-b border-border border-dashed pb-4'>
// <div className='text-description'>تعداد آیتم‌ها:</div>
// <div>{formatFaNumber(order.totalItems)}</div>
// </div>
// </>
// )}
// <div className={`flex font-bold justify-between items-center ${isCompact ? '' : 'h-14 border-t-2 border-gray-300 pt-3 mt-3'}`}>
// <div>جمع کل:</div>
// <div className={`${isCompact ? 'text-base' : 'text-lg'}`}>{formatPrice(order.total)}</div>
// </div>
// </div>
// )
// }
// export default OrderSummary
const OrderSummary = () => {
return null
}
export default OrderSummary
@@ -69,14 +69,14 @@ export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnT
key: 'deliveryMethod',
title: 'نوع تحویل',
render: (item: Order) => {
return item.deliveryMethod?.title || '-'
return item.deliveryMethod?.description || '-'
}
},
{
key: 'paymentMethod',
title: 'نوع پرداخت',
render: (item: Order) => {
return item.paymentMethod?.title || '-'
return item.paymentMethod?.description || '-'
}
},
{
+150
View File
@@ -0,0 +1,150 @@
import type { FC } from 'react'
import type { Order } from '../types/Types'
import { PaymentStatusEnum } from '../enum/Enum'
import { OrderStatus } from '../enum/Enum'
import { formatPrice, formatFaNumber } from '@/helpers/func'
interface PaymentInfoProps {
order: Order
getPaymentStatusColor: (status: string) => string
getPaymentStatusTextColor: (status: string) => string
getPaymentStatusText: (status: string) => string
}
const PaymentInfo: FC<PaymentInfoProps> = ({
order,
getPaymentStatusColor,
getPaymentStatusTextColor,
getPaymentStatusText
}) => {
const paymentStatus = order.paymentStatus || (order.status === OrderStatus.PAID ? PaymentStatusEnum.Paid : PaymentStatusEnum.Pending)
return (
<div className='w-[330px] h-fit bg-white rounded-4xl p-8'>
<div className='flex justify-between items-center'>
<div className='font-light'>
اطلاعات پرداخت
</div>
<div className='flex gap-1.5 items-center'>
<div className={`size-2 rounded-full ${getPaymentStatusColor(paymentStatus)}`}></div>
<div className={`text-[13px] ${getPaymentStatusTextColor(paymentStatus)}`}>
{getPaymentStatusText(paymentStatus)}
</div>
</div>
</div>
<div className='mt-10'>
<div className='flex text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
نوع پرداخت
</div>
<div>{order.paymentMethod?.description || '-'}</div>
</div>
{order.paymentMethod?.gateway && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
درگاه پرداخت
</div>
<div>{order.paymentMethod.gateway}</div>
</div>
)}
{order.paymentMethod?.merchantId && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
شناسه مرchant
</div>
<div className='font-mono text-xs'>{order.paymentMethod.merchantId}</div>
</div>
)}
{order.paymentId && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
شناسه پرداخت
</div>
<div className='font-mono text-xs'>{order.paymentId}</div>
</div>
)}
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
جمع آیتمها
</div>
<div>{formatPrice(order.subTotal)}</div>
</div>
{order.itemsDiscount > 0 && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
تخفیف آیتمها
</div>
<div className='text-green-600'>-{formatPrice(order.itemsDiscount)}</div>
</div>
)}
{order.couponDiscount > 0 && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
تخفیف کوپن
</div>
<div className='text-green-600'>-{formatPrice(order.couponDiscount)}</div>
</div>
)}
{order.totalDiscount > 0 && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description font-medium'>
جمع تخفیفها
</div>
<div className='text-green-600 font-medium'>-{formatPrice(order.totalDiscount)}</div>
</div>
)}
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
هزینه ارسال
</div>
<div>{formatPrice(order.deliveryFee)}</div>
</div>
{order.deliveryMethod?.minOrderPrice && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
حداقل سفارش
</div>
<div>{formatPrice(order.deliveryMethod.minOrderPrice)}</div>
</div>
)}
{order.tax > 0 && (
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
مالیات
</div>
<div>{formatPrice(order.tax)}</div>
</div>
)}
<div className='flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4'>
<div className='text-description'>
تعداد آیتمها
</div>
<div>{formatFaNumber(order.totalItems)}</div>
</div>
<div className='flex mt-4 text-[13px] font-bold justify-between items-center'>
<div>
مبلغ کل
</div>
<div>{formatPrice(order.total)}</div>
</div>
</div>
</div>
)
}
export default PaymentInfo