feat(discount-codes): add product and category search functionality in DiscountCodeFormPage
This commit is contained in:
parent
f7e0d7f508
commit
bcb52961a2
|
|
@ -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],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue