base create order
This commit is contained in:
@@ -9,6 +9,12 @@ export const useGetUsers = (params?: GetUsersParams) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useSearchUserByPhone = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.searchUserByPhone,
|
||||
});
|
||||
};
|
||||
|
||||
export const useImportUsersFromExcel = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
GetCustomersResponse,
|
||||
GetUsersParams,
|
||||
ImportUsersResponse,
|
||||
SearchUserByPhoneResponse,
|
||||
} from "@/pages/customers/types/Types";
|
||||
|
||||
export const getUsers = async (params?: GetUsersParams) => {
|
||||
@@ -12,6 +13,14 @@ export const getUsers = async (params?: GetUsersParams) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const searchUserByPhone = async (phone: string) => {
|
||||
const { data } = await axios.get<SearchUserByPhoneResponse>(
|
||||
"/admin/users/search-by-phone",
|
||||
{ params: { phone } },
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const importUsersFromExcel = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
@@ -72,3 +72,5 @@ export type ImportUsersResult = {
|
||||
};
|
||||
|
||||
export type ImportUsersResponse = IResponse<ImportUsersResult>;
|
||||
|
||||
export type SearchUserByPhoneResponse = IResponse<User>;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { type FC } from 'react'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import Button from '@/components/Button'
|
||||
import { useCreateOrderForm } from './hooks/useCreateOrderForm'
|
||||
import CreateOrderCustomerSection from './components/CreateOrderCustomerSection'
|
||||
import CreateOrderItemsSection from './components/CreateOrderItemsSection'
|
||||
import CreateOrderDeliverySection from './components/CreateOrderDeliverySection'
|
||||
import CreateOrderSidebar from './components/CreateOrderSidebar'
|
||||
|
||||
const CreateOrder: FC = () => {
|
||||
const {
|
||||
formik,
|
||||
foods,
|
||||
deliveryMethods,
|
||||
cashPaymentMethod,
|
||||
selectedDeliveryMethod,
|
||||
selectedPaymentMethod,
|
||||
isCourierDelivery,
|
||||
isCarDelivery,
|
||||
isDineIn,
|
||||
orderSummary,
|
||||
addFoodToOrder,
|
||||
updateItemQuantity,
|
||||
removeItem,
|
||||
isSubmitting,
|
||||
} = useCreateOrderForm()
|
||||
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h1 className='text-lg font-light'>ثبت سفارش جدید</h1>
|
||||
<Button
|
||||
className='w-fit px-6'
|
||||
isloading={isSubmitting}
|
||||
onClick={() => formik.handleSubmit()}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='#fff' />
|
||||
<span>ثبت سفارش</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col xl:flex-row gap-7 mt-9'>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<CreateOrderCustomerSection formik={formik} />
|
||||
|
||||
<div className='mt-8'>
|
||||
<CreateOrderItemsSection
|
||||
formik={formik}
|
||||
foods={foods}
|
||||
onAddFood={addFoodToOrder}
|
||||
onUpdateQuantity={updateItemQuantity}
|
||||
onRemoveItem={removeItem}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateOrderDeliverySection
|
||||
formik={formik}
|
||||
deliveryMethods={deliveryMethods}
|
||||
cashPaymentMethod={cashPaymentMethod}
|
||||
isCourierDelivery={isCourierDelivery}
|
||||
isCarDelivery={isCarDelivery}
|
||||
isDineIn={isDineIn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='w-full xl:w-[330px] flex-shrink-0 xl:sticky xl:top-4 xl:self-start'>
|
||||
<CreateOrderSidebar
|
||||
formik={formik}
|
||||
orderSummary={orderSummary}
|
||||
selectedDeliveryMethod={selectedDeliveryMethod}
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateOrder
|
||||
@@ -1,7 +1,11 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Add } from 'iconsax-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import Table from '@/components/Table'
|
||||
import Filters from '@/components/Filters'
|
||||
import Button from '@/components/Button'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import { useGetOrders } from './hooks/useOrderData'
|
||||
import { useOrderFilters } from './hooks/useOrderFilters'
|
||||
import { getOrderTableColumns } from './components/OrderTableColumns'
|
||||
@@ -32,6 +36,14 @@ const OrdersList: FC = () => {
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h1 className='text-lg font-light'>لیست سفارشات</h1>
|
||||
<Link to={Pages.orders.add}>
|
||||
<Button className='w-fit px-6'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add color='#fff' size={20} />
|
||||
<span>ثبت سفارش</span>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { SearchNormal, TickCircle, UserAdd } from 'iconsax-react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { toast } from 'react-toastify'
|
||||
import Button from '@/components/Button'
|
||||
import Input from '@/components/Input'
|
||||
import { useSearchUserByPhone } from '@/pages/customers/hooks/useUsersData'
|
||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
||||
import type { ErrorType } from '@/helpers/types'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
import { isAxiosError } from 'axios'
|
||||
|
||||
interface CreateOrderCustomerSectionProps {
|
||||
formik: FormikProps<CreateOrderFormValues>
|
||||
}
|
||||
|
||||
type CustomerSearchState = 'idle' | 'found' | 'not_found'
|
||||
|
||||
const CreateOrderCustomerSection: FC<CreateOrderCustomerSectionProps> = ({ formik }) => {
|
||||
const { mutate: searchUser, isPending: isSearching } = useSearchUserByPhone()
|
||||
const [searchState, setSearchState] = useState<CustomerSearchState>('idle')
|
||||
|
||||
const handleSearch = () => {
|
||||
const phone = formik.values.userPhone.trim()
|
||||
if (!phone) {
|
||||
toast.error('شماره تلفن را وارد کنید')
|
||||
return
|
||||
}
|
||||
|
||||
searchUser(phone, {
|
||||
onSuccess: (response) => {
|
||||
const user = response?.data
|
||||
if (!user) {
|
||||
setSearchState('not_found')
|
||||
formik.setFieldValue('firstName', '')
|
||||
formik.setFieldValue('lastName', '')
|
||||
toast.info('مشتری یافت نشد. اطلاعات را وارد کنید.')
|
||||
return
|
||||
}
|
||||
|
||||
formik.setFieldValue('userPhone', user.phone || phone)
|
||||
formik.setFieldValue('firstName', user.firstName || '')
|
||||
formik.setFieldValue('lastName', user.lastName || '')
|
||||
setSearchState('found')
|
||||
toast.success('اطلاعات مشتری بارگذاری شد')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
setSearchState('not_found')
|
||||
formik.setFieldValue('firstName', '')
|
||||
formik.setFieldValue('lastName', '')
|
||||
toast.info('مشتری یافت نشد. اطلاعات را وارد کنید.')
|
||||
return
|
||||
}
|
||||
|
||||
setSearchState('idle')
|
||||
toast.error(extractErrorMessage(error, 'خطا در جستجوی مشتری'))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
formik.handleChange(e)
|
||||
if (searchState !== 'idle') {
|
||||
setSearchState('idle')
|
||||
}
|
||||
}
|
||||
|
||||
const isCustomerLocked = searchState !== 'not_found'
|
||||
|
||||
return (
|
||||
<div className='w-full bg-white rounded-4xl p-8'>
|
||||
<div className='text-lg font-light'>اطلاعات مشتری</div>
|
||||
|
||||
<div className='flex flex-col sm:flex-row gap-3 mt-6 sm:items-end'>
|
||||
<div className='flex-1'>
|
||||
<Input
|
||||
label='شماره تلفن'
|
||||
name='userPhone'
|
||||
placeholder='۰۹۱۲۱۲۳۴۵۶۷'
|
||||
value={formik.values.userPhone}
|
||||
onChange={handlePhoneChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSearch()
|
||||
}
|
||||
}}
|
||||
error_text={
|
||||
formik.touched.userPhone && formik.errors.userPhone
|
||||
? formik.errors.userPhone
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
className='w-full sm:w-fit px-6'
|
||||
isloading={isSearching}
|
||||
onClick={handleSearch}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<SearchNormal size={18} color='#fff' />
|
||||
<span>جستجو</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searchState === 'found' && (
|
||||
<div className='mt-4 flex items-center gap-2 text-xs text-green-600 bg-green-50 rounded-xl px-4 py-3'>
|
||||
<TickCircle size={16} color='#16A34A' />
|
||||
<span>مشتری یافت شد و اطلاعات تکمیل شد</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState === 'not_found' && (
|
||||
<div className='mt-4 flex items-center gap-2 text-xs text-description bg-[#F8F9FC] rounded-xl px-4 py-3'>
|
||||
<UserAdd size={16} color='#8C90A3' />
|
||||
<span>مشتری یافت نشد. نام و نام خانوادگی را وارد کنید.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
||||
<Input
|
||||
label='نام'
|
||||
name='firstName'
|
||||
value={formik.values.firstName}
|
||||
onChange={formik.handleChange}
|
||||
readOnly={isCustomerLocked}
|
||||
error_text={
|
||||
formik.touched.firstName && formik.errors.firstName
|
||||
? formik.errors.firstName
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label='نام خانوادگی'
|
||||
name='lastName'
|
||||
value={formik.values.lastName}
|
||||
onChange={formik.handleChange}
|
||||
readOnly={isCustomerLocked}
|
||||
error_text={
|
||||
formik.touched.lastName && formik.errors.lastName
|
||||
? formik.errors.lastName
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{searchState === 'idle' && (
|
||||
<div className='mt-4 text-xs text-description'>
|
||||
ابتدا با شماره تلفن مشتری را جستجو کنید.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateOrderCustomerSection
|
||||
@@ -0,0 +1,223 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/components/Input'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import { formatPrice } from '@/helpers/func'
|
||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
||||
import type { RestaurantPaymentMethod, RestaurantShipmentMethod } from '../hooks/useCreateOrderForm'
|
||||
|
||||
interface CreateOrderDeliverySectionProps {
|
||||
formik: FormikProps<CreateOrderFormValues>
|
||||
deliveryMethods: RestaurantShipmentMethod[]
|
||||
cashPaymentMethod?: RestaurantPaymentMethod
|
||||
isCourierDelivery: boolean
|
||||
isCarDelivery: boolean
|
||||
isDineIn: boolean
|
||||
}
|
||||
|
||||
const CreateOrderDeliverySection: FC<CreateOrderDeliverySectionProps> = ({
|
||||
formik,
|
||||
deliveryMethods,
|
||||
cashPaymentMethod,
|
||||
isCourierDelivery,
|
||||
isCarDelivery,
|
||||
isDineIn,
|
||||
}) => {
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
const getDeliveryLabel = (method: string) => {
|
||||
return t(`shipment.${method}`, {
|
||||
defaultValue: method,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full bg-white rounded-4xl p-8'>
|
||||
<div className='text-lg font-light'>تحویل و پرداخت</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm mb-3'>روش تحویل</div>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
|
||||
{deliveryMethods.map((method) => {
|
||||
const isSelected = formik.values.deliveryMethodId === method.id
|
||||
return (
|
||||
<button
|
||||
key={method.id}
|
||||
type='button'
|
||||
onClick={() => formik.setFieldValue('deliveryMethodId', method.id)}
|
||||
className={`text-right p-4 rounded-xl border transition-colors ${
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/40'
|
||||
}`}
|
||||
>
|
||||
<div className='text-sm font-medium'>
|
||||
{getDeliveryLabel(method.method)}
|
||||
</div>
|
||||
{method.description && (
|
||||
<div className='text-xs text-description mt-1 line-clamp-2'>
|
||||
{method.description}
|
||||
</div>
|
||||
)}
|
||||
{method.deliveryFee > 0 && (
|
||||
<div className='text-xs mt-2 text-primary'>
|
||||
{formatPrice(method.deliveryFee)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{formik.touched.deliveryMethodId && formik.errors.deliveryMethodId && (
|
||||
<div className='text-xs text-red-500 mt-2'>
|
||||
{formik.errors.deliveryMethodId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
<div className='text-sm mb-3'>روش پرداخت</div>
|
||||
<div className='p-4 rounded-xl border border-primary bg-primary/5'>
|
||||
<div className='text-sm font-medium'>پرداخت نقدی</div>
|
||||
{cashPaymentMethod?.description && (
|
||||
<div className='text-xs text-description mt-1'>
|
||||
{cashPaymentMethod.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!cashPaymentMethod && (
|
||||
<div className='text-xs text-red-500 mt-2'>
|
||||
روش پرداخت نقدی فعال یافت نشد
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(isDineIn || isCarDelivery) && (
|
||||
<div className='mt-8'>
|
||||
<Input
|
||||
label='شماره میز'
|
||||
name='tableNumber'
|
||||
placeholder='مثال: ۱۲'
|
||||
value={formik.values.tableNumber}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCourierDelivery && (
|
||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
||||
<div className='text-lg font-light'>آدرس تحویل</div>
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
||||
<Input
|
||||
label='نام گیرنده'
|
||||
name='userAddress.fullName'
|
||||
value={formik.values.userAddress.fullName}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='شماره تماس گیرنده'
|
||||
name='userAddress.phone'
|
||||
value={formik.values.userAddress.phone}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='استان'
|
||||
name='userAddress.province'
|
||||
value={formik.values.userAddress.province}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='شهر'
|
||||
name='userAddress.city'
|
||||
value={formik.values.userAddress.city}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='کد پستی'
|
||||
name='userAddress.postalCode'
|
||||
value={formik.values.userAddress.postalCode}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='عرض جغرافیایی'
|
||||
type='number'
|
||||
name='userAddress.latitude'
|
||||
value={formik.values.userAddress.latitude || ''}
|
||||
onChange={formik.handleChange}
|
||||
isNotRequired
|
||||
/>
|
||||
<Input
|
||||
label='طول جغرافیایی'
|
||||
type='number'
|
||||
name='userAddress.longitude'
|
||||
value={formik.values.userAddress.longitude || ''}
|
||||
onChange={formik.handleChange}
|
||||
isNotRequired
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Textarea
|
||||
label='آدرس کامل'
|
||||
name='userAddress.address'
|
||||
value={formik.values.userAddress.address}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCarDelivery && (
|
||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
||||
<div className='text-lg font-light'>اطلاعات خودرو</div>
|
||||
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6'>
|
||||
<Input
|
||||
label='مدل خودرو'
|
||||
name='carAddress.carModel'
|
||||
value={formik.values.carAddress.carModel}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='رنگ خودرو'
|
||||
name='carAddress.carColor'
|
||||
value={formik.values.carAddress.carColor}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='پلاک'
|
||||
name='carAddress.plateNumber'
|
||||
value={formik.values.carAddress.plateNumber}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<Input
|
||||
label='شماره تماس'
|
||||
name='carAddress.phone'
|
||||
value={formik.values.carAddress.phone}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='w-full bg-white rounded-4xl p-8 mt-8'>
|
||||
<div className='text-lg font-light'>توضیحات سفارش</div>
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label='توضیحات'
|
||||
name='description'
|
||||
placeholder='توضیحات اضافی برای سفارش...'
|
||||
value={formik.values.description}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateOrderDeliverySection
|
||||
@@ -0,0 +1,191 @@
|
||||
import { type FC, useMemo, useState } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Add, Minus, Trash } from 'iconsax-react'
|
||||
import Input from '@/components/Input'
|
||||
import type { Food } from '@/pages/food/types/Types'
|
||||
import { formatFaNumber, formatPrice } from '@/helpers/func'
|
||||
import { getFoodUnitPrice } from '../hooks/useCreateOrderForm'
|
||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
||||
|
||||
interface CreateOrderItemsSectionProps {
|
||||
formik: FormikProps<CreateOrderFormValues>
|
||||
foods: Food[]
|
||||
onAddFood: (foodId: string) => void
|
||||
onUpdateQuantity: (foodId: string, quantity: number) => void
|
||||
onRemoveItem: (foodId: string) => void
|
||||
}
|
||||
|
||||
const CreateOrderItemsSection: FC<CreateOrderItemsSectionProps> = ({
|
||||
formik,
|
||||
foods,
|
||||
onAddFood,
|
||||
onUpdateQuantity,
|
||||
onRemoveItem,
|
||||
}) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const foodsMap = useMemo(
|
||||
() => new Map(foods.map((food) => [food.id, food])),
|
||||
[foods]
|
||||
)
|
||||
|
||||
const filteredFoods = useMemo(() => {
|
||||
const query = search.trim().toLowerCase()
|
||||
if (!query) return foods
|
||||
return foods.filter((food) => food.title.toLowerCase().includes(query))
|
||||
}, [foods, search])
|
||||
|
||||
const selectedItems = formik.values.items
|
||||
.map((item) => {
|
||||
const food = foodsMap.get(item.foodId)
|
||||
if (!food) return null
|
||||
return { ...item, food }
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className='w-full bg-white rounded-4xl p-8'>
|
||||
<div className='text-lg font-light'>آیتمهای سفارش</div>
|
||||
|
||||
<div className='mt-6 border border-border rounded-xl p-4'>
|
||||
<Input
|
||||
variant='search'
|
||||
className='bg-[#EEF0F7]'
|
||||
placeholder='جستجوی غذا...'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className='mt-3 space-y-2 max-h-56 overflow-y-auto'>
|
||||
{filteredFoods.length === 0 ? (
|
||||
<div className='text-center py-6 text-xs text-description'>
|
||||
غذایی یافت نشد
|
||||
</div>
|
||||
) : (
|
||||
filteredFoods.map((food) => {
|
||||
const inCart = formik.values.items.some(
|
||||
(item) => item.foodId === food.id
|
||||
)
|
||||
return (
|
||||
<div
|
||||
key={food.id}
|
||||
className='flex items-center gap-3 p-2 rounded-xl hover:bg-[#F8F9FC] transition-colors'
|
||||
>
|
||||
{food.images?.[0] ? (
|
||||
<img
|
||||
src={food.images[0]}
|
||||
alt={food.title}
|
||||
className='w-11 h-11 rounded-lg object-cover flex-shrink-0'
|
||||
/>
|
||||
) : (
|
||||
<div className='w-11 h-11 rounded-lg bg-gray-100 flex-shrink-0' />
|
||||
)}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='text-sm truncate'>{food.title}</div>
|
||||
<div className='text-xs text-description mt-0.5'>
|
||||
{formatPrice(getFoodUnitPrice(food))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => onAddFood(food.id)}
|
||||
className={`flex items-center gap-1 px-3 h-8 rounded-lg text-xs transition-colors ${
|
||||
inCart
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-primary text-white'
|
||||
}`}
|
||||
>
|
||||
<Add size={14} color={inCart ? '#000' : '#fff'} />
|
||||
{inCart ? 'افزودن' : 'انتخاب'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm mb-3'>سبد سفارش</div>
|
||||
|
||||
{selectedItems.length === 0 ? (
|
||||
<div className='border border-dashed border-border rounded-xl py-10 text-center text-xs text-description'>
|
||||
هنوز آیتمی انتخاب نشده است
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-3'>
|
||||
{selectedItems.map((item) => {
|
||||
if (!item) return null
|
||||
const unitPrice = getFoodUnitPrice(item.food)
|
||||
return (
|
||||
<div
|
||||
key={item.foodId}
|
||||
className='flex items-center gap-3 p-3 border border-border rounded-xl'
|
||||
>
|
||||
{item.food.images?.[0] ? (
|
||||
<img
|
||||
src={item.food.images[0]}
|
||||
alt={item.food.title}
|
||||
className='w-12 h-12 rounded-lg object-cover flex-shrink-0'
|
||||
/>
|
||||
) : (
|
||||
<div className='w-12 h-12 rounded-lg bg-gray-100 flex-shrink-0' />
|
||||
)}
|
||||
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='text-sm truncate'>{item.food.title}</div>
|
||||
<div className='text-xs text-description mt-1'>
|
||||
{formatPrice(unitPrice)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() =>
|
||||
onUpdateQuantity(item.foodId, item.quantity - 1)
|
||||
}
|
||||
className='w-8 h-8 rounded-lg border border-border flex items-center justify-center'
|
||||
>
|
||||
<Minus size={14} color='#000' />
|
||||
</button>
|
||||
<span className='w-6 text-center text-sm'>
|
||||
{formatFaNumber(item.quantity)}
|
||||
</span>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() =>
|
||||
onUpdateQuantity(item.foodId, item.quantity + 1)
|
||||
}
|
||||
className='w-8 h-8 rounded-lg border border-border flex items-center justify-center'
|
||||
>
|
||||
<Add size={14} color='#000' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='text-sm font-medium min-w-[90px] text-left'>
|
||||
{formatPrice(unitPrice * item.quantity)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => onRemoveItem(item.foodId)}
|
||||
className='text-red-500 p-1'
|
||||
>
|
||||
<Trash size={18} color='#EF4444' />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formik.touched.items && formik.errors.items && typeof formik.errors.items === 'string' && (
|
||||
<div className='text-xs text-red-500 mt-2'>{formik.errors.items}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateOrderItemsSection
|
||||
@@ -0,0 +1,148 @@
|
||||
import { type FC } from 'react'
|
||||
import { ShoppingCart, User } from 'iconsax-react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { formatFaNumber, formatPrice } from '@/helpers/func'
|
||||
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
||||
import { DeliveryMethodEnum } from '@/pages/shipmentMethod/enum/Enum'
|
||||
import type { CreateOrderFormValues } from '../hooks/useCreateOrderForm'
|
||||
import type { RestaurantPaymentMethod, RestaurantShipmentMethod } from '../hooks/useCreateOrderForm'
|
||||
|
||||
interface OrderSummaryData {
|
||||
lineItems: Array<{
|
||||
foodId: string
|
||||
title: string
|
||||
image: string
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
totalPrice: number
|
||||
} | null>
|
||||
subTotal: number
|
||||
deliveryFee: number
|
||||
total: number
|
||||
totalItems: number
|
||||
}
|
||||
|
||||
interface CreateOrderSidebarProps {
|
||||
formik: FormikProps<CreateOrderFormValues>
|
||||
orderSummary: OrderSummaryData
|
||||
selectedDeliveryMethod?: RestaurantShipmentMethod
|
||||
selectedPaymentMethod?: RestaurantPaymentMethod
|
||||
}
|
||||
|
||||
const getDeliveryMethodText = (method?: string): string => {
|
||||
const methodMap: Record<string, string> = {
|
||||
[DeliveryMethodEnum.DineIn]: 'سرو در رستوران',
|
||||
[DeliveryMethodEnum.CustomerPickup]: 'دریافت از محل',
|
||||
[DeliveryMethodEnum.DeliveryCar]: 'تحویل به خودرو',
|
||||
[DeliveryMethodEnum.DeliveryCourier]: 'بیرونبر',
|
||||
}
|
||||
return method ? methodMap[method] || method : '-'
|
||||
}
|
||||
|
||||
const getPaymentMethodText = (method?: string): string => {
|
||||
const methodMap: Record<string, string> = {
|
||||
[PaymentMethodEnum.Cash]: 'پرداخت نقدی',
|
||||
[PaymentMethodEnum.Online]: 'پرداخت آنلاین',
|
||||
[PaymentMethodEnum.CreditCard]: 'کارت به کارت',
|
||||
[PaymentMethodEnum.Wallet]: 'کیف پول',
|
||||
}
|
||||
return method ? methodMap[method] || method : '-'
|
||||
}
|
||||
|
||||
const CreateOrderSidebar: FC<CreateOrderSidebarProps> = ({
|
||||
formik,
|
||||
orderSummary,
|
||||
selectedDeliveryMethod,
|
||||
selectedPaymentMethod,
|
||||
}) => {
|
||||
const customerName = [formik.values.firstName, formik.values.lastName]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='bg-white rounded-4xl p-8'>
|
||||
<div className='flex items-center gap-2 text-lg font-light'>
|
||||
<User size={20} color='#000' />
|
||||
خلاصه سفارش
|
||||
</div>
|
||||
|
||||
<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'>
|
||||
<span className='text-description'>مشتری</span>
|
||||
<span>{customerName || '-'}</span>
|
||||
</div>
|
||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
||||
<span className='text-description'>شماره تماس</span>
|
||||
<span>{formik.values.userPhone || '-'}</span>
|
||||
</div>
|
||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
||||
<span className='text-description'>روش تحویل</span>
|
||||
<span>{getDeliveryMethodText(selectedDeliveryMethod?.method)}</span>
|
||||
</div>
|
||||
<div className='flex justify-between items-center border-b border-border border-dashed pb-3'>
|
||||
<span className='text-description'>روش پرداخت</span>
|
||||
<span>{getPaymentMethodText(selectedPaymentMethod?.method)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white rounded-4xl p-8'>
|
||||
<div className='flex items-center gap-2 text-lg font-light'>
|
||||
<ShoppingCart size={20} color='#000' />
|
||||
آیتمها
|
||||
{orderSummary.totalItems > 0 && (
|
||||
<span className='text-xs text-description'>
|
||||
({formatFaNumber(orderSummary.totalItems)})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{orderSummary.lineItems.length === 0 ? (
|
||||
<div className='mt-6 text-center text-xs text-description py-6'>
|
||||
سبد خالی است
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-6 space-y-3'>
|
||||
{orderSummary.lineItems.map((item) => {
|
||||
if (!item) return null
|
||||
return (
|
||||
<div
|
||||
key={item.foodId}
|
||||
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 className='text-description mt-1'>
|
||||
{formatFaNumber(item.quantity)} × {formatPrice(item.unitPrice)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{formatPrice(item.totalPrice)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='mt-6 space-y-3 text-[13px] font-light'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<span className='text-description'>جمع آیتمها</span>
|
||||
<span>{formatPrice(orderSummary.subTotal)}</span>
|
||||
</div>
|
||||
{orderSummary.deliveryFee > 0 && (
|
||||
<div className='flex justify-between items-center'>
|
||||
<span className='text-description'>هزینه ارسال</span>
|
||||
<span>{formatPrice(orderSummary.deliveryFee)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex justify-between items-center font-bold text-base pt-3 border-t-2 border-gray-200'>
|
||||
<span>جمع کل (تقریبی)</span>
|
||||
<span>{formatPrice(orderSummary.total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateOrderSidebar
|
||||
@@ -0,0 +1,308 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useCreateOrder } from './useOrderData'
|
||||
import { useGetFoods } from '@/pages/food/hooks/useFoodData'
|
||||
import { useGetRestaurantShipmentMethods } from '@/pages/shipmentMethod/hooks/useShipmentMethodData'
|
||||
import { useGetRestaurantPaymentMethods } from '@/pages/paymentMethods/hooks/usePaymentMethodData'
|
||||
import { DeliveryMethodEnum } from '@/pages/shipmentMethod/enum/Enum'
|
||||
import { PaymentMethodEnum } from '@/pages/paymentMethods/enum/Enum'
|
||||
import type {
|
||||
CreateOrderType,
|
||||
CreateOrderItem,
|
||||
CreateOrderUserAddress,
|
||||
CreateOrderCarAddress,
|
||||
} from '../types/Types'
|
||||
import type { Food } from '@/pages/food/types/Types'
|
||||
import type { RestaurantShipmentMethod } from '@/pages/shipmentMethod/types/Types'
|
||||
import type { RestaurantPaymentMethod } from '@/pages/paymentMethods/types/Types'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import type { ErrorType } from '@/helpers/types'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
|
||||
export interface CreateOrderFormValues {
|
||||
userPhone: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
items: CreateOrderItem[]
|
||||
deliveryMethodId: string
|
||||
paymentMethodId: string
|
||||
description: string
|
||||
tableNumber: string
|
||||
userAddress: CreateOrderUserAddress
|
||||
carAddress: CreateOrderCarAddress
|
||||
paymentDesc: string
|
||||
}
|
||||
|
||||
const emptyUserAddress = (): CreateOrderUserAddress => ({
|
||||
fullName: '',
|
||||
phone: '',
|
||||
city: '',
|
||||
province: '',
|
||||
address: '',
|
||||
postalCode: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
})
|
||||
|
||||
const emptyCarAddress = (): CreateOrderCarAddress => ({
|
||||
carModel: '',
|
||||
carColor: '',
|
||||
plateNumber: '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
export const getFoodUnitPrice = (food: Food) => {
|
||||
const discount = food.discount || 0
|
||||
return Math.max(food.price - discount, 0)
|
||||
}
|
||||
|
||||
export const useCreateOrderForm = () => {
|
||||
const navigate = useNavigate()
|
||||
const { mutate: createOrder, isPending: isCreating } = useCreateOrder()
|
||||
|
||||
const { data: foodsData } = useGetFoods({ limit: 500 })
|
||||
const { data: deliveryMethodsData } = useGetRestaurantShipmentMethods({ limit: 100 })
|
||||
const { data: paymentMethodsData } = useGetRestaurantPaymentMethods({ limit: 100 })
|
||||
|
||||
const foods = useMemo(() => foodsData?.data || [], [foodsData?.data])
|
||||
const deliveryMethods = useMemo(
|
||||
() => (deliveryMethodsData?.data || []).filter((method) => method.enabled),
|
||||
[deliveryMethodsData?.data]
|
||||
)
|
||||
const paymentMethods = useMemo(
|
||||
() =>
|
||||
(paymentMethodsData?.data || []).filter(
|
||||
(method) => method.enabled && method.method === PaymentMethodEnum.Cash
|
||||
),
|
||||
[paymentMethodsData?.data]
|
||||
)
|
||||
|
||||
const formik = useFormik<CreateOrderFormValues>({
|
||||
initialValues: {
|
||||
userPhone: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
items: [],
|
||||
deliveryMethodId: '',
|
||||
paymentMethodId: '',
|
||||
description: '',
|
||||
tableNumber: '',
|
||||
userAddress: emptyUserAddress(),
|
||||
carAddress: emptyCarAddress(),
|
||||
paymentDesc: '',
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
userPhone: Yup.string().required('شماره تلفن الزامی است'),
|
||||
firstName: Yup.string().required('نام الزامی است'),
|
||||
lastName: Yup.string().required('نام خانوادگی الزامی است'),
|
||||
deliveryMethodId: Yup.string().required('روش تحویل الزامی است'),
|
||||
paymentMethodId: Yup.string().required('روش پرداخت الزامی است'),
|
||||
items: Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
foodId: Yup.string().required(),
|
||||
quantity: Yup.number().min(1).required(),
|
||||
})
|
||||
)
|
||||
.min(1, 'حداقل یک آیتم الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const selectedDelivery = deliveryMethods.find(
|
||||
(method) => method.id === values.deliveryMethodId
|
||||
)
|
||||
const selectedPayment = paymentMethods.find(
|
||||
(method) => method.id === values.paymentMethodId
|
||||
)
|
||||
|
||||
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCourier) {
|
||||
const address = values.userAddress
|
||||
if (
|
||||
!address.fullName ||
|
||||
!address.phone ||
|
||||
!address.city ||
|
||||
!address.province ||
|
||||
!address.address
|
||||
) {
|
||||
toast.error('لطفاً آدرس تحویل را کامل کنید')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedDelivery?.method === DeliveryMethodEnum.DeliveryCar) {
|
||||
const car = values.carAddress
|
||||
if (!car.carModel || !car.carColor || !car.plateNumber || !car.phone) {
|
||||
toast.error('لطفاً اطلاعات خودرو را کامل کنید')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const submitOrder = () => {
|
||||
const payload: CreateOrderType = {
|
||||
userPhone: values.userPhone,
|
||||
firstName: values.firstName,
|
||||
lastName: values.lastName,
|
||||
items: values.items,
|
||||
deliveryMethodId: values.deliveryMethodId,
|
||||
paymentMethodId: values.paymentMethodId,
|
||||
}
|
||||
|
||||
if (values.description.trim()) {
|
||||
payload.description = values.description.trim()
|
||||
}
|
||||
|
||||
if (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.DeliveryCar) {
|
||||
payload.carAddress = values.carAddress
|
||||
}
|
||||
|
||||
createOrder(payload, {
|
||||
onSuccess: (response) => {
|
||||
toast.success('سفارش با موفقیت ثبت شد')
|
||||
const orderId = response?.data?.id
|
||||
if (orderId) {
|
||||
navigate(Pages.orders.detail + orderId)
|
||||
} else {
|
||||
navigate(Pages.orders.list)
|
||||
}
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!selectedPayment) {
|
||||
toast.error('روش پرداخت نقدی یافت نشد')
|
||||
return
|
||||
}
|
||||
|
||||
submitOrder()
|
||||
},
|
||||
})
|
||||
|
||||
const cashPaymentMethod = paymentMethods[0]
|
||||
|
||||
useEffect(() => {
|
||||
if (cashPaymentMethod && formik.values.paymentMethodId !== cashPaymentMethod.id) {
|
||||
formik.setFieldValue('paymentMethodId', cashPaymentMethod.id)
|
||||
}
|
||||
}, [cashPaymentMethod, formik.values.paymentMethodId, formik.setFieldValue])
|
||||
|
||||
const selectedDeliveryMethod = deliveryMethods.find(
|
||||
(method) => method.id === formik.values.deliveryMethodId
|
||||
)
|
||||
const selectedPaymentMethod = paymentMethods.find(
|
||||
(method) => method.id === formik.values.paymentMethodId
|
||||
)
|
||||
|
||||
const isCourierDelivery =
|
||||
selectedDeliveryMethod?.method === DeliveryMethodEnum.DeliveryCourier
|
||||
const isCarDelivery =
|
||||
selectedDeliveryMethod?.method === DeliveryMethodEnum.DeliveryCar
|
||||
const isDineIn = selectedDeliveryMethod?.method === DeliveryMethodEnum.DineIn
|
||||
|
||||
const foodsMap = useMemo(
|
||||
() => new Map(foods.map((food) => [food.id, food])),
|
||||
[foods]
|
||||
)
|
||||
|
||||
const orderSummary = useMemo(() => {
|
||||
const lineItems = formik.values.items
|
||||
.map((item) => {
|
||||
const food = foodsMap.get(item.foodId)
|
||||
if (!food) return null
|
||||
const unitPrice = getFoodUnitPrice(food)
|
||||
return {
|
||||
foodId: item.foodId,
|
||||
title: food.title,
|
||||
image: food.images?.[0] || '',
|
||||
quantity: item.quantity,
|
||||
unitPrice,
|
||||
totalPrice: unitPrice * item.quantity,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
const subTotal = lineItems.reduce((sum, item) => sum + (item?.totalPrice || 0), 0)
|
||||
const deliveryFee = selectedDeliveryMethod?.deliveryFee || 0
|
||||
const totalItems = lineItems.reduce((sum, item) => sum + (item?.quantity || 0), 0)
|
||||
|
||||
return {
|
||||
lineItems,
|
||||
subTotal,
|
||||
deliveryFee,
|
||||
total: subTotal + deliveryFee,
|
||||
totalItems,
|
||||
}
|
||||
}, [formik.values.items, foodsMap, selectedDeliveryMethod])
|
||||
|
||||
const addFoodToOrder = (foodId: string) => {
|
||||
const existingIndex = formik.values.items.findIndex((item) => item.foodId === foodId)
|
||||
if (existingIndex >= 0) {
|
||||
const updated = [...formik.values.items]
|
||||
updated[existingIndex] = {
|
||||
...updated[existingIndex],
|
||||
quantity: updated[existingIndex].quantity + 1,
|
||||
}
|
||||
formik.setFieldValue('items', updated)
|
||||
return
|
||||
}
|
||||
formik.setFieldValue('items', [...formik.values.items, { foodId, quantity: 1 }])
|
||||
}
|
||||
|
||||
const updateItemQuantity = (foodId: string, quantity: number) => {
|
||||
if (quantity < 1) {
|
||||
formik.setFieldValue(
|
||||
'items',
|
||||
formik.values.items.filter((item) => item.foodId !== foodId)
|
||||
)
|
||||
return
|
||||
}
|
||||
formik.setFieldValue(
|
||||
'items',
|
||||
formik.values.items.map((item) =>
|
||||
item.foodId === foodId ? { ...item, quantity } : item
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const removeItem = (foodId: string) => {
|
||||
formik.setFieldValue(
|
||||
'items',
|
||||
formik.values.items.filter((item) => item.foodId !== foodId)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
formik,
|
||||
foods,
|
||||
deliveryMethods,
|
||||
selectedDeliveryMethod,
|
||||
selectedPaymentMethod,
|
||||
cashPaymentMethod,
|
||||
isCourierDelivery,
|
||||
isCarDelivery,
|
||||
isDineIn,
|
||||
orderSummary,
|
||||
addFoodToOrder,
|
||||
updateItemQuantity,
|
||||
removeItem,
|
||||
isSubmitting: isCreating,
|
||||
}
|
||||
}
|
||||
|
||||
export type { RestaurantShipmentMethod, RestaurantPaymentMethod }
|
||||
@@ -2,6 +2,16 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as api from "../service/OrderService";
|
||||
import type { GetOrdersParams, ChangeOrderStatusParams, RefundPaymentParams } from "../service/OrderService";
|
||||
|
||||
export const useCreateOrder = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: api.createOrder,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrders = (params?: GetOrdersParams) => {
|
||||
return useQuery({
|
||||
queryKey: ["orders", params],
|
||||
|
||||
@@ -4,6 +4,8 @@ import type {
|
||||
GetOrderByIdResponse,
|
||||
GetFoodOrderReportResponse,
|
||||
GetDailyOrderReportResponse,
|
||||
CreateOrderType,
|
||||
CreateOrderResponse,
|
||||
} from "../types/Types";
|
||||
|
||||
export interface GetOrdersParams {
|
||||
@@ -22,6 +24,16 @@ export interface RefundPaymentParams {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const createOrder = async (
|
||||
params: CreateOrderType
|
||||
): Promise<CreateOrderResponse> => {
|
||||
const { data } = await axios.post<CreateOrderResponse>(
|
||||
"/admin/orders",
|
||||
params
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getOrders = async (
|
||||
params?: GetOrdersParams
|
||||
): Promise<GetOrdersResponse> => {
|
||||
|
||||
@@ -242,3 +242,43 @@ export interface DailyOrderReportItem extends RowDataType {
|
||||
}
|
||||
|
||||
export type GetDailyOrderReportResponse = IResponse<DailyOrderReportItem[]>;
|
||||
|
||||
export interface CreateOrderItem {
|
||||
foodId: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface CreateOrderUserAddress {
|
||||
fullName: string;
|
||||
phone: string;
|
||||
city: string;
|
||||
province: string;
|
||||
address: string;
|
||||
postalCode: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface CreateOrderCarAddress {
|
||||
carModel: string;
|
||||
carColor: string;
|
||||
plateNumber: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface CreateOrderType {
|
||||
userPhone: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
items: CreateOrderItem[];
|
||||
deliveryMethodId: string;
|
||||
paymentMethodId: string;
|
||||
description?: string;
|
||||
tableNumber?: string;
|
||||
userAddress?: CreateOrderUserAddress;
|
||||
carAddress?: CreateOrderCarAddress;
|
||||
paymentDesc?: string;
|
||||
paymentAttachments?: string[];
|
||||
}
|
||||
|
||||
export type CreateOrderResponse = IResponse<Order>;
|
||||
|
||||
@@ -14,6 +14,7 @@ import CustomersList from '@/pages/customers/List'
|
||||
import CustomerDetails from '@/pages/customers/Details'
|
||||
import CreateCustomer from '@/pages/customers/Create'
|
||||
import OrdersList from '@/pages/orders/List'
|
||||
import CreateOrder from '@/pages/orders/Create'
|
||||
import OrderDetails from '@/pages/orders/Details'
|
||||
import FoodOrderReport from '@/pages/orders/FoodOrderReport'
|
||||
import DailyOrderReport from '@/pages/orders/DailyOrderReport'
|
||||
@@ -73,6 +74,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} />
|
||||
<Route path={Pages.customers.add} element={<CreateCustomer />} />
|
||||
<Route path={Pages.orders.list} element={<OrdersList />} />
|
||||
<Route path={Pages.orders.add} element={<CreateOrder />} />
|
||||
<Route path={Pages.orders.foodReport} element={<FoodOrderReport />} />
|
||||
<Route path={Pages.orders.dailyReport} element={<DailyOrderReport />} />
|
||||
<Route path={Pages.orders.detail + ':id'} element={<OrderDetails />} />
|
||||
|
||||
@@ -43,11 +43,11 @@ const OrdersSubMenu: FC = () => {
|
||||
isActive={isActive('report/by-day')}
|
||||
link={Pages.orders.dailyReport}
|
||||
/>
|
||||
{/* <SubMenuItem
|
||||
<SubMenuItem
|
||||
title={t('submenu.add_order')}
|
||||
isActive={isActive('add')}
|
||||
link={Pages.orders.add}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user