add stock modal
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-06-18 18:52:50 +03:30
parent c588b6f6b4
commit 1137780cf0
6 changed files with 214 additions and 37 deletions
+78 -9
View File
@@ -1,16 +1,26 @@
import { type FC } from 'react'
import { type FC, useCallback, useState } from 'react'
import Table from '@/components/Table'
import Filters from '@/components/Filters'
import { Add } from 'iconsax-react'
import Button from '@/components/Button'
import { useGetFoods, useDeleteFood } from './hooks/useFoodData'
import { useGetFoods, useDeleteFood, useSetStockForFood } from './hooks/useFoodData'
import { useFoodFilters } from './hooks/useFoodFilters'
import { getFoodTableColumns } from './components/FoodTableColumns'
import { useFoodFiltersFields } from './components/FoodFiltersFields'
import { Link } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'
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 navigate = useNavigate()
const [stockModalFood, setStockModalFood] = useState<Food | null>(null)
const [deleteConfirmFood, setDeleteConfirmFood] = useState<Food | null>(null)
const {
filters,
currentPage,
@@ -21,18 +31,60 @@ const FoodsList: FC = () => {
const { data: foodsData, isLoading } = useGetFoods(apiParams, filters)
const { mutate: deleteFood, isPending: isDeleting } = useDeleteFood()
const { mutate: setStock, isPending: isUpdatingStock } = useSetStockForFood()
const foods = foodsData?.data || []
const columns = getFoodTableColumns({
onDelete: (id: string) => deleteFood(id),
isDeleting
})
const columns = getFoodTableColumns()
const filterFields = useFoodFiltersFields()
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 (
<div className='mt-5'>
<div className='flex justify-between items-center'>
@@ -63,12 +115,29 @@ const FoodsList: FC = () => {
columns={columns}
data={foods}
isloading={isLoading}
rowActions={handleRowActions}
pagination={{
currentPage,
totalPages,
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>
)
}
+1 -27
View File
@@ -1,18 +1,9 @@
import { Eye } from 'iconsax-react'
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'
import TrashWithConfrim from '@/components/TrashWithConfrim'
interface GetFoodTableColumnsParams {
onDelete: (id: string) => void
isDeleting: boolean
}
export const getFoodTableColumns = ({ onDelete, isDeleting }: GetFoodTableColumnsParams): ColumnType<Food>[] => {
export const getFoodTableColumns = (): ColumnType<Food>[] => {
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
+12
View File
@@ -4,6 +4,7 @@ import type {
CreateCategoryType,
CreateFoodType,
GetFoodsParams,
SetStockType,
} from "../types/Types";
import type { FilterValues } from "@/components/Filters";
@@ -91,3 +92,14 @@ export const useGetIcons = () => {
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"] });
},
});
};
+9
View File
@@ -7,6 +7,7 @@ import type {
GetFoodsParams,
GetFoodsResponseType,
GetIconsResponseType,
SetStockType,
} from "../types/Types";
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`);
return data;
};
export const setStockForFood = async (foodId: string, params: SetStockType) => {
const { data } = await axios.patch(
`/admin/inventory/food/${foodId}/stock`,
params,
);
return data;
};
+5
View File
@@ -150,3 +150,8 @@ export type IconGroup = {
};
export type GetIconsResponseType = IResponse<IconGroup[]>;
export type SetStockType = {
totalStock: number;
availableStock: number;
};