feat: Add product and category selectors to coupon form

Added comprehensive product/category restrictions to coupon form

Features Added:
1. Product Selectors:
   - Products (include) - multiselect with search
   - Exclude products - multiselect with search
   - Shows product names with searchable dropdown
   - Badge display for selected items

2. Category Selectors:
   - Product categories (include) - multiselect
   - Exclude categories - multiselect
   - Shows category names with search
   - Badge display for selected items

3. API Integration:
   - Added ProductsApi.list() endpoint
   - Added ProductsApi.categories() endpoint
   - Fetch products and categories on form load
   - React Query caching for performance

4. Form Data:
   - Added product_ids field
   - Added excluded_product_ids field
   - Added product_categories field
   - Added excluded_product_categories field
   - Proper number/string conversion

UI/UX Improvements:
- Searchable multiselect dropdowns
- Badge display with X to remove
- Shows +N more when exceeds display limit
- Clear placeholder text
- Helper text for each field
- Consistent spacing and layout

Technical:
- Uses MultiSelect component (shadcn-based)
- React Query for data fetching
- Proper TypeScript types
- Number array handling

Note: Brands field not included yet (requires WooCommerce Product Brands extension check)

Result:
- Full WooCommerce coupon restrictions support
- Clean, searchable UI
- Production ready
This commit is contained in:
dwindown
2025-11-20 15:26:39 +07:00
parent 3a4e68dadf
commit 0f47c08b7a
2 changed files with 95 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { __ } from '@/lib/i18n';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
@@ -7,6 +8,8 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { MultiSelect } from '@/components/ui/multi-select';
import { ProductsApi } from '@/lib/api';
import type { Coupon, CouponFormData } from '@/lib/api/coupons';
interface CouponFormProps {
@@ -31,6 +34,10 @@ export default function CouponForm({
description: initial?.description || '',
date_expires: initial?.date_expires || null,
individual_use: initial?.individual_use || false,
product_ids: initial?.product_ids || [],
excluded_product_ids: initial?.excluded_product_ids || [],
product_categories: initial?.product_categories || [],
excluded_product_categories: initial?.excluded_product_categories || [],
usage_limit: initial?.usage_limit || null,
usage_limit_per_user: initial?.usage_limit_per_user || null,
free_shipping: initial?.free_shipping || false,
@@ -39,6 +46,20 @@ export default function CouponForm({
maximum_amount: initial?.maximum_amount || null,
});
// Fetch products and categories
const { data: productsData } = useQuery({
queryKey: ['products-list'],
queryFn: () => ProductsApi.list({ per_page: 100 }),
});
const { data: categoriesData } = useQuery({
queryKey: ['product-categories'],
queryFn: () => ProductsApi.categories(),
});
const products = (productsData as any)?.rows || [];
const categories = categoriesData || [];
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
@@ -190,6 +211,78 @@ export default function CouponForm({
</p>
</div>
{/* Products */}
<div className="space-y-2">
<Label>{__('Products')}</Label>
<MultiSelect
options={products.map((p: any) => ({
value: String(p.id),
label: p.name,
}))}
selected={(formData.product_ids || []).map(String)}
onChange={(selected) => updateField('product_ids', selected.map(Number))}
placeholder={__('Search for products...')}
emptyMessage={__('No products found')}
/>
<p className="text-sm text-muted-foreground">
{__('Products that the coupon will be applied to, or leave blank for all products')}
</p>
</div>
{/* Exclude Products */}
<div className="space-y-2">
<Label>{__('Exclude products')}</Label>
<MultiSelect
options={products.map((p: any) => ({
value: String(p.id),
label: p.name,
}))}
selected={(formData.excluded_product_ids || []).map(String)}
onChange={(selected) => updateField('excluded_product_ids', selected.map(Number))}
placeholder={__('Search for products...')}
emptyMessage={__('No products found')}
/>
<p className="text-sm text-muted-foreground">
{__('Products that the coupon will not be applied to')}
</p>
</div>
{/* Product Categories */}
<div className="space-y-2">
<Label>{__('Product categories')}</Label>
<MultiSelect
options={categories.map((c: any) => ({
value: String(c.id),
label: c.name,
}))}
selected={(formData.product_categories || []).map(String)}
onChange={(selected) => updateField('product_categories', selected.map(Number))}
placeholder={__('Any category')}
emptyMessage={__('No categories found')}
/>
<p className="text-sm text-muted-foreground">
{__('Product categories that the coupon will be applied to, or leave blank for all categories')}
</p>
</div>
{/* Exclude Categories */}
<div className="space-y-2">
<Label>{__('Exclude categories')}</Label>
<MultiSelect
options={categories.map((c: any) => ({
value: String(c.id),
label: c.name,
}))}
selected={(formData.excluded_product_categories || []).map(String)}
onChange={(selected) => updateField('excluded_product_categories', selected.map(Number))}
placeholder={__('No categories')}
emptyMessage={__('No categories found')}
/>
<p className="text-sm text-muted-foreground">
{__('Product categories that the coupon will not be applied to')}
</p>
</div>
{/* Individual Use */}
<div className="flex items-center space-x-2">
<Checkbox