From 1137780cf052d658da5690e4000529dcabf826b9 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 18 Jun 2026 18:52:50 +0330 Subject: [PATCH] add stock modal --- src/pages/food/List.tsx | 89 +++++++++++++-- .../food/components/FoodTableColumns.tsx | 28 +---- .../food/components/StockUpdateModal.tsx | 108 ++++++++++++++++++ src/pages/food/hooks/useFoodData.ts | 12 ++ src/pages/food/service/FoodService.ts | 9 ++ src/pages/food/types/Types.ts | 5 + 6 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 src/pages/food/components/StockUpdateModal.tsx diff --git a/src/pages/food/List.tsx b/src/pages/food/List.tsx index 13510c4..756f645 100644 --- a/src/pages/food/List.tsx +++ b/src/pages/food/List.tsx @@ -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(null) + const [deleteConfirmFood, setDeleteConfirmFood] = useState(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 (
@@ -63,14 +115,31 @@ const FoodsList: FC = () => { columns={columns} data={foods} isloading={isLoading} + rowActions={handleRowActions} pagination={{ currentPage, totalPages, onPageChange: handlePageChange, }} /> + + setStockModalFood(null)} + onSubmit={handleStockSubmit} + isLoading={isUpdatingStock} + /> + + setDeleteConfirmFood(null)} + onConfrim={handleDeleteConfirm} + isloading={isDeleting} + label={deleteConfirmFood ? `آیا از حذف «${deleteConfirmFood.title}» اطمینان دارید؟` : undefined} + />
) } -export default FoodsList \ No newline at end of file +export default FoodsList diff --git a/src/pages/food/components/FoodTableColumns.tsx b/src/pages/food/components/FoodTableColumns.tsx index 10b328d..26effc4 100644 --- a/src/pages/food/components/FoodTableColumns.tsx +++ b/src/pages/food/components/FoodTableColumns.tsx @@ -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[] => { +export const getFoodTableColumns = (): ColumnType[] => { return [ { @@ -87,23 +78,6 @@ export const getFoodTableColumns = ({ onDelete, isDeleting }: GetFoodTableColumn ) } }, - { - key: 'view', - title: '', - render: (item: Food) => { - return ( -
- - - - onDelete(item.id)} - isloading={isDeleting} - /> -
- ) - } - }, ] } diff --git a/src/pages/food/components/StockUpdateModal.tsx b/src/pages/food/components/StockUpdateModal.tsx new file mode 100644 index 0000000..71dedf1 --- /dev/null +++ b/src/pages/food/components/StockUpdateModal.tsx @@ -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 = ({ + 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 ( + + {food && ( +
+

+ غذا: {food.title} +

+ +
+ { + setTotalStock(Number(e.target.value)) + setError('') + }} + /> + + { + setAvailableStock(Number(e.target.value)) + setError('') + }} + error_text={error} + /> +
+ +
+
+
+ )} +
+ ) +} + +export default StockUpdateModal diff --git a/src/pages/food/hooks/useFoodData.ts b/src/pages/food/hooks/useFoodData.ts index 01ee295..67227e8 100644 --- a/src/pages/food/hooks/useFoodData.ts +++ b/src/pages/food/hooks/useFoodData.ts @@ -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"] }); + }, + }); +}; diff --git a/src/pages/food/service/FoodService.ts b/src/pages/food/service/FoodService.ts index b0f90e5..3b3b6fc 100644 --- a/src/pages/food/service/FoodService.ts +++ b/src/pages/food/service/FoodService.ts @@ -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 => { const { data } = await axios.get(`/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; +}; diff --git a/src/pages/food/types/Types.ts b/src/pages/food/types/Types.ts index ea55680..20e6612 100644 --- a/src/pages/food/types/Types.ts +++ b/src/pages/food/types/Types.ts @@ -150,3 +150,8 @@ export type IconGroup = { }; export type GetIconsResponseType = IResponse; + +export type SetStockType = { + totalStock: number; + availableStock: number; +};