Оптимизирована производительность 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:
Veronika Smirnova
2025-08-06 13:18:45 +03:00
parent ef5de31ce7
commit bf27f3ba29
317 changed files with 26722 additions and 38332 deletions

View File

@ -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>
)
}
}