fix: Vertical tabs visibility and add mobile search/filter

Fixed 3 critical issues:

1. Fixed Vertical Tabs - Cards All Showing
   - Updated VerticalTabForm to hide inactive sections
   - Only active section visible (className: hidden for others)
   - Proper tab switching now works

2. Added Mobile Search/Filter to Coupons
   - Created CouponFilterSheet component
   - Added mobile search bar with icon
   - Filter button with active count badge
   - Matches Products pattern exactly
   - Sheet with Apply/Reset buttons

3. Removed max-height from VerticalTabForm
   - User removed max-h-[calc(100vh-200px)]
   - Content now flows naturally
   - Better for forms with varying heights

Components Created:
- CouponFilterSheet.tsx - Mobile filter bottom sheet
  - Discount type filter
  - Apply/Reset actions
  - Active filter count

Changes to Coupons/index.tsx:
- Added mobile search bar (md:hidden)
- Added filter sheet state
- Added activeFiltersCount
- Search icon + SlidersHorizontal icon
- Filter badge indicator

Changes to VerticalTabForm:
- Hide inactive sections (className: hidden)
- Only show section matching activeTab
- Proper visibility control

Result:
 Vertical tabs work correctly (only one section visible)
 Mobile search/filter on Coupons (like Products)
 Filter count badge
 Professional mobile UX

Next: Move customer site member checkbox to settings
This commit is contained in:
dwindown
2025-11-20 20:32:46 +07:00
parent be671b66ec
commit e8ca3ceeb2
5 changed files with 186 additions and 58 deletions

View File

