388 lines
21 KiB
TypeScript
388 lines
21 KiB
TypeScript
import { type FC, useState, useEffect } from 'react'
|
||
import Button from '../../components/Button'
|
||
import { TickCircle, ArrowLeft } from 'iconsax-react'
|
||
import SwitchComponent from '../../components/Switch'
|
||
import Input from '../../components/Input'
|
||
import Textarea from '../../components/Textarea'
|
||
import UploadBox from '../../components/UploadBox'
|
||
import Select from '../../components/Select'
|
||
import { type CategoryType, type CategoryTreeNode } from './types/Types'
|
||
import { useFormik } from 'formik'
|
||
import * as Yup from 'yup'
|
||
import { useUpdateCategory, useGetCategories, useGetCategoryById, useSingleUpload } from './hooks/useCategoryData'
|
||
import { useNavigate, useParams } from 'react-router-dom'
|
||
import { toast } from 'react-toastify'
|
||
import { Pages } from '../../config/Pages'
|
||
|
||
const UpdateCategory: FC = () => {
|
||
const navigate = useNavigate()
|
||
const { id } = useParams<{ id: string }>()
|
||
const [iconFile, setIconFile] = useState<File>()
|
||
const [imageFile, setImageFile] = useState<File>()
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
const [isActive, setIsActive] = useState(true)
|
||
const [hasIconChanged, setHasIconChanged] = useState(false)
|
||
const [hasImageChanged, setHasImageChanged] = useState(false)
|
||
|
||
const singleUpload = useSingleUpload()
|
||
const updateCategoryMutation = useUpdateCategory()
|
||
const getCategories = useGetCategories()
|
||
const getCategoryById = useGetCategoryById(id!)
|
||
|
||
const formik = useFormik<CategoryType>({
|
||
initialValues: {
|
||
title_fa: '',
|
||
title_en: '',
|
||
icon: '',
|
||
imageUrl: '',
|
||
description: '',
|
||
parent: null,
|
||
theme: '',
|
||
},
|
||
validationSchema: Yup.object({
|
||
title_fa: Yup.string().required('عنوان فارسی دسته الزامی است'),
|
||
title_en: Yup.string().required('عنوان انگلیسی دسته الزامی است'),
|
||
description: Yup.string().required('توضیحات دسته الزامی است'),
|
||
parent: Yup.string().nullable().optional(),
|
||
theme: Yup.string().optional(),
|
||
}),
|
||
onSubmit: async (values) => {
|
||
const updatedValues = { ...values }
|
||
|
||
// آپلود آیکون جدید اگر تغییر کرده باشد
|
||
if (hasIconChanged && iconFile) {
|
||
const iconRes = await singleUpload.mutateAsync(iconFile)
|
||
updatedValues.icon = iconRes.results?.url?.url
|
||
}
|
||
|
||
// آپلود تصویر جدید اگر تغییر کرده باشد
|
||
if (hasImageChanged && imageFile) {
|
||
const imageRes = await singleUpload.mutateAsync(imageFile)
|
||
updatedValues.imageUrl = imageRes.results?.url?.url
|
||
}
|
||
|
||
updateCategoryMutation.mutate(
|
||
{ id: id!, data: updatedValues },
|
||
{
|
||
onSuccess: () => {
|
||
toast.success('دسته با موفقیت ویرایش شد')
|
||
navigate(Pages.category.list)
|
||
},
|
||
onError: () => {
|
||
toast.error('خطا در ویرایش دسته')
|
||
},
|
||
}
|
||
)
|
||
},
|
||
})
|
||
|
||
// بارگذاری دادههای دسته برای ویرایش
|
||
useEffect(() => {
|
||
if (getCategoryById.data?.results?.data) {
|
||
const categoryData = getCategoryById.data.results.data
|
||
formik.setValues({
|
||
title_fa: categoryData.title_fa || '',
|
||
title_en: categoryData.title_en || '',
|
||
icon: categoryData.icon || '',
|
||
imageUrl: categoryData.imageUrl || '',
|
||
description: categoryData.description || '',
|
||
parent: categoryData.parent,
|
||
theme: categoryData.theme || '',
|
||
})
|
||
setIsActive(!categoryData.deleted)
|
||
}
|
||
}, [getCategoryById.data, formik])
|
||
|
||
const categoriesData: CategoryTreeNode[] = getCategories.data?.results?.data || []
|
||
|
||
|
||
const filteredCategories = categoriesData.filter(cat =>
|
||
cat.title_fa.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
cat.title_en.toLowerCase().includes(searchTerm.toLowerCase())
|
||
)
|
||
|
||
const handleParentSelect = (category: CategoryTreeNode) => {
|
||
formik.setFieldValue('parent', category._id)
|
||
}
|
||
|
||
const handleIconChange = (files: File[]) => {
|
||
setIconFile(files[0])
|
||
setHasIconChanged(true)
|
||
}
|
||
|
||
const handleImageChange = (files: File[]) => {
|
||
setImageFile(files[0])
|
||
setHasImageChanged(true)
|
||
}
|
||
|
||
if (getCategoryById.isLoading) {
|
||
return (
|
||
<div className="flex justify-center items-center h-64">
|
||
<div className="text-gray-500">در حال بارگذاری...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (getCategoryById.isError) {
|
||
return (
|
||
<div className="flex justify-center items-center h-64">
|
||
<div className="text-red-500">خطا در بارگذاری اطلاعات دسته</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className='mt-4'>
|
||
<div className='flex justify-between items-center'>
|
||
<div className='flex items-center gap-3'>
|
||
<button
|
||
onClick={() => navigate(-1)}
|
||
className='p-2 hover:bg-gray-100 rounded-lg transition-colors'
|
||
>
|
||
<ArrowLeft size={20} />
|
||
</button>
|
||
<h1 className=''>ویرایش دسته</h1>
|
||
</div>
|
||
<div>
|
||
<Button
|
||
className='px-6'
|
||
onClick={() => formik.handleSubmit()}
|
||
isLoading={updateCategoryMutation.isPending}
|
||
disabled={!formik.isValid || updateCategoryMutation.isPending}
|
||
>
|
||
<div className='flex gap-2 items-center'>
|
||
<TickCircle size={16} color='#fff' />
|
||
<div>ذخیره تغییرات</div>
|
||
</div>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className='flex gap-6 xl:mt-8 mt-6'>
|
||
{/* اطلاعات اصلی */}
|
||
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
|
||
<h2 className='text-lg font-medium mb-6'>اطلاعات اصلی</h2>
|
||
|
||
<div className='space-y-6'>
|
||
<div className='flex items-center justify-between'>
|
||
<span className='text-gray-700'>وضعیت دسته</span>
|
||
<SwitchComponent
|
||
active={isActive}
|
||
onChange={setIsActive}
|
||
/>
|
||
</div>
|
||
|
||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||
<Input
|
||
label='عنوان فارسی دسته'
|
||
placeholder='مثال: الکترونیک'
|
||
{...formik.getFieldProps('title_fa')}
|
||
error_text={formik.touched.title_fa && formik.errors.title_fa ? formik.errors.title_fa : ''}
|
||
/>
|
||
<Input
|
||
label='عنوان انگلیسی دسته'
|
||
placeholder='مثال: Electronics'
|
||
{...formik.getFieldProps('title_en')}
|
||
error_text={formik.touched.title_en && formik.errors.title_en ? formik.errors.title_en : ''}
|
||
/>
|
||
</div>
|
||
|
||
<Textarea
|
||
label='توضیحات دسته'
|
||
placeholder='توضیحات کامل دسته را وارد کنید...'
|
||
{...formik.getFieldProps('description')}
|
||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
|
||
/>
|
||
|
||
<Select
|
||
label='تم دسته'
|
||
placeholder='انتخاب تم دسته'
|
||
items={[
|
||
{ value: 'Color', label: 'رنگی' },
|
||
{ value: 'Monochrome', label: 'تکرنگ' },
|
||
{ value: 'Gradient', label: 'گرادیانت' },
|
||
{ value: 'Minimal', label: 'مینیمال' },
|
||
]}
|
||
value={formik.values.theme}
|
||
onChange={(e) => formik.setFieldValue('theme', e.target.value)}
|
||
error_text={formik.touched.theme && formik.errors.theme ? formik.errors.theme : ''}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ستون سمت راست */}
|
||
<div className='w-80 space-y-6'>
|
||
{/* آپلود فایلها */}
|
||
<div className='text-sm bg-white py-8 px-6 rounded-3xl'>
|
||
<h2 className='text-lg font-medium mb-6'>فایلها</h2>
|
||
|
||
<div className='space-y-6'>
|
||
<div>
|
||
<UploadBox
|
||
label='آیکون دسته'
|
||
onChange={handleIconChange}
|
||
/>
|
||
{formik.values.icon && !hasIconChanged && (
|
||
<div className='mt-2 p-2 bg-gray-50 rounded-lg'>
|
||
<div className='text-xs text-gray-600 mb-2'>آیکون فعلی:</div>
|
||
<div className='flex justify-center'>
|
||
<img
|
||
src={formik.values.icon}
|
||
alt='آیکون فعلی'
|
||
className='w-12 h-12 rounded-full object-cover border border-gray-200'
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{formik.errors.icon && formik.touched.icon && (
|
||
<p className='text-red-500 text-xs mt-1'>{formik.errors.icon}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<UploadBox
|
||
label='تصویر دسته'
|
||
onChange={handleImageChange}
|
||
/>
|
||
{formik.values.imageUrl && !hasImageChanged && (
|
||
<div className='mt-2 p-2 bg-gray-50 rounded-lg'>
|
||
<div className='text-xs text-gray-600 mb-2'>تصویر فعلی:</div>
|
||
<div className='flex justify-center'>
|
||
<img
|
||
src={formik.values.imageUrl}
|
||
alt='تصویر فعلی'
|
||
className='w-16 h-16 rounded-lg object-cover border border-gray-200'
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{formik.errors.imageUrl && formik.touched.imageUrl && (
|
||
<p className='text-red-500 text-xs mt-1'>{formik.errors.imageUrl}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* انتخاب دسته والد */}
|
||
<div className='text-sm bg-white py-6 px-4 rounded-2xl'>
|
||
<h2 className='text-lg font-medium mb-4'>انتخاب دسته والد</h2>
|
||
|
||
{/* جستجو */}
|
||
<div className='mb-4'>
|
||
<div className='relative'>
|
||
<input
|
||
type='text'
|
||
placeholder='جستجو در دستهها...'
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className='w-full px-3 py-2.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white'
|
||
/>
|
||
<div className='absolute left-3 top-1/2 transform -translate-y-1/2'>
|
||
<svg className='w-4 h-4 text-gray-400' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* درخت دستهبندی */}
|
||
<div className='bg-gray-50 border border-gray-200 rounded-lg p-3 max-h-72 overflow-y-auto'>
|
||
{filteredCategories.length > 0 ? (
|
||
<div className='space-y-2'>
|
||
{filteredCategories.map((category: CategoryTreeNode) => (
|
||
<div key={category._id} className='space-y-1'>
|
||
{/* دسته اصلی */}
|
||
<div
|
||
className={`
|
||
flex items-center gap-3 p-2.5 rounded-lg cursor-pointer transition-colors
|
||
${category._id === formik.values.parent
|
||
? 'bg-blue-100 border border-blue-300'
|
||
: 'hover:bg-gray-100'
|
||
}
|
||
`}
|
||
onClick={() => handleParentSelect(category)}
|
||
>
|
||
<div className='flex-1 min-w-0'>
|
||
<div className='flex items-center gap-2 mb-1'>
|
||
<span className={`text-sm font-medium ${category._id === formik.values.parent ? 'text-blue-700' : 'text-gray-900'}`}>
|
||
{category.title_fa}
|
||
</span>
|
||
{category.children.length > 0 && (
|
||
<span className='bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full text-xs'>
|
||
{category.children.length}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className='text-xs text-gray-500'>{category.title_en}</div>
|
||
</div>
|
||
{category._id === formik.values.parent && (
|
||
<div className='flex-shrink-0'>
|
||
<div className='w-2 h-2 bg-blue-500 rounded-full'></div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* زیردستهها - همیشه باز */}
|
||
{category.children.length > 0 && (
|
||
<div className='mr-4 space-y-1'>
|
||
{category.children.map((child: CategoryTreeNode) => (
|
||
<div
|
||
key={child._id}
|
||
className={`
|
||
flex items-center gap-3 p-2.5 rounded-lg cursor-pointer transition-colors
|
||
${child._id === formik.values.parent
|
||
? 'bg-blue-100 border border-blue-300'
|
||
: 'hover:bg-gray-100'
|
||
}
|
||
`}
|
||
onClick={() => handleParentSelect(child)}
|
||
>
|
||
<div className='flex-1 min-w-0'>
|
||
<div className='flex items-center gap-2 mb-1'>
|
||
<span className={`text-sm font-medium ${child._id === formik.values.parent ? 'text-blue-700' : 'text-gray-800'}`}>
|
||
{child.title_fa}
|
||
</span>
|
||
{child.children.length > 0 && (
|
||
<span className='bg-blue-100 text-blue-600 px-2 py-0.5 rounded-full text-xs'>
|
||
{child.children.length}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className='text-xs text-gray-500'>{child.title_en}</div>
|
||
</div>
|
||
{child._id === formik.values.parent && (
|
||
<div className='flex-shrink-0'>
|
||
<div className='w-2 h-2 bg-blue-500 rounded-full'></div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className='text-center py-8 text-gray-500'>
|
||
<svg className='w-12 h-12 mx-auto mb-3 text-gray-300' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1} d='M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z' />
|
||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1} d='M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z' />
|
||
</svg>
|
||
<p className='text-sm font-medium text-gray-400 mb-1'>
|
||
{searchTerm ? 'نتیجهای یافت نشد' : 'دستهای موجود نیست'}
|
||
</p>
|
||
<p className='text-xs text-gray-400'>
|
||
{searchTerm ? 'کلمه جستجو را تغییر دهید' : 'برای شروع، دستهای جدید ایجاد کنید'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default UpdateCategory
|