This commit is contained in:
+166
-114
@@ -5,6 +5,9 @@ import {
|
|||||||
useChangeOrderStatus,
|
useChangeOrderStatus,
|
||||||
useRefundPayment,
|
useRefundPayment,
|
||||||
useUpdateOrderFees,
|
useUpdateOrderFees,
|
||||||
|
useAddOrderItem,
|
||||||
|
useUpdateOrderItemQuantity,
|
||||||
|
useRemoveOrderItem,
|
||||||
} from './hooks/useOrderData'
|
} from './hooks/useOrderData'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { formatFaNumber } from '@/helpers/func'
|
import { formatFaNumber } from '@/helpers/func'
|
||||||
@@ -14,10 +17,12 @@ import { DeliveryMethodEnum } from '@/pages/shipmentMethod/enum/Enum'
|
|||||||
import ModalConfrim from '@/components/ModalConfrim'
|
import ModalConfrim from '@/components/ModalConfrim'
|
||||||
import CustomerInfo from './components/CustomerInfo'
|
import CustomerInfo from './components/CustomerInfo'
|
||||||
import OrderItemsDetails from './components/OrderItemsDetails'
|
import OrderItemsDetails from './components/OrderItemsDetails'
|
||||||
|
import PaymentsList from './components/PaymentsList'
|
||||||
import PaymentInfo from './components/PaymentInfo'
|
import PaymentInfo from './components/PaymentInfo'
|
||||||
import PaymentVerificationBox from './components/PaymentVerificationBox'
|
|
||||||
import OrderActions from './components/OrderActions'
|
import OrderActions from './components/OrderActions'
|
||||||
import UpdateOrderFeesModal from './components/UpdateOrderFeesModal'
|
import UpdateOrderFeesModal from './components/UpdateOrderFeesModal'
|
||||||
|
import AddOrderItemModal from './components/AddOrderItemModal'
|
||||||
|
import RefundPaymentModal from './components/RefundPaymentModal'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { printOrderReceipt } from './print/orderReceiptPrint'
|
import { printOrderReceipt } from './print/orderReceiptPrint'
|
||||||
@@ -30,53 +35,32 @@ const OrderDetails: FC = () => {
|
|||||||
const { mutate: changeOrderStatus, isPending: isChangingStatus } = useChangeOrderStatus()
|
const { mutate: changeOrderStatus, isPending: isChangingStatus } = useChangeOrderStatus()
|
||||||
const { mutate: refundPayment, isPending: isRefunding } = useRefundPayment()
|
const { mutate: refundPayment, isPending: isRefunding } = useRefundPayment()
|
||||||
const { mutate: updateOrderFees, isPending: isUpdatingFees } = useUpdateOrderFees()
|
const { mutate: updateOrderFees, isPending: isUpdatingFees } = useUpdateOrderFees()
|
||||||
|
const { mutate: addOrderItem, isPending: isAddingItem } = useAddOrderItem()
|
||||||
|
const { mutate: updateOrderItemQuantity, isPending: isUpdatingItemQuantity } = useUpdateOrderItemQuantity()
|
||||||
|
const { mutate: removeOrderItem, isPending: isRemovingItem } = useRemoveOrderItem()
|
||||||
|
|
||||||
const [showCancelModal, setShowCancelModal] = useState(false)
|
const [showCancelModal, setShowCancelModal] = useState(false)
|
||||||
const [showRefundModal, setShowRefundModal] = useState(false)
|
const [refundModal, setRefundModal] = useState<{ defaultAmount: number; maxAmount: number } | null>(null)
|
||||||
const [showEditFeesModal, setShowEditFeesModal] = useState(false)
|
const [showEditFeesModal, setShowEditFeesModal] = useState(false)
|
||||||
|
const [showAddItemModal, setShowAddItemModal] = useState(false)
|
||||||
|
const [itemIdToRemove, setItemIdToRemove] = useState<string | null>(null)
|
||||||
|
const [verifyingPaymentId, setVerifyingPaymentId] = useState<string | null>(null)
|
||||||
|
|
||||||
const order = orderData?.data
|
const order = orderData?.data
|
||||||
|
|
||||||
const getStatusColor = (status: string): string => {
|
const getStatusColor = (status: string): string => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
'pendingPayment': '#FFEECC',
|
pendingPayment: '#FFEECC',
|
||||||
'paid': '#E3F2FD',
|
paid: '#E3F2FD',
|
||||||
'preparing': '#FFF3E0',
|
preparing: '#FFF3E0',
|
||||||
'ready': '#E3F2FD',
|
ready: '#E3F2FD',
|
||||||
'shipped': '#E3F2FD',
|
shipped: '#E3F2FD',
|
||||||
'completed': '#E8F5E9',
|
completed: '#E8F5E9',
|
||||||
'canceled': '#FFEBEE',
|
canceled: '#FFEBEE',
|
||||||
}
|
}
|
||||||
return colorMap[status] || '#FFEECC'
|
return colorMap[status] || '#FFEECC'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentStatusColor = (status: string): string => {
|
|
||||||
const colorMap: Record<string, string> = {
|
|
||||||
[PaymentStatusEnum.Paid]: 'bg-green-500',
|
|
||||||
[PaymentStatusEnum.Pending]: 'bg-yellow-500',
|
|
||||||
[PaymentStatusEnum.Failed]: 'bg-red-500',
|
|
||||||
}
|
|
||||||
return colorMap[status] || 'bg-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPaymentStatusTextColor = (status: string): string => {
|
|
||||||
const colorMap: Record<string, string> = {
|
|
||||||
[PaymentStatusEnum.Paid]: 'text-green-500',
|
|
||||||
[PaymentStatusEnum.Pending]: 'text-yellow-500',
|
|
||||||
[PaymentStatusEnum.Failed]: 'text-red-500',
|
|
||||||
}
|
|
||||||
return colorMap[status] || 'text-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPaymentStatusText = (status: string): string => {
|
|
||||||
const textMap: Record<string, string> = {
|
|
||||||
[PaymentStatusEnum.Paid]: 'پرداخت شده',
|
|
||||||
[PaymentStatusEnum.Pending]: 'در انتظار پرداخت',
|
|
||||||
[PaymentStatusEnum.Failed]: 'پرداخت ناموفق',
|
|
||||||
}
|
|
||||||
return textMap[status] || status
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-5 flex items-center justify-center h-64'>
|
<div className='mt-5 flex items-center justify-center h-64'>
|
||||||
@@ -93,113 +77,113 @@ const OrderDetails: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// منطق نمایش دکمهها
|
|
||||||
const isCashPayment = order.paymentMethod?.method === PaymentMethodEnum.Cash
|
const isCashPayment = order.paymentMethod?.method === PaymentMethodEnum.Cash
|
||||||
const isCreditCardPayment = order.paymentMethod?.method === PaymentMethodEnum.CreditCard
|
const isOrderPaid = Number(order.paidAmount ?? 0) > 0 || order.payments?.some(
|
||||||
const isPendingPayment = order.status === OrderStatus.NEW
|
(payment) => payment.status === PaymentStatusEnum.Paid
|
||||||
const isOrderPaid = order.payments?.some(payment => payment.status === PaymentStatusEnum.Paid)
|
)
|
||||||
const isPreparing = order.status === OrderStatus.PREPARING
|
const isPreparing = order.status === OrderStatus.PREPARING
|
||||||
const isCanceled = order.status === OrderStatus.CANCELED
|
const isCanceled = order.status === OrderStatus.CANCELED
|
||||||
const isCompleted = order.status === OrderStatus.COMPLETED
|
const isCompleted = order.status === OrderStatus.COMPLETED
|
||||||
|
const isPendingPayment = order.status === OrderStatus.NEW
|
||||||
const canEditFees = !isCanceled && !isCompleted
|
const canEditFees = !isCanceled && !isCompleted
|
||||||
|
const canEditItems = !isCanceled && !isCompleted
|
||||||
|
const isUpdatingItem = isAddingItem || isUpdatingItemQuantity || isRemovingItem
|
||||||
const canCompleteOrder = [
|
const canCompleteOrder = [
|
||||||
OrderStatus.DELIVERED_TO_WAITER,
|
OrderStatus.DELIVERED_TO_WAITER,
|
||||||
OrderStatus.DELIVERED_TO_RECEPTIONIST,
|
OrderStatus.DELIVERED_TO_RECEPTIONIST,
|
||||||
OrderStatus.SHIPPED,
|
OrderStatus.SHIPPED,
|
||||||
].includes(order.status as OrderStatus)
|
].includes(order.status as OrderStatus)
|
||||||
// بررسی وضعیت پرداخت از payments array یا paymentStatus
|
|
||||||
// ابتدا از paymentStatus استفاده میکنیم، اگر نبود از اولین payment در payments array استفاده میکنیم
|
|
||||||
const paymentStatus = order.paymentStatus || order.payments?.[0]?.status
|
|
||||||
// بررسی وضعیت pending (هم enum و هم string literal)
|
|
||||||
const isPaymentPending = paymentStatus === PaymentStatusEnum.Pending || paymentStatus === 'pending'
|
|
||||||
const isCourierDelivery = order.deliveryMethod?.method === DeliveryMethodEnum.DeliveryCourier
|
const isCourierDelivery = order.deliveryMethod?.method === DeliveryMethodEnum.DeliveryCourier
|
||||||
const isDineInOrCarDelivery = order.deliveryMethod?.method === DeliveryMethodEnum.DineIn || order.deliveryMethod?.method === DeliveryMethodEnum.DeliveryCar
|
const isDineInOrCarDelivery =
|
||||||
|
order.deliveryMethod?.method === DeliveryMethodEnum.DineIn ||
|
||||||
|
order.deliveryMethod?.method === DeliveryMethodEnum.DeliveryCar
|
||||||
const isCustomerPickup = order.deliveryMethod?.method === DeliveryMethodEnum.CustomerPickup
|
const isCustomerPickup = order.deliveryMethod?.method === DeliveryMethodEnum.CustomerPickup
|
||||||
|
|
||||||
const payment = order.payments?.[0]
|
|
||||||
const hasPaymentDescription = Boolean(payment?.description)
|
|
||||||
const hasPaymentAttachments = Boolean(payment?.attachments && payment.attachments.length > 0)
|
|
||||||
const showPaymentReceiptBox = isCreditCardPayment && (hasPaymentDescription || hasPaymentAttachments)
|
|
||||||
const showVerifyCashPaymentButton = isCashPayment && isPaymentPending && !isCanceled
|
|
||||||
const showVerifyPaymentButton = (isCashPayment || isCreditCardPayment) && isPaymentPending && !isCanceled
|
|
||||||
const showPaymentVerificationBox = showPaymentReceiptBox || showVerifyCashPaymentButton
|
|
||||||
|
|
||||||
// نمایش دکمه ارسال به آشپزخانه
|
|
||||||
// برای pendingPayment با پرداخت نقدی یا برای paid
|
|
||||||
const showSendToKitchenButton = (isPendingPayment && isCashPayment) || isOrderPaid
|
const showSendToKitchenButton = (isPendingPayment && isCashPayment) || isOrderPaid
|
||||||
|
|
||||||
// نمایش دکمه کنسل
|
|
||||||
// همیشه نمایش داده میشود مگر اینکه سفارش قبلاً کنسل شده باشد
|
|
||||||
const showCancelButton = !isCanceled
|
const showCancelButton = !isCanceled
|
||||||
|
|
||||||
const handleVerifyPayment = () => {
|
const handleVerifyPayment = (paymentId: string) => {
|
||||||
const paymentId = order.paymentId || order.payments?.[0]?.id
|
setVerifyingPaymentId(paymentId)
|
||||||
if (paymentId) {
|
|
||||||
verifyPayment(paymentId, {
|
verifyPayment(paymentId, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Success handled by query invalidation
|
setVerifyingPaymentId(null)
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
setVerifyingPaymentId(null)
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const handleSendToKitchen = () => {
|
const handleSendToKitchen = () => {
|
||||||
changeOrderStatus({
|
changeOrderStatus(
|
||||||
|
{
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
status: OrderStatus.PREPARING
|
status: OrderStatus.PREPARING,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeliverToCourier = () => {
|
const handleDeliverToCourier = () => {
|
||||||
changeOrderStatus({
|
changeOrderStatus(
|
||||||
|
{
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
status: OrderStatus.SHIPPED
|
status: OrderStatus.SHIPPED,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeliverToWaiter = () => {
|
const handleDeliverToWaiter = () => {
|
||||||
changeOrderStatus({
|
changeOrderStatus(
|
||||||
|
{
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
status: OrderStatus.DELIVERED_TO_WAITER
|
status: OrderStatus.DELIVERED_TO_WAITER,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeliverToReceptionist = () => {
|
const handleDeliverToReceptionist = () => {
|
||||||
changeOrderStatus({
|
changeOrderStatus(
|
||||||
|
{
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
status: OrderStatus.DELIVERED_TO_RECEPTIONIST
|
status: OrderStatus.DELIVERED_TO_RECEPTIONIST,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancelOrder = (description?: string) => {
|
const handleCancelOrder = (description?: string) => {
|
||||||
changeOrderStatus({
|
changeOrderStatus(
|
||||||
|
{
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
status: OrderStatus.CANCELED,
|
status: OrderStatus.CANCELED,
|
||||||
params: description ? { description } : undefined
|
params: description ? { description } : undefined,
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
setShowCancelModal(false)
|
setShowCancelModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,17 +201,22 @@ const OrderDetails: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefund = (text?: string) => {
|
const handleRefund = (amount: number, description: string) => {
|
||||||
if (!text) return
|
refundPayment(
|
||||||
refundPayment({
|
{
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
params: { description: text }
|
params: { amount, description },
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('بازگشت وجه با موفقیت انجام شد')
|
||||||
|
setRefundModal(null)
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
setShowRefundModal(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePrintOrder = () => {
|
const handlePrintOrder = () => {
|
||||||
@@ -249,6 +238,48 @@ const OrderDetails: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddOrderItem = (foodId: string, quantity: number) => {
|
||||||
|
addOrderItem(
|
||||||
|
{ orderId: order.id, params: { foodId, quantity } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('آیتم به سفارش اضافه شد')
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateItemQuantity = (itemId: string, quantity: number) => {
|
||||||
|
if (quantity < 1) return
|
||||||
|
updateOrderItemQuantity(
|
||||||
|
{ orderId: order.id, itemId, params: { quantity } },
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmRemoveItem = () => {
|
||||||
|
if (!itemIdToRemove) return
|
||||||
|
removeOrderItem(
|
||||||
|
{ orderId: order.id, itemId: itemIdToRemove },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('آیتم از سفارش حذف شد')
|
||||||
|
setItemIdToRemove(null)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
@@ -260,29 +291,37 @@ const OrderDetails: FC = () => {
|
|||||||
<div className='flex gap-7 mt-9'>
|
<div className='flex gap-7 mt-9'>
|
||||||
<div className='flex-1 space-y-8'>
|
<div className='flex-1 space-y-8'>
|
||||||
<CustomerInfo order={order} />
|
<CustomerInfo order={order} />
|
||||||
<OrderItemsDetails order={order} getStatusColor={getStatusColor} />
|
<OrderItemsDetails
|
||||||
|
order={order}
|
||||||
|
getStatusColor={getStatusColor}
|
||||||
|
canEditItems={canEditItems}
|
||||||
|
isUpdatingItem={isUpdatingItem}
|
||||||
|
onAddItem={() => setShowAddItemModal(true)}
|
||||||
|
onUpdateQuantity={handleUpdateItemQuantity}
|
||||||
|
onRemoveItem={(itemId) => setItemIdToRemove(itemId)}
|
||||||
|
/>
|
||||||
|
<PaymentsList
|
||||||
|
order={order}
|
||||||
|
isVerifyingPayment={isVerifyingPayment}
|
||||||
|
verifyingPaymentId={verifyingPaymentId}
|
||||||
|
onVerifyPayment={handleVerifyPayment}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PaymentInfo
|
<PaymentInfo
|
||||||
order={order}
|
order={order}
|
||||||
getPaymentStatusColor={getPaymentStatusColor}
|
canEditFees={canEditFees}
|
||||||
getPaymentStatusTextColor={getPaymentStatusTextColor}
|
onEditFees={() => setShowEditFeesModal(true)}
|
||||||
getPaymentStatusText={getPaymentStatusText}
|
isRefunding={isRefunding}
|
||||||
|
onRefund={(defaultAmount, maxAmount) =>
|
||||||
|
setRefundModal({ defaultAmount, maxAmount })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{showPaymentVerificationBox && (
|
|
||||||
<PaymentVerificationBox
|
|
||||||
order={order}
|
|
||||||
isVerifyingPayment={isVerifyingPayment}
|
|
||||||
onVerifyPayment={handleVerifyPayment}
|
|
||||||
showVerifyButton={showVerifyPaymentButton}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<OrderActions
|
<OrderActions
|
||||||
onPrint={handlePrintOrder}
|
onPrint={handlePrintOrder}
|
||||||
showSendToKitchenButton={showSendToKitchenButton}
|
showSendToKitchenButton={showSendToKitchenButton}
|
||||||
showCancelButton={showCancelButton}
|
showCancelButton={showCancelButton}
|
||||||
showEditFeesButton={canEditFees}
|
|
||||||
isPreparing={isPreparing}
|
isPreparing={isPreparing}
|
||||||
isCourierDelivery={isCourierDelivery}
|
isCourierDelivery={isCourierDelivery}
|
||||||
isDineInOrCarDelivery={isDineInOrCarDelivery}
|
isDineInOrCarDelivery={isDineInOrCarDelivery}
|
||||||
@@ -296,7 +335,6 @@ const OrderDetails: FC = () => {
|
|||||||
onDeliverToReceptionist={handleDeliverToReceptionist}
|
onDeliverToReceptionist={handleDeliverToReceptionist}
|
||||||
onCompleteOrder={handleCompleteOrder}
|
onCompleteOrder={handleCompleteOrder}
|
||||||
onCancelOrder={() => setShowCancelModal(true)}
|
onCancelOrder={() => setShowCancelModal(true)}
|
||||||
onEditFees={() => setShowEditFeesModal(true)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,7 +347,14 @@ const OrderDetails: FC = () => {
|
|||||||
onSubmit={handleUpdateOrderFees}
|
onSubmit={handleUpdateOrderFees}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* مدال کنسل سفارش */}
|
<AddOrderItemModal
|
||||||
|
isOpen={showAddItemModal}
|
||||||
|
onClose={() => setShowAddItemModal(false)}
|
||||||
|
existingItems={order.items}
|
||||||
|
isLoading={isAddingItem}
|
||||||
|
onAdd={handleAddOrderItem}
|
||||||
|
/>
|
||||||
|
|
||||||
<ModalConfrim
|
<ModalConfrim
|
||||||
isOpen={showCancelModal}
|
isOpen={showCancelModal}
|
||||||
close={() => setShowCancelModal(false)}
|
close={() => setShowCancelModal(false)}
|
||||||
@@ -319,14 +364,21 @@ const OrderDetails: FC = () => {
|
|||||||
isHasDescription
|
isHasDescription
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* مدال ریفاند */}
|
<RefundPaymentModal
|
||||||
|
isOpen={!!refundModal}
|
||||||
|
onClose={() => setRefundModal(null)}
|
||||||
|
defaultAmount={refundModal?.defaultAmount ?? 0}
|
||||||
|
maxAmount={refundModal?.maxAmount ?? 0}
|
||||||
|
isLoading={isRefunding}
|
||||||
|
onSubmit={handleRefund}
|
||||||
|
/>
|
||||||
|
|
||||||
<ModalConfrim
|
<ModalConfrim
|
||||||
isOpen={showRefundModal}
|
isOpen={!!itemIdToRemove}
|
||||||
close={() => setShowRefundModal(false)}
|
close={() => setItemIdToRemove(null)}
|
||||||
onConfrim={handleRefund}
|
onConfrim={handleConfirmRemoveItem}
|
||||||
isloading={isRefunding}
|
isloading={isRemovingItem}
|
||||||
label='آیا از ریفاند این سفارش مطمئن هستید؟'
|
label='آیا از حذف این آیتم مطمئن هستید؟'
|
||||||
isHasDescription
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { type FC, useMemo, useState } from 'react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import { Add } from 'iconsax-react'
|
||||||
|
import { useGetFoods } from '@/pages/food/hooks/useFoodData'
|
||||||
|
import { formatPrice } from '@/helpers/func'
|
||||||
|
import { getFoodUnitPrice } from '../hooks/useCreateOrderForm'
|
||||||
|
import type { OrderItem } from '../types/Types'
|
||||||
|
|
||||||
|
interface AddOrderItemModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
existingItems: OrderItem[]
|
||||||
|
isLoading?: boolean
|
||||||
|
onAdd: (foodId: string, quantity: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddOrderItemModal: FC<AddOrderItemModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
existingItems,
|
||||||
|
isLoading,
|
||||||
|
onAdd,
|
||||||
|
}) => {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
|
||||||
|
const { data: foodsData } = useGetFoods({ limit: 500 })
|
||||||
|
const foods = foodsData?.data ?? []
|
||||||
|
|
||||||
|
const categories = useMemo(() => {
|
||||||
|
const categoryMap = new Map<string, string>()
|
||||||
|
foods.forEach((food) => {
|
||||||
|
if (food.category?.id) {
|
||||||
|
categoryMap.set(food.category.id, food.category.title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(categoryMap.entries())
|
||||||
|
.map(([id, title]) => ({ id, title }))
|
||||||
|
.sort((a, b) => a.title.localeCompare(b.title, 'fa'))
|
||||||
|
}, [foods])
|
||||||
|
|
||||||
|
const filteredFoods = useMemo(() => {
|
||||||
|
let result = foods
|
||||||
|
if (selectedCategoryId) {
|
||||||
|
result = result.filter((food) => food.category?.id === selectedCategoryId)
|
||||||
|
}
|
||||||
|
const query = search.trim().toLowerCase()
|
||||||
|
if (query) {
|
||||||
|
result = result.filter((food) => food.title.toLowerCase().includes(query))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [foods, search, selectedCategoryId])
|
||||||
|
|
||||||
|
const existingQtyByFoodId = useMemo(() => {
|
||||||
|
const map = new Map<string, number>()
|
||||||
|
existingItems.forEach((item) => {
|
||||||
|
map.set(item.food.id, item.quantity)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, [existingItems])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={isOpen}
|
||||||
|
close={onClose}
|
||||||
|
isHeader
|
||||||
|
title_header='افزودن آیتم'
|
||||||
|
width={480}
|
||||||
|
>
|
||||||
|
<div className='mt-4 flex flex-col max-h-[60vh]'>
|
||||||
|
<Input
|
||||||
|
variant='search'
|
||||||
|
className='bg-[#EEF0F7]'
|
||||||
|
placeholder='جستجوی غذا...'
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{categories.length > 0 && (
|
||||||
|
<div className='mt-2 flex gap-1.5 overflow-x-auto flex-shrink-0 pb-0.5'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => setSelectedCategoryId(null)}
|
||||||
|
className={`flex-shrink-0 px-2.5 h-7 rounded-full text-[11px] transition-colors ${
|
||||||
|
selectedCategoryId === null
|
||||||
|
? 'bg-primary text-white'
|
||||||
|
: 'bg-[#EEF0F7] text-description hover:bg-[#E4E7F0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
همه
|
||||||
|
</button>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category.id}
|
||||||
|
type='button'
|
||||||
|
onClick={() => setSelectedCategoryId(category.id)}
|
||||||
|
className={`flex-shrink-0 px-2.5 h-7 rounded-full text-[11px] transition-colors whitespace-nowrap ${
|
||||||
|
selectedCategoryId === category.id
|
||||||
|
? 'bg-primary text-white'
|
||||||
|
: 'bg-[#EEF0F7] text-description hover:bg-[#E4E7F0]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='mt-3 flex-1 min-h-0 overflow-y-auto space-y-1'>
|
||||||
|
{filteredFoods.length === 0 ? (
|
||||||
|
<div className='text-center py-8 text-xs text-description'>
|
||||||
|
غذایی یافت نشد
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredFoods.map((food) => {
|
||||||
|
const existingQty = existingQtyByFoodId.get(food.id)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={food.id}
|
||||||
|
className='flex items-center gap-2 p-1.5 rounded-lg hover:bg-[#F8F9FC] transition-colors'
|
||||||
|
>
|
||||||
|
{food.images?.[0] ? (
|
||||||
|
<img
|
||||||
|
src={food.images[0]}
|
||||||
|
alt={food.title}
|
||||||
|
className='w-9 h-9 rounded-lg object-cover flex-shrink-0'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='w-9 h-9 rounded-lg bg-gray-100 flex-shrink-0' />
|
||||||
|
)}
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<div className='text-xs truncate'>{food.title}</div>
|
||||||
|
<div className='text-[11px] text-description'>
|
||||||
|
{formatPrice(getFoodUnitPrice(food))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => onAdd(food.id, 1)}
|
||||||
|
className={`flex items-center gap-1 px-2.5 h-7 rounded-lg text-[11px] transition-colors flex-shrink-0 disabled:opacity-50 ${
|
||||||
|
existingQty
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'bg-primary text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Add size={12} color={existingQty ? '#000' : '#fff'} />
|
||||||
|
{existingQty ? `×${existingQty}` : 'افزودن'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddOrderItemModal
|
||||||
@@ -5,7 +5,6 @@ interface OrderActionsProps {
|
|||||||
onPrint: () => void
|
onPrint: () => void
|
||||||
showSendToKitchenButton: boolean
|
showSendToKitchenButton: boolean
|
||||||
showCancelButton: boolean
|
showCancelButton: boolean
|
||||||
showEditFeesButton?: boolean
|
|
||||||
isPreparing: boolean
|
isPreparing: boolean
|
||||||
isCourierDelivery: boolean
|
isCourierDelivery: boolean
|
||||||
isDineInOrCarDelivery: boolean
|
isDineInOrCarDelivery: boolean
|
||||||
@@ -19,14 +18,12 @@ interface OrderActionsProps {
|
|||||||
onDeliverToReceptionist: () => void
|
onDeliverToReceptionist: () => void
|
||||||
onCompleteOrder: () => void
|
onCompleteOrder: () => void
|
||||||
onCancelOrder: () => void
|
onCancelOrder: () => void
|
||||||
onEditFees?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderActions: FC<OrderActionsProps> = ({
|
const OrderActions: FC<OrderActionsProps> = ({
|
||||||
onPrint,
|
onPrint,
|
||||||
showSendToKitchenButton,
|
showSendToKitchenButton,
|
||||||
showCancelButton,
|
showCancelButton,
|
||||||
showEditFeesButton = false,
|
|
||||||
isPreparing,
|
isPreparing,
|
||||||
isCourierDelivery,
|
isCourierDelivery,
|
||||||
isDineInOrCarDelivery,
|
isDineInOrCarDelivery,
|
||||||
@@ -40,7 +37,6 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
onDeliverToReceptionist,
|
onDeliverToReceptionist,
|
||||||
onCompleteOrder,
|
onCompleteOrder,
|
||||||
onCancelOrder,
|
onCancelOrder,
|
||||||
onEditFees,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [loadingAction, setLoadingAction] = useState<string | null>(null)
|
const [loadingAction, setLoadingAction] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -88,14 +84,6 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
className='bg-slate-700 hover:bg-slate-800'
|
className='bg-slate-700 hover:bg-slate-800'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showEditFeesButton && onEditFees && (
|
|
||||||
<Button
|
|
||||||
label='ویرایش هزینهها'
|
|
||||||
onClick={onEditFees}
|
|
||||||
className='bg-transparent border border-slate-700 text-slate-700 hover:bg-slate-50'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isCanceled && showSendToKitchenButton && (
|
{!isCanceled && showSendToKitchenButton && (
|
||||||
<Button
|
<Button
|
||||||
label='ارسال به آشپزخانه'
|
label='ارسال به آشپزخانه'
|
||||||
@@ -154,4 +142,3 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default OrderActions
|
export default OrderActions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import Status from '@/components/Status'
|
import Status from '@/components/Status'
|
||||||
import { ShoppingCart, Calendar, Clock, Tag } from 'iconsax-react'
|
import Button from '@/components/Button'
|
||||||
|
import { ShoppingCart, Calendar, Clock, Tag, Add, Minus, Trash } from 'iconsax-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
||||||
import type { Order, OrderItem as OrderItemType } from '../types/Types'
|
import type { Order, OrderItem as OrderItemType } from '../types/Types'
|
||||||
@@ -19,6 +20,11 @@ interface OrderItemTable extends RowDataType {
|
|||||||
interface OrderItemsDetailsProps {
|
interface OrderItemsDetailsProps {
|
||||||
order: Order
|
order: Order
|
||||||
getStatusColor: (status: string) => string
|
getStatusColor: (status: string) => string
|
||||||
|
canEditItems?: boolean
|
||||||
|
isUpdatingItem?: boolean
|
||||||
|
onAddItem?: () => void
|
||||||
|
onUpdateQuantity?: (itemId: string, quantity: number) => void
|
||||||
|
onRemoveItem?: (itemId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
|
const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
|
||||||
@@ -34,7 +40,15 @@ const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'in
|
|||||||
return variantMap[status] || 'pending'
|
return variantMap[status] || 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderItemsDetails: FC<OrderItemsDetailsProps> = ({ order, getStatusColor }) => {
|
const OrderItemsDetails: FC<OrderItemsDetailsProps> = ({
|
||||||
|
order,
|
||||||
|
getStatusColor,
|
||||||
|
canEditItems = false,
|
||||||
|
isUpdatingItem = false,
|
||||||
|
onAddItem,
|
||||||
|
onUpdateQuantity,
|
||||||
|
onRemoveItem,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
|
||||||
const orderItems: OrderItemTable[] = order.items.map((item: OrderItemType) => ({
|
const orderItems: OrderItemTable[] = order.items.map((item: OrderItemType) => ({
|
||||||
@@ -76,7 +90,32 @@ const OrderItemsDetails: FC<OrderItemsDetailsProps> = ({ order, getStatusColor }
|
|||||||
title: 'تعداد',
|
title: 'تعداد',
|
||||||
key: 'quantity',
|
key: 'quantity',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (item) => formatFaNumber(item.quantity)
|
render: (item) =>
|
||||||
|
canEditItems && onUpdateQuantity ? (
|
||||||
|
<div className='inline-flex items-center gap-1'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
disabled={isUpdatingItem || item.quantity <= 1}
|
||||||
|
onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}
|
||||||
|
className='w-7 h-7 rounded-md flex items-center justify-center hover:bg-[#F8F9FC] transition-colors disabled:opacity-40'
|
||||||
|
>
|
||||||
|
<Minus size={13} color='#000' />
|
||||||
|
</button>
|
||||||
|
<span className='w-6 text-center text-xs tabular-nums'>
|
||||||
|
{formatFaNumber(item.quantity)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
disabled={isUpdatingItem}
|
||||||
|
onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}
|
||||||
|
className='w-7 h-7 rounded-md flex items-center justify-center hover:bg-[#F8F9FC] transition-colors disabled:opacity-40'
|
||||||
|
>
|
||||||
|
<Add size={13} color='#000' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
formatFaNumber(item.quantity)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'قیمت واحد',
|
title: 'قیمت واحد',
|
||||||
@@ -94,15 +133,44 @@ const OrderItemsDetails: FC<OrderItemsDetailsProps> = ({ order, getStatusColor }
|
|||||||
title: 'مبلغ کل',
|
title: 'مبلغ کل',
|
||||||
key: 'amount',
|
key: 'amount',
|
||||||
align: 'right'
|
align: 'right'
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (canEditItems && onRemoveItem) {
|
||||||
|
columns.push({
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'id',
|
||||||
|
align: 'center',
|
||||||
|
render: (item) => (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
disabled={isUpdatingItem || order.items.length <= 1}
|
||||||
|
onClick={() => onRemoveItem(item.id)}
|
||||||
|
className='w-8 h-8 rounded-md flex items-center justify-center hover:bg-red-50 transition-colors disabled:opacity-40 mx-auto'
|
||||||
|
title='حذف آیتم'
|
||||||
|
>
|
||||||
|
<Trash size={16} color='#EF4444' />
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full bg-white rounded-4xl p-8'>
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
<div className='text-lg font-light mb-6 flex items-center gap-2'>
|
<div className='mb-6 flex items-center justify-between gap-3'>
|
||||||
|
<div className='text-lg font-light flex items-center gap-2'>
|
||||||
<ShoppingCart color='#000' size={20} />
|
<ShoppingCart color='#000' size={20} />
|
||||||
جزییات سفارش
|
جزییات سفارش
|
||||||
</div>
|
</div>
|
||||||
|
{canEditItems && onAddItem && (
|
||||||
|
<Button
|
||||||
|
label='افزودن آیتم'
|
||||||
|
onClick={onAddItem}
|
||||||
|
className='w-fit px-4 h-9 text-xs'
|
||||||
|
disabled={isUpdatingItem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mb-6 flex flex-wrap gap-4 text-[13px] font-light'>
|
<div className='mb-6 flex flex-wrap gap-4 text-[13px] font-light'>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
@@ -234,4 +302,3 @@ const OrderItemsDetails: FC<OrderItemsDetailsProps> = ({ order, getStatusColor }
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default OrderItemsDetails
|
export default OrderItemsDetails
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
import { formatFaNumber } from '@/helpers/func'
|
import { formatFaNumber } from '@/helpers/func'
|
||||||
import type { TFunction } from 'i18next'
|
import type { TFunction } from 'i18next'
|
||||||
import { PaymentStatusEnum } from '../enum/Enum'
|
|
||||||
|
|
||||||
interface GetOrderTableColumnsParams {
|
interface GetOrderTableColumnsParams {
|
||||||
onDelete?: (id: string) => void
|
onDelete?: (id: string) => void
|
||||||
@@ -26,35 +25,18 @@ const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'in
|
|||||||
return variantMap[status] || 'pending'
|
return variantMap[status] || 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
|
const getShiftAdminName = (item: Order): string => {
|
||||||
const variantMap: Record<string, 'success' | 'error' | 'warning' | 'info' | 'pending'> = {
|
const admin = item.cashShift?.admin
|
||||||
[PaymentStatusEnum.Paid]: 'success',
|
if (!admin) return '-'
|
||||||
[PaymentStatusEnum.Pending]: 'warning',
|
const name = [admin.firstName, admin.lastName].filter(Boolean).join(' ')
|
||||||
[PaymentStatusEnum.Failed]: 'error',
|
return name || admin.phone || '-'
|
||||||
[PaymentStatusEnum.Refunded]: 'info',
|
|
||||||
}
|
|
||||||
return variantMap[status] || 'pending'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPaymentStatusLabel = (status: string): string => {
|
|
||||||
const labelMap: Record<string, string> = {
|
|
||||||
[PaymentStatusEnum.Paid]: 'پرداخت شده',
|
|
||||||
[PaymentStatusEnum.Pending]: 'در انتظار پرداخت',
|
|
||||||
[PaymentStatusEnum.Failed]: 'پرداخت ناموفق',
|
|
||||||
[PaymentStatusEnum.Refunded]: 'بازگشت شده',
|
|
||||||
}
|
|
||||||
return labelMap[status] || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvePaymentStatus = (item: Order): string | undefined => {
|
|
||||||
return item.paymentStatus || item.payments?.[0]?.status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnType<Order>[] => {
|
export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnType<Order>[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'orderNumber',
|
key: 'orderNumber',
|
||||||
title: 'شماره سفارش',
|
title: 'شماره',
|
||||||
render: (item: Order) => {
|
render: (item: Order) => {
|
||||||
return formatFaNumber(item.orderNumber)
|
return formatFaNumber(item.orderNumber)
|
||||||
}
|
}
|
||||||
@@ -68,50 +50,72 @@ export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnT
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'total',
|
key: 'shiftAdmin',
|
||||||
title: 'مبلغ سفارش',
|
title: 'شیفت',
|
||||||
render: (item: Order) => {
|
render: (item: Order) => getShiftAdminName(item),
|
||||||
return formatPrice(item.total)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
title: 'زمان سفارش',
|
title: 'تاریخ',
|
||||||
render: (item: Order) => {
|
render: (item: Order) => {
|
||||||
return formatOptionalDate(item.createdAt, {
|
if (!item.createdAt) return '-'
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col whitespace-nowrap">
|
||||||
|
<span className="text-[11px] text-description">
|
||||||
|
{formatOptionalDate(item.createdAt, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="text-[15px] font-medium">
|
||||||
|
{formatOptionalDate(item.createdAt, {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'deliveryMethod',
|
key: 'methods',
|
||||||
title: 'نوع تحویل',
|
title: 'تحویل / پرداخت',
|
||||||
render: (item: Order) => {
|
render: (item: Order) => {
|
||||||
return item.deliveryMethod?.description || '-'
|
const rows = [
|
||||||
}
|
{ label: 'نوع تحویل', value: item.deliveryMethod?.description || '-' },
|
||||||
},
|
{ label: 'نوع پرداخت', value: item.paymentMethod?.description || '-' },
|
||||||
{
|
]
|
||||||
key: 'paymentMethod',
|
|
||||||
title: 'نوع پرداخت',
|
|
||||||
render: (item: Order) => {
|
|
||||||
return item.paymentMethod?.description || '-'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'paymentStatus',
|
|
||||||
title: 'وضعیت پرداخت',
|
|
||||||
render: (item: Order) => {
|
|
||||||
const paymentStatus = resolvePaymentStatus(item)
|
|
||||||
if (!paymentStatus) return '-'
|
|
||||||
return (
|
return (
|
||||||
<Status
|
<div className="flex flex-col gap-1 text-[12px] whitespace-nowrap">
|
||||||
variant={getPaymentStatusVariant(paymentStatus)}
|
{rows.map((row) => (
|
||||||
label={getPaymentStatusLabel(paymentStatus)}
|
<div key={row.label} className="flex items-center justify-between gap-3">
|
||||||
/>
|
<span className="text-description">{row.label}</span>
|
||||||
|
<span>{row.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'amounts',
|
||||||
|
title: 'مبالغ',
|
||||||
|
render: (item: Order) => {
|
||||||
|
const rows = [
|
||||||
|
{ label: 'مبلغ کل', value: formatPrice(item.total) },
|
||||||
|
{ label: 'پرداخت شده', value: formatPrice(Number(item.paidAmount ?? 0)) },
|
||||||
|
{ label: 'مانده', value: formatPrice(Number(item.balance ?? 0)) },
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1 text-[12px] whitespace-nowrap">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<div key={row.label} className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-description">{row.label}</span>
|
||||||
|
<span>{row.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,30 +1,19 @@
|
|||||||
import { formatFaNumber, formatPrice } from "@/helpers/func";
|
import { formatPrice } from "@/helpers/func";
|
||||||
import { PaymentMethodEnum } from "@/pages/paymentMethods/enum/Enum";
|
import { PaymentMethodEnum } from "@/pages/paymentMethods/enum/Enum";
|
||||||
import { DeliveryMethodEnum } from "@/pages/shipmentMethod/enum/Enum";
|
import { Edit2, Refresh } from "iconsax-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import type { Order } from "../types/Types";
|
import type { Order } from "../types/Types";
|
||||||
|
import { OrderStatus } from "../enum/Enum";
|
||||||
|
|
||||||
interface PaymentInfoProps {
|
interface PaymentInfoProps {
|
||||||
order: Order;
|
order: Order;
|
||||||
getPaymentStatusColor: (status: string) => string;
|
canEditFees?: boolean;
|
||||||
getPaymentStatusTextColor: (status: string) => string;
|
onEditFees?: () => void;
|
||||||
getPaymentStatusText: (status: string) => string;
|
onRefund?: (defaultAmount: number, maxAmount: number) => void;
|
||||||
|
isRefunding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaymentInfo: FC<PaymentInfoProps> = ({ order, getPaymentStatusColor, getPaymentStatusTextColor, getPaymentStatusText }) => {
|
const getPaymentMethodText = (method: string): string => {
|
||||||
const paymentStatus = order.payments?.[0]?.status;
|
|
||||||
|
|
||||||
const getDeliveryMethodText = (method: string): string => {
|
|
||||||
const methodMap: Record<string, string> = {
|
|
||||||
[DeliveryMethodEnum.DineIn]: "سرو در رستوران",
|
|
||||||
[DeliveryMethodEnum.CustomerPickup]: "دریافت از محل",
|
|
||||||
[DeliveryMethodEnum.DeliveryCar]: "تحویل به خودرو",
|
|
||||||
[DeliveryMethodEnum.DeliveryCourier]: "بیرون بر",
|
|
||||||
};
|
|
||||||
return methodMap[method] || method;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPaymentMethodText = (method: string): string => {
|
|
||||||
const methodMap: Record<string, string> = {
|
const methodMap: Record<string, string> = {
|
||||||
[PaymentMethodEnum.Cash]: "پرداخت نقدی",
|
[PaymentMethodEnum.Cash]: "پرداخت نقدی",
|
||||||
[PaymentMethodEnum.Online]: "پرداخت آنلاین",
|
[PaymentMethodEnum.Online]: "پرداخت آنلاین",
|
||||||
@@ -32,27 +21,55 @@ const PaymentInfo: FC<PaymentInfoProps> = ({ order, getPaymentStatusColor, getPa
|
|||||||
[PaymentMethodEnum.Wallet]: "کیف پول",
|
[PaymentMethodEnum.Wallet]: "کیف پول",
|
||||||
};
|
};
|
||||||
return methodMap[method] || method;
|
return methodMap[method] || method;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PaymentInfo: FC<PaymentInfoProps> = ({
|
||||||
|
order,
|
||||||
|
canEditFees = false,
|
||||||
|
onEditFees,
|
||||||
|
onRefund,
|
||||||
|
isRefunding = false,
|
||||||
|
}) => {
|
||||||
|
const paidAmount = Number(order.paidAmount ?? 0);
|
||||||
|
const balance = Number(order.balance ?? 0);
|
||||||
|
const refundedAmount = Number(order.refundedAmount ?? 0);
|
||||||
|
const isCanceled = order.status === OrderStatus.CANCELED;
|
||||||
|
const showRefundOnPaid = isCanceled && paidAmount > 0;
|
||||||
|
const showRefundOnBalance = balance < 0;
|
||||||
|
|
||||||
|
const editFeesButton =
|
||||||
|
canEditFees && onEditFees ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onEditFees}
|
||||||
|
title="ویرایش هزینهها"
|
||||||
|
className="p-0.5 rounded hover:bg-slate-100 transition-colors shrink-0"
|
||||||
|
>
|
||||||
|
<Edit2 size={14} color="#64748b" />
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const refundButton = (defaultAmount: number, maxAmount: number) =>
|
||||||
|
onRefund ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRefund(defaultAmount, maxAmount)}
|
||||||
|
disabled={isRefunding}
|
||||||
|
title="بازگشت وجه"
|
||||||
|
className="inline-flex items-center gap-1 text-[11px] text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded px-1.5 py-0.5 transition-colors shrink-0 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Refresh size={12} color="currentColor" />
|
||||||
|
بازگشت وجه
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[330px] h-fit bg-white rounded-4xl p-8">
|
<div className="w-[330px] h-fit bg-white rounded-4xl p-8">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="font-light">اطلاعات پرداخت</div>
|
<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>
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-5">
|
||||||
<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.deliveryMethod ? getDeliveryMethodText(order.deliveryMethod.method) : "-"}</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="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-description">روش پرداخت</div>
|
||||||
<div>{order.paymentMethod ? getPaymentMethodText(order.paymentMethod.method) : "-"}</div>
|
<div>{order.paymentMethod ? getPaymentMethodText(order.paymentMethod.method) : "-"}</div>
|
||||||
@@ -65,25 +82,11 @@ const PaymentInfo: FC<PaymentInfoProps> = ({ order, getPaymentStatusColor, getPa
|
|||||||
</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="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-description">جمع آیتمها</div>
|
||||||
<div>{formatPrice(order.subTotal)}</div>
|
<div>{formatPrice(order.subTotal)}</div>
|
||||||
</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 && (
|
{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="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-description">تخفیف کوپن</div>
|
||||||
@@ -91,25 +94,23 @@ const PaymentInfo: FC<PaymentInfoProps> = ({ order, getPaymentStatusColor, getPa
|
|||||||
</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="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-description flex items-center gap-1">
|
||||||
|
هزینه ارسال
|
||||||
|
{editFeesButton}
|
||||||
|
</div>
|
||||||
<div>{formatPrice(order.deliveryFee)}</div>
|
<div>{formatPrice(order.deliveryFee)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(order.packingFee ?? 0) > 0 && (
|
|
||||||
<div className="flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4">
|
<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-description">هزینه بستهبندی</div>
|
||||||
<div>{formatPrice(order.packingFee)}</div>
|
<div>{formatPrice(order.packingFee ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{order.prepareTime != null && order.prepareTime > 0 && (
|
|
||||||
<div className="flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4">
|
<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-description">زمان آمادهسازی</div>
|
||||||
<div>{formatFaNumber(order.prepareTime)} دقیقه</div>
|
<div>{order.prepareTime ? `${order.prepareTime} دقیقه` : "-"}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{order.tax > 0 && (
|
{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="flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4">
|
||||||
@@ -118,15 +119,35 @@ const PaymentInfo: FC<PaymentInfoProps> = ({ order, getPaymentStatusColor, getPa
|
|||||||
</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="flex mt-4 text-[13px] font-bold 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>مبلغ کل</div>
|
||||||
<div>{formatPrice(order.total)}</div>
|
<div>{formatPrice(order.total)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex mt-4 text-[13px] font-light justify-between items-center border-b border-border border-dashed pb-4 gap-2">
|
||||||
|
<div className="text-description shrink-0">پرداخت شده</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||||
|
<div className="text-green-600">{formatPrice(paidAmount)}</div>
|
||||||
|
{showRefundOnPaid && refundButton(paidAmount, paidAmount)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{refundedAmount > 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-blue-600">{formatPrice(refundedAmount)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex mt-4 text-[13px] font-bold justify-between items-center gap-2">
|
||||||
|
<div className="shrink-0">مانده</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||||
|
<div className={balance > 0 ? "text-orange-600" : "text-green-600"}>
|
||||||
|
{formatPrice(balance)}
|
||||||
|
</div>
|
||||||
|
{showRefundOnBalance && refundButton(Math.abs(balance), Math.abs(balance))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import Button from '@/components/Button'
|
|
||||||
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import type { Order } from '../types/Types'
|
|
||||||
|
|
||||||
interface PaymentVerificationBoxProps {
|
|
||||||
order: Order
|
|
||||||
isVerifyingPayment: boolean
|
|
||||||
onVerifyPayment: () => void
|
|
||||||
showVerifyButton: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const PaymentVerificationBox: FC<PaymentVerificationBoxProps> = ({
|
|
||||||
order,
|
|
||||||
isVerifyingPayment,
|
|
||||||
onVerifyPayment,
|
|
||||||
showVerifyButton,
|
|
||||||
}) => {
|
|
||||||
const payment = order.payments?.[0]
|
|
||||||
const isCash = order.paymentMethod?.method === PaymentMethodEnum.Cash
|
|
||||||
const isCreditCard = order.paymentMethod?.method === PaymentMethodEnum.CreditCard
|
|
||||||
const hasDescription = Boolean(payment?.description)
|
|
||||||
const hasAttachments = Boolean(payment?.attachments && payment.attachments.length > 0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-[330px] h-fit bg-white rounded-4xl p-8 mt-6'>
|
|
||||||
<div className='font-light'>اطلاعات تایید پرداخت</div>
|
|
||||||
|
|
||||||
<div className='mt-6 space-y-4 text-[13px] font-light'>
|
|
||||||
{isCreditCard && hasDescription && (
|
|
||||||
<div>
|
|
||||||
<div className='text-description mb-2'>توضیحات پرداخت</div>
|
|
||||||
<div className='text-gray-700 bg-gray-50 p-3 rounded-lg'>{payment?.description}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCreditCard && hasAttachments && (
|
|
||||||
<div>
|
|
||||||
<div className='text-description mb-2'>پیوست پرداخت</div>
|
|
||||||
<div className='flex gap-3 flex-wrap'>
|
|
||||||
{payment?.attachments?.map((attachment, index) => (
|
|
||||||
<a
|
|
||||||
key={index}
|
|
||||||
href={attachment}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={attachment}
|
|
||||||
alt={`پیوست پرداخت ${index + 1}`}
|
|
||||||
className='w-24 h-24 object-cover rounded-xl border border-border'
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showVerifyButton && (
|
|
||||||
<Button
|
|
||||||
label={isCash ? 'تایید پرداخت نقدی' : 'تایید پرداخت'}
|
|
||||||
onClick={onVerifyPayment}
|
|
||||||
isloading={isVerifyingPayment}
|
|
||||||
className='bg-green-600 hover:bg-green-700 w-full'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PaymentVerificationBox
|
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import Table from '@/components/Table'
|
||||||
|
import Status from '@/components/Status'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { Moneys } from 'iconsax-react'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
|
||||||
|
import type { Order, OrderPayment } from '../types/Types'
|
||||||
|
import { formatOptionalDate, formatPrice } from '@/helpers/func'
|
||||||
|
import { PaymentStatusEnum } from '../enum/Enum'
|
||||||
|
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
||||||
|
|
||||||
|
interface PaymentRow extends RowDataType {
|
||||||
|
amount: number
|
||||||
|
method: string
|
||||||
|
status: string
|
||||||
|
referenceId: string | null
|
||||||
|
transactionId: string | null
|
||||||
|
paidAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
description: string | null
|
||||||
|
attachments: string[] | null
|
||||||
|
canVerify: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaymentsListProps {
|
||||||
|
order: Order
|
||||||
|
isVerifyingPayment: boolean
|
||||||
|
verifyingPaymentId?: string | null
|
||||||
|
onVerifyPayment: (paymentId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentMethodText = (method: string): string => {
|
||||||
|
const methodMap: Record<string, string> = {
|
||||||
|
[PaymentMethodEnum.Cash]: 'نقدی',
|
||||||
|
[PaymentMethodEnum.Online]: 'آنلاین',
|
||||||
|
[PaymentMethodEnum.CreditCard]: 'کارت به کارت',
|
||||||
|
[PaymentMethodEnum.Wallet]: 'کیف پول',
|
||||||
|
}
|
||||||
|
return methodMap[method] || method
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => {
|
||||||
|
const variantMap: Record<string, 'success' | 'error' | 'warning' | 'info' | 'pending'> = {
|
||||||
|
[PaymentStatusEnum.Paid]: 'success',
|
||||||
|
[PaymentStatusEnum.Pending]: 'warning',
|
||||||
|
[PaymentStatusEnum.Failed]: 'error',
|
||||||
|
[PaymentStatusEnum.Refunded]: 'info',
|
||||||
|
}
|
||||||
|
return variantMap[status] || 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentStatusLabel = (status: string): string => {
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
[PaymentStatusEnum.Paid]: 'پرداخت شده',
|
||||||
|
[PaymentStatusEnum.Pending]: 'در انتظار پرداخت',
|
||||||
|
[PaymentStatusEnum.Failed]: 'پرداخت ناموفق',
|
||||||
|
[PaymentStatusEnum.Refunded]: 'بازگشت شده',
|
||||||
|
}
|
||||||
|
return labelMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const canVerifyPayment = (payment: OrderPayment, orderCanceled: boolean): boolean => {
|
||||||
|
if (orderCanceled) return false
|
||||||
|
if (payment.status !== PaymentStatusEnum.Pending) return false
|
||||||
|
return (
|
||||||
|
payment.method === PaymentMethodEnum.Cash ||
|
||||||
|
payment.method === PaymentMethodEnum.CreditCard
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentsList: FC<PaymentsListProps> = ({
|
||||||
|
order,
|
||||||
|
isVerifyingPayment,
|
||||||
|
verifyingPaymentId,
|
||||||
|
onVerifyPayment,
|
||||||
|
}) => {
|
||||||
|
const isCanceled = order.status === 'canceled'
|
||||||
|
const payments = order.payments ?? []
|
||||||
|
|
||||||
|
const rows: PaymentRow[] = payments.map((payment) => ({
|
||||||
|
id: payment.id,
|
||||||
|
amount: payment.amount,
|
||||||
|
method: payment.method,
|
||||||
|
status: payment.status,
|
||||||
|
referenceId: payment.referenceId,
|
||||||
|
transactionId: payment.transactionId,
|
||||||
|
paidAt: payment.paidAt,
|
||||||
|
createdAt: payment.createdAt,
|
||||||
|
description: payment.description,
|
||||||
|
attachments: payment.attachments,
|
||||||
|
canVerify: canVerifyPayment(payment, isCanceled),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const columns: ColumnType<PaymentRow>[] = [
|
||||||
|
{
|
||||||
|
title: 'مبلغ',
|
||||||
|
key: 'amount',
|
||||||
|
align: 'right',
|
||||||
|
render: (item) => formatPrice(item.amount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'روش',
|
||||||
|
key: 'method',
|
||||||
|
align: 'right',
|
||||||
|
render: (item) => getPaymentMethodText(item.method),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'وضعیت',
|
||||||
|
key: 'status',
|
||||||
|
align: 'center',
|
||||||
|
render: (item) => (
|
||||||
|
<Status
|
||||||
|
variant={getPaymentStatusVariant(item.status)}
|
||||||
|
label={getPaymentStatusLabel(item.status)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'شناسه مرجع',
|
||||||
|
key: 'referenceId',
|
||||||
|
align: 'right',
|
||||||
|
render: (item) =>
|
||||||
|
item.referenceId ? (
|
||||||
|
<span className='font-mono text-xs'>{item.referenceId}</span>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'شناسه تراکنش',
|
||||||
|
key: 'transactionId',
|
||||||
|
align: 'right',
|
||||||
|
render: (item) =>
|
||||||
|
item.transactionId ? (
|
||||||
|
<span className='font-mono text-xs'>{item.transactionId}</span>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'زمان پرداخت',
|
||||||
|
key: 'paidAt',
|
||||||
|
align: 'right',
|
||||||
|
render: (item) =>
|
||||||
|
formatOptionalDate(item.paidAt || item.createdAt, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
key: 'canVerify',
|
||||||
|
align: 'center',
|
||||||
|
render: (item) => {
|
||||||
|
if (!item.canVerify) return '-'
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label='تایید پرداخت'
|
||||||
|
onClick={() => onVerifyPayment(String(item.id))}
|
||||||
|
isloading={isVerifyingPayment && verifyingPaymentId === item.id}
|
||||||
|
className='bg-green-600 hover:bg-green-700 !py-1.5 !px-3 text-xs w-fit'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const paymentsWithReceipt = payments.filter(
|
||||||
|
(p) =>
|
||||||
|
p.method === PaymentMethodEnum.CreditCard &&
|
||||||
|
(Boolean(p.description) || Boolean(p.attachments?.length))
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
|
<div className='text-lg font-light mb-6 flex items-center gap-2'>
|
||||||
|
<Moneys color='#000' size={20} />
|
||||||
|
لیست پرداختها
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<div className='text-[13px] text-description font-light py-8 text-center'>
|
||||||
|
پرداختی برای این سفارش ثبت نشده است
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table columns={columns} data={rows} showHeader={true} className='mt-0' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{paymentsWithReceipt.length > 0 && (
|
||||||
|
<div className='mt-6 pt-6 border-t border-dashed border-border space-y-6'>
|
||||||
|
{paymentsWithReceipt.map((payment) => (
|
||||||
|
<div key={payment.id} className='space-y-3 text-[13px] font-light'>
|
||||||
|
<div className='text-description'>
|
||||||
|
جزئیات پرداخت کارت به کارت
|
||||||
|
{payment.referenceId ? ` (${payment.referenceId})` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{payment.description && (
|
||||||
|
<div>
|
||||||
|
<div className='text-description mb-2'>توضیحات</div>
|
||||||
|
<div className='text-gray-700 bg-gray-50 p-3 rounded-lg'>
|
||||||
|
{payment.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{payment.attachments && payment.attachments.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className='text-description mb-2'>پیوستها</div>
|
||||||
|
<div className='flex gap-3 flex-wrap'>
|
||||||
|
{payment.attachments.map((attachment, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={attachment}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={attachment}
|
||||||
|
alt={`پیوست پرداخت ${index + 1}`}
|
||||||
|
className='w-24 h-24 object-cover rounded-xl border border-border'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaymentsList
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { type FC, useEffect, useState } from 'react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Textarea from '@/components/Textarea'
|
||||||
|
|
||||||
|
interface RefundPaymentModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
defaultAmount: number
|
||||||
|
maxAmount: number
|
||||||
|
isLoading?: boolean
|
||||||
|
onSubmit: (amount: number, description: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RefundPaymentModal: FC<RefundPaymentModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
defaultAmount,
|
||||||
|
maxAmount,
|
||||||
|
isLoading,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const [amount, setAmount] = useState(String(defaultAmount))
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return
|
||||||
|
setAmount(String(Math.abs(defaultAmount)))
|
||||||
|
setDescription('')
|
||||||
|
setError('')
|
||||||
|
}, [isOpen, defaultAmount])
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const parsed = Number(amount)
|
||||||
|
if (!parsed || parsed <= 0) {
|
||||||
|
setError('مبلغ بازگشت وجه باید بیشتر از صفر باشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (parsed > maxAmount) {
|
||||||
|
setError(`حداکثر مبلغ قابل بازگشت ${maxAmount.toLocaleString('fa-IR')} تومان است`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!description.trim()) {
|
||||||
|
setError('توضیحات الزامی است')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSubmit(parsed, description.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={isOpen}
|
||||||
|
close={onClose}
|
||||||
|
isHeader
|
||||||
|
title_header='بازگشت وجه'
|
||||||
|
width={420}
|
||||||
|
>
|
||||||
|
<div className='mt-6 space-y-4'>
|
||||||
|
<p className='text-sm text-center text-description'>
|
||||||
|
آیا از بازگشت وجه این سفارش مطمئن هستید؟
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='مبلغ بازگشت (تومان)'
|
||||||
|
type='number'
|
||||||
|
inputMode='numeric'
|
||||||
|
min={1}
|
||||||
|
max={maxAmount}
|
||||||
|
seprator
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAmount(e.target.value)
|
||||||
|
setError('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
placeholder='توضیحات'
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => {
|
||||||
|
setDescription(e.target.value)
|
||||||
|
setError('')
|
||||||
|
}}
|
||||||
|
className='bg-transparent border border-gray-500 rounded-xl w-full p-2 min-h-[80px]'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <p className='text-xs text-red-500 text-center'>{error}</p>}
|
||||||
|
|
||||||
|
<div className='flex gap-4 justify-center pt-4'>
|
||||||
|
<Button
|
||||||
|
label='تایید'
|
||||||
|
onClick={handleSubmit}
|
||||||
|
isloading={isLoading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label='انصراف'
|
||||||
|
className='bg-transparent text-black border border-primary'
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RefundPaymentModal
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/OrderService";
|
import * as api from "../service/OrderService";
|
||||||
import type { GetOrdersParams, ChangeOrderStatusParams, RefundPaymentParams } from "../service/OrderService";
|
import type { GetOrdersParams, ChangeOrderStatusParams, RefundPaymentParams } from "../service/OrderService";
|
||||||
import type { UpdateOrderFeesType } from "../types/Types";
|
import type { UpdateOrderFeesType, AddOrderItemType, UpdateOrderItemQuantityType } from "../types/Types";
|
||||||
|
|
||||||
export const useCreateOrder = () => {
|
export const useCreateOrder = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -90,3 +90,56 @@ export const useUpdateOrderFees = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAddOrderItem = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
orderId,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
orderId: string;
|
||||||
|
params: AddOrderItemType;
|
||||||
|
}) => api.addOrderItem(orderId, params),
|
||||||
|
onSuccess: (_, { orderId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["order", orderId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateOrderItemQuantity = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
orderId,
|
||||||
|
itemId,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
orderId: string;
|
||||||
|
itemId: string;
|
||||||
|
params: UpdateOrderItemQuantityType;
|
||||||
|
}) => api.updateOrderItemQuantity(orderId, itemId, params),
|
||||||
|
onSuccess: (_, { orderId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["order", orderId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveOrderItem = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
orderId,
|
||||||
|
itemId,
|
||||||
|
}: {
|
||||||
|
orderId: string;
|
||||||
|
itemId: string;
|
||||||
|
}) => api.removeOrderItem(orderId, itemId),
|
||||||
|
onSuccess: (_, { orderId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["order", orderId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type {
|
|||||||
CreateOrderType,
|
CreateOrderType,
|
||||||
CreateOrderResponse,
|
CreateOrderResponse,
|
||||||
UpdateOrderFeesType,
|
UpdateOrderFeesType,
|
||||||
|
AddOrderItemType,
|
||||||
|
UpdateOrderItemQuantityType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export interface GetOrdersParams {
|
export interface GetOrdersParams {
|
||||||
@@ -22,6 +24,7 @@ export interface ChangeOrderStatusParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RefundPaymentParams {
|
export interface RefundPaymentParams {
|
||||||
|
amount: number;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +115,39 @@ export const updateOrderFees = async (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addOrderItem = async (
|
||||||
|
orderId: string,
|
||||||
|
params: AddOrderItemType
|
||||||
|
): Promise<GetOrderByIdResponse> => {
|
||||||
|
const { data } = await axios.post<GetOrderByIdResponse>(
|
||||||
|
`/admin/orders/${orderId}/items`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateOrderItemQuantity = async (
|
||||||
|
orderId: string,
|
||||||
|
itemId: string,
|
||||||
|
params: UpdateOrderItemQuantityType
|
||||||
|
): Promise<GetOrderByIdResponse> => {
|
||||||
|
const { data } = await axios.patch<GetOrderByIdResponse>(
|
||||||
|
`/admin/orders/${orderId}/items/${itemId}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeOrderItem = async (
|
||||||
|
orderId: string,
|
||||||
|
itemId: string
|
||||||
|
): Promise<GetOrderByIdResponse> => {
|
||||||
|
const { data } = await axios.delete<GetOrderByIdResponse>(
|
||||||
|
`/admin/orders/${orderId}/items/${itemId}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export interface GetFoodOrderReportParams {
|
export interface GetFoodOrderReportParams {
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
|||||||
@@ -210,11 +210,23 @@ export interface Order extends RowDataType {
|
|||||||
packingFee: number;
|
packingFee: number;
|
||||||
prepareTime?: number | null;
|
prepareTime?: number | null;
|
||||||
total: number;
|
total: number;
|
||||||
|
paidAmount: number;
|
||||||
|
balance: number;
|
||||||
|
refundedAmount: number;
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
description: string;
|
description: string;
|
||||||
tableNumber: string | null;
|
tableNumber: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
paymentStatus?: string;
|
paymentStatus?: string;
|
||||||
|
cashShift?: {
|
||||||
|
id: string;
|
||||||
|
admin?: {
|
||||||
|
id: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
phone?: string;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
history: OrderHistory[];
|
history: OrderHistory[];
|
||||||
payments: OrderPayment[];
|
payments: OrderPayment[];
|
||||||
items: OrderItem[];
|
items: OrderItem[];
|
||||||
@@ -226,6 +238,15 @@ export interface UpdateOrderFeesType {
|
|||||||
prepareTime?: number;
|
prepareTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddOrderItemType {
|
||||||
|
foodId: string;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateOrderItemQuantityType {
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type PaginationMeta = {
|
export type PaginationMeta = {
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user