Реализация реферальной системы и улучшение системы авторизации

- Добавлена полная реферальная система с GraphQL резолверами и UI компонентами
- Улучшена система регистрации с поддержкой ВКонтакте и реферальных ссылок
- Обновлена схема Prisma для поддержки реферальной системы
- Добавлены новые файлы документации правил системы
- Улучшена система партнерства и контрагентов
- Обновлены компоненты авторизации для поддержки новых функций
- Удален устаревший server.log

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-11 09:47:00 +03:00
parent af16402f22
commit 8f7ec70fe6
28 changed files with 5827 additions and 4313 deletions

View File

@ -3,6 +3,8 @@
import { CheckCircle } from 'lucide-react'
import { useState, useEffect } from 'react'
import { useAuth } from '@/hooks/useAuth'
import { CabinetSelectStep } from './cabinet-select-step'
import { ConfirmationStep } from './confirmation-step'
import { InnStep } from './inn-step'
@ -39,14 +41,31 @@ interface AuthData {
ozonApiValidation: ApiKeyValidation | null
isAuthenticated: boolean
partnerCode?: string | null
referralCode?: string | null
}
interface AuthFlowProps {
partnerCode?: string | null
referralCode?: string | null
}
export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
const [step, setStep] = useState<AuthStep>('phone')
export function AuthFlow({ partnerCode, referralCode }: AuthFlowProps = {}) {
const { isAuthenticated, user } = useAuth()
console.log('🎢 AuthFlow - Полученные props:', { partnerCode, referralCode })
console.log('🎢 AuthFlow - Статус авторизации:', { isAuthenticated, hasUser: !!user })
// Определяем начальный шаг в зависимости от авторизации
const initialStep = isAuthenticated ? 'cabinet-select' : 'phone'
const [step, setStep] = useState<AuthStep>(initialStep)
// Определяем тип регистрации на основе параметров
// Только один из них должен быть активен (валидация уже прошла в RegisterPage)
const registrationType = partnerCode ? 'PARTNER' : (referralCode ? 'REFERRAL' : null)
const activeCode = partnerCode || referralCode || null
console.log('🎢 AuthFlow - Обработанные данные:', { registrationType, activeCode })
const [authData, setAuthData] = useState<AuthData>({
phone: '',
smsCode: '',
@ -58,8 +77,23 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
ozonApiKey: '',
ozonApiValidation: null,
isAuthenticated: false,
partnerCode: partnerCode,
// Сохраняем только активный код в правильное поле
partnerCode: registrationType === 'PARTNER' ? activeCode : null,
referralCode: registrationType === 'REFERRAL' ? activeCode : null,
})
console.log('🎢 AuthFlow - Сохраненные в authData:', {
partnerCode: authData.partnerCode,
referralCode: authData.referralCode
})
// Обновляем шаг при изменении статуса авторизации
useEffect(() => {
if (isAuthenticated && step === 'phone') {
console.log('🎢 AuthFlow - Пользователь авторизовался, переход к выбору кабинета')
setStep('cabinet-select')
}
}, [isAuthenticated, step])
// При завершении авторизации инициируем проверку и перенаправление
useEffect(() => {
@ -197,7 +231,13 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
return (
<>
{step === 'phone' && <PhoneStep onNext={handlePhoneNext} />}
{step === 'phone' && (
<PhoneStep
onNext={handlePhoneNext}
registrationType={registrationType}
referrerCode={activeCode}
/>
)}
{step === 'sms' && <SmsStep phone={authData.phone} onNext={handleSmsNext} onBack={handleSmsBack} />}
{step === 'cabinet-select' && <CabinetSelectStep onNext={handleCabinetNext} onBack={handleCabinetBack} />}
{step === 'inn' && <InnStep onNext={handleInnNext} onBack={handleInnBack} />}
@ -208,13 +248,15 @@ export function AuthFlow({ partnerCode }: AuthFlowProps = {}) {
<ConfirmationStep
data={{
phone: authData.phone,
cabinetType: authData.cabinetType!,
cabinetType: authData.cabinetType as 'fulfillment' | 'seller' | 'logist' | 'wholesale',
inn: authData.inn || undefined,
organizationData: authData.organizationData || undefined,
wbApiKey: authData.wbApiKey || undefined,
wbApiValidation: authData.wbApiValidation || undefined,
ozonApiKey: authData.ozonApiKey || undefined,
ozonApiValidation: authData.ozonApiValidation || undefined,
referralCode: authData.referralCode,
partnerCode: authData.partnerCode,
}}
onConfirm={handleConfirmation}
onBack={handleConfirmationBack}

View File

@ -33,6 +33,8 @@ interface ConfirmationStepProps {
wbApiValidation?: ApiKeyValidation
ozonApiKey?: string
ozonApiValidation?: ApiKeyValidation
referralCode?: string | null
partnerCode?: string | null
}
onConfirm: () => void
onBack: () => void
@ -65,6 +67,13 @@ export function ConfirmationStep({ data, onConfirm, onBack }: ConfirmationStepPr
const handleConfirm = async () => {
setIsLoading(true)
setError(null)
console.log('📝 ConfirmationStep - Данные для регистрации:', {
cabinetType: data.cabinetType,
inn: data.inn,
referralCode: data.referralCode,
partnerCode: data.partnerCode
})
try {
let result
@ -73,16 +82,25 @@ export function ConfirmationStep({ data, onConfirm, onBack }: ConfirmationStepPr
(data.cabinetType === 'fulfillment' || data.cabinetType === 'logist' || data.cabinetType === 'wholesale') &&
data.inn
) {
console.log('📝 ConfirmationStep - Вызов registerFulfillmentOrganization с кодами:', {
referralCode: data.referralCode,
partnerCode: data.partnerCode
})
result = await registerFulfillmentOrganization(
data.phone.replace(/\D/g, ''),
data.inn,
getOrganizationType(data.cabinetType),
data.referralCode,
data.partnerCode,
)
} else if (data.cabinetType === 'seller') {
result = await registerSellerOrganization({
phone: data.phone.replace(/\D/g, ''),
wbApiKey: data.wbApiKey,
ozonApiKey: data.ozonApiKey,
referralCode: data.referralCode,
partnerCode: data.partnerCode,
})
}

View File

@ -13,9 +13,11 @@ import { AuthLayout } from './auth-layout'
interface PhoneStepProps {
onNext: (phone: string) => void
registrationType?: 'REFERRAL' | 'PARTNER' | null
referrerCode?: string | null
}
export function PhoneStep({ onNext }: PhoneStepProps) {
export function PhoneStep({ onNext, registrationType, referrerCode }: PhoneStepProps) {
const [phone, setPhone] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
@ -96,6 +98,35 @@ export function PhoneStep({ onNext }: PhoneStepProps) {
totalSteps={5}
stepName="Авторизация"
>
{/* Индикатор типа регистрации */}
{registrationType && (
<div className="mb-6 p-4 rounded-xl bg-gradient-to-r from-white/5 to-white/10 border border-white/20">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${
registrationType === 'PARTNER'
? 'bg-purple-500/20 border border-purple-500/30'
: 'bg-blue-500/20 border border-blue-500/30'
}`}>
<span className="text-xl">
{registrationType === 'PARTNER' ? '🤝' : '📎'}
</span>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-white">
{registrationType === 'PARTNER'
? 'Регистрация по партнерской ссылке'
: 'Регистрация по реферальной ссылке'}
</p>
<p className="text-xs text-white/60 mt-1">
{registrationType === 'PARTNER'
? 'Вы получите +100 сфер ⚡ и автоматически станете партнером'
: 'Вы получите +100 сфер ⚡ за регистрацию'}
</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="phone" className="text-white text-sm font-medium flex items-center gap-2">

View File

@ -10,8 +10,6 @@ import {
CreditCard,
Key,
Edit3,
ExternalLink,
Copy,
CheckCircle,
AlertTriangle,
MessageCircle,
@ -53,8 +51,6 @@ export function UserSettings() {
type: 'success' | 'error'
text: string
} | null>(null)
const [partnerLink, setPartnerLink] = useState('')
const [isGenerating, setIsGenerating] = useState(false)
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
const [localAvatarUrl, setLocalAvatarUrl] = useState<string | null>(null)
const phoneInputRef = useRef<HTMLInputElement | null>(null)
@ -278,56 +274,6 @@ export function UserSettings() {
const profileStatus = checkProfileCompleteness()
const isIncomplete = profileStatus.percentage < 100
const generatePartnerLink = async () => {
if (!user?.id) return
setIsGenerating(true)
setSaveMessage(null)
try {
// Генерируем уникальный код партнера
const partnerCode = btoa(user.id + Date.now())
.replace(/[^a-zA-Z0-9]/g, '')
.substring(0, 12)
const link = `${window.location.origin}/register?partner=${partnerCode}`
setPartnerLink(link)
setSaveMessage({
type: 'success',
text: 'Партнерская ссылка сгенерирована!',
})
// TODO: Сохранить партнерский код в базе данных
} catch (error) {
console.error('Error generating partner link:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при генерации ссылки' })
} finally {
setIsGenerating(false)
}
}
const handleCopyLink = async () => {
if (!partnerLink) {
await generatePartnerLink()
return
}
try {
await navigator.clipboard.writeText(partnerLink)
setSaveMessage({ type: 'success', text: 'Ссылка скопирована!' })
} catch (error) {
console.error('Error copying to clipboard:', error)
setSaveMessage({ type: 'error', text: 'Ошибка при копировании' })
}
}
const handleOpenLink = async () => {
if (!partnerLink) {
await generatePartnerLink()
return
}
window.open(partnerLink, '_blank')
}
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
@ -1589,61 +1535,23 @@ export function UserSettings() {
</div>
</div>
{(user?.organization?.type === 'FULFILLMENT' ||
user?.organization?.type === 'LOGIST' ||
user?.organization?.type === 'WHOLESALE') && (
<div className="space-y-6">
<div>
<h4 className="text-white font-medium mb-2">Партнерская программа</h4>
<p className="text-white/70 text-sm mb-4">
Приглашайте новых контрагентов по уникальной ссылке. При регистрации они автоматически
становятся вашими партнерами.
</p>
<div className="space-y-3">
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="glass-secondary text-white hover:text-white cursor-pointer text-xs px-3 py-2"
onClick={generatePartnerLink}
disabled={isGenerating}
>
<RefreshCw className={`h-3 w-3 mr-1 ${isGenerating ? 'animate-spin' : ''}`} />
{isGenerating ? 'Генерируем...' : 'Сгенерировать ссылку'}
</Button>
</div>
{partnerLink && (
<div className="space-y-2">
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="glass-secondary text-white hover:text-white cursor-pointer text-xs px-3 py-2"
onClick={handleOpenLink}
>
<ExternalLink className="h-3 w-3 mr-1" />
Открыть ссылку
</Button>
<Button
size="sm"
variant="outline"
className="glass-secondary text-white hover:text-white cursor-pointer px-2"
onClick={handleCopyLink}
>
<Copy className="h-3 w-3" />
</Button>
</div>
<p className="text-white/60 text-xs">
Ваша партнерская ссылка сгенерирована и готова к использованию
</p>
</div>
)}
</div>
<div className="space-y-6">
<div className="text-center py-12">
<Settings className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">
Инструменты в разработке
</h3>
<p className="text-white/60 text-sm max-w-md mx-auto">
Здесь будут размещены полезные бизнес-инструменты:
калькуляторы, аналитика, планировщики и автоматизация процессов.
</p>
<div className="mt-6">
<Badge variant="outline" className="bg-blue-500/20 text-blue-300 border-blue-500/30">
Скоро появится
</Badge>
</div>
</div>
)}
</div>
</Card>
</TabsContent>
</Tabs>

View File

@ -15,11 +15,15 @@ import {
Mail,
MapPin,
X,
Copy,
Gift,
} from 'lucide-react'
import React, { useState, useMemo } from 'react'
import { toast } from 'sonner'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { GlassInput } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
@ -29,6 +33,7 @@ import {
GET_INCOMING_REQUESTS,
GET_OUTGOING_REQUESTS,
SEARCH_ORGANIZATIONS,
GET_MY_PARTNER_LINK,
} from '@/graphql/queries'
import { OrganizationAvatar } from './organization-avatar'
@ -68,6 +73,7 @@ export function MarketCounterparties() {
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
const { data: incomingData, loading: incomingLoading } = useQuery(GET_INCOMING_REQUESTS)
const { data: outgoingData, loading: outgoingLoading } = useQuery(GET_OUTGOING_REQUESTS)
const { data: partnerLinkData } = useQuery(GET_MY_PARTNER_LINK)
const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST, {
refetchQueries: [
@ -103,6 +109,23 @@ export function MarketCounterparties() {
awaitRefetchQueries: true,
})
// Функция копирования партнерской ссылки
const copyPartnerLink = async () => {
try {
const partnerLink = partnerLinkData?.myPartnerLink
if (!partnerLink) {
toast.error('Партнерская ссылка недоступна')
return
}
await navigator.clipboard.writeText(partnerLink)
toast.success('Партнерская ссылка скопирована!', {
description: 'Поделитесь ей для прямого делового сотрудничества'
})
} catch {
toast.error('Не удалось скопировать ссылку')
}
}
// Фильтрация и сортировка контрагентов
const filteredAndSortedCounterparties = useMemo(() => {
const filtered = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => {
@ -298,6 +321,28 @@ export function MarketCounterparties() {
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-3 flex flex-col">
{/* Блок с партнерской ссылкой */}
<Card className="glass-card p-4 mb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-500/20 border border-purple-500/30">
<Gift className="h-5 w-5 text-purple-400" />
</div>
<div>
<h3 className="text-white font-medium">Пригласить партнера</h3>
<p className="text-white/60 text-sm">Прямое деловое сотрудничество с автоматическим добавлением в партнеры</p>
</div>
</div>
<Button
onClick={copyPartnerLink}
className="glass-button hover:bg-white/20 transition-all duration-200"
>
<Copy className="h-4 w-4 mr-2" />
Копировать ссылку
</Button>
</div>
</Card>
{/* Компактная панель фильтров */}
<div className="glass-card p-3 mb-3 space-y-3">
<div className="flex flex-col xl:flex-row gap-3">

View File

@ -13,6 +13,7 @@ import { MarketFulfillment } from '../market/market-fulfillment'
import { MarketLogistics } from '../market/market-logistics'
import { MarketSellers } from '../market/market-sellers'
import { MarketSuppliers } from '../market/market-suppliers'
import { ReferralsTab } from './referrals-tab'
export function PartnersDashboard() {
const { getSidebarMargin } = useSidebar()
@ -36,7 +37,7 @@ export function PartnersDashboard() {
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
<TabsList
className={`grid w-full grid-cols-5 bg-white/5 backdrop-blur border-white/10 flex-shrink-0 ${
className={`grid w-full grid-cols-6 bg-white/5 backdrop-blur border-white/10 flex-shrink-0 ${
hasIncomingRequests ? 'ring-2 ring-blue-400/50' : ''
}`}
>
@ -75,6 +76,12 @@ export function PartnersDashboard() {
>
Поставщик
</TabsTrigger>
<TabsTrigger
value="referrals"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Рефералы
</TabsTrigger>
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-6">
@ -106,6 +113,10 @@ export function PartnersDashboard() {
<MarketSuppliers />
</Card>
</TabsContent>
<TabsContent value="referrals" className="flex-1 overflow-hidden mt-6">
<ReferralsTab />
</TabsContent>
</Tabs>
</div>
</div>

View File

@ -0,0 +1,420 @@
'use client'
import { useQuery } from '@apollo/client'
import {
Copy,
Gift,
Users,
TrendingUp,
Zap,
UserPlus,
ShoppingCart,
Search,
Filter,
Calendar,
Building,
CheckCircle,
X,
} from 'lucide-react'
import React, { useState, useMemo } from 'react'
import { toast } from 'sonner'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { GlassInput } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { GET_REFERRAL_DASHBOARD_DATA } from '@/graphql/referral-queries'
export function ReferralsTab() {
console.log('🚀 ReferralsTab COMPONENT RENDERED!')
const [searchQuery, setSearchQuery] = useState('')
const [typeFilter, setTypeFilter] = useState<string>('all')
const [sourceFilter, setSourceFilter] = useState<string>('all')
// GraphQL запрос для получения данных
const { data, loading, error } = useQuery(GET_REFERRAL_DASHBOARD_DATA, {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
})
console.log('🔥 ReferralsTab - useQuery result:', {
loading,
hasData: !!data,
error: error?.message,
data
})
// Извлекаем данные из GraphQL ответа или используем fallback для разработки
const referralLink = data?.myReferralLink || 'http://localhost:3000/register?ref=LOADING'
const stats = data?.myReferralStats || {
totalPartners: 0,
totalSpheres: 0,
monthlyPartners: 0,
monthlySpheres: 0,
}
const allReferrals = useMemo(() => data?.myReferrals?.referrals || [], [data])
const copyReferralLink = async () => {
try {
await navigator.clipboard.writeText(referralLink)
toast.success('Реферальная ссылка скопирована!', {
description: 'Теперь вы можете поделиться ей с партнерами',
})
} catch {
toast.error('Не удалось скопировать ссылку')
}
}
// Фильтрация и поиск
const filteredReferrals = useMemo(() => {
return allReferrals.filter((referral) => {
const matchesSearch =
!searchQuery ||
referral.organization.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
referral.organization.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
referral.organization.inn.includes(searchQuery)
const matchesType = typeFilter === 'all' || referral.organization.type === typeFilter
const matchesSource = sourceFilter === 'all' || referral.source === sourceFilter
return matchesSearch && matchesType && matchesSource
})
}, [allReferrals, searchQuery, typeFilter, sourceFilter])
const getTypeLabel = (type: string) => {
switch (type) {
case 'SELLER': return 'Селлер'
case 'WHOLESALE': return 'Поставщик'
case 'FULFILLMENT': return 'Фулфилмент'
case 'LOGIST': return 'Логистика'
default: return type
}
}
const getTypeBadgeStyles = (type: string) => {
switch (type) {
case 'SELLER': return 'bg-green-500/20 text-green-300 border-green-500/30'
case 'WHOLESALE': return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
case 'FULFILLMENT': return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
case 'LOGIST': return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
default: return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
const clearFilters = () => {
setSearchQuery('')
setTypeFilter('all')
setSourceFilter('all')
}
const hasActiveFilters = searchQuery || typeFilter !== 'all' || sourceFilter !== 'all'
return (
<div className="h-full flex flex-col space-y-6">
{/* Блок с реферальной ссылкой */}
<Card className="glass-card p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Gift className="h-5 w-5 text-yellow-400" />
</div>
<h3 className="text-lg font-semibold text-white">Ваша реферальная ссылка</h3>
</div>
<div className="flex items-center gap-4 mb-4">
<div className="flex-1 px-4 py-3 glass-input rounded-lg text-white/60 font-mono text-sm">
</div>
<Button
onClick={copyReferralLink}
className="glass-button hover:bg-white/20 transition-all duration-200"
>
<Copy className="h-4 w-4 mr-2" />
Копировать ссылку
</Button>
</div>
<p className="text-sm text-white/60">
Поделитесь ссылкой с партнерами и получайте <span className="text-yellow-400 font-medium">100 сфер </span> за каждую регистрацию.
Также получайте <span className="text-yellow-400 font-medium">50 сфер </span> при одобрении заявок от новых клиентов.
</p>
</Card>
{/* Статистика */}
<div className="grid grid-cols-4 gap-4">
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-blue-500/20 border border-blue-500/30">
<Users className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Всего партнеров</p>
<p className="text-2xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
) : (
stats.totalPartners
)}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Zap className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Сфер заработано</p>
<div className="flex items-center gap-1">
<p className="text-2xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
) : (
stats.totalSpheres
)}
</p>
<Zap className="h-5 w-5 text-yellow-400" />
</div>
</div>
</div>
</Card>
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-green-500/20 border border-green-500/30">
<TrendingUp className="h-4 w-4 text-green-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Партнеров за месяц</p>
<p className="text-2xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
) : (
stats.monthlyPartners
)}
</p>
</div>
</div>
</Card>
<Card className="glass-card p-4 hover:bg-white/5 transition-all duration-200">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-yellow-500/20 border border-yellow-500/30">
<Zap className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-xs text-white/60 uppercase tracking-wide">Сфер за месяц</p>
<div className="flex items-center gap-1">
<p className="text-2xl font-bold text-white">
{loading ? (
<span className="inline-block h-8 w-12 bg-white/10 rounded animate-pulse" />
) : (
stats.monthlySpheres
)}
</p>
<Zap className="h-5 w-5 text-yellow-400" />
</div>
</div>
</div>
</Card>
</div>
{/* Фильтры */}
<Card className="glass-card p-4">
<div className="flex flex-col xl:flex-row gap-4">
{/* Поиск */}
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<GlassInput
placeholder="Поиск по названию или ИНН..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-9"
/>
</div>
</div>
{/* Фильтры */}
<div className="flex gap-2">
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[130px]">
<Filter className="h-3 w-3 mr-1" />
<SelectValue placeholder="Тип" />
</SelectTrigger>
<SelectContent className="glass-card border-white/20">
<SelectItem value="all">Все типы</SelectItem>
<SelectItem value="SELLER">Селлер</SelectItem>
<SelectItem value="WHOLESALE">Поставщик</SelectItem>
<SelectItem value="FULFILLMENT">Фулфилмент</SelectItem>
<SelectItem value="LOGIST">Логистика</SelectItem>
</SelectContent>
</Select>
<Select value={sourceFilter} onValueChange={setSourceFilter}>
<SelectTrigger className="glass-input text-white border-white/20 h-9 min-w-[140px]">
<Filter className="h-3 w-3 mr-1" />
<SelectValue placeholder="Источник" />
</SelectTrigger>
<SelectContent className="glass-card border-white/20">
<SelectItem value="all">Все источники</SelectItem>
<SelectItem value="REFERRAL_LINK">Реферальная ссылка</SelectItem>
<SelectItem value="AUTO_BUSINESS">Бизнес-сделка</SelectItem>
</SelectContent>
</Select>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-white/60 hover:text-white hover:bg-white/10 h-9 w-9 p-0"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
{/* Краткая статистика */}
<div className="flex items-center justify-between mt-3 text-xs">
<div className="text-white/60">
Показано {filteredReferrals.length} из {allReferrals.length} партнеров
</div>
<div className="flex gap-2">
<div className="flex items-center gap-1 px-2 py-1 rounded-md bg-blue-500/10 text-blue-300">
<UserPlus className="h-3 w-3" />
<span>Рефералы: {allReferrals.filter(r => r.source === 'REFERRAL_LINK').length}</span>
</div>
<div className="flex items-center gap-1 px-2 py-1 rounded-md bg-orange-500/10 text-orange-300">
<ShoppingCart className="h-3 w-3" />
<span>Бизнес: {allReferrals.filter(r => r.source === 'AUTO_BUSINESS').length}</span>
</div>
</div>
</div>
</Card>
{/* Таблица партнеров */}
<Card className="glass-card flex-1 overflow-hidden">
<div className="h-full overflow-auto">
<div className="p-6 space-y-3">
{/* Заголовок таблицы */}
<div className="p-4 rounded-xl bg-gradient-to-r from-white/5 to-white/10 border border-white/10">
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-white/80">
<div className="col-span-2 flex items-center gap-2">
<Calendar className="h-4 w-4 text-blue-400" />
<span>Дата регистрации</span>
</div>
<div className="col-span-3 flex items-center gap-2">
<Building className="h-4 w-4 text-green-400" />
<span>Организация</span>
</div>
<div className="col-span-1 text-center flex items-center justify-center">
<span>Тип</span>
</div>
<div className="col-span-2 text-center flex items-center justify-center gap-1">
<UserPlus className="h-4 w-4 text-purple-400" />
<span>Источник</span>
</div>
<div className="col-span-2 text-center flex items-center justify-center gap-1">
<Zap className="h-4 w-4 text-yellow-400" />
<span>Начислено</span>
</div>
<div className="col-span-2 text-center flex items-center justify-center">
<span>Статус</span>
</div>
</div>
</div>
{/* Строки партнеров */}
{filteredReferrals.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
<Gift className="h-12 w-12 text-white/20 mb-2" />
<p className="text-white/60">
{loading ? 'Загрузка...' : allReferrals.length === 0 ? 'У вас пока нет партнеров' : 'Ничего не найдено'}
</p>
<p className="text-white/40 text-sm mt-1">
{loading
? 'Получаем данные о ваших партнерах...'
: allReferrals.length === 0
? 'Поделитесь реферальной ссылкой или начните работать с клиентами'
: 'Попробуйте изменить параметры поиска'
}
</p>
</div>
) : (
filteredReferrals.map((referral) => (
<div key={referral.id} className="p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-all duration-200 border border-white/10">
<div className="grid grid-cols-12 gap-4 items-center">
<div className="col-span-2 text-white/80">
<div className="flex items-center gap-2">
<Calendar className="h-3 w-3 text-white/40" />
<span className="text-sm">{formatDate(referral.registeredAt)}</span>
</div>
</div>
<div className="col-span-3">
<div>
<p className="text-white font-medium text-sm">
{referral.organization.name || referral.organization.fullName}
</p>
<p className="text-white/60 text-xs flex items-center gap-1">
<Building className="h-3 w-3" />
{referral.organization.inn}
</p>
</div>
</div>
<div className="col-span-1 text-center">
<Badge className={getTypeBadgeStyles(referral.organization.type) + ' text-xs'}>
{getTypeLabel(referral.organization.type)}
</Badge>
</div>
<div className="col-span-2 text-center">
<div className="flex items-center justify-center gap-2">
{referral.source === 'REFERRAL_LINK' ? (
<>
<UserPlus className="h-4 w-4 text-blue-400" />
<span className="text-blue-300 text-sm">Ссылка</span>
</>
) : (
<>
<ShoppingCart className="h-4 w-4 text-orange-400" />
<span className="text-orange-300 text-sm">Бизнес</span>
</>
)}
</div>
</div>
<div className="col-span-2 text-center">
<div className="flex items-center justify-center gap-2">
<span className="text-green-400 font-semibold">+{referral.spheresEarned}</span>
<Zap className="h-4 w-4 text-yellow-400" />
</div>
</div>
<div className="col-span-2 text-center">
<div className="flex items-center justify-center gap-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span className="text-green-300 text-sm">Активен</span>
</div>
</div>
</div>
</div>
))
)}
</div>
</div>
</Card>
</div>
)
}