Files
shop-admin/src/pages/category/Update.tsx
T
2025-09-21 10:45:52 +03:30

388 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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