From 27100ad3f3b634de0e08ee4fa155796373c67d63 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 3 Dec 2025 15:24:39 +0330 Subject: [PATCH] step payment: manage address --- .../[id]/components/AddressSection.tsx | 96 +++++++++++++++---- .../[id]/components/AddressSelectionModal.tsx | 95 ++++++++++++++++++ .../order/checkout/hooks/useOrderData.ts | 14 ++- .../order/checkout/service/OrderService.ts | 5 + .../(Dialogs)/order/checkout/store/Store.ts | 13 +++ .../(Dialogs)/order/checkout/types/Types.ts | 10 ++ 6 files changed, 211 insertions(+), 22 deletions(-) create mode 100644 src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSelectionModal.tsx create mode 100644 src/app/[name]/(Dialogs)/order/checkout/store/Store.ts diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx index da53cd3..629cf2d 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx @@ -6,18 +6,66 @@ import PrimaryButton from '@/components/button/PrimaryButton'; import { ChevronLeft } from 'lucide-react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; +import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSetAddressCart } from '../../hooks/useOrderData'; +import { useCheckoutStore } from '../../store/Store'; +import useToggle from '@/hooks/helpers/useToggle'; +import { AddressSelectionModal } from './AddressSelectionModal'; const formatAddress = (address: Address) => { return `${address.address}، ${address.city}، ${address.province}`; }; export const AddressSection = () => { + + const { mutate: setAddressCart } = useSetAddressCart(); + const { setIsSelectedAddress, selectedAddressId, setSelectedAddressId } = useCheckoutStore(); const params = useParams<{ name: string; id: string }>(); const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' }); const { data: addressesResponse, isLoading } = useGetAddresses(); - const addresses = addressesResponse?.data || []; + const addresses = useMemo(() => addressesResponse?.data || [], [addressesResponse?.data]); const redirectUrl = `/${params.name}/order/checkout/${params.id}`; + const { state: modalVisible, toggle: toggleModal, set: setModalVisible } = useToggle(false); + + // اگر آدرس پیش‌فرض وجود داشت، آن را تنظیم کن، در غیر این صورت آدرس اول غیر پیش‌فرض + useEffect(() => { + if (!isLoading && addresses.length > 0 && !selectedAddressId) { + const defaultAddress = addresses.find(addr => addr.isDefault); + const addressToSet = defaultAddress || addresses.filter(addr => !addr.isDefault)[0]; + if (addressToSet) { + setAddressCart(addressToSet.id, { + onSuccess: () => { + setIsSelectedAddress(true); + setSelectedAddressId(addressToSet.id); + }, + }); + } + } + }, [isLoading, addresses, setAddressCart, setIsSelectedAddress, selectedAddressId, setSelectedAddressId]); + + const handleSelectAddress = (addressId: string) => { + setAddressCart(addressId, { + onSuccess: () => { + setIsSelectedAddress(true); + setSelectedAddressId(addressId); + setModalVisible(false); + }, + }); + }; + + // پیدا کردن آدرس انتخاب شده یا آدرس پیش‌فرض یا اولین آدرس غیر پیش‌فرض + const selectedAddress = useMemo(() => { + if (!addresses.length) return null; + if (selectedAddressId) { + return addresses.find(addr => addr.id === selectedAddressId); + } + const defaultAddress = addresses.find(addr => addr.isDefault); + if (defaultAddress) { + return defaultAddress; + } + return addresses.filter(addr => !addr.isDefault)[0]; + }, [addresses, selectedAddressId]); // اگر در حال بارگذاری است، چیزی نمایش نده if (isLoading) { @@ -43,36 +91,44 @@ export const AddressSection = () => { ); } - // فیلتر کردن آدرس‌های غیر پیش‌فرض - const nonDefaultAddresses = addresses.filter(addr => !addr.isDefault); - - // اگر فقط آدرس پیش‌فرض دارد، نمایش نده - if (nonDefaultAddresses.length === 0) { + // اگر آدرس انتخاب شده وجود ندارد، نمایش نده + if (!selectedAddress) { return null; } - // آدرس اول غیر پیش‌فرض را نمایش بده - const firstNonDefaultAddress = nonDefaultAddresses[0]; - return ( -
-
-
-

{t("SectionAddress.Title")}

- + <> +
+
+
+

{t("SectionAddress.Title")}

- +
+

+

+ {selectedAddress.title} +
+ {formatAddress(selectedAddress)} +

-

- {formatAddress(firstNonDefaultAddress)} -

