Files
sfera-new/src/components/market/market-counterparties/index.tsx
Veronika Smirnova 24a6ff74b5 feat: migrate from useAuth to AuthContext for centralized auth state
• Полная миграция 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>
2025-09-19 17:21:52 +03:00

250 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Основной компонент управления контрагентами (Модульная архитектура)
* Объединяет все 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 }