bulk action + payment

This commit is contained in:
hamid zarghami
2025-09-14 16:17:11 +03:30
parent 6d52d1e91a
commit 1f49a01157
9 changed files with 400 additions and 38 deletions
+28 -1
View File
@@ -12,13 +12,18 @@ import { extractErrorMessage } from '@/helpers/errorUtils'
import Error from '@/components/Error'
import { setRefreshToken, setToken } from '@/config/func'
import { useCountDown } from '@/hooks/useCountDown'
import { useBulkAddCart } from '@/app/cart/hooks/useCartData'
import { useLocalCart } from '@/app/product/hooks/useLocalCart'
import { useSharedStore } from '@/share/store/sharedStore'
const LoginStep2: FC = () => {
const { phone } = useAuthStore()
const { isPending, mutate: verifyOtp } = useVerifyOtp()
const { mutate: authentication, isPending: isAuthenticationPending } = useAuthentication();
const { mutate: bulkAddCart } = useBulkAddCart()
const { getLocalCartItems, clearLocalCart } = useLocalCart()
const { setIsLogin } = useSharedStore()
const { value: countdownValue, reset: resetCountdown } = useCountDown(2, true, () => {
// When countdown ends, enable resend button
@@ -49,6 +54,28 @@ const LoginStep2: FC = () => {
toast('ورود با موفقیت انجام شد', 'success')
setToken(data?.results?.data?.accessToken?.token)
setRefreshToken(data?.results?.data?.refreshToken?.token)
setIsLogin(true)
// همگام‌سازی سبد خرید آفلاین
const localCartItems = getLocalCartItems()
if (localCartItems.length > 0) {
const bulkItems = localCartItems.map(item => ({
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity
}))
bulkAddCart({ items: bulkItems }, {
onSuccess: () => {
toast('سبد خرید آفلاین با موفقیت همگام‌سازی شد', 'success')
clearLocalCart()
},
onError: (error) => {
toast(extractErrorMessage(error), 'error')
}
})
}
window.location.href = '/'
},
+39
View File
@@ -54,3 +54,42 @@ export const usePaymentMethods = () => {
queryFn: api.paymentMethods,
});
};
export const useCheckoutCart = () => {
return useMutation({
mutationFn: api.checkoutCart,
});
};
export const useApplyDiscountCode = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.applyDiscountCode,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
};
export const useCheckPaymentStatus = () => {
return useMutation({
mutationFn: api.checkPaymentStatus,
});
};
export const useRemoveAllCart = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.removeAllCart,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
};
export const useBulkAddCart = () => {
return useMutation({
mutationFn: api.bulkAddCart,
});
};
+3 -13
View File
@@ -5,14 +5,14 @@ import CartSummary from "@/components/CartSummary"
import Layout from "@/hoc/Layout"
import { Trash } from "iconsax-react"
import { NextPage } from "next"
import { useGetCart, useRemoveFromCart } from "./hooks/useCartData"
import { useGetCart, useRemoveAllCart } from "./hooks/useCartData"
import { toast } from "@/components/Toast"
import { useState } from "react"
const Cart: NextPage = () => {
const { data, isLoading, error } = useGetCart();
const removeFromCartMutation = useRemoveFromCart()
const removeAllCartMutation = useRemoveAllCart()
const [isClearingAll, setIsClearingAll] = useState(false)
const handleClearAll = async () => {
@@ -20,17 +20,7 @@ const Cart: NextPage = () => {
setIsClearingAll(true)
try {
// حذف همه آیتم‌ها یکی یکی
const removePromises = data.results.cart.items
.filter(item => item && item.product && item.variant)
.map(item =>
removeFromCartMutation.mutateAsync({
productId: item.product._id,
variantId: item.variant._id
})
)
await Promise.all(removePromises)
await removeAllCartMutation.mutateAsync()
toast('همه محصولات از سبد خرید حذف شدند', 'success')
} catch {
toast('خطا در حذف محصولات', 'error')
+156
View File
@@ -0,0 +1,156 @@
'use client'
import Layout from '@/hoc/Layout'
import { NextPage } from 'next'
import { useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useCheckPaymentStatus } from '../../hooks/useCartData'
import { toast } from '@/components/Toast'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
const PaymentCallback: NextPage = () => {
const router = useRouter()
const searchParams = useSearchParams()
const checkPaymentMutation = useCheckPaymentStatus()
const [paymentStatus, setPaymentStatus] = useState<'loading' | 'success' | 'failed' | 'pending'>('loading')
const [orderData, setOrderData] = useState<any>(null)
useEffect(() => {
const authority = searchParams.get('Authority')
const status = searchParams.get('Status')
if (!authority) {
toast('خطا در دریافت اطلاعات پرداخت', 'error')
setPaymentStatus('failed')
return
}
// بررسی وضعیت پرداخت - اطلاعات سفارش از سرور دریافت می‌شود
checkPaymentStatus(authority)
}, [searchParams])
const checkPaymentStatus = async (authority: string) => {
try {
const response = await checkPaymentMutation.mutateAsync({ authority })
// ذخیره اطلاعات سفارش از response
if (response?.results?.orderData) {
setOrderData(response.results.orderData)
}
if (response?.results?.paymentStatus === 'success') {
setPaymentStatus('success')
toast('پرداخت با موفقیت انجام شد', 'success')
} else if (response?.results?.paymentStatus === 'failed') {
setPaymentStatus('failed')
toast('پرداخت ناموفق بود', 'error')
} else {
setPaymentStatus('pending')
toast('وضعیت پرداخت در حال بررسی است', 'warning')
}
} catch (error: unknown) {
setPaymentStatus('failed')
const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'خطا در بررسی وضعیت پرداخت'
toast(errorMessage, 'error')
}
}
const getStatusIcon = () => {
switch (paymentStatus) {
case 'success':
return '✅'
case 'failed':
return '❌'
case 'pending':
return '⏳'
default:
return '🔄'
}
}
const getStatusMessage = () => {
switch (paymentStatus) {
case 'success':
return 'پرداخت با موفقیت انجام شد'
case 'failed':
return 'پرداخت ناموفق بود'
case 'pending':
return 'وضعیت پرداخت در حال بررسی است'
default:
return 'در حال بررسی وضعیت پرداخت...'
}
}
const getStatusColor = () => {
switch (paymentStatus) {
case 'success':
return 'text-green-600'
case 'failed':
return 'text-red-600'
case 'pending':
return 'text-yellow-600'
default:
return 'text-blue-600'
}
}
return (
<div className="mt-14 px-4 sm:px-8 lg:px-20">
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center max-w-md w-full">
<div className="text-6xl mb-6">
{getStatusIcon()}
</div>
<h1 className={`text-2xl font-bold mb-4 ${getStatusColor()}`}>
{getStatusMessage()}
</h1>
{orderData && (
<div className="bg-gray-50 rounded-lg p-4 mb-6 text-right">
<div className="text-sm text-gray-600 mb-2">اطلاعات سفارش:</div>
<div className="text-sm">
<div>شماره سفارش: {orderData.orderId}</div>
<div>مبلغ پرداخت: {orderData.totalPaymentPrice?.toLocaleString()} تومان</div>
<div>تاریخ: {new Date(orderData.createdAt).toLocaleDateString('fa-IR')}</div>
</div>
</div>
)}
<div className="space-y-3">
{paymentStatus === 'success' && (
<Button asChild className="w-full">
<Link href="/profile/orders">
مشاهده سفارشات
</Link>
</Button>
)}
<Button asChild variant="outline" className="w-full">
<Link href="/">
بازگشت به صفحه اصلی
</Link>
</Button>
{paymentStatus === 'failed' && (
<Button asChild variant="outline" className="w-full">
<Link href="/cart/payment">
تلاش مجدد برای پرداخت
</Link>
</Button>
)}
</div>
</div>
</div>
</div>
)
}
export default function PaymentCallbackWithLayout() {
return (
<Layout>
<PaymentCallback />
</Layout>
)
}
+19 -20
View File
@@ -5,7 +5,7 @@ import { NextPage } from 'next'
import TitleBack from '../components/TitleBack'
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import DiscountCard from '../components/DiscountCard'
import { usePaymentMethods, useGetCart, useShippingCost } from '../hooks/useCartData'
import { usePaymentMethods, useGetCart, useShippingCost, useApplyDiscountCode, useCheckoutCart } from '../hooks/useCartData'
import { useState } from 'react'
import { toast } from '@/components/Toast'
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
@@ -16,9 +16,11 @@ const CartPayment: NextPage = () => {
const { data: shippingData, isLoading: shippingLoading } = useShippingCost()
const { data: profileData, isLoading: profileLoading } = useGetProfile()
const applyDiscountMutation = useApplyDiscountCode()
const checkoutMutation = useCheckoutCart()
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('')
const [discountCode, setDiscountCode] = useState<string>('')
const [isApplyingDiscount, setIsApplyingDiscount] = useState(false)
const isLoading = paymentMethodsLoading || cartLoading || shippingLoading || profileLoading
@@ -64,15 +66,13 @@ const CartPayment: NextPage = () => {
return
}
setIsApplyingDiscount(true)
try {
// TODO: پیاده‌سازی API اعمال کد تخفیف
// await applyDiscountCode(discountCode)
await applyDiscountMutation.mutateAsync({ discount_code: discountCode })
toast('کد تخفیف با موفقیت اعمال شد', 'success')
} catch {
toast('کد تخفیف نامعتبر است', 'error')
} finally {
setIsApplyingDiscount(false)
setDiscountCode('')
} catch (error: unknown) {
const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'کد تخفیف نامعتبر است'
toast(errorMessage, 'error')
}
}
@@ -87,16 +87,14 @@ const CartPayment: NextPage = () => {
return
}
try {
// TODO: پیاده‌سازی API پرداخت نهایی
// await processPayment({
// paymentMethodId: selectedPaymentMethod,
// discountCode: discountCode || null,
// shippingAddress: profile.userAddress
// })
toast('پرداخت با موفقیت انجام شد', 'success')
} catch {
toast('خطا در پردازش پرداخت', 'error')
const response = await checkoutMutation.mutateAsync({
payment_method_id: selectedPaymentMethod
})
if (response?.results?.paymentData?.redirectUrl) {
window.location.href = response.results.paymentData.redirectUrl
} else {
toast('خطا در دریافت لینک پرداخت', 'error')
}
}
@@ -109,7 +107,7 @@ const CartPayment: NextPage = () => {
discountCode={discountCode}
setDiscountCode={setDiscountCode}
onSubmit={handleDiscountCodeSubmit}
isLoading={isApplyingDiscount}
isLoading={applyDiscountMutation.isPending}
/>
<div className='mt-5 border border-border rounded-2xl p-4 sm:p-6 font-light'>
<div>انتخاب روش پرداخت</div>
@@ -153,6 +151,7 @@ const CartPayment: NextPage = () => {
onConfirm={handlePayment}
confirmLabel="پرداخت"
className="w-full"
isLoading={checkoutMutation.isPending}
/>
</div>
</div>
+38
View File
@@ -6,6 +6,13 @@ import {
CartResponseType,
ShippingCostResponseType,
PaymentMethodsResponseType,
CheckoutCartType,
CheckoutResponseType,
ApplyDiscountType,
ApplyDiscountResponseType,
PaymentStatusType,
PaymentStatusResponseType,
BulkAddCartType,
} from "../types/Types";
export const getCart = async (): Promise<CartResponseType> => {
@@ -37,3 +44,34 @@ export const paymentMethods = async (): Promise<PaymentMethodsResponseType> => {
const { data } = await axios.get("/payment/methods");
return data;
};
export const checkoutCart = async (
params: CheckoutCartType
): Promise<CheckoutResponseType> => {
const { data } = await axios.post("/payment/checkout/cart", params);
return data;
};
export const applyDiscountCode = async (
params: ApplyDiscountType
): Promise<ApplyDiscountResponseType> => {
const { data } = await axios.post("/cart/apply-discount", params);
return data;
};
export const checkPaymentStatus = async (
params: PaymentStatusType
): Promise<PaymentStatusResponseType> => {
const { data } = await axios.post("/payment/verify", params);
return data;
};
export const removeAllCart = async () => {
const { data } = await axios.post("/cart/clear");
return data;
};
export const bulkAddCart = async (params: BulkAddCartType) => {
const { data } = await axios.post("/cart/bulk-add", params);
return data;
};
+111
View File
@@ -167,6 +167,16 @@ export type RemoveCartType = {
variantId: string;
};
export type BulkAddCartItemType = {
productId: number;
variantId: string;
quantity: number;
};
export type BulkAddCartType = {
items: BulkAddCartItemType[];
};
// Shipping Cost Types
export type ShippingItemType = {
product: number;
@@ -302,3 +312,104 @@ export type PaymentMethodsResponseType = {
paymentMethods: PaymentMethodType[];
};
};
export type CheckoutCartType = {
payment_method_id: string;
};
export type CheckoutResponseType = {
status: number;
success: boolean;
results: {
paymentData: {
redirectUrl: string;
message: string;
authority: string;
};
order: {
user: string;
payment: string;
shipmentAddress: {
address: string;
city: string;
province: string;
phone: string;
plaque: string;
postalCode: string;
_id: string;
};
orderStatus: string;
createdAt: string;
updatedAt: string;
_id: number;
};
orderItems: Array<{
order: number;
shop: string;
shipmentItems: Array<{
product: number;
variant: string;
quantity: number;
cancelled_quantity: number;
returned_quantity: number;
selling_price: number;
retail_price: number;
discount_percent: number;
shipmentCost: number;
isFreeShip: boolean;
_id: string;
}>;
shipper: number;
totalSellingPrice: number;
totalRetailPrice: number;
totalShipmentCost: number;
totalCouponDiscount: number;
totalPaymentPrice: number;
itemsCount: number;
trackCode: string | null;
postingDate: string | null;
postingReceipt: string | null;
status: string;
_id: string;
createdAt: string;
updatedAt: string;
}>;
};
};
export type ApplyDiscountType = {
discount_code: string;
};
export type ApplyDiscountResponseType = {
status: number;
success: boolean;
results: {
discount: {
code: string;
amount: number;
type: string;
};
};
};
export type PaymentStatusType = {
authority: string;
};
export type PaymentStatusResponseType = {
status: number;
success: boolean;
results: {
paymentStatus: "success" | "failed" | "pending";
orderStatus: string;
message: string;
orderId?: number;
orderData?: {
orderId: number;
totalPaymentPrice: number;
createdAt: string;
orderStatus: string;
};
};
};
+5 -3
View File
@@ -16,6 +16,7 @@ type CartSummaryProps = {
confirmLabel?: string;
onDownloadInvoice?: () => void;
className?: string;
isLoading?: boolean;
};
const Row: FC<{ label: string; value: string | number; emphasize?: boolean }> = (
@@ -32,7 +33,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 } = props;
const { itemsCount, itemsPrice, discount, shippingCost, total, onConfirm, onDownloadInvoice, className, confirmHref, confirmLabel, isLoading } = props;
return (
<div className={"rounded-xl sm:rounded-2xl border border-border p-4 sm:p-6 h-fit bg-[#FAFAFA] " + (className ?? "")}>
@@ -61,14 +62,15 @@ const CartSummary: FC<CartSummaryProps> = (props) => {
<Button
type="button"
onClick={() => !confirmHref && onConfirm?.()}
className="w-full h-10 sm:h-12 rounded-lg sm:rounded-xl text-white text-sm sm:text-base"
disabled={isLoading}
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 ? (
<Link href={confirmHref} className="w-full h-full flex items-center justify-center">
{confirmLabel ?? "تایید و تکمیل سفارش"}
</Link>
) : (
confirmLabel ?? "تایید و تکمیل سفارش"
isLoading ? "در حال پردازش..." : (confirmLabel ?? "تایید و تکمیل سفارش")
)}
</Button>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
export const TOKEN_NAME = "sh_token";
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
export const BASE_URL = "https://api.shinan.ir";
export const BASE_URL = "https://shop-api.dev.danakcorp.com";
// export const BASE_URL = "https://api-sondos.run.danakcorp.com";
export const PRIMARY_COLOR = "#015699";