This commit is contained in:
+79
-10
@@ -1,16 +1,26 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC, useCallback, useState } from 'react'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import Filters from '@/components/Filters'
|
import Filters from '@/components/Filters'
|
||||||
import { Add } from 'iconsax-react'
|
import { Add } from 'iconsax-react'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { useGetFoods, useDeleteFood } from './hooks/useFoodData'
|
import { useGetFoods, useDeleteFood, useSetStockForFood } from './hooks/useFoodData'
|
||||||
import { useFoodFilters } from './hooks/useFoodFilters'
|
import { useFoodFilters } from './hooks/useFoodFilters'
|
||||||
import { getFoodTableColumns } from './components/FoodTableColumns'
|
import { getFoodTableColumns } from './components/FoodTableColumns'
|
||||||
import { useFoodFiltersFields } from './components/FoodFiltersFields'
|
import { useFoodFiltersFields } from './components/FoodFiltersFields'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
|
import type { ActionType } from '@/components/types/TableTypes'
|
||||||
|
import type { Food } from './types/Types'
|
||||||
|
import StockUpdateModal from './components/StockUpdateModal'
|
||||||
|
import ModalConfrim from '@/components/ModalConfrim'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
|
||||||
const FoodsList: FC = () => {
|
const FoodsList: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [stockModalFood, setStockModalFood] = useState<Food | null>(null)
|
||||||
|
const [deleteConfirmFood, setDeleteConfirmFood] = useState<Food | null>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filters,
|
filters,
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -21,18 +31,60 @@ const FoodsList: FC = () => {
|
|||||||
|
|
||||||
const { data: foodsData, isLoading } = useGetFoods(apiParams, filters)
|
const { data: foodsData, isLoading } = useGetFoods(apiParams, filters)
|
||||||
const { mutate: deleteFood, isPending: isDeleting } = useDeleteFood()
|
const { mutate: deleteFood, isPending: isDeleting } = useDeleteFood()
|
||||||
|
const { mutate: setStock, isPending: isUpdatingStock } = useSetStockForFood()
|
||||||
|
|
||||||
|
|
||||||
const foods = foodsData?.data || []
|
const foods = foodsData?.data || []
|
||||||
const columns = getFoodTableColumns({
|
const columns = getFoodTableColumns()
|
||||||
onDelete: (id: string) => deleteFood(id),
|
|
||||||
isDeleting
|
|
||||||
})
|
|
||||||
const filterFields = useFoodFiltersFields()
|
const filterFields = useFoodFiltersFields()
|
||||||
|
|
||||||
const totalPages = foodsData?.meta?.totalPages || 1
|
const totalPages = foodsData?.meta?.totalPages || 1
|
||||||
|
|
||||||
|
const handleRowActions = useCallback((item: Food): ActionType[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'ویرایش',
|
||||||
|
onClick: () => navigate(Pages.foods.update + item.id),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'بروزرسانی موجودی',
|
||||||
|
onClick: () => setStockModalFood(item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'حذف',
|
||||||
|
onClick: () => setDeleteConfirmFood(item),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
|
const handleStockSubmit = (foodId: string, totalStock: number, availableStock: number) => {
|
||||||
|
setStock(
|
||||||
|
{ foodId, params: { totalStock, availableStock } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('موجودی با موفقیت بروزرسانی شد')
|
||||||
|
setStockModalFood(null)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
if (!deleteConfirmFood) return
|
||||||
|
|
||||||
|
deleteFood(deleteConfirmFood.id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('غذا با موفقیت حذف شد')
|
||||||
|
setDeleteConfirmFood(null)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
@@ -63,14 +115,31 @@ const FoodsList: FC = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={foods}
|
data={foods}
|
||||||
isloading={isLoading}
|
isloading={isLoading}
|
||||||
|
rowActions={handleRowActions}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
onPageChange: handlePageChange,
|
onPageChange: handlePageChange,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StockUpdateModal
|
||||||
|
food={stockModalFood}
|
||||||
|
open={!!stockModalFood}
|
||||||
|
onClose={() => setStockModalFood(null)}
|
||||||
|
onSubmit={handleStockSubmit}
|
||||||
|
isLoading={isUpdatingStock}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={!!deleteConfirmFood}
|
||||||
|
close={() => setDeleteConfirmFood(null)}
|
||||||
|
onConfrim={handleDeleteConfirm}
|
||||||
|
isloading={isDeleting}
|
||||||
|
label={deleteConfirmFood ? `آیا از حذف «${deleteConfirmFood.title}» اطمینان دارید؟` : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FoodsList
|
export default FoodsList
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
import { Eye } from 'iconsax-react'
|
|
||||||
import type { ColumnType } from '@/components/types/TableTypes'
|
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'
|
|
||||||
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
|
||||||
|
|
||||||
interface GetFoodTableColumnsParams {
|
export const getFoodTableColumns = (): ColumnType<Food>[] => {
|
||||||
onDelete: (id: string) => void
|
|
||||||
isDeleting: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFoodTableColumns = ({ onDelete, isDeleting }: GetFoodTableColumnsParams): ColumnType<Food>[] => {
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -87,23 +78,6 @@ export const getFoodTableColumns = ({ onDelete, isDeleting }: GetFoodTableColumn
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'view',
|
|
||||||
title: '',
|
|
||||||
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' />
|
|
||||||
</Link>
|
|
||||||
<TrashWithConfrim
|
|
||||||
onDelete={() => onDelete(item.id)}
|
|
||||||
isloading={isDeleting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { type FC, useEffect, useState } from 'react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import type { Food } from '../types/Types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
food: Food | null
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (foodId: string, totalStock: number, availableStock: number) => void
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockUpdateModal: FC<Props> = ({
|
||||||
|
food,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
isLoading = false,
|
||||||
|
}) => {
|
||||||
|
const [totalStock, setTotalStock] = useState(0)
|
||||||
|
const [availableStock, setAvailableStock] = useState(0)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (food) {
|
||||||
|
setTotalStock(food.inventory?.totalStock ?? 0)
|
||||||
|
setAvailableStock(food.inventory?.availableStock ?? 0)
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
}, [food])
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (availableStock > totalStock) {
|
||||||
|
setError('موجودی فعلی نمیتواند بیشتر از موجودی روزانه باشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!food) return
|
||||||
|
|
||||||
|
onSubmit(food.id, totalStock, availableStock)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={onClose}
|
||||||
|
isHeader
|
||||||
|
title_header='بروزرسانی موجودی'
|
||||||
|
width={480}
|
||||||
|
>
|
||||||
|
{food && (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<p className='text-sm text-gray-600 mb-5'>
|
||||||
|
غذا: <span className='font-medium text-gray-800'>{food.title}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<Input
|
||||||
|
name='totalStock'
|
||||||
|
label='موجودی روزانه'
|
||||||
|
placeholder='عدد'
|
||||||
|
type='number'
|
||||||
|
seprator
|
||||||
|
value={totalStock}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTotalStock(Number(e.target.value))
|
||||||
|
setError('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name='availableStock'
|
||||||
|
label='موجودی فعلی'
|
||||||
|
placeholder='عدد'
|
||||||
|
type='number'
|
||||||
|
seprator
|
||||||
|
value={availableStock}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAvailableStock(Number(e.target.value))
|
||||||
|
setError('')
|
||||||
|
}}
|
||||||
|
error_text={error}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-3 mt-6'>
|
||||||
|
<Button
|
||||||
|
label='انصراف'
|
||||||
|
type='button'
|
||||||
|
onClick={onClose}
|
||||||
|
className='bg-gray-100 text-gray-700'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label='ذخیره'
|
||||||
|
type='button'
|
||||||
|
onClick={handleSubmit}
|
||||||
|
isloading={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StockUpdateModal
|
||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
CreateCategoryType,
|
CreateCategoryType,
|
||||||
CreateFoodType,
|
CreateFoodType,
|
||||||
GetFoodsParams,
|
GetFoodsParams,
|
||||||
|
SetStockType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
import type { FilterValues } from "@/components/Filters";
|
import type { FilterValues } from "@/components/Filters";
|
||||||
|
|
||||||
@@ -91,3 +92,14 @@ export const useGetIcons = () => {
|
|||||||
queryFn: api.getIcons,
|
queryFn: api.getIcons,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useSetStockForFood = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ foodId, params }: { foodId: string; params: SetStockType }) =>
|
||||||
|
api.setStockForFood(foodId, params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["foods"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
GetFoodsParams,
|
GetFoodsParams,
|
||||||
GetFoodsResponseType,
|
GetFoodsResponseType,
|
||||||
GetIconsResponseType,
|
GetIconsResponseType,
|
||||||
|
SetStockType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
import type { FilterValues } from "@/components/Filters";
|
import type { FilterValues } from "@/components/Filters";
|
||||||
|
|
||||||
@@ -76,3 +77,11 @@ export const getIcons = async (): Promise<GetIconsResponseType> => {
|
|||||||
const { data } = await axios.get<GetIconsResponseType>(`/admin/groups/icons`);
|
const { data } = await axios.get<GetIconsResponseType>(`/admin/groups/icons`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setStockForFood = async (foodId: string, params: SetStockType) => {
|
||||||
|
const { data } = await axios.patch(
|
||||||
|
`/admin/inventory/food/${foodId}/stock`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -150,3 +150,8 @@ export type IconGroup = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GetIconsResponseType = IResponse<IconGroup[]>;
|
export type GetIconsResponseType = IResponse<IconGroup[]>;
|
||||||
|
|
||||||
|
export type SetStockType = {
|
||||||
|
totalStock: number;
|
||||||
|
availableStock: number;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user