fix: исправить критические ошибки системы партнерских заявок

КРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ:
- Исправлено отображение входящих заявок (неправильное извлечение данных)
- Устранен ApolloError при принятии заявок (неправильная структура мутаций)
- Исправлено отображение контрагентов после принятия заявки
- Обновлены типы возврата GraphQL мутаций для соответствия резолверам

UI/UX УЛУЧШЕНИЯ:
- Обновлены все компоненты на темную glass-morphism тему
- Компактные карточки контрагентов (удалена избыточная информация)
- Удален дублирующий блок поиска новых партнеров

ЗАТРОНУТЫЕ ФАЙЛЫ:
- useCounterpartyData.ts: исправлено извлечение данных
- useCounterpartyActions.ts: исправлены структуры мутаций
- IncomingRequestsBlock.tsx: темная тема + исправления UI
- OutgoingRequestsBlock.tsx: темная тема
- CounterpartiesListBlock.tsx: компактные карточки + темная тема
- typedefs.ts: исправлены типы возврата мутаций

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-09-19 23:23:03 +03:00
parent ca4d44d090
commit fe24b73634
15 changed files with 3050 additions and 561 deletions

View File

@ -5,7 +5,20 @@
'use client'
import { Users, ArrowDownCircle, TrendingUp, ArrowUpCircle, Building, Phone, Mail, MapPin, X, Calendar, Gift, Copy, Search, SortAsc, SortDesc, Send } from 'lucide-react'
import {
Users,
ArrowDownCircle,
TrendingUp,
ArrowUpCircle,
Building,
X,
Calendar,
Gift,
Copy,
Search,
SortAsc,
SortDesc,
} from 'lucide-react'
import React from 'react'
import { Badge } from '@/components/ui/badge'
@ -38,26 +51,18 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
onSort,
filteredCount,
totalCount,
// Поиск новых организаций
searchResults = [],
searchLoading = false,
onSendRequest,
searchNewQuery = '',
onSearchNewChange,
searchNewTypeFilter = 'all',
onSearchNewTypeFilterChange,
}: CounterpartiesListBlockProps) {
if (loading) {
return (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i} className="p-6">
<Card key={i} className="glass-card p-6">
<div className="animate-pulse">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 bg-gray-200 rounded-full"></div>
<div className="h-12 w-12 bg-white/10 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
<div className="h-4 bg-white/10 rounded w-3/4"></div>
<div className="h-3 bg-white/10 rounded w-1/2"></div>
</div>
</div>
</div>
@ -71,14 +76,12 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
const emptyState = !counterparties.length && (
<Card className="glass-card p-8 text-center">
<div className="flex flex-col items-center space-y-4">
<div className="h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center">
<Building className="h-8 w-8 text-gray-400" />
<div className="h-16 w-16 bg-white/10 rounded-full flex items-center justify-center">
<Building className="h-8 w-8 text-white/40" />
</div>
<div>
<h3 className="text-lg font-medium text-white">Контрагенты не найдены</h3>
<p className="text-white/60 mt-1">
Начните отправлять заявки на партнерство другим организациям
</p>
<p className="text-white/60 mt-1">Начните отправлять заявки на партнерство другим организациям</p>
</div>
</div>
</Card>
@ -179,9 +182,7 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
</div>
<h3 className="text-base font-semibold text-white">Партнерская ссылка</h3>
</div>
<div className="text-xs text-white/60">
Прямое деловое сотрудничество с автоматическим добавлением
</div>
<div className="text-xs text-white/60">Прямое деловое сотрудничество с автоматическим добавлением</div>
</div>
<div className="flex items-center gap-3">
<div className="flex-1 px-3 py-2 glass-input rounded-lg text-white/60 font-mono text-sm truncate">
@ -211,7 +212,7 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
icon={Search}
/>
</div>
{/* Фильтр по типу */}
<div className="w-full xl:w-48">
<Select value={typeFilter} onValueChange={onTypeFilterChange}>
@ -244,17 +245,8 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
</div>
{/* Порядок сортировки */}
<Button
variant="outline"
size="sm"
onClick={() => onSort?.(sortField)}
className="glass-button"
>
{sortOrder === 'asc' ? (
<SortAsc className="h-4 w-4" />
) : (
<SortDesc className="h-4 w-4" />
)}
<Button variant="outline" size="sm" onClick={() => onSort?.(sortField)} className="glass-button">
{sortOrder === 'asc' ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
</Button>
{/* Сброс фильтров */}
@ -277,16 +269,18 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
<div className="flex items-center justify-between text-xs text-white/60 mt-3">
<div>
{filteredCount !== undefined && totalCount !== undefined ? (
<>Показано {filteredCount} из {totalCount} контрагентов</>
<>
Показано {filteredCount} из {totalCount} контрагентов
</>
) : (
<>Показано {counterparties.length} контрагентов</>
)}
</div>
{/* Быстрые фильтры по типам */}
<div className="flex gap-1">
{(['FULFILLMENT', 'SELLER', 'LOGIST', 'WHOLESALE'] as const).map((type) => {
const count = counterparties.filter(org => org.type === type).length
const count = counterparties.filter((org) => org.type === type).length
return count > 0 ? (
<Button
key={type}
@ -294,9 +288,7 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
size="sm"
onClick={() => onTypeFilterChange?.(type)}
className={`text-xs px-2 py-1 ${
typeFilter === type
? 'bg-blue-500/20 text-blue-300'
: 'text-white/40 hover:text-white/70'
typeFilter === type ? 'bg-blue-500/20 text-blue-300' : 'text-white/40 hover:text-white/70'
}`}
>
{ORGANIZATION_TYPES[type]} ({count})
@ -316,58 +308,24 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Аватар организации */}
<OrganizationAvatar
organization={org}
size="lg"
className="flex-shrink-0"
/>
<OrganizationAvatar organization={org} size="lg" className="flex-shrink-0" />
{/* Основная информация */}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{org.name || org.fullName}
</h3>
<Badge variant="outline" className="flex-shrink-0">
<h3 className="text-lg font-semibold text-white truncate">{org.name || org.fullName}</h3>
<Badge variant="outline" className="border-white/20 text-white/80 flex-shrink-0">
{ORGANIZATION_TYPES[org.type]}
</Badge>
</div>
{/* ИНН */}
<p className="text-sm text-gray-600 mb-2">
ИНН: {org.inn}
</p>
{/* Контактная информация */}
<div className="space-y-1">
{org.address && (
<div className="flex items-center space-x-2 text-sm text-gray-600">
<MapPin className="h-4 w-4" />
<span className="truncate">{org.address}</span>
</div>
)}
{org.phones && org.phones.length > 0 && (
<div className="flex items-center space-x-2 text-sm text-gray-600">
<Phone className="h-4 w-4" />
<span>{org.phones[0].value}</span>
</div>
)}
{org.emails && org.emails.length > 0 && (
<div className="flex items-center space-x-2 text-sm text-gray-600">
<Mail className="h-4 w-4" />
<span>{org.emails[0].value}</span>
</div>
)}
</div>
<p className="text-sm text-white/70 mb-2">ИНН: {org.inn}</p>
{/* Дата добавления */}
<div className="flex items-center space-x-2 text-xs text-gray-500 mt-3">
<div className="flex items-center space-x-2 text-xs text-white/50 mt-2">
<Calendar className="h-3 w-3" />
<span>
Партнеры с {new Date(org.createdAt).toLocaleDateString('ru-RU')}
</span>
<span>Партнеры с {new Date(org.createdAt).toLocaleDateString('ru-RU')}</span>
</div>
</div>
</div>
@ -375,20 +333,16 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
{/* Действия */}
<div className="flex items-center space-x-2 flex-shrink-0 ml-4">
{onViewDetails && (
<Button
variant="outline"
size="sm"
onClick={() => onViewDetails(org)}
>
<Button variant="outline" size="sm" onClick={() => onViewDetails(org)} className="glass-button">
Подробнее
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => onRemove(org.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 hover:border-red-400/50"
>
<X className="h-4 w-4" />
</Button>
@ -400,142 +354,12 @@ export const CounterpartiesListBlock = React.memo(function CounterpartiesListBlo
{/* Статистика */}
{counterparties.length > 0 && (
<div className="text-center text-sm text-white/60 pt-4">
Показано {counterparties.length} контрагент{counterparties.length === 1 ? '' :
counterparties.length < 5 ? 'а' : 'ов'}
Показано {counterparties.length} контрагент
{counterparties.length === 1 ? '' : counterparties.length < 5 ? 'а' : 'ов'}
</div>
)}
{/* Поиск новых организаций (интеграция функций из удаленной вкладки "Поиск") */}
<Card className="glass-card p-4 mt-6">
<div className="flex items-center gap-2 mb-4">
<Search className="h-5 w-5 text-blue-400" />
<h3 className="text-lg font-semibold text-white">Поиск новых партнеров</h3>
</div>
{/* Фильтры поиска новых организаций */}
<div className="flex flex-col md:flex-row gap-4 mb-4">
<div className="flex-1">
<GlassInput
placeholder="Поиск новых организаций по названию, ИНН..."
value={searchNewQuery}
onChange={(e) => onSearchNewChange?.(e.target.value)}
icon={Search}
/>
</div>
<div className="w-full md:w-48">
<Select value={searchNewTypeFilter} onValueChange={onSearchNewTypeFilterChange}>
<SelectTrigger className="glass-input">
<SelectValue placeholder="Тип организации" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Все типы</SelectItem>
<SelectItem value="FULFILLMENT">Фулфилмент</SelectItem>
<SelectItem value="SELLER">Селлеры</SelectItem>
<SelectItem value="LOGIST">Логистика</SelectItem>
<SelectItem value="WHOLESALE">Поставщики</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Результаты поиска новых организаций */}
{searchLoading && (
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="glass-card p-4 animate-pulse">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 bg-white/10 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-white/10 rounded w-3/4"></div>
<div className="h-3 bg-white/10 rounded w-1/2"></div>
</div>
<div className="h-8 w-20 bg-white/10 rounded"></div>
</div>
</div>
))}
</div>
)}
{!searchLoading && searchNewQuery && !searchResults.length && (
<div className="text-center py-8">
<div className="h-16 w-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Search className="h-8 w-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-white">Организации не найдены</h3>
<p className="text-white/60 mt-1">Попробуйте изменить параметры поиска</p>
</div>
)}
{!searchNewQuery && (
<div className="text-center py-8">
<div className="h-16 w-16 bg-blue-500/20 rounded-full flex items-center justify-center mx-auto mb-4 border border-blue-500/30">
<Search className="h-8 w-8 text-blue-400" />
</div>
<h3 className="text-lg font-medium text-white">Поиск новых партнеров</h3>
<p className="text-white/60 mt-1">Введите название или ИНН организации для поиска</p>
</div>
)}
{/* Список найденных организаций */}
{!searchLoading && searchResults.length > 0 && (
<div className="space-y-4">
{searchResults.map((org) => (
<div key={org.id} className="glass-card p-4">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<OrganizationAvatar organization={org} size="lg" className="flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h3 className="text-lg font-semibold text-white truncate">
{org.name || org.fullName}
</h3>
<Badge variant="outline">{ORGANIZATION_TYPES[org.type]}</Badge>
{org.isCounterparty && (
<Badge variant="secondary" className="bg-green-100 text-green-800">
Уже партнер
</Badge>
)}
{org.hasOutgoingRequest && (
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
Заявка отправлена
</Badge>
)}
</div>
<p className="text-sm text-white/60 mb-2">ИНН: {org.inn}</p>
{org.address && (
<p className="text-sm text-white/60 truncate">{org.address}</p>
)}
</div>
</div>
{/* Кнопка отправки заявки */}
<div className="flex items-center space-x-2 flex-shrink-0 ml-4">
{!org.isCounterparty && !org.hasOutgoingRequest && (
<Button
size="sm"
onClick={() => onSendRequest?.(org.id)}
className="glass-button"
>
<Send className="h-4 w-4 mr-1" />
Отправить заявку
</Button>
)}
</div>
</div>
</div>
))}
<div className="text-center text-sm text-white/60 pt-4">
Найдено {searchResults.length} организаци{searchResults.length === 1 ? 'я' :
searchResults.length < 5 ? 'и' : 'й'}
</div>
</div>
)}
</Card>
</div>
)
})
CounterpartiesListBlock.displayName = 'CounterpartiesListBlock'
CounterpartiesListBlock.displayName = 'CounterpartiesListBlock'

View File

@ -25,17 +25,17 @@ export const IncomingRequestsBlock = React.memo(function IncomingRequestsBlock({
return (
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
<Card key={i} className="p-6">
<Card key={i} className="glass-card p-6">
<div className="animate-pulse">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 bg-gray-200 rounded-full"></div>
<div className="h-12 w-12 bg-white/10 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
<div className="h-4 bg-white/10 rounded w-3/4"></div>
<div className="h-3 bg-white/10 rounded w-1/2"></div>
</div>
<div className="flex space-x-2">
<div className="h-8 w-16 bg-gray-200 rounded"></div>
<div className="h-8 w-16 bg-gray-200 rounded"></div>
<div className="h-8 w-16 bg-white/10 rounded"></div>
<div className="h-8 w-16 bg-white/10 rounded"></div>
</div>
</div>
</div>
@ -47,16 +47,14 @@ export const IncomingRequestsBlock = React.memo(function IncomingRequestsBlock({
if (!requests.length) {
return (
<Card className="p-8 text-center">
<Card className="glass-card p-8 text-center">
<div className="flex flex-col items-center space-y-4">
<div className="h-16 w-16 bg-blue-100 rounded-full flex items-center justify-center">
<ArrowDownCircle className="h-8 w-8 text-blue-500" />
<div className="h-16 w-16 bg-white/10 rounded-full flex items-center justify-center">
<ArrowDownCircle className="h-8 w-8 text-white/40" />
</div>
<div>
<h3 className="text-lg font-medium text-gray-900">Входящих заявок нет</h3>
<p className="text-gray-500 mt-1">
Когда другие организации отправят вам заявки, они появятся здесь
</p>
<h3 className="text-lg font-medium text-white">Входящих заявок нет</h3>
<p className="text-white/60 mt-1">Когда другие организации отправят вам заявки, они появятся здесь</p>
</div>
</div>
</Card>
@ -66,47 +64,40 @@ export const IncomingRequestsBlock = React.memo(function IncomingRequestsBlock({
return (
<div className="space-y-4">
{requests.map((request) => (
<Card key={request.id} className="p-6 border-l-4 border-l-blue-500">
<Card key={request.id} className="glass-card p-6 border-l-4 border-l-blue-400/50">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Аватар отправителя */}
<OrganizationAvatar
organization={request.sender}
size="lg"
className="flex-shrink-0"
/>
<OrganizationAvatar organization={request.sender} size="lg" className="flex-shrink-0" />
{/* Информация о заявке */}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
<h3 className="text-lg font-semibold text-white truncate">
{request.sender.name || request.sender.fullName}
</h3>
<Badge variant="outline">
<Badge variant="outline" className="border-white/20 text-white/80">
{ORGANIZATION_TYPES[request.sender.type]}
</Badge>
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
Новая заявка
</Badge>
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30">Новая заявка</Badge>
</div>
{/* ИНН отправителя */}
<p className="text-sm text-gray-600 mb-2">
ИНН: {request.sender.inn}
</p>
<p className="text-sm text-white/70 mb-2">ИНН: {request.sender.inn}</p>
{/* Сообщение заявки */}
{request.message && (
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<p className="text-sm text-gray-700">{request.message}</p>
<div className="bg-white/5 rounded-lg p-3 mb-3 border border-white/10">
<p className="text-sm text-white/80">{request.message}</p>
</div>
)}
{/* Дата заявки */}
<div className="flex items-center space-x-2 text-xs text-gray-500">
<div className="flex items-center space-x-2 text-xs text-white/50">
<Calendar className="h-3 w-3" />
<span>
Заявка от {new Date(request.createdAt).toLocaleDateString('ru-RU', {
Заявка от{' '}
{new Date(request.createdAt).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
hour: '2-digit',
@ -123,17 +114,17 @@ export const IncomingRequestsBlock = React.memo(function IncomingRequestsBlock({
variant="outline"
size="sm"
onClick={() => onAccept(request.id)}
className="text-green-600 hover:text-green-700 hover:bg-green-50"
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30 hover:border-green-400/50"
>
<CheckCircle className="h-4 w-4 mr-1" />
Принять
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onReject(request.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 hover:border-red-400/50"
>
<XCircle className="h-4 w-4 mr-1" />
Отклонить
@ -144,12 +135,12 @@ export const IncomingRequestsBlock = React.memo(function IncomingRequestsBlock({
))}
{/* Статистика */}
<div className="text-center text-sm text-gray-500 pt-4">
{requests.length} входящ{requests.length === 1 ? 'ая заявка' :
requests.length < 5 ? 'ие заявки' : 'их заявок'} ожида{requests.length === 1 ? 'ет' : 'ют'} рассмотрения
<div className="text-center text-sm text-white/50 pt-4">
{requests.length} входящ{requests.length === 1 ? 'ая заявка' : requests.length < 5 ? 'ие заявки' : 'их заявок'}{' '}
ожида{requests.length === 1 ? 'ет' : 'ют'} рассмотрения
</div>
</div>
)
})
IncomingRequestsBlock.displayName = 'IncomingRequestsBlock'
IncomingRequestsBlock.displayName = 'IncomingRequestsBlock'

View File

@ -24,15 +24,15 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
return (
<div className="space-y-4">
{Array.from({ length: 2 }).map((_, i) => (
<Card key={i} className="p-6">
<Card key={i} className="glass-card p-6">
<div className="animate-pulse">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 bg-gray-200 rounded-full"></div>
<div className="h-12 w-12 bg-white/10 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
<div className="h-4 bg-white/10 rounded w-3/4"></div>
<div className="h-3 bg-white/10 rounded w-1/2"></div>
</div>
<div className="h-8 w-16 bg-gray-200 rounded"></div>
<div className="h-8 w-16 bg-white/10 rounded"></div>
</div>
</div>
</Card>
@ -43,16 +43,14 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
if (!requests.length) {
return (
<Card className="p-8 text-center">
<Card className="glass-card p-8 text-center">
<div className="flex flex-col items-center space-y-4">
<div className="h-16 w-16 bg-orange-100 rounded-full flex items-center justify-center">
<ArrowUpCircle className="h-8 w-8 text-orange-500" />
<div className="h-16 w-16 bg-white/10 rounded-full flex items-center justify-center">
<ArrowUpCircle className="h-8 w-8 text-white/40" />
</div>
<div>
<h3 className="text-lg font-medium text-gray-900">Исходящих заявок нет</h3>
<p className="text-gray-500 mt-1">
Найдите организации для сотрудничества и отправьте им заявки
</p>
<h3 className="text-lg font-medium text-white">Исходящих заявок нет</h3>
<p className="text-white/60 mt-1">Найдите организации для сотрудничества и отправьте им заявки</p>
</div>
</div>
</Card>
@ -62,33 +60,28 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
return (
<div className="space-y-4">
{requests.map((request) => (
<Card key={request.id} className="p-6 border-l-4 border-l-orange-500">
<Card key={request.id} className="glass-card p-6 border-l-4 border-l-orange-400/50">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
{/* Аватар получателя */}
<OrganizationAvatar
organization={request.receiver}
size="lg"
className="flex-shrink-0"
/>
<OrganizationAvatar organization={request.receiver} size="lg" className="flex-shrink-0" />
{/* Информация о заявке */}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
<h3 className="text-lg font-semibold text-white truncate">
{request.receiver.name || request.receiver.fullName}
</h3>
<Badge variant="outline">
<Badge variant="outline" className="border-white/20 text-white/80">
{ORGANIZATION_TYPES[request.receiver.type]}
</Badge>
<Badge
variant="secondary"
<Badge
className={
request.status === 'PENDING'
? 'bg-yellow-100 text-yellow-800'
request.status === 'PENDING'
? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
: request.status === 'ACCEPTED'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
? 'bg-green-500/20 text-green-300 border-green-500/30'
: 'bg-red-500/20 text-red-300 border-red-500/30'
}
>
{REQUEST_STATUSES[request.status]}
@ -96,22 +89,21 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
</div>
{/* ИНН получателя */}
<p className="text-sm text-gray-600 mb-2">
ИНН: {request.receiver.inn}
</p>
<p className="text-sm text-white/70 mb-2">ИНН: {request.receiver.inn}</p>
{/* Сообщение заявки */}
{request.message && (
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<p className="text-sm text-gray-700">{request.message}</p>
<div className="bg-white/5 rounded-lg p-3 mb-3 border border-white/10">
<p className="text-sm text-white/80">{request.message}</p>
</div>
)}
{/* Дата заявки */}
<div className="flex items-center space-x-2 text-xs text-gray-500">
<div className="flex items-center space-x-2 text-xs text-white/50">
<Calendar className="h-3 w-3" />
<span>
Отправлена {new Date(request.createdAt).toLocaleDateString('ru-RU', {
Отправлена{' '}
{new Date(request.createdAt).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
hour: '2-digit',
@ -129,7 +121,7 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
variant="outline"
size="sm"
onClick={() => onCancel(request.id)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 hover:border-red-400/50"
>
<X className="h-4 w-4 mr-1" />
Отменить
@ -137,15 +129,11 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
)}
{request.status === 'ACCEPTED' && (
<Badge variant="secondary" className="bg-green-100 text-green-800">
Принята
</Badge>
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">Принята</Badge>
)}
{request.status === 'REJECTED' && (
<Badge variant="secondary" className="bg-red-100 text-red-800">
Отклонена
</Badge>
<Badge className="bg-red-500/20 text-red-300 border-red-500/30">Отклонена</Badge>
)}
</div>
</div>
@ -153,12 +141,11 @@ export const OutgoingRequestsBlock = React.memo(function OutgoingRequestsBlock({
))}
{/* Статистика */}
<div className="text-center text-sm text-gray-500 pt-4">
{requests.length} исходящ{requests.length === 1 ? 'ая заявка' :
requests.length < 5 ? 'ие заявки' : 'их заявок'}
<div className="text-center text-sm text-white/50 pt-4">
{requests.length} исходящ{requests.length === 1 ? 'ая заявка' : requests.length < 5 ? 'ие заявки' : 'их заявок'}
</div>
</div>
)
})
OutgoingRequestsBlock.displayName = 'OutgoingRequestsBlock'
OutgoingRequestsBlock.displayName = 'OutgoingRequestsBlock'

View File

@ -76,148 +76,169 @@ export function useCounterpartyActions(): UseCounterpartyActionsReturn {
})
// Принять заявку на партнерство
const acceptRequest = useCallback(async (requestId: string) => {
setLoading(true)
setError(null)
const acceptRequest = useCallback(
async (requestId: string) => {
setLoading(true)
setError(null)
try {
const { data } = await respondToRequestMutation({
variables: {
requestId,
response: 'ACCEPTED',
},
})
try {
const { data } = await respondToRequestMutation({
variables: {
input: {
requestId,
action: 'APPROVE',
},
},
})
if (data?.respondToCounterpartyRequest?.success) {
toast.success('Заявка принята! Организация добавлена в контрагенты')
} else {
const errorMessage = data?.respondToCounterpartyRequest?.message || 'Не удалось принять заявку'
if (data?.respondToCounterpartyRequest?.success) {
toast.success('Заявка принята! Организация добавлена в контрагенты')
} else {
const errorMessage = data?.respondToCounterpartyRequest?.message || 'Не удалось принять заявку'
setError(errorMessage)
toast.error(errorMessage)
}
} catch (error) {
console.error('Error accepting request:', error)
const errorMessage = 'Ошибка при принятии заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
} catch (error) {
console.error('Error accepting request:', error)
const errorMessage = 'Ошибка при принятии заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [respondToRequestMutation])
},
[respondToRequestMutation],
)
// Отклонить заявку на партнерство
const rejectRequest = useCallback(async (requestId: string) => {
setLoading(true)
setError(null)
const rejectRequest = useCallback(
async (requestId: string) => {
setLoading(true)
setError(null)
try {
const { data } = await respondToRequestMutation({
variables: {
requestId,
response: 'REJECTED',
},
})
try {
const { data } = await respondToRequestMutation({
variables: {
input: {
requestId,
action: 'REJECT',
},
},
})
if (data?.respondToCounterpartyRequest?.success) {
toast.success('Заявка отклонена')
} else {
const errorMessage = data?.respondToCounterpartyRequest?.message || 'Не удалось отклонить заявку'
if (data?.respondToCounterpartyRequest?.success) {
toast.success('Заявка отклонена')
} else {
const errorMessage = data?.respondToCounterpartyRequest?.message || 'Не удалось отклонить заявку'
setError(errorMessage)
toast.error(errorMessage)
}
} catch (error) {
console.error('Error rejecting request:', error)
const errorMessage = 'Ошибка при отклонении заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
} catch (error) {
console.error('Error rejecting request:', error)
const errorMessage = 'Ошибка при отклонении заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [respondToRequestMutation])
},
[respondToRequestMutation],
)
// Отменить исходящую заявку
const cancelRequest = useCallback(async (requestId: string) => {
setLoading(true)
setError(null)
const cancelRequest = useCallback(
async (requestId: string) => {
setLoading(true)
setError(null)
try {
const { data } = await cancelRequestMutation({
variables: { requestId },
})
try {
const { data } = await cancelRequestMutation({
variables: { requestId },
})
if (data?.cancelCounterpartyRequest?.success) {
toast.success('Заявка отменена')
} else {
const errorMessage = data?.cancelCounterpartyRequest?.message || 'Не удалось отменить заявку'
if (data?.cancelCounterpartyRequest?.success) {
toast.success('Заявка отменена')
} else {
const errorMessage = data?.cancelCounterpartyRequest?.message || 'Не удалось отменить заявку'
setError(errorMessage)
toast.error(errorMessage)
}
} catch (error) {
console.error('Error canceling request:', error)
const errorMessage = 'Ошибка при отмене заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
} catch (error) {
console.error('Error canceling request:', error)
const errorMessage = 'Ошибка при отмене заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [cancelRequestMutation])
},
[cancelRequestMutation],
)
// Отправить заявку на партнерство
const sendRequest = useCallback(async (organizationId: string, message?: string) => {
setLoading(true)
setError(null)
const sendRequest = useCallback(
async (organizationId: string, message?: string) => {
setLoading(true)
setError(null)
try {
const { data } = await sendRequestMutation({
variables: {
organizationId,
message: message || 'Предлагаем партнерское сотрудничество',
},
})
try {
const { data } = await sendRequestMutation({
variables: {
input: {
receiverId: organizationId,
message: message || 'Предлагаем партнерское сотрудничество',
},
},
})
if (data?.sendCounterpartyRequest?.success) {
toast.success('Заявка отправлена! Ожидайте ответа от организации')
} else {
const errorMessage = data?.sendCounterpartyRequest?.message || 'Не удалось отправить заявку'
if (data?.sendCounterpartyRequest?.success) {
toast.success('Заявка отправлена! Ожидайте ответа от организации')
} else {
const errorMessage = data?.sendCounterpartyRequest?.message || 'Не удалось отправить заявку'
setError(errorMessage)
toast.error(errorMessage)
}
} catch (error) {
console.error('Error sending request:', error)
const errorMessage = 'Ошибка при отправке заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
} catch (error) {
console.error('Error sending request:', error)
const errorMessage = 'Ошибка при отправке заявки'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [sendRequestMutation])
},
[sendRequestMutation],
)
// Удалить контрагента
const removeCounterparty = useCallback(async (organizationId: string) => {
setLoading(true)
setError(null)
const removeCounterparty = useCallback(
async (organizationId: string) => {
setLoading(true)
setError(null)
try {
const { data } = await removeCounterpartyMutation({
variables: { organizationId },
})
try {
const { data } = await removeCounterpartyMutation({
variables: { organizationId },
})
if (data?.removeCounterparty?.success) {
toast.success('Контрагент удален из списка')
} else {
const errorMessage = data?.removeCounterparty?.message || 'Не удалось удалить контрагента'
if (data?.removeCounterparty?.success) {
toast.success('Контрагент удален из списка')
} else {
const errorMessage = data?.removeCounterparty?.message || 'Не удалось удалить контрагента'
setError(errorMessage)
toast.error(errorMessage)
}
} catch (error) {
console.error('Error removing counterparty:', error)
const errorMessage = 'Ошибка при удалении контрагента'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
} catch (error) {
console.error('Error removing counterparty:', error)
const errorMessage = 'Ошибка при удалении контрагента'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [removeCounterpartyMutation])
},
[removeCounterpartyMutation],
)
return {
// Действия с заявками
@ -233,4 +254,4 @@ export function useCounterpartyActions(): UseCounterpartyActionsReturn {
loading,
error,
}
}
}

View File

@ -14,12 +14,7 @@ import {
} from '@/graphql/queries'
import { GET_MY_PARTNER_LINK } from '@/graphql/referral-queries'
import type {
UseCounterpartyDataReturn,
Organization,
CounterpartyRequest,
OrganizationType,
} from '../types'
import type { UseCounterpartyDataReturn, Organization, CounterpartyRequest, OrganizationType } from '../types'
export function useCounterpartyData(): UseCounterpartyDataReturn {
const [searchResults, setSearchResults] = useState<Organization[]>([])
@ -45,8 +40,21 @@ export function useCounterpartyData(): UseCounterpartyDataReturn {
refetch: refetchIncoming,
} = useQuery(GET_INCOMING_REQUESTS, {
errorPolicy: 'all',
onCompleted: (data) => {
console.warn('🎯 INCOMING_REQUESTS ФРОНТЕНД:', {
requestsCount: data?.incomingRequests?.length || 0,
requests:
data?.incomingRequests?.map((r: CounterpartyRequest) => ({
id: r.id,
senderId: r.sender?.id,
senderName: r.sender?.name || r.sender?.fullName,
status: r.status,
})) || [],
timestamp: new Date().toISOString(),
})
},
onError: (error) => {
console.error('Error loading incoming requests:', error)
console.error('Error loading incoming requests:', error)
setError('Ошибка загрузки входящих заявок')
},
})
@ -88,7 +96,7 @@ export function useCounterpartyData(): UseCounterpartyDataReturn {
try {
// Используем Apollo Client напрямую для поиска
const { apolloClient } = await import('@/lib/apollo-client')
const variables: { search: string; type?: string } = { search: query }
if (type && type !== 'all') {
variables.type = type
@ -118,14 +126,9 @@ export function useCounterpartyData(): UseCounterpartyDataReturn {
// Обновление всех данных
const refetchAll = useCallback(async () => {
setError(null)
try {
await Promise.all([
refetchCounterparties(),
refetchIncoming(),
refetchOutgoing(),
refetchPartnerLink(),
])
await Promise.all([refetchCounterparties(), refetchIncoming(), refetchOutgoing(), refetchPartnerLink()])
} catch (error) {
console.error('Error refetching data:', error)
setError('Ошибка обновления данных')
@ -133,9 +136,9 @@ export function useCounterpartyData(): UseCounterpartyDataReturn {
}, [refetchCounterparties, refetchIncoming, refetchOutgoing, refetchPartnerLink])
// Извлечение данных из responses
const counterparties: Organization[] = counterpartiesData?.getMyCounterparties || []
const incomingRequests: CounterpartyRequest[] = incomingData?.getIncomingCounterpartyRequests || []
const outgoingRequests: CounterpartyRequest[] = outgoingData?.getOutgoingCounterpartyRequests || []
const counterparties: Organization[] = counterpartiesData?.myCounterparties || []
const incomingRequests: CounterpartyRequest[] = incomingData?.incomingRequests || []
const outgoingRequests: CounterpartyRequest[] = outgoingData?.outgoingRequests || []
const partnerLink: string | null = partnerLinkData?.myPartnerLink || null
return {
@ -160,4 +163,4 @@ export function useCounterpartyData(): UseCounterpartyDataReturn {
refetchAll,
searchOrganizations,
}
}
}