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,
|
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) => {
|
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
|
}, 0) || 0
|
||||||
|
|
||||||
// محاسبه قیمت نهایی
|
// محاسبه قیمت نهایی
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
PaymentStatusType,
|
PaymentStatusType,
|
||||||
PaymentStatusResponseType,
|
PaymentStatusResponseType,
|
||||||
BulkAddCartType,
|
BulkAddCartType,
|
||||||
|
ShipmentCartResponseType,
|
||||||
|
ShipmentSaveType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getCart = async (): Promise<CartResponseType> => {
|
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);
|
const { data } = await axios.post("/cart/bulk-add", params);
|
||||||
return data;
|
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 CartSummary from '@/components/CartSummary'
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import CartItem from '../components/CartItem'
|
// import CartItem from '../components/CartItem'
|
||||||
import TitleBack from '../components/TitleBack'
|
import TitleBack from '../components/TitleBack'
|
||||||
|
import ShippingMethodSelector from '../components/ShippingMethodSelector'
|
||||||
import { Location } from 'iconsax-react'
|
import { Location } from 'iconsax-react'
|
||||||
import { useGetCart, useShippingCost } from '../hooks/useCartData'
|
import { useGetCart, useSaveShipmentCart, useShippingCost } from '../hooks/useCartData'
|
||||||
import { CartItemType } from '../types/Types'
|
// import { CartItemType } from '../types/Types'
|
||||||
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
|
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
const CartShipping: NextPage = () => {
|
const CartShipping: NextPage = () => {
|
||||||
|
|
||||||
const { data: cartData, isLoading: cartLoading, error: cartError } = useGetCart()
|
const { data: cartData, isLoading: cartLoading, error: cartError } = useGetCart()
|
||||||
const { data: shippingData, isLoading: shippingLoading, error: shippingError } = useShippingCost()
|
const { data: shippingData, isLoading: shippingLoading, error: shippingError } = useShippingCost()
|
||||||
const { data: profileData, isLoading: profileLoading, error: profileError } = useGetProfile()
|
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) {
|
if (cartLoading || shippingLoading || profileLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -50,9 +127,11 @@ const CartShipping: NextPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// محاسبه هزینه ارسال
|
// محاسبه هزینه ارسال بر اساس انتخابهای کاربر
|
||||||
const totalShippingCost = shippingData?.results?.shipping?.reduce((total, shop) => {
|
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
|
}, 0) || 0
|
||||||
|
|
||||||
// محاسبه قیمت نهایی
|
// محاسبه قیمت نهایی
|
||||||
@@ -82,7 +161,15 @@ const CartShipping: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Link href={'/profile'} className='text-primary text-sm self-end sm:self-auto'>ویرایش</Link>
|
<Link href={'/profile'} className='text-primary text-sm self-end sm:self-auto'>ویرایش</Link>
|
||||||
</div>
|
</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) => {
|
{items.map((item: CartItemType, index: number) => {
|
||||||
if (!item || !item.product || !item.variant) {
|
if (!item || !item.product || !item.variant) {
|
||||||
return null
|
return null
|
||||||
@@ -95,7 +182,7 @@ const CartShipping: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full lg:w-[360px] lg:flex-shrink-0">
|
<div className="w-full lg:w-[360px] lg:flex-shrink-0">
|
||||||
<CartSummary
|
<CartSummary
|
||||||
@@ -104,10 +191,17 @@ const CartShipping: NextPage = () => {
|
|||||||
discount={cart?.total_discount || 0}
|
discount={cart?.total_discount || 0}
|
||||||
shippingCost={totalShippingCost}
|
shippingCost={totalShippingCost}
|
||||||
total={finalTotal}
|
total={finalTotal}
|
||||||
confirmHref="/cart/payment"
|
onConfirm={handleProceedToPayment}
|
||||||
confirmLabel="پرداخت"
|
confirmLabel="پرداخت"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
confirmDisabled={!allShipmentsSelected}
|
||||||
|
isLoading={isSavingShipment}
|
||||||
/>
|
/>
|
||||||
|
{!allShipmentsSelected && (
|
||||||
|
<div className="mt-2 text-sm text-red-600 text-center">
|
||||||
|
لطفا برای همه فروشگاهها روش ارسال انتخاب کنید
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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'
|
'use client'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { ArrowRight2 } from 'iconsax-react'
|
import { ArrowLeft, TaskSquare } from 'iconsax-react'
|
||||||
import { useRouter, useParams } from 'next/navigation'
|
import { useRouter, useParams } from 'next/navigation'
|
||||||
import PageLoading from '@/components/PageLoading'
|
import PageLoading from '@/components/PageLoading'
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
@@ -10,6 +10,7 @@ import OrderMainContent from './components/OrderMainContent'
|
|||||||
import CancelOrderModal from './components/CancelOrderModal'
|
import CancelOrderModal from './components/CancelOrderModal'
|
||||||
import { useCancelOrder, useGetOrderDetail } from '../hooks/useOrdersData'
|
import { useCancelOrder, useGetOrderDetail } from '../hooks/useOrdersData'
|
||||||
import { toast } from '@/components/Toast'
|
import { toast } from '@/components/Toast'
|
||||||
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
|
|
||||||
|
|
||||||
const OrderDetail: FC = () => {
|
const OrderDetail: FC = () => {
|
||||||
@@ -66,14 +67,21 @@ const OrderDetail: FC = () => {
|
|||||||
<Menu pageActive='orders' />
|
<Menu pageActive='orders' />
|
||||||
|
|
||||||
<div className='border p-6 rounded-2xl text-sm mt-7'>
|
<div className='border p-6 rounded-2xl text-sm mt-7'>
|
||||||
<div className='flex gap-2 items-center border-b pb-6'>
|
<div className='flex justify-between items-center border-b pb-6'>
|
||||||
<button
|
<div className='flex gap-2 items-center'>
|
||||||
onClick={() => router.push('/profile/orders')}
|
<button
|
||||||
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
onClick={() => router.push('/profile/orders')}
|
||||||
>
|
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
||||||
<ArrowRight2 size={20} className="rotate-180" />
|
>
|
||||||
</button>
|
<ArrowLeft color='black' size={20} className="rotate-180" />
|
||||||
<div className="text-lg font-semibold">جزئیات سفارش</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{cancelOrderMutation.isPending ? (
|
{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 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 (
|
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,
|
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,
|
OrderDetailResponse,
|
||||||
CancelOrderType,
|
CancelOrderType,
|
||||||
CancelReasonsResponse,
|
CancelReasonsResponse,
|
||||||
|
ReturnOrderType,
|
||||||
|
ReturnReasonsResponse,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getOrders = async (activeTab: string): Promise<OrdersResponse> => {
|
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);
|
const { data } = await axios.post(`/cancel/user`, params);
|
||||||
return data;
|
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;
|
orderId: number;
|
||||||
items: CancelOrderItem[];
|
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;
|
onConfirm?: () => void;
|
||||||
confirmHref?: string;
|
confirmHref?: string;
|
||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
|
confirmDisabled?: boolean;
|
||||||
onDownloadInvoice?: () => void;
|
onDownloadInvoice?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -33,7 +34,7 @@ const Row: FC<{ label: string; value: string | number; emphasize?: boolean }> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CartSummary: FC<CartSummaryProps> = (props) => {
|
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 (
|
return (
|
||||||
<div className={"rounded-xl sm:rounded-2xl border border-border p-4 sm:p-6 h-fit bg-[#FAFAFA] " + (className ?? "")}>
|
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => !confirmHref && onConfirm?.()}
|
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"
|
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">
|
<Link href={confirmHref} className="w-full h-full flex items-center justify-center">
|
||||||
{confirmLabel ?? "تایید و تکمیل سفارش"}
|
{confirmLabel ?? "تایید و تکمیل سفارش"}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
Reference in New Issue
Block a user