update food
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { type FC, Fragment } from 'react'
|
||||
import Td from './Td'
|
||||
import Skeleton from 'react-loading-skeleton'
|
||||
|
||||
type Props = {
|
||||
@@ -15,9 +14,9 @@ const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
|
||||
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
|
||||
{
|
||||
Array.from({ length: tdCount }).map((_, colIndex) => (
|
||||
<Td text={''} key={colIndex}>
|
||||
<Skeleton />
|
||||
</Td>
|
||||
<td key={colIndex} className="px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs">
|
||||
<Skeleton height={20} width="100%" />
|
||||
</td>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useState, memo } from 'react';
|
||||
import React, { useState, memo } from 'react';
|
||||
import DefaultTableSkeleton from '../components/DefaultTableSkeleton';
|
||||
import Td from '../components/Td';
|
||||
import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes';
|
||||
@@ -147,9 +147,7 @@ const Table = <T extends RowDataType>({
|
||||
const renderTableBody = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Fragment>
|
||||
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
||||
</Fragment>
|
||||
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -321,43 +319,4 @@ const Table = <T extends RowDataType>({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Table) as <T extends RowDataType>(props: TableProps<T>) => React.ReactElement;
|
||||
|
||||
/*
|
||||
مثال استفاده با pagination:
|
||||
|
||||
import Table from '@/components/Table';
|
||||
import { useState } from 'react';
|
||||
|
||||
const MyComponent = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 10;
|
||||
|
||||
const columns = [
|
||||
{ title: 'نام', key: 'name' },
|
||||
{ title: 'ایمیل', key: 'email' },
|
||||
];
|
||||
|
||||
const data = [
|
||||
{ id: 1, name: 'علی', email: 'ali@example.com' },
|
||||
{ id: 2, name: 'فاطمه', email: 'fateme@example.com' },
|
||||
];
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
// اینجا میتوانید API call جدید برای دریافت دادههای صفحه جدید بزنید
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange: handlePageChange
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
*/
|
||||
export default memo(Table) as <T extends RowDataType>(props: TableProps<T>) => React.ReactElement;
|
||||
@@ -12,7 +12,8 @@ type Props = {
|
||||
preview?: string[],
|
||||
onChangePreview?: (preview: string[]) => void,
|
||||
getCover?: (url: string) => void,
|
||||
coverUrl?: string
|
||||
coverUrl?: string,
|
||||
imageSize?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
@@ -92,9 +93,17 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
|
||||
{
|
||||
files.length > 0 || perviews ? (
|
||||
<div className='mt-4 flex gap-4 items-center'>
|
||||
<div className={clx(
|
||||
'mt-4',
|
||||
props.imageSize === 'large' ? 'grid grid-cols-2 gap-4' : 'flex gap-4 items-center flex-wrap'
|
||||
)}>
|
||||
{
|
||||
perviews && perviews.map((item, index) => {
|
||||
const imageSizeClass = props.imageSize === 'large'
|
||||
? 'w-full h-32 rounded-lg'
|
||||
: props.imageSize === 'medium'
|
||||
? 'size-20 rounded-lg'
|
||||
: 'size-10 rounded-full'
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div key={index} className='flex relative items-center gap-2'>
|
||||
@@ -105,10 +114,11 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
}
|
||||
}} src={item}
|
||||
className={clx(
|
||||
'size-10 rounded-full object-cover',
|
||||
imageSizeClass,
|
||||
'object-cover cursor-pointer',
|
||||
cover === item ? 'border-2 border-red-400' : ''
|
||||
)} />
|
||||
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center cursor-pointer z-10'>
|
||||
<CloseCircle className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
@@ -123,24 +133,30 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
<div key={index} className='flex border p-2 rounded-lg items-center gap-2'>
|
||||
<div className='flex relative items-center gap-2'>
|
||||
<div className='text-xs'>{file.name}</div>
|
||||
<div className='absolute -left-4 -top-4 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<div className='absolute -left-4 -top-4 shadow-md bg-white size-5 rounded-full flex justify-center items-center cursor-pointer'>
|
||||
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else
|
||||
else {
|
||||
const imageSizeClass = props.imageSize === 'large'
|
||||
? 'w-full h-32 rounded-lg'
|
||||
: props.imageSize === 'medium'
|
||||
? 'size-20 rounded-lg'
|
||||
: 'size-10 rounded-full'
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div key={index} className='flex relative items-center gap-2'>
|
||||
<img src={URL.createObjectURL(file)} alt={file.name} className='size-10 rounded-full object-cover' />
|
||||
<div className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<img src={URL.createObjectURL(file)} alt={file.name} className={clx(imageSizeClass, 'object-cover')} />
|
||||
<div className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center cursor-pointer z-10'>
|
||||
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -52,3 +52,23 @@ export const timeAgo = (date: string | Date): string => {
|
||||
|
||||
return "همین الان";
|
||||
};
|
||||
|
||||
export interface IGeneralError {
|
||||
status: number;
|
||||
success: boolean;
|
||||
error: {
|
||||
message: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export const extractErrorMessage = (
|
||||
error: Error | unknown,
|
||||
fallbackMessage: string = "خطایی رخ داده است"
|
||||
): string => {
|
||||
try {
|
||||
const axiosError = error as { response?: { data: IGeneralError } };
|
||||
return axiosError?.response?.data?.error?.message?.[0] || fallbackMessage;
|
||||
} catch {
|
||||
return fallbackMessage;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
html {
|
||||
direction: rtl;
|
||||
|
||||
@@ -75,7 +75,8 @@ const CreateFood: FC = () => {
|
||||
const foodData: CreateFoodType = {
|
||||
...values,
|
||||
isActive,
|
||||
images: imageUrls
|
||||
images: imageUrls,
|
||||
content: values.content.filter(item => item && item.trim().length > 0)
|
||||
}
|
||||
|
||||
createFood(foodData, {
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import Button from '@/components/Button'
|
||||
import CreateFoodSidebar from './components/CreateFoodSidebar'
|
||||
import BasicInfoSection from './components/BasicInfoSection'
|
||||
import FoodContentSection from './components/FoodContentSection'
|
||||
import MealTimesSection from './components/MealTimesSection'
|
||||
import WeekDaysSection from './components/WeekDaysSection'
|
||||
import ServiceOptionsSection from './components/ServiceOptionsSection'
|
||||
import type { CreateFoodType } from './types/Types'
|
||||
import { useUpdateFood, useGetFoodDetails, useGetCategories } from './hooks/useFoodData'
|
||||
import { useMultipleUpload } from '../uploader/hooks/useUploaderData'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import type { ErrorType } from '@/helpers/types'
|
||||
import { extractErrorMessage } from '@/config/func'
|
||||
|
||||
const UpdateFood: FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const { data: foodData, isLoading } = useGetFoodDetails(id || '')
|
||||
const { data: categoriesData } = useGetCategories()
|
||||
const [isActive, setIsActive] = useState<boolean>(true)
|
||||
const [isSpecial, setIsSpecial] = useState<boolean>(false)
|
||||
const [imageFiles, setImageFiles] = useState<File[]>([])
|
||||
const [existingImages, setExistingImages] = useState<string[]>([])
|
||||
const { mutate: updateFood } = useUpdateFood()
|
||||
const { mutate: multipleUpload } = useMultipleUpload()
|
||||
|
||||
const categories = categoriesData?.data?.map(category => ({
|
||||
label: category.title,
|
||||
value: category.id
|
||||
})) || []
|
||||
|
||||
const food = foodData?.data
|
||||
|
||||
const formik = useFormik<CreateFoodType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
desc: '',
|
||||
content: [],
|
||||
categoryIds: [],
|
||||
price: 0,
|
||||
discount: 0,
|
||||
points: 0,
|
||||
prepareTime: 0,
|
||||
stock: 0,
|
||||
stockDefault: 0,
|
||||
breakfast: false,
|
||||
noon: false,
|
||||
dinner: false,
|
||||
mon: false,
|
||||
tue: false,
|
||||
wed: false,
|
||||
thu: false,
|
||||
fri: false,
|
||||
sat: false,
|
||||
sun: false,
|
||||
pickupServe: false,
|
||||
inPlaceServe: false,
|
||||
isActive: true,
|
||||
images: []
|
||||
},
|
||||
validationSchema: Yup.object().shape({
|
||||
title: Yup.string().required('نام غذا الزامی است'),
|
||||
desc: Yup.string().required('توضیحات غذا الزامی است'),
|
||||
categoryIds: Yup.array().min(1, 'حداقل یک دستهبندی الزامی است'),
|
||||
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
||||
prepareTime: Yup.number().required('زمان آمادهسازی الزامی است').min(0, 'زمان آمادهسازی باید مثبت باشد'),
|
||||
stock: Yup.number().required('موجودی الزامی است').min(0, 'موجودی باید مثبت باشد'),
|
||||
stockDefault: Yup.number().required('موجودی پیشفرض الزامی است').min(0, 'موجودی پیشفرض باید مثبت باشد'),
|
||||
points: Yup.number().min(0, 'امتیاز باید مثبت باشد'),
|
||||
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'),
|
||||
content: Yup.array().of(Yup.string())
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (!id) {
|
||||
toast.error('شناسه غذا یافت نشد')
|
||||
return
|
||||
}
|
||||
|
||||
const submitFood = (newImageUrls: string[] = []) => {
|
||||
const allImages = [...values.images, ...newImageUrls]
|
||||
const foodData: CreateFoodType = {
|
||||
...values,
|
||||
isActive,
|
||||
images: allImages,
|
||||
content: values.content.filter(item => item && item.trim().length > 0)
|
||||
}
|
||||
|
||||
updateFood({ id, params: foodData }, {
|
||||
onSuccess: () => {
|
||||
toast.success('غذا با موفقیت بهروزرسانی شد')
|
||||
window.location.href = Pages.foods.list
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
multipleUpload(imageFiles, {
|
||||
onSuccess: (response) => {
|
||||
const imageUrls = response?.data?.map(item => item.url) || []
|
||||
submitFood(imageUrls)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error?.response?.data?.error?.message[0] || 'خطا در آپلود تصاویر')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
submitFood()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (food) {
|
||||
formik.setValues({
|
||||
title: food.title || '',
|
||||
desc: food.desc || '',
|
||||
content: food.content || [],
|
||||
categoryIds: food.categories?.map(cat => cat.id) || [],
|
||||
price: food.price || 0,
|
||||
discount: food.discount || 0,
|
||||
points: food.points || 0,
|
||||
prepareTime: food.prepareTime || 0,
|
||||
stock: food.stock || 0,
|
||||
stockDefault: food.stockDefault || 0,
|
||||
breakfast: food.breakfast || false,
|
||||
noon: food.noon || false,
|
||||
dinner: food.dinner || false,
|
||||
mon: food.mon || false,
|
||||
tue: food.tue || false,
|
||||
wed: food.wed || false,
|
||||
thu: food.thu || false,
|
||||
fri: food.fri || false,
|
||||
sat: food.sat || false,
|
||||
sun: food.sun || false,
|
||||
pickupServe: food.pickupServe || false,
|
||||
inPlaceServe: food.inPlaceServe || false,
|
||||
isActive: food.isActive || false,
|
||||
images: food.images || []
|
||||
})
|
||||
setIsActive(food.isActive || false)
|
||||
setExistingImages(food.images || [])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [food])
|
||||
|
||||
useEffect(() => {
|
||||
formik.setFieldValue('images', existingImages)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [existingImages])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='w-full mt-4 flex justify-center items-center'>
|
||||
<div>در حال بارگذاری...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!food) {
|
||||
return (
|
||||
<div className='w-full mt-4 flex justify-center items-center'>
|
||||
<div>غذا یافت نشد</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div className='text-lg font-light'>
|
||||
ویرایش غذا
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className='px-5'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle className='size-5' color='white' />
|
||||
<div>ذخیره تغییرات</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 mt-6'>
|
||||
<div className='flex-1'>
|
||||
<div className='bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||
<BasicInfoSection formik={formik} categories={categories} />
|
||||
<FoodContentSection formik={formik} />
|
||||
<MealTimesSection formik={formik} />
|
||||
<WeekDaysSection formik={formik} />
|
||||
<ServiceOptionsSection formik={formik} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateFoodSidebar
|
||||
isActive={isActive}
|
||||
isSpecial={isSpecial}
|
||||
onChangeActive={setIsActive}
|
||||
onChangeSpecial={setIsSpecial}
|
||||
onChangeImages={setImageFiles}
|
||||
existingImages={existingImages}
|
||||
onChangeExistingImages={setExistingImages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateFood
|
||||
@@ -0,0 +1,121 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
|
||||
type BasicInfoSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
categories: Array<{ label: string; value: string }>
|
||||
}
|
||||
|
||||
const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) => {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label='نام غذا'
|
||||
name='title'
|
||||
placeholder=''
|
||||
value={formik.values.title}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Select
|
||||
items={categories}
|
||||
label='دسته بندی'
|
||||
placeholder='انتخاب کنید'
|
||||
name='categoryIds'
|
||||
value={formik.values.categoryIds[0] || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
formik.setFieldValue('categoryIds', value ? [value] : [])
|
||||
}}
|
||||
error_text={formik.touched.categoryIds && formik.errors.categoryIds ? String(formik.errors.categoryIds) : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 gap-4 mt-5'>
|
||||
<Input
|
||||
name='price'
|
||||
label='قیمت'
|
||||
placeholder='تومان'
|
||||
type='number'
|
||||
seprator={true}
|
||||
value={formik.values.price}
|
||||
onChange={(e) => formik.setFieldValue('price', Number(e.target.value))}
|
||||
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name='discount'
|
||||
label='تخفیف'
|
||||
placeholder='تومان'
|
||||
type='number'
|
||||
seprator={true}
|
||||
value={formik.values.discount}
|
||||
onChange={(e) => formik.setFieldValue('discount', Number(e.target.value))}
|
||||
error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name='points'
|
||||
label='امتیاز'
|
||||
placeholder=''
|
||||
type='number'
|
||||
value={formik.values.points}
|
||||
onChange={(e) => formik.setFieldValue('points', Number(e.target.value))}
|
||||
error_text={formik.touched.points && formik.errors.points ? formik.errors.points : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 gap-4 mt-5'>
|
||||
<Input
|
||||
name='prepareTime'
|
||||
label='زمان آماده سازی'
|
||||
placeholder='دقیقه'
|
||||
type='number'
|
||||
value={formik.values.prepareTime}
|
||||
onChange={(e) => formik.setFieldValue('prepareTime', Number(e.target.value))}
|
||||
error_text={formik.touched.prepareTime && formik.errors.prepareTime ? formik.errors.prepareTime : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name='stock'
|
||||
label='موجودی'
|
||||
placeholder=''
|
||||
type='number'
|
||||
value={formik.values.stock}
|
||||
onChange={(e) => formik.setFieldValue('stock', Number(e.target.value))}
|
||||
error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
name='stockDefault'
|
||||
label='موجودی پیشفرض'
|
||||
placeholder=''
|
||||
type='number'
|
||||
value={formik.values.stockDefault}
|
||||
onChange={(e) => formik.setFieldValue('stockDefault', Number(e.target.value))}
|
||||
error_text={formik.touched.stockDefault && formik.errors.stockDefault ? formik.errors.stockDefault : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-5'>
|
||||
<Textarea
|
||||
name='desc'
|
||||
label='توضیحات غذا'
|
||||
placeholder=''
|
||||
value={formik.values.desc}
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.desc && formik.errors.desc ? formik.errors.desc : ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BasicInfoSection
|
||||
|
||||
@@ -9,6 +9,8 @@ type Props = {
|
||||
onChangeActive: (value: boolean) => void
|
||||
onChangeSpecial: (value: boolean) => void
|
||||
onChangeImages: (files: File[]) => void
|
||||
existingImages?: string[]
|
||||
onChangeExistingImages?: (images: string[]) => void
|
||||
}
|
||||
|
||||
const CreateFoodSidebar: FC<Props> = (props: Props) => {
|
||||
@@ -43,6 +45,9 @@ const CreateFoodSidebar: FC<Props> = (props: Props) => {
|
||||
label='تصویر غذا را آپلود کنید'
|
||||
isMultiple={true}
|
||||
onChange={(files: File[]) => props.onChangeImages(files)}
|
||||
preview={props.existingImages}
|
||||
onChangePreview={props.onChangeExistingImages}
|
||||
imageSize='medium'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import Input from '@/components/Input'
|
||||
import Button from '@/components/Button'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
|
||||
type FoodContentSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
}
|
||||
|
||||
const FoodContentSection: FC<FoodContentSectionProps> = ({ formik }) => {
|
||||
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='text-sm mb-2'>محتوای غذا</div>
|
||||
<div className='space-y-2'>
|
||||
{formik.values.content.map((item, index) => (
|
||||
<div key={index} className='flex gap-2 items-center'>
|
||||
<Input
|
||||
placeholder='متن محتوا'
|
||||
value={item}
|
||||
onChange={(e) => {
|
||||
const newContent = [...formik.values.content]
|
||||
newContent[index] = e.target.value
|
||||
formik.setFieldValue('content', newContent)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
className='px-3 bg-red-500 hover:bg-red-600 w-auto'
|
||||
onClick={() => {
|
||||
const newContent = formik.values.content.filter((_, i) => i !== index)
|
||||
formik.setFieldValue('content', newContent)
|
||||
}}
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type='button'
|
||||
className='w-full bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
onClick={() => {
|
||||
formik.setFieldValue('content', [...formik.values.content, ''])
|
||||
}}
|
||||
>
|
||||
افزودن محتوا
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FoodContentSection
|
||||
@@ -3,6 +3,8 @@ import type { ColumnType } from '@/components/types/TableTypes'
|
||||
import type { Food } from '../types/Types'
|
||||
import { formatPrice, formatTime } from '../utils/formatters'
|
||||
import Status from '@/components/Status'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '@/config/Pages'
|
||||
|
||||
export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
||||
return [
|
||||
@@ -63,11 +65,11 @@ export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
||||
{
|
||||
key: 'view',
|
||||
title: '',
|
||||
render: () => {
|
||||
render: (item: Food) => {
|
||||
return (
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Link to={Pages.foods.update + item.id} className='flex gap-2 items-center'>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
|
||||
type MealTimesSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
}
|
||||
|
||||
const MealTimesSection: FC<MealTimesSectionProps> = ({ formik }) => {
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm mb-3'>وعدههای غذایی</div>
|
||||
<div className='flex gap-6 flex-wrap'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={formik.values.breakfast}
|
||||
onCheckedChange={(checked) => formik.setFieldValue('breakfast', checked)}
|
||||
/>
|
||||
<label className='text-xs cursor-pointer'>صبحانه</label>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={formik.values.noon}
|
||||
onCheckedChange={(checked) => formik.setFieldValue('noon', checked)}
|
||||
/>
|
||||
<label className='text-xs cursor-pointer'>ناهار</label>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={formik.values.dinner}
|
||||
onCheckedChange={(checked) => formik.setFieldValue('dinner', checked)}
|
||||
/>
|
||||
<label className='text-xs cursor-pointer'>شام</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MealTimesSection
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
|
||||
type ServiceOptionsSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
}
|
||||
|
||||
const ServiceOptionsSection: FC<ServiceOptionsSectionProps> = ({ formik }) => {
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm mb-3'>گزینههای سرویس</div>
|
||||
<div className='flex gap-6 flex-wrap'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={formik.values.pickupServe}
|
||||
onCheckedChange={(checked) => formik.setFieldValue('pickupServe', checked)}
|
||||
/>
|
||||
<label className='text-xs cursor-pointer'> بیرونبر</label>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={formik.values.inPlaceServe}
|
||||
onCheckedChange={(checked) => formik.setFieldValue('inPlaceServe', checked)}
|
||||
/>
|
||||
<label className='text-xs cursor-pointer'>سرو در محل</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceOptionsSection
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type FC } from 'react'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import type { CreateFoodType } from '../types/Types'
|
||||
|
||||
type WeekDaysSectionProps = {
|
||||
formik: FormikProps<CreateFoodType>
|
||||
}
|
||||
|
||||
const WeekDaysSection: FC<WeekDaysSectionProps> = ({ formik }) => {
|
||||
const weekDays = [
|
||||
{ key: 'sat', label: 'شنبه' },
|
||||
{ key: 'sun', label: 'یکشنبه' },
|
||||
{ key: 'mon', label: 'دوشنبه' },
|
||||
{ key: 'tue', label: 'سهشنبه' },
|
||||
{ key: 'wed', label: 'چهارشنبه' },
|
||||
{ key: 'thu', label: 'پنجشنبه' },
|
||||
{ key: 'fri', label: 'جمعه' }
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm mb-3'>روزهای هفته</div>
|
||||
<div className='flex gap-6 flex-wrap'>
|
||||
{weekDays.map((day) => (
|
||||
<div key={day.key} className='flex items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={formik.values[day.key]}
|
||||
onCheckedChange={(checked) => formik.setFieldValue(day.key, checked)}
|
||||
/>
|
||||
<label className='text-xs cursor-pointer'>{day.label}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WeekDaysSection
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/FoodService";
|
||||
import type { GetFoodsParams } from "../types/Types";
|
||||
import type { CreateFoodType, GetFoodsParams } from "../types/Types";
|
||||
|
||||
export const useCreateFood = () => {
|
||||
return useMutation({
|
||||
@@ -15,6 +15,20 @@ export const useGetFoods = (params?: GetFoodsParams) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetFoodDetails = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["food", id],
|
||||
queryFn: () => api.getFoodDetails(id),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateFood = () => {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, params }: { id: string; params: CreateFoodType }) =>
|
||||
api.updateFood(id, params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCategories = () => {
|
||||
return useQuery({
|
||||
queryKey: ["categories"],
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from "@/config/axios";
|
||||
import type {
|
||||
CreateFoodType,
|
||||
GetCategoriesResponseType,
|
||||
GetFoodDetailsResponseType,
|
||||
GetFoodsParams,
|
||||
GetFoodsResponseType,
|
||||
} from "../types/Types";
|
||||
@@ -18,6 +19,18 @@ export const getFoods = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getFoodDetails = async (
|
||||
id: string
|
||||
): Promise<GetFoodDetailsResponseType> => {
|
||||
const { data } = await axios.get<GetFoodDetailsResponseType>(`/foods/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateFood = async (id: string, params: CreateFoodType) => {
|
||||
const { data } = await axios.patch(`/foods/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCategories = async (): Promise<GetCategoriesResponseType> => {
|
||||
const { data } = await axios.get<GetCategoriesResponseType>(`/categories`);
|
||||
return data;
|
||||
|
||||
@@ -53,6 +53,10 @@ export type Food = {
|
||||
sat: boolean;
|
||||
sun: boolean;
|
||||
mon: boolean;
|
||||
tue: boolean;
|
||||
wed: boolean;
|
||||
thu: boolean;
|
||||
fri: boolean;
|
||||
breakfast: boolean;
|
||||
noon: boolean;
|
||||
dinner: boolean;
|
||||
@@ -79,3 +83,4 @@ export type GetFoodsParams = {
|
||||
|
||||
export type GetCategoriesResponseType = IResponse<Category[]>;
|
||||
export type GetFoodsResponseType = IResponse<Food[]>;
|
||||
export type GetFoodDetailsResponseType = IResponse<Food>;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSharedStore } from '../shared/store/sharedStore'
|
||||
import FoodsList from '@/pages/food/List'
|
||||
import { Pages } from '@/config/Pages'
|
||||
import CreateFood from '@/pages/food/Create'
|
||||
import UpdateFood from '@/pages/food/Update'
|
||||
import CategoryFood from '@/pages/food/Category'
|
||||
import CustomersList from '@/pages/customers/List'
|
||||
import CustomerDetails from '@/pages/customers/Details'
|
||||
@@ -35,6 +36,7 @@ const MainRouter: FC = () => {
|
||||
<Routes>
|
||||
<Route path={Pages.foods.list} element={<FoodsList />} />
|
||||
<Route path={Pages.foods.add} element={<CreateFood />} />
|
||||
<Route path={Pages.foods.update + ':id'} element={<UpdateFood />} />
|
||||
<Route path={Pages.foods.category} element={<CategoryFood />} />
|
||||
<Route path={Pages.customers.list} element={<CustomersList />} />
|
||||
<Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} />
|
||||
|
||||
Reference in New Issue
Block a user