category compelete

This commit is contained in:
hamid zarghami
2025-11-15 16:18:29 +03:30
parent 8900e85892
commit 322120635d
8 changed files with 374 additions and 167 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ const DefaulModal: FC<Props> = (props: Props) => {
</div> </div>
</div> </div>
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 modalGlass inset-0 z-50 '></div> <div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 bg-black/40 inset-0 z-50 '></div>
</Fragment> </Fragment>
) )
} }
+6
View File
@@ -24,6 +24,12 @@ body,
} }
* { * {
outline: none; outline: none;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
*::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
} }
input::placeholder, input::placeholder,
textarea::placeholder { textarea::placeholder {
+149 -160
View File
@@ -1,126 +1,165 @@
import { type FC, useState } from 'react' 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 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 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 { type CategoryFormValues = {
id: number title: string
name: string icon: File[]
icon: string
iconColor: string
order: number
foodCount: number
status: boolean
[key: string]: string | number | boolean
} }
const CategoryFood: FC = () => { const CategoryFood: FC = () => {
const [selectedRow, setSelectedRow] = useState<number | null>(null) const [isActive, setIsActive] = useState<boolean>(true)
const [currentPage, setCurrentPage] = useState(1) const [iconFiles, setIconFiles] = useState<File[]>([])
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [filters, setFilters] = useState<{ search?: string | null }>({})
// فرم دسته‌بندی جدید const { data: categoriesData, isLoading } = useGetCategories()
const [formData, setFormData] = useState({ const { mutate: createCategory, isPending: isCreating } = useCreateCategory()
isActive: true, const { mutate: updateCategory, isPending: isUpdating } = useUpdateCategory()
categoryStatus: '', const { mutate: deleteCategory, isPending: isDeleting } = useDeleteCategory()
title: '', const { mutate: singleUpload, isPending: isUploading } = useSingleUpload()
order: '',
icon: [] as File[] const categories = categoriesData?.data || []
const filteredCategories = categories.filter((category) => {
if (filters.search) {
return category.title.toLowerCase().includes(filters.search.toLowerCase())
}
return true
}) })
// داده‌های تستی const formik = useFormik<CategoryFormValues>({
const [categories] = useState<CategoryData[]>([ initialValues: {
{ 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: '',
title: '', 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: [] icon: []
}) })
setIsActive(category.isActive)
setIconFiles([])
setEditingCategory(category)
} }
const rowActions = (item: CategoryData): RowActionItem[] => [ const handleDelete = (id: string) => {
{ deleteCategory(id, {
label: 'ویرایش', onSuccess: () => {
onClick: () => { toast.success('دسته‌بندی با موفقیت حذف شد')
console.log('ویرایش', item) },
// بارگذاری داده‌ها در فرم onError: (error: ErrorType) => {
setFormData({ toast.error(extractErrorMessage(error))
isActive: item.status, }
categoryStatus: '',
title: item.name,
order: String(item.order),
icon: []
}) })
} }
},
{
label: 'حذف',
onClick: () => console.log('حذف', item)
}
]
const columns = [ 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
}
updateCategory(
{ id, params: categoryData },
{ {
title: 'آیکون', onSuccess: () => {
key: 'icon', toast.success('وضعیت دسته‌بندی با موفقیت تغییر کرد')
render: (item: CategoryData) => (
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-lg"
style={{ backgroundColor: item.iconColor }}
>
{item.icon}
</div>
)
}, },
{ onError: (error: ErrorType) => {
title: 'عنوان', toast.error(extractErrorMessage(error))
key: 'name', }
}, }
{
title: 'ترتیب نمایش',
key: 'order',
},
{
title: 'تعداد غذا',
key: 'foodCount',
},
{
title: 'وضعیت',
key: 'status',
render: (item: CategoryData) => (
<SwitchComponent
active={item.status}
onChange={(value) => console.log('تغییر وضعیت:', item.id, value)}
/>
) )
} }
] }
const columns = CategoryTableColumns({
onStatusChange: handleStatusChange,
onEdit: handleEdit,
onDelete: handleDelete,
isDeleting
})
return ( return (
<div className="flex gap-6 p-6"> <div className="flex gap-6 p-6">
{/* بخش اصلی - جدول */}
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h1 className="text-lg font-light">دسته بندی غذا</h1> <h1 className="text-lg font-light">دسته بندی غذا</h1>
@@ -131,81 +170,31 @@ const CategoryFood: FC = () => {
{ {
type: 'input', type: 'input',
name: 'search', name: 'search',
placeholder: 'جستجو', placeholder: 'جستجو'
} }
]} ]}
onChange={() => { }} onChange={(filters) => setFilters(filters)}
/> />
</div> </div>
<Table <Table
columns={columns} columns={columns}
data={categories} data={filteredCategories}
isLoading={isLoading}
selectable selectable
rowActions={rowActions}
onRowClick={(id) => 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
}}
/> />
</div> </div>
{/* سایدبار - فرم اضافه کردن */} <CategoryForm
<div className="w-[300px] bg-white rounded-3xl p-6 h-fit sticky top-6"> formik={formik}
<h2 className="font-light mb-6">اضافه کردن دسته بندی</h2> isActive={isActive}
onActiveChange={setIsActive}
<div className="flex items-center justify-between mb-6"> onIconChange={setIconFiles}
<div className="text-sm">فعال</div> iconFiles={iconFiles}
<SwitchComponent isSubmitting={isCreating || isUpdating || isUploading}
active={formData.isActive} isEditing={!!editingCategory}
onChange={(value) => setFormData({ ...formData, isActive: value })}
/> />
</div> </div>
<div className="mb-4">
<Input
label="وضعیت دسته بندی"
value={formData.categoryStatus}
onChange={(e) => setFormData({ ...formData, categoryStatus: e.target.value })}
/>
</div>
<div className="mb-4">
<Input
label="عنوان دسته بندی"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
</div>
<div className="mb-4">
<Select
label="ترتیب نمایش"
items={orderOptions}
value={formData.order}
onChange={(e) => setFormData({ ...formData, order: e.target.value })}
placeholder="انتخاب کنید"
/>
</div>
<div className="mb-6">
<UploadBox
label="آیکون دسته بندی"
onChange={(files) => setFormData({ ...formData, icon: files })}
/>
</div>
<Button onClick={handleSave} className="w-full">
<Add size={20} className="ml-2" />
ذخیره
</Button>
</div>
</div>
) )
} }
@@ -0,0 +1,79 @@
import { type FC } from 'react'
import { Add } from 'iconsax-react'
import Input from '@/components/Input'
import UploadBox from '@/components/UploadBox'
import SwitchComponent from '@/components/Switch'
import Button from '@/components/Button'
import type { FormikProps } from 'formik'
type CategoryFormValues = {
title: string
icon: File[]
}
type Props = {
formik: FormikProps<CategoryFormValues>
isActive: boolean
onActiveChange: (value: boolean) => void
onIconChange: (files: File[]) => void
iconFiles: File[]
isSubmitting: boolean
isEditing?: boolean
}
const CategoryForm: FC<Props> = ({
formik,
isActive,
onActiveChange,
onIconChange,
iconFiles,
isSubmitting,
isEditing = false
}) => {
return (
<div className="w-[300px] bg-white rounded-3xl p-6 h-fit sticky top-6">
<h2 className="font-light mb-6">{isEditing ? 'ویرایش دسته بندی' : 'اضافه کردن دسته بندی'}</h2>
<div className="flex items-center justify-between mb-6">
<div className="text-sm">فعال</div>
<SwitchComponent
active={isActive}
onChange={onActiveChange}
/>
</div>
<div className="mb-4">
<Input
label="عنوان دسته بندی"
name="title"
value={formik.values.title}
onChange={formik.handleChange}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/>
</div>
<div className="mb-6">
<UploadBox
label="آیکون دسته بندی"
onChange={(files) => {
onIconChange(files)
formik.setFieldValue('icon', files)
}}
isReset={iconFiles.length === 0 && formik.values.icon.length === 0}
/>
</div>
<Button
onClick={() => formik.handleSubmit()}
className="w-full"
isLoading={isSubmitting}
>
<Add color='#fff' size={20} className="ml-2" />
ذخیره
</Button>
</div>
)
}
export default CategoryForm
@@ -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) => (
<div className="w-8 h-8 rounded-lg flex items-center justify-center overflow-hidden">
{item.avatarUrl ? (
<img
src={item.avatarUrl}
alt={item.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center text-xs">
بدون آیکون
</div>
)}
</div>
)
},
{
title: 'عنوان',
key: 'title'
},
{
title: 'وضعیت',
key: 'isActive',
render: (item: Category) => (
<SwitchComponent
active={item.isActive}
onChange={(value) => onStatusChange(item.id, value)}
/>
)
},
{
key: 'actions',
title: '',
render: (item: Category) => {
return (
<div className='flex gap-2 items-center'>
<Edit
onClick={() => onEdit(item)}
size={20}
color='#8C90A3'
className='cursor-pointer'
/>
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isLoading={isDeleting}
/>
</div>
)
}
}
]
}
export default CategoryTableColumns
+36 -1
View File
@@ -1,6 +1,10 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/FoodService"; import * as api from "../service/FoodService";
import type { CreateFoodType, GetFoodsParams } from "../types/Types"; import type {
CreateCategoryType,
CreateFoodType,
GetFoodsParams,
} from "../types/Types";
export const useCreateFood = () => { export const useCreateFood = () => {
return useMutation({ 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"] });
},
});
};
+19
View File
@@ -1,5 +1,6 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { import type {
CreateCategoryType,
CreateFoodType, CreateFoodType,
GetCategoriesResponseType, GetCategoriesResponseType,
GetFoodDetailsResponseType, GetFoodDetailsResponseType,
@@ -40,3 +41,21 @@ export const deleteFood = async (id: string) => {
const { data } = await axios.delete(`/foods/${id}`); const { data } = await axios.delete(`/foods/${id}`);
return data; 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;
};
+7
View File
@@ -36,6 +36,7 @@ export type Category = {
isActive: boolean; isActive: boolean;
restId: string; restId: string;
avatarUrl: string; avatarUrl: string;
order: number | null;
}; };
export type Food = { export type Food = {
@@ -84,3 +85,9 @@ export type GetFoodsParams = {
export type GetCategoriesResponseType = IResponse<Category[]>; export type GetCategoriesResponseType = IResponse<Category[]>;
export type GetFoodsResponseType = IResponse<Food[]>; export type GetFoodsResponseType = IResponse<Food[]>;
export type GetFoodDetailsResponseType = IResponse<Food>; export type GetFoodDetailsResponseType = IResponse<Food>;
export type CreateCategoryType = {
title: string;
isActive: boolean;
avatarUrl: string;
};