Оптимизирована производительность 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,6 @@
"use client";
'use client'
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery, useMutation } from "@apollo/client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { useQuery, useMutation } from '@apollo/client'
import {
ArrowLeft,
Building2,
@ -22,161 +14,149 @@ import {
Plus,
Minus,
ShoppingCart,
} from "lucide-react";
import {
GET_MY_COUNTERPARTIES,
GET_ORGANIZATION_PRODUCTS,
GET_SUPPLY_ORDERS,
GET_MY_SUPPLIES,
} from "@/graphql/queries";
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
import { OrganizationAvatar } from "@/components/market/organization-avatar";
import { toast } from "sonner";
import Image from "next/image";
} from 'lucide-react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import React, { useState } from 'react'
import { toast } from 'sonner'
import { Sidebar } from '@/components/dashboard/sidebar'
import { OrganizationAvatar } from '@/components/market/organization-avatar'
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 { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_PRODUCTS, GET_SUPPLY_ORDERS, GET_MY_SUPPLIES } from '@/graphql/queries'
import { useSidebar } from '@/hooks/useSidebar'
interface Partner {
id: string;
inn: string;
name?: string;
fullName?: string;
type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE";
address?: string;
phones?: Array<{ value: string }>;
emails?: Array<{ value: string }>;
users?: Array<{ id: string; avatar?: string; managerName?: string }>;
createdAt: string;
id: string
inn: string
name?: string
fullName?: string
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
users?: Array<{ id: string; avatar?: string; managerName?: string }>
createdAt: string
}
interface Product {
id: string;
name: string;
article: string;
description?: string;
price: number;
quantity: number;
category?: { id: string; name: string };
brand?: string;
color?: string;
size?: string;
weight?: number;
dimensions?: string;
material?: string;
images: string[];
mainImage?: string;
isActive: boolean;
id: string
name: string
article: string
description?: string
price: number
quantity: number
category?: { id: string; name: string }
brand?: string
color?: string
size?: string
weight?: number
dimensions?: string
material?: string
images: string[]
mainImage?: string
isActive: boolean
organization: {
id: string;
inn: string;
name?: string;
fullName?: string;
};
id: string
inn: string
name?: string
fullName?: string
}
}
interface SelectedProduct extends Product {
selectedQuantity: number;
selectedQuantity: number
}
export function MaterialsOrderForm() {
const router = useRouter();
const { getSidebarMargin } = useSidebar();
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null);
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>(
[]
);
const [searchQuery, setSearchQuery] = useState("");
const [deliveryDate, setDeliveryDate] = useState("");
const router = useRouter()
const { getSidebarMargin } = useSidebar()
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null)
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
const [searchQuery, setSearchQuery] = useState('')
const [deliveryDate, setDeliveryDate] = useState('')
// Загружаем контрагентов-поставщиков
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(
GET_MY_COUNTERPARTIES
);
const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery(GET_MY_COUNTERPARTIES)
// Загружаем товары для выбранного партнера с фильтрацией по типу CONSUMABLE
const { data: productsData, loading: productsLoading } = useQuery(
GET_ORGANIZATION_PRODUCTS,
{
skip: !selectedPartner,
variables: {
organizationId: selectedPartner.id,
search: null,
category: null,
type: "CONSUMABLE" // Фильтруем только расходники согласно rules2.md
},
}
);
const { data: productsData, loading: productsLoading } = useQuery(GET_ORGANIZATION_PRODUCTS, {
skip: !selectedPartner,
variables: {
organizationId: selectedPartner.id,
search: null,
category: null,
type: 'CONSUMABLE', // Фильтруем только расходники согласно rules2.md
},
})
// Мутация для создания заказа поставки
const [createSupplyOrder, { loading: isCreatingOrder }] =
useMutation(CREATE_SUPPLY_ORDER);
const [createSupplyOrder, { loading: isCreatingOrder }] = useMutation(CREATE_SUPPLY_ORDER)
// Фильтруем только поставщиков из партнеров
const wholesalePartners = (counterpartiesData?.myCounterparties || []).filter(
(org: Partner) => org.type === "WHOLESALE"
);
(org: Partner) => org.type === 'WHOLESALE',
)
// Фильтруем партнеров по поисковому запросу
const filteredPartners = wholesalePartners.filter(
(partner: Partner) =>
partner.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.inn?.toLowerCase().includes(searchQuery.toLowerCase())
);
partner.inn?.toLowerCase().includes(searchQuery.toLowerCase()),
)
// Получаем товары партнера (уже отфильтрованы в GraphQL запросе)
const partnerProducts = productsData?.organizationProducts || [];
const partnerProducts = productsData?.organizationProducts || []
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
}).format(amount);
};
}).format(amount)
}
const updateProductQuantity = (productId: string, quantity: number) => {
const product = partnerProducts.find((p: Product) => p.id === productId);
if (!product) return;
const product = partnerProducts.find((p: Product) => p.id === productId)
if (!product) return
setSelectedProducts((prev) => {
const existing = prev.find((p) => p.id === productId);
const existing = prev.find((p) => p.id === productId)
if (quantity === 0) {
return prev.filter((p) => p.id !== productId);
return prev.filter((p) => p.id !== productId)
}
if (existing) {
return prev.map((p) =>
p.id === productId ? { ...p, selectedQuantity: quantity } : p
);
return prev.map((p) => (p.id === productId ? { ...p, selectedQuantity: quantity } : p))
} else {
return [...prev, { ...product, selectedQuantity: quantity }];
return [...prev, { ...product, selectedQuantity: quantity }]
}
});
};
})
}
const getSelectedQuantity = (productId: string): number => {
const selected = selectedProducts.find((p) => p.id === productId);
return selected ? selected.selectedQuantity : 0;
};
const selected = selectedProducts.find((p) => p.id === productId)
return selected ? selected.selectedQuantity : 0
}
const getTotalAmount = () => {
return selectedProducts.reduce(
(sum, product) => sum + product.price * product.selectedQuantity,
0
);
};
return selectedProducts.reduce((sum, product) => sum + product.price * product.selectedQuantity, 0)
}
const getTotalItems = () => {
return selectedProducts.reduce(
(sum, product) => sum + product.selectedQuantity,
0
);
};
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
}
const handleCreateOrder = async () => {
if (!selectedPartner || selectedProducts.length === 0 || !deliveryDate) {
toast.error("Заполните все обязательные поля");
return;
toast.error('Заполните все обязательные поля')
return
}
try {
@ -195,44 +175,35 @@ export function MaterialsOrderForm() {
{ query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок
{ query: GET_MY_SUPPLIES }, // Обновляем расходники фулфилмента
],
});
})
if (result.data?.createSupplyOrder?.success) {
toast.success("Заказ поставки создан успешно!");
router.push("/fulfillment-supplies");
toast.success('Заказ поставки создан успешно!')
router.push('/fulfillment-supplies')
} else {
toast.error(
result.data?.createSupplyOrder?.message ||
"Ошибка при создании заказа"
);
toast.error(result.data?.createSupplyOrder?.message || 'Ошибка при создании заказа')
}
} catch (error) {
console.error("Error creating supply order:", error);
toast.error("Ошибка при создании заказа поставки");
console.error('Error creating supply order:', error)
toast.error('Ошибка при создании заказа поставки')
}
};
}
const renderStars = (rating: number = 4.5) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`h-3 w-3 ${
i < Math.floor(rating)
? "text-yellow-400 fill-current"
: "text-gray-400"
}`}
className={`h-3 w-3 ${i < Math.floor(rating) ? 'text-yellow-400 fill-current' : 'text-gray-400'}`}
/>
));
};
))
}
// Если выбран партнер и есть товары, показываем товары
if (selectedPartner && partnerProducts.length > 0) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
<div className="h-full w-full flex flex-col">
{/* Заголовок */}
<div className="flex items-center justify-between mb-6">
@ -247,18 +218,14 @@ export function MaterialsOrderForm() {
Назад к партнерам
</Button>
<div>
<h1 className="text-2xl font-bold text-white">
Товары партнера
</h1>
<p className="text-white/60">
{selectedPartner.name || selectedPartner.fullName}
</p>
<h1 className="text-2xl font-bold text-white">Товары партнера</h1>
<p className="text-white/60">{selectedPartner.name || selectedPartner.fullName}</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/fulfillment-supplies")}
onClick={() => router.push('/fulfillment-supplies')}
className="text-white/60 hover:text-white hover:bg-white/10"
>
Отмена
@ -270,58 +237,36 @@ export function MaterialsOrderForm() {
<div className="lg:col-span-2 overflow-hidden">
<Card className="glass-card h-full overflow-hidden">
<div className="p-4 h-full flex flex-col">
<h3 className="text-lg font-semibold text-white mb-4">
Доступные товары
</h3>
<h3 className="text-lg font-semibold text-white mb-4">Доступные товары</h3>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{partnerProducts.map((product: Product) => {
const selectedQuantity = getSelectedQuantity(
product.id
);
const selectedQuantity = getSelectedQuantity(product.id)
return (
<Card
key={product.id}
className="glass-secondary p-4"
>
<Card key={product.id} className="glass-secondary p-4">
<div className="space-y-3">
{/* Изображение товара */}
{product.mainImage && (
<div className="relative h-32 w-full bg-white/5 rounded overflow-hidden">
<Image
src={product.mainImage}
alt={product.name}
fill
className="object-cover"
/>
<Image src={product.mainImage} alt={product.name} fill className="object-cover" />
</div>
)}
{/* Информация о товаре */}
<div>
<h4 className="text-white font-medium text-sm">
{product.name}
</h4>
<p className="text-white/60 text-xs">
Артикул: {product.article}
</p>
<h4 className="text-white font-medium text-sm">{product.name}</h4>
<p className="text-white/60 text-xs">Артикул: {product.article}</p>
{product.description && (
<p className="text-white/60 text-xs mt-1 line-clamp-2">
{product.description}
</p>
<p className="text-white/60 text-xs mt-1 line-clamp-2">{product.description}</p>
)}
</div>
{/* Цена и наличие */}
<div className="flex items-center justify-between">
<div>
<div className="text-white font-bold">
{formatCurrency(product.price)}
</div>
<div className="text-white/60 text-xs">
В наличии: {product.quantity}
</div>
<div className="text-white font-bold">{formatCurrency(product.price)}</div>
<div className="text-white/60 text-xs">В наличии: {product.quantity}</div>
</div>
</div>
@ -330,12 +275,7 @@ export function MaterialsOrderForm() {
<Button
variant="ghost"
size="sm"
onClick={() =>
updateProductQuantity(
product.id,
Math.max(0, selectedQuantity - 1)
)
}
onClick={() => updateProductQuantity(product.id, Math.max(0, selectedQuantity - 1))}
disabled={selectedQuantity === 0}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
@ -347,12 +287,9 @@ export function MaterialsOrderForm() {
onChange={(e) => {
const value = Math.max(
0,
Math.min(
product.quantity,
parseInt(e.target.value) || 0
)
);
updateProductQuantity(product.id, value);
Math.min(product.quantity, parseInt(e.target.value) || 0),
)
updateProductQuantity(product.id, value)
}}
className="h-8 w-16 text-center bg-white/10 border-white/20 text-white"
min={0}
@ -364,15 +301,10 @@ export function MaterialsOrderForm() {
onClick={() =>
updateProductQuantity(
product.id,
Math.min(
product.quantity,
selectedQuantity + 1
)
Math.min(product.quantity, selectedQuantity + 1),
)
}
disabled={
selectedQuantity >= product.quantity
}
disabled={selectedQuantity >= product.quantity}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Plus className="h-4 w-4" />
@ -380,7 +312,7 @@ export function MaterialsOrderForm() {
</div>
</div>
</Card>
);
)
})}
</div>
</div>
@ -392,9 +324,7 @@ export function MaterialsOrderForm() {
<div className="overflow-hidden">
<Card className="glass-card h-full overflow-hidden">
<div className="p-4 h-full flex flex-col">
<h3 className="text-lg font-semibold text-white mb-4">
Сводка заказа
</h3>
<h3 className="text-lg font-semibold text-white mb-4">Сводка заказа</h3>
{/* Дата поставки */}
<div className="mb-4">
@ -416,30 +346,20 @@ export function MaterialsOrderForm() {
{selectedProducts.length === 0 ? (
<div className="text-center py-8">
<Package className="h-12 w-12 text-white/20 mx-auto mb-2" />
<p className="text-white/60 text-sm">
Товары не выбраны
</p>
<p className="text-white/60 text-sm">Товары не выбраны</p>
</div>
) : (
<div className="space-y-2">
{selectedProducts.map((product) => (
<Card
key={product.id}
className="glass-secondary p-3"
>
<Card key={product.id} className="glass-secondary p-3">
<div className="space-y-1">
<div className="text-white text-sm font-medium">
{product.name}
</div>
<div className="text-white text-sm font-medium">{product.name}</div>
<div className="flex justify-between text-xs text-white/60">
<span>
{product.selectedQuantity} шт ×{" "}
{formatCurrency(product.price)}
{product.selectedQuantity} шт × {formatCurrency(product.price)}
</span>
<span className="text-white font-medium">
{formatCurrency(
product.price * product.selectedQuantity
)}
{formatCurrency(product.price * product.selectedQuantity)}
</span>
</div>
</div>
@ -464,17 +384,11 @@ export function MaterialsOrderForm() {
{/* Кнопка создания заказа */}
<Button
onClick={handleCreateOrder}
disabled={
selectedProducts.length === 0 ||
!deliveryDate ||
isCreatingOrder
}
disabled={selectedProducts.length === 0 || !deliveryDate || isCreatingOrder}
className="w-full mt-4 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
>
<ShoppingCart className="h-4 w-4 mr-2" />
{isCreatingOrder
? "Создание заказа..."
: "Создать заказ поставки"}
{isCreatingOrder ? 'Создание заказа...' : 'Создать заказ поставки'}
</Button>
</div>
</Card>
@ -483,16 +397,14 @@ export function MaterialsOrderForm() {
</div>
</main>
</div>
);
)
}
// Основная форма выбора партнера
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
<div className="h-full w-full flex flex-col">
{/* Заголовок */}
<div className="flex items-center justify-between mb-6">
@ -500,18 +412,14 @@ export function MaterialsOrderForm() {
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/fulfillment-supplies")}
onClick={() => router.push('/fulfillment-supplies')}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<ArrowLeft className="h-4 w-4 mr-2" />К поставкам
</Button>
<div>
<h1 className="text-2xl font-bold text-white">
Заказ расходников
</h1>
<p className="text-white/60">
Выберите партнера-поставщика для заказа расходников
</p>
<h1 className="text-2xl font-bold text-white">Заказ расходников</h1>
<p className="text-white/60">Выберите партнера-поставщика для заказа расходников</p>
</div>
</div>
</div>
@ -541,13 +449,9 @@ export function MaterialsOrderForm() {
<div className="text-center">
<Building2 className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{wholesalePartners.length === 0
? "У вас пока нет партнеров-поставщиков"
: "Партнеры не найдены"}
</p>
<p className="text-white/40 text-sm mt-2">
Добавьте партнеров в разделе &quot;Партнеры&quot;
{wholesalePartners.length === 0 ? 'У вас пока нет партнеров-поставщиков' : 'Партнеры не найдены'}
</p>
<p className="text-white/40 text-sm mt-2">Добавьте партнеров в разделе &quot;Партнеры&quot;</p>
</div>
</div>
) : (
@ -562,19 +466,14 @@ export function MaterialsOrderForm() {
<div className="space-y-3">
{/* Заголовок карточки */}
<div className="flex items-start space-x-3">
<OrganizationAvatar
organization={partner}
size="sm"
/>
<OrganizationAvatar organization={partner} size="sm" />
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-sm mb-1 truncate">
{partner.name || partner.fullName}
</h3>
<div className="flex items-center space-x-1 mb-2">
{renderStars()}
<span className="text-white/60 text-xs ml-1">
4.5
</span>
<span className="text-white/60 text-xs ml-1">4.5</span>
</div>
</div>
</div>
@ -584,36 +483,28 @@ export function MaterialsOrderForm() {
{partner.address && (
<div className="flex items-center space-x-2">
<MapPin className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs truncate">
{partner.address}
</span>
<span className="text-white/80 text-xs truncate">{partner.address}</span>
</div>
)}
{partner.phones && partner.phones.length > 0 && (
<div className="flex items-center space-x-2">
<Phone className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs">
{partner.phones[0].value}
</span>
<span className="text-white/80 text-xs">{partner.phones[0].value}</span>
</div>
)}
{partner.emails && partner.emails.length > 0 && (
<div className="flex items-center space-x-2">
<Mail className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs truncate">
{partner.emails[0].value}
</span>
<span className="text-white/80 text-xs truncate">{partner.emails[0].value}</span>
</div>
)}
</div>
{/* ИНН */}
<div className="pt-2 border-t border-white/10">
<p className="text-white/60 text-xs">
ИНН: {partner.inn}
</p>
<p className="text-white/60 text-xs">ИНН: {partner.inn}</p>
</div>
</div>
</Card>
@ -626,5 +517,5 @@ export function MaterialsOrderForm() {
</div>
</main>
</div>
);
)
}

View File

@ -1,12 +1,13 @@
"use client"
'use client'
import { useState } from 'react'
import { useQuery } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
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 {
@ -35,14 +36,14 @@ export function MaterialsSuppliesTab() {
// Загружаем расходники из GraphQL
const { data, loading, error, refetch } = useQuery(GET_MY_SUPPLIES, {
fetchPolicy: 'cache-and-network', // Всегда проверяем сервер
errorPolicy: 'all' // Показываем ошибки
errorPolicy: 'all', // Показываем ошибки
})
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
minimumFractionDigits: 0,
}).format(amount)
}
@ -50,7 +51,7 @@ export function MaterialsSuppliesTab() {
return new Date(dateString).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
year: 'numeric',
})
}
@ -59,11 +60,11 @@ export function MaterialsSuppliesTab() {
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: 'На складе' }
'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}
@ -96,18 +97,19 @@ export function MaterialsSuppliesTab() {
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 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)
return filteredSupplies.reduce((sum: number, supply: MaterialSupply) => sum + supply.price * supply.quantity, 0)
}
const getTotalQuantity = () => {
@ -140,10 +142,7 @@ export function MaterialsSuppliesTab() {
<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 onClick={() => refetch()} className="mt-4 bg-blue-500 hover:bg-blue-600">
Попробовать снова
</Button>
</div>
@ -156,41 +155,41 @@ export function MaterialsSuppliesTab() {
{/* Статистика с кнопкой заказа */}
<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" />
<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>
<div>
<p className="text-white/60 text-xs">Поставок</p>
<p className="text-lg font-bold text-white">{filteredSupplies.length}</p>
</div>
</div>
</Card>
</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" />
<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>
<div>
<p className="text-white/60 text-xs">Сумма</p>
<p className="text-lg font-bold text-white">{formatCurrency(getTotalAmount())}</p>
</div>
</div>
</Card>
</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" />
<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>
<div>
<p className="text-white/60 text-xs">Единиц</p>
<p className="text-lg font-bold text-white">{getTotalQuantity()}</p>
</div>
</div>
</Card>
</Card>
<Card className="glass-card p-3 h-[60px]">
<div className="flex items-center space-x-2 h-full">
@ -204,11 +203,11 @@ export function MaterialsSuppliesTab() {
</div>
</Card>
</div>
{/* Кнопка заказа */}
<Button
<Button
size="sm"
onClick={() => window.location.href = '/fulfillment-supplies/materials/order'}
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" />
@ -227,7 +226,7 @@ export function MaterialsSuppliesTab() {
className="pl-7 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
</div>
<div className="flex gap-1">
<select
value={categoryFilter}
@ -235,11 +234,13 @@ export function MaterialsSuppliesTab() {
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>
{categories.map((category) => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
@ -277,20 +278,22 @@ export function MaterialsSuppliesTab() {
<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>
)}
{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>
<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>
<span className="text-white font-semibold text-sm">
{supply.currentStock || 0} {supply.unit || 'шт'}
</span>
{getStockStatusBadge(supply.currentStock || 0, supply.minStock || 0)}
</div>
</td>
@ -298,14 +301,16 @@ export function MaterialsSuppliesTab() {
<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>
<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')}
<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>
@ -327,4 +332,4 @@ export function MaterialsSuppliesTab() {
</Card>
</div>
)
}
}