Реализация реферальной системы и улучшение системы авторизации
- Добавлена полная реферальная система с 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:
@ -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}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
420
src/components/partners/referrals-tab.tsx
Normal file
420
src/components/partners/referrals-tab.tsx
Normal 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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user