user Addresses

This commit is contained in:
hamid zarghami
2026-06-23 11:16:25 +03:30
parent 223b1b5e8f
commit 45494d7ae5
9 changed files with 422 additions and 167 deletions
+2 -2
View File
@@ -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'
+23 -1
View File
@@ -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>;
+20 -4
View File
@@ -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;
+99 -1
View File
@@ -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,
} }
} }
+16
View File
@@ -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,
})