/** * NetworkMultiSelect Component * Multi-select dropdown for network selection with badge display * Features: Search, "All" option, condensed display, badge pills */ 'use client'; import * as React from 'react'; import { Check, ChevronsUpDown, X, Building2, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@/components/ui/command'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import type { Network } from '../_types/dashboard.types'; export interface NetworkMultiSelectProps { /** * Available networks to select from */ networks: Network[]; /** * Currently selected network IDs */ selectedNetworkIds: number[]; /** * Callback when selection changes (called immediately for UI updates) */ onSelectionChange: (networkIds: number[]) => void; /** * Callback when popover closes with final selection * Use this for API calls to avoid multiple requests * @param finalSelection - The final selected network IDs when dropdown closes */ onPopoverClose?: (finalSelection: number[]) => void; /** * Placeholder text when no networks selected */ placeholder?: string; /** * Whether the select is disabled */ disabled?: boolean; /** * Whether to show the "All" option */ showAllOption?: boolean; /** * Loading state */ isLoading?: boolean; /** * Additional CSS classes */ className?: string; /** * Maximum number of badges to show before condensing */ maxDisplayBadges?: number; } const ALL_NETWORKS_ID = 0; export function NetworkMultiSelect({ networks, selectedNetworkIds, onSelectionChange, onPopoverClose, placeholder = 'Seleccionar sucursales', disabled = false, showAllOption = true, isLoading = false, className, maxDisplayBadges = 1, }: NetworkMultiSelectProps) { const [open, setOpen] = React.useState(false); const [searchQuery, setSearchQuery] = React.useState(''); // Track selection when popover opens to detect changes on close const selectionOnOpenRef = React.useRef([]); // Check if "All" is selected const isAllSelected = selectedNetworkIds.includes(ALL_NETWORKS_ID); // Get selected networks for display const selectedNetworks = React.useMemo(() => { if (isAllSelected) { return [{ id: ALL_NETWORKS_ID, name: 'Todas las Sucursales' }]; } return networks.filter((network) => selectedNetworkIds.includes(network.id) ); }, [networks, selectedNetworkIds, isAllSelected]); // Filter networks by search query const filteredNetworks = React.useMemo(() => { if (!searchQuery) return networks; const query = searchQuery.toLowerCase(); return networks.filter( (network) => network.name.toLowerCase().includes(query) || network.location?.toLowerCase().includes(query) ); }, [networks, searchQuery]); // Group networks by status for better organization const activeNetworks = filteredNetworks.filter( (n) => n.status === 'active' ); const maintenanceNetworks = filteredNetworks.filter( (n) => n.status === 'maintenance' ); const inactiveNetworks = filteredNetworks.filter( (n) => n.status === 'inactive' ); // Detect when popover opens/closes and trigger callback on close React.useEffect(() => { if (open) { // Popover just opened - save current selection selectionOnOpenRef.current = [...selectedNetworkIds]; } else { // Popover just closed - check if selection changed if (onPopoverClose && selectionOnOpenRef.current.length > 0) { const selectionChanged = selectedNetworkIds.length !== selectionOnOpenRef.current.length || !selectedNetworkIds.every(id => selectionOnOpenRef.current.includes(id)) || !selectionOnOpenRef.current.every(id => selectedNetworkIds.includes(id)); if (selectionChanged) { // Selection changed while dropdown was open - trigger API call onPopoverClose(selectedNetworkIds); } } } }, [open, selectedNetworkIds, onPopoverClose]); const handleSelectAll = () => { if (isAllSelected) { onSelectionChange([]); } else { onSelectionChange([ALL_NETWORKS_ID]); } }; const handleToggleNetwork = (networkId: number) => { // If selecting "All", replace all selections if (networkId === ALL_NETWORKS_ID) { handleSelectAll(); return; } // If "All" is currently selected, deselect it if (isAllSelected) { onSelectionChange([networkId]); return; } // Toggle individual network const newSelection = selectedNetworkIds.includes(networkId) ? selectedNetworkIds.filter((id) => id !== networkId) : [...selectedNetworkIds, networkId]; onSelectionChange(newSelection); }; const handleRemoveNetwork = (networkId: number, e: React.MouseEvent) => { e.stopPropagation(); if (networkId === ALL_NETWORKS_ID) { onSelectionChange([]); return; } const newSelection = selectedNetworkIds.filter((id) => id !== networkId); onSelectionChange(newSelection); }; const handleClearAll = (e: React.MouseEvent) => { e.stopPropagation(); onSelectionChange([]); }; // Format display text const getDisplayText = () => { if (selectedNetworks.length === 0) { return placeholder; } if (isAllSelected) { return 'Todas las Sucursales'; } if (selectedNetworks.length === 1) { return selectedNetworks[0].name; } return `${selectedNetworks.length} Sucursales`; }; const displayBadges = selectedNetworks.slice(0, maxDisplayBadges); const remainingCount = selectedNetworks.length - maxDisplayBadges; return ( )} No se encontraron sucursales. {/* All Networks Option */} {showAllOption && !searchQuery && ( <>
{isAllSelected && }
Todas las Sucursales {networks.length}
)} {/* Active Networks */} {activeNetworks.length > 0 && ( {activeNetworks.map((network) => { const isSelected = selectedNetworkIds.includes(network.id); return ( handleToggleNetwork(network.id)} className="cursor-pointer" >
{isSelected && }
{network.name}
{network.location && (
{network.location}
)}
); })}
)} {/* Maintenance Networks */} {maintenanceNetworks.length > 0 && ( <> {maintenanceNetworks.map((network) => { const isSelected = selectedNetworkIds.includes(network.id); return ( handleToggleNetwork(network.id)} className="cursor-pointer opacity-75" >
{isSelected && }
{network.name}
{network.location && (
{network.location}
)}
); })}
)} {/* Inactive Networks */} {inactiveNetworks.length > 0 && ( <> {inactiveNetworks.map((network) => { const isSelected = selectedNetworkIds.includes(network.id); return ( handleToggleNetwork(network.id)} className="cursor-pointer opacity-50" >
{isSelected && }
{network.name}
{network.location && (
{network.location}
)}
); })}
)}
{/* Footer */} {selectedNetworks.length > 0 && ( <>
{selectedNetworks.length}{' '} {selectedNetworks.length === 1 ? 'sucursal' : 'sucursales'}{' '} {selectedNetworks.length === 1 ? 'seleccionada' : 'seleccionadas'} {!disabled && ( )}
)}
); }