From 322120635d2255af44aab11e347ef3351fa26828 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 15 Nov 2025 16:18:29 +0330 Subject: [PATCH] category compelete --- src/components/DefaulModal.tsx | 2 +- src/index.css | 6 + src/pages/food/Category.tsx | 319 +++++++++--------- src/pages/food/components/CategoryForm.tsx | 79 +++++ .../food/components/CategoryTableColumns.tsx | 72 ++++ src/pages/food/hooks/useFoodData.ts | 37 +- src/pages/food/service/FoodService.ts | 19 ++ src/pages/food/types/Types.ts | 7 + 8 files changed, 374 insertions(+), 167 deletions(-) create mode 100644 src/pages/food/components/CategoryForm.tsx create mode 100644 src/pages/food/components/CategoryTableColumns.tsx diff --git a/src/components/DefaulModal.tsx b/src/components/DefaulModal.tsx index 2f5c201..0f2b2d5 100644 --- a/src/components/DefaulModal.tsx +++ b/src/components/DefaulModal.tsx @@ -46,7 +46,7 @@ const DefaulModal: FC = (props: Props) => { -
+
) } diff --git a/src/index.css b/src/index.css index bc9871f..ed8915b 100644 --- a/src/index.css +++ b/src/index.css @@ -24,6 +24,12 @@ body, } * { outline: none; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ +} + +*::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ } input::placeholder, textarea::placeholder { diff --git a/src/pages/food/Category.tsx b/src/pages/food/Category.tsx index 117ddac..fe877ec 100644 --- a/src/pages/food/Category.tsx +++ b/src/pages/food/Category.tsx @@ -1,126 +1,165 @@ import { type FC, useState } from 'react' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { toast } from 'react-toastify' import Table from '@/components/Table' -import Button from '@/components/Button' -import Input from '@/components/Input' -import Select from '@/components/Select' -import SwitchComponent from '@/components/Switch' -import UploadBox from '@/components/UploadBox' -import { type RowActionItem } from '@/components/RowActionsDropdown' -import { Add } from 'iconsax-react' import Filters from '@/components/Filters' +import CategoryForm from './components/CategoryForm' +import CategoryTableColumns from './components/CategoryTableColumns' +import type { Category, CreateCategoryType } from './types/Types' +import type { ErrorType } from '@/helpers/types' +import { + useGetCategories, + useCreateCategory, + useUpdateCategory, + useDeleteCategory +} from './hooks/useFoodData' +import { useSingleUpload } from '../uploader/hooks/useUploaderData' +import { extractErrorMessage } from '@/config/func' -interface CategoryData { - id: number - name: string - icon: string - iconColor: string - order: number - foodCount: number - status: boolean - [key: string]: string | number | boolean +type CategoryFormValues = { + title: string + icon: File[] } const CategoryFood: FC = () => { - const [selectedRow, setSelectedRow] = useState(null) - const [currentPage, setCurrentPage] = useState(1) + const [isActive, setIsActive] = useState(true) + const [iconFiles, setIconFiles] = useState([]) + const [editingCategory, setEditingCategory] = useState(null) + const [filters, setFilters] = useState<{ search?: string | null }>({}) - // فرم دسته‌بندی جدید - const [formData, setFormData] = useState({ - isActive: true, - categoryStatus: '', - title: '', - order: '', - icon: [] as File[] + const { data: categoriesData, isLoading } = useGetCategories() + const { mutate: createCategory, isPending: isCreating } = useCreateCategory() + const { mutate: updateCategory, isPending: isUpdating } = useUpdateCategory() + const { mutate: deleteCategory, isPending: isDeleting } = useDeleteCategory() + const { mutate: singleUpload, isPending: isUploading } = useSingleUpload() + + const categories = categoriesData?.data || [] + + const filteredCategories = categories.filter((category) => { + if (filters.search) { + return category.title.toLowerCase().includes(filters.search.toLowerCase()) + } + return true }) - // داده‌های تستی - const [categories] = useState([ - { id: 1, name: 'ایرانی', icon: '🍕', iconColor: '#FF6B6B', order: 1, foodCount: 14, status: true }, - { id: 2, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 2, foodCount: 14, status: true }, - { id: 3, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 3, foodCount: 14, status: true }, - { id: 4, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 4, foodCount: 14, status: true }, - { id: 5, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 5, foodCount: 14, status: true }, - { id: 6, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 6, foodCount: 14, status: true }, - ]) - - const orderOptions = Array.from({ length: 20 }, (_, i) => ({ - value: String(i + 1), - label: String(i + 1) - })) - - const handleSave = () => { - console.log('ذخیره:', formData) - // پاک کردن فرم بعد از ذخیره - setFormData({ - isActive: true, - categoryStatus: '', + const formik = useFormik({ + initialValues: { title: '', - order: '', icon: [] + }, + validationSchema: Yup.object().shape({ + title: Yup.string().required('عنوان دسته‌بندی الزامی است') + }), + onSubmit: (values) => { + const submitCategory = (avatarUrl?: string) => { + const categoryData: CreateCategoryType = { + title: values.title, + isActive, + avatarUrl: avatarUrl || editingCategory?.avatarUrl || '' + } + + if (editingCategory) { + updateCategory( + { id: editingCategory.id, params: categoryData }, + { + onSuccess: () => { + toast.success('دسته‌بندی با موفقیت به‌روزرسانی شد') + resetForm() + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)) + } + } + ) + } else { + createCategory(categoryData, { + onSuccess: () => { + toast.success('دسته‌بندی با موفقیت ایجاد شد') + resetForm() + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)) + } + }) + } + } + + if (iconFiles.length > 0) { + singleUpload(iconFiles[0], { + onSuccess: (response) => { + const avatarUrl = response?.data?.url || '' + submitCategory(avatarUrl) + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error, 'خطا در آپلود تصویر')) + } + }) + } else { + submitCategory() + } + } + }) + + const resetForm = () => { + formik.resetForm() + setIsActive(true) + setIconFiles([]) + setEditingCategory(null) + } + + const handleEdit = (category: Category) => { + formik.setValues({ + title: category.title, + icon: [] + }) + setIsActive(category.isActive) + setIconFiles([]) + setEditingCategory(category) + } + + const handleDelete = (id: string) => { + deleteCategory(id, { + onSuccess: () => { + toast.success('دسته‌بندی با موفقیت حذف شد') + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)) + } }) } - const rowActions = (item: CategoryData): RowActionItem[] => [ - { - label: 'ویرایش', - onClick: () => { - console.log('ویرایش', item) - // بارگذاری داده‌ها در فرم - setFormData({ - isActive: item.status, - categoryStatus: '', - title: item.name, - order: String(item.order), - icon: [] - }) + const handleStatusChange = (id: string, value: boolean) => { + const category = categories.find((cat) => cat.id === id) + if (category) { + const categoryData: CreateCategoryType = { + title: category.title, + isActive: value, + avatarUrl: category.avatarUrl } - }, - { - label: 'حذف', - onClick: () => console.log('حذف', item) + updateCategory( + { id, params: categoryData }, + { + onSuccess: () => { + toast.success('وضعیت دسته‌بندی با موفقیت تغییر کرد') + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)) + } + } + ) } - ] + } - const columns = [ - { - title: 'آیکون', - key: 'icon', - render: (item: CategoryData) => ( -
- {item.icon} -
- ) - }, - { - title: 'عنوان', - key: 'name', - }, - { - title: 'ترتیب نمایش', - key: 'order', - }, - { - title: 'تعداد غذا', - key: 'foodCount', - }, - { - title: 'وضعیت', - key: 'status', - render: (item: CategoryData) => ( - console.log('تغییر وضعیت:', item.id, value)} - /> - ) - } - ] + const columns = CategoryTableColumns({ + onStatusChange: handleStatusChange, + onEdit: handleEdit, + onDelete: handleDelete, + isDeleting + }) return (
- {/* بخش اصلی - جدول */}

دسته بندی غذا

@@ -131,80 +170,30 @@ const CategoryFood: FC = () => { { type: 'input', name: 'search', - placeholder: 'جستجو', + placeholder: 'جستجو' } ]} - onChange={() => { }} + onChange={(filters) => setFilters(filters)} />
setSelectedRow(id as number)} - rowClassName={(item) => - selectedRow === item.id ? 'bg-blue-50 border-2 border-blue-400 border-dashed' : '' - } - pagination={{ - currentPage, - totalPages: 2, - onPageChange: setCurrentPage - }} /> - {/* سایدبار - فرم اضافه کردن */} -
-

