From 7130c304c546809022ee039dc8e032885c2ce772 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 23 Jun 2026 12:31:39 +0330 Subject: [PATCH] Create order --- src/components/Input.tsx | 11 +- src/components/Textarea.tsx | 8 +- src/pages/customers/hooks/useUsersData.ts | 12 +- src/pages/customers/service/UsersService.ts | 13 ++ src/pages/customers/types/Types.ts | 23 ++- src/pages/orders/Create.tsx | 2 + .../components/CreateOrderAddressSelector.tsx | 4 - .../components/CreateOrderDeliverySection.tsx | 88 ++++++---- src/pages/orders/hooks/useCreateOrderForm.ts | 161 +++++++++++------- src/pages/orders/types/Types.ts | 12 +- .../mapFormAddressToCreateUserAddress.ts | 23 +++ src/pages/orders/utils/mapSavedAddress.ts | 9 +- 12 files changed, 247 insertions(+), 119 deletions(-) create mode 100644 src/pages/orders/utils/mapFormAddressToCreateUserAddress.ts diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 4ec21e6..ecb8469 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -84,9 +84,14 @@ const Input: FC = (props: Props) => { return (
- + {props.label && ( +
+ + {props.isNotRequired && ( + (اختیاری) + )} +
+ )}
{ diff --git a/src/components/Textarea.tsx b/src/components/Textarea.tsx index b66940f..3638665 100644 --- a/src/components/Textarea.tsx +++ b/src/components/Textarea.tsx @@ -5,6 +5,7 @@ import { clx } from '../helpers/utils' type Props = { label?: string, error_text?: string + isNotRequired?: boolean } & InputHTMLAttributes const Textarea: FC = (props: Props) => { @@ -12,8 +13,11 @@ const Textarea: FC = (props: Props) => {
{ props.label && -
- {props.label} +
+
{props.label}
+ {props.isNotRequired && ( + (اختیاری) + )}
} diff --git a/src/pages/customers/hooks/useUsersData.ts b/src/pages/customers/hooks/useUsersData.ts index 3e1bed1..1c8a79f 100644 --- a/src/pages/customers/hooks/useUsersData.ts +++ b/src/pages/customers/hooks/useUsersData.ts @@ -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(); diff --git a/src/pages/customers/service/UsersService.ts b/src/pages/customers/service/UsersService.ts index 20fe137..6e25137 100644 --- a/src/pages/customers/service/UsersService.ts +++ b/src/pages/customers/service/UsersService.ts @@ -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( + `/admin/users/${userId}/addresses`, + params, + ); + return data; +}; + export const importUsersFromExcel = async (file: File) => { const formData = new FormData(); formData.append("file", file); diff --git a/src/pages/customers/types/Types.ts b/src/pages/customers/types/Types.ts index 992a67a..e5cf13e 100644 --- a/src/pages/customers/types/Types.ts +++ b/src/pages/customers/types/Types.ts @@ -103,4 +103,25 @@ export type CreateUserType = { lastName: string; }; -export type CreateUserResponse = IResponse; +export type CreateUserResult = { + id: string; + createdAt: string; + updatedAt: string; + user: User; + orderCount: number; + totalOrderAmount: number; +}; + +export type CreateUserResponse = IResponse; + +export type CreateUserAddressType = { + title: string; + address: string; + postalCode?: string; + latitude: number; + longitude: number; + phone: string; + isDefault: boolean; +}; + +export type CreateUserAddressResponse = IResponse; diff --git a/src/pages/orders/Create.tsx b/src/pages/orders/Create.tsx index 04ca2e1..3ddd83f 100644 --- a/src/pages/orders/Create.tsx +++ b/src/pages/orders/Create.tsx @@ -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} />
diff --git a/src/pages/orders/components/CreateOrderAddressSelector.tsx b/src/pages/orders/components/CreateOrderAddressSelector.tsx index 647f083..411e558 100644 --- a/src/pages/orders/components/CreateOrderAddressSelector.tsx +++ b/src/pages/orders/components/CreateOrderAddressSelector.tsx @@ -27,7 +27,6 @@ const CreateOrderAddressSelector: FC = ({
{addresses.map((address) => { const isSelected = selectedAddressId === address.id - const location = [address.province, address.city].filter(Boolean).join('، ') return (
- {location && ( -
{location}
- )}
{address.address}
) diff --git a/src/pages/orders/components/CreateOrderDeliverySection.tsx b/src/pages/orders/components/CreateOrderDeliverySection.tsx index e3f1fd8..da23bef 100644 --- a/src/pages/orders/components/CreateOrderDeliverySection.tsx +++ b/src/pages/orders/components/CreateOrderDeliverySection.tsx @@ -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) => void + onLocationSelect: (lat: number, lng: number) => void } const CreateOrderDeliverySection: FC = ({ @@ -35,8 +39,10 @@ const CreateOrderDeliverySection: FC = ({ 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 = ({ 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 (
@@ -109,7 +123,7 @@ const CreateOrderDeliverySection: FC = ({ )}
- {(isDineIn || isCarDelivery) && ( + {isDineIn && (
= ({
- - - - + +
+ setIsLocationModalOpen(true)} />
@@ -198,6 +191,28 @@ const CreateOrderDeliverySection: FC = ({ readOnly={isAddressReadOnly} />
+ + setIsLocationModalOpen(false)} + isHeader={true} + title_header='انتخاب موقعیت روی نقشه' + width={800} + > +
+ +
+ روی نقشه کلیک کنید تا موقعیت را انتخاب کنید +
+
+
)} @@ -243,6 +258,7 @@ const CreateOrderDeliverySection: FC = ({ placeholder='توضیحات اضافی برای سفارش...' value={formik.values.description} onChange={formik.handleChange} + isNotRequired />
diff --git a/src/pages/orders/hooks/useCreateOrderForm.ts b/src/pages/orders/hooks/useCreateOrderForm.ts index 2ee4eb1..c662e77 100644 --- a/src/pages/orders/hooks/useCreateOrderForm.ts +++ b/src/pages/orders/hooks/useCreateOrderForm.ts @@ -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([]) + const [customerUserId, setCustomerUserId] = useState(null) const [selectedAddressId, setSelectedAddressId] = useState(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 => { if (!isNewCustomer) { - submitOrder() - return + return customerUserId } + 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) + return userId + } + + const resolveAddressId = async (userId: string): Promise => { + 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 { - await createUser({ - phone: values.userPhone.trim(), - firstName: values.firstName.trim(), - lastName: values.lastName.trim(), - }) - setIsNewCustomer(false) - submitOrder() + 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, } } diff --git a/src/pages/orders/types/Types.ts b/src/pages/orders/types/Types.ts index 304b03e..a5aef65 100644 --- a/src/pages/orders/types/Types.ts +++ b/src/pages/orders/types/Types.ts @@ -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; diff --git a/src/pages/orders/utils/mapFormAddressToCreateUserAddress.ts b/src/pages/orders/utils/mapFormAddressToCreateUserAddress.ts new file mode 100644 index 0000000..d4b4e59 --- /dev/null +++ b/src/pages/orders/utils/mapFormAddressToCreateUserAddress.ts @@ -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 +} diff --git a/src/pages/orders/utils/mapSavedAddress.ts b/src/pages/orders/utils/mapSavedAddress.ts index a85ef2f..5e215df 100644 --- a/src/pages/orders/utils/mapSavedAddress.ts +++ b/src/pages/orders/utils/mapSavedAddress.ts @@ -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, })