Оптимизирована производительность 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,14 +1,14 @@
"use client"
'use client'
import { useQuery, gql } from '@apollo/client'
import { Search, Phone, Building, Calendar, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { useState, useEffect } from 'react'
import { useQuery } from '@apollo/client'
import { gql } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Search, Phone, Building, Calendar, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
// GraphQL запрос для получения пользователей
const ALL_USERS = gql`
@ -55,7 +55,7 @@ interface User {
}
}
export function UsersSection() {
const UsersSection = React.memo(() => {
const [search, setSearch] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const [searchQuery, setSearchQuery] = useState('')
@ -65,9 +65,9 @@ export function UsersSection() {
variables: {
search: searchQuery || undefined,
limit,
offset: (currentPage - 1) * limit
offset: (currentPage - 1) * limit,
},
fetchPolicy: 'cache-and-network'
fetchPolicy: 'cache-and-network',
})
// Обновляем запрос при изменении поиска с дебаунсом
@ -80,22 +80,22 @@ export function UsersSection() {
return () => clearTimeout(timer)
}, [search])
const users = data?.allUsers?.users || []
const total = data?.allUsers?.total || 0
const hasMore = data?.allUsers?.hasMore || false
const totalPages = Math.ceil(total / limit)
const users = useMemo(() => data?.allUsers?.users || [], [data?.allUsers?.users])
const total = useMemo(() => data?.allUsers?.total || 0, [data?.allUsers?.total])
const _hasMore = useMemo(() => data?.allUsers?.hasMore || false, [data?.allUsers?.hasMore])
const totalPages = useMemo(() => Math.ceil(total / limit), [total, limit])
const getOrganizationTypeBadge = (type: string) => {
const getOrganizationTypeBadge = useCallback((type: string) => {
const typeMap = {
FULFILLMENT: { label: 'Фулфилмент', variant: 'default' as const },
SELLER: { label: 'Селлер', variant: 'secondary' as const },
LOGIST: { label: 'Логистика', variant: 'outline' as const },
WHOLESALE: { label: 'Поставщик', variant: 'destructive' as const }
WHOLESALE: { label: 'Поставщик', variant: 'destructive' as const },
}
return typeMap[type as keyof typeof typeMap] || { label: type, variant: 'outline' as const }
}
}, [])
const formatDate = (dateString: string) => {
const formatDate = useCallback((dateString: string) => {
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
@ -106,44 +106,50 @@ export function UsersSection() {
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
})
} catch (error) {
} catch {
return 'Неизвестно'
}
}
}, [])
const getInitials = (name?: string, phone?: string) => {
const getInitials = useCallback((name?: string, phone?: string) => {
if (name) {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
return name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
}
if (phone) {
return phone.slice(-2)
}
return 'У'
}
}, [])
const handlePrevPage = () => {
const handlePrevPage = useCallback(() => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1)
}
}
}, [currentPage])
const handleNextPage = () => {
const handleNextPage = useCallback(() => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1)
}
}
}, [currentPage, totalPages])
const handleSearchChange = useCallback((value: string) => {
setSearch(value)
}, [])
if (error) {
return (
<div className="p-8">
<div className="glass-card p-6 text-center">
<p className="text-red-400">Ошибка загрузки пользователей: {error.message}</p>
<Button
onClick={() => refetch()}
className="mt-4 glass-button"
>
<Button onClick={() => refetch()} className="mt-4 glass-button">
Попробовать снова
</Button>
</div>
@ -166,7 +172,7 @@ export function UsersSection() {
type="text"
placeholder="Поиск по телефону, имени, ИНН организации..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-10 glass-input text-white placeholder:text-white/50"
/>
</div>
@ -184,9 +190,7 @@ export function UsersSection() {
</div>
) : users.length === 0 ? (
<div className="glass-card p-8 text-center">
<p className="text-white/70">
{searchQuery ? 'Пользователи не найдены' : 'Пользователи отсутствуют'}
</p>
<p className="text-white/70">{searchQuery ? 'Пользователи не найдены' : 'Пользователи отсутствуют'}</p>
</div>
) : (
<div className="space-y-4">
@ -206,9 +210,7 @@ export function UsersSection() {
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="text-white font-medium">
{user.managerName || 'Без имени'}
</h3>
<h3 className="text-white font-medium">{user.managerName || 'Без имени'}</h3>
<div className="flex items-center text-white/70 text-sm mt-1">
<Phone className="h-3 w-3 mr-1" />
{user.phone}
@ -236,13 +238,9 @@ export function UsersSection() {
{getOrganizationTypeBadge(user.organization.type).label}
</Badge>
</div>
<p className="text-white/70 text-sm">
ИНН: {user.organization.inn}
</p>
<p className="text-white/70 text-sm">ИНН: {user.organization.inn}</p>
{user.organization.status && (
<p className="text-white/60 text-xs">
Статус: {user.organization.status}
</p>
<p className="text-white/60 text-xs">Статус: {user.organization.status}</p>
)}
</div>
<div className="text-right">
@ -291,4 +289,8 @@ export function UsersSection() {
)}
</div>
)
}
})
UsersSection.displayName = 'UsersSection'
export { UsersSection }