اضافه کردن دسته بندی

- -
-
فعال
- setFormData({ ...formData, isActive: value })} - /> -
- -
- setFormData({ ...formData, categoryStatus: e.target.value })} - /> -
- -
- setFormData({ ...formData, title: e.target.value })} - /> -
- -
- +
+ +
+ { + onIconChange(files) + formik.setFieldValue('icon', files) + }} + isReset={iconFiles.length === 0 && formik.values.icon.length === 0} + /> +
+ + +
+ ) +} + +export default CategoryForm + diff --git a/src/pages/food/components/CategoryTableColumns.tsx b/src/pages/food/components/CategoryTableColumns.tsx new file mode 100644 index 0000000..59c8b42 --- /dev/null +++ b/src/pages/food/components/CategoryTableColumns.tsx @@ -0,0 +1,72 @@ +import { Edit } from 'iconsax-react' +import SwitchComponent from '@/components/Switch' +import TrashWithConfrim from '@/components/TrashWithConfrim' +import type { Category } from '../types/Types' + +type Props = { + onStatusChange: (id: string, value: boolean) => void + onEdit: (category: Category) => void + onDelete: (id: string) => void + isDeleting?: boolean +} + +const CategoryTableColumns = ({ onStatusChange, onEdit, onDelete, isDeleting = false }: Props) => { + return [ + { + title: 'آیکون', + key: 'avatarUrl', + render: (item: Category) => ( +
+ {item.avatarUrl ? ( + {item.title} + ) : ( +
+ بدون آیکون +
+ )} +
+ ) + }, + { + title: 'عنوان', + key: 'title' + }, + { + title: 'وضعیت', + key: 'isActive', + render: (item: Category) => ( + onStatusChange(item.id, value)} + /> + ) + }, + { + key: 'actions', + title: '', + render: (item: Category) => { + return ( +
+ onEdit(item)} + size={20} + color='#8C90A3' + className='cursor-pointer' + /> + onDelete(item.id)} + isLoading={isDeleting} + /> +
+ ) + } + } + ] +} + +export default CategoryTableColumns + diff --git a/src/pages/food/hooks/useFoodData.ts b/src/pages/food/hooks/useFoodData.ts index 1b2cdc9..781d548 100644 --- a/src/pages/food/hooks/useFoodData.ts +++ b/src/pages/food/hooks/useFoodData.ts @@ -1,6 +1,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as api from "../service/FoodService"; -import type { CreateFoodType, GetFoodsParams } from "../types/Types"; +import type { + CreateCategoryType, + CreateFoodType, + GetFoodsParams, +} from "../types/Types"; export const useCreateFood = () => { return useMutation({ @@ -45,3 +49,34 @@ export const useDeleteFood = () => { }, }); }; + +export const useCreateCategory = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: api.createCategory, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["categories"] }); + }, + }); +}; + +export const useDeleteCategory = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: api.deleteCategory, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["categories"] }); + }, + }); +}; + +export const useUpdateCategory = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, params }: { id: string; params: CreateCategoryType }) => + api.updateCategory(id, params), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["categories"] }); + }, + }); +}; diff --git a/src/pages/food/service/FoodService.ts b/src/pages/food/service/FoodService.ts index 0d87576..05b3d51 100644 --- a/src/pages/food/service/FoodService.ts +++ b/src/pages/food/service/FoodService.ts @@ -1,5 +1,6 @@ import axios from "@/config/axios"; import type { + CreateCategoryType, CreateFoodType, GetCategoriesResponseType, GetFoodDetailsResponseType, @@ -40,3 +41,21 @@ export const deleteFood = async (id: string) => { const { data } = await axios.delete(`/foods/${id}`); return data; }; + +export const createCategory = async (params: CreateCategoryType) => { + const { data } = await axios.post(`/categories`, params); + return data; +}; + +export const deleteCategory = async (id: string) => { + const { data } = await axios.delete(`/categories/${id}`); + return data; +}; + +export const updateCategory = async ( + id: string, + params: CreateCategoryType +) => { + const { data } = await axios.patch(`/categories/${id}`, params); + return data; +}; diff --git a/src/pages/food/types/Types.ts b/src/pages/food/types/Types.ts index 126e2b4..53d4513 100644 --- a/src/pages/food/types/Types.ts +++ b/src/pages/food/types/Types.ts @@ -36,6 +36,7 @@ export type Category = { isActive: boolean; restId: string; avatarUrl: string; + order: number | null; }; export type Food = { @@ -84,3 +85,9 @@ export type GetFoodsParams = { export type GetCategoriesResponseType = IResponse; export type GetFoodsResponseType = IResponse; export type GetFoodDetailsResponseType = IResponse; + +export type CreateCategoryType = { + title: string; + isActive: boolean; + avatarUrl: string; +};