add category

This commit is contained in:
2026-07-12 14:31:35 +03:30
parent 7009bcc660
commit 40ae2a0fba
4 changed files with 137 additions and 33 deletions
@@ -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>
+64 -2
View File
@@ -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}`)}
+9 -4
View File
@@ -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,
}); });
}; };
+20 -3
View File
@@ -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`, {