@@ -92,13 +92,15 @@ export function VerticalTabForm({ tabs, children, className }: VerticalTabFormPr
{/* Content Area */} {/* Content Area */}
<div <div
ref={contentRef} ref={contentRef}
className="flex-1 overflow-y-auto max-h-[calc(100vh-200px)] pr-2" className="flex-1 overflow-y-auto pr-2"
> >
{React.Children.map(children, (child) => { {React.Children.map(children, (child) => {
if (React.isValidElement(child) && child.props['data-section-id']) { if (React.isValidElement(child) && child.props['data-section-id']) {
const sectionId = child.props['data-section-id']; const sectionId = child.props['data-section-id'];
const isActive = sectionId === activeTab;
return React.cloneElement(child as React.ReactElement<any>, { return React.cloneElement(child as React.ReactElement<any>, {
ref: (el: HTMLElement) => registerSection(sectionId, el), ref: (el: HTMLElement) => registerSection(sectionId, el),
className: isActive ? '' : 'hidden',
}); });
} }
return child; return child;

View File

@@ -89,7 +89,7 @@ export default function CouponEdit() {
} }
return ( return (
<div className="max-w-4xl"> <div>
<CouponForm <CouponForm
mode="edit" mode="edit"
initial={coupon} initial={coupon}

View File

@@ -53,7 +53,7 @@ export default function CouponNew() {
}, [createMutation.isPending, setPageHeader, clearPageHeader, navigate]); }, [createMutation.isPending, setPageHeader, clearPageHeader, navigate]);
return ( return (
<div className="max-w-4xl"> <div>
<CouponForm <CouponForm
mode="create" mode="create"
onSubmit={async (data) => { onSubmit={async (data) => {

View File

@@ -0,0 +1,80 @@
import React, { useState } from 'react';
import { __ } from '@/lib/i18n';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
interface CouponFilterSheetProps {
open: boolean;
onClose: () => void;
filters: {
discount_type: string;
};
onFiltersChange: (filters: { discount_type: string }) => void;
onReset: () => void;
}
export function CouponFilterSheet({
open,
onClose,
filters,
onFiltersChange,
onReset,
}: CouponFilterSheetProps) {
const [localFilters, setLocalFilters] = useState(filters);
const handleApply = () => {
onFiltersChange(localFilters);
onClose();
};
const handleReset = () => {
setLocalFilters({ discount_type: '' });
onReset();
onClose();
};
return (
<Sheet open={open} onOpenChange={onClose}>
<SheetContent side="bottom" className="h-[400px]">
<SheetHeader>
<SheetTitle>{__('Filter Coupons')}</SheetTitle>
</SheetHeader>
<div className="mt-6 space-y-6">
{/* Discount Type */}
<div className="space-y-2">
<Label>{__('Discount Type')}</Label>
<Select
value={localFilters.discount_type || 'all'}
onValueChange={(value) =>
setLocalFilters({ ...localFilters, discount_type: value === 'all' ? '' : value })
}
>
<SelectTrigger>
<SelectValue placeholder={__('All types')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{__('All types')}</SelectItem>
<SelectItem value="percent">{__('Percentage')}</SelectItem>
<SelectItem value="fixed_cart">{__('Fixed Cart')}</SelectItem>
<SelectItem value="fixed_product">{__('Fixed Product')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Actions */}
<div className="absolute bottom-0 left-0 right-0 p-4 border-t bg-background flex gap-3">
<Button variant="outline" onClick={handleReset} className="flex-1">
{__('Reset')}
</Button>
<Button onClick={handleApply} className="flex-1">
{__('Apply Filters')}
</Button>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -11,8 +11,9 @@ import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Trash2, RefreshCw, Edit, Tag } from 'lucide-react'; import { Trash2, RefreshCw, Edit, Tag, Search, SlidersHorizontal } from 'lucide-react';
import { useFABConfig } from '@/hooks/useFABConfig'; import { useFABConfig } from '@/hooks/useFABConfig';
import { CouponFilterSheet } from './components/CouponFilterSheet';
export default function CouponsIndex() { export default function CouponsIndex() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -21,9 +22,13 @@ export default function CouponsIndex() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [discountType, setDiscountType] = useState(''); const [discountType, setDiscountType] = useState('');
const [selectedIds, setSelectedIds] = useState<number[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
// Configure FAB to navigate to new coupon page // Configure FAB to navigate to new coupon page
useFABConfig('navigate', '/coupons/new'); useFABConfig('coupons');
// Count active filters
const activeFiltersCount = discountType && discountType !== 'all' ? 1 : 0;
// Fetch coupons // Fetch coupons
const { data, isLoading, isError, error, refetch } = useQuery({ const { data, isLoading, isError, error, refetch } = useQuery({
@@ -110,69 +115,101 @@ export default function CouponsIndex() {
const hasActiveFilters = search || (discountType && discountType !== 'all'); const hasActiveFilters = search || (discountType && discountType !== 'all');
return ( return (
<div className="space-y-4"> <div className="space-y-4 w-full pb-4">
{/* Toolbar */} {/* Mobile: Search + Filter */}
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-3"> <div className="md:hidden">
{/* Left: Bulk Actions */} <div className="flex gap-2 items-center">
<div className="flex gap-3"> {/* Search Input */}
{/* Delete - Show only when items selected */} <div className="relative flex-1">
{selectedIds.length > 0 && ( <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<button <input
className="border rounded-md px-3 py-2 text-sm bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 inline-flex items-center gap-2" value={search}
onClick={handleBulkDelete} onChange={(e) => setSearch(e.target.value)}
disabled={deleteMutation.isPending} placeholder={__('Search coupons...')}
> className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
<Trash2 className="w-4 h-4" /> />
{__('Delete')} ({selectedIds.length}) </div>
</button>
)}
{/* Refresh - Always visible (REQUIRED per SOP) */} {/* Filter Button */}
<button <button
className="border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2" onClick={() => setFilterSheetOpen(true)}
onClick={() => refetch()} className="relative flex-shrink-0 p-2.5 rounded-lg border border-border bg-background hover:bg-accent transition-colors"
disabled={isLoading}
> >
<RefreshCw className="w-4 h-4" /> <SlidersHorizontal className="w-5 h-5" />
{__('Refresh')} {activeFiltersCount > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-primary text-primary-foreground text-xs font-medium rounded-full flex items-center justify-center">
{activeFiltersCount}
</span>
)}
</button> </button>
</div> </div>
</div>
{/* Right: Filters */} {/* Desktop Toolbar */}
<div className="flex gap-3 flex-wrap items-center"> <div className="hidden md:block rounded-lg border border-border p-4 bg-card">
{/* Discount Type Filter */} <div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-3">
<Select value={discountType || undefined} onValueChange={(value) => setDiscountType(value || '')}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={__('All types')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{__('All types')}</SelectItem>
<SelectItem value="percent">{__('Percentage')}</SelectItem>
<SelectItem value="fixed_cart">{__('Fixed Cart')}</SelectItem>
<SelectItem value="fixed_product">{__('Fixed Product')}</SelectItem>
</SelectContent>
</Select>
{/* Search */} {/* Left: Bulk Actions */}
<Input <div className="flex gap-3">
placeholder={__('Search coupons...')} {/* Delete - Show only when items selected */}
value={search} {selectedIds.length > 0 && (
onChange={(e) => setSearch(e.target.value)} <button
className="w-[200px]" className="border rounded-md px-3 py-2 text-sm bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 inline-flex items-center gap-2"
/> onClick={handleBulkDelete}
disabled={deleteMutation.isPending}
>
<Trash2 className="w-4 h-4" />
{__('Delete')} ({selectedIds.length})
</button>
)}
{/* Reset Filters - Text link style per SOP */} {/* Refresh - Always visible (REQUIRED per SOP) */}
{hasActiveFilters && (
<button <button
className="text-sm text-muted-foreground hover:text-foreground underline" className="border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2"
onClick={() => { onClick={() => refetch()}
setSearch(''); disabled={isLoading}
setDiscountType('');
}}
> >
{__('Clear filters')} <RefreshCw className="w-4 h-4" />
{__('Refresh')}
</button> </button>
)} </div>
{/* Right: Filters */}
<div className="flex gap-3 flex-wrap items-center">
{/* Discount Type Filter */}
<Select value={discountType || undefined} onValueChange={(value) => setDiscountType(value || '')}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={__('All types')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{__('All types')}</SelectItem>
<SelectItem value="percent">{__('Percentage')}</SelectItem>
<SelectItem value="fixed_cart">{__('Fixed Cart')}</SelectItem>
<SelectItem value="fixed_product">{__('Fixed Product')}</SelectItem>
</SelectContent>
</Select>
{/* Search */}
<Input
placeholder={__('Search coupons...')}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-[200px]"
/>
{/* Reset Filters - Text link style per SOP */}
{hasActiveFilters && (
<button
className="text-sm text-muted-foreground hover:text-foreground underline"
onClick={() => {
setSearch('');
setDiscountType('');
}}
>
{__('Clear filters')}
</button>
)}
</div>
</div> </div>
</div> </div>
@@ -328,6 +365,15 @@ export default function CouponsIndex() {
</div> </div>
</div> </div>
)} )}
{/* Mobile Filter Sheet */}
<CouponFilterSheet
open={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
filters={{ discount_type: discountType }}
onFiltersChange={(filters) => setDiscountType(filters.discount_type)}
onReset={() => setDiscountType('')}
/>
</div> </div>
); );
} }