Оптимизирована производительность 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,10 +1,5 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import React, { useState } from "react";
|
||||
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 {
|
||||
Package,
|
||||
Building2,
|
||||
@ -21,111 +16,123 @@ import {
|
||||
TrendingUp,
|
||||
AlertTriangle,
|
||||
Warehouse,
|
||||
} from "lucide-react";
|
||||
import { formatCurrency } from "@/lib/utils";
|
||||
} from 'lucide-react'
|
||||
import React, { 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 { formatCurrency } from '@/lib/utils'
|
||||
|
||||
// Простые компоненты таблицы
|
||||
const Table = ({ children, ...props }: any) => (
|
||||
<div className="w-full overflow-auto" {...props}>
|
||||
<table className="w-full">{children}</table>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
|
||||
const TableHeader = ({ children, ...props }: any) => <thead {...props}>{children}</thead>;
|
||||
const TableBody = ({ children, ...props }: any) => <tbody {...props}>{children}</tbody>;
|
||||
const TableHeader = ({ children, ...props }: any) => <thead {...props}>{children}</thead>
|
||||
const TableBody = ({ children, ...props }: any) => <tbody {...props}>{children}</tbody>
|
||||
const TableRow = ({ children, className, ...props }: any) => (
|
||||
<tr className={className} {...props}>{children}</tr>
|
||||
);
|
||||
<tr className={className} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
)
|
||||
const TableHead = ({ children, className, ...props }: any) => (
|
||||
<th className={`px-4 py-3 text-left font-medium ${className}`} {...props}>{children}</th>
|
||||
);
|
||||
<th className={`px-4 py-3 text-left font-medium ${className}`} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
const TableCell = ({ children, className, ...props }: any) => (
|
||||
<td className={`px-4 py-3 ${className}`} {...props}>{children}</td>
|
||||
);
|
||||
<td className={`px-4 py-3 ${className}`} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
|
||||
// Расширенные типы данных для детальной структуры поставок
|
||||
interface ProductParameter {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
unit?: string
|
||||
}
|
||||
|
||||
interface GoodsSupplyProduct {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
category: string;
|
||||
plannedQty: number;
|
||||
actualQty: number;
|
||||
defectQty: number;
|
||||
productPrice: number;
|
||||
parameters: ProductParameter[];
|
||||
id: string
|
||||
name: string
|
||||
sku: string
|
||||
category: string
|
||||
plannedQty: number
|
||||
actualQty: number
|
||||
defectQty: number
|
||||
productPrice: number
|
||||
parameters: ProductParameter[]
|
||||
}
|
||||
|
||||
interface GoodsSupplyWholesaler {
|
||||
id: string;
|
||||
name: string;
|
||||
inn: string;
|
||||
contact: string;
|
||||
address: string;
|
||||
products: GoodsSupplyProduct[];
|
||||
totalAmount: number;
|
||||
id: string
|
||||
name: string
|
||||
inn: string
|
||||
contact: string
|
||||
address: string
|
||||
products: GoodsSupplyProduct[]
|
||||
totalAmount: number
|
||||
}
|
||||
|
||||
interface GoodsSupplyRoute {
|
||||
id: string;
|
||||
from: string;
|
||||
fromAddress: string;
|
||||
to: string;
|
||||
toAddress: string;
|
||||
wholesalers: GoodsSupplyWholesaler[];
|
||||
totalProductPrice: number;
|
||||
fulfillmentServicePrice: number;
|
||||
logisticsPrice: number;
|
||||
totalAmount: number;
|
||||
id: string
|
||||
from: string
|
||||
fromAddress: string
|
||||
to: string
|
||||
toAddress: string
|
||||
wholesalers: GoodsSupplyWholesaler[]
|
||||
totalProductPrice: number
|
||||
fulfillmentServicePrice: number
|
||||
logisticsPrice: number
|
||||
totalAmount: number
|
||||
}
|
||||
|
||||
// Основной интерфейс поставки товаров согласно rules2.md 9.5.4
|
||||
interface GoodsSupply {
|
||||
id: string;
|
||||
number: string;
|
||||
creationMethod: 'cards' | 'suppliers'; // 📱 карточки / 🏢 поставщик
|
||||
deliveryDate: string;
|
||||
createdAt: string;
|
||||
status: string;
|
||||
|
||||
id: string
|
||||
number: string
|
||||
creationMethod: 'cards' | 'suppliers' // 📱 карточки / 🏢 поставщик
|
||||
deliveryDate: string
|
||||
createdAt: string
|
||||
status: string
|
||||
|
||||
// Агрегированные данные
|
||||
plannedTotal: number;
|
||||
actualTotal: number;
|
||||
defectTotal: number;
|
||||
totalProductPrice: number;
|
||||
totalFulfillmentPrice: number;
|
||||
totalLogisticsPrice: number;
|
||||
grandTotal: number;
|
||||
|
||||
plannedTotal: number
|
||||
actualTotal: number
|
||||
defectTotal: number
|
||||
totalProductPrice: number
|
||||
totalFulfillmentPrice: number
|
||||
totalLogisticsPrice: number
|
||||
grandTotal: number
|
||||
|
||||
// Детальная структура
|
||||
routes: GoodsSupplyRoute[];
|
||||
|
||||
routes: GoodsSupplyRoute[]
|
||||
|
||||
// Для обратной совместимости
|
||||
goodsCount?: number;
|
||||
totalAmount?: number;
|
||||
supplier?: string;
|
||||
items?: GoodsSupplyItem[];
|
||||
goodsCount?: number
|
||||
totalAmount?: number
|
||||
supplier?: string
|
||||
items?: GoodsSupplyItem[]
|
||||
}
|
||||
|
||||
// Простой интерфейс товара для базовой детализации
|
||||
interface GoodsSupplyItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
category?: string;
|
||||
id: string
|
||||
name: string
|
||||
quantity: number
|
||||
price: number
|
||||
category?: string
|
||||
}
|
||||
|
||||
interface GoodsSuppliesTableProps {
|
||||
supplies?: GoodsSupply[];
|
||||
loading?: boolean;
|
||||
supplies?: GoodsSupply[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// Компонент для иконки способа создания
|
||||
@ -136,152 +143,165 @@ function CreationMethodIcon({ method }: { method: 'cards' | 'suppliers' }) {
|
||||
<Smartphone className="h-3 w-3" />
|
||||
<span className="text-xs hidden sm:inline">Карточки</span>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-green-400">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span className="text-xs hidden sm:inline">Поставщик</span>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент для статуса поставки
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'pending': return 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30';
|
||||
case 'supplier_approved': return 'bg-blue-500/20 text-blue-300 border-blue-500/30';
|
||||
case 'confirmed': return 'bg-purple-500/20 text-purple-300 border-purple-500/30';
|
||||
case 'shipped': return 'bg-orange-500/20 text-orange-300 border-orange-500/30';
|
||||
case 'in_transit': return 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30';
|
||||
case 'delivered': return 'bg-green-500/20 text-green-300 border-green-500/30';
|
||||
default: return 'bg-gray-500/20 text-gray-300 border-gray-500/30';
|
||||
case 'pending':
|
||||
return 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30'
|
||||
case 'supplier_approved':
|
||||
return 'bg-blue-500/20 text-blue-300 border-blue-500/30'
|
||||
case 'confirmed':
|
||||
return 'bg-purple-500/20 text-purple-300 border-purple-500/30'
|
||||
case 'shipped':
|
||||
return 'bg-orange-500/20 text-orange-300 border-orange-500/30'
|
||||
case 'in_transit':
|
||||
return 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30'
|
||||
case 'delivered':
|
||||
return 'bg-green-500/20 text-green-300 border-green-500/30'
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'pending': return 'Ожидает';
|
||||
case 'supplier_approved': return 'Одобрена';
|
||||
case 'confirmed': return 'Подтверждена';
|
||||
case 'shipped': return 'Отгружена';
|
||||
case 'in_transit': return 'В пути';
|
||||
case 'delivered': return 'Доставлена';
|
||||
default: return status;
|
||||
case 'pending':
|
||||
return 'Ожидает'
|
||||
case 'supplier_approved':
|
||||
return 'Одобрена'
|
||||
case 'confirmed':
|
||||
return 'Подтверждена'
|
||||
case 'shipped':
|
||||
return 'Отгружена'
|
||||
case 'in_transit':
|
||||
return 'В пути'
|
||||
case 'delivered':
|
||||
return 'Доставлена'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`${getStatusColor(status)} border text-xs`}>
|
||||
{getStatusText(status)}
|
||||
</Badge>
|
||||
);
|
||||
return <Badge className={`${getStatusColor(status)} border text-xs`}>{getStatusText(status)}</Badge>
|
||||
}
|
||||
|
||||
export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSuppliesTableProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedMethod, setSelectedMethod] = useState<string>("all");
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("all");
|
||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set());
|
||||
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
|
||||
const [expandedWholesalers, setExpandedWholesalers] = useState<Set<string>>(new Set());
|
||||
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedMethod, setSelectedMethod] = useState<string>('all')
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all')
|
||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(new Set())
|
||||
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set())
|
||||
const [expandedWholesalers, setExpandedWholesalers] = useState<Set<string>>(new Set())
|
||||
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set())
|
||||
|
||||
// Фильтрация согласно rules2.md 9.5.4 с поддержкой расширенной структуры
|
||||
const filteredSupplies = supplies.filter(supply => {
|
||||
const matchesSearch = supply.number.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(supply.supplier && supply.supplier.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(supply.routes && supply.routes.some(route =>
|
||||
route.wholesalers.some(wholesaler =>
|
||||
wholesaler.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
));
|
||||
const matchesMethod = selectedMethod === "all" || supply.creationMethod === selectedMethod;
|
||||
const matchesStatus = selectedStatus === "all" || supply.status === selectedStatus;
|
||||
|
||||
return matchesSearch && matchesMethod && matchesStatus;
|
||||
});
|
||||
const filteredSupplies = supplies.filter((supply) => {
|
||||
const matchesSearch =
|
||||
supply.number.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(supply.supplier && supply.supplier.toLowerCase().includes(searchQuery.toLowerCase())) ||
|
||||
(supply.routes &&
|
||||
supply.routes.some((route) =>
|
||||
route.wholesalers.some((wholesaler) => wholesaler.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
))
|
||||
const matchesMethod = selectedMethod === 'all' || supply.creationMethod === selectedMethod
|
||||
const matchesStatus = selectedStatus === 'all' || supply.status === selectedStatus
|
||||
|
||||
return matchesSearch && matchesMethod && matchesStatus
|
||||
})
|
||||
|
||||
const toggleSupplyExpansion = (supplyId: string) => {
|
||||
const newExpanded = new Set(expandedSupplies);
|
||||
const newExpanded = new Set(expandedSupplies)
|
||||
if (newExpanded.has(supplyId)) {
|
||||
newExpanded.delete(supplyId);
|
||||
newExpanded.delete(supplyId)
|
||||
} else {
|
||||
newExpanded.add(supplyId);
|
||||
newExpanded.add(supplyId)
|
||||
}
|
||||
setExpandedSupplies(newExpanded);
|
||||
};
|
||||
setExpandedSupplies(newExpanded)
|
||||
}
|
||||
|
||||
const toggleRouteExpansion = (routeId: string) => {
|
||||
const newExpanded = new Set(expandedRoutes);
|
||||
const newExpanded = new Set(expandedRoutes)
|
||||
if (newExpanded.has(routeId)) {
|
||||
newExpanded.delete(routeId);
|
||||
newExpanded.delete(routeId)
|
||||
} else {
|
||||
newExpanded.add(routeId);
|
||||
newExpanded.add(routeId)
|
||||
}
|
||||
setExpandedRoutes(newExpanded);
|
||||
};
|
||||
setExpandedRoutes(newExpanded)
|
||||
}
|
||||
|
||||
const toggleWholesalerExpansion = (wholesalerId: string) => {
|
||||
const newExpanded = new Set(expandedWholesalers);
|
||||
const newExpanded = new Set(expandedWholesalers)
|
||||
if (newExpanded.has(wholesalerId)) {
|
||||
newExpanded.delete(wholesalerId);
|
||||
newExpanded.delete(wholesalerId)
|
||||
} else {
|
||||
newExpanded.add(wholesalerId);
|
||||
newExpanded.add(wholesalerId)
|
||||
}
|
||||
setExpandedWholesalers(newExpanded);
|
||||
};
|
||||
setExpandedWholesalers(newExpanded)
|
||||
}
|
||||
|
||||
const toggleProductExpansion = (productId: string) => {
|
||||
const newExpanded = new Set(expandedProducts);
|
||||
const newExpanded = new Set(expandedProducts)
|
||||
if (newExpanded.has(productId)) {
|
||||
newExpanded.delete(productId);
|
||||
newExpanded.delete(productId)
|
||||
} else {
|
||||
newExpanded.add(productId);
|
||||
newExpanded.add(productId)
|
||||
}
|
||||
setExpandedProducts(newExpanded);
|
||||
};
|
||||
setExpandedProducts(newExpanded)
|
||||
}
|
||||
|
||||
// Вспомогательные функции
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusMap = {
|
||||
pending: { label: "Ожидает", color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30" },
|
||||
supplier_approved: { label: "Одобрена", color: "bg-blue-500/20 text-blue-300 border-blue-500/30" },
|
||||
confirmed: { label: "Подтверждена", color: "bg-purple-500/20 text-purple-300 border-purple-500/30" },
|
||||
shipped: { label: "Отгружена", color: "bg-orange-500/20 text-orange-300 border-orange-500/30" },
|
||||
in_transit: { label: "В пути", color: "bg-indigo-500/20 text-indigo-300 border-indigo-500/30" },
|
||||
delivered: { label: "Доставлена", color: "bg-green-500/20 text-green-300 border-green-500/30" },
|
||||
planned: { label: "Запланирована", color: "bg-blue-500/20 text-blue-300 border-blue-500/30" },
|
||||
completed: { label: "Завершена", color: "bg-purple-500/20 text-purple-300 border-purple-500/30" },
|
||||
};
|
||||
const statusInfo = statusMap[status as keyof typeof statusMap] || { label: status, color: "bg-gray-500/20 text-gray-300 border-gray-500/30" };
|
||||
return <Badge className={`${statusInfo.color} border`}>{statusInfo.label}</Badge>;
|
||||
};
|
||||
pending: { label: 'Ожидает', color: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' },
|
||||
supplier_approved: { label: 'Одобрена', color: 'bg-blue-500/20 text-blue-300 border-blue-500/30' },
|
||||
confirmed: { label: 'Подтверждена', color: 'bg-purple-500/20 text-purple-300 border-purple-500/30' },
|
||||
shipped: { label: 'Отгружена', color: 'bg-orange-500/20 text-orange-300 border-orange-500/30' },
|
||||
in_transit: { label: 'В пути', color: 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30' },
|
||||
delivered: { label: 'Доставлена', color: 'bg-green-500/20 text-green-300 border-green-500/30' },
|
||||
planned: { label: 'Запланирована', color: 'bg-blue-500/20 text-blue-300 border-blue-500/30' },
|
||||
completed: { label: 'Завершена', color: 'bg-purple-500/20 text-purple-300 border-purple-500/30' },
|
||||
}
|
||||
const statusInfo = statusMap[status as keyof typeof statusMap] || {
|
||||
label: status,
|
||||
color: 'bg-gray-500/20 text-gray-300 border-gray-500/30',
|
||||
}
|
||||
return <Badge className={`${statusInfo.color} border`}>{statusInfo.label}</Badge>
|
||||
}
|
||||
|
||||
const getEfficiencyBadge = (planned: number, actual: number, defect: number) => {
|
||||
const efficiency = ((actual - defect) / planned) * 100;
|
||||
const efficiency = ((actual - defect) / planned) * 100
|
||||
if (efficiency >= 95) {
|
||||
return <Badge className="bg-green-500/20 text-green-300 border-green-500/30 border">Отлично</Badge>;
|
||||
return <Badge className="bg-green-500/20 text-green-300 border-green-500/30 border">Отлично</Badge>
|
||||
} else if (efficiency >= 90) {
|
||||
return <Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30 border">Хорошо</Badge>;
|
||||
return <Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30 border">Хорошо</Badge>
|
||||
} else {
|
||||
return <Badge className="bg-red-500/20 text-red-300 border-red-500/30 border">Проблемы</Badge>;
|
||||
return <Badge className="bg-red-500/20 text-red-300 border-red-500/30 border">Проблемы</Badge>
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const calculateProductTotal = (product: GoodsSupplyProduct) => {
|
||||
return product.actualQty * product.productPrice;
|
||||
};
|
||||
return product.actualQty * product.productPrice
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -295,7 +315,7 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -371,20 +391,19 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
{filteredSupplies.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={12} className="text-center py-8 text-white/60">
|
||||
{searchQuery || selectedMethod !== "all" || selectedStatus !== "all"
|
||||
? "Поставки не найдены по заданным фильтрам"
|
||||
: "Поставки товаров отсутствуют"
|
||||
}
|
||||
{searchQuery || selectedMethod !== 'all' || selectedStatus !== 'all'
|
||||
? 'Поставки не найдены по заданным фильтрам'
|
||||
: 'Поставки товаров отсутствуют'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredSupplies.map((supply) => {
|
||||
const isSupplyExpanded = expandedSupplies.has(supply.id);
|
||||
|
||||
const isSupplyExpanded = expandedSupplies.has(supply.id)
|
||||
|
||||
return (
|
||||
<React.Fragment key={supply.id}>
|
||||
{/* Основная строка поставки */}
|
||||
<TableRow
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-purple-500/10"
|
||||
onClick={() => toggleSupplyExpansion(supply.id)}
|
||||
>
|
||||
@ -401,15 +420,11 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Calendar className="h-3 w-3 text-white/40" />
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatDate(supply.deliveryDate)}
|
||||
</span>
|
||||
<span className="text-white font-semibold text-sm">{formatDate(supply.deliveryDate)}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
<span className="text-white/80 text-sm">
|
||||
{formatDate(supply.createdAt)}
|
||||
</span>
|
||||
<span className="text-white/80 text-sm">{formatDate(supply.createdAt)}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
@ -422,9 +437,11 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`font-semibold text-sm ${
|
||||
(supply.defectTotal || 0) > 0 ? "text-red-400" : "text-white"
|
||||
}`}>
|
||||
<span
|
||||
className={`font-semibold text-sm ${
|
||||
(supply.defectTotal || 0) > 0 ? 'text-red-400' : 'text-white'
|
||||
}`}
|
||||
>
|
||||
{supply.defectTotal || 0}
|
||||
</span>
|
||||
</TableCell>
|
||||
@ -451,250 +468,269 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getStatusBadge(supply.status)}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(supply.status)}</TableCell>
|
||||
<TableCell>
|
||||
<CreationMethodIcon method={supply.creationMethod} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Развернутые уровни - маршруты, поставщики, товары */}
|
||||
{isSupplyExpanded && supply.routes && supply.routes.map((route) => {
|
||||
const isRouteExpanded = expandedRoutes.has(route.id);
|
||||
return (
|
||||
<React.Fragment key={route.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-blue-500/10"
|
||||
onClick={() => toggleRouteExpansion(route.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-blue-400 mr-1"></div>
|
||||
<MapPin className="h-3 w-3 text-blue-400" />
|
||||
<span className="text-white font-medium text-sm">Маршрут</span>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-0.5 h-full bg-blue-400/30"></div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={1}>
|
||||
<div className="text-white">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<span className="font-medium text-sm">{route.from}</span>
|
||||
<span className="text-white/60">→</span>
|
||||
<span className="font-medium text-sm">{route.to}</span>
|
||||
{isSupplyExpanded &&
|
||||
supply.routes &&
|
||||
supply.routes.map((route) => {
|
||||
const isRouteExpanded = expandedRoutes.has(route.id)
|
||||
return (
|
||||
<React.Fragment key={route.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-blue-500/10"
|
||||
onClick={() => toggleRouteExpansion(route.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-blue-400 mr-1"></div>
|
||||
<MapPin className="h-3 w-3 text-blue-400" />
|
||||
<span className="text-white font-medium text-sm">Маршрут</span>
|
||||
</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{route.fromAddress} → {route.toAddress}
|
||||
<div className="absolute left-0 top-0 w-0.5 h-full bg-blue-400/30"></div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={1}>
|
||||
<div className="text-white">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<span className="font-medium text-sm">{route.from}</span>
|
||||
<span className="text-white/60">→</span>
|
||||
<span className="font-medium text-sm">{route.to}</span>
|
||||
</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{route.fromAddress} → {route.toAddress}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell"></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{route.wholesalers.reduce((sum, w) =>
|
||||
sum + w.products.reduce((pSum, p) => pSum + p.plannedQty, 0), 0
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{route.wholesalers.reduce((sum, w) =>
|
||||
sum + w.products.reduce((pSum, p) => pSum + p.actualQty, 0), 0
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{route.wholesalers.reduce((sum, w) =>
|
||||
sum + w.products.reduce((pSum, p) => pSum + p.defectQty, 0), 0
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(route.totalProductPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
<span className="text-blue-400 font-medium text-sm">
|
||||
{formatCurrency(route.fulfillmentServicePrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
<span className="text-purple-400 font-medium text-sm">
|
||||
{formatCurrency(route.logisticsPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(route.totalAmount)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell"></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{route.wholesalers.reduce(
|
||||
(sum, w) => sum + w.products.reduce((pSum, p) => pSum + p.plannedQty, 0),
|
||||
0,
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{route.wholesalers.reduce(
|
||||
(sum, w) => sum + w.products.reduce((pSum, p) => pSum + p.actualQty, 0),
|
||||
0,
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{route.wholesalers.reduce(
|
||||
(sum, w) => sum + w.products.reduce((pSum, p) => pSum + p.defectQty, 0),
|
||||
0,
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(route.totalProductPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
<span className="text-blue-400 font-medium text-sm">
|
||||
{formatCurrency(route.fulfillmentServicePrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
<span className="text-purple-400 font-medium text-sm">
|
||||
{formatCurrency(route.logisticsPrice)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(route.totalAmount)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Поставщики в маршруте */}
|
||||
{isRouteExpanded && route.wholesalers.map((wholesaler) => {
|
||||
const isWholesalerExpanded = expandedWholesalers.has(wholesaler.id);
|
||||
return (
|
||||
<React.Fragment key={wholesaler.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-green-500/10"
|
||||
onClick={() => toggleWholesalerExpansion(wholesaler.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
|
||||
<Building2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-white font-medium text-sm">Поставщик</span>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-0.5 h-full bg-green-400/30"></div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={1}>
|
||||
<div className="text-white">
|
||||
<div className="font-medium mb-1 text-sm">{wholesaler.name}</div>
|
||||
<div className="text-xs text-white/60 mb-1 hidden sm:block">
|
||||
ИНН: {wholesaler.inn}
|
||||
</div>
|
||||
<div className="text-xs text-white/60 mb-1 hidden lg:block">
|
||||
{wholesaler.address}
|
||||
</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{wholesaler.contact}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell"></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{wholesaler.products.reduce((sum, p) => sum + p.plannedQty, 0)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{wholesaler.products.reduce((sum, p) => sum + p.actualQty, 0)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{wholesaler.products.reduce((sum, p) => sum + p.defectQty, 0)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(wholesaler.products.reduce((sum, p) => sum + calculateProductTotal(p), 0))}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell" colSpan={2}></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(wholesaler.totalAmount)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
{/* Поставщики в маршруте */}
|
||||
{isRouteExpanded &&
|
||||
route.wholesalers.map((wholesaler) => {
|
||||
const isWholesalerExpanded = expandedWholesalers.has(wholesaler.id)
|
||||
return (
|
||||
<React.Fragment key={wholesaler.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-green-500/10"
|
||||
onClick={() => toggleWholesalerExpansion(wholesaler.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-green-400 mr-1"></div>
|
||||
<Building2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-white font-medium text-sm">Поставщик</span>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-0.5 h-full bg-green-400/30"></div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={1}>
|
||||
<div className="text-white">
|
||||
<div className="font-medium mb-1 text-sm">{wholesaler.name}</div>
|
||||
<div className="text-xs text-white/60 mb-1 hidden sm:block">
|
||||
ИНН: {wholesaler.inn}
|
||||
</div>
|
||||
<div className="text-xs text-white/60 mb-1 hidden lg:block">
|
||||
{wholesaler.address}
|
||||
</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{wholesaler.contact}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell"></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{wholesaler.products.reduce((sum, p) => sum + p.plannedQty, 0)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{wholesaler.products.reduce((sum, p) => sum + p.actualQty, 0)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white/80 text-sm">
|
||||
{wholesaler.products.reduce((sum, p) => sum + p.defectQty, 0)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-green-400 font-medium text-sm">
|
||||
{formatCurrency(
|
||||
wholesaler.products.reduce((sum, p) => sum + calculateProductTotal(p), 0),
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell" colSpan={2}></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(wholesaler.totalAmount)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Товары поставщика */}
|
||||
{isWholesalerExpanded && wholesaler.products.map((product) => {
|
||||
const isProductExpanded = expandedProducts.has(product.id);
|
||||
return (
|
||||
<React.Fragment key={product.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-yellow-500/10"
|
||||
onClick={() => toggleProductExpansion(product.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
|
||||
<Package className="h-3 w-3 text-yellow-400" />
|
||||
<span className="text-white font-medium text-sm">Товар</span>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-0.5 h-full bg-yellow-400/30"></div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={1}>
|
||||
<div className="text-white">
|
||||
<div className="font-medium mb-1 text-sm">{product.name}</div>
|
||||
<div className="text-xs text-white/60 mb-1 hidden sm:block">
|
||||
Артикул: {product.sku}
|
||||
</div>
|
||||
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs hidden sm:inline-flex">
|
||||
{product.category}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell"></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">{product.plannedQty}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">{product.actualQty}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={`font-semibold text-sm ${
|
||||
product.defectQty > 0 ? "text-red-400" : "text-white"
|
||||
}`}>
|
||||
{product.defectQty}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-white">
|
||||
<div className="font-medium text-sm">
|
||||
{formatCurrency(calculateProductTotal(product))}
|
||||
</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{formatCurrency(product.productPrice)} за шт.
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell" colSpan={2}>
|
||||
{getEfficiencyBadge(product.plannedQty, product.actualQty, product.defectQty)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(calculateProductTotal(product))}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
{/* Товары поставщика */}
|
||||
{isWholesalerExpanded &&
|
||||
wholesaler.products.map((product) => {
|
||||
const isProductExpanded = expandedProducts.has(product.id)
|
||||
return (
|
||||
<React.Fragment key={product.id}>
|
||||
<TableRow
|
||||
className="border-white/10 hover:bg-white/5 cursor-pointer transition-colors bg-yellow-500/10"
|
||||
onClick={() => toggleProductExpansion(product.id)}
|
||||
>
|
||||
<TableCell className="relative">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
|
||||
<div className="w-1 h-1 rounded-full bg-yellow-400 mr-1"></div>
|
||||
<Package className="h-3 w-3 text-yellow-400" />
|
||||
<span className="text-white font-medium text-sm">Товар</span>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-0.5 h-full bg-yellow-400/30"></div>
|
||||
</TableCell>
|
||||
<TableCell colSpan={1}>
|
||||
<div className="text-white">
|
||||
<div className="font-medium mb-1 text-sm">{product.name}</div>
|
||||
<div className="text-xs text-white/60 mb-1 hidden sm:block">
|
||||
Артикул: {product.sku}
|
||||
</div>
|
||||
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs hidden sm:inline-flex">
|
||||
{product.category}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell"></TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{product.plannedQty}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{product.actualQty}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`font-semibold text-sm ${
|
||||
product.defectQty > 0 ? 'text-red-400' : 'text-white'
|
||||
}`}
|
||||
>
|
||||
{product.defectQty}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-white">
|
||||
<div className="font-medium text-sm">
|
||||
{formatCurrency(calculateProductTotal(product))}
|
||||
</div>
|
||||
<div className="text-xs text-white/60 hidden sm:block">
|
||||
{formatCurrency(product.productPrice)} за шт.
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell" colSpan={2}>
|
||||
{getEfficiencyBadge(
|
||||
product.plannedQty,
|
||||
product.actualQty,
|
||||
product.defectQty,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-white font-semibold text-sm">
|
||||
{formatCurrency(calculateProductTotal(product))}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Параметры товара */}
|
||||
{isProductExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={12} className="p-0">
|
||||
<div className="bg-white/5 border-t border-white/10">
|
||||
<div className="p-4">
|
||||
<h4 className="text-white font-medium mb-3 flex items-center space-x-2">
|
||||
<span className="text-xs text-white/60">📋 Параметры товара:</span>
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{product.parameters.map((param) => (
|
||||
<div key={param.id} className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-white/80 text-xs font-medium mb-1">
|
||||
{param.name}
|
||||
</div>
|
||||
<div className="text-white text-sm">
|
||||
{param.value} {param.unit || ""}
|
||||
{/* Параметры товара */}
|
||||
{isProductExpanded && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={12} className="p-0">
|
||||
<div className="bg-white/5 border-t border-white/10">
|
||||
<div className="p-4">
|
||||
<h4 className="text-white font-medium mb-3 flex items-center space-x-2">
|
||||
<span className="text-xs text-white/60">
|
||||
📋 Параметры товара:
|
||||
</span>
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{product.parameters.map((param) => (
|
||||
<div key={param.id} className="bg-white/5 rounded-lg p-3">
|
||||
<div className="text-white/80 text-xs font-medium mb-1">
|
||||
{param.name}
|
||||
</div>
|
||||
<div className="text-white text-sm">
|
||||
{param.value} {param.unit || ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Базовая детализация для поставок без маршрутов */}
|
||||
{isSupplyExpanded && supply.items && !supply.routes && (
|
||||
@ -704,7 +740,10 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
<h4 className="text-white font-medium">Детализация товаров:</h4>
|
||||
<div className="grid gap-2">
|
||||
{supply.items.map((item) => (
|
||||
<div key={item.id} className="flex justify-between items-center py-2 px-3 bg-white/5 rounded-lg">
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex justify-between items-center py-2 px-3 bg-white/5 rounded-lg"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<span className="text-white text-sm">{item.name}</span>
|
||||
{item.category && (
|
||||
@ -714,7 +753,9 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-white/80">{item.quantity} шт</span>
|
||||
<span className="text-white/80">{formatCurrency(item.price)}</span>
|
||||
<span className="text-white font-medium">{formatCurrency(item.price * item.quantity)}</span>
|
||||
<span className="text-white font-medium">
|
||||
{formatCurrency(item.price * item.quantity)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -724,12 +765,12 @@ export function GoodsSuppliesTable({ supplies = [], loading = false }: GoodsSupp
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user