feat(discount-codes): add product and category search functionality in DiscountCodeFormPage

This commit is contained in:
hosseintaromi 2025-09-29 08:02:57 +03:30
parent f7e0d7f508
commit bcb52961a2
5 changed files with 158 additions and 1 deletions

View File

@ -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) => { export const useCategory = (id: string, enabled: boolean = true) => {
return useQuery({ return useQuery({
queryKey: [QUERY_KEYS.GET_CATEGORY, id], queryKey: [QUERY_KEYS.GET_CATEGORY, id],

View File

@ -53,6 +53,8 @@ export interface DiscountCode {
user_restrictions?: DiscountUserRestrictions; user_restrictions?: DiscountUserRestrictions;
stepped_discount?: SteppedDiscount; stepped_discount?: SteppedDiscount;
meta?: DiscountMeta; meta?: DiscountMeta;
specific_product_ids?: number[];
specific_category_ids?: number[];
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@ -85,6 +87,8 @@ export interface CreateDiscountCodeRequest {
user_restrictions?: DiscountUserRestrictions; user_restrictions?: DiscountUserRestrictions;
stepped_discount?: SteppedDiscount; stepped_discount?: SteppedDiscount;
meta?: DiscountMeta; meta?: DiscountMeta;
specific_product_ids?: number[];
specific_category_ids?: number[];
} }
export interface UpdateDiscountCodeRequest export interface UpdateDiscountCodeRequest

View File

@ -13,6 +13,8 @@ import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAuto
import { FormHeader, PageContainer, Label, SectionTitle } from '../../../components/ui/Typography'; import { FormHeader, PageContainer, Label, SectionTitle } from '../../../components/ui/Typography';
import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react'; import { ArrowRight, BadgePercent, Calendar, Settings, Users, Tag, Info } from 'lucide-react';
import { useUsers, useSearchUsers } from '../../users-admin/core/_hooks'; import { useUsers, useSearchUsers } from '../../users-admin/core/_hooks';
import { useSearchProducts } from '../../products/core/_hooks';
import { useSearchCategories } from '../../categories/core/_hooks';
const schema = yup.object({ const schema = yup.object({
code: yup.string().min(3, 'کد باید حداقل ۳ کاراکتر باشد').max(50, 'کد نباید بیشتر از ۵۰ کاراکتر باشد').required('کد الزامی است'), code: yup.string().min(3, 'کد باید حداقل ۳ کاراکتر باشد').max(50, 'کد نباید بیشتر از ۵۰ کاراکتر باشد').required('کد الزامی است'),
@ -73,6 +75,8 @@ const DiscountCodeFormPage = () => {
const isEdit = !!id; const isEdit = !!id;
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]); const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]);
const [selectedCategoryIds, setSelectedCategoryIds] = useState<number[]>([]);
const { data: dc, isLoading: dcLoading } = useDiscountCode(id || ''); const { data: dc, isLoading: dcLoading } = useDiscountCode(id || '');
const { mutate: create, isPending: creating } = useCreateDiscountCode(); 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 }); 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<string>('');
const [productPage, setProductPage] = useState<number>(1);
const [accumulatedProducts, setAccumulatedProducts] = useState<any[]>([]);
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<string>('');
const [categoryPage, setCategoryPage] = useState<number>(1);
const [accumulatedCategories, setAccumulatedCategories] = useState<any[]>([]);
const CATEGORIES_PAGE_SIZE = 20;
const { data: categorySearchResult, isLoading: categoriesLoading } = useSearchCategories({
search: categorySearch,
page: categoryPage,
limit: CATEGORIES_PAGE_SIZE
});
useEffect(() => { useEffect(() => {
if (searchResult?.users) { if (searchResult?.users) {
setAccumulatedUsers(prev => { setAccumulatedUsers(prev => {
@ -98,6 +126,30 @@ const DiscountCodeFormPage = () => {
} }
}, [searchResult, userOffset]); }, [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 // Convert users to options
const userOptions: Option[] = (accumulatedUsers || []).map((user: any) => ({ const userOptions: Option[] = (accumulatedUsers || []).map((user: any) => ({
id: user.id, id: user.id,
@ -107,12 +159,28 @@ const DiscountCodeFormPage = () => {
description: user.phone_number description: user.phone_number
})); }));
const { register, handleSubmit, formState: { errors, isValid }, reset } = useForm<CreateDiscountCodeRequest>({ // 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<CreateDiscountCodeRequest>({
resolver: yupResolver(schema as any), resolver: yupResolver(schema as any),
mode: 'onChange', mode: 'onChange',
defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false } defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false }
}); });
const applicationLevel = watch('application_level');
useEffect(() => { useEffect(() => {
if (isEdit && dc) { if (isEdit && dc) {
reset({ reset({
@ -138,6 +206,16 @@ const DiscountCodeFormPage = () => {
if (dc.user_restrictions?.user_ids) { if (dc.user_restrictions?.user_ids) {
setSelectedUserIds(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]); }, [isEdit, dc, reset]);
@ -153,6 +231,8 @@ const DiscountCodeFormPage = () => {
...cleanRestrictions, ...cleanRestrictions,
user_ids: selectedUserIds.length > 0 ? selectedUserIds : undefined, 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) { if (isEdit && id) {
@ -295,6 +375,59 @@ const DiscountCodeFormPage = () => {
</select> </select>
{errors.application_level && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{errors.application_level.message as string}</p>} {errors.application_level && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{errors.application_level.message as string}</p>}
</div> </div>
{/* Conditional Product Selection */}
{applicationLevel === 'product' && (
<div className="lg:col-span-3">
<MultiSelectAutocomplete
label="انتخاب محصولات خاص"
options={productOptions}
selectedValues={selectedProductIds}
onChange={setSelectedProductIds}
placeholder="جستجو و انتخاب محصولات..."
isLoading={productsLoading && productPage === 1}
disabled={false}
onSearchChange={(q) => { 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}
/>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
در صورت انتخاب محصولات، تخفیف فقط روی این محصولات اعمال خواهد شد.
</p>
</div>
)}
{/* Conditional Category Selection */}
{applicationLevel === 'category' && (
<div className="lg:col-span-3">
<MultiSelectAutocomplete
label="انتخاب دسته‌بندی‌های خاص"
options={categoryOptions}
selectedValues={selectedCategoryIds}
onChange={setSelectedCategoryIds}
placeholder="جستجو و انتخاب دسته‌بندی‌ها..."
isLoading={categoriesLoading && categoryPage === 1}
disabled={false}
onSearchChange={(q) => { 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}
/>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
در صورت انتخاب دستهبندیها، تخفیف فقط روی محصولات این دستهها اعمال خواهد شد.
</p>
</div>
)}
<Input <Input
label="حداقل مبلغ خرید" label="حداقل مبلغ خرید"
type="number" type="number"

View File

@ -28,6 +28,15 @@ export const useProducts = (filters?: ProductFilters) => {
}); });
}; };
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) => { export const useProduct = (id: string, enabled: boolean = true) => {
return useQuery({ return useQuery({
queryKey: [QUERY_KEYS.GET_PRODUCT, id], queryKey: [QUERY_KEYS.GET_PRODUCT, id],

View File

@ -40,6 +40,7 @@ export const QUERY_KEYS = {
// Categories // Categories
GET_CATEGORIES: "get_categories", GET_CATEGORIES: "get_categories",
GET_CATEGORY: "get_category", GET_CATEGORY: "get_category",
SEARCH_CATEGORIES: "search_categories",
CREATE_CATEGORY: "create_category", CREATE_CATEGORY: "create_category",
UPDATE_CATEGORY: "update_category", UPDATE_CATEGORY: "update_category",
DELETE_CATEGORY: "delete_category", DELETE_CATEGORY: "delete_category",
@ -47,6 +48,7 @@ export const QUERY_KEYS = {
// Products // Products
GET_PRODUCTS: "get_products", GET_PRODUCTS: "get_products",
GET_PRODUCT: "get_product", GET_PRODUCT: "get_product",
SEARCH_PRODUCTS: "search_products",
CREATE_PRODUCT: "create_product", CREATE_PRODUCT: "create_product",
UPDATE_PRODUCT: "update_product", UPDATE_PRODUCT: "update_product",
DELETE_PRODUCT: "delete_product", DELETE_PRODUCT: "delete_product",