Files
sfera-new/src/components/fulfillment-supplies/materials-supplies/materials-supplies-tab.tsx
Veronika Smirnova bf27f3ba29 Оптимизирована производительность 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>
2025-08-06 13:18:45 +03:00

336 lines
14 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.

'use client'
import { useQuery } from '@apollo/client'
import { Wrench, Plus, Calendar, TrendingUp, AlertCircle, Search, Filter } from 'lucide-react'
import { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { GET_MY_SUPPLIES } from '@/graphql/queries'
interface MaterialSupply {
id: string
name: string
description?: string
price: number
quantity: number
unit?: string
category?: string
status?: string
date: string
supplier?: string
minStock?: number
currentStock?: number
imageUrl?: string
createdAt: string
updatedAt: string
}
export function MaterialsSuppliesTab() {
const [searchTerm, setSearchTerm] = useState('')
const [categoryFilter, setCategoryFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('all')
// Загружаем расходники из GraphQL
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
fetchPolicy: 'cache-and-network', // Всегда проверяем сервер
errorPolicy: 'all', // Показываем ошибки
})
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
}).format(amount)
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
const getStatusBadge = (status: string) => {
const statusConfig = {
planned: { variant: 'outline' as const, color: 'text-blue-300 border-blue-400/30', label: 'Запланировано' },
'in-transit': { variant: 'outline' as const, color: 'text-yellow-300 border-yellow-400/30', label: 'В пути' },
delivered: { variant: 'outline' as const, color: 'text-green-300 border-green-400/30', label: 'Доставлено' },
'in-stock': { variant: 'outline' as const, color: 'text-purple-300 border-purple-400/30', label: 'На складе' },
}
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.planned
return (
<Badge variant={config.variant} className={`glass-secondary ${config.color}`}>
{config.label}
</Badge>
)
}
const getStockStatusBadge = (currentStock: number, minStock: number) => {
if (currentStock <= minStock) {
return (
<Badge variant="outline" className="glass-secondary text-red-300 border-red-400/30">
Низкий остаток
</Badge>
)
} else if (currentStock <= minStock * 1.5) {
return (
<Badge variant="outline" className="glass-secondary text-yellow-300 border-yellow-400/30">
Требует заказа
</Badge>
)
}
return (
<Badge variant="outline" className="glass-secondary text-green-300 border-green-400/30">
В норме
</Badge>
)
}
// Обрабатываем данные из GraphQL
const supplies: MaterialSupply[] = data?.mySupplies || []
const filteredSupplies = supplies.filter((supply: MaterialSupply) => {
const matchesSearch =
supply.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(supply.category || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(supply.supplier || '').toLowerCase().includes(searchTerm.toLowerCase())
const matchesCategory = categoryFilter === 'all' || supply.category === categoryFilter
const matchesStatus = statusFilter === 'all' || supply.status === statusFilter
return matchesSearch && matchesCategory && matchesStatus
})
const getTotalAmount = () => {
return filteredSupplies.reduce((sum: number, supply: MaterialSupply) => sum + supply.price * supply.quantity, 0)
}
const getTotalQuantity = () => {
return filteredSupplies.reduce((sum: number, supply: MaterialSupply) => sum + supply.quantity, 0)
}
const getLowStockCount = () => {
return supplies.filter((supply: MaterialSupply) => (supply.currentStock || 0) <= (supply.minStock || 0)).length
}
const categories = Array.from(new Set(supplies.map((supply: MaterialSupply) => supply.category)))
// Показываем индикатор загрузки
if (loading) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-4"></div>
<p className="text-white/60">Загрузка расходников...</p>
</div>
</div>
)
}
// Показываем ошибку
if (error) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<AlertCircle className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-white/60">Ошибка загрузки данных</p>
<p className="text-white/40 text-sm mt-2">{error.message}</p>
<Button onClick={() => refetch()} className="mt-4 bg-blue-500 hover:bg-blue-600">
Попробовать снова
</Button>
</div>
</div>
)
}
return (
<div className="h-full flex flex-col space-y-4 p-4">
{/* Статистика с кнопкой заказа */}
<div className="flex items-center justify-between gap-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 flex-1">
<Card className="glass-card p-3 h-[60px]">
<div className="flex items-center space-x-2 h-full">
<div className="p-1.5 bg-purple-500/20 rounded">
<Wrench className="h-3 w-3 text-purple-400" />
</div>
<div>
<p className="text-white/60 text-xs">Поставок</p>
<p className="text-lg font-bold text-white">{filteredSupplies.length}</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 h-[60px]">
<div className="flex items-center space-x-2 h-full">
<div className="p-1.5 bg-green-500/20 rounded">
<TrendingUp className="h-3 w-3 text-green-400" />
</div>
<div>
<p className="text-white/60 text-xs">Сумма</p>
<p className="text-lg font-bold text-white">{formatCurrency(getTotalAmount())}</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 h-[60px]">
<div className="flex items-center space-x-2 h-full">
<div className="p-1.5 bg-blue-500/20 rounded">
<AlertCircle className="h-3 w-3 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-xs">Единиц</p>
<p className="text-lg font-bold text-white">{getTotalQuantity()}</p>
</div>
</div>
</Card>
<Card className="glass-card p-3 h-[60px]">
<div className="flex items-center space-x-2 h-full">
<div className="p-1.5 bg-red-500/20 rounded">
<Calendar className="h-3 w-3 text-red-400" />
</div>
<div>
<p className="text-white/60 text-xs">Низкий остаток</p>
<p className="text-lg font-bold text-white">{getLowStockCount()}</p>
</div>
</div>
</Card>
</div>
{/* Кнопка заказа */}
<Button
size="sm"
onClick={() => (window.location.href = '/fulfillment-supplies/materials/order')}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-sm px-6 h-[60px] whitespace-nowrap"
>
<Plus className="h-4 w-4 mr-2" />
Заказать
</Button>
</div>
{/* Компактный поиск и фильтры */}
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-2 h-3 w-3 text-white/40" />
<Input
placeholder="Поиск..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-7 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
</div>
<div className="flex gap-1">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="px-2 py-1 h-8 bg-white/5 border border-white/20 rounded text-white text-xs focus:outline-none focus:ring-1 focus:ring-purple-500"
>
<option value="all">Все категории</option>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-2 py-1 h-8 bg-white/5 border border-white/20 rounded text-white text-xs focus:outline-none focus:ring-1 focus:ring-purple-500"
>
<option value="all">Все статусы</option>
<option value="planned">Запланировано</option>
<option value="in-transit">В пути</option>
<option value="delivered">Доставлено</option>
<option value="in-stock">На складе</option>
</select>
</div>
</div>
{/* Компактная таблица расходников */}
<Card className="glass-card flex-1 overflow-hidden">
<div className="p-3 h-full flex flex-col">
<div className="overflow-x-auto flex-1">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/20">
<th className="text-left p-2 text-white font-medium text-xs">Наименование</th>
<th className="text-left p-2 text-white font-medium text-xs">Категория</th>
<th className="text-left p-2 text-white font-medium text-xs">Кол-во</th>
<th className="text-left p-2 text-white font-medium text-xs">Остаток</th>
<th className="text-left p-2 text-white font-medium text-xs">Поставщик</th>
<th className="text-left p-2 text-white font-medium text-xs">Дата</th>
<th className="text-left p-2 text-white font-medium text-xs">Сумма</th>
<th className="text-left p-2 text-white font-medium text-xs">Статус</th>
</tr>
</thead>
<tbody>
{filteredSupplies.map((supply) => (
<tr key={supply.id} className="border-b border-white/10 hover:bg-white/5 transition-colors">
<td className="p-2">
<div>
<span className="text-white font-medium text-sm">{supply.name}</span>
{supply.description && <p className="text-white/60 text-xs mt-0.5">{supply.description}</p>}
</div>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">{supply.category || 'Не указано'}</span>
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{supply.quantity} {supply.unit || 'шт'}
</span>
</td>
<td className="p-2">
<div className="flex flex-col gap-0.5">
<span className="text-white font-semibold text-sm">
{supply.currentStock || 0} {supply.unit || 'шт'}
</span>
{getStockStatusBadge(supply.currentStock || 0, supply.minStock || 0)}
</div>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">{supply.supplier || 'Не указан'}</span>
</td>
<td className="p-2">
<span className="text-white/80 text-sm">
{supply.date ? formatDate(supply.date) : 'Не указано'}
</span>
</td>
<td className="p-2">
<span className="text-white font-semibold text-sm">
{formatCurrency(supply.price * supply.quantity)}
</span>
</td>
<td className="p-2">{getStatusBadge(supply.status || 'planned')}</td>
</tr>
))}
</tbody>
</table>
{filteredSupplies.length === 0 && (
<div className="flex items-center justify-center p-8">
<div className="text-center">
<Wrench 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>
</div>
</div>
)}
</div>
</div>
</Card>
</div>
)
}