return order + success page return order + ...
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,319 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Truck, Sort, Flash } from 'iconsax-react'
|
||||
import { ShopShippingType, ShipmentInfoType } from '../types/Types'
|
||||
|
||||
interface ShippingMethodSelectorProps {
|
||||
shippingData: ShopShippingType[]
|
||||
selectedShipments: Record<string, number>
|
||||
onShipmentSelect: (shopId: string, shipperId: number) => void
|
||||
onClearSelections?: () => void
|
||||
}
|
||||
|
||||
const ShippingMethodSelector = ({
|
||||
shippingData,
|
||||
selectedShipments,
|
||||
onShipmentSelect,
|
||||
onClearSelections
|
||||
}: ShippingMethodSelectorProps) => {
|
||||
const selectedCount = Object.keys(selectedShipments).length
|
||||
const totalShops = shippingData?.length || 0
|
||||
|
||||
// State برای مرتبسازی
|
||||
const [sortBy, setSortBy] = useState<'price' | 'time' | 'default'>('default')
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
// State برای collapse/expand محصولات
|
||||
const [expandedShops, setExpandedShops] = useState<Set<string>>(new Set())
|
||||
|
||||
// تابع مرتبسازی روشهای ارسال
|
||||
const sortShippers = (shippers: any[]) => {
|
||||
if (sortBy === 'default') return shippers
|
||||
|
||||
return [...shippers].sort((a, b) => {
|
||||
let aValue, bValue
|
||||
|
||||
if (sortBy === 'price') {
|
||||
aValue = a.totalShippingCost
|
||||
bValue = b.totalShippingCost
|
||||
} else if (sortBy === 'time') {
|
||||
aValue = a.shippingDaysRange?.[0] || 0
|
||||
bValue = b.shippingDaysRange?.[0] || 0
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return aValue - bValue
|
||||
} else {
|
||||
return bValue - aValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// هندلر تغییر مرتبسازی
|
||||
const handleSortChange = (newSortBy: 'price' | 'time' | 'default') => {
|
||||
if (sortBy === newSortBy) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortBy(newSortBy)
|
||||
setSortOrder('asc')
|
||||
}
|
||||
}
|
||||
|
||||
// هندلر toggle محصولات فروشگاه
|
||||
const toggleShopExpansion = (shopId: string) => {
|
||||
const newExpanded = new Set(expandedShops)
|
||||
if (newExpanded.has(shopId)) {
|
||||
newExpanded.delete(shopId)
|
||||
} else {
|
||||
newExpanded.add(shopId)
|
||||
}
|
||||
setExpandedShops(newExpanded)
|
||||
}
|
||||
|
||||
// هندلر انتخاب سریع بهترین روش ارسال برای همه فروشگاهها
|
||||
const handleQuickSelect = (criteria: 'cheapest' | 'fastest') => {
|
||||
const newSelections: Record<string, number> = {}
|
||||
|
||||
shippingData?.forEach((shop) => {
|
||||
if (shop.shippers && shop.shippers.length > 0) {
|
||||
let bestShipper = shop.shippers[0]
|
||||
|
||||
shop.shippers.forEach((shipper) => {
|
||||
if (criteria === 'cheapest') {
|
||||
if (shipper.totalShippingCost < bestShipper.totalShippingCost) {
|
||||
bestShipper = shipper
|
||||
}
|
||||
} else if (criteria === 'fastest') {
|
||||
const shipperTime = shipper.shippingDaysRange?.[0] || 999
|
||||
const bestTime = bestShipper.shippingDaysRange?.[0] || 999
|
||||
if (shipperTime < bestTime) {
|
||||
bestShipper = shipper
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
newSelections[shop.shopId] = bestShipper.shipperId
|
||||
}
|
||||
})
|
||||
|
||||
// اعمال انتخابها
|
||||
Object.entries(newSelections).forEach(([shopId, shipperId]) => {
|
||||
onShipmentSelect(shopId, shipperId)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-5 border border-border rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Truck size={20} color="#333333" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">روش ارسال</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{selectedCount} از {totalShops} فروشگاه انتخاب شده
|
||||
</p>
|
||||
{selectedCount > 0 && (
|
||||
<p className="text-sm text-gray-600">
|
||||
مجموع ارسال: {Object.entries(selectedShipments).reduce((total, [shopId, shipperId]) => {
|
||||
const shop = shippingData?.find(s => s.shopId === shopId)
|
||||
const shipper = shop?.shippers?.find(s => s.shipperId === shipperId)
|
||||
return total + (shipper?.totalShippingCost || 0)
|
||||
}, 0).toLocaleString('fa-IR')} تومان
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* انتخاب سریع */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Flash size={16} color="#666" />
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleQuickSelect('cheapest')}
|
||||
className="text-xs bg-green-100 hover:bg-green-200 text-green-800 px-2 py-1 rounded transition-colors"
|
||||
title="انتخاب ارزانترین روش برای همه فروشگاهها"
|
||||
>
|
||||
ارزانترین
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleQuickSelect('fastest')}
|
||||
className="text-xs bg-blue-100 hover:bg-blue-200 text-blue-800 px-2 py-1 rounded transition-colors"
|
||||
title="انتخاب سریعترین روش برای همه فروشگاهها"
|
||||
>
|
||||
سریعترین
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* کنترلهای مرتبسازی */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Sort size={16} color="#666" />
|
||||
<select
|
||||
value={`${sortBy}-${sortOrder}`}
|
||||
onChange={(e) => {
|
||||
const [newSortBy, newSortOrder] = e.target.value.split('-') as ['price' | 'time' | 'default', 'asc' | 'desc']
|
||||
setSortBy(newSortBy)
|
||||
setSortOrder(newSortOrder)
|
||||
}}
|
||||
className="text-sm border border-gray-300 rounded px-2 py-1 bg-white"
|
||||
>
|
||||
<option value="default-asc">پیشفرض</option>
|
||||
<option value="price-asc">ارزانترین</option>
|
||||
<option value="price-desc">گرانترین</option>
|
||||
<option value="time-asc">سریعترین</option>
|
||||
<option value="time-desc">آهستهترین</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{onClearSelections && Object.keys(selectedShipments).length > 0 && (
|
||||
<button
|
||||
onClick={onClearSelections}
|
||||
className="text-sm text-red-600 hover:text-red-800 font-medium px-3 py-1 rounded-md hover:bg-red-50 transition-colors"
|
||||
>
|
||||
پاک کردن انتخابها
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{shippingData?.map((shop) => (
|
||||
<div key={shop.shopId} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 text-lg">{shop.shopName}</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{shop.items?.length || 0} محصول
|
||||
</p>
|
||||
</div>
|
||||
{selectedShipments[shop.shopId] && (
|
||||
<div className="flex items-center gap-1 px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
انتخاب شد
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* نمایش محصولات فروشگاه */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => toggleShopExpansion(shop.shopId)}
|
||||
className="flex items-center justify-between w-full text-sm font-medium text-gray-700 mb-3 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<span>محصولات این فروشگاه ({shop.items?.length || 0})</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${expandedShops.has(shop.shopId) ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{expandedShops.has(shop.shopId) && (
|
||||
<div className="space-y-3 max-h-48 overflow-y-auto">
|
||||
{shop.items?.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-100">
|
||||
<img
|
||||
src={item.product.imagesUrl.cover}
|
||||
alt={item.product.title_fa}
|
||||
className="w-12 h-12 object-cover rounded-lg flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 truncate mb-1">
|
||||
{item.product.title_fa}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-gray-600">
|
||||
تعداد: {item.quantity}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-gray-900">
|
||||
{item.variant.price.selling_price.toLocaleString('fa-IR')} تومان
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* روشهای ارسال */}
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-700 mb-3">روش ارسال:</h5>
|
||||
<div className="space-y-2">
|
||||
{(() => {
|
||||
const shippers = sortShippers(shop.shippers || [])
|
||||
const cheapestPrice = Math.min(...shippers.map(s => s.totalShippingCost))
|
||||
|
||||
return shippers.map((shipper) => {
|
||||
const priceDifference = shipper.totalShippingCost - cheapestPrice
|
||||
const isCheapest = priceDifference === 0
|
||||
|
||||
return (
|
||||
<label
|
||||
key={shipper.shipperId}
|
||||
className={`flex items-center justify-between p-3 border rounded-lg cursor-pointer transition-colors ${selectedShipments[shop.shopId] === shipper.shipperId
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
name={`shipment-${shop.shopId}`}
|
||||
value={shipper.shipperId}
|
||||
checked={selectedShipments[shop.shopId] === shipper.shipperId}
|
||||
onChange={() => onShipmentSelect(shop.shopId, shipper.shipperId)}
|
||||
className="text-primary focus:ring-primary"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 flex items-center gap-2">
|
||||
{shipper.shipperName}
|
||||
{isCheapest && (
|
||||
<span className="text-xs bg-green-100 text-green-800 px-1.5 py-0.5 rounded">
|
||||
ارزانترین
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
زمان ارسال: {shipper.shippingDaysRange?.join(' تا ') || 'نامشخص'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-gray-900 relative group">
|
||||
{shipper.totalShippingCost.toLocaleString('fa-IR')} تومان
|
||||
{priceDifference > 0 && (
|
||||
<div className="text-xs text-orange-600 mt-1">
|
||||
+{priceDifference.toLocaleString('fa-IR')} تومان
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-full right-0 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap z-10">
|
||||
هزینه ارسال کل این فروشگاه
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{(!shop.shippers || shop.shippers.length === 0) && (
|
||||
<div className="text-center py-4 text-gray-500">
|
||||
روش ارسالی برای این فروشگاه موجود نیست
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShippingMethodSelector
|
||||
@@ -97,3 +97,16 @@ export const useBulkAddCart = () => {
|
||||
mutationFn: api.bulkAddCart,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetShipmentCart = () => {
|
||||
return useQuery({
|
||||
queryKey: ["shipment-cart"],
|
||||
queryFn: api.getShipmentCart,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSaveShipmentCart = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.saveShipmentCart,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -48,9 +48,15 @@ const CartPayment: NextPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// محاسبه هزینه ارسال
|
||||
// بارگذاری انتخابهای ارسال از localStorage
|
||||
const savedShipments = typeof window !== 'undefined' ? localStorage.getItem('selectedShipments') : null
|
||||
const selectedShipments = savedShipments ? JSON.parse(savedShipments) : {}
|
||||
|
||||
// محاسبه هزینه ارسال بر اساس انتخابهای کاربر
|
||||
const totalShippingCost = shippingData?.results?.shipping?.reduce((total, shop) => {
|
||||
return total + (shop.shippers?.[0]?.totalShippingCost || 0)
|
||||
const selectedShipperId = selectedShipments[shop.shopId]
|
||||
const selectedShipper = shop.shippers?.find(shipper => shipper.shipperId === selectedShipperId)
|
||||
return total + (selectedShipper?.totalShippingCost || shop.shippers?.[0]?.totalShippingCost || 0)
|
||||
}, 0) || 0
|
||||
|
||||
// محاسبه قیمت نهایی
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
PaymentStatusType,
|
||||
PaymentStatusResponseType,
|
||||
BulkAddCartType,
|
||||
ShipmentCartResponseType,
|
||||
ShipmentSaveType,
|
||||
} from "../types/Types";
|
||||
|
||||
export const getCart = async (): Promise<CartResponseType> => {
|
||||
@@ -75,3 +77,13 @@ export const bulkAddCart = async (params: BulkAddCartType) => {
|
||||
const { data } = await axios.post("/cart/bulk-add", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getShipmentCart = async (): Promise<ShipmentCartResponseType> => {
|
||||
const { data } = await axios.get<ShipmentCartResponseType>("/shipment/cart");
|
||||
return data;
|
||||
};
|
||||
|
||||
export const saveShipmentCart = async (params: ShipmentSaveType) => {
|
||||
const { data } = await axios.post("/shipment/save", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -2,19 +2,96 @@
|
||||
import CartSummary from '@/components/CartSummary'
|
||||
import Layout from '@/hoc/Layout'
|
||||
import { NextPage } from 'next'
|
||||
import CartItem from '../components/CartItem'
|
||||
// import CartItem from '../components/CartItem'
|
||||
import TitleBack from '../components/TitleBack'
|
||||
import ShippingMethodSelector from '../components/ShippingMethodSelector'
|
||||
import { Location } from 'iconsax-react'
|
||||
import { useGetCart, useShippingCost } from '../hooks/useCartData'
|
||||
import { CartItemType } from '../types/Types'
|
||||
import { useGetCart, useSaveShipmentCart, useShippingCost } from '../hooks/useCartData'
|
||||
// import { CartItemType } from '../types/Types'
|
||||
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const CartShipping: NextPage = () => {
|
||||
|
||||
const { data: cartData, isLoading: cartLoading, error: cartError } = useGetCart()
|
||||
const { data: shippingData, isLoading: shippingLoading, error: shippingError } = useShippingCost()
|
||||
const { data: profileData, isLoading: profileLoading, error: profileError } = useGetProfile()
|
||||
const { mutate: saveShipmentCart } = useSaveShipmentCart()
|
||||
|
||||
// State برای ذخیره انتخابهای روش ارسال
|
||||
const [selectedShipments, setSelectedShipments] = useState<Record<string, number>>({})
|
||||
const [isSavingShipment, setIsSavingShipment] = useState(false)
|
||||
|
||||
// بارگذاری انتخابهای قبلی از localStorage
|
||||
useEffect(() => {
|
||||
const savedShipments = localStorage.getItem('selectedShipments')
|
||||
if (savedShipments) {
|
||||
try {
|
||||
setSelectedShipments(JSON.parse(savedShipments))
|
||||
} catch (error) {
|
||||
console.error('Error parsing saved shipments:', error)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// ذخیره انتخابها در localStorage
|
||||
useEffect(() => {
|
||||
if (Object.keys(selectedShipments).length > 0) {
|
||||
localStorage.setItem('selectedShipments', JSON.stringify(selectedShipments))
|
||||
}
|
||||
}, [selectedShipments])
|
||||
|
||||
// هندلر انتخاب روش ارسال
|
||||
const handleShipmentSelect = (shopId: string, shipperId: number) => {
|
||||
setSelectedShipments(prev => ({
|
||||
...prev,
|
||||
[shopId]: shipperId
|
||||
}))
|
||||
}
|
||||
|
||||
// چک کردن اینکه آیا همه فروشگاهها روش ارسال انتخاب کردهاند
|
||||
const allShipmentsSelected = shippingData?.results?.shipping?.every(shop =>
|
||||
selectedShipments[shop.shopId] !== undefined
|
||||
) || false
|
||||
|
||||
// هندلر پاک کردن همه انتخابها
|
||||
const handleClearSelections = () => {
|
||||
setSelectedShipments({})
|
||||
localStorage.removeItem('selectedShipments')
|
||||
}
|
||||
|
||||
// هندلر ذخیره انتخابهای ارسال و انتقال به صفحه پرداخت
|
||||
const handleProceedToPayment = async () => {
|
||||
if (!allShipmentsSelected) return
|
||||
|
||||
setIsSavingShipment(true)
|
||||
|
||||
try {
|
||||
// تبدیل selectedShipments به فرمت مورد نیاز API
|
||||
const shipmentsInfo = Object.entries(selectedShipments).map(([shopId, shipperId]) => ({
|
||||
shopId,
|
||||
shipmentId: shipperId
|
||||
}))
|
||||
|
||||
// ذخیره انتخابها در API
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
saveShipmentCart({ shipmentsInfo }, {
|
||||
onSuccess: () => resolve(),
|
||||
onError: (error) => reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// انتقال به صفحه پرداخت
|
||||
window.location.href = '/cart/payment'
|
||||
} catch (error) {
|
||||
console.error('Error saving shipment cart:', error)
|
||||
// در صورت خطا همچنان به صفحه پرداخت برویم
|
||||
window.location.href = '/cart/payment'
|
||||
} finally {
|
||||
setIsSavingShipment(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (cartLoading || shippingLoading || profileLoading) {
|
||||
return (
|
||||
@@ -50,9 +127,11 @@ const CartShipping: NextPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// محاسبه هزینه ارسال
|
||||
// محاسبه هزینه ارسال بر اساس انتخابهای کاربر
|
||||
const totalShippingCost = shippingData?.results?.shipping?.reduce((total, shop) => {
|
||||
return total + (shop.shippers?.[0]?.totalShippingCost || 0)
|
||||
const selectedShipperId = selectedShipments[shop.shopId]
|
||||
const selectedShipper = shop.shippers?.find(shipper => shipper.shipperId === selectedShipperId)
|
||||
return total + (selectedShipper?.totalShippingCost || shop.shippers?.[0]?.totalShippingCost || 0)
|
||||
}, 0) || 0
|
||||
|
||||
// محاسبه قیمت نهایی
|
||||
@@ -82,7 +161,15 @@ const CartShipping: NextPage = () => {
|
||||
</div>
|
||||
<Link href={'/profile'} className='text-primary text-sm self-end sm:self-auto'>ویرایش</Link>
|
||||
</div>
|
||||
<div className="flex-1 mt-5 border border-border p-4 sm:p-5 rounded-2xl">
|
||||
|
||||
<ShippingMethodSelector
|
||||
shippingData={shippingData?.results?.shipping || []}
|
||||
selectedShipments={selectedShipments}
|
||||
onShipmentSelect={handleShipmentSelect}
|
||||
onClearSelections={handleClearSelections}
|
||||
/>
|
||||
|
||||
{/* <div className="flex-1 mt-5 border border-border p-4 sm:p-5 rounded-2xl">
|
||||
{items.map((item: CartItemType, index: number) => {
|
||||
if (!item || !item.product || !item.variant) {
|
||||
return null
|
||||
@@ -95,7 +182,7 @@ const CartShipping: NextPage = () => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="w-full lg:w-[360px] lg:flex-shrink-0">
|
||||
<CartSummary
|
||||
@@ -104,10 +191,17 @@ const CartShipping: NextPage = () => {
|
||||
discount={cart?.total_discount || 0}
|
||||
shippingCost={totalShippingCost}
|
||||
total={finalTotal}
|
||||
confirmHref="/cart/payment"
|
||||
onConfirm={handleProceedToPayment}
|
||||
confirmLabel="پرداخت"
|
||||
className="w-full"
|
||||
confirmDisabled={!allShipmentsSelected}
|
||||
isLoading={isSavingShipment}
|
||||
/>
|
||||
{!allShipmentsSelected && (
|
||||
<div className="mt-2 text-sm text-red-600 text-center">
|
||||
لطفا برای همه فروشگاهها روش ارسال انتخاب کنید
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -413,3 +413,23 @@ export type PaymentStatusResponseType = {
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Shipment Save Types
|
||||
export type ShipmentInfoType = {
|
||||
shopId: string;
|
||||
shipmentId: number;
|
||||
};
|
||||
|
||||
export type ShipmentSaveType = {
|
||||
shipmentsInfo: ShipmentInfoType[];
|
||||
};
|
||||
|
||||
// Shipment Cart Response Type
|
||||
export type ShipmentCartResponseType = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
cart: CartType;
|
||||
recommended: RecommendedProductType[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type CartSummaryProps = {
|
||||
onConfirm?: () => void;
|
||||
confirmHref?: string;
|
||||
confirmLabel?: string;
|
||||
confirmDisabled?: boolean;
|
||||
onDownloadInvoice?: () => void;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
@@ -33,7 +34,7 @@ const Row: FC<{ label: string; value: string | number; emphasize?: boolean }> =
|
||||
};
|
||||
|
||||
const CartSummary: FC<CartSummaryProps> = (props) => {
|
||||
const { itemsCount, itemsPrice, discount, shippingCost, total, onConfirm, onDownloadInvoice, className, confirmHref, confirmLabel, isLoading } = props;
|
||||
const { itemsCount, itemsPrice, discount, shippingCost, total, onConfirm, onDownloadInvoice, className, confirmHref, confirmLabel, confirmDisabled, isLoading } = props;
|
||||
|
||||
return (
|
||||
<div className={"rounded-xl sm:rounded-2xl border border-border p-4 sm:p-6 h-fit bg-[#FAFAFA] " + (className ?? "")}>
|
||||
@@ -62,10 +63,10 @@ const CartSummary: FC<CartSummaryProps> = (props) => {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => !confirmHref && onConfirm?.()}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || confirmDisabled}
|
||||
className="w-full h-10 sm:h-12 rounded-lg sm:rounded-xl text-white text-sm sm:text-base disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{confirmHref ? (
|
||||
{confirmHref && !confirmDisabled ? (
|
||||
<Link href={confirmHref} className="w-full h-full flex items-center justify-center">
|
||||
{confirmLabel ?? "تایید و تکمیل سفارش"}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user