+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>negareh-console</title>
|
<title>پنل مدیریت</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Copy } from 'iconsax-react'
|
||||||
|
import { type FC, useEffect, useState } from 'react'
|
||||||
|
import ModalConfrim from './ModalConfrim'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onDuplicate: () => void,
|
||||||
|
isloading?: boolean,
|
||||||
|
colorIcon?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyWithConfirm: FC<Props> = ({ onDuplicate, isloading = false, colorIcon }) => {
|
||||||
|
const [isConfirm, setIsConfirm] = useState(false)
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitting && !isloading) {
|
||||||
|
setIsConfirm(false)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}, [isloading, isSubmitting])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => setIsConfirm(true)}
|
||||||
|
disabled={isloading}
|
||||||
|
className='disabled:opacity-50'
|
||||||
|
>
|
||||||
|
<Copy size={20} color={colorIcon || '#0037FF'} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isOpen={isConfirm}
|
||||||
|
close={() => setIsConfirm(false)}
|
||||||
|
onConfrim={() => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
onDuplicate()
|
||||||
|
}}
|
||||||
|
isloading={isloading}
|
||||||
|
title_header='کپی محصول'
|
||||||
|
label='آیا از کپی این محصول مطمئن هستید؟'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyWithConfirm
|
||||||
@@ -9,7 +9,8 @@ type Props = {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
onConfrim: (text?: string) => void,
|
onConfrim: (text?: string) => void,
|
||||||
label?: string,
|
label?: string,
|
||||||
isHasDescription?: boolean
|
isHasDescription?: boolean,
|
||||||
|
title_header?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalConfrim: FC<Props> = (props: Props) => {
|
const ModalConfrim: FC<Props> = (props: Props) => {
|
||||||
@@ -20,7 +21,7 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
|||||||
<DefaulModal
|
<DefaulModal
|
||||||
open={props.isOpen}
|
open={props.isOpen}
|
||||||
close={props.close}
|
close={props.close}
|
||||||
title_header={'حذف'}
|
title_header={props.title_header ?? 'حذف'}
|
||||||
isHeader
|
isHeader
|
||||||
>
|
>
|
||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import Button from '@/components/Button'
|
|||||||
import Filters from '@/components/Filters'
|
import Filters from '@/components/Filters'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import { AddSquare, Edit, More2 } from 'iconsax-react'
|
import { AddSquare, Edit, More2 } from 'iconsax-react'
|
||||||
import { type FC } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
import { useDeleteProduct, useGetProducts } from './hooks/useProductData'
|
import { useDeleteProduct, useDuplicateProduct, useGetProducts } from './hooks/useProductData'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||||
|
import CopyWithConfirm from '@/components/CopyWithConfirm'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
|
||||||
@@ -14,6 +15,23 @@ const ProductList: FC = () => {
|
|||||||
|
|
||||||
const { data: products, refetch } = useGetProducts()
|
const { data: products, refetch } = useGetProducts()
|
||||||
const { mutate: deleteProduct, isPending: isDeleting } = useDeleteProduct()
|
const { mutate: deleteProduct, isPending: isDeleting } = useDeleteProduct()
|
||||||
|
const { mutate: duplicateProduct, isPending: isDuplicating } = useDuplicateProduct()
|
||||||
|
const [duplicatingId, setDuplicatingId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleDuplicate = (id: string) => {
|
||||||
|
setDuplicatingId(id)
|
||||||
|
duplicateProduct(id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
toast.success('محصول با موفقیت کپی شد')
|
||||||
|
setDuplicatingId(null)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
setDuplicatingId(null)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = (id: string) => {
|
||||||
deleteProduct(id, {
|
deleteProduct(id, {
|
||||||
@@ -97,6 +115,10 @@ const ProductList: FC = () => {
|
|||||||
render: (item) => {
|
render: (item) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
|
<CopyWithConfirm
|
||||||
|
onDuplicate={() => handleDuplicate(item.id)}
|
||||||
|
isloading={isDuplicating && duplicatingId === item.id}
|
||||||
|
/>
|
||||||
<Link to={Paths.product.update + item.id}>
|
<Link to={Paths.product.update + item.id}>
|
||||||
<Edit size={20} color='#0037FF' />
|
<Edit size={20} color='#0037FF' />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ 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 { Paths } from '@/config/Paths'
|
||||||
|
import BackButton from '@/components/BackButton'
|
||||||
|
|
||||||
const ProductCategory: FC = () => {
|
const ProductCategory: FC = () => {
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
const { data } = useGetCategory()
|
const { data } = useGetCategory()
|
||||||
const { mutate, isPending } = useCreateCategory()
|
const { mutate, isPending } = useCreateCategory()
|
||||||
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
const { mutate: upload, isPending: isUploading } = useSingleUpload()
|
||||||
@@ -29,6 +33,7 @@ const ProductCategory: FC = () => {
|
|||||||
mutate(values, {
|
mutate(values, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('با موفقیت ذخیره شد')
|
toast.success('با موفقیت ذخیره شد')
|
||||||
|
navigate(Paths.product.category.list)
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(extractErrorMessage(error))
|
toast.error(extractErrorMessage(error))
|
||||||
@@ -64,8 +69,10 @@ const ProductCategory: 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'>
|
||||||
|
<BackButton to={Paths.product.category.list} />
|
||||||
<h1 className='text-lg font-light'>دسته جدید</h1>
|
<h1 className='text-lg font-light'>دسته جدید</h1>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
isLoading={isPending || isUploading}
|
isLoading={isPending || isUploading}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { toast } from 'react-toastify'
|
|||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
|
import BackButton from '@/components/BackButton'
|
||||||
|
|
||||||
const CategoryList: FC = () => {
|
const CategoryList: FC = () => {
|
||||||
|
|
||||||
@@ -82,7 +83,10 @@ const CategoryList: 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'>
|
||||||
|
<BackButton to={Paths.product.list} />
|
||||||
<h1 className='text-lg font-light'>دسته بندی محصولات</h1>
|
<h1 className='text-lg font-light'>دسته بندی محصولات</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link to={Paths.product.category.create}>
|
<Link to={Paths.product.category.create}>
|
||||||
<Button className='w-fit px-6'>
|
<Button className='w-fit px-6'>
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ export const useGetCategory = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateCategory = () => {
|
export const useCreateCategory = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: api.createCategory,
|
mutationFn: api.createCategory,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["category"],
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,6 +98,18 @@ export const useDeleteProduct = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDuplicateProduct = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => api.duplicateProduct(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["products"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreateAttribute = () => {
|
export const useCreateAttribute = () => {
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ export const deleteProduct = async (id: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const duplicateProduct = async (id: string) => {
|
||||||
|
const { data } = await axios.post(`/admin/products/${id}/duplicate`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const createAttribute = async (id: number, params:CreateAttributeType) => {
|
export const createAttribute = async (id: number, params:CreateAttributeType) => {
|
||||||
const { data } = await axios.post(`/admin/product/${id}/attribute`, params);
|
const { data } = await axios.post(`/admin/product/${id}/attribute`, params);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user