return order

This commit is contained in:
hamid zarghami
2025-11-01 17:16:54 +03:30
parent a893677d1c
commit 91ab1717ac
8 changed files with 169 additions and 153 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ const Resume: FC<ResumeProps> = ({ jobId }) => {
const uploadFileMutation = useMutation({ const uploadFileMutation = useMutation({
mutationFn: api.uploadSingleFile, mutationFn: api.uploadSingleFile,
onSuccess: (data) => { onSuccess: (data) => {
setValue('resumeUrl', data?.results?.url?.url) setValue('resumeUrl', data?.results?.data?.url?.url)
toast('فایل با موفقیت آپلود شد', 'success') toast('فایل با موفقیت آپلود شد', 'success')
}, },
onError: () => { onError: () => {
+5 -2
View File
@@ -36,17 +36,20 @@ export type ResumeType = {
}; };
export interface FileUrl { export interface FileUrl {
originalname: string;
size: number; size: number;
url: string; url: string;
type: string; type: string;
} }
export interface UploadResults { export interface UploadData {
message: string; message: string;
url: FileUrl; url: FileUrl;
} }
export interface UploadResults {
data: UploadData;
}
export interface UploadResponse { export interface UploadResponse {
status: number; status: number;
success: boolean; success: boolean;
@@ -9,8 +9,8 @@ interface ReturnConfirmationProps {
quantities: { [key: string]: number } quantities: { [key: string]: number }
reason: string reason: string
description: string description: string
images: File[] images: { [key: string]: File }
imagePreviewUrls: string[] imagePreviewUrls: { [key: string]: string }
returnReasons: Array<{ _id: string; title: string }> returnReasons: Array<{ _id: string; title: string }>
onSubmit: () => void onSubmit: () => void
onBack: () => void onBack: () => void
@@ -39,7 +39,7 @@ const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
<div className='space-y-4'> <div className='space-y-4'>
<h4 className='font-medium'>محصولات انتخاب شده</h4> <h4 className='font-medium'>محصولات انتخاب شده</h4>
{selectedItems.map((item) => ( {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'> <div className='flex items-center gap-4'>
<Image <Image
src={item.product.imagesUrl.cover} src={item.product.imagesUrl.cover}
@@ -58,6 +58,19 @@ const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
</div> </div>
</div> </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>
))} ))}
</div> </div>
@@ -79,26 +92,6 @@ const ReturnConfirmation: React.FC<ReturnConfirmationProps> = ({
</div> </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='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"> <div className="size-5 bg-primary/20 rounded-full flex items-center justify-center">
@@ -17,7 +17,7 @@ interface ReturnFormData {
interface ReturnFormProps { interface ReturnFormProps {
selectedItems: ShipmentItemDetail[] 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 onBack: () => void
isLoading: boolean isLoading: boolean
returnReasons: Array<{ _id: string; title: string }> returnReasons: Array<{ _id: string; title: string }>
@@ -47,9 +47,8 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
isLoading, isLoading,
returnReasons returnReasons
}) => { }) => {
const [images, setImages] = useState<File[]>([]) const [images, setImages] = useState<{ [key: string]: File }>({})
const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([]) const [imagePreviewUrls, setImagePreviewUrls] = useState<{ [key: string]: string }>({})
const fileInputRef = useRef<HTMLInputElement>(null)
const { const {
register, register,
@@ -78,30 +77,33 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
reset() reset()
} }
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = (itemId: string, event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []) const file = event.target.files?.[0]
const validFiles = files.filter(file => { 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) { const isValidType = file.type === 'image/jpeg' || file.type === 'image/png'
const isValidSize = file.size <= 10 * 1024 * 1024 // 10MB
if (!isValidType || !isValidSize) {
alert('فقط فایل‌های JPG و PNG با حداکثر حجم ۱۰ مگابایت مجاز هستند') alert('فقط فایل‌های JPG و PNG با حداکثر حجم ۱۰ مگابایت مجاز هستند')
return
} }
const newImages = [...images, ...validFiles].slice(0, 5) // حداکثر ۵ عکس setImages(prev => ({ ...prev, [itemId]: file }))
setImages(newImages) setImagePreviewUrls(prev => ({ ...prev, [itemId]: URL.createObjectURL(file) }))
const newPreviewUrls = newImages.map(file => URL.createObjectURL(file))
setImagePreviewUrls(newPreviewUrls)
} }
const removeImage = (index: number) => { const removeImage = (itemId: string) => {
const newImages = images.filter((_, i) => i !== index) setImages(prev => {
const newPreviewUrls = imagePreviewUrls.filter((_, i) => i !== index) const newImages = { ...prev }
setImages(newImages) delete newImages[itemId]
setImagePreviewUrls(newPreviewUrls) return newImages
})
setImagePreviewUrls(prev => {
const newUrls = { ...prev }
delete newUrls[itemId]
return newUrls
})
} }
return ( return (
@@ -110,12 +112,12 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
{/* انتخاب تعداد مرجوعی برای هر آیتم */} {/* انتخاب تعداد مرجوعی برای هر آیتم */}
<div className='space-y-4'> <div className='space-y-4'>
<h4 className='font-medium'>تعداد مرجوعی</h4> <h4 className='font-medium'>تعداد مرجوعی و آپلود تصویر</h4>
{selectedItems.map((item) => { {selectedItems.map((item) => {
const availableQuantity = item.quantity - item.cancelled_quantity - item.returned_quantity const availableQuantity = item.quantity - item.cancelled_quantity - item.returned_quantity
return ( return (
<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 mb-2'> <div className='flex items-center gap-4'>
<Image <Image
src={item.product.imagesUrl.cover} src={item.product.imagesUrl.cover}
width={40} width={40}
@@ -143,6 +145,48 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
className='w-20 px-2 py-1 border rounded text-center' className='w-20 px-2 py-1 border rounded text-center'
/> />
</div> </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> </div>
) )
})} })}
@@ -178,60 +222,6 @@ const ReturnForm: React.FC<ReturnFormProps> = ({
)} )}
</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'> <div className='flex gap-3 justify-center'>
<Button <Button
+61 -47
View File
@@ -3,7 +3,7 @@ import Layout from '@/hoc/Layout'
import { type NextPage } from 'next' import { type NextPage } from 'next'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { useState } from 'react' 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 ProductSelection from './components/ProductSelection'
import ReturnForm from './components/ReturnForm' import ReturnForm from './components/ReturnForm'
import ReturnConfirmation from './components/ReturnConfirmation' import ReturnConfirmation from './components/ReturnConfirmation'
@@ -17,7 +17,7 @@ interface FormData {
quantities: { [key: string]: number } quantities: { [key: string]: number }
reason: string reason: string
description: string description: string
images: File[] images: { [key: string]: File }
} }
const ReturnPage: NextPage = () => { const ReturnPage: NextPage = () => {
@@ -26,11 +26,12 @@ const ReturnPage: NextPage = () => {
const { data: order, isLoading: isOrderLoading } = useGetOrderDetail(id as string) const { data: order, isLoading: isOrderLoading } = useGetOrderDetail(id as string)
const returnOrderMutation = useReturnOrder() const returnOrderMutation = useReturnOrder()
const { data: returnReasons } = useGetReturnReasons() const { data: returnReasons } = useGetReturnReasons()
const uploadImageMutation = useUploadReturnImage()
const [currentStep, setCurrentStep] = useState<Step>(1) const [currentStep, setCurrentStep] = useState<Step>(1)
const [selectedItems, setSelectedItems] = useState<string[]>([]) const [selectedItems, setSelectedItems] = useState<string[]>([])
const [formData, setFormData] = useState<FormData | null>(null) 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) => { const handleItemSelect = (itemId: string, checked: boolean) => {
if (checked) { if (checked) {
@@ -43,7 +44,10 @@ const ReturnPage: NextPage = () => {
const handleFormSubmit = (data: FormData) => { const handleFormSubmit = (data: FormData) => {
setFormData(data) setFormData(data)
// ایجاد preview URLs برای تصاویر // ایجاد 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) setImagePreviewUrls(previewUrls)
setCurrentStep(3) setCurrentStep(3)
} }
@@ -54,55 +58,65 @@ const ReturnPage: NextPage = () => {
return return
} }
// تبدیل تصاویر به base64 یا URL try {
const imagePromises = formData.images.map(async (file) => { const returnItems = []
return new Promise<string>((resolve) => {
const reader = new FileReader() // آپلود تصاویر به صورت ترتیبی (یکی یکی)
reader.onload = () => resolve(reader.result as string) for (const itemId of selectedItems) {
reader.readAsDataURL(file) // 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) continue
// آپلود تصویر این آیتم
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,
imagesUrl: imageUrl
})
}
// ارسال درخواست مرجوعی به سرور
await returnOrderMutation.mutateAsync({
orderId: Number(id),
items: returnItems
}, {
onSuccess: () => {
toast('درخواست مرجوعی با موفقیت ثبت شد', 'success')
router.push(`/profile/orders/${id}/return/success`)
},
onError: (error) => {
toast(extractErrorMessage(error), 'error')
}
}) })
}) } catch (error) {
// خطا در آپلود تصاویر یا ارسال درخواست
const imageUrls = await Promise.all(imagePromises) console.error('Error in handleFinalSubmit:', error)
}
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 = () => { const handleBackToStep1 = () => {
setCurrentStep(1) setCurrentStep(1)
setFormData(null) setFormData(null)
setImagePreviewUrls([]) setImagePreviewUrls({})
} }
const handleBackToStep2 = () => { const handleBackToStep2 = () => {
@@ -50,3 +50,9 @@ export const useGetReturnOrder = () => {
queryFn: api.getReturnOrder, queryFn: api.getReturnOrder,
}); });
}; };
export const useUploadReturnImage = () => {
return useMutation({
mutationFn: api.uploadReturnImage,
});
};
+10
View File
@@ -8,6 +8,7 @@ import {
ReturnReasonsResponse, ReturnReasonsResponse,
ReturnOrdersResponse, ReturnOrdersResponse,
} from "../types/Types"; } from "../types/Types";
import { UploadResponse } from "@/app/jobs/types/Types";
export const getOrders = async (activeTab: string): Promise<OrdersResponse> => { export const getOrders = async (activeTab: string): Promise<OrdersResponse> => {
const { data } = await axios.get(`/user/profile/orders?status=${activeTab}`); 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`); const { data } = await axios.get(`/returns/user`);
return data; 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;
};
+1 -1
View File
@@ -339,7 +339,7 @@ export interface ReturnOrderItem {
quantity: number; quantity: number;
comment: string; comment: string;
reason: string; reason: string;
images: string[]; imagesUrl: string;
} }
export interface ReturnOrderType { export interface ReturnOrderType {