diff --git a/src/api/referralNetwork.ts b/src/api/referralNetwork.ts index 360bd66..5d63908 100644 --- a/src/api/referralNetwork.ts +++ b/src/api/referralNetwork.ts @@ -4,11 +4,19 @@ import type { NetworkUserDetail, NetworkCampaignDetail, NetworkSearchResult, + ScopeOptionsData, } from '@/types/referralNetwork'; export const referralNetworkApi = { - getNetworkGraph: async (): Promise => { - const response = await apiClient.get('/cabinet/admin/referral-network/'); + getScopeOptions: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/referral-network/scope-options'); + return response.data; + }, + + getScopedGraph: async (scope: string, id: number): Promise => { + const response = await apiClient.get('/cabinet/admin/referral-network/scoped', { + params: { scope, id }, + }); return response.data; }, diff --git a/src/locales/en.json b/src/locales/en.json index aa08f88..68551ab 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1292,6 +1292,22 @@ "resetZoom": "Reset zoom", "fullscreen": "Fullscreen" }, + "scope": { + "title": "Scope", + "campaign": "Campaign", + "partner": "Partner", + "user": "User", + "searchPlaceholder": "Search...", + "selectCampaign": "Select campaign", + "selectPartner": "Select partner", + "selectUser": "Search user", + "emptyState": "Select a campaign, partner, or user to view their network", + "users": "users", + "campaigns": "campaigns", + "active": "Active", + "inactive": "Inactive", + "noResults": "Nothing found" + }, "loading": "Loading graph...", "error": "Failed to load network data", "empty": "No data to display" diff --git a/src/locales/fa.json b/src/locales/fa.json index 6adaf93..067c200 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1126,6 +1126,22 @@ "resetZoom": "بازنشانی زوم", "fullscreen": "تمام‌صفحه" }, + "scope": { + "title": "محدوده", + "campaign": "کمپین", + "partner": "شریک", + "user": "کاربر", + "searchPlaceholder": "جستجو...", + "selectCampaign": "انتخاب کمپین", + "selectPartner": "انتخاب شریک", + "selectUser": "جستجوی کاربر", + "emptyState": "یک کمپین، شریک یا کاربر را برای مشاهده شبکه انتخاب کنید", + "users": "کاربران", + "campaigns": "کمپین‌ها", + "active": "فعال", + "inactive": "غیرفعال", + "noResults": "نتیجه‌ای یافت نشد" + }, "loading": "بارگذاری گراف...", "error": "خطا در بارگذاری داده‌های شبکه", "empty": "داده‌ای برای نمایش وجود ندارد" diff --git a/src/locales/ru.json b/src/locales/ru.json index f43c612..5e50c7c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1313,6 +1313,22 @@ "resetZoom": "Сбросить зум", "fullscreen": "Полный экран" }, + "scope": { + "title": "Область", + "campaign": "Кампания", + "partner": "Партнёр", + "user": "Пользователь", + "searchPlaceholder": "Поиск...", + "selectCampaign": "Выберите кампанию", + "selectPartner": "Выберите партнёра", + "selectUser": "Найти пользователя", + "emptyState": "Выберите кампанию, партнёра или пользователя для просмотра сети", + "users": "пользователей", + "campaigns": "кампаний", + "active": "Активна", + "inactive": "Неактивна", + "noResults": "Ничего не найдено" + }, "loading": "Загрузка графа...", "error": "Ошибка загрузки данных сети", "empty": "Нет данных для отображения" diff --git a/src/locales/zh.json b/src/locales/zh.json index fa558a8..c8fa8a7 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1126,6 +1126,22 @@ "resetZoom": "重置缩放", "fullscreen": "全屏" }, + "scope": { + "title": "范围", + "campaign": "活动", + "partner": "合作伙伴", + "user": "用户", + "searchPlaceholder": "搜索...", + "selectCampaign": "选择活动", + "selectPartner": "选择合作伙伴", + "selectUser": "搜索用户", + "emptyState": "选择活动、合作伙伴或用户以查看其网络", + "users": "用户", + "campaigns": "活动", + "active": "活跃", + "inactive": "非活跃", + "noResults": "未找到" + }, "loading": "加载图表...", "error": "加载网络数据失败", "empty": "无数据显示" diff --git a/src/pages/ReferralNetwork/ReferralNetwork.tsx b/src/pages/ReferralNetwork/ReferralNetwork.tsx index 8f729de..936952a 100644 --- a/src/pages/ReferralNetwork/ReferralNetwork.tsx +++ b/src/pages/ReferralNetwork/ReferralNetwork.tsx @@ -5,8 +5,7 @@ import { referralNetworkApi } from '@/api/referralNetwork'; import { useReferralNetworkStore } from '@/store/referralNetwork'; import { AdminBackButton } from '@/components/admin/AdminBackButton'; import { NetworkGraph } from './components/NetworkGraph'; -import { NetworkSearch } from './components/NetworkSearch'; -import { NetworkFilters } from './components/NetworkFilters'; +import { ScopeSelector } from './components/ScopeSelector'; import { UserDetailPanel } from './components/UserDetailPanel'; import { CampaignDetailPanel } from './components/CampaignDetailPanel'; import { NetworkStats } from './components/NetworkStats'; @@ -16,14 +15,17 @@ import { NetworkControls } from './components/NetworkControls'; export function ReferralNetwork() { const { t } = useTranslation(); const selectedNode = useReferralNetworkStore((s) => s.selectedNode); + const scope = useReferralNetworkStore((s) => s.scope); + const setScope = useReferralNetworkStore((s) => s.setScope); const { data: networkData, isLoading, isError, } = useQuery({ - queryKey: ['referral-network-graph'], - queryFn: referralNetworkApi.getNetworkGraph, + queryKey: ['referral-network', 'scoped', scope?.type, scope?.id], + queryFn: () => referralNetworkApi.getScopedGraph(scope!.type, scope!.id), + enabled: scope !== null, staleTime: 120_000, }); @@ -40,13 +42,37 @@ export function ReferralNetwork() {

{t('admin.referralNetwork.title')}

- - {networkData && } +
- {isLoading && ( + {/* Empty state: no scope selected */} + {!scope && ( +
+
+ + + +

+ {t('admin.referralNetwork.scope.emptyState')} +

+
+
+ )} + + {/* Loading state */} + {scope && isLoading && (
@@ -55,36 +81,45 @@ export function ReferralNetwork() {
)} - {isError && ( + {/* Error state */} + {scope && isError && (

{t('admin.referralNetwork.error')}

)} - {networkData && networkData.users.length === 0 && networkData.campaigns.length === 0 && ( -
-

{t('admin.referralNetwork.empty')}

-
- )} - - {networkData && (networkData.users.length > 0 || networkData.campaigns.length > 0) && ( - <> - - -
- + {/* Empty data */} + {scope && + networkData && + networkData.users.length === 0 && + networkData.campaigns.length === 0 && ( +
+

{t('admin.referralNetwork.empty')}

+ )} -
- -
+ {/* Graph + overlays */} + {scope && + networkData && + (networkData.users.length > 0 || networkData.campaigns.length > 0) && ( + <> + -
- -
- - )} +
+ +
+
+ +
+ +
+ +
+ + )} + + {/* Detail panel (slide-in from right) */}
(null); @@ -15,8 +19,7 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) { const filters = useReferralNetworkStore((s) => s.filters); const updateFilters = useReferralNetworkStore((s) => s.updateFilters); const resetFilters = useReferralNetworkStore((s) => s.resetFilters); - const isOpen = useReferralNetworkStore((s) => s.isFiltersOpen); - const setIsOpen = useReferralNetworkStore((s) => s.setIsFiltersOpen); + const [isOpen, setIsOpen] = useState(false); const hasActiveFilters = filters.campaigns.length > 0 || filters.partnersOnly || filters.minReferrals > 0; diff --git a/src/pages/ReferralNetwork/components/NetworkGraph.tsx b/src/pages/ReferralNetwork/components/NetworkGraph.tsx index 10cd9d8..1671e64 100644 --- a/src/pages/ReferralNetwork/components/NetworkGraph.tsx +++ b/src/pages/ReferralNetwork/components/NetworkGraph.tsx @@ -6,7 +6,7 @@ import { inferSettings } from 'graphology-layout-forceatlas2'; import { useReferralNetworkStore } from '@/store/referralNetwork'; import type { NetworkGraphData, NetworkFilters } from '@/types/referralNetwork'; import { getUserNodeColor, getUserNodeSize, getCampaignColor } from '../utils'; -import { setSigmaInstance, setGraphInstance } from '../sigmaGlobals'; +import { setSigmaInstance } from '../sigmaGlobals'; interface NetworkGraphProps { data: NetworkGraphData; @@ -145,6 +145,7 @@ export function NetworkGraph({ data, className }: NetworkGraphProps) { const setSelectedNode = useReferralNetworkStore((s) => s.setSelectedNode); const setHoveredNode = useReferralNetworkStore((s) => s.setHoveredNode); + const setHighlightedNodes = useReferralNetworkStore((s) => s.setHighlightedNodes); const hoveredNodeId = useReferralNetworkStore((s) => s.hoveredNodeId); const highlightedNodes = useReferralNetworkStore((s) => s.highlightedNodes); const filters = useReferralNetworkStore((s) => s.filters); @@ -268,7 +269,6 @@ export function NetworkGraph({ data, className }: NetworkGraphProps) { sigmaRef.current = sigma; setSigmaInstance(sigma); - setGraphInstance(graph); if (graph.order > 0) { const inferred = inferSettings(graph); @@ -348,9 +348,11 @@ export function NetworkGraph({ data, className }: NetworkGraphProps) { } graphRef.current = null; setSigmaInstance(null); - setGraphInstance(null); + + setHoveredNode(null); + setHighlightedNodes(new Set()); }; - }, [data, setSelectedNode, setHoveredNode, killFA2]); + }, [data, setSelectedNode, setHoveredNode, setHighlightedNodes, killFA2]); useEffect(() => { const container = containerRef.current; diff --git a/src/pages/ReferralNetwork/components/NetworkSearch.tsx b/src/pages/ReferralNetwork/components/NetworkSearch.tsx deleted file mode 100644 index 735f0f9..0000000 --- a/src/pages/ReferralNetwork/components/NetworkSearch.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useQuery } from '@tanstack/react-query'; -import { referralNetworkApi } from '@/api/referralNetwork'; -import { useReferralNetworkStore } from '@/store/referralNetwork'; -import { getSigmaInstance, getGraphInstance } from '../sigmaGlobals'; - -interface NetworkSearchProps { - className?: string; -} - -export function NetworkSearch({ className }: NetworkSearchProps) { - const { t } = useTranslation(); - const [inputValue, setInputValue] = useState(''); - const [debouncedQuery, setDebouncedQuery] = useState(''); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - const inputRef = useRef(null); - - const setHighlightedNodes = useReferralNetworkStore((s) => s.setHighlightedNodes); - const setSelectedNode = useReferralNetworkStore((s) => s.setSelectedNode); - - // Debounce - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedQuery(inputValue.trim()); - }, 300); - return () => clearTimeout(timer); - }, [inputValue]); - - const { data: searchResults, isFetching } = useQuery({ - queryKey: ['referral-network-search', debouncedQuery], - queryFn: () => referralNetworkApi.search(debouncedQuery), - enabled: debouncedQuery.length >= 2, - staleTime: 30_000, - }); - - // Open dropdown when results arrive - useEffect(() => { - if (searchResults && debouncedQuery.length >= 2) { - setIsDropdownOpen(true); - } - }, [searchResults, debouncedQuery]); - - // Close on outside click - useEffect(() => { - function handleClickOutside(e: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { - setIsDropdownOpen(false); - } - } - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - function navigateToNode(nodeId: string) { - const sigma = getSigmaInstance(); - const graph = getGraphInstance(); - - if (sigma && graph && graph.hasNode(nodeId)) { - // getNodeDisplayData returns viewport (pixel) coordinates. - // Convert to framed graph coordinates that the camera operates in. - const displayData = sigma.getNodeDisplayData(nodeId); - if (displayData) { - const camera = sigma.getCamera(); - const framedCoords = sigma.viewportToFramedGraph({ - x: displayData.x, - y: displayData.y, - }); - camera.animate({ ...framedCoords, ratio: 0.15 }, { duration: 600 }); - } - - setHighlightedNodes(new Set([nodeId])); - } - } - - function handleSelectUser(userId: number) { - setIsDropdownOpen(false); - setSelectedNode({ type: 'user', id: userId }); - navigateToNode(`user_${userId}`); - } - - function handleSelectCampaign(campaignId: number) { - setIsDropdownOpen(false); - setSelectedNode({ type: 'campaign', id: campaignId }); - navigateToNode(`campaign_${campaignId}`); - } - - function handleClear() { - setInputValue(''); - setDebouncedQuery(''); - setIsDropdownOpen(false); - setHighlightedNodes(new Set()); - setSelectedNode(null); - } - - const totalResults = - (searchResults?.users?.length ?? 0) + (searchResults?.campaigns?.length ?? 0); - - return ( -
-
- - - - setInputValue(e.target.value)} - onFocus={() => { - if (searchResults && debouncedQuery.length >= 2) setIsDropdownOpen(true); - }} - placeholder={t('admin.referralNetwork.search.placeholder')} - className="w-full rounded-lg border border-dark-700/50 bg-dark-800/80 py-2 pl-10 pr-10 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" - /> - {inputValue && ( - - )} - {isFetching && ( -
-
-
- )} -
- - {/* Dropdown */} - {isDropdownOpen && debouncedQuery.length >= 2 && ( -
- {totalResults === 0 && !isFetching && ( -
- {t('admin.referralNetwork.search.noResults')} -
- )} - - {searchResults?.users && searchResults.users.length > 0 && ( -
- {searchResults.users.map((user) => ( - - ))} -
- )} - - {searchResults?.campaigns && searchResults.campaigns.length > 0 && ( -
- {searchResults.users && searchResults.users.length > 0 && ( -
- )} - {searchResults.campaigns.map((campaign) => ( - - ))} -
- )} - - {totalResults > 0 && ( -
- {t('admin.referralNetwork.search.resultsCount', { count: totalResults })} -
- )} -
- )} -
- ); -} diff --git a/src/pages/ReferralNetwork/components/ScopeSelector.tsx b/src/pages/ReferralNetwork/components/ScopeSelector.tsx new file mode 100644 index 0000000..b1fe24e --- /dev/null +++ b/src/pages/ReferralNetwork/components/ScopeSelector.tsx @@ -0,0 +1,358 @@ +import { useState, useEffect, useRef, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { referralNetworkApi } from '@/api/referralNetwork'; +import type { ScopeSelection, ScopeType } from '@/types/referralNetwork'; + +interface ScopeSelectorProps { + value: ScopeSelection | null; + onSelect: (selection: ScopeSelection | null) => void; + className?: string; +} + +const SCOPE_TABS: ScopeType[] = ['campaign', 'partner', 'user']; + +export function ScopeSelector({ value, onSelect, className }: ScopeSelectorProps) { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(value?.type ?? 'campaign'); + const [searchInput, setSearchInput] = useState(''); + const [debouncedUserQuery, setDebouncedUserQuery] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const containerRef = useRef(null); + const inputRef = useRef(null); + + 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, + }); + + // Close dropdown on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsDropdownOpen(false); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // 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 handleSelectCampaign(campaign: { id: number; name: string }) { + onSelect({ type: 'campaign', id: campaign.id, label: campaign.name }); + setIsDropdownOpen(false); + setSearchInput(''); + } + + function handleSelectPartner(partner: { id: number; display_name: string }) { + onSelect({ type: 'partner', id: partner.id, label: partner.display_name }); + setIsDropdownOpen(false); + setSearchInput(''); + } + + function handleSelectUser(user: { id: number; display_name: string }) { + onSelect({ type: 'user', id: user.id, label: user.display_name }); + setIsDropdownOpen(false); + setSearchInput(''); + } + + function handleClear() { + onSelect(null); + setSearchInput(''); + setDebouncedUserQuery(''); + setIsDropdownOpen(false); + } + + const tabLabels: Record = { + campaign: t('admin.referralNetwork.scope.campaign'), + partner: t('admin.referralNetwork.scope.partner'), + user: t('admin.referralNetwork.scope.user'), + }; + + const placeholders: Record = { + campaign: t('admin.referralNetwork.scope.selectCampaign'), + partner: t('admin.referralNetwork.scope.selectPartner'), + user: t('admin.referralNetwork.scope.selectUser'), + }; + + // When scope is selected, show compact display + if (value) { + return ( +
+ + {tabLabels[value.type]} + + {value.label} + +
+ ); + } + + const isLoading = activeTab === 'user' ? isUserSearching : isScopeLoading; + + return ( +
+ {/* Tab bar + search input row */} +
+ {/* Tabs */} +
+ {SCOPE_TABS.map((tab) => ( + + ))} +
+ + {/* Search input */} +
+ + + + { + setSearchInput(e.target.value); + if (!isDropdownOpen) setIsDropdownOpen(true); + }} + onFocus={() => setIsDropdownOpen(true)} + placeholder={placeholders[activeTab]} + className="w-full rounded-lg border border-dark-700/50 bg-dark-800/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 && ( +
+
+
+ )} +
+
+ + {/* Dropdown */} + {isDropdownOpen && ( +
+ {activeTab === 'campaign' && renderCampaignList()} + {activeTab === 'partner' && renderPartnerList()} + {activeTab === 'user' && renderUserList()} +
+ )} +
+ ); + + function renderCampaignList() { + if (isScopeLoading) { + return ( +
+
+
+ ); + } + + if (filteredCampaigns.length === 0) { + return ( +
+ {t('admin.referralNetwork.scope.noResults')} +
+ ); + } + + return filteredCampaigns.map((campaign) => ( + + )); + } + + function renderPartnerList() { + if (isScopeLoading) { + return ( +
+
+
+ ); + } + + if (filteredPartners.length === 0) { + return ( +
+ {t('admin.referralNetwork.scope.noResults')} +
+ ); + } + + return filteredPartners.map((partner) => ( + + )); + } + + function renderUserList() { + if (debouncedUserQuery.length < 2) { + return ( +
+ {t('admin.referralNetwork.scope.selectUser')} +
+ ); + } + + if (isUserSearching) { + return ( +
+
+
+ ); + } + + const users = userSearchResults?.users ?? []; + + if (users.length === 0) { + return ( +
+ {t('admin.referralNetwork.scope.noResults')} +
+ ); + } + + return users.map((user) => ( + + )); + } +} diff --git a/src/pages/ReferralNetwork/sigmaGlobals.ts b/src/pages/ReferralNetwork/sigmaGlobals.ts index b702492..1052628 100644 --- a/src/pages/ReferralNetwork/sigmaGlobals.ts +++ b/src/pages/ReferralNetwork/sigmaGlobals.ts @@ -1,12 +1,10 @@ import type Sigma from 'sigma'; -import type Graph from 'graphology'; /** - * We store sigma and graph instances on a module-level variable - * so that search and controls can access them without prop drilling. + * We store the sigma instance on a module-level variable + * so that controls can access it without prop drilling. */ let sigmaInstance: Sigma | null = null; -let graphInstance: Graph | null = null; export function setSigmaInstance(sigma: Sigma | null): void { sigmaInstance = sigma; @@ -15,11 +13,3 @@ export function setSigmaInstance(sigma: Sigma | null): void { export function getSigmaInstance(): Sigma | null { return sigmaInstance; } - -export function setGraphInstance(graph: Graph | null): void { - graphInstance = graph; -} - -export function getGraphInstance(): Graph | null { - return graphInstance; -} diff --git a/src/store/referralNetwork.ts b/src/store/referralNetwork.ts index 903f22e..48295c0 100644 --- a/src/store/referralNetwork.ts +++ b/src/store/referralNetwork.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import type { SelectedNode, NetworkFilters } from '@/types/referralNetwork'; +import type { SelectedNode, NetworkFilters, ScopeSelection } from '@/types/referralNetwork'; const DEFAULT_FILTERS: NetworkFilters = { campaigns: [], @@ -12,14 +12,14 @@ interface ReferralNetworkState { hoveredNodeId: string | null; highlightedNodes: Set; filters: NetworkFilters; - isFiltersOpen: boolean; + scope: ScopeSelection | null; setSelectedNode: (node: SelectedNode) => void; setHoveredNode: (id: string | null) => void; setHighlightedNodes: (nodes: Set) => void; updateFilters: (filters: Partial) => void; resetFilters: () => void; - setIsFiltersOpen: (open: boolean) => void; + setScope: (scope: ScopeSelection | null) => void; } export const useReferralNetworkStore = create()((set) => ({ @@ -27,7 +27,7 @@ export const useReferralNetworkStore = create()((set) => ( hoveredNodeId: null, highlightedNodes: new Set(), filters: { ...DEFAULT_FILTERS }, - isFiltersOpen: false, + scope: null, setSelectedNode: (node) => set({ selectedNode: node }), @@ -42,5 +42,12 @@ export const useReferralNetworkStore = create()((set) => ( resetFilters: () => set({ filters: { ...DEFAULT_FILTERS } }), - setIsFiltersOpen: (open) => set({ isFiltersOpen: open }), + setScope: (scope) => + set({ + scope, + selectedNode: null, + hoveredNodeId: null, + highlightedNodes: new Set(), + filters: { ...DEFAULT_FILTERS }, + }), })); diff --git a/src/types/referralNetwork.ts b/src/types/referralNetwork.ts index 92c8ab0..10802b4 100644 --- a/src/types/referralNetwork.ts +++ b/src/types/referralNetwork.ts @@ -100,3 +100,31 @@ export interface NetworkFilters { partnersOnly: boolean; minReferrals: number; } + +export interface CampaignOption { + id: number; + name: string; + start_parameter: string; + is_active: boolean; + direct_users: number; +} + +export interface PartnerOption { + id: number; + display_name: string; + username: string | null; + campaign_count: number; +} + +export interface ScopeOptionsData { + campaigns: CampaignOption[]; + partners: PartnerOption[]; +} + +export type ScopeType = 'campaign' | 'partner' | 'user'; + +export interface ScopeSelection { + type: ScopeType; + id: number; + label: string; +}