update food
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { type FC, Fragment } from 'react'
|
import { type FC, Fragment } from 'react'
|
||||||
import Td from './Td'
|
|
||||||
import Skeleton from 'react-loading-skeleton'
|
import Skeleton from 'react-loading-skeleton'
|
||||||
|
|
||||||
type Props = {
|
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}>
|
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
|
||||||
{
|
{
|
||||||
Array.from({ length: tdCount }).map((_, colIndex) => (
|
Array.from({ length: tdCount }).map((_, colIndex) => (
|
||||||
<Td text={''} key={colIndex}>
|
<td key={colIndex} className="px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs">
|
||||||
<Skeleton />
|
<Skeleton height={20} width="100%" />
|
||||||
</Td>
|
</td>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment, useState, memo } from 'react';
|
import React, { useState, memo } from 'react';
|
||||||
import DefaultTableSkeleton from '../components/DefaultTableSkeleton';
|
import DefaultTableSkeleton from '../components/DefaultTableSkeleton';
|
||||||
import Td from '../components/Td';
|
import Td from '../components/Td';
|
||||||
import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes';
|
import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes';
|
||||||
@@ -147,9 +147,7 @@ const Table = <T extends RowDataType>({
|
|||||||
const renderTableBody = () => {
|
const renderTableBody = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
|
||||||
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,42 +320,3 @@ const Table = <T extends RowDataType>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(Table) as <T extends RowDataType>(props: TableProps<T>) => React.ReactElement;
|
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
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
@@ -12,7 +12,8 @@ type Props = {
|
|||||||
preview?: string[],
|
preview?: string[],
|
||||||
onChangePreview?: (preview: string[]) => void,
|
onChangePreview?: (preview: string[]) => void,
|
||||||
getCover?: (url: string) => void,
|
getCover?: (url: string) => void,
|
||||||
coverUrl?: string
|
coverUrl?: string,
|
||||||
|
imageSize?: 'small' | 'medium' | 'large'
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||||
@@ -92,9 +93,17 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
files.length > 0 || perviews ? (
|
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) => {
|
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 (
|
return (
|
||||||
<div key={index} className='flex items-center gap-2'>
|
<div key={index} className='flex items-center gap-2'>
|
||||||
<div key={index} className='flex relative 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}
|
}} src={item}
|
||||||
className={clx(
|
className={clx(
|
||||||
'size-10 rounded-full object-cover',
|
imageSizeClass,
|
||||||
|
'object-cover cursor-pointer',
|
||||||
cover === item ? 'border-2 border-red-400' : ''
|
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' />
|
<CloseCircle className='size-4 ' color='red' />
|
||||||
</div>
|
</div>
|
||||||
</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 key={index} className='flex border p-2 rounded-lg items-center gap-2'>
|
||||||
<div className='flex relative items-center gap-2'>
|
<div className='flex relative items-center gap-2'>
|
||||||
<div className='text-xs'>{file.name}</div>
|
<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' />
|
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div key={index} className='flex items-center gap-2'>
|
<div key={index} className='flex items-center gap-2'>
|
||||||
<div key={index} className='flex relative 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' />
|
<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'>
|
<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' />
|
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,3 +52,23 @@ export const timeAgo = (date: string | Date): string => {
|
|||||||
|
|
||||||
return "همین الان";
|
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 "tailwindcss";
|
||||||
|
@import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
|
||||||
html {
|
html {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ const CreateFood: FC = () => {
|
|||||||
const foodData: CreateFoodType = {
|
const foodData: CreateFoodType = {
|
||||||
...values,
|
...values,
|
||||||
isActive,
|
isActive,
|
||||||
images: imageUrls
|
images: imageUrls,
|
||||||
|
content: values.content.filter(item => item && item.trim().length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
createFood(foodData, {
|
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
|
onChangeActive: (value: boolean) => void
|
||||||
onChangeSpecial: (value: boolean) => void
|
onChangeSpecial: (value: boolean) => void
|
||||||
onChangeImages: (files: File[]) => void
|
onChangeImages: (files: File[]) => void
|
||||||
|
existingImages?: string[]
|
||||||
|
onChangeExistingImages?: (images: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateFoodSidebar: FC<Props> = (props: Props) => {
|
const CreateFoodSidebar: FC<Props> = (props: Props) => {
|
||||||
@@ -43,6 +45,9 @@ const CreateFoodSidebar: FC<Props> = (props: Props) => {
|
|||||||
label='تصویر غذا را آپلود کنید'
|
label='تصویر غذا را آپلود کنید'
|
||||||
isMultiple={true}
|
isMultiple={true}
|
||||||
onChange={(files: File[]) => props.onChangeImages(files)}
|
onChange={(files: File[]) => props.onChangeImages(files)}
|
||||||
|
preview={props.existingImages}
|
||||||
|
onChangePreview={props.onChangeExistingImages}
|
||||||
|
imageSize='medium'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 type { Food } from '../types/Types'
|
||||||
import { formatPrice, formatTime } from '../utils/formatters'
|
import { formatPrice, formatTime } from '../utils/formatters'
|
||||||
import Status from '@/components/Status'
|
import Status from '@/components/Status'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Pages } from '@/config/Pages'
|
||||||
|
|
||||||
export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
||||||
return [
|
return [
|
||||||
@@ -63,11 +65,11 @@ export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
|||||||
{
|
{
|
||||||
key: 'view',
|
key: 'view',
|
||||||
title: '',
|
title: '',
|
||||||
render: () => {
|
render: (item: Food) => {
|
||||||
return (
|
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' />
|
<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 { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import * as api from "../service/FoodService";
|
import * as api from "../service/FoodService";
|
||||||
import type { GetFoodsParams } from "../types/Types";
|
import type { CreateFoodType, GetFoodsParams } from "../types/Types";
|
||||||
|
|
||||||
export const useCreateFood = () => {
|
export const useCreateFood = () => {
|
||||||
return useMutation({
|
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 = () => {
|
export const useGetCategories = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["categories"],
|
queryKey: ["categories"],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios from "@/config/axios";
|
|||||||
import type {
|
import type {
|
||||||
CreateFoodType,
|
CreateFoodType,
|
||||||
GetCategoriesResponseType,
|
GetCategoriesResponseType,
|
||||||
|
GetFoodDetailsResponseType,
|
||||||
GetFoodsParams,
|
GetFoodsParams,
|
||||||
GetFoodsResponseType,
|
GetFoodsResponseType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
@@ -18,6 +19,18 @@ export const getFoods = async (
|
|||||||
return data;
|
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> => {
|
export const getCategories = async (): Promise<GetCategoriesResponseType> => {
|
||||||
const { data } = await axios.get<GetCategoriesResponseType>(`/categories`);
|
const { data } = await axios.get<GetCategoriesResponseType>(`/categories`);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ export type Food = {
|
|||||||
sat: boolean;
|
sat: boolean;
|
||||||
sun: boolean;
|
sun: boolean;
|
||||||
mon: boolean;
|
mon: boolean;
|
||||||
|
tue: boolean;
|
||||||
|
wed: boolean;
|
||||||
|
thu: boolean;
|
||||||
|
fri: boolean;
|
||||||
breakfast: boolean;
|
breakfast: boolean;
|
||||||
noon: boolean;
|
noon: boolean;
|
||||||
dinner: boolean;
|
dinner: boolean;
|
||||||
@@ -79,3 +83,4 @@ export type GetFoodsParams = {
|
|||||||
|
|
||||||
export type GetCategoriesResponseType = IResponse<Category[]>;
|
export type GetCategoriesResponseType = IResponse<Category[]>;
|
||||||
export type GetFoodsResponseType = IResponse<Food[]>;
|
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 FoodsList from '@/pages/food/List'
|
||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
import CreateFood from '@/pages/food/Create'
|
import CreateFood from '@/pages/food/Create'
|
||||||
|
import UpdateFood from '@/pages/food/Update'
|
||||||
import CategoryFood from '@/pages/food/Category'
|
import CategoryFood from '@/pages/food/Category'
|
||||||
import CustomersList from '@/pages/customers/List'
|
import CustomersList from '@/pages/customers/List'
|
||||||
import CustomerDetails from '@/pages/customers/Details'
|
import CustomerDetails from '@/pages/customers/Details'
|
||||||
@@ -35,6 +36,7 @@ const MainRouter: FC = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path={Pages.foods.list} element={<FoodsList />} />
|
<Route path={Pages.foods.list} element={<FoodsList />} />
|
||||||
<Route path={Pages.foods.add} element={<CreateFood />} />
|
<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.foods.category} element={<CategoryFood />} />
|
||||||
<Route path={Pages.customers.list} element={<CustomersList />} />
|
<Route path={Pages.customers.list} element={<CustomersList />} />
|
||||||
<Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} />
|
<Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} />
|
||||||
|
|||||||
Reference in New Issue
Block a user