user Addresses
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
VITE_TOKEN_NAME = 'dmnu_a_t'
|
VITE_TOKEN_NAME = 'dmnu_a_t'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
||||||
|
|
||||||
VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
# VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
||||||
# VITE_BASE_URL = 'http://192.168.99.131:2000'
|
VITE_BASE_URL = 'http://192.168.99.131:2000'
|
||||||
|
|
||||||
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
||||||
@@ -14,6 +14,28 @@ export type User = {
|
|||||||
referrer?: string | null;
|
referrer?: string | null;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
gender?: boolean;
|
gender?: boolean;
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserSavedAddress = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
user: string;
|
||||||
|
title: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
province: string;
|
||||||
|
postalCode: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
phone: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserWithAddresses = User & {
|
||||||
|
addresses?: UserSavedAddress[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserRestaurant = {
|
export type UserRestaurant = {
|
||||||
@@ -73,4 +95,4 @@ export type ImportUsersResult = {
|
|||||||
|
|
||||||
export type ImportUsersResponse = IResponse<ImportUsersResult>;
|
export type ImportUsersResponse = IResponse<ImportUsersResult>;
|
||||||
|
|
||||||
export type SearchUserByPhoneResponse = IResponse<User>;
|
export type SearchUserByPhoneResponse = IResponse<UserWithAddresses>;
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ const CreateOrder: FC = () => {
|
|||||||
updateItemQuantity,
|
updateItemQuantity,
|
||||||
removeItem,
|
removeItem,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
customerAddresses,
|
||||||
|
selectedAddressId,
|
||||||
|
handleUserFound,
|
||||||
|
handleUserNotFound,
|
||||||
|
clearCustomerData,
|
||||||
|
applySavedAddress,
|
||||||
|
selectManualAddress,
|
||||||
|
handleAddressFieldChange,
|
||||||
} = useCreateOrderForm()
|
} = useCreateOrderForm()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,10 +50,14 @@ const CreateOrder: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col xl:flex-row gap-7 mt-9'>
|
<div className='flex flex-col xl:flex-row gap-7 mt-9'>
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex-1 min-w-0 flex flex-col gap-8'>
|
||||||
<CreateOrderCustomerSection formik={formik} />
|
<CreateOrderCustomerSection
|
||||||
|
formik={formik}
|
||||||
|
onUserFound={handleUserFound}
|
||||||
|
onUserNotFound={handleUserNotFound}
|
||||||
|
onClearCustomerData={clearCustomerData}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<CreateOrderItemsSection
|
<CreateOrderItemsSection
|
||||||
formik={formik}
|
formik={formik}
|
||||||
foods={foods}
|
foods={foods}
|
||||||
@@ -53,7 +65,6 @@ const CreateOrder: FC = () => {
|
|||||||
onUpdateQuantity={updateItemQuantity}
|
onUpdateQuantity={updateItemQuantity}
|
||||||
onRemoveItem={removeItem}
|
onRemoveItem={removeItem}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<CreateOrderDeliverySection
|
<CreateOrderDeliverySection
|
||||||
formik={formik}
|
formik={formik}
|
||||||
@@ -62,6 +73,11 @@ const CreateOrder: FC = () => {
|
|||||||
isCourierDelivery={isCourierDelivery}
|
isCourierDelivery={isCourierDelivery}
|
||||||
isCarDelivery={isCarDelivery}
|
isCarDelivery={isCarDelivery}
|
||||||
isDineIn={isDineIn}
|
isDineIn={isDineIn}
|
||||||
|
customerAddresses={customerAddresses}
|
||||||
|
selectedAddressId={selectedAddressId}
|
||||||
|
onSelectSavedAddress={applySavedAddress}
|
||||||
|
onSelectManualAddress={selectManualAddress}
|
||||||
|
onAddressFieldChange={handleAddressFieldChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { Location } from 'iconsax-react'
|
||||||
|
import type { UserSavedAddress } from '@/pages/customers/types/Types'
|
||||||
|
|
||||||
|
interface CreateOrderAddressSelectorProps {
|
||||||
|
addresses: UserSavedAddress[]
|
||||||
|
selectedAddressId: string | null
|
||||||
|
onSelectAddress: (address: UserSavedAddress) => void
|
||||||
|
onSelectManual: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateOrderAddressSelector: FC<CreateOrderAddressSelectorProps> = ({
|
||||||
|
addresses,
|
||||||
|
selectedAddressId,
|
||||||
|
onSelectAddress,
|
||||||
|
onSelectManual,
|
||||||
|
}) => {
|
||||||
|
if (addresses.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isManualSelected = selectedAddressId === null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mb-6'>
|
||||||
|
<div className='text-sm mb-3'>انتخاب آدرس ذخیرهشده</div>
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
|
||||||
|
{addresses.map((address) => {
|
||||||
|
const isSelected = selectedAddressId === address.id
|
||||||
|
const location = [address.province, address.city].filter(Boolean).join('، ')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={address.id}
|
||||||
|
type='button'
|
||||||
|
onClick={() => onSelectAddress(address)}
|
||||||
|
className={`text-right p-4 rounded-xl border transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-border hover:border-primary/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-2'>
|
||||||
|
<div className='text-sm font-medium'>{address.title || 'آدرس'}</div>
|
||||||
|
{address.isDefault && (
|
||||||
|
<span className='text-[10px] px-2 py-0.5 rounded-full bg-primary/10 text-primary'>
|
||||||
|
پیشفرض
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{location && (
|
||||||
|
<div className='text-xs text-description mt-1'>{location}</div>
|
||||||
|
)}
|
||||||
|
<div className='text-xs mt-2 line-clamp-2'>{address.address}</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={onSelectManual}
|
||||||
|
className={`text-right p-4 rounded-xl border transition-colors ${
|
||||||
|
isManualSelected
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-border hover:border-primary/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className='flex items-center gap-2 text-sm font-medium'>
|
||||||
|
<Location size={16} color='#000' />
|
||||||
|
<span>ورود آدرس جدید</span>
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-description mt-1'>
|
||||||
|
آدرس را بهصورت دستی وارد کنید
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateOrderAddressSelector
|
||||||
@@ -5,6 +5,7 @@ import { toast } from 'react-toastify'
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import { useSearchUserByPhone } from '@/pages/customers/hooks/useUsersData'
|
import { useSearchUserByPhone } from '@/pages/customers/hooks/useUsersData'
|
||||||
|
import type { UserWithAddresses } from '@/pages/customers/types/Types'
|
||||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
||||||
import type { ErrorType } from '@/helpers/types'
|
import type { ErrorType } from '@/helpers/types'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
@@ -12,11 +13,19 @@ import { isAxiosError } from 'axios'
|
|||||||
|
|
||||||
interface CreateOrderCustomerSectionProps {
|
interface CreateOrderCustomerSectionProps {
|
||||||
formik: FormikProps<CreateOrderFormValues>
|
formik: FormikProps<CreateOrderFormValues>
|
||||||
|
onUserFound: (user: UserWithAddresses) => void
|
||||||
|
onUserNotFound: () => void
|
||||||
|
onClearCustomerData: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerSearchState = 'idle' | 'found' | 'not_found'
|
type CustomerSearchState = 'idle' | 'found' | 'not_found'
|
||||||
|
|
||||||
const CreateOrderCustomerSection: FC<CreateOrderCustomerSectionProps> = ({ formik }) => {
|
const CreateOrderCustomerSection: FC<CreateOrderCustomerSectionProps> = ({
|
||||||
|
formik,
|
||||||
|
onUserFound,
|
||||||
|
onUserNotFound,
|
||||||
|
onClearCustomerData,
|
||||||
|
}) => {
|
||||||
const { mutate: searchUser, isPending: isSearching } = useSearchUserByPhone()
|
const { mutate: searchUser, isPending: isSearching } = useSearchUserByPhone()
|
||||||
const [searchState, setSearchState] = useState<CustomerSearchState>('idle')
|
const [searchState, setSearchState] = useState<CustomerSearchState>('idle')
|
||||||
|
|
||||||
@@ -32,8 +41,7 @@ const CreateOrderCustomerSection: FC<CreateOrderCustomerSectionProps> = ({ formi
|
|||||||
const user = response?.data
|
const user = response?.data
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setSearchState('not_found')
|
setSearchState('not_found')
|
||||||
formik.setFieldValue('firstName', '')
|
onUserNotFound()
|
||||||
formik.setFieldValue('lastName', '')
|
|
||||||
toast.info('مشتری یافت نشد. اطلاعات را وارد کنید.')
|
toast.info('مشتری یافت نشد. اطلاعات را وارد کنید.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -41,14 +49,14 @@ const CreateOrderCustomerSection: FC<CreateOrderCustomerSectionProps> = ({ formi
|
|||||||
formik.setFieldValue('userPhone', user.phone || phone)
|
formik.setFieldValue('userPhone', user.phone || phone)
|
||||||
formik.setFieldValue('firstName', user.firstName || '')
|
formik.setFieldValue('firstName', user.firstName || '')
|
||||||
formik.setFieldValue('lastName', user.lastName || '')
|
formik.setFieldValue('lastName', user.lastName || '')
|
||||||
|
onUserFound(user)
|
||||||
setSearchState('found')
|
setSearchState('found')
|
||||||
toast.success('اطلاعات مشتری بارگذاری شد')
|
toast.success('اطلاعات مشتری بارگذاری شد')
|
||||||
},
|
},
|
||||||
onError: (error: ErrorType) => {
|
onError: (error: ErrorType) => {
|
||||||
if (isAxiosError(error) && error.response?.status === 404) {
|
if (isAxiosError(error) && error.response?.status === 404) {
|
||||||
setSearchState('not_found')
|
setSearchState('not_found')
|
||||||
formik.setFieldValue('firstName', '')
|
onUserNotFound()
|
||||||
formik.setFieldValue('lastName', '')
|
|
||||||
toast.info('مشتری یافت نشد. اطلاعات را وارد کنید.')
|
toast.info('مشتری یافت نشد. اطلاعات را وارد کنید.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -63,6 +71,7 @@ const CreateOrderCustomerSection: FC<CreateOrderCustomerSectionProps> = ({ formi
|
|||||||
formik.handleChange(e)
|
formik.handleChange(e)
|
||||||
if (searchState !== 'idle') {
|
if (searchState !== 'idle') {
|
||||||
setSearchState('idle')
|
setSearchState('idle')
|
||||||
|
onClearCustomerData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import Textarea from '@/components/Textarea'
|
|||||||
import { formatPrice } from '@/helpers/func'
|
import { formatPrice } from '@/helpers/func'
|
||||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
||||||
import type { RestaurantPaymentMethod, RestaurantShipmentMethod } from '../hooks/useCreateOrderForm'
|
import type { RestaurantPaymentMethod, RestaurantShipmentMethod } from '../hooks/useCreateOrderForm'
|
||||||
|
import type { UserSavedAddress } from '@/pages/customers/types/Types'
|
||||||
|
import CreateOrderAddressSelector from './CreateOrderAddressSelector'
|
||||||
|
|
||||||
interface CreateOrderDeliverySectionProps {
|
interface CreateOrderDeliverySectionProps {
|
||||||
formik: FormikProps<CreateOrderFormValues>
|
formik: FormikProps<CreateOrderFormValues>
|
||||||
@@ -14,6 +16,11 @@ interface CreateOrderDeliverySectionProps {
|
|||||||
isCourierDelivery: boolean
|
isCourierDelivery: boolean
|
||||||
isCarDelivery: boolean
|
isCarDelivery: boolean
|
||||||
isDineIn: boolean
|
isDineIn: boolean
|
||||||
|
customerAddresses: UserSavedAddress[]
|
||||||
|
selectedAddressId: string | null
|
||||||
|
onSelectSavedAddress: (address: UserSavedAddress) => void
|
||||||
|
onSelectManualAddress: () => void
|
||||||
|
onAddressFieldChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
||||||
@@ -23,6 +30,11 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
isCourierDelivery,
|
isCourierDelivery,
|
||||||
isCarDelivery,
|
isCarDelivery,
|
||||||
isDineIn,
|
isDineIn,
|
||||||
|
customerAddresses,
|
||||||
|
selectedAddressId,
|
||||||
|
onSelectSavedAddress,
|
||||||
|
onSelectManualAddress,
|
||||||
|
onAddressFieldChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
|
||||||
@@ -32,8 +44,11 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAddressReadOnly =
|
||||||
|
selectedAddressId !== null && customerAddresses.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='flex flex-col gap-8'>
|
||||||
<div className='w-full bg-white rounded-4xl p-8'>
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
<div className='text-lg font-light'>تحویل و پرداخت</div>
|
<div className='text-lg font-light'>تحویل و پرداخت</div>
|
||||||
|
|
||||||
@@ -108,46 +123,59 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCourierDelivery && (
|
{isCourierDelivery && (
|
||||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
<div className='text-lg font-light'>آدرس تحویل</div>
|
<div className='text-lg font-light'>آدرس تحویل</div>
|
||||||
|
|
||||||
|
<CreateOrderAddressSelector
|
||||||
|
addresses={customerAddresses}
|
||||||
|
selectedAddressId={selectedAddressId}
|
||||||
|
onSelectAddress={onSelectSavedAddress}
|
||||||
|
onSelectManual={onSelectManualAddress}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
||||||
<Input
|
<Input
|
||||||
label='نام گیرنده'
|
label='نام گیرنده'
|
||||||
name='userAddress.fullName'
|
name='userAddress.fullName'
|
||||||
value={formik.values.userAddress.fullName}
|
value={formik.values.userAddress.fullName}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='شماره تماس گیرنده'
|
label='شماره تماس گیرنده'
|
||||||
name='userAddress.phone'
|
name='userAddress.phone'
|
||||||
value={formik.values.userAddress.phone}
|
value={formik.values.userAddress.phone}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='استان'
|
label='استان'
|
||||||
name='userAddress.province'
|
name='userAddress.province'
|
||||||
value={formik.values.userAddress.province}
|
value={formik.values.userAddress.province}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='شهر'
|
label='شهر'
|
||||||
name='userAddress.city'
|
name='userAddress.city'
|
||||||
value={formik.values.userAddress.city}
|
value={formik.values.userAddress.city}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='کد پستی'
|
label='کد پستی'
|
||||||
name='userAddress.postalCode'
|
name='userAddress.postalCode'
|
||||||
value={formik.values.userAddress.postalCode}
|
value={formik.values.userAddress.postalCode}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label='عرض جغرافیایی'
|
label='عرض جغرافیایی'
|
||||||
type='number'
|
type='number'
|
||||||
name='userAddress.latitude'
|
name='userAddress.latitude'
|
||||||
value={formik.values.userAddress.latitude || ''}
|
value={formik.values.userAddress.latitude || ''}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
isNotRequired
|
isNotRequired
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -155,7 +183,8 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
type='number'
|
type='number'
|
||||||
name='userAddress.longitude'
|
name='userAddress.longitude'
|
||||||
value={formik.values.userAddress.longitude || ''}
|
value={formik.values.userAddress.longitude || ''}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
isNotRequired
|
isNotRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,14 +194,15 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
label='آدرس کامل'
|
label='آدرس کامل'
|
||||||
name='userAddress.address'
|
name='userAddress.address'
|
||||||
value={formik.values.userAddress.address}
|
value={formik.values.userAddress.address}
|
||||||
onChange={formik.handleChange}
|
onChange={onAddressFieldChange}
|
||||||
|
readOnly={isAddressReadOnly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCarDelivery && (
|
{isCarDelivery && (
|
||||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
<div className='text-lg font-light'>اطلاعات خودرو</div>
|
<div className='text-lg font-light'>اطلاعات خودرو</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
||||||
@@ -204,7 +234,7 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
<div className='w-full bg-white rounded-4xl p-8'>
|
||||||
<div className='text-lg font-light'>توضیحات سفارش</div>
|
<div className='text-lg font-light'>توضیحات سفارش</div>
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -216,7 +246,7 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,148 +1,131 @@
|
|||||||
import { type FC } from 'react'
|
import { formatFaNumber, formatPrice } from "@/helpers/func";
|
||||||
import { ShoppingCart, User } from 'iconsax-react'
|
import { PaymentMethodEnum } from "@/pages/paymentMethods/enum/Enum";
|
||||||
import type { FormikProps } from 'formik'
|
import { DeliveryMethodEnum } from "@/pages/shipmentMethod/enum/Enum";
|
||||||
import { formatFaNumber, formatPrice } from '@/helpers/func'
|
import type { FormikProps } from "formik";
|
||||||
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
import { ShoppingCart, User } from "iconsax-react";
|
||||||
import { DeliveryMethodEnum } from '@/pages/shipmentMethod/enum/Enum'
|
import { type FC } from "react";
|
||||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
import type { CreateOrderFormValues, RestaurantPaymentMethod, RestaurantShipmentMethod } from "../hooks/useCreateOrderForm";
|
||||||
import type { RestaurantPaymentMethod, RestaurantShipmentMethod } from '../hooks/useCreateOrderForm'
|
|
||||||
|
|
||||||
interface OrderSummaryData {
|
interface OrderSummaryData {
|
||||||
lineItems: Array<{
|
lineItems: Array<{
|
||||||
foodId: string
|
foodId: string;
|
||||||
title: string
|
title: string;
|
||||||
image: string
|
image: string;
|
||||||
quantity: number
|
quantity: number;
|
||||||
unitPrice: number
|
unitPrice: number;
|
||||||
totalPrice: number
|
totalPrice: number;
|
||||||
} | null>
|
} | null>;
|
||||||
subTotal: number
|
subTotal: number;
|
||||||
deliveryFee: number
|
deliveryFee: number;
|
||||||
total: number
|
total: number;
|
||||||
totalItems: number
|
totalItems: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateOrderSidebarProps {
|
interface CreateOrderSidebarProps {
|
||||||
formik: FormikProps<CreateOrderFormValues>
|
formik: FormikProps<CreateOrderFormValues>;
|
||||||
orderSummary: OrderSummaryData
|
orderSummary: OrderSummaryData;
|
||||||
selectedDeliveryMethod?: RestaurantShipmentMethod
|
selectedDeliveryMethod?: RestaurantShipmentMethod;
|
||||||
selectedPaymentMethod?: RestaurantPaymentMethod
|
selectedPaymentMethod?: RestaurantPaymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDeliveryMethodText = (method?: string): string => {
|
const getDeliveryMethodText = (method?: string): string => {
|
||||||
const methodMap: Record<string, string> = {
|
const methodMap: Record<string, string> = {
|
||||||
[DeliveryMethodEnum.DineIn]: 'سرو در رستوران',
|
[DeliveryMethodEnum.DineIn]: "سرو در رستوران",
|
||||||
[DeliveryMethodEnum.CustomerPickup]: 'دریافت از محل',
|
[DeliveryMethodEnum.CustomerPickup]: "دریافت از محل",
|
||||||
[DeliveryMethodEnum.DeliveryCar]: 'تحویل به خودرو',
|
[DeliveryMethodEnum.DeliveryCar]: "تحویل به خودرو",
|
||||||
[DeliveryMethodEnum.DeliveryCourier]: 'بیرونبر',
|
[DeliveryMethodEnum.DeliveryCourier]: "بیرونبر",
|
||||||
}
|
};
|
||||||
return method ? methodMap[method] || method : '-'
|
return method ? methodMap[method] || method : "-";
|
||||||
}
|
};
|
||||||
|
|
||||||
const getPaymentMethodText = (method?: string): string => {
|
const getPaymentMethodText = (method?: string): string => {
|
||||||
const methodMap: Record<string, string> = {
|
const methodMap: Record<string, string> = {
|
||||||
[PaymentMethodEnum.Cash]: 'پرداخت نقدی',
|
[PaymentMethodEnum.Cash]: "پرداخت نقدی",
|
||||||
[PaymentMethodEnum.Online]: 'پرداخت آنلاین',
|
[PaymentMethodEnum.Online]: "پرداخت آنلاین",
|
||||||
[PaymentMethodEnum.CreditCard]: 'کارت به کارت',
|
[PaymentMethodEnum.CreditCard]: "کارت به کارت",
|
||||||
[PaymentMethodEnum.Wallet]: 'کیف پول',
|
[PaymentMethodEnum.Wallet]: "کیف پول",
|
||||||
}
|
};
|
||||||
return method ? methodMap[method] || method : '-'
|
return method ? methodMap[method] || method : "-";
|
||||||
}
|
};
|
||||||
|
|
||||||
const CreateOrderSidebar: FC<CreateOrderSidebarProps> = ({
|
const CreateOrderSidebar: FC<CreateOrderSidebarProps> = ({ formik, orderSummary, selectedDeliveryMethod, selectedPaymentMethod }) => {
|
||||||
formik,
|
const customerName = [formik.values.firstName, formik.values.lastName].filter(Boolean).join(" ");
|
||||||
orderSummary,
|
|
||||||
selectedDeliveryMethod,
|
|
||||||
selectedPaymentMethod,
|
|
||||||
}) => {
|
|
||||||
const customerName = [formik.values.firstName, formik.values.lastName]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-6'>
|
<div className="space-y-6">
|
||||||
<div className='bg-white rounded-4xl p-8'>
|
<div className="bg-white rounded-4xl p-8">
|
||||||
<div className='flex items-center gap-2 text-lg font-light'>
|
<div className="flex items-center gap-2 text-lg font-light">
|
||||||
<User size={20} color='#000' />
|
<User size={20} color="#000" />
|
||||||
خلاصه سفارش
|
خلاصه سفارش
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 space-y-3 text-[13px] font-light'>
|
<div className="mt-6 space-y-3 text-[13px] font-light">
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
<div className="flex justify-between items-center border-b border-border border-dashed pb-3">
|
||||||
<span className='text-description'>مشتری</span>
|
<span className="text-description">مشتری</span>
|
||||||
<span>{customerName || '-'}</span>
|
<span>{customerName || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
<div className="flex justify-between items-center border-b border-border border-dashed pb-3">
|
||||||
<span className='text-description'>شماره تماس</span>
|
<span className="text-description">شماره تماس</span>
|
||||||
<span>{formik.values.userPhone || '-'}</span>
|
<span>{formik.values.userPhone || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
<div className="flex justify-between items-center border-b border-border border-dashed pb-3">
|
||||||
<span className='text-description'>روش تحویل</span>
|
<span className="text-description">روش تحویل</span>
|
||||||
<span>{getDeliveryMethodText(selectedDeliveryMethod?.method)}</span>
|
<span>{getDeliveryMethodText(selectedDeliveryMethod?.method)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
<div className="flex justify-between items-center border-b border-border border-dashed pb-3">
|
||||||
<span className='text-description'>روش پرداخت</span>
|
<span className="text-description">روش پرداخت</span>
|
||||||
<span>{getPaymentMethodText(selectedPaymentMethod?.method)}</span>
|
<span>{getPaymentMethodText(selectedPaymentMethod?.method)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='bg-white rounded-4xl p-8'>
|
<div className="bg-white rounded-4xl p-8">
|
||||||
<div className='flex items-center gap-2 text-lg font-light'>
|
<div className="flex items-center gap-2 text-lg font-light">
|
||||||
<ShoppingCart size={20} color='#000' />
|
<ShoppingCart size={20} color="#000" />
|
||||||
آیتمها
|
آیتمها
|
||||||
{orderSummary.totalItems > 0 && (
|
{orderSummary.totalItems > 0 && <span className="text-xs text-description">({formatFaNumber(orderSummary.totalItems)})</span>}
|
||||||
<span className='text-xs text-description'>
|
|
||||||
({formatFaNumber(orderSummary.totalItems)})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{orderSummary.lineItems.length === 0 ? (
|
{orderSummary.lineItems.length === 0 ? (
|
||||||
<div className='mt-6 text-center text-xs text-description py-6'>
|
<div className="mt-6 text-center text-xs text-description py-6">سبد خالی است</div>
|
||||||
سبد خالی است
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='mt-6 space-y-3'>
|
<div className="mt-6 space-y-3">
|
||||||
{orderSummary.lineItems.map((item) => {
|
{orderSummary.lineItems.map((item) => {
|
||||||
if (!item) return null
|
if (!item) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={item.foodId} className="flex justify-between items-start gap-3 text-[13px] border-b border-border border-dashed pb-3">
|
||||||
key={item.foodId}
|
<div className="flex-1">
|
||||||
className='flex justify-between items-start gap-3 text-[13px] border-b border-border border-dashed pb-3'
|
|
||||||
>
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div>{item.title}</div>
|
<div>{item.title}</div>
|
||||||
<div className='text-description mt-1'>
|
<div className="text-description mt-1">
|
||||||
{formatFaNumber(item.quantity)} × {formatPrice(item.unitPrice)}
|
{formatFaNumber(item.quantity)} × {formatPrice(item.unitPrice)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{formatPrice(item.totalPrice)}</div>
|
<div>{formatPrice(item.totalPrice)}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='mt-6 space-y-3 text-[13px] font-light'>
|
<div className="mt-6 space-y-3 text-[13px] font-light">
|
||||||
<div className='flex justify-between items-center'>
|
<div className="flex justify-between items-center">
|
||||||
<span className='text-description'>جمع آیتمها</span>
|
<span className="text-description">جمع آیتمها</span>
|
||||||
<span>{formatPrice(orderSummary.subTotal)}</span>
|
<span>{formatPrice(orderSummary.subTotal)}</span>
|
||||||
</div>
|
</div>
|
||||||
{orderSummary.deliveryFee > 0 && (
|
{orderSummary.deliveryFee > 0 && (
|
||||||
<div className='flex justify-between items-center'>
|
<div className="flex justify-between items-center">
|
||||||
<span className='text-description'>هزینه ارسال</span>
|
<span className="text-description">هزینه ارسال</span>
|
||||||
<span>{formatPrice(orderSummary.deliveryFee)}</span>
|
<span>{formatPrice(orderSummary.deliveryFee)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='flex justify-between items-center font-bold text-base pt-3 border-t-2 border-gray-200'>
|
<div className="flex justify-between items-center font-bold text-sm pt-3 border-t-2 border-gray-200">
|
||||||
<span>جمع کل (تقریبی)</span>
|
<span>جمع کل</span>
|
||||||
<span>{formatPrice(orderSummary.total)}</span>
|
<span>{formatPrice(orderSummary.total)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CreateOrderSidebar
|
export default CreateOrderSidebar;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
@@ -18,6 +18,8 @@ import type {
|
|||||||
import type { Food } from '@/pages/food/types/Types'
|
import type { Food } from '@/pages/food/types/Types'
|
||||||
import type { RestaurantShipmentMethod } from '@/pages/shipmentMethod/types/Types'
|
import type { RestaurantShipmentMethod } from '@/pages/shipmentMethod/types/Types'
|
||||||
import type { RestaurantPaymentMethod } from '@/pages/paymentMethods/types/Types'
|
import type { RestaurantPaymentMethod } from '@/pages/paymentMethods/types/Types'
|
||||||
|
import type { UserSavedAddress, UserWithAddresses } from '@/pages/customers/types/Types'
|
||||||
|
import { mapSavedAddressToOrderAddress } from '../utils/mapSavedAddress'
|
||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
import type { ErrorType } from '@/helpers/types'
|
import type { ErrorType } from '@/helpers/types'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
@@ -62,6 +64,8 @@ export const getFoodUnitPrice = (food: Food) => {
|
|||||||
export const useCreateOrderForm = () => {
|
export const useCreateOrderForm = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { mutate: createOrder, isPending: isCreating } = useCreateOrder()
|
const { mutate: createOrder, isPending: isCreating } = useCreateOrder()
|
||||||
|
const [customerAddresses, setCustomerAddresses] = useState<UserSavedAddress[]>([])
|
||||||
|
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null)
|
||||||
|
|
||||||
const { data: foodsData } = useGetFoods({ limit: 500 })
|
const { data: foodsData } = useGetFoods({ limit: 500 })
|
||||||
const { data: deliveryMethodsData } = useGetRestaurantShipmentMethods({ limit: 100 })
|
const { data: deliveryMethodsData } = useGetRestaurantShipmentMethods({ limit: 100 })
|
||||||
@@ -194,6 +198,72 @@ export const useCreateOrderForm = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getCustomerFullName = useCallback(
|
||||||
|
(firstName?: string, lastName?: string) =>
|
||||||
|
[firstName ?? formik.values.firstName, lastName ?? formik.values.lastName]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' '),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[formik.values.firstName, formik.values.lastName]
|
||||||
|
)
|
||||||
|
|
||||||
|
const applySavedAddress = useCallback(
|
||||||
|
(address: UserSavedAddress, fullName?: string) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'userAddress',
|
||||||
|
mapSavedAddressToOrderAddress(address, fullName ?? getCustomerFullName())
|
||||||
|
)
|
||||||
|
setSelectedAddressId(address.id)
|
||||||
|
},
|
||||||
|
[formik, getCustomerFullName]
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectManualAddress = useCallback(() => {
|
||||||
|
setSelectedAddressId(null)
|
||||||
|
formik.setFieldValue('userAddress', {
|
||||||
|
...emptyUserAddress(),
|
||||||
|
fullName: getCustomerFullName(),
|
||||||
|
phone: formik.values.userPhone,
|
||||||
|
})
|
||||||
|
}, [formik, getCustomerFullName])
|
||||||
|
|
||||||
|
const handleUserFound = useCallback(
|
||||||
|
(user: UserWithAddresses) => {
|
||||||
|
const addresses = user.addresses ?? []
|
||||||
|
setCustomerAddresses(addresses)
|
||||||
|
|
||||||
|
if (addresses.length === 0) {
|
||||||
|
setSelectedAddressId(null)
|
||||||
|
formik.setFieldValue('userAddress', {
|
||||||
|
...emptyUserAddress(),
|
||||||
|
fullName: getCustomerFullName(user.firstName, user.lastName),
|
||||||
|
phone: user.phone || formik.values.userPhone,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAddress =
|
||||||
|
addresses.find((address) => address.isDefault) ?? addresses[0]
|
||||||
|
applySavedAddress(
|
||||||
|
defaultAddress,
|
||||||
|
[user.firstName, user.lastName].filter(Boolean).join(' ')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[applySavedAddress, formik, getCustomerFullName]
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearCustomerData = useCallback(() => {
|
||||||
|
setCustomerAddresses([])
|
||||||
|
setSelectedAddressId(null)
|
||||||
|
formik.setFieldValue('userAddress', emptyUserAddress())
|
||||||
|
}, [formik])
|
||||||
|
|
||||||
|
const handleUserNotFound = useCallback(() => {
|
||||||
|
clearCustomerData()
|
||||||
|
formik.setFieldValue('firstName', '')
|
||||||
|
formik.setFieldValue('lastName', '')
|
||||||
|
}, [clearCustomerData, formik])
|
||||||
|
|
||||||
const cashPaymentMethod = paymentMethods[0]
|
const cashPaymentMethod = paymentMethods[0]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -215,6 +285,16 @@ export const useCreateOrderForm = () => {
|
|||||||
selectedDeliveryMethod?.method === DeliveryMethodEnum.DeliveryCar
|
selectedDeliveryMethod?.method === DeliveryMethodEnum.DeliveryCar
|
||||||
const isDineIn = selectedDeliveryMethod?.method === DeliveryMethodEnum.DineIn
|
const isDineIn = selectedDeliveryMethod?.method === DeliveryMethodEnum.DineIn
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCourierDelivery || customerAddresses.length === 0 || selectedAddressId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAddress =
|
||||||
|
customerAddresses.find((address) => address.isDefault) ?? customerAddresses[0]
|
||||||
|
applySavedAddress(defaultAddress)
|
||||||
|
}, [isCourierDelivery, customerAddresses, selectedAddressId, applySavedAddress])
|
||||||
|
|
||||||
const foodsMap = useMemo(
|
const foodsMap = useMemo(
|
||||||
() => new Map(foods.map((food) => [food.id, food])),
|
() => new Map(foods.map((food) => [food.id, food])),
|
||||||
[foods]
|
[foods]
|
||||||
@@ -280,6 +360,16 @@ export const useCreateOrderForm = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddressFieldChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
if (e.target.name?.startsWith('userAddress.')) {
|
||||||
|
setSelectedAddressId(null)
|
||||||
|
}
|
||||||
|
formik.handleChange(e)
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
)
|
||||||
|
|
||||||
const removeItem = (foodId: string) => {
|
const removeItem = (foodId: string) => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'items',
|
'items',
|
||||||
@@ -302,6 +392,14 @@ export const useCreateOrderForm = () => {
|
|||||||
updateItemQuantity,
|
updateItemQuantity,
|
||||||
removeItem,
|
removeItem,
|
||||||
isSubmitting: isCreating,
|
isSubmitting: isCreating,
|
||||||
|
customerAddresses,
|
||||||
|
selectedAddressId,
|
||||||
|
handleUserFound,
|
||||||
|
handleUserNotFound,
|
||||||
|
clearCustomerData,
|
||||||
|
applySavedAddress,
|
||||||
|
selectManualAddress,
|
||||||
|
handleAddressFieldChange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { UserSavedAddress } from '@/pages/customers/types/Types'
|
||||||
|
import type { CreateOrderUserAddress } from '../types/Types'
|
||||||
|
|
||||||
|
export const mapSavedAddressToOrderAddress = (
|
||||||
|
saved: UserSavedAddress,
|
||||||
|
fullName: string
|
||||||
|
): CreateOrderUserAddress => ({
|
||||||
|
fullName: fullName || saved.title,
|
||||||
|
phone: saved.phone,
|
||||||
|
city: saved.city,
|
||||||
|
province: saved.province,
|
||||||
|
address: saved.address,
|
||||||
|
postalCode: saved.postalCode,
|
||||||
|
latitude: saved.latitude,
|
||||||
|
longitude: saved.longitude,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user