Оптимизирована производительность React компонентов с помощью мемоизации
КРИТИЧНЫЕ КОМПОНЕНТЫ ОПТИМИЗИРОВАНЫ: • AdminDashboard (346 kB) - добавлены React.memo, useCallback, useMemo • SellerStatisticsDashboard (329 kB) - мемоизация кэша и callback функций • CreateSupplyPage (276 kB) - оптимизированы вычисления и обработчики • EmployeesDashboard (268 kB) - мемоизация списков и функций • SalesTab + AdvertisingTab - React.memo обертка ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ: ✅ React.memo() для предотвращения лишних рендеров ✅ useMemo() для тяжелых вычислений ✅ useCallback() для стабильных ссылок на функции ✅ Мемоизация фильтрации и сортировки списков ✅ Оптимизация пропсов в компонентах-контейнерах РЕЗУЛЬТАТЫ: • Все компоненты успешно компилируются • Линтер проходит без критических ошибок • Сохранена вся функциональность • Улучшена производительность рендеринга • Снижена нагрузка на React дерево 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,15 +1,8 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { GlassInput } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Users,
|
||||
import {
|
||||
Users,
|
||||
ArrowUpCircle,
|
||||
ArrowDownCircle,
|
||||
Search,
|
||||
@ -21,12 +14,25 @@ import {
|
||||
Phone,
|
||||
Mail,
|
||||
MapPin,
|
||||
X
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { GET_MY_COUNTERPARTIES, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS, SEARCH_ORGANIZATIONS } from '@/graphql/queries'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
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'
|
||||
import { RESPOND_TO_COUNTERPARTY_REQUEST, CANCEL_COUNTERPARTY_REQUEST, REMOVE_COUNTERPARTY } from '@/graphql/mutations'
|
||||
import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_INCOMING_REQUESTS,
|
||||
GET_OUTGOING_REQUESTS,
|
||||
SEARCH_ORGANIZATIONS,
|
||||
} from '@/graphql/queries'
|
||||
|
||||
import { OrganizationAvatar } from './organization-avatar'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
@ -38,7 +44,7 @@ interface Organization {
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
users?: Array<{ id: string; avatar?: string }>
|
||||
}
|
||||
|
||||
interface CounterpartyRequest {
|
||||
@ -70,9 +76,9 @@ export function MarketCounterparties() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } }
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const [cancelRequest] = useMutation(CANCEL_COUNTERPARTY_REQUEST, {
|
||||
@ -81,9 +87,9 @@ export function MarketCounterparties() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } }
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const [removeCounterparty] = useMutation(REMOVE_COUNTERPARTY, {
|
||||
@ -92,22 +98,23 @@ export function MarketCounterparties() {
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'SELLER' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'FULFILLMENT' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'LOGIST' } },
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } }
|
||||
{ query: SEARCH_ORGANIZATIONS, variables: { type: 'WHOLESALE' } },
|
||||
],
|
||||
awaitRefetchQueries: true
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
// Фильтрация и сортировка контрагентов
|
||||
const filteredAndSortedCounterparties = useMemo(() => {
|
||||
const filtered = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => {
|
||||
const matchesSearch = !searchQuery ||
|
||||
(org.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.inn.includes(searchQuery) ||
|
||||
org.address?.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
|
||||
const matchesSearch =
|
||||
!searchQuery ||
|
||||
org.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
org.inn.includes(searchQuery) ||
|
||||
org.address?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesType = typeFilter === 'all' || org.type === typeFilter
|
||||
|
||||
|
||||
return matchesSearch && matchesType
|
||||
})
|
||||
|
||||
@ -150,7 +157,7 @@ export function MarketCounterparties() {
|
||||
const handleAcceptRequest = async (requestId: string) => {
|
||||
try {
|
||||
await respondToRequest({
|
||||
variables: { requestId, accept: true }
|
||||
variables: { requestId, accept: true },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при принятии заявки:', error)
|
||||
@ -160,7 +167,7 @@ export function MarketCounterparties() {
|
||||
const handleRejectRequest = async (requestId: string) => {
|
||||
try {
|
||||
await respondToRequest({
|
||||
variables: { requestId, accept: false }
|
||||
variables: { requestId, accept: false },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отклонении заявки:', error)
|
||||
@ -170,7 +177,7 @@ export function MarketCounterparties() {
|
||||
const handleCancelRequest = async (requestId: string) => {
|
||||
try {
|
||||
await cancelRequest({
|
||||
variables: { requestId }
|
||||
variables: { requestId },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отмене заявки:', error)
|
||||
@ -180,7 +187,7 @@ export function MarketCounterparties() {
|
||||
const handleRemoveCounterparty = async (organizationId: string) => {
|
||||
try {
|
||||
await removeCounterparty({
|
||||
variables: { organizationId }
|
||||
variables: { organizationId },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении контрагента:', error)
|
||||
@ -191,7 +198,7 @@ export function MarketCounterparties() {
|
||||
if (!dateString) return ''
|
||||
try {
|
||||
let date: Date
|
||||
|
||||
|
||||
// Проверяем, является ли строка числом (Unix timestamp)
|
||||
if (/^\d+$/.test(dateString)) {
|
||||
// Если это Unix timestamp в миллисекундах
|
||||
@ -201,15 +208,15 @@ export function MarketCounterparties() {
|
||||
// Обычная строка даты
|
||||
date = new Date(dateString)
|
||||
}
|
||||
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Неверная дата'
|
||||
}
|
||||
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
day: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
return 'Ошибка даты'
|
||||
@ -231,21 +238,31 @@ export function MarketCounterparties() {
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'FULFILLMENT': return 'Фулфилмент'
|
||||
case 'SELLER': return 'Селлер'
|
||||
case 'LOGIST': return 'Логистика'
|
||||
case 'WHOLESALE': return 'Поставщик'
|
||||
default: return type
|
||||
case 'FULFILLMENT':
|
||||
return 'Фулфилмент'
|
||||
case 'SELLER':
|
||||
return 'Селлер'
|
||||
case 'LOGIST':
|
||||
return 'Логистика'
|
||||
case 'WHOLESALE':
|
||||
return 'Поставщик'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeBadgeStyles = (type: string) => {
|
||||
switch (type) {
|
||||
case 'FULFILLMENT': return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
|
||||
case 'SELLER': return 'bg-green-500/20 text-green-300 border-green-500/30'
|
||||
case 'LOGIST': return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
|
||||
case 'WHOLESALE': return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
|
||||
default: return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
case 'FULFILLMENT':
|
||||
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
|
||||
case 'SELLER':
|
||||
return 'bg-green-500/20 text-green-300 border-green-500/30'
|
||||
case 'LOGIST':
|
||||
return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
|
||||
case 'WHOLESALE':
|
||||
return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,18 +271,27 @@ export function MarketCounterparties() {
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-white/5 border-white/10">
|
||||
<TabsTrigger value="counterparties" className="data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300">
|
||||
<TabsTrigger
|
||||
value="counterparties"
|
||||
className="data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300"
|
||||
>
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
Контрагенты ({counterparties.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="incoming" className={`data-[state=active]:bg-green-500/20 data-[state=active]:text-green-300 relative ${incomingRequests.length > 0 ? 'ring-2 ring-green-400/50 animate-pulse' : ''}`}>
|
||||
<TabsTrigger
|
||||
value="incoming"
|
||||
className={`data-[state=active]:bg-green-500/20 data-[state=active]:text-green-300 relative ${incomingRequests.length > 0 ? 'ring-2 ring-green-400/50 animate-pulse' : ''}`}
|
||||
>
|
||||
<ArrowDownCircle className="h-4 w-4 mr-2" />
|
||||
Входящие ({incomingRequests.length})
|
||||
{incomingRequests.length > 0 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="outgoing" className="data-[state=active]:bg-orange-500/20 data-[state=active]:text-orange-300">
|
||||
<TabsTrigger
|
||||
value="outgoing"
|
||||
className="data-[state=active]:bg-orange-500/20 data-[state=active]:text-orange-300"
|
||||
>
|
||||
<ArrowUpCircle className="h-4 w-4 mr-2" />
|
||||
Исходящие ({outgoingRequests.length})
|
||||
</TabsTrigger>
|
||||
@ -348,7 +374,7 @@ export function MarketCounterparties() {
|
||||
{['FULFILLMENT', 'SELLER', 'LOGIST', 'WHOLESALE'].map((type) => {
|
||||
const count = counterparties.filter((org: Organization) => org.type === type).length
|
||||
if (count === 0) return null
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={type}
|
||||
@ -356,8 +382,8 @@ export function MarketCounterparties() {
|
||||
size="sm"
|
||||
onClick={() => setTypeFilter(typeFilter === type ? 'all' : type)}
|
||||
className={`h-6 px-2 text-xs ${
|
||||
typeFilter === type
|
||||
? getTypeBadgeStyles(type) + ' border'
|
||||
typeFilter === type
|
||||
? getTypeBadgeStyles(type) + ' border'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
@ -382,9 +408,7 @@ export function MarketCounterparties() {
|
||||
<>
|
||||
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">У вас пока нет контрагентов</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Перейдите на другие вкладки, чтобы найти партнеров
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">Перейдите на другие вкладки, чтобы найти партнеров</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -415,34 +439,34 @@ export function MarketCounterparties() {
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Building className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>ИНН: {organization.inn}</span>
|
||||
</div>
|
||||
|
||||
|
||||
{organization.address && (
|
||||
<div className="flex items-start text-white/60 text-sm">
|
||||
<MapPin className="h-3 w-3 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="line-clamp-2">{organization.address}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.phones && organization.phones.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Phone className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>{organization.phones[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{organization.emails && organization.emails.length > 0 && (
|
||||
<div className="flex items-center text-white/60 text-sm">
|
||||
<Mail className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span className="truncate">{organization.emails[0].value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center text-white/40 text-xs pt-2">
|
||||
<Calendar className="h-3 w-3 mr-2 flex-shrink-0" />
|
||||
<span>Добавлен {formatDate(organization.createdAt)}</span>
|
||||
@ -450,7 +474,7 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@ -497,7 +521,7 @@ export function MarketCounterparties() {
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-white/60 text-sm">ИНН: {request.sender.inn}</p>
|
||||
{request.sender.address && (
|
||||
@ -516,7 +540,7 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
@ -569,16 +593,24 @@ export function MarketCounterparties() {
|
||||
<Badge className={getTypeBadgeStyles(request.receiver.type)}>
|
||||
{getTypeLabel(request.receiver.type)}
|
||||
</Badge>
|
||||
<Badge className={
|
||||
request.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' :
|
||||
request.status === 'REJECTED' ? 'bg-red-500/20 text-red-300 border-red-500/30' :
|
||||
'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}>
|
||||
{request.status === 'PENDING' ? 'Ожидает ответа' : request.status === 'REJECTED' ? 'Отклонено' : request.status}
|
||||
<Badge
|
||||
className={
|
||||
request.status === 'PENDING'
|
||||
? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
|
||||
: request.status === 'REJECTED'
|
||||
? 'bg-red-500/20 text-red-300 border-red-500/30'
|
||||
: 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
>
|
||||
{request.status === 'PENDING'
|
||||
? 'Ожидает ответа'
|
||||
: request.status === 'REJECTED'
|
||||
? 'Отклонено'
|
||||
: request.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-white/60 text-sm">ИНН: {request.receiver.inn}</p>
|
||||
{request.receiver.address && (
|
||||
@ -597,7 +629,7 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{request.status === 'PENDING' && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -618,4 +650,4 @@ export function MarketCounterparties() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user