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:
@@ -92,13 +92,15 @@ export function VerticalTabForm({ tabs, children, className }: VerticalTabFormPr
|
||||
{/* Content Area */}
|
||||
<div
|
||||
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) => {
|
||||
if (React.isValidElement(child) && child.props['data-section-id']) {
|
||||
const sectionId = child.props['data-section-id'];
|
||||
const isActive = sectionId === activeTab;
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
ref: (el: HTMLElement) => registerSection(sectionId, el),
|
||||
className: isActive ? '' : 'hidden',
|
||||
});
|
||||
}
|
||||
return child;
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function CouponEdit() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div>
|
||||
<CouponForm
|
||||
mode="edit"
|
||||
initial={coupon}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function CouponNew() {
|
||||
}, [createMutation.isPending, setPageHeader, clearPageHeader, navigate]);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div>
|
||||
<CouponForm
|
||||
mode="create"
|
||||
onSubmit={async (data) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,9 @@ import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
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 { CouponFilterSheet } from './components/CouponFilterSheet';
|
||||
|
||||
export default function CouponsIndex() {
|
||||
const navigate = useNavigate();
|
||||
@@ -21,9 +22,13 @@ export default function CouponsIndex() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [discountType, setDiscountType] = useState('');
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
|
||||
|
||||
// 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
|
||||
const { data, isLoading, isError, error, refetch } = useQuery({
|
||||
@@ -110,69 +115,101 @@ export default function CouponsIndex() {
|
||||
const hasActiveFilters = search || (discountType && discountType !== 'all');
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-3">
|
||||
{/* Left: Bulk Actions */}
|
||||
<div className="flex gap-3">
|
||||
{/* Delete - Show only when items selected */}
|
||||
{selectedIds.length > 0 && (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
<div className="space-y-4 w-full pb-4">
|
||||
{/* Mobile: Search + Filter */}
|
||||
<div className="md:hidden">
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* Search Input */}
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Refresh - Always visible (REQUIRED per SOP) */}
|
||||
{/* Filter 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={() => refetch()}
|
||||
disabled={isLoading}
|
||||
onClick={() => setFilterSheetOpen(true)}
|
||||
className="relative flex-shrink-0 p-2.5 rounded-lg border border-border bg-background hover:bg-accent transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
{__('Refresh')}
|
||||
<SlidersHorizontal className="w-5 h-5" />
|
||||
{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>
|
||||
</div>
|
||||
</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>
|
||||
{/* Desktop Toolbar */}
|
||||
<div className="hidden md:block rounded-lg border border-border p-4 bg-card">
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-3">
|
||||
|
||||
{/* Search */}
|
||||
<Input
|
||||
placeholder={__('Search coupons...')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-[200px]"
|
||||
/>
|
||||
{/* Left: Bulk Actions */}
|
||||
<div className="flex gap-3">
|
||||
{/* Delete - Show only when items selected */}
|
||||
{selectedIds.length > 0 && (
|
||||
<button
|
||||
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 */}
|
||||
{hasActiveFilters && (
|
||||
{/* Refresh - Always visible (REQUIRED per SOP) */}
|
||||
<button
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline"
|
||||
onClick={() => {
|
||||
setSearch('');
|
||||
setDiscountType('');
|
||||
}}
|
||||
className="border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2"
|
||||
onClick={() => refetch()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{__('Clear filters')}
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
{__('Refresh')}
|
||||
</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>
|
||||
|
||||
@@ -328,6 +365,15 @@ export default function CouponsIndex() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Filter Sheet */}
|
||||
<CouponFilterSheet
|
||||
open={filterSheetOpen}
|
||||
onClose={() => setFilterSheetOpen(false)}
|
||||
filters={{ discount_type: discountType }}
|
||||
onFiltersChange={(filters) => setDiscountType(filters.discount_type)}
|
||||
onReset={() => setDiscountType('')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user