return order
This commit is contained in:
@@ -44,7 +44,7 @@ const Resume: FC<ResumeProps> = ({ jobId }) => {
|
||||
const uploadFileMutation = useMutation({
|
||||
mutationFn: api.uploadSingleFile,
|
||||
onSuccess: (data) => {
|
||||
setValue('resumeUrl', data?.results?.url?.url)
|
||||
setValue('resumeUrl', data?.results?.data?.url?.url)
|
||||
toast('فایل با موفقیت آپلود شد', 'success')
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
@@ -36,17 +36,20 @@ export type ResumeType = {
|
||||
};
|
||||
|
||||
export interface FileUrl {
|
||||
originalname: string;
|
||||
size: number;
|
||||
url: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface UploadResults {
|
||||
export interface UploadData {
|
||||
message: string;
|
||||
url: FileUrl;
|
||||
}
|
||||
|
||||
export interface UploadResults {
|
||||
data: UploadData;
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
|
||||
@@ -9,8 +9,8 @@ interface ReturnConfirmationProps {
|
||||
quantities: { [key: string]: number }
|
||||
reason: string
|
||||
description: string
|
||||
images: File[]
|
||||
imagePreviewUrls: string[]
|
||||
images: { [key: string]: File }
|
||||
imagePreviewUrls: { [key: string]: string }
|
||||
returnReasons: Array<{ _id: string; title: string }>
|
||||
onSubmit: () => void
|
||||
onBack: () => void
|
||||
@@ -39,7 +39,7 @@ const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
|
||||
<div className='space-y-4'>
|
||||
<h4 className='font-medium'>محصولات انتخاب شده</h4>
|
||||
{selectedItems.map((item) => (
|
||||
<div key={item._id} className='p-4 border rounded-lg'>
|
||||
<div key={item._id} className='p-4 border rounded-lg space-y-3'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
@@ -58,6 +58,19 @@ const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* تصویر این آیتم */}
|
||||
{imagePreviewUrls[item._id] && (
|
||||
<div>
|
||||
<div className='text-xs text-gray-600 mb-2'>تصویر آپلود شده:</div>
|
||||
<Image
|
||||
src={imagePreviewUrls[item._id]}
|
||||
width={100}
|
||||
height={100}
|
||||
className='max-w-[150px] max-h-[150px] object-contain rounded border'
|
||||
alt='تصویر محصول'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -79,26 +92,6 @@ const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
|
||||
</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">
|
||||
|
||||
@@ -17,7 +17,7 @@ interface ReturnFormData {
|
||||
|
||||
interface ReturnFormProps {
|
||||
selectedItems: ShipmentItemDetail[]
|
||||
onSubmit: (data: { quantities: { [key: string]: number }; reason: string; description: string; images: File[] }) => void
|
||||
onSubmit: (data: { quantities: { [key: string]: number }; reason: string; description: string; images: { [key: string]: File } }) => void
|
||||
onBack: () => void
|
||||
isLoading: boolean
|
||||
returnReasons: Array<{ _id: string; title: string }>
|
||||
@@ -47,9 +47,8 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
|
||||
isLoading,
|
||||
returnReasons
|
||||
}) => {
|
||||
const [images, setImages] = useState<File[]>([])
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [images, setImages] = useState<{ [key: string]: File }>({})
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<{ [key: string]: string }>({})
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -78,30 +77,33 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
|
||||
reset()
|
||||
}
|
||||
|
||||
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || [])
|
||||
const validFiles = files.filter(file => {
|
||||
const handleImageUpload = (itemId: string, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
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) {
|
||||
if (!isValidType || !isValidSize) {
|
||||
alert('فقط فایلهای JPG و PNG با حداکثر حجم ۱۰ مگابایت مجاز هستند')
|
||||
return
|
||||
}
|
||||
|
||||
const newImages = [...images, ...validFiles].slice(0, 5) // حداکثر ۵ عکس
|
||||
setImages(newImages)
|
||||
|
||||
const newPreviewUrls = newImages.map(file => URL.createObjectURL(file))
|
||||
setImagePreviewUrls(newPreviewUrls)
|
||||
setImages(prev => ({ ...prev, [itemId]: file }))
|
||||
setImagePreviewUrls(prev => ({ ...prev, [itemId]: URL.createObjectURL(file) }))
|
||||
}
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
const newImages = images.filter((_, i) => i !== index)
|
||||
const newPreviewUrls = imagePreviewUrls.filter((_, i) => i !== index)
|
||||
setImages(newImages)
|
||||
setImagePreviewUrls(newPreviewUrls)
|
||||
const removeImage = (itemId: string) => {
|
||||
setImages(prev => {
|
||||
const newImages = { ...prev }
|
||||
delete newImages[itemId]
|
||||
return newImages
|
||||
})
|
||||
setImagePreviewUrls(prev => {
|
||||
const newUrls = { ...prev }
|
||||
delete newUrls[itemId]
|
||||
return newUrls
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -110,12 +112,12 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
|
||||
|
||||
{/* انتخاب تعداد مرجوعی برای هر آیتم */}
|
||||
<div className='space-y-4'>
|
||||
<h4 className='font-medium'>تعداد مرجوعی</h4>
|
||||
<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'>
|
||||
<div key={item._id} className='p-4 border rounded-lg space-y-3'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
width={40}
|
||||
@@ -143,6 +145,48 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
|
||||
className='w-20 px-2 py-1 border rounded text-center'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* آپلود تصویر برای این آیتم */}
|
||||
<div>
|
||||
<div className='text-xs text-gray-600 mb-2'>
|
||||
تصویر محصول (JPG یا PNG، حداکثر ۱۰ مگابایت)
|
||||
</div>
|
||||
{imagePreviewUrls[item._id] ? (
|
||||
<div className='relative inline-block'>
|
||||
<Image
|
||||
src={imagePreviewUrls[item._id]}
|
||||
width={100}
|
||||
height={100}
|
||||
className='max-w-[150px] max-h-[150px] object-contain rounded border'
|
||||
alt='تصویر محصول'
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => removeImage(item._id)}
|
||||
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>
|
||||
<input
|
||||
type='file'
|
||||
accept='image/jpeg,image/png'
|
||||
onChange={(e) => handleImageUpload(item._id, e)}
|
||||
className='hidden'
|
||||
id={`file-${item._id}`}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`file-${item._id}`}
|
||||
className='flex items-center gap-2 px-4 py-2 border-2 border-dashed border-gray-300 rounded-lg hover:border-primary transition-colors cursor-pointer inline-flex'
|
||||
>
|
||||
<DocumentUpload color='gray' size={20} />
|
||||
<span className='text-sm'>انتخاب تصویر</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -178,60 +222,6 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
|
||||
)}
|
||||
</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
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { useGetOrderDetail, useReturnOrder, useGetReturnReasons, useUploadReturnImage } from '../../hooks/useOrdersData'
|
||||
import ProductSelection from './components/ProductSelection'
|
||||
import ReturnForm from './components/ReturnForm'
|
||||
import ReturnConfirmation from './components/ReturnConfirmation'
|
||||
@@ -17,7 +17,7 @@ interface FormData {
|
||||
quantities: { [key: string]: number }
|
||||
reason: string
|
||||
description: string
|
||||
images: File[]
|
||||
images: { [key: string]: File }
|
||||
}
|
||||
|
||||
const ReturnPage: NextPage = () => {
|
||||
@@ -26,11 +26,12 @@ const ReturnPage: NextPage = () => {
|
||||
const { data: order, isLoading: isOrderLoading } = useGetOrderDetail(id as string)
|
||||
const returnOrderMutation = useReturnOrder()
|
||||
const { data: returnReasons } = useGetReturnReasons()
|
||||
const uploadImageMutation = useUploadReturnImage()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<Step>(1)
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
||||
const [formData, setFormData] = useState<FormData | null>(null)
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([])
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState<{ [key: string]: string }>({})
|
||||
|
||||
const handleItemSelect = (itemId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
@@ -43,7 +44,10 @@ const ReturnPage: NextPage = () => {
|
||||
const handleFormSubmit = (data: FormData) => {
|
||||
setFormData(data)
|
||||
// ایجاد preview URLs برای تصاویر
|
||||
const previewUrls = data.images.map(file => URL.createObjectURL(file))
|
||||
const previewUrls: { [key: string]: string } = {}
|
||||
Object.entries(data.images).forEach(([itemId, file]) => {
|
||||
previewUrls[itemId] = URL.createObjectURL(file)
|
||||
})
|
||||
setImagePreviewUrls(previewUrls)
|
||||
setCurrentStep(3)
|
||||
}
|
||||
@@ -54,18 +58,11 @@ const ReturnPage: NextPage = () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
try {
|
||||
const returnItems = []
|
||||
|
||||
const imageUrls = await Promise.all(imagePromises)
|
||||
|
||||
const returnItems = selectedItems.map(itemId => {
|
||||
// آپلود تصاویر به صورت ترتیبی (یکی یکی)
|
||||
for (const itemId of selectedItems) {
|
||||
// Find the order item that contains this shipment item
|
||||
const orderItem = order?.results.order.orderItems.find(orderItem =>
|
||||
orderItem.shipmentItems.some(shipmentItem => shipmentItem._id === itemId)
|
||||
@@ -73,18 +70,31 @@ const ReturnPage: NextPage = () => {
|
||||
|
||||
const shipmentItem = orderItem?.shipmentItems.find(item => item._id === itemId)
|
||||
|
||||
if (!orderItem || !shipmentItem) return null
|
||||
if (!orderItem || !shipmentItem) continue
|
||||
|
||||
return {
|
||||
// آپلود تصویر این آیتم
|
||||
let imageUrl = ''
|
||||
if (formData.images[itemId]) {
|
||||
try {
|
||||
const response = await uploadImageMutation.mutateAsync(formData.images[itemId])
|
||||
imageUrl = response.results.data.url.url
|
||||
} catch (error) {
|
||||
toast(`خطا در آپلود تصویر: ${extractErrorMessage(error)}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
returnItems.push({
|
||||
orderItemId: orderItem._id,
|
||||
shipmentItemId: shipmentItem._id,
|
||||
quantity: formData.quantities[itemId],
|
||||
comment: formData.description,
|
||||
reason: formData.reason,
|
||||
images: imageUrls
|
||||
imagesUrl: imageUrl
|
||||
})
|
||||
}
|
||||
}).filter(Boolean) as { orderItemId: string; shipmentItemId: string; quantity: number; comment: string; reason: string; images: string[] }[]
|
||||
|
||||
// ارسال درخواست مرجوعی به سرور
|
||||
await returnOrderMutation.mutateAsync({
|
||||
orderId: Number(id),
|
||||
items: returnItems
|
||||
@@ -97,12 +107,16 @@ const ReturnPage: NextPage = () => {
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// خطا در آپلود تصاویر یا ارسال درخواست
|
||||
console.error('Error in handleFinalSubmit:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackToStep1 = () => {
|
||||
setCurrentStep(1)
|
||||
setFormData(null)
|
||||
setImagePreviewUrls([])
|
||||
setImagePreviewUrls({})
|
||||
}
|
||||
|
||||
const handleBackToStep2 = () => {
|
||||
|
||||
@@ -50,3 +50,9 @@ export const useGetReturnOrder = () => {
|
||||
queryFn: api.getReturnOrder,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadReturnImage = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.uploadReturnImage,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ReturnReasonsResponse,
|
||||
ReturnOrdersResponse,
|
||||
} from "../types/Types";
|
||||
import { UploadResponse } from "@/app/jobs/types/Types";
|
||||
|
||||
export const getOrders = async (activeTab: string): Promise<OrdersResponse> => {
|
||||
const { data } = await axios.get(`/user/profile/orders?status=${activeTab}`);
|
||||
@@ -45,3 +46,12 @@ export const getReturnOrder = async (): Promise<ReturnOrdersResponse> => {
|
||||
const { data } = await axios.get(`/returns/user`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const uploadReturnImage = async (
|
||||
file: File
|
||||
): Promise<UploadResponse> => {
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
const { data } = await axios.post(`/returns/image/upload`, formData);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -339,7 +339,7 @@ export interface ReturnOrderItem {
|
||||
quantity: number;
|
||||
comment: string;
|
||||
reason: string;
|
||||
images: string[];
|
||||
imagesUrl: string;
|
||||
}
|
||||
|
||||
export interface ReturnOrderType {
|
||||
|
||||
Reference in New Issue
Block a user