diff --git a/src/app/auth/components/LoginStep2.tsx b/src/app/auth/components/LoginStep2.tsx index c6a8281..06e838f 100644 --- a/src/app/auth/components/LoginStep2.tsx +++ b/src/app/auth/components/LoginStep2.tsx @@ -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 = '/' }, diff --git a/src/app/cart/hooks/useCartData.ts b/src/app/cart/hooks/useCartData.ts index b36edb3..cebe1d3 100644 --- a/src/app/cart/hooks/useCartData.ts +++ b/src/app/cart/hooks/useCartData.ts @@ -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, + }); +}; diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx index 067150d..9d78b98 100644 --- a/src/app/cart/page.tsx +++ b/src/app/cart/page.tsx @@ -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') diff --git a/src/app/cart/payment/callback/page.tsx b/src/app/cart/payment/callback/page.tsx new file mode 100644 index 0000000..8ad7e39 --- /dev/null +++ b/src/app/cart/payment/callback/page.tsx @@ -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(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 ( +
+
+
+
+ {getStatusIcon()} +
+ +

+ {getStatusMessage()} +

+ + {orderData && ( +
+
اطلاعات سفارش:
+
+
شماره سفارش: {orderData.orderId}
+
مبلغ پرداخت: {orderData.totalPaymentPrice?.toLocaleString()} تومان
+
تاریخ: {new Date(orderData.createdAt).toLocaleDateString('fa-IR')}
+
+
+ )} + +
+ {paymentStatus === 'success' && ( + + )} + + + + {paymentStatus === 'failed' && ( + + )} +
+
+
+
+ ) +} + +export default function PaymentCallbackWithLayout() { + return ( + + + + ) +} diff --git a/src/app/cart/payment/page.tsx b/src/app/cart/payment/page.tsx index 84ac908..c6a3b3f 100644 --- a/src/app/cart/payment/page.tsx +++ b/src/app/cart/payment/page.tsx @@ -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('') const [discountCode, setDiscountCode] = useState('') - 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} />
انتخاب روش پرداخت
@@ -153,6 +151,7 @@ const CartPayment: NextPage = () => { onConfirm={handlePayment} confirmLabel="پرداخت" className="w-full" + isLoading={checkoutMutation.isPending} />
diff --git a/src/app/cart/service/Service.ts b/src/app/cart/service/Service.ts index d8cf84e..0aba998 100644 --- a/src/app/cart/service/Service.ts +++ b/src/app/cart/service/Service.ts @@ -6,6 +6,13 @@ import { CartResponseType, ShippingCostResponseType, PaymentMethodsResponseType, + CheckoutCartType, + CheckoutResponseType, + ApplyDiscountType, + ApplyDiscountResponseType, + PaymentStatusType, + PaymentStatusResponseType, + BulkAddCartType, } from "../types/Types"; export const getCart = async (): Promise => { @@ -37,3 +44,34 @@ export const paymentMethods = async (): Promise => { const { data } = await axios.get("/payment/methods"); return data; }; + +export const checkoutCart = async ( + params: CheckoutCartType +): Promise => { + const { data } = await axios.post("/payment/checkout/cart", params); + return data; +}; + +export const applyDiscountCode = async ( + params: ApplyDiscountType +): Promise => { + const { data } = await axios.post("/cart/apply-discount", params); + return data; +}; + +export const checkPaymentStatus = async ( + params: PaymentStatusType +): Promise => { + 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; +}; diff --git a/src/app/cart/types/Types.ts b/src/app/cart/types/Types.ts index 253eb6f..10fa6b2 100644 --- a/src/app/cart/types/Types.ts +++ b/src/app/cart/types/Types.ts @@ -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; + }; + }; +}; diff --git a/src/components/CartSummary.tsx b/src/components/CartSummary.tsx index 7cf6335..678edfb 100644 --- a/src/components/CartSummary.tsx +++ b/src/components/CartSummary.tsx @@ -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 = (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 (
@@ -61,14 +62,15 @@ const CartSummary: FC = (props) => {
diff --git a/src/config/const.ts b/src/config/const.ts index b8dc482..da5fde4 100644 --- a/src/config/const.ts +++ b/src/config/const.ts @@ -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";