add category
This commit is contained in:
@@ -13,6 +13,7 @@ type Props = {
|
||||
error_text?: string
|
||||
categories: CategoryTreeNode[]
|
||||
disabled?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
const CategorySelect: FC<Props> = ({
|
||||
@@ -22,7 +23,8 @@ const CategorySelect: FC<Props> = ({
|
||||
onChange,
|
||||
error_text,
|
||||
categories,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
compact = false
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
@@ -163,7 +165,10 @@ const CategorySelect: FC<Props> = ({
|
||||
return (
|
||||
<div className="w-full relative" ref={dropdownRef}>
|
||||
{label && (
|
||||
<label className="text-sm text-gray-700 mb-2 block font-medium">
|
||||
<label className={clx(
|
||||
'text-sm block',
|
||||
compact ? 'text-black' : 'text-gray-700 mb-2 font-medium'
|
||||
)}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
@@ -171,39 +176,54 @@ const CategorySelect: FC<Props> = ({
|
||||
{/* دکمه انتخاب */}
|
||||
<div
|
||||
className={clx(
|
||||
'w-full border border-gray-300 rounded-xl bg-white cursor-pointer transition-all duration-200',
|
||||
'hover:border-blue-400 hover:shadow-sm',
|
||||
isOpen && 'border-blue-500 ring-2 ring-blue-100',
|
||||
'w-full border bg-white cursor-pointer transition-all duration-200',
|
||||
compact
|
||||
? 'h-10 rounded-[10px] border-border hover:border-border'
|
||||
: 'border-gray-300 rounded-xl hover:border-blue-400 hover:shadow-sm',
|
||||
isOpen && (compact ? 'border-primary' : 'border-blue-500 ring-2 ring-blue-100'),
|
||||
disabled && 'opacity-50 cursor-not-allowed bg-gray-50',
|
||||
label && 'mt-1'
|
||||
)}
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className={clx(
|
||||
'flex items-center justify-between',
|
||||
compact ? 'h-full px-[10px]' : 'p-4'
|
||||
)}>
|
||||
<div className="flex-1 min-w-0">
|
||||
{selectedCategory ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{selectedCategory.children.length > 0 ? (
|
||||
<FolderOpen size={20} className="text-blue-500" />
|
||||
) : (
|
||||
<DocumentText size={20} className="text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{selectedCategory.title_fa}
|
||||
compact ? (
|
||||
<span className="text-sm text-black truncate block">
|
||||
{selectedCategory.title_fa}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{selectedCategory.children.length > 0 ? (
|
||||
<FolderOpen size={20} className="text-blue-500" />
|
||||
) : (
|
||||
<DocumentText size={20} className="text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{getCategoryPath(selectedCategory)}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{selectedCategory.title_fa}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{getCategoryPath(selectedCategory)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Folder2 size={20} className="text-gray-400" />
|
||||
<span className="text-gray-400">{placeholder}</span>
|
||||
</div>
|
||||
compact ? (
|
||||
<span className="text-sm text-gray-400">{placeholder}</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<Folder2 size={20} className="text-gray-400" />
|
||||
<span className="text-gray-400">{placeholder}</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { type FC, useState } from 'react'
|
||||
import { useDeleteProduct, useDraftProduct, useGetProducts } from './hooks/useProductData';
|
||||
import { type ProductListType } from './types/Types';
|
||||
import { ProductStatus } from './enum/ProductEnum';
|
||||
import CategorySelect from '../category/components/CategorySelect';
|
||||
import { useGetCategories } from '../category/hooks/useCategoryData';
|
||||
import Select from '@/components/Select';
|
||||
import PageLoading from '../../components/PageLoading';
|
||||
import Error from '../../components/Error';
|
||||
import PaginationUi from '../../components/PaginationUi';
|
||||
@@ -22,9 +25,16 @@ const List: FC = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [categoryId, setCategoryId] = useState('');
|
||||
const [status, setStatus] = useState<ProductStatus | ''>('');
|
||||
const [isDraftConfirmOpen, setIsDraftConfirmOpen] = useState(false);
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
const { data: productsData, isLoading, error, refetch } = useGetProducts(currentPage);
|
||||
const { data: categoriesData } = useGetCategories();
|
||||
const categories = categoriesData?.results?.data || [];
|
||||
const { data: productsData, isLoading, error, refetch } = useGetProducts(currentPage, 10, {
|
||||
...(categoryId && { categoryId }),
|
||||
...(status && { status }),
|
||||
});
|
||||
const deleteProductMutation = useDeleteProduct();
|
||||
const draftProductMutation = useDraftProduct();
|
||||
|
||||
@@ -68,10 +78,62 @@ const List: FC = () => {
|
||||
};
|
||||
|
||||
|
||||
const hasActiveFilters = !!categoryId || !!status;
|
||||
|
||||
const handleCategoryChange = (value: string) => {
|
||||
setCategoryId(value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleStatusChange = (value: string) => {
|
||||
setStatus(value as ProductStatus | '');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setCategoryId('');
|
||||
setStatus('');
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle />
|
||||
<div className='flex justify-end mt-5'>
|
||||
<div className='flex flex-wrap items-end justify-between gap-4 mt-5'>
|
||||
<div className='flex flex-wrap items-end gap-4'>
|
||||
<div className='w-72'>
|
||||
<CategorySelect
|
||||
label='دستهبندی'
|
||||
placeholder='همه دستهبندیها'
|
||||
value={categoryId}
|
||||
onChange={handleCategoryChange}
|
||||
categories={categories}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<div className='w-48'>
|
||||
<Select
|
||||
label='وضعیت'
|
||||
value={status}
|
||||
onChange={(e) => handleStatusChange(e.target.value)}
|
||||
items={[
|
||||
{ value: '', label: 'همه وضعیتها' },
|
||||
{ value: ProductStatus.Approved, label: 'تایید شده' },
|
||||
{ value: ProductStatus.Pending, label: 'در انتظار' },
|
||||
{ value: ProductStatus.Rejected, label: 'رد شده' },
|
||||
{ value: ProductStatus.Draft, label: 'پیشنویس' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
label='پاک کردن فیلترها'
|
||||
variant='outline'
|
||||
onClick={clearFilters}
|
||||
className='w-fit h-10'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
label='افزودن محصول'
|
||||
onClick={() => navigate(`${Pages.products.create}`)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type UpdateProductRequestType
|
||||
} from "../types/Types";
|
||||
import { useSharedStore } from '@/shared/store/sharedStore';
|
||||
import { type GetProductsFilters } from '../service/ProductService';
|
||||
|
||||
export const useCreateProductDetail = () => {
|
||||
return useMutation({
|
||||
@@ -29,11 +30,15 @@ export const useSaveProduct = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProducts = (page: number = 1, limit: number = 10) => {
|
||||
export const useGetProducts = (
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
filters?: Pick<GetProductsFilters, 'categoryId' | 'status'>
|
||||
) => {
|
||||
const { search } = useSharedStore();
|
||||
return useQuery({
|
||||
queryKey: ['products', page, limit, search],
|
||||
queryFn: () => api.getProducts(page, limit, search),
|
||||
queryKey: ['products', page, limit, search, filters?.categoryId, filters?.status],
|
||||
queryFn: () => api.getProducts(page, limit, { search, ...filters }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -94,7 +99,7 @@ export const useUpdateProduct = () => {
|
||||
export const useSearchProducts = (search: string, enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['products-search', search],
|
||||
queryFn: () => api.getProducts(1, 50, search),
|
||||
queryFn: () => api.getProducts(1, 50, { search }),
|
||||
enabled: enabled,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { ProductStatus } from "../enum/ProductEnum";
|
||||
import {
|
||||
type CreateProductDetailRequestType,
|
||||
type CreateProductDetailResponseType,
|
||||
@@ -24,6 +25,12 @@ import {
|
||||
type UpdateProductResponseType
|
||||
} from "../types/Types";
|
||||
|
||||
export type GetProductsFilters = {
|
||||
search?: string;
|
||||
categoryId?: string;
|
||||
status?: ProductStatus;
|
||||
};
|
||||
|
||||
export const createProductDetail = async (
|
||||
variables: CreateProductDetailRequestType
|
||||
): Promise<CreateProductDetailResponseType> => {
|
||||
@@ -45,10 +52,20 @@ export const saveProduct = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getProducts = async (page: number = 1, limit: number = 10, search?: string): Promise<GetProductsResponseType> => {
|
||||
export const getProducts = async (
|
||||
page: number = 1,
|
||||
limit: number = 10,
|
||||
filters?: GetProductsFilters
|
||||
): Promise<GetProductsResponseType> => {
|
||||
const params: Record<string, string | number> = { page, limit };
|
||||
if (search && search.trim()) {
|
||||
params.q = search.trim();
|
||||
if (filters?.search?.trim()) {
|
||||
params.q = filters.search.trim();
|
||||
}
|
||||
if (filters?.categoryId) {
|
||||
params.categoryId = filters.categoryId;
|
||||
}
|
||||
if (filters?.status) {
|
||||
params.status = filters.status;
|
||||
}
|
||||
|
||||
const { data } = await axios.get(`/admin/products/native`, {
|
||||
|
||||
Reference in New Issue
Block a user