
• Полная миграция 64 компонентов с useAuth на AuthContext • Исправлена race condition в SMS регистрации • Улучшена SSR совместимость с таймаутами • Удалена дублирующая система регистрации • Обновлена документация архитектуры аутентификации Технические изменения: - AuthContext.tsx: централизованная система состояния - auth-flow.tsx: убрана агрессивная логика logout - confirmation-step.tsx: исправлена передача телефона - page.tsx: добавлена синхронизация состояния - 64 файла: миграция useAuth → useAuthContext 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
250 lines
8.4 KiB
TypeScript
250 lines
8.4 KiB
TypeScript
/**
|
||
* Основной компонент управления контрагентами (Модульная архитектура)
|
||
* Объединяет все hooks и блоки в единую систему
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import { Users, ArrowDownCircle, ArrowUpCircle } from 'lucide-react'
|
||
import React, { useState } from 'react'
|
||
import { toast } from 'sonner'
|
||
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||
|
||
// Hooks
|
||
import { CounterpartiesListBlock } from './blocks/CounterpartiesListBlock'
|
||
import { IncomingRequestsBlock } from './blocks/IncomingRequestsBlock'
|
||
import { OutgoingRequestsBlock } from './blocks/OutgoingRequestsBlock'
|
||
import { useCounterpartyActions } from './hooks/useCounterpartyActions'
|
||
import { useCounterpartyData } from './hooks/useCounterpartyData'
|
||
import { useCounterpartyFilters } from './hooks/useCounterpartyFilters'
|
||
|
||
// UI Blocks
|
||
// Types
|
||
import type { Organization } from './types'
|
||
|
||
interface MarketCounterpartiesProps {
|
||
className?: string
|
||
}
|
||
|
||
export default function MarketCounterparties({ className }: MarketCounterpartiesProps) {
|
||
// Состояние активной вкладки
|
||
const [activeTab, setActiveTab] = useState('counterparties')
|
||
|
||
// Data Hooks
|
||
const {
|
||
counterparties,
|
||
incomingRequests,
|
||
outgoingRequests,
|
||
_searchResults,
|
||
partnerLink,
|
||
counterpartiesLoading,
|
||
incomingLoading,
|
||
outgoingLoading,
|
||
_searchLoading,
|
||
_partnerLinkLoading,
|
||
_error,
|
||
refetchAll,
|
||
_searchOrganizations,
|
||
} = useCounterpartyData()
|
||
|
||
// Action Hooks
|
||
const {
|
||
removeCounterparty,
|
||
acceptRequest,
|
||
rejectRequest,
|
||
cancelRequest,
|
||
sendRequest,
|
||
loading: _actionLoading,
|
||
} = useCounterpartyActions()
|
||
|
||
// Filter Hooks
|
||
const {
|
||
_searchQuery,
|
||
_typeFilter,
|
||
_debouncedSearch,
|
||
_handleSearchChange,
|
||
_handleTypeFilterChange,
|
||
} = useCounterpartyFilters({
|
||
onSearch: _searchOrganizations,
|
||
})
|
||
|
||
// Unified loading states for blocks
|
||
const loading = {
|
||
counterparties: counterpartiesLoading,
|
||
incoming: incomingLoading,
|
||
outgoing: outgoingLoading,
|
||
search: _searchLoading,
|
||
}
|
||
|
||
// Обработчики действий с callback для обновления данных
|
||
const handleRemoveCounterparty = async (id: string) => {
|
||
try {
|
||
await removeCounterparty(id)
|
||
await refetchAll()
|
||
toast.success('Контрагент удален')
|
||
} catch {
|
||
toast.error('Ошибка удаления контрагента')
|
||
}
|
||
}
|
||
|
||
const handleAcceptRequest = async (id: string) => {
|
||
try {
|
||
await acceptRequest(id)
|
||
await refetchAll()
|
||
toast.success('Заявка принята')
|
||
} catch {
|
||
toast.error('Ошибка принятия заявки')
|
||
}
|
||
}
|
||
|
||
const handleRejectRequest = async (id: string) => {
|
||
try {
|
||
await rejectRequest(id)
|
||
await refetchAll()
|
||
toast.success('Заявка отклонена')
|
||
} catch {
|
||
toast.error('Ошибка отклонения заявки')
|
||
}
|
||
}
|
||
|
||
const handleCancelRequest = async (id: string) => {
|
||
try {
|
||
await cancelRequest(id)
|
||
await refetchAll()
|
||
toast.success('Заявка отменена')
|
||
} catch {
|
||
toast.error('Ошибка отмены заявки')
|
||
}
|
||
}
|
||
|
||
const _handleSendRequest = async (organizationId: string, message?: string) => {
|
||
try {
|
||
await sendRequest(organizationId, message)
|
||
await refetchAll()
|
||
toast.success('Заявка отправлена')
|
||
} catch {
|
||
toast.error('Ошибка отправки заявки')
|
||
}
|
||
}
|
||
|
||
const handleCopyLink = (url: string) => {
|
||
navigator.clipboard.writeText(url)
|
||
toast.success('Ссылка скопирована в буфер обмена')
|
||
}
|
||
|
||
const _handleGenerateLink = async () => {
|
||
try {
|
||
// TODO: Реализовать создание партнерской ссылки
|
||
await refetchAll()
|
||
toast.success('Партнерская ссылка создана')
|
||
} catch {
|
||
toast.error('Ошибка создания ссылки')
|
||
}
|
||
}
|
||
|
||
// Обработчик просмотра деталей организации
|
||
const handleViewDetails = (organization: Organization) => {
|
||
// TODO: Реализовать модальное окно с деталями организации
|
||
toast.info(`Детали организации: ${organization.name || organization.fullName}`)
|
||
}
|
||
|
||
// Подсчет уведомлений для вкладок
|
||
const pendingIncomingCount = incomingRequests.filter(req => req.status === 'PENDING').length
|
||
const pendingOutgoingCount = outgoingRequests.filter(req => req.status === 'PENDING').length
|
||
|
||
return (
|
||
<div className={className}>
|
||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||
<TabsList className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border-white/10">
|
||
{/* Контрагенты */}
|
||
<TabsTrigger
|
||
value="counterparties"
|
||
className="flex items-center space-x-2 data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300 text-white/70"
|
||
>
|
||
<Users className="h-4 w-4" />
|
||
<span>Контрагенты</span>
|
||
{counterparties.length > 0 && (
|
||
<span className="ml-1 px-1.5 py-0.5 text-xs bg-blue-500/20 text-blue-300 rounded-full border border-blue-500/30">
|
||
{counterparties.length}
|
||
</span>
|
||
)}
|
||
</TabsTrigger>
|
||
|
||
{/* Входящие заявки */}
|
||
<TabsTrigger
|
||
value="incoming"
|
||
className={`flex items-center space-x-2 data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300 text-white/70 relative ${
|
||
pendingIncomingCount > 0 ? 'animate-pulse ring-2 ring-green-400/50' : ''
|
||
}`}
|
||
>
|
||
<ArrowDownCircle className="h-4 w-4" />
|
||
<span>Входящие</span>
|
||
{pendingIncomingCount > 0 && (
|
||
<>
|
||
<span className="ml-1 px-1.5 py-0.5 text-xs bg-green-500/20 text-green-300 rounded-full border border-green-500/30">
|
||
{pendingIncomingCount}
|
||
</span>
|
||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
||
</>
|
||
)}
|
||
</TabsTrigger>
|
||
|
||
{/* Исходящие заявки */}
|
||
<TabsTrigger
|
||
value="outgoing"
|
||
className="flex items-center space-x-2 data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300 text-white/70"
|
||
>
|
||
<ArrowUpCircle className="h-4 w-4" />
|
||
<span>Исходящие</span>
|
||
{pendingOutgoingCount > 0 && (
|
||
<span className="ml-1 px-1.5 py-0.5 text-xs bg-orange-500/20 text-orange-300 rounded-full border border-orange-500/30">
|
||
{pendingOutgoingCount}
|
||
</span>
|
||
)}
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* Контент вкладок */}
|
||
|
||
{/* Список контрагентов */}
|
||
<TabsContent value="counterparties" className="mt-6">
|
||
<CounterpartiesListBlock
|
||
counterparties={counterparties}
|
||
loading={loading.counterparties}
|
||
onRemove={handleRemoveCounterparty}
|
||
onViewDetails={handleViewDetails}
|
||
incomingRequestsCount={incomingRequests.length}
|
||
outgoingRequestsCount={outgoingRequests.length}
|
||
incomingLoading={loading.incoming}
|
||
outgoingLoading={loading.outgoing}
|
||
partnerLink={partnerLink}
|
||
onCopyPartnerLink={handleCopyLink}
|
||
/>
|
||
</TabsContent>
|
||
|
||
{/* Входящие заявки */}
|
||
<TabsContent value="incoming" className="mt-6">
|
||
<IncomingRequestsBlock
|
||
requests={incomingRequests}
|
||
loading={loading.incoming}
|
||
onAccept={handleAcceptRequest}
|
||
onReject={handleRejectRequest}
|
||
/>
|
||
</TabsContent>
|
||
|
||
{/* Исходящие заявки */}
|
||
<TabsContent value="outgoing" className="mt-6">
|
||
<OutgoingRequestsBlock
|
||
requests={outgoingRequests}
|
||
loading={loading.outgoing}
|
||
onCancel={handleCancelRequest}
|
||
/>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Экспорт для обратной совместимости
|
||
export { MarketCounterparties } |