import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { referralNetworkApi } from '@/api/referralNetwork'; import { MAX_SCOPE_ITEMS } from '@/store/referralNetwork'; import type { ScopeSelection, ScopeType } from '@/types/referralNetwork'; interface ScopeSelectorProps { value: ScopeSelection[]; onAdd: (selection: ScopeSelection) => void; onRemove: (type: ScopeSelection['type'], id: number) => void; onClear: () => void; className?: string; } const SCOPE_TABS: ScopeType[] = ['campaign', 'partner', 'user']; const CHIP_COLORS: Record = { campaign: 'bg-success-500/20 text-success-400', partner: 'bg-warning-500/20 text-warning-400', user: 'bg-accent-500/20 text-accent-400', }; // Reuse CHIP_COLORS for avatar backgrounds (same palette) const AVATAR_COLORS = CHIP_COLORS; const AVATAR_LETTERS: Record = { campaign: 'C', partner: 'P', user: 'U', }; function CheckIcon() { return ( ); } function CloseIcon({ className = 'h-3 w-3' }: { className?: string }) { return ( ); } function Spinner({ size = 'h-5 w-5' }: { size?: string }) { return (
); } function EmptyMessage({ text }: { text: string }) { return
{text}
; } interface ScopeListItemProps { type: ScopeType; selected: boolean; onClick: () => void; title: string; subtitle: string; badge?: React.ReactNode; } function ScopeListItem({ type, selected, onClick, title, subtitle, badge }: ScopeListItemProps) { return ( ); } export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: ScopeSelectorProps) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('campaign'); const [searchInput, setSearchInput] = useState(''); const [debouncedUserQuery, setDebouncedUserQuery] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const containerRef = useRef(null); const inputRef = useRef(null); const isMaxReached = value.length >= MAX_SCOPE_ITEMS; const { data: scopeOptions, isLoading: isScopeLoading } = useQuery({ queryKey: ['referral-network', 'scope-options'], queryFn: referralNetworkApi.getScopeOptions, staleTime: 120_000, }); // Debounce user search useEffect(() => { if (activeTab !== 'user') return; const timer = setTimeout(() => { setDebouncedUserQuery(searchInput.trim()); }, 300); return () => clearTimeout(timer); }, [searchInput, activeTab]); const { data: userSearchResults, isFetching: isUserSearching } = useQuery({ queryKey: ['referral-network-search', debouncedUserQuery], queryFn: () => referralNetworkApi.search(debouncedUserQuery), enabled: activeTab === 'user' && debouncedUserQuery.length >= 2, staleTime: 30_000, }); // Focus input when dropdown opens useEffect(() => { if (isDropdownOpen) { inputRef.current?.focus(); } }, [isDropdownOpen]); // Close dropdown on outside click or Escape key (only when open) useEffect(() => { if (!isDropdownOpen) return; function handleClickOutside(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setIsDropdownOpen(false); } } function handleEscape(e: KeyboardEvent) { if (e.key === 'Escape') { setIsDropdownOpen(false); } } document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleEscape); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; }, [isDropdownOpen]); const selectedSet = useMemo(() => new Set(value.map((s) => `${s.type}:${s.id}`)), [value]); const isSelected = useCallback( (type: ScopeType, id: number): boolean => selectedSet.has(`${type}:${id}`), [selectedSet], ); // Filter campaigns/partners by local search input const filteredCampaigns = useMemo(() => { if (!scopeOptions) return []; const q = searchInput.toLowerCase().trim(); if (!q) return scopeOptions.campaigns; return scopeOptions.campaigns.filter( (c) => c.name.toLowerCase().includes(q) || c.start_parameter.toLowerCase().includes(q), ); }, [scopeOptions, searchInput]); const filteredPartners = useMemo(() => { if (!scopeOptions) return []; const q = searchInput.toLowerCase().trim(); if (!q) return scopeOptions.partners; return scopeOptions.partners.filter( (p) => p.display_name.toLowerCase().includes(q) || (p.username && p.username.toLowerCase().includes(q)), ); }, [scopeOptions, searchInput]); function handleTabChange(tab: ScopeType) { setActiveTab(tab); setSearchInput(''); setDebouncedUserQuery(''); setIsDropdownOpen(true); inputRef.current?.focus(); } function handleToggle(type: ScopeType, id: number, label: string) { if (isSelected(type, id)) { onRemove(type, id); } else { if (isMaxReached) return; onAdd({ type, id, label }); if (type === 'user') { setSearchInput(''); setDebouncedUserQuery(''); } } } const tabLabels = useMemo>( () => ({ campaign: t('admin.referralNetwork.scope.campaign'), partner: t('admin.referralNetwork.scope.partner'), user: t('admin.referralNetwork.scope.user'), }), [t], ); const placeholders = useMemo>( () => ({ campaign: t('admin.referralNetwork.scope.selectCampaign'), partner: t('admin.referralNetwork.scope.selectPartner'), user: t('admin.referralNetwork.scope.selectUser'), }), [t], ); const isLoading = activeTab === 'user' ? isUserSearching : isScopeLoading; return (
{/* Chips row + add trigger */}
{/* Selected chips */} {value.length > 0 && (
{value.map((item) => ( {item.label} ))} {/* Clear all (only when 2+ items) */} {value.length > 1 && ( )}
)} {/* Add button */}
{/* Dropdown */} {isDropdownOpen && (
{/* Max reached banner */} {isMaxReached && (
{t('admin.referralNetwork.scope.maxReached', { max: MAX_SCOPE_ITEMS })}
)} {/* Tab bar + search input */}
{SCOPE_TABS.map((tab) => ( ))}
setSearchInput(e.target.value)} placeholder={placeholders[activeTab]} className="w-full rounded-lg border border-dark-700/50 bg-dark-900/50 py-1.5 pl-8 pr-8 text-sm text-dark-100 placeholder-dark-500 outline-none transition-colors focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/30" /> {isLoading && (
)}
{/* List */}
{activeTab === 'campaign' && renderCampaignList()} {activeTab === 'partner' && renderPartnerList()} {activeTab === 'user' && renderUserList()}
)}
); function renderCampaignList() { if (isScopeLoading) { return (
); } if (filteredCampaigns.length === 0) { return ; } return filteredCampaigns.map((campaign) => ( handleToggle('campaign', campaign.id, campaign.name)} title={campaign.name} subtitle={`${campaign.start_parameter} / ${campaign.direct_users} ${t('admin.referralNetwork.scope.users')}`} badge={ {campaign.is_active ? t('admin.referralNetwork.scope.active') : t('admin.referralNetwork.scope.inactive')} } /> )); } function renderPartnerList() { if (isScopeLoading) { return (
); } if (filteredPartners.length === 0) { return ; } return filteredPartners.map((partner) => ( handleToggle('partner', partner.id, partner.display_name)} title={partner.display_name} subtitle={`${partner.username ? `@${partner.username} / ` : ''}${partner.campaign_count} ${t('admin.referralNetwork.scope.campaigns')}`} /> )); } function renderUserList() { if (debouncedUserQuery.length < 2) { return ; } if (isUserSearching) { return (
); } const users = userSearchResults?.users ?? []; if (users.length === 0) { return ; } return users.map((user) => ( handleToggle('user', user.id, user.display_name)} title={user.display_name} subtitle={`${user.username ? `@${user.username}` : ''}${user.tg_id ? ` #${user.tg_id}` : ''}`} badge={ user.is_partner ? ( {t('admin.referralNetwork.user.partner')} ) : undefined } /> )); } }