return order + success page return order + ...
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { FC, useState } from 'react'
|
||||
import { ArrowRight2 } from 'iconsax-react'
|
||||
import { ArrowLeft, TaskSquare } from 'iconsax-react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import Layout from '@/hoc/Layout'
|
||||
@@ -10,6 +10,7 @@ import OrderMainContent from './components/OrderMainContent'
|
||||
import CancelOrderModal from './components/CancelOrderModal'
|
||||
import { useCancelOrder, useGetOrderDetail } from '../hooks/useOrdersData'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { PRIMARY_COLOR } from '@/config/const'
|
||||
|
||||
|
||||
const OrderDetail: FC = () => {
|
||||
@@ -66,14 +67,21 @@ const OrderDetail: FC = () => {
|
||||
<Menu pageActive='orders' />
|
||||
|
||||
<div className='border p-6 rounded-2xl text-sm mt-7'>
|
||||
<div className='flex gap-2 items-center border-b pb-6'>
|
||||
<button
|
||||
onClick={() => router.push('/profile/orders')}
|
||||
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
||||
>
|
||||
<ArrowRight2 size={20} className="rotate-180" />
|
||||
</button>
|
||||
<div className="text-lg font-semibold">جزئیات سفارش</div>
|
||||
<div className='flex justify-between items-center border-b pb-6'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<button
|
||||
onClick={() => router.push('/profile/orders')}
|
||||
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
||||
>
|
||||
<ArrowLeft color='black' size={20} className="rotate-180" />
|
||||
</button>
|
||||
<div className="text-lg font-semibold">جزئیات سفارش</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2 items-center cursor-pointer' onClick={() => router.push(`/profile/orders/${id}/return`)}>
|
||||
<TaskSquare color={PRIMARY_COLOR} size={20} />
|
||||
<div className='text-primary'>فرم درخواست مرجوعی</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cancelOrderMutation.isPending ? (
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { ShipmentItemDetail } from '../../../types/Types'
|
||||
|
||||
interface ProductSelectionProps {
|
||||
items: ShipmentItemDetail[]
|
||||
selectedItems: string[]
|
||||
onItemSelect: (itemId: string, checked: boolean) => void
|
||||
}
|
||||
|
||||
const ProductSelection: React.FC<ProductSelectionProps> = ({
|
||||
items,
|
||||
selectedItems,
|
||||
onItemSelect
|
||||
}) => {
|
||||
return (
|
||||
<div className='space-y-0'>
|
||||
{items.length === 0 && (
|
||||
<div className='text-center py-8 text-gray-500'>
|
||||
هیچ آیتمی برای نمایش وجود ندارد
|
||||
</div>
|
||||
)}
|
||||
{items.map((item, index) => {
|
||||
const availableQuantity = item.quantity - item.cancelled_quantity - item.returned_quantity
|
||||
const canReturn = availableQuantity > 0
|
||||
|
||||
return (
|
||||
<div key={item._id}>
|
||||
<div className={`p-4 ${!canReturn ? 'opacity-60' : ''}`}>
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<Checkbox
|
||||
id={item._id}
|
||||
checked={selectedItems.includes(item._id)}
|
||||
onCheckedChange={(checked) => onItemSelect(item._id, checked as boolean)}
|
||||
disabled={!canReturn}
|
||||
className={`w-5 h-5 ${!canReturn ? 'opacity-70' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
width={80}
|
||||
height={80}
|
||||
className='w-20 h-20 object-cover rounded-lg border flex-shrink-0'
|
||||
alt={item.product.title_fa}
|
||||
/>
|
||||
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-medium text-sm leading-relaxed mb-2'>
|
||||
{item.product.title_fa}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mb-2'>
|
||||
<div className='w-3 h-3 bg-gray-800 rounded-full'></div>
|
||||
<span className='text-xs text-gray-600'>مشکی</span>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-gray-500 mb-1'>
|
||||
کد کالا: {item.product._id || 'نامشخص'}
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-gray-500 mb-1'>
|
||||
فروشنده: سندس کالا
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-gray-500 mb-2'>
|
||||
وضعیت: آونگ
|
||||
</div>
|
||||
|
||||
<div className='text-sm font-bold text-primary'>
|
||||
{Intl.NumberFormat('fa-IR').format(item.selling_price)} تومان
|
||||
</div>
|
||||
|
||||
{!canReturn && (
|
||||
<div className='text-xs text-red-500 mt-2'>
|
||||
این آیتم قابل مرجوع کردن نیست
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{index < items.length - 1 && (
|
||||
<div className='border-t border-gray-200'></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductSelection
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ShipmentItemDetail } from '../../../types/Types'
|
||||
|
||||
interface ReturnConfirmationProps {
|
||||
selectedItems: ShipmentItemDetail[]
|
||||
quantities: { [key: string]: number }
|
||||
reason: string
|
||||
description: string
|
||||
images: File[]
|
||||
imagePreviewUrls: string[]
|
||||
returnReasons: Array<{ _id: string; title: string }>
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
|
||||
selectedItems,
|
||||
quantities,
|
||||
reason,
|
||||
description,
|
||||
// images,
|
||||
imagePreviewUrls,
|
||||
returnReasons,
|
||||
onSubmit,
|
||||
onBack,
|
||||
isLoading
|
||||
}) => {
|
||||
const selectedReason = returnReasons.find(r => r._id === reason)
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<h3 className='text-lg font-semibold'>تایید اطلاعات مرجوعی</h3>
|
||||
|
||||
{/* محصولات انتخاب شده */}
|
||||
<div className='space-y-4'>
|
||||
<h4 className='font-medium'>محصولات انتخاب شده</h4>
|
||||
{selectedItems.map((item) => (
|
||||
<div key={item._id} className='p-4 border rounded-lg'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
width={60}
|
||||
height={60}
|
||||
className='w-15 h-15 object-cover rounded-lg border flex-shrink-0'
|
||||
alt={item.product.title_fa}
|
||||
/>
|
||||
<div className='flex-1'>
|
||||
<div className='font-medium'>{item.product.title_fa}</div>
|
||||
<div className='text-sm text-gray-600 mt-1'>
|
||||
تعداد مرجوعی: {quantities[item._id]}
|
||||
</div>
|
||||
<div className='text-sm text-primary font-medium mt-1'>
|
||||
{Intl.NumberFormat('fa-IR').format(item.selling_price)} تومان
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* اطلاعات مرجوعی */}
|
||||
<div className='space-y-4'>
|
||||
<h4 className='font-medium'>اطلاعات مرجوعی</h4>
|
||||
|
||||
<div className='p-4 border rounded-lg space-y-3'>
|
||||
<div>
|
||||
<span className='font-medium text-sm'>دلیل مرجوعی: </span>
|
||||
<span className='text-sm'>{selectedReason?.title}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className='font-medium text-sm'>توضیحات: </span>
|
||||
<p className='text-sm text-gray-700 mt-1 leading-relaxed'>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* تصاویر آپلود شده */}
|
||||
{imagePreviewUrls.length > 0 && (
|
||||
<div className='space-y-4'>
|
||||
<h4 className='font-medium'>تصاویر آپلود شده</h4>
|
||||
<div className='grid grid-cols-2 md:grid-cols-3 gap-4'>
|
||||
{imagePreviewUrls.map((url, index) => (
|
||||
<div key={index} className='relative'>
|
||||
<Image
|
||||
src={url}
|
||||
width={100}
|
||||
height={100}
|
||||
className='max-w-[200px] max-h-[200px] object-contain rounded border'
|
||||
alt={`تصویر ${index + 1}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* اخطار */}
|
||||
<div className='flex gap-2 text-xs text-[#7F7F7F] p-3 bg-primary/10 rounded-lg'>
|
||||
<div className="size-5 bg-primary/20 rounded-full flex items-center justify-center">
|
||||
<span className="text-primary text-xs">!</span>
|
||||
</div>
|
||||
<span>
|
||||
لطفاً قبل از تایید نهایی، تمامی اطلاعات را بررسی کنید. پس از ثبت درخواست مرجوعی، امکان تغییر اطلاعات وجود ندارد.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* دکمههای عملیات */}
|
||||
<div className='flex gap-3 justify-center'>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onBack}
|
||||
>
|
||||
بازگشت
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
تایید و ثبت مرجوعی
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReturnConfirmation
|
||||
@@ -0,0 +1,257 @@
|
||||
'use client'
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import * as yup from 'yup'
|
||||
import Select from '@/components/Select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ShipmentItemDetail } from '../../../types/Types'
|
||||
import Image from 'next/image'
|
||||
import { CloseCircle, DocumentUpload } from 'iconsax-react'
|
||||
|
||||
interface ReturnFormData {
|
||||
quantities: { [key: string]: number }
|
||||
reason: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface ReturnFormProps {
|
||||
selectedItems: ShipmentItemDetail[]
|
||||
onSubmit: (data: { quantities: { [key: string]: number }; reason: string; description: string; images: File[] }) => void
|
||||
onBack: () => void
|
||||
isLoading: boolean
|
||||
returnReasons: Array<{ _id: string; title: string }>
|
||||
}
|
||||
|
||||
const returnFormSchema = yup.object({
|
||||
reason: yup.string().required('دلیل مرجوعی را انتخاب کنید'),
|
||||
description: yup.string().required('توضیحات را وارد کنید').min(10, 'توضیحات باید حداقل ۱۰ کاراکتر باشد'),
|
||||
quantities: yup.object().test('quantities', 'تعداد مرجوعی باید معتبر باشد', function (value: Record<string, number>) {
|
||||
if (!value) return false
|
||||
const selectedItems = this.parent.selectedItems || []
|
||||
for (const item of selectedItems) {
|
||||
const quantity = value[item._id]
|
||||
const availableQuantity = item.quantity - item.cancelled_quantity - item.returned_quantity
|
||||
if (!quantity || quantity < 1 || quantity > availableQuantity) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const ReturnForm: React.FC<ReturnFormProps> = ({
|
||||
selectedItems,
|
||||
onSubmit,
|
||||
onBack,
|
||||
isLoading,
|
||||
returnReasons
|
||||
}) => {
|
||||
const [images, setImages] = useState<File[]>([])
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
reset
|
||||
} = useForm<ReturnFormData>({
|
||||
resolver: yupResolver(returnFormSchema),
|
||||
defaultValues: {
|
||||
quantities: selectedItems.reduce((acc, item) => ({
|
||||
...acc,
|
||||
[item._id]: 1
|
||||
}), {})
|
||||
}
|
||||
})
|
||||
|
||||
const handleFormSubmit = (data: ReturnFormData) => {
|
||||
onSubmit({
|
||||
quantities: data.quantities,
|
||||
reason: data.reason,
|
||||
description: data.description,
|
||||
images
|
||||
})
|
||||
reset()
|
||||
}
|
||||
|
||||
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || [])
|
||||
const validFiles = files.filter(file => {
|
||||
const isValidType = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
const isValidSize = file.size <= 10 * 1024 * 1024 // 10MB
|
||||
return isValidType && isValidSize
|
||||
})
|
||||
|
||||
if (validFiles.length !== files.length) {
|
||||
alert('فقط فایلهای JPG و PNG با حداکثر حجم ۱۰ مگابایت مجاز هستند')
|
||||
}
|
||||
|
||||
const newImages = [...images, ...validFiles].slice(0, 5) // حداکثر ۵ عکس
|
||||
setImages(newImages)
|
||||
|
||||
const newPreviewUrls = newImages.map(file => URL.createObjectURL(file))
|
||||
setImagePreviewUrls(newPreviewUrls)
|
||||
}
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
const newImages = images.filter((_, i) => i !== index)
|
||||
const newPreviewUrls = imagePreviewUrls.filter((_, i) => i !== index)
|
||||
setImages(newImages)
|
||||
setImagePreviewUrls(newPreviewUrls)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<h3 className='text-lg font-semibold'>تکمیل اطلاعات مرجوعی</h3>
|
||||
|
||||
{/* انتخاب تعداد مرجوعی برای هر آیتم */}
|
||||
<div className='space-y-4'>
|
||||
<h4 className='font-medium'>تعداد مرجوعی</h4>
|
||||
{selectedItems.map((item) => {
|
||||
const availableQuantity = item.quantity - item.cancelled_quantity - item.returned_quantity
|
||||
return (
|
||||
<div key={item._id} className='p-4 border rounded-lg'>
|
||||
<div className='flex items-center gap-4 mb-2'>
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
width={40}
|
||||
height={40}
|
||||
className='w-10 h-10 object-cover rounded border'
|
||||
alt={item.product.title_fa}
|
||||
/>
|
||||
<div className='flex-1'>
|
||||
<div className='font-medium text-sm'>{item.product.title_fa}</div>
|
||||
<div className='text-xs text-gray-600'>قابل مرجوع: {availableQuantity}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<label className='text-sm'>تعداد:</label>
|
||||
<input
|
||||
type='number'
|
||||
min={1}
|
||||
max={availableQuantity}
|
||||
defaultValue={1}
|
||||
{...register(`quantities.${item._id}`, {
|
||||
valueAsNumber: true,
|
||||
min: { value: 1, message: 'حداقل تعداد ۱ است' },
|
||||
max: { value: availableQuantity, message: `حداکثر تعداد ${availableQuantity} است` }
|
||||
})}
|
||||
className='w-20 px-2 py-1 border rounded text-center'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className='space-y-6'>
|
||||
{/* انتخاب دلیل مرجوعی */}
|
||||
<Select
|
||||
placeholder='دلیل مرجوعی'
|
||||
items={returnReasons.map((item) => ({
|
||||
label: item.title,
|
||||
value: item._id
|
||||
}))}
|
||||
className='bg-white border'
|
||||
value={watch('reason')}
|
||||
name='reason'
|
||||
onChange={(e) => setValue('reason', e.target.value)}
|
||||
error_text={errors.reason?.message}
|
||||
/>
|
||||
|
||||
{/* توضیحات */}
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-2">توضیحات :</div>
|
||||
<textarea
|
||||
{...register('description')}
|
||||
className='h-[123px] rounded-xl p-3 text-sm border w-full resize-none focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||
placeholder='توضیحات خود را بنویسید (حداقل ۱۰ کاراکتر)'
|
||||
/>
|
||||
{errors.description && (
|
||||
<div className='text-xs text-red-500 mt-1'>
|
||||
{errors.description.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* آپلود عکس */}
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-2">آپلود عکس</div>
|
||||
<div className='text-xs text-gray-600 mb-3'>
|
||||
اگر ایراد کالا قابل مشاهده است به صورتی عکس بگیرید که مشخص باشد (با فرمت JPG یا PNG و هر عکس حداکثر ۱۰ مگابایت)
|
||||
</div>
|
||||
|
||||
{/* نمایش عکسهای آپلود شده */}
|
||||
{imagePreviewUrls.length > 0 && (
|
||||
<div className='grid grid-cols-2 md:grid-cols-3 gap-4 mb-4'>
|
||||
{imagePreviewUrls.map((url, index) => (
|
||||
<div key={index} className='relative'>
|
||||
<Image
|
||||
src={url}
|
||||
width={100}
|
||||
height={100}
|
||||
className='max-w-[200px] max-h-[200px] object-contain rounded border'
|
||||
alt={`تصویر ${index + 1}`}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => removeImage(index)}
|
||||
className='absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center'
|
||||
>
|
||||
<CloseCircle color='white' size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* دکمه آپلود */}
|
||||
{images.length < 5 && (
|
||||
<div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='image/jpeg,image/png'
|
||||
multiple
|
||||
onChange={handleImageUpload}
|
||||
className='hidden'
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className='flex items-center gap-2 px-4 py-2 border-2 border-dashed border-gray-300 rounded-lg hover:border-primary transition-colors'
|
||||
>
|
||||
<DocumentUpload color='gray' size={20} />
|
||||
<span>انتخاب عکس</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* دکمههای عملیات */}
|
||||
<div className='flex gap-3 justify-center'>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onBack}
|
||||
>
|
||||
بازگشت
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
ادامه
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReturnForm
|
||||
@@ -1,8 +1,227 @@
|
||||
import { NextPage } from 'next'
|
||||
'use client'
|
||||
import Layout from '@/hoc/Layout'
|
||||
import { type NextPage } from 'next'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useGetOrderDetail, useReturnOrder, useGetReturnReasons } from '../../hooks/useOrdersData'
|
||||
import ProductSelection from './components/ProductSelection'
|
||||
import ReturnForm from './components/ReturnForm'
|
||||
import ReturnConfirmation from './components/ReturnConfirmation'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { extractErrorMessage } from '@/helpers/errorUtils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
type Step = 1 | 2 | 3
|
||||
|
||||
interface FormData {
|
||||
quantities: { [key: string]: number }
|
||||
reason: string
|
||||
description: string
|
||||
images: File[]
|
||||
}
|
||||
|
||||
const ReturnPage: NextPage = () => {
|
||||
const { id } = useParams()
|
||||
const router = useRouter()
|
||||
const { data: order, isLoading: isOrderLoading } = useGetOrderDetail(id as string)
|
||||
const returnOrderMutation = useReturnOrder()
|
||||
const { data: returnReasons } = useGetReturnReasons()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<Step>(1)
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
const [formData, setFormData] = useState<FormData | null>(null)
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([])
|
||||
|
||||
const handleItemSelect = (itemId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedItems(prev => [...prev, itemId])
|
||||
} else {
|
||||
setSelectedItems(prev => prev.filter(id => id !== itemId))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSubmit = (data: FormData) => {
|
||||
setFormData(data)
|
||||
// ایجاد preview URLs برای تصاویر
|
||||
const previewUrls = data.images.map(file => URL.createObjectURL(file))
|
||||
setImagePreviewUrls(previewUrls)
|
||||
setCurrentStep(3)
|
||||
}
|
||||
|
||||
const handleFinalSubmit = async () => {
|
||||
if (!formData || selectedItems.length === 0) {
|
||||
toast('اطلاعات ناقص است', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// تبدیل تصاویر به base64 یا URL
|
||||
const imagePromises = formData.images.map(async (file) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
})
|
||||
|
||||
const imageUrls = await Promise.all(imagePromises)
|
||||
|
||||
const returnItems = selectedItems.map(itemId => {
|
||||
// Find the order item that contains this shipment item
|
||||
const orderItem = order?.results.order.orderItems.find(orderItem =>
|
||||
orderItem.shipmentItems.some(shipmentItem => shipmentItem._id === itemId)
|
||||
)
|
||||
|
||||
const shipmentItem = orderItem?.shipmentItems.find(item => item._id === itemId)
|
||||
|
||||
if (!orderItem || !shipmentItem) return null
|
||||
|
||||
return {
|
||||
orderItemId: orderItem._id,
|
||||
shipmentItemId: shipmentItem._id,
|
||||
quantity: formData.quantities[itemId],
|
||||
comment: formData.description,
|
||||
reason: formData.reason,
|
||||
images: imageUrls
|
||||
}
|
||||
}).filter(Boolean) as { orderItemId: string; shipmentItemId: string; quantity: number; comment: string; reason: string; images: string[] }[]
|
||||
|
||||
await returnOrderMutation.mutateAsync({
|
||||
orderId: Number(id),
|
||||
items: returnItems
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast('درخواست مرجوعی با موفقیت ثبت شد', 'success')
|
||||
router.push(`/profile/orders/${id}/return/success`)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBackToStep1 = () => {
|
||||
setCurrentStep(1)
|
||||
setFormData(null)
|
||||
setImagePreviewUrls([])
|
||||
}
|
||||
|
||||
const handleBackToStep2 = () => {
|
||||
setCurrentStep(2)
|
||||
}
|
||||
|
||||
// Flatten all shipment items from all order items
|
||||
const allShipmentItems = order?.results.order.orderItems
|
||||
.flatMap(orderItem => orderItem.shipmentItems) || []
|
||||
|
||||
// Filter selected shipment items
|
||||
const selectedShipmentItems = allShipmentItems.filter(item =>
|
||||
selectedItems.includes(item._id)
|
||||
)
|
||||
|
||||
if (isOrderLoading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className='mt-14 px-4 sm:px-8 lg:px-20'>
|
||||
<div className='flex justify-center items-center min-h-[400px]'>
|
||||
<div className='text-center'>
|
||||
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4'></div>
|
||||
<p>در حال بارگذاری...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className='mt-14 px-4 sm:px-8 lg:px-20'>
|
||||
<div className='text-center min-h-[400px] flex items-center justify-center'>
|
||||
<p>سفارش یافت نشد</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>page</div>
|
||||
<Layout>
|
||||
<div className='mt-14 px-4 sm:px-8 lg:px-20 mx-auto'>
|
||||
{/* Header */}
|
||||
<div className='mb-6'>
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<h1 className='text-xl font-bold'>انتخاب کالاهای مرجوعی</h1>
|
||||
<div className='text-sm text-gray-600'>شناسه سفارش: {order.results.order._id}</div>
|
||||
</div>
|
||||
<div className='border-t border-gray-200 mb-4'></div>
|
||||
|
||||
{/* Warning Message */}
|
||||
<div className='flex items-start gap-3 p-4 bg-orange-50 border border-orange-200 rounded-lg mb-6'>
|
||||
<div className='w-5 h-5 bg-orange-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
|
||||
<span className='text-white text-xs font-bold'>!</span>
|
||||
</div>
|
||||
<p className='text-sm text-gray-700 leading-relaxed'>
|
||||
دقت کنید که برچسب سریال کالا (چسبیده روی بسته) را سالم نگه دارید و کالای اصلی را با تمام لوازم در بسته بندی اولیه قرار دهید
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='space-y-8'>
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
<ProductSelection
|
||||
items={allShipmentItems}
|
||||
selectedItems={selectedItems}
|
||||
onItemSelect={handleItemSelect}
|
||||
/>
|
||||
<div className='flex gap-3 mt-8 justify-center'>
|
||||
<Button
|
||||
onClick={() => router.back()}
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
بازگشت
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCurrentStep(2)}
|
||||
disabled={selectedItems.length === 0}
|
||||
size="default"
|
||||
>
|
||||
ادامه
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<ReturnForm
|
||||
selectedItems={selectedShipmentItems}
|
||||
onSubmit={handleFormSubmit}
|
||||
onBack={handleBackToStep1}
|
||||
isLoading={false}
|
||||
returnReasons={returnReasons?.results.reasons || []}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && formData && (
|
||||
<ReturnConfirmation
|
||||
selectedItems={selectedShipmentItems}
|
||||
quantities={formData.quantities}
|
||||
reason={formData.reason}
|
||||
description={formData.description}
|
||||
images={formData.images}
|
||||
imagePreviewUrls={imagePreviewUrls}
|
||||
returnReasons={returnReasons?.results.reasons || []}
|
||||
onSubmit={handleFinalSubmit}
|
||||
onBack={handleBackToStep2}
|
||||
isLoading={returnOrderMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Layout from "@/hoc/Layout"
|
||||
import { NextPage } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
const SuccesPage: NextPage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<div className='w-full lg:px-10 px-4 mt-20 '>
|
||||
<div className="shadow-lg rounded-2xl p-6 max-w-4xl mx-auto flex justify-between items-center">
|
||||
<div className="flex-1">
|
||||
<div className="text-[#019907]">
|
||||
درخواست مرجوعی ثبت شد
|
||||
</div>
|
||||
|
||||
<p className="mt-5 text-[#7F7F7F] font-light text-sm">
|
||||
درخواست شما درحال بررسی است در صورت تایید یا عدم تایید درخواست، با پیامک به شما اطلاع میدهیم
|
||||
</p>
|
||||
|
||||
<Link href="/profile/orders">
|
||||
<Button className="mt-20">پیگیری درخواست</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Image
|
||||
src="/images/success.png"
|
||||
alt="return-success"
|
||||
width={200}
|
||||
height={130}
|
||||
className="max-w-[200px] max-h-[130px] h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuccesPage
|
||||
@@ -30,3 +30,16 @@ export const useGetCancelReasons = () => {
|
||||
queryFn: api.getCancelReasons,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetReturnReasons = () => {
|
||||
return useQuery({
|
||||
queryKey: ["returnReasons"],
|
||||
queryFn: api.getReturnReasons,
|
||||
});
|
||||
};
|
||||
|
||||
export const useReturnOrder = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.returnOrder,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
OrderDetailResponse,
|
||||
CancelOrderType,
|
||||
CancelReasonsResponse,
|
||||
ReturnOrderType,
|
||||
ReturnReasonsResponse,
|
||||
} from "../types/Types";
|
||||
|
||||
export const getOrders = async (activeTab: string): Promise<OrdersResponse> => {
|
||||
@@ -34,3 +36,13 @@ export const cancelOrder = async (params: CancelOrderType) => {
|
||||
const { data } = await axios.post(`/cancel/user`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getReturnReasons = async (): Promise<ReturnReasonsResponse> => {
|
||||
const { data } = await axios.get(`/returns/reasons`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const returnOrder = async (params: ReturnOrderType) => {
|
||||
const { data } = await axios.post(`/returns/user`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -313,3 +313,31 @@ export interface CancelOrderType {
|
||||
orderId: number;
|
||||
items: CancelOrderItem[];
|
||||
}
|
||||
|
||||
export interface ReturnReason {
|
||||
_id: string;
|
||||
title: string;
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface ReturnReasonsResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
reasons: ReturnReason[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReturnOrderItem {
|
||||
orderItemId: string;
|
||||
shipmentItemId: string;
|
||||
quantity: number;
|
||||
comment: string;
|
||||
reason: string;
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export interface ReturnOrderType {
|
||||
orderId: number;
|
||||
items: ReturnOrderItem[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user