From bcb52961a2b7e3cfb6ad710b2eb2f073c14294bf Mon Sep 17 00:00:00 2001 From: hosseintaromi Date: Mon, 29 Sep 2025 08:02:57 +0330 Subject: [PATCH] feat(discount-codes): add product and category search functionality in DiscountCodeFormPage --- src/pages/categories/core/_hooks.ts | 9 ++ src/pages/discount-codes/core/_models.ts | 4 + .../DiscountCodeFormPage.tsx | 135 +++++++++++++++++- src/pages/products/core/_hooks.ts | 9 ++ src/utils/query-key.ts | 2 + 5 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/pages/categories/core/_hooks.ts b/src/pages/categories/core/_hooks.ts index 0897711..554222d 100644 --- a/src/pages/categories/core/_hooks.ts +++ b/src/pages/categories/core/_hooks.ts @@ -22,6 +22,15 @@ export const useCategories = (filters?: CategoryFilters) => { }); }; +export const useSearchCategories = (filters: CategoryFilters) => { + return useQuery({ + queryKey: [QUERY_KEYS.SEARCH_CATEGORIES, filters], + queryFn: () => getCategories(filters), + enabled: Object.keys(filters).length > 0, + staleTime: 2 * 60 * 1000, // 2 minutes for search results + }); +}; + export const useCategory = (id: string, enabled: boolean = true) => { return useQuery({ queryKey: [QUERY_KEYS.GET_CATEGORY, id], diff --git a/src/pages/discount-codes/core/_models.ts b/src/pages/discount-codes/core/_models.ts index 92fbafe..0584f25 100644 --- a/src/pages/discount-codes/core/_models.ts +++ b/src/pages/discount-codes/core/_models.ts @@ -53,6 +53,8 @@ export interface DiscountCode { user_restrictions?: DiscountUserRestrictions; stepped_discount?: SteppedDiscount; meta?: DiscountMeta; + specific_product_ids?: number[]; + specific_category_ids?: number[]; created_at?: string; updated_at?: string; } @@ -85,6 +87,8 @@ export interface CreateDiscountCodeRequest { user_restrictions?: DiscountUserRestrictions; stepped_discount?: SteppedDiscount; meta?: DiscountMeta; + specific_product_ids?: number[]; + specific_category_ids?: number[]; } export interface UpdateDiscountCodeRequest diff --git a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx index b8a59fd..d694d82 100644 --- a/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx +++ b/src/pages/discount-codes/discount-code-form/DiscountCodeFormPage.tsx @@ -13,6 +13,8 @@ import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAuto import { FormHeader, PageContainer, Label, SectionTitle } from '../../../components/ui/Typography'; import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react'; import { useUsers, useSearchUsers } from '../../users-admin/core/_hooks'; +import { useSearchProducts } from '../../products/core/_hooks'; +import { useSearchCategories } from '../../categories/core/_hooks'; const schema = yup.object({ code: yup.string().min(3, 'کد باید حداقل ۳ کاراکتر باشد').max(50, 'کد نباید بیشتر از ۵۰ کاراکتر باشد').required('کد الزامی است'), @@ -73,6 +75,8 @@ const DiscountCodeFormPage = () => { const isEdit = !!id; const [selectedUserIds, setSelectedUserIds] = useState([]); + const [selectedProductIds, setSelectedProductIds] = useState([]); + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); const { data: dc, isLoading: dcLoading } = useDiscountCode(id || ''); const { mutate: create, isPending: creating } = useCreateDiscountCode(); @@ -86,6 +90,30 @@ const DiscountCodeFormPage = () => { const { data: searchResult, isLoading: usersLoading } = useSearchUsers({ limit: USERS_PAGE_SIZE, offset: userOffset, search_text: userSearch }); + // Products list with pagination and search for dropdown + const [productSearch, setProductSearch] = useState(''); + const [productPage, setProductPage] = useState(1); + const [accumulatedProducts, setAccumulatedProducts] = useState([]); + const PRODUCTS_PAGE_SIZE = 20; + + const { data: productSearchResult, isLoading: productsLoading } = useSearchProducts({ + search: productSearch, + page: productPage, + limit: PRODUCTS_PAGE_SIZE + }); + + // Categories list with pagination and search for dropdown + const [categorySearch, setCategorySearch] = useState(''); + const [categoryPage, setCategoryPage] = useState(1); + const [accumulatedCategories, setAccumulatedCategories] = useState([]); + const CATEGORIES_PAGE_SIZE = 20; + + const { data: categorySearchResult, isLoading: categoriesLoading } = useSearchCategories({ + search: categorySearch, + page: categoryPage, + limit: CATEGORIES_PAGE_SIZE + }); + useEffect(() => { if (searchResult?.users) { setAccumulatedUsers(prev => { @@ -98,6 +126,30 @@ const DiscountCodeFormPage = () => { } }, [searchResult, userOffset]); + useEffect(() => { + if (productSearchResult?.products) { + setAccumulatedProducts(prev => { + // If page is 1 (new search), replace; otherwise append unique + if (productPage === 1) return productSearchResult.products; + const byId = new Map(prev.map((p: any) => [p.id, p])); + for (const p of productSearchResult.products) byId.set(p.id, p); + return Array.from(byId.values()); + }); + } + }, [productSearchResult, productPage]); + + useEffect(() => { + if (categorySearchResult) { + setAccumulatedCategories(prev => { + // If page is 1 (new search), replace; otherwise append unique + if (categoryPage === 1) return categorySearchResult; + const byId = new Map(prev.map((c: any) => [c.id, c])); + for (const c of categorySearchResult) byId.set(c.id, c); + return Array.from(byId.values()); + }); + } + }, [categorySearchResult, categoryPage]); + // Convert users to options const userOptions: Option[] = (accumulatedUsers || []).map((user: any) => ({ id: user.id, @@ -107,12 +159,28 @@ const DiscountCodeFormPage = () => { description: user.phone_number })); - const { register, handleSubmit, formState: { errors, isValid }, reset } = useForm({ + // Convert products to options + const productOptions: Option[] = (accumulatedProducts || []).map((product: any) => ({ + id: product.id, + title: product.name, + description: product.description || `محصول #${product.id}` + })); + + // Convert categories to options + const categoryOptions: Option[] = (accumulatedCategories || []).map((category: any) => ({ + id: category.id, + title: category.name, + description: category.description || `دسته‌بندی #${category.id}` + })); + + const { register, handleSubmit, formState: { errors, isValid }, reset, watch } = useForm({ resolver: yupResolver(schema as any), mode: 'onChange', defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false } }); + const applicationLevel = watch('application_level'); + useEffect(() => { if (isEdit && dc) { reset({ @@ -138,6 +206,16 @@ const DiscountCodeFormPage = () => { if (dc.user_restrictions?.user_ids) { setSelectedUserIds(dc.user_restrictions.user_ids); } + + // Set selected product IDs + if (dc.specific_product_ids) { + setSelectedProductIds(dc.specific_product_ids); + } + + // Set selected category IDs + if (dc.specific_category_ids) { + setSelectedCategoryIds(dc.specific_category_ids); + } } }, [isEdit, dc, reset]); @@ -153,6 +231,8 @@ const DiscountCodeFormPage = () => { ...cleanRestrictions, user_ids: selectedUserIds.length > 0 ? selectedUserIds : undefined, }, + specific_product_ids: selectedProductIds.length > 0 ? selectedProductIds : undefined, + specific_category_ids: selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined, }; if (isEdit && id) { @@ -295,6 +375,59 @@ const DiscountCodeFormPage = () => { {errors.application_level &&

{errors.application_level.message as string}

} + + {/* Conditional Product Selection */} + {applicationLevel === 'product' && ( +
+ { setProductSearch(q); setProductPage(1); }} + onLoadMore={() => { + if (!productsLoading && productSearchResult && (productSearchResult.total > accumulatedProducts.length)) { + setProductPage(prev => prev + 1); + } + }} + hasMore={!!productSearchResult && accumulatedProducts.length < (productSearchResult.total || 0)} + loadingMore={productsLoading && productPage > 1} + /> +

+ در صورت انتخاب محصولات، تخفیف فقط روی این محصولات اعمال خواهد شد. +

+
+ )} + + {/* Conditional Category Selection */} + {applicationLevel === 'category' && ( +
+ { setCategorySearch(q); setCategoryPage(1); }} + onLoadMore={() => { + if (!categoriesLoading && categorySearchResult && (categorySearchResult.length > 0)) { + setCategoryPage(prev => prev + 1); + } + }} + hasMore={!!categorySearchResult && categorySearchResult.length >= CATEGORIES_PAGE_SIZE} + loadingMore={categoriesLoading && categoryPage > 1} + /> +

+ در صورت انتخاب دسته‌بندی‌ها، تخفیف فقط روی محصولات این دسته‌ها اعمال خواهد شد. +

+
+ )} + { }); }; +export const useSearchProducts = (filters: ProductFilters) => { + return useQuery({ + queryKey: [QUERY_KEYS.SEARCH_PRODUCTS, filters], + queryFn: () => getProducts(filters), + enabled: Object.keys(filters).length > 0, + staleTime: 2 * 60 * 1000, // 2 minutes for search results + }); +}; + export const useProduct = (id: string, enabled: boolean = true) => { return useQuery({ queryKey: [QUERY_KEYS.GET_PRODUCT, id], diff --git a/src/utils/query-key.ts b/src/utils/query-key.ts index 539d0a0..9a9b023 100644 --- a/src/utils/query-key.ts +++ b/src/utils/query-key.ts @@ -40,6 +40,7 @@ export const QUERY_KEYS = { // Categories GET_CATEGORIES: "get_categories", GET_CATEGORY: "get_category", + SEARCH_CATEGORIES: "search_categories", CREATE_CATEGORY: "create_category", UPDATE_CATEGORY: "update_category", DELETE_CATEGORY: "delete_category", @@ -47,6 +48,7 @@ export const QUERY_KEYS = { // Products GET_PRODUCTS: "get_products", GET_PRODUCT: "get_product", + SEARCH_PRODUCTS: "search_products", CREATE_PRODUCT: "create_product", UPDATE_PRODUCT: "update_product", DELETE_PRODUCT: "delete_product",