up
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-24 18:16:47 +03:30
parent 7d4e2865bb
commit 3676688849
8 changed files with 122 additions and 84 deletions
+1 -1
View File
@@ -8,8 +8,8 @@ export const Paths = {
create: '/product/create', create: '/product/create',
update: '/product/update/', update: '/product/update/',
category: { category: {
create: '/product/category/create',
list: '/product/category/list', list: '/product/category/list',
children: '/product/category/list/',
update: '/product/category/update/', update: '/product/category/update/',
}, },
attribute: { attribute: {
+6 -2
View File
@@ -15,6 +15,8 @@ import { toast } from 'react-toastify';
import type { ErrorType } from '@/helpers/types'; import type { ErrorType } from '@/helpers/types';
import { extractErrorMessage } from '@/config/func'; import { extractErrorMessage } from '@/config/func';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Paths } from '@/config/Paths'
import BackButton from '@/components/BackButton'
const CreateProduct: FC = () => { const CreateProduct: FC = () => {
@@ -71,8 +73,10 @@ const CreateProduct: FC = () => {
return ( return (
<div className='mt-5'> <div className='mt-5'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<div className='flex items-center gap-4'>
<h1 className='text-lg font-light'>محصول جدید</h1> <BackButton to={Paths.product.list} />
<h1 className='text-lg font-light'>محصول جدید</h1>
</div>
<Button <Button
className='w-fit px-6' className='w-fit px-6'
onClick={() => formik.handleSubmit()} onClick={() => formik.handleSubmit()}
+4
View File
@@ -152,6 +152,10 @@ const ProductList: FC = () => {
) )
} }
}, },
{
key: 'order',
title: 'ترتیب',
},
{ {
key: 'actions', key: 'actions',
title: '', title: '',
+55 -49
View File
@@ -3,39 +3,45 @@ import { useFormik } from 'formik'
import { useState, type FC } from 'react' import { useState, type FC } from 'react'
import type { CreateCategoryType } from '../types/Types' import type { CreateCategoryType } from '../types/Types'
import * as Yup from 'yup' import * as Yup from 'yup'
import { useCreateCategory, useGetCategory } from '../hooks/useProductData' import { useCreateCategory } from '../hooks/useProductData'
import Select from '@/components/Select'
import Button from '@/components/Button' import Button from '@/components/Button'
import { AddSquare } from 'iconsax-react' import { AddSquare, TickCircle } from 'iconsax-react'
import UploadBox from '@/components/UploadBox' import UploadBox from '@/components/UploadBox'
import SwitchComponent from '@/components/Switch' import SwitchComponent from '@/components/Switch'
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader' import { useSingleUpload } from '@/pages/uploader/hooks/useUploader'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/config/func' import { extractErrorMessage } from '@/config/func'
import { useNavigate } from 'react-router-dom' import DefaulModal from '@/components/DefaulModal'
import { Paths } from '@/config/Paths'
import BackButton from '@/components/BackButton'
const ProductCategory: FC = () => { type Props = {
parentId?: string
refetch: () => void
}
const navigate = useNavigate() const CreateCategory: FC<Props> = ({ parentId, refetch }) => {
const { data } = useGetCategory()
const { mutate, isPending } = useCreateCategory() const { mutate, isPending } = useCreateCategory()
const { mutate: upload, isPending: isUploading } = useSingleUpload() const { mutate: upload, isPending: isUploading } = useSingleUpload()
const [showModal, setShowModal] = useState(false)
const [file, setFile] = useState<File>() const [file, setFile] = useState<File>()
const handleClose = () => {
setShowModal(false)
setFile(undefined)
formik.resetForm()
}
const handleSave = (values: CreateCategoryType) => { const handleSave = (values: CreateCategoryType) => {
const payload: CreateCategoryType = { const payload: CreateCategoryType = {
...values, ...values,
parentId: values.parentId || undefined, parentId: parentId || undefined,
} }
mutate(payload, { mutate(payload, {
onSuccess: () => { onSuccess: () => {
toast.success('با موفقیت ذخیره شد') toast.success('با موفقیت ذخیره شد')
navigate(Paths.product.category.list) refetch()
handleClose()
}, },
onError: (error) => { onError: (error) => {
toast.error(extractErrorMessage(error)) toast.error(extractErrorMessage(error))
@@ -69,55 +75,39 @@ const ProductCategory: FC = () => {
}) })
return ( return (
<div className='mt-5'> <div>
<div className='flex justify-between items-center'> <Button
<div className='flex items-center gap-4'> className='w-fit px-6'
<BackButton to={Paths.product.category.list} /> onClick={() => setShowModal(true)}
<h1 className='text-lg font-light'>دسته جدید</h1> >
<div className='flex gap-1.5'>
<AddSquare size={18} color='black' />
<div className='text-[13px] font-light'>دستهبندی جدید</div>
</div> </div>
<Button </Button>
className='w-fit px-6'
isLoading={isPending || isUploading}
onClick={() => formik.handleSubmit()}
>
<div className='flex gap-1.5'>
<AddSquare size={18} color='black' />
<div className='text-[13px] font-light'>ساخت دسته</div>
</div>
</Button>
</div>
<div className='mt-8 bg-white p-6 rounded-3xl '>
<div> <DefaulModal
open={showModal}
close={handleClose}
isHeader
title_header='دسته جدید'
>
<div className='mt-5'>
<UploadBox <UploadBox
label='تصویر دسته (اختیاری)' label='تصویر دسته (اختیاری)'
onChange={(files) => setFile(files[0])} onChange={(files) => setFile(files[0])}
/> />
</div> </div>
<div className='rowTwoInput mt-6'> <div className='mt-5'>
<Input <Input
label='عنوان' label='عنوان'
{...formik.getFieldProps('title')} {...formik.getFieldProps('title')}
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''} error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
/> />
<Select
label='انتخاب والد درسته'
placeholder='اختیاری'
items={data?.data?.map((item) => {
return {
label: item.title,
value: item.id + ''
}
}) || []}
onChange={formik.handleChange}
name='parentId'
value={formik.values.parentId || ''}
/>
</div> </div>
<div className='mt-6'> <div className='mt-5'>
<Input <Input
label='ترتیب اولویت' label='ترتیب اولویت'
{...formik.getFieldProps('order')} {...formik.getFieldProps('order')}
@@ -125,15 +115,31 @@ const ProductCategory: FC = () => {
/> />
</div> </div>
<div className='mt-6'> <div className='mt-5'>
<SwitchComponent <SwitchComponent
active={formik.values.isActive} active={formik.values.isActive}
onChange={(value) => formik.setFieldValue('isActive', value)} onChange={(value) => formik.setFieldValue('isActive', value)}
/> />
</div> </div>
</div>
<div className='mt-6 flex justify-end'>
<Button
className='w-fit px-6'
isLoading={isPending || isUploading}
onClick={() => formik.handleSubmit()}
>
<div className='flex gap-2 items-center'>
<TickCircle
size={18}
color='black'
/>
<div>ذخیره</div>
</div>
</Button>
</div>
</DefaulModal>
</div> </div>
) )
} }
export default ProductCategory export default CreateCategory
+50 -28
View File
@@ -1,24 +1,34 @@
import { type FC, useMemo } from 'react' import { type FC, useMemo } from 'react'
import PresignedImage from '@/components/PresignedImage' import PresignedImage from '@/components/PresignedImage'
import { useDeleteCategory, useGetCategory } from '../hooks/useProductData' import { useDeleteCategory, useGetCategory, useGetCategoryDetail } from '../hooks/useProductData'
import Table from '@/components/Table' import Table from '@/components/Table'
import type { ColumnType, RowDataType } from '@/components/types/TableTypes' import type { ColumnType, RowDataType } from '@/components/types/TableTypes'
import type { CategoryType } from '../types/Types' import type { CategoryType } from '../types/Types'
import Button from '@/components/Button' import { ArrowLeft2, Edit } from 'iconsax-react'
import { AddSquare, Edit } from 'iconsax-react'
import TrashWithConfrim from '@/components/TrashWithConfrim' import TrashWithConfrim from '@/components/TrashWithConfrim'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/config/func' import { extractErrorMessage } from '@/config/func'
import { Link } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import { Paths } from '@/config/Paths' import { Paths } from '@/config/Paths'
import BackButton from '@/components/BackButton' import BackButton from '@/components/BackButton'
import CreateCategory from './Create'
const CategoryList: FC = () => { const CategoryList: FC = () => {
const { parentId } = useParams<{ parentId?: string }>()
const { data, refetch } = useGetCategory() const { data, refetch } = useGetCategory({
limit: 1000,
parentId: parentId ?? 'null',
})
const { data: parentDetail } = useGetCategoryDetail(parentId ?? '')
const { mutate, isPending } = useDeleteCategory() const { mutate, isPending } = useDeleteCategory()
const categories = (data?.data ?? []) as (CategoryType & RowDataType)[] const categories = (data?.data ?? []) as (CategoryType & RowDataType)[]
const parentCategory = parentDetail?.data
const backTo = parentCategory?.parent?.id
? Paths.product.category.children + parentCategory.parent.id
: Paths.product.category.list
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
mutate(id, { mutate(id, {
onSuccess: () => { onSuccess: () => {
@@ -39,19 +49,34 @@ const CategoryList: FC = () => {
{ {
title: 'عنوان', title: 'عنوان',
key: 'title', key: 'title',
render: (item) => {
return (
<Link
to={Paths.product.category.children + item.id}
className='inline-flex items-center gap-1.5 text-sm text-black hover:opacity-70'
>
<span>{item.title}</span>
<ArrowLeft2 size={16} color='#8C90A3' />
</Link>
)
}
}, },
{ {
title: 'والد', title: 'زیر دسته‌ها',
key: 'parent',
render: (item) => item.parent?.title || '-',
},
{
title: 'تعداد زیر دسته',
key: 'children', key: 'children',
render: (item) => <div>{item.children?.length || 0}</div> render: (item) => {
if (!item.children?.length) return '0'
return (
<div className='flex flex-col gap-0.5'>
{item.children.map((child) => (
<span key={child.id}>{child.title}</span>
))}
</div>
)
}
}, },
{ {
title: 'اولویت', title: 'ترتیب',
key: 'order', key: 'order',
render: (item) => item.order ?? '-', render: (item) => item.order ?? '-',
}, },
@@ -75,33 +100,30 @@ const CategoryList: FC = () => {
} }
], [isPending]) ], [isPending])
const title = parentCategory
? `زیر دسته‌های «${parentCategory.title}»`
: 'دسته بندی محصولات'
return ( return (
<div className='mt-5'> <div className='mt-5'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<BackButton to={Paths.product.list} /> <BackButton to={parentId ? backTo : Paths.product.list} />
<h1 className='text-lg font-light'>دسته بندی محصولات</h1> <h1 className='text-lg font-light'>{title}</h1>
</div> </div>
<Link to={Paths.product.category.create}> <CreateCategory parentId={parentId} refetch={refetch} />
<Button className='w-fit px-6'>
<div className='flex gap-1.5'>
<AddSquare size={18} color='black' />
<div className='text-[13px] font-light'>دستهبندی جدید</div>
</div>
</Button>
</Link>
</div> </div>
<div className='mt-8'> <div className='mt-8'>
<Table<CategoryType & RowDataType> <Table<CategoryType & RowDataType>
columns={columns} columns={columns}
data={categories} data={categories}
/> />
</div> </div>
</div> </div>
) )
} }
export default CategoryList export default CategoryList
+3 -2
View File
@@ -16,10 +16,11 @@ export const useGetCategory = (params?: api.GetCategoryParams) => {
const page = params?.page ?? 1; const page = params?.page ?? 1;
const limit = params?.limit; const limit = params?.limit;
const search = params?.search; const search = params?.search;
const parentId = params?.parentId;
return useQuery({ return useQuery({
queryKey: ["category", page, limit, search], queryKey: ["category", page, limit, search, parentId],
queryFn: () => api.getCategory({ page, limit, search }), queryFn: () => api.getCategory({ page, limit, search, parentId }),
}); });
}; };
@@ -5,6 +5,7 @@ export type GetCategoryParams = {
page?: number; page?: number;
limit?: number; limit?: number;
search?: string; search?: string;
parentId?: string;
}; };
export const getCategory = async (params?: GetCategoryParams) => { export const getCategory = async (params?: GetCategoryParams) => {
@@ -13,6 +14,7 @@ export const getCategory = async (params?: GetCategoryParams) => {
page: params?.page, page: params?.page,
limit: params?.limit, limit: params?.limit,
search: params?.search || undefined, search: params?.search || undefined,
parentId: params?.parentId || undefined,
}, },
}); });
return data; return data;
+1 -2
View File
@@ -28,7 +28,6 @@ import LearningList from "@/pages/learning/List";
import LearningCreate from "@/pages/learning/Create"; import LearningCreate from "@/pages/learning/Create";
import LearningCategory from "@/pages/learning/Category"; import LearningCategory from "@/pages/learning/Category";
import CategoryList from "@/pages/product/category/List"; import CategoryList from "@/pages/product/category/List";
import CreateCategory from "@/pages/product/category/Create";
import UpdateCategory from "@/pages/product/category/Update"; import UpdateCategory from "@/pages/product/category/Update";
import UpdateProduct from "@/pages/product/Update"; import UpdateProduct from "@/pages/product/Update";
import CreateAttribute from "@/pages/product/attribute/Create"; import CreateAttribute from "@/pages/product/attribute/Create";
@@ -108,7 +107,7 @@ const MainRouter: FC = () => {
<Route path={Paths.product.attribute.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateAttribute />)} /> <Route path={Paths.product.attribute.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateAttribute />)} />
<Route path={Paths.product.attributeValue.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeValues />)} /> <Route path={Paths.product.attributeValue.list + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <AttributeValues />)} />
<Route path={Paths.product.category.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} /> <Route path={Paths.product.category.list} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
<Route path={Paths.product.category.create} element={withPermission(Permissions.MANAGE_PRODUCTS, <CreateCategory />)} /> <Route path={Paths.product.category.children + ":parentId"} element={withPermission(Permissions.MANAGE_PRODUCTS, <CategoryList />)} />
<Route path={Paths.product.category.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateCategory />)} /> <Route path={Paths.product.category.update + ":id"} element={withPermission(Permissions.MANAGE_PRODUCTS, <UpdateCategory />)} />
<Route path={Paths.formBuilder.list + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderList />)} /> <Route path={Paths.formBuilder.list + ":id/:type"} element={withPermission(Permissions.MANAGE_PRODUCTS, <FormBuilderList />)} />