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 */}
|
{/* 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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 { 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]">
|
{/* Left: Bulk Actions */}
|
||||||
<SelectValue placeholder={__('All types')} />
|
<div className="flex gap-3">
|
||||||
</SelectTrigger>
|
{/* Delete - Show only when items selected */}
|
||||||
<SelectContent>
|
{selectedIds.length > 0 && (
|
||||||
<SelectItem value="all">{__('All types')}</SelectItem>
|
<button
|
||||||
<SelectItem value="percent">{__('Percentage')}</SelectItem>
|
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"
|
||||||
<SelectItem value="fixed_cart">{__('Fixed Cart')}</SelectItem>
|
onClick={handleBulkDelete}
|
||||||
<SelectItem value="fixed_product">{__('Fixed Product')}</SelectItem>
|
disabled={deleteMutation.isPending}
|
||||||
</SelectContent>
|
>
|
||||||
</Select>
|
<Trash2 className="w-4 h-4" />
|
||||||
|
{__('Delete')} ({selectedIds.length})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Search */}
|
{/* Refresh - Always visible (REQUIRED per SOP) */}
|
||||||
<Input
|
|
||||||
placeholder={__('Search coupons...')}
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="w-[200px]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Reset Filters - Text link style 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user