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