list foods
This commit is contained in:
+39
-83
@@ -1,10 +1,31 @@
|
||||
|
||||
import { type FC } from 'react'
|
||||
import Table from '@/components/Table'
|
||||
import { Add, Eye } from 'iconsax-react'
|
||||
import Filters from '@/components/Filters'
|
||||
import { Add } from 'iconsax-react'
|
||||
import Button from '@/components/Button'
|
||||
import { useGetFoods } from './hooks/useFoodData'
|
||||
import { useFoodFilters } from './hooks/useFoodFilters'
|
||||
import { getFoodTableColumns } from './components/FoodTableColumns'
|
||||
import { useFoodFiltersFields } from './components/FoodFiltersFields'
|
||||
|
||||
const FoodsList: FC = () => {
|
||||
const {
|
||||
filters,
|
||||
currentPage,
|
||||
apiParams,
|
||||
handleFiltersChange,
|
||||
handlePageChange,
|
||||
limit,
|
||||
} = useFoodFilters()
|
||||
|
||||
const { data: foodsData, isLoading } = useGetFoods(apiParams)
|
||||
|
||||
const foods = foodsData?.data || []
|
||||
const columns = getFoodTableColumns()
|
||||
const filterFields = useFoodFiltersFields()
|
||||
|
||||
const totalPages = Math.ceil(foods.length / limit) || 1
|
||||
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
@@ -18,89 +39,24 @@ const FoodsList: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
key: 'image',
|
||||
title: 'تصویر',
|
||||
render: (item) => {
|
||||
return (
|
||||
<div className='w-10 h-10 rounded-full overflow-hidden'>
|
||||
<img src={item.image} alt={item.name} className='w-full h-full object-cover' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: 'نام غذا',
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
title: 'دسته بندی',
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
title: 'قیمت ',
|
||||
},
|
||||
{
|
||||
key: 'preparationTime',
|
||||
title: 'زمان آماده سازی',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: 'وضعیت',
|
||||
render: (item) => {
|
||||
return (
|
||||
<div className='h-6 w-fit flex items-center bg-[#FFEDCA] text-[#FF7B00] rounded-full px-2.5 text-xs'>
|
||||
{item.status}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'view',
|
||||
title: '',
|
||||
render: () => {
|
||||
return (
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
{
|
||||
id: 1,
|
||||
image: 'https://picsum.photos/seed/food1/200',
|
||||
name: 'کباب کوبیده',
|
||||
category: 'غذای ایرانی',
|
||||
price: '۱۵۰,۰۰۰ تومان',
|
||||
preparationTime: '۳۰ دقیقه',
|
||||
status: 'موجود',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: 'https://picsum.photos/seed/food2/200',
|
||||
name: 'پیتزا پپرونی',
|
||||
category: 'فست فود',
|
||||
price: '۲۰۰,۰۰۰ تومان',
|
||||
preparationTime: '۴۵ دقیقه',
|
||||
status: 'موجود',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: 'https://picsum.photos/seed/food3/200',
|
||||
name: 'سالاد سزار',
|
||||
category: 'پیش غذا',
|
||||
price: '۸۰,۰۰۰ تومان',
|
||||
preparationTime: '۱۵ دقیقه',
|
||||
status: 'ناموجود',
|
||||
}
|
||||
]}
|
||||
<Filters
|
||||
fields={filterFields}
|
||||
onChange={handleFiltersChange}
|
||||
initialValues={filters}
|
||||
searchField="search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
data={foods}
|
||||
isLoading={isLoading}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange: handlePageChange,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { FieldType } from '@/components/Filters'
|
||||
|
||||
export const useFoodFiltersFields = (): FieldType[] => {
|
||||
return useMemo(() => [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'search',
|
||||
placeholder: 'جستجو',
|
||||
},
|
||||
], [])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Eye } from 'iconsax-react'
|
||||
import type { ColumnType } from '@/components/types/TableTypes'
|
||||
import type { Food } from '../types/Types'
|
||||
import { formatPrice, formatTime } from '../utils/formatters'
|
||||
|
||||
export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
||||
return [
|
||||
{
|
||||
key: 'image',
|
||||
title: 'تصویر',
|
||||
render: (item: Food) => {
|
||||
const imageUrl = item.images && item.images.length > 0 ? item.images[0] : ''
|
||||
return (
|
||||
<div className='w-10 h-10 rounded-full overflow-hidden bg-gray-200'>
|
||||
{imageUrl ? (
|
||||
<img src={imageUrl} alt={item.title} className='w-full h-full object-cover' />
|
||||
) : (
|
||||
<div className='w-full h-full flex items-center justify-center text-gray-400 text-xs'>
|
||||
بدون تصویر
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
title: 'نام غذا',
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
title: 'دسته بندی',
|
||||
render: (item: Food) => {
|
||||
return item.categories && item.categories.length > 0
|
||||
? item.categories.map(cat => cat.title).join(', ')
|
||||
: '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
title: 'قیمت',
|
||||
render: (item: Food) => {
|
||||
return formatPrice(item.price)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'prepareTime',
|
||||
title: 'زمان آماده سازی',
|
||||
render: (item: Food) => {
|
||||
return formatTime(item.prepareTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
title: 'وضعیت',
|
||||
render: (item: Food) => {
|
||||
return (
|
||||
<div className={`h-6 w-fit flex items-center rounded-full px-2.5 text-xs ${
|
||||
item.isActive
|
||||
? 'bg-[#FFEDCA] text-[#FF7B00]'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{item.isActive ? 'فعال' : 'غیرفعال'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'view',
|
||||
title: '',
|
||||
render: () => {
|
||||
return (
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/FoodService";
|
||||
import type { GetFoodsParams } from "../service/FoodService";
|
||||
|
||||
export const useCreateFood = () => {
|
||||
return useMutation({
|
||||
@@ -7,6 +8,13 @@ export const useCreateFood = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetFoods = (params?: GetFoodsParams) => {
|
||||
return useQuery({
|
||||
queryKey: ["foods", params],
|
||||
queryFn: () => api.getFoods(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetCategories = () => {
|
||||
return useQuery({
|
||||
queryKey: ["categories"],
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import type { FilterValues } from '@/components/Filters'
|
||||
import type { GetFoodsParams } from '../service/FoodService'
|
||||
|
||||
const DEFAULT_PAGE = 1
|
||||
const DEFAULT_LIMIT = 10
|
||||
|
||||
export const useFoodFilters = () => {
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE)
|
||||
const [limit] = useState(DEFAULT_LIMIT)
|
||||
|
||||
const apiParams = useMemo<GetFoodsParams>(() => {
|
||||
const params: GetFoodsParams = {
|
||||
page: currentPage,
|
||||
limit: limit,
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
params.search = filters.search
|
||||
}
|
||||
|
||||
return params
|
||||
}, [filters, currentPage, limit])
|
||||
|
||||
const handleFiltersChange = (newFilters: FilterValues) => {
|
||||
setFilters(newFilters)
|
||||
setCurrentPage(DEFAULT_PAGE)
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
currentPage,
|
||||
apiParams,
|
||||
handleFiltersChange,
|
||||
handlePageChange,
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
import axios from "@/config/axios";
|
||||
import type { CreateFoodType, GetCategoriesResponseType } from "../types/Types";
|
||||
import type {
|
||||
CreateFoodType,
|
||||
GetCategoriesResponseType,
|
||||
GetFoodsResponseType,
|
||||
} from "../types/Types";
|
||||
|
||||
export const createFood = async (params: CreateFoodType) => {
|
||||
const { data } = await axios.post(`/foods`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export type GetFoodsParams = {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
orderBy?: string;
|
||||
order?: "asc" | "desc";
|
||||
categoryId?: string;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const getFoods = async (
|
||||
params?: GetFoodsParams
|
||||
): Promise<GetFoodsResponseType> => {
|
||||
const { data } = await axios.get<GetFoodsResponseType>(`/foods`, { params });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCategories = async (): Promise<GetCategoriesResponseType> => {
|
||||
const { data } = await axios.get<GetCategoriesResponseType>(`/categories`);
|
||||
return data;
|
||||
|
||||
@@ -29,13 +29,43 @@ export type CreateFoodType = {
|
||||
|
||||
export type Category = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
title: string;
|
||||
isActive: boolean;
|
||||
restId: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
|
||||
export type Food = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
foods: unknown[];
|
||||
deletedAt: string | null;
|
||||
title: string;
|
||||
desc: string | null;
|
||||
content: string[];
|
||||
price: number;
|
||||
points: number;
|
||||
order: number | null;
|
||||
prepareTime: number;
|
||||
sat: boolean;
|
||||
sun: boolean;
|
||||
mon: boolean;
|
||||
breakfast: boolean;
|
||||
noon: boolean;
|
||||
dinner: boolean;
|
||||
stock: number;
|
||||
stockDefault: number;
|
||||
isActive: boolean;
|
||||
images: string[];
|
||||
inPlaceServe: boolean;
|
||||
pickupServe: boolean;
|
||||
rate: number;
|
||||
discount: number;
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
export type GetCategoriesResponseType = IResponse<Category[]>;
|
||||
export type GetFoodsResponseType = IResponse<Food[]>;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const formatPrice = (price: number): string => {
|
||||
return new Intl.NumberFormat('fa-IR', {
|
||||
style: 'currency',
|
||||
currency: 'IRR',
|
||||
minimumFractionDigits: 0,
|
||||
}).format(price)
|
||||
}
|
||||
|
||||
export const formatTime = (minutes: number): string => {
|
||||
return `${minutes} دقیقه`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user