Create order

This commit is contained in:
hamid zarghami
2026-06-23 12:31:39 +03:30
parent f83d0743c6
commit 7130c304c5
12 changed files with 247 additions and 119 deletions
+8 -3
View File
@@ -84,9 +84,14 @@ const Input: FC<Props> = (props: Props) => {
return (
<div className='w-full'>
<label className='text-sm'>
{props.label}
</label>
{props.label && (
<div className='flex items-center gap-1'>
<label className='text-sm'>{props.label}</label>
{props.isNotRequired && (
<span className='text-xs text-description'>(اختیاری)</span>
)}
</div>
)}
<div className='w-full relative mt-1'>
<input {...props} onChange={(e) => {
+6 -2
View File
@@ -5,6 +5,7 @@ import { clx } from '../helpers/utils'
type Props = {
label?: string,
error_text?: string
isNotRequired?: boolean
} & InputHTMLAttributes<HTMLTextAreaElement>
const Textarea: FC<Props> = (props: Props) => {
@@ -12,8 +13,11 @@ const Textarea: FC<Props> = (props: Props) => {
<div className='w-full relative'>
{
props.label &&
<div className='text-sm' >
{props.label}
<div className='flex items-center gap-1'>
<div className='text-sm'>{props.label}</div>
{props.isNotRequired && (
<span className='text-xs text-description'>(اختیاری)</span>
)}
</div>
}
+11 -1
View File
@@ -1,6 +1,6 @@
import * as api from "../service/UsersService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { GetUsersParams } from "../types/Types";
import type { CreateUserAddressType, GetUsersParams } from "../types/Types";
export const useGetUsers = (params?: GetUsersParams) => {
return useQuery({
@@ -26,6 +26,16 @@ export const useCreateUser = () => {
});
};
export const useCreateUserAddress = () => {
return useMutation({
mutationFn: ({
userId,
...params
}: CreateUserAddressType & { userId: string }) =>
api.createUserAddress(userId, params),
});
};
export const useImportUsersFromExcel = () => {
const queryClient = useQueryClient();
@@ -1,5 +1,7 @@
import axios from "@/config/axios";
import type {
CreateUserAddressResponse,
CreateUserAddressType,
CreateUserResponse,
CreateUserType,
GetCustomersResponse,
@@ -28,6 +30,17 @@ export const createUser = async (params: CreateUserType) => {
return data;
};
export const createUserAddress = async (
userId: string,
params: CreateUserAddressType,
) => {
const { data } = await axios.post<CreateUserAddressResponse>(
`/admin/users/${userId}/addresses`,
params,
);
return data;
};
export const importUsersFromExcel = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
+22 -1
View File
@@ -103,4 +103,25 @@ export type CreateUserType = {
lastName: string;
};
export type CreateUserResponse = IResponse<User>;
export type CreateUserResult = {
id: string;
createdAt: string;
updatedAt: string;
user: User;
orderCount: number;
totalOrderAmount: number;
};
export type CreateUserResponse = IResponse<CreateUserResult>;
export type CreateUserAddressType = {
title: string;
address: string;
postalCode?: string;
latitude: number;
longitude: number;
phone: string;
isDefault: boolean;
};
export type CreateUserAddressResponse = IResponse<UserSavedAddress>;
+2
View File
@@ -31,6 +31,7 @@ const CreateOrder: FC = () => {
applySavedAddress,
selectManualAddress,
handleAddressFieldChange,
handleLocationSelect,
} = useCreateOrderForm()
return (
@@ -78,6 +79,7 @@ const CreateOrder: FC = () => {
onSelectSavedAddress={applySavedAddress}
onSelectManualAddress={selectManualAddress}
onAddressFieldChange={handleAddressFieldChange}
onLocationSelect={handleLocationSelect}
/>
</div>
@@ -27,7 +27,6 @@ const CreateOrderAddressSelector: FC<CreateOrderAddressSelectorProps> = ({
<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
@@ -48,9 +47,6 @@ const CreateOrderAddressSelector: FC<CreateOrderAddressSelectorProps> = ({
</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>
)
@@ -1,8 +1,11 @@
import { type FC } from 'react'
import { type FC, useState } from 'react'
import type { FormikProps } from 'formik'
import { useTranslation } from 'react-i18next'
import Input from '@/components/Input'
import Textarea from '@/components/Textarea'
import DefaulModal from '@/components/DefaulModal'
import LocationInput from '@/pages/settings/components/LocationInput'
import LocationPicker from '@/pages/settings/components/LocationPicker'
import { formatPrice } from '@/helpers/func'
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
import type { RestaurantPaymentMethod, RestaurantShipmentMethod } from '../hooks/useCreateOrderForm'
@@ -21,6 +24,7 @@ interface CreateOrderDeliverySectionProps {
onSelectSavedAddress: (address: UserSavedAddress) => void
onSelectManualAddress: () => void
onAddressFieldChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void
onLocationSelect: (lat: number, lng: number) => void
}
const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
@@ -35,8 +39,10 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
onSelectSavedAddress,
onSelectManualAddress,
onAddressFieldChange,
onLocationSelect,
}) => {
const { t } = useTranslation('global')
const [isLocationModalOpen, setIsLocationModalOpen] = useState(false)
const getDeliveryLabel = (method: string) => {
return t(`shipment.${method}`, {
@@ -47,6 +53,14 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
const isAddressReadOnly =
selectedAddressId !== null && customerAddresses.length > 0
const handleMapLocationSelect = (lat: number, lng: number) => {
onLocationSelect(lat, lng)
setIsLocationModalOpen(false)
}
const latitude = formik.values.userAddress.latitude
const longitude = formik.values.userAddress.longitude
return (
<div className='flex flex-col gap-8'>
<div className='w-full bg-white rounded-4xl p-8'>
@@ -109,7 +123,7 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
)}
</div>
{(isDineIn || isCarDelivery) && (
{isDineIn && (
<div className='mt-8'>
<Input
label='شماره میز'
@@ -135,57 +149,36 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
<Input
label='نام گیرنده'
name='userAddress.fullName'
value={formik.values.userAddress.fullName}
label='عنوان آدرس'
name='userAddress.title'
placeholder='مثال: منزل'
value={formik.values.userAddress.title}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
/>
<Input
label='شماره تماس گیرنده'
label='شماره تماس'
name='userAddress.phone'
value={formik.values.userAddress.phone}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
/>
<Input
label='استان'
name='userAddress.province'
value={formik.values.userAddress.province}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
/>
<Input
label='شهر'
name='userAddress.city'
value={formik.values.userAddress.city}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
/>
<Input
label='کد پستی'
name='userAddress.postalCode'
value={formik.values.userAddress.postalCode}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
/>
<Input
label='عرض جغرافیایی'
type='number'
name='userAddress.latitude'
value={formik.values.userAddress.latitude || ''}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
isNotRequired
/>
<Input
label='طول جغرافیایی'
type='number'
name='userAddress.longitude'
value={formik.values.userAddress.longitude || ''}
onChange={onAddressFieldChange}
readOnly={isAddressReadOnly}
isNotRequired
</div>
<div className={isAddressReadOnly ? 'pointer-events-none opacity-60' : ''}>
<LocationInput
label='موقعیت روی نقشه'
latitude={latitude || undefined}
longitude={longitude || undefined}
onClick={() => setIsLocationModalOpen(true)}
/>
</div>
@@ -198,6 +191,28 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
readOnly={isAddressReadOnly}
/>
</div>
<DefaulModal
open={isLocationModalOpen}
close={() => setIsLocationModalOpen(false)}
isHeader={true}
title_header='انتخاب موقعیت روی نقشه'
width={800}
>
<div className='p-4'>
<LocationPicker
initialPosition={
latitude && longitude
? [latitude, longitude]
: undefined
}
onLocationSelect={handleMapLocationSelect}
/>
<div className='mt-4 text-xs text-gray-600 text-center'>
روی نقشه کلیک کنید تا موقعیت را انتخاب کنید
</div>
</div>
</DefaulModal>
</div>
)}
@@ -243,6 +258,7 @@ const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
placeholder='توضیحات اضافی برای سفارش...'
value={formik.values.description}
onChange={formik.handleChange}
isNotRequired
/>
</div>
</div>
+100 -53
View File
@@ -4,7 +4,7 @@ import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
import { useCreateOrder } from './useOrderData'
import { useCreateUser } from '@/pages/customers/hooks/useUsersData'
import { useCreateUser, useCreateUserAddress } from '@/pages/customers/hooks/useUsersData'
import { useGetFoods } from '@/pages/food/hooks/useFoodData'
import { useGetRestaurantShipmentMethods } from '@/pages/shipmentMethod/hooks/useShipmentMethodData'
import { useGetRestaurantPaymentMethods } from '@/pages/paymentMethods/hooks/usePaymentMethodData'
@@ -21,6 +21,7 @@ import type { RestaurantShipmentMethod } from '@/pages/shipmentMethod/types/Type
import type { RestaurantPaymentMethod } from '@/pages/paymentMethods/types/Types'
import type { UserSavedAddress, UserWithAddresses } from '@/pages/customers/types/Types'
import { mapSavedAddressToOrderAddress } from '../utils/mapSavedAddress'
import { mapFormAddressToCreateUserAddress } from '../utils/mapFormAddressToCreateUserAddress'
import { Pages } from '@/config/Pages'
import type { ErrorType } from '@/helpers/types'
import { extractErrorMessage } from '@/config/func'
@@ -40,10 +41,8 @@ export interface CreateOrderFormValues {
}
const emptyUserAddress = (): CreateOrderUserAddress => ({
fullName: '',
title: '',
phone: '',
city: '',
province: '',
address: '',
postalCode: '',
latitude: 0,
@@ -66,7 +65,10 @@ export const useCreateOrderForm = () => {
const navigate = useNavigate()
const { mutate: createOrder, isPending: isCreating } = useCreateOrder()
const { mutateAsync: createUser, isPending: isCreatingUser } = useCreateUser()
const { mutateAsync: createUserAddress, isPending: isCreatingAddress } =
useCreateUserAddress()
const [customerAddresses, setCustomerAddresses] = useState<UserSavedAddress[]>([])
const [customerUserId, setCustomerUserId] = useState<string | null>(null)
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null)
const [isNewCustomer, setIsNewCustomer] = useState(false)
@@ -126,16 +128,14 @@ export const useCreateOrderForm = () => {
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCourier) {
const address = values.userAddress
if (
!address.fullName ||
!address.phone ||
!address.city ||
!address.province ||
!address.address
) {
if (!address.title || !address.phone || !address.address) {
toast.error('لطفاً آدرس تحویل را کامل کنید')
return
}
if (!address.latitude || !address.longitude) {
toast.error('لطفاً موقعیت روی نقشه را انتخاب کنید')
return
}
}
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCar) {
@@ -146,11 +146,17 @@ export const useCreateOrderForm = () => {
}
}
const submitOrder = () => {
if (
selectedDelivery?.method === DeliveryMethodEnum.DineIn &&
!values.tableNumber.trim()
) {
toast.error('لطفاً شماره میز را وارد کنید')
return
}
const submitOrder = (userId: string, addressId?: string) => {
const payload: CreateOrderType = {
userPhone: values.userPhone,
firstName: values.firstName,
lastName: values.lastName,
userId,
items: values.items,
deliveryMethodId: values.deliveryMethodId,
paymentMethodId: values.paymentMethodId,
@@ -160,16 +166,15 @@ export const useCreateOrderForm = () => {
payload.description = values.description.trim()
}
if (values.tableNumber.trim()) {
if (
selectedDelivery?.method === DeliveryMethodEnum.DineIn &&
values.tableNumber.trim()
) {
payload.tableNumber = values.tableNumber.trim()
}
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCourier) {
payload.userAddress = {
...values.userAddress,
latitude: Number(values.userAddress.latitude) || 0,
longitude: Number(values.userAddress.longitude) || 0,
}
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCourier && addressId) {
payload.addressId = addressId
}
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCar) {
@@ -192,20 +197,66 @@ export const useCreateOrderForm = () => {
})
}
const registerNewCustomer = async () => {
const registerNewCustomer = async (): Promise<string | null> => {
if (!isNewCustomer) {
submitOrder()
return
return customerUserId
}
try {
await createUser({
const response = await createUser({
phone: values.userPhone.trim(),
firstName: values.firstName.trim(),
lastName: values.lastName.trim(),
})
const userId = response.data.user.id
setCustomerUserId(userId)
setIsNewCustomer(false)
submitOrder()
return userId
}
const resolveAddressId = async (userId: string): Promise<string | undefined> => {
if (selectedDelivery?.method !== DeliveryMethodEnum.DeliveryCourier) {
return undefined
}
if (selectedAddressId) {
return selectedAddressId
}
const response = await createUserAddress({
userId,
...mapFormAddressToCreateUserAddress(
values.userAddress,
customerAddresses.length === 0
),
})
return response.data.id
}
const submitFlow = async () => {
try {
const userId = await registerNewCustomer()
if (!userId) {
toast.error('کاربر انتخاب نشده است')
return
}
let addressId: string | undefined
try {
addressId = await resolveAddressId(userId)
} catch (error) {
toast.error(extractErrorMessage(error as ErrorType, 'خطا در ثبت آدرس'))
return
}
if (
selectedDelivery?.method === DeliveryMethodEnum.DeliveryCourier &&
!addressId
) {
toast.error('لطفاً آدرس تحویل را انتخاب کنید')
return
}
submitOrder(userId, addressId)
} catch (error) {
toast.error(extractErrorMessage(error as ErrorType, 'خطا در ثبت مشتری'))
}
@@ -216,42 +267,30 @@ export const useCreateOrderForm = () => {
return
}
await registerNewCustomer()
await submitFlow()
},
})
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())
)
(address: UserSavedAddress) => {
formik.setFieldValue('userAddress', mapSavedAddressToOrderAddress(address))
setSelectedAddressId(address.id)
},
[formik, getCustomerFullName]
[formik]
)
const selectManualAddress = useCallback(() => {
setSelectedAddressId(null)
formik.setFieldValue('userAddress', {
...emptyUserAddress(),
fullName: getCustomerFullName(),
phone: formik.values.userPhone,
})
}, [formik, getCustomerFullName])
}, [formik])
const handleUserFound = useCallback(
(user: UserWithAddresses) => {
setIsNewCustomer(false)
setCustomerUserId(user.id)
const addresses = user.addresses ?? []
setCustomerAddresses(addresses)
@@ -259,7 +298,6 @@ export const useCreateOrderForm = () => {
setSelectedAddressId(null)
formik.setFieldValue('userAddress', {
...emptyUserAddress(),
fullName: getCustomerFullName(user.firstName, user.lastName),
phone: user.phone || formik.values.userPhone,
})
return
@@ -267,22 +305,21 @@ export const useCreateOrderForm = () => {
const defaultAddress =
addresses.find((address) => address.isDefault) ?? addresses[0]
applySavedAddress(
defaultAddress,
[user.firstName, user.lastName].filter(Boolean).join(' ')
)
applySavedAddress(defaultAddress)
},
[applySavedAddress, formik, getCustomerFullName]
[applySavedAddress, formik]
)
const clearCustomerData = useCallback(() => {
setIsNewCustomer(false)
setCustomerUserId(null)
setCustomerAddresses([])
setSelectedAddressId(null)
formik.setFieldValue('userAddress', emptyUserAddress())
}, [formik])
const handleUserNotFound = useCallback(() => {
setCustomerUserId(null)
setCustomerAddresses([])
setSelectedAddressId(null)
formik.setFieldValue('userAddress', emptyUserAddress())
@@ -397,6 +434,15 @@ export const useCreateOrderForm = () => {
[formik]
)
const handleLocationSelect = useCallback(
(lat: number, lng: number) => {
setSelectedAddressId(null)
formik.setFieldValue('userAddress.latitude', lat)
formik.setFieldValue('userAddress.longitude', lng)
},
[formik]
)
const removeItem = (foodId: string) => {
formik.setFieldValue(
'items',
@@ -418,7 +464,7 @@ export const useCreateOrderForm = () => {
addFoodToOrder,
updateItemQuantity,
removeItem,
isSubmitting: isCreating || isCreatingUser,
isSubmitting: isCreating || isCreatingUser || isCreatingAddress,
customerAddresses,
selectedAddressId,
handleUserFound,
@@ -427,6 +473,7 @@ export const useCreateOrderForm = () => {
applySavedAddress,
selectManualAddress,
handleAddressFieldChange,
handleLocationSelect,
}
}
+3 -9
View File
@@ -249,10 +249,8 @@ export interface CreateOrderItem {
}
export interface CreateOrderUserAddress {
fullName: string;
title: string;
phone: string;
city: string;
province: string;
address: string;
postalCode: string;
latitude: number;
@@ -267,18 +265,14 @@ export interface CreateOrderCarAddress {
}
export interface CreateOrderType {
userPhone: string;
firstName: string;
lastName: string;
userId: string;
items: CreateOrderItem[];
deliveryMethodId: string;
paymentMethodId: string;
description?: string;
tableNumber?: string;
userAddress?: CreateOrderUserAddress;
addressId?: string;
carAddress?: CreateOrderCarAddress;
paymentDesc?: string;
paymentAttachments?: string[];
}
export type CreateOrderResponse = IResponse<Order>;
@@ -0,0 +1,23 @@
import type { CreateUserAddressType } from '@/pages/customers/types/Types'
import type { CreateOrderUserAddress } from '../types/Types'
export const mapFormAddressToCreateUserAddress = (
address: CreateOrderUserAddress,
isDefault: boolean
): CreateUserAddressType => {
const payload: CreateUserAddressType = {
title: address.title || 'آدرس جدید',
address: address.address,
latitude: Number(address.latitude) || 0,
longitude: Number(address.longitude) || 0,
phone: address.phone,
isDefault,
}
const postalCode = address.postalCode?.trim()
if (postalCode) {
payload.postalCode = postalCode
}
return payload
}
+3 -6
View File
@@ -2,15 +2,12 @@ import type { UserSavedAddress } from '@/pages/customers/types/Types'
import type { CreateOrderUserAddress } from '../types/Types'
export const mapSavedAddressToOrderAddress = (
saved: UserSavedAddress,
fullName: string
saved: UserSavedAddress
): CreateOrderUserAddress => ({
fullName: fullName || saved.title,
title: saved.title || 'آدرس',
phone: saved.phone,
city: saved.city,
province: saved.province,
address: saved.address,
postalCode: saved.postalCode,
postalCode: saved.postalCode || '',
latitude: saved.latitude,
longitude: saved.longitude,
})