-
-
+ + + setModalVisible(false)} + onSelectAddress={handleSelectAddress} + /> + ); }; diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSelectionModal.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSelectionModal.tsx new file mode 100644 index 0000000..5963699 --- /dev/null +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSelectionModal.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { Address } from '@/app/[name]/(Profile)/profile/address/types/Types'; +import PrimaryButton from '@/components/button/PrimaryButton'; +import Modal from '@/components/utils/Modal'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import { ef } from '@/lib/helpers/utfNumbers'; + +const formatAddress = (address: Address) => { + return `${address.address}، ${address.city}، ${address.province}`; +}; + +interface AddressSelectionModalProps { + visible: boolean; + addresses: Address[]; + redirectUrl: string; + name: string; + selectedAddressId: string | null; + onClose: () => void; + onSelectAddress: (addressId: string) => void; +} + +export const AddressSelectionModal = ({ + visible, + addresses, + redirectUrl, + name, + selectedAddressId: initialSelectedAddressId, + onClose, + onSelectAddress, +}: AddressSelectionModalProps) => { + const [selectedAddressId, setSelectedAddressId] = useState(initialSelectedAddressId); + + useEffect(() => { + if (visible) { + setSelectedAddressId(initialSelectedAddressId); + } else { + setSelectedAddressId(null); + } + }, [visible, initialSelectedAddressId]); + + const handleSelectAddress = (addressId: string) => { + setSelectedAddressId(addressId); + onSelectAddress(addressId); + }; + + return ( + +
+

انتخاب آدرس

+
+ {addresses.map((address) => ( +
handleSelectAddress(address.id)} + className={`bg-background rounded-container p-4 cursor-pointer transition-all ${ + selectedAddressId === address.id ? 'border border-primary' : '' + }`} + > +
+

{address.title}

+ {address.isDefault && ( + + پیش‌فرض + + )} +
+

+ {formatAddress(address)} +

+

+ کد پستی: {ef(address.postalCode || 'ثبت نشده')} +

+

+ تلفن: {ef(address.phone)} +

+
+ ))} +
+
+ + + افزودن آدرس جدید + + +
+
+
+ ); +}; + diff --git a/src/app/[name]/(Dialogs)/order/checkout/hooks/useOrderData.ts b/src/app/[name]/(Dialogs)/order/checkout/hooks/useOrderData.ts index c9d7d3c..95db1b6 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/hooks/useOrderData.ts +++ b/src/app/[name]/(Dialogs)/order/checkout/hooks/useOrderData.ts @@ -1,5 +1,9 @@ -import { useQuery } from "@tanstack/react-query"; -import { getPaymentMethod, getShipmentMethod } from "../service/OrderService"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + getPaymentMethod, + getShipmentMethod, + setAddressCart, +} from "../service/OrderService"; export const useGetShipmentMethod = () => { return useQuery({ @@ -14,3 +18,9 @@ export const useGetPaymentMethod = () => { queryFn: getPaymentMethod, }); }; + +export const useSetAddressCart = () => { + return useMutation({ + mutationFn: setAddressCart, + }); +}; diff --git a/src/app/[name]/(Dialogs)/order/checkout/service/OrderService.ts b/src/app/[name]/(Dialogs)/order/checkout/service/OrderService.ts index 14b5e06..5470faf 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/service/OrderService.ts +++ b/src/app/[name]/(Dialogs)/order/checkout/service/OrderService.ts @@ -17,3 +17,8 @@ export const getPaymentMethod = async (): Promise => { ); return data; }; + +export const setAddressCart = async (id: string) => { + const { data } = await api.patch("/public/cart/address", { addressId: id }); + return data; +}; diff --git a/src/app/[name]/(Dialogs)/order/checkout/store/Store.ts b/src/app/[name]/(Dialogs)/order/checkout/store/Store.ts new file mode 100644 index 0000000..5435105 --- /dev/null +++ b/src/app/[name]/(Dialogs)/order/checkout/store/Store.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; +import { CheckoutStoreType } from "../types/Types"; + +export const useCheckoutStore = create((set) => ({ + isSelectedAddress: false, + setIsSelectedAddress: (value) => set({ isSelectedAddress: value }), + selectedAddressId: null, + setSelectedAddressId: (value) => set({ selectedAddressId: value }), + isSelectedPayment: false, + setIsSelectedPayment: (value) => set({ isSelectedPayment: value }), + isSelectedShipment: false, + setIsSelectedShipment: (value) => set({ isSelectedShipment: value }), +})); diff --git a/src/app/[name]/(Dialogs)/order/checkout/types/Types.ts b/src/app/[name]/(Dialogs)/order/checkout/types/Types.ts index 296cb19..1311d4f 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/types/Types.ts +++ b/src/app/[name]/(Dialogs)/order/checkout/types/Types.ts @@ -118,3 +118,13 @@ export interface PaymentMethodResponse { export type PaymentMethodsResponse = BaseResponse; +export type CheckoutStoreType = { + isSelectedAddress: boolean; + setIsSelectedAddress: (value: boolean) => void; + selectedAddressId: string | null; + setSelectedAddressId: (value: string | null) => void; + isSelectedPayment: boolean; + setIsSelectedPayment: (value: boolean) => void; + isSelectedShipment: boolean; + setIsSelectedShipment: (value: boolean) => void; +};