From fc37472415fba19bd80213115930a0b1074fe978 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Mon, 21 Jul 2025 14:35:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=BE=D0=BA:=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=82=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=20=D0=B8=20=D1=80=D0=B0=D1=81=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2,=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D1=84=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C.=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B8=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B8,=20=D0=BE=D0=BF=D1=82=D0=B8?= =?UTF-8?q?=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=B4=D0=BB=D1=8F=20=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B5=D0=B9=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumables-supplies-tab.tsx | 948 ++++++++++++++++++ .../goods-supplies/goods-supplies-tab.tsx | 866 ++++++++++++++++ .../supplies/supplies-dashboard.tsx | 768 +------------- 3 files changed, 1869 insertions(+), 713 deletions(-) create mode 100644 src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx create mode 100644 src/components/supplies/goods-supplies/goods-supplies-tab.tsx diff --git a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx new file mode 100644 index 0000000..c7d2b24 --- /dev/null +++ b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx @@ -0,0 +1,948 @@ +"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 { + ChevronDown, + ChevronRight, + Calendar, + Package, + MapPin, + Building2, + TrendingUp, + AlertTriangle, + DollarSign, + Wrench, + Box, +} from "lucide-react"; + +// Типы данных для расходников +interface ConsumableParameter { + id: string; + name: string; + value: string; + unit?: string; +} + +interface Consumable { + id: string; + name: string; + sku: string; + category: string; + type: "packaging" | "labels" | "protective" | "tools" | "other"; + plannedQty: number; + actualQty: number; + defectQty: number; + unitPrice: number; + parameters: ConsumableParameter[]; +} + +interface ConsumableSupplier { + id: string; + name: string; + inn: string; + contact: string; + address: string; + consumables: Consumable[]; + totalAmount: number; +} + +interface ConsumableRoute { + id: string; + from: string; + fromAddress: string; + to: string; + toAddress: string; + suppliers: ConsumableSupplier[]; + totalConsumablesPrice: number; + logisticsPrice: number; + totalAmount: number; +} + +interface ConsumableSupply { + id: string; + number: number; + deliveryDate: string; + createdDate: string; + routes: ConsumableRoute[]; + plannedTotal: number; + actualTotal: number; + defectTotal: number; + totalConsumablesPrice: number; + totalLogisticsPrice: number; + grandTotal: number; + status: "planned" | "in-transit" | "delivered" | "completed"; +} + +// Моковые данные для расходников +const mockConsumableSupplies: ConsumableSupply[] = [ + { + id: "c1", + number: 101, + deliveryDate: "2024-01-18", + createdDate: "2024-01-14", + status: "delivered", + plannedTotal: 5000, + actualTotal: 4950, + defectTotal: 50, + totalConsumablesPrice: 125000, + totalLogisticsPrice: 8000, + grandTotal: 133000, + routes: [ + { + id: "cr1", + from: "Склад расходников", + fromAddress: "Москва, ул. Промышленная, 12", + to: "SFERAV Logistics", + toAddress: "Москва, ул. Складская, 15", + totalConsumablesPrice: 125000, + logisticsPrice: 8000, + totalAmount: 133000, + suppliers: [ + { + id: "cs1", + name: 'ООО "УпакСервис"', + inn: "7703456789", + contact: "+7 (495) 777-88-99", + address: "Москва, ул. Упаковочная, 5", + totalAmount: 75000, + consumables: [ + { + id: "cons1", + name: "Коробки картонные 30x20x10", + sku: "BOX-302010", + category: "Упаковка", + type: "packaging", + plannedQty: 2000, + actualQty: 1980, + defectQty: 20, + unitPrice: 35, + parameters: [ + { id: "cp1", name: "Размер", value: "30x20x10", unit: "см" }, + { id: "cp2", name: "Материал", value: "Гофрокартон" }, + { id: "cp3", name: "Плотность", value: "3", unit: "слоя" }, + ], + }, + { + id: "cons2", + name: "Пузырчатая пленка", + sku: "BUBBLE-100", + category: "Защитная упаковка", + type: "protective", + plannedQty: 1000, + actualQty: 1000, + defectQty: 0, + unitPrice: 25, + parameters: [ + { id: "cp4", name: "Ширина", value: "100", unit: "см" }, + { id: "cp5", name: "Толщина", value: "0.1", unit: "мм" }, + { id: "cp6", name: "Длина рулона", value: "50", unit: "м" }, + ], + }, + ], + }, + { + id: "cs2", + name: "ИП Маркин С.А.", + inn: "123456789013", + contact: "+7 (499) 111-22-33", + address: "Москва, пр-т Маркировочный, 8", + totalAmount: 50000, + consumables: [ + { + id: "cons3", + name: "Этикетки самоклеящиеся", + sku: "LABEL-5030", + category: "Маркировка", + type: "labels", + plannedQty: 2000, + actualQty: 1970, + defectQty: 30, + unitPrice: 15, + parameters: [ + { id: "cp7", name: "Размер", value: "50x30", unit: "мм" }, + { id: "cp8", name: "Материал", value: "Полипропилен" }, + { id: "cp9", name: "Клей", value: "Акриловый" }, + ], + }, + ], + }, + ], + }, + ], + }, + { + id: "c2", + number: 102, + deliveryDate: "2024-01-22", + createdDate: "2024-01-16", + status: "in-transit", + plannedTotal: 1500, + actualTotal: 1500, + defectTotal: 0, + totalConsumablesPrice: 45000, + totalLogisticsPrice: 3000, + grandTotal: 48000, + routes: [ + { + id: "cr2", + from: "Инструментальный склад", + fromAddress: "Подольск, ул. Индустриальная, 25", + to: "WB Подольск", + toAddress: "Подольск, ул. Складская, 25", + totalConsumablesPrice: 45000, + logisticsPrice: 3000, + totalAmount: 48000, + suppliers: [ + { + id: "cs3", + name: 'ООО "ИнструментПро"', + inn: "5001234567", + contact: "+7 (4967) 55-66-77", + address: "Подольск, ул. Инструментальная, 15", + totalAmount: 45000, + consumables: [ + { + id: "cons4", + name: "Сканер штрих-кодов", + sku: "SCANNER-2D", + category: "Оборудование", + type: "tools", + plannedQty: 5, + actualQty: 5, + defectQty: 0, + unitPrice: 8000, + parameters: [ + { id: "cp10", name: "Тип", value: "2D сканер" }, + { id: "cp11", name: "Интерфейс", value: "USB" }, + { id: "cp12", name: "Дальность", value: "30", unit: "см" }, + ], + }, + { + id: "cons5", + name: "Термопринтер этикеток", + sku: "PRINTER-THERMAL", + category: "Оборудование", + type: "tools", + plannedQty: 2, + actualQty: 2, + defectQty: 0, + unitPrice: 2500, + parameters: [ + { + id: "cp13", + name: "Ширина печати", + value: "108", + unit: "мм", + }, + { id: "cp14", name: "Разрешение", value: "203", unit: "dpi" }, + { id: "cp15", name: "Скорость", value: "102", unit: "мм/с" }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export function SuppliesConsumablesTab() { + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); + const [expandedSuppliers, setExpandedSuppliers] = useState>( + new Set() + ); + const [expandedConsumables, setExpandedConsumables] = useState>( + new Set() + ); + + const toggleSupplyExpansion = (supplyId: string) => { + const newExpanded = new Set(expandedSupplies); + if (newExpanded.has(supplyId)) { + newExpanded.delete(supplyId); + } else { + newExpanded.add(supplyId); + } + setExpandedSupplies(newExpanded); + }; + + const toggleRouteExpansion = (routeId: string) => { + const newExpanded = new Set(expandedRoutes); + if (newExpanded.has(routeId)) { + newExpanded.delete(routeId); + } else { + newExpanded.add(routeId); + } + setExpandedRoutes(newExpanded); + }; + + const toggleSupplierExpansion = (supplierId: string) => { + const newExpanded = new Set(expandedSuppliers); + if (newExpanded.has(supplierId)) { + newExpanded.delete(supplierId); + } else { + newExpanded.add(supplierId); + } + setExpandedSuppliers(newExpanded); + }; + + const toggleConsumableExpansion = (consumableId: string) => { + const newExpanded = new Set(expandedConsumables); + if (newExpanded.has(consumableId)) { + newExpanded.delete(consumableId); + } else { + newExpanded.add(consumableId); + } + setExpandedConsumables(newExpanded); + }; + + const getStatusBadge = (status: ConsumableSupply["status"]) => { + const statusMap = { + planned: { + label: "Запланирована", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + "in-transit": { + label: "В пути", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + }, + delivered: { + label: "Доставлена", + color: "bg-green-500/20 text-green-300 border-green-500/30", + }, + completed: { + label: "Завершена", + color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + }, + }; + const { label, color } = statusMap[status]; + return {label}; + }; + + const getTypeBadge = (type: Consumable["type"]) => { + const typeMap = { + packaging: { + label: "Упаковка", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + labels: { + label: "Этикетки", + color: "bg-green-500/20 text-green-300 border-green-500/30", + }, + protective: { + label: "Защитная", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + }, + tools: { + label: "Инструменты", + color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + }, + other: { + label: "Прочее", + color: "bg-gray-500/20 text-gray-300 border-gray-500/30", + }, + }; + const { label, color } = typeMap[type]; + return {label}; + }; + + 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 calculateConsumableTotal = (consumable: Consumable) => { + return consumable.actualQty * consumable.unitPrice; + }; + + const getEfficiencyBadge = ( + planned: number, + actual: number, + defect: number + ) => { + const efficiency = ((actual - defect) / planned) * 100; + if (efficiency >= 95) { + return ( + + Отлично + + ); + } else if (efficiency >= 90) { + return ( + + Хорошо + + ); + } else { + return ( + + Проблемы + + ); + } + }; + + return ( +
+ {/* Статистика расходников */} +
+ +
+
+ +
+
+

Поставок расходников

+

+ {mockConsumableSupplies.length} +

+
+
+
+ + +
+
+ +
+
+

Сумма расходников

+

+ {formatCurrency( + mockConsumableSupplies.reduce( + (sum, supply) => sum + supply.grandTotal, + 0 + ) + )} +

+
+
+
+ + +
+
+ +
+
+

В пути

+

+ { + mockConsumableSupplies.filter( + (supply) => supply.status === "in-transit" + ).length + } +

+
+
+
+ + +
+
+ +
+
+

С браком

+

+ { + mockConsumableSupplies.filter( + (supply) => supply.defectTotal > 0 + ).length + } +

+
+
+
+
+ + {/* Таблица поставок расходников */} + +
+ + + + + + + + + + + + + + + + + {mockConsumableSupplies.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Основная строка поставки расходников */} + + + + + + + + + + + + + + {/* Развернутые уровни */} + {isSupplyExpanded && + supply.routes.map((route) => { + const isRouteExpanded = expandedRoutes.has(route.id); + return ( + + + + + + + + + + + + + + {/* Поставщики */} + {isRouteExpanded && + route.suppliers.map((supplier) => { + const isSupplierExpanded = + expandedSuppliers.has(supplier.id); + return ( + + + + + + + + + + + + + + {/* Расходники */} + {isSupplierExpanded && + supplier.consumables.map((consumable) => { + const isConsumableExpanded = + expandedConsumables.has( + consumable.id + ); + return ( + + + + + + + + + + + + + + {/* Параметры расходника */} + {isConsumableExpanded && ( + + + + )} + + ); + })} + + ); + })} + + ); + })} + + ); + })} + +
+ Дата поставки + + Дата создания + ПланФактБрак + Цена расходников + + Логистика + + Итого сумма + + Статус +
+
+ + + #{supply.number} + +
+
+
+ + + {formatDate(supply.deliveryDate)} + +
+
+ + {formatDate(supply.createdDate)} + + + + {supply.plannedTotal} + + + + {supply.actualTotal} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {supply.defectTotal} + + + + {formatCurrency(supply.totalConsumablesPrice)} + + + + {formatCurrency(supply.totalLogisticsPrice)} + + +
+ + + {formatCurrency(supply.grandTotal)} + +
+
{getStatusBadge(supply.status)}
+
+ + + + Маршрут + +
+
+
+
+ + {route.from} + + + + {route.to} + +
+
+ {route.fromAddress} → {route.toAddress} +
+
+
+ + {route.suppliers.reduce( + (sum, s) => + sum + + s.consumables.reduce( + (cSum, c) => cSum + c.plannedQty, + 0 + ), + 0 + )} + + + + {route.suppliers.reduce( + (sum, s) => + sum + + s.consumables.reduce( + (cSum, c) => cSum + c.actualQty, + 0 + ), + 0 + )} + + + + {route.suppliers.reduce( + (sum, s) => + sum + + s.consumables.reduce( + (cSum, c) => cSum + c.defectQty, + 0 + ), + 0 + )} + + + + {formatCurrency(route.totalConsumablesPrice)} + + + + {formatCurrency(route.logisticsPrice)} + + + + {formatCurrency(route.totalAmount)} + +
+
+ + + + Поставщик + +
+
+
+
+ {supplier.name} +
+
+ ИНН: {supplier.inn} +
+
+ {supplier.address} +
+
+ {supplier.contact} +
+
+
+ + {supplier.consumables.reduce( + (sum, c) => sum + c.plannedQty, + 0 + )} + + + + {supplier.consumables.reduce( + (sum, c) => sum + c.actualQty, + 0 + )} + + + + {supplier.consumables.reduce( + (sum, c) => sum + c.defectQty, + 0 + )} + + + + {formatCurrency( + supplier.consumables.reduce( + (sum, c) => + sum + + calculateConsumableTotal(c), + 0 + ) + )} + + + + {formatCurrency(supplier.totalAmount)} + +
+
+ + + + Расходник + +
+
+
+
+ {consumable.name} +
+
+ Артикул: {consumable.sku} +
+
+ + {consumable.category} + + {getTypeBadge( + consumable.type + )} +
+
+
+ + {consumable.plannedQty} + + + + {consumable.actualQty} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {consumable.defectQty} + + +
+
+ {formatCurrency( + calculateConsumableTotal( + consumable + ) + )} +
+
+ {formatCurrency( + consumable.unitPrice + )}{" "} + за шт. +
+
+
+ {getEfficiencyBadge( + consumable.plannedQty, + consumable.actualQty, + consumable.defectQty + )} + + + {formatCurrency( + calculateConsumableTotal( + consumable + ) + )} + +
+
+
+

+ + 📋 Параметры + расходника: + +

+
+ {consumable.parameters.map( + (param) => ( +
+
+ {param.name} +
+
+ {param.value}{" "} + {param.unit || + ""} +
+
+ ) + )} +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/supplies/goods-supplies/goods-supplies-tab.tsx b/src/components/supplies/goods-supplies/goods-supplies-tab.tsx new file mode 100644 index 0000000..0165009 --- /dev/null +++ b/src/components/supplies/goods-supplies/goods-supplies-tab.tsx @@ -0,0 +1,866 @@ +"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 { + ChevronDown, + ChevronRight, + Calendar, + Package, + MapPin, + Building2, + TrendingUp, + AlertTriangle, + DollarSign, +} from "lucide-react"; + +// Типы данных для товаров +interface ProductParameter { + id: string; + name: string; + value: string; + unit?: string; +} + +interface Product { + id: string; + name: string; + sku: string; + category: string; + plannedQty: number; + actualQty: number; + defectQty: number; + productPrice: number; + parameters: ProductParameter[]; +} + +interface Wholesaler { + id: string; + name: string; + inn: string; + contact: string; + address: string; + products: Product[]; + totalAmount: number; +} + +interface Route { + id: string; + from: string; + fromAddress: string; + to: string; + toAddress: string; + wholesalers: Wholesaler[]; + totalProductPrice: number; + fulfillmentServicePrice: number; + logisticsPrice: number; + totalAmount: number; +} + +interface Supply { + id: string; + number: number; + deliveryDate: string; + createdDate: string; + routes: Route[]; + plannedTotal: number; + actualTotal: number; + defectTotal: number; + totalProductPrice: number; + totalFulfillmentPrice: number; + totalLogisticsPrice: number; + grandTotal: number; + status: "planned" | "in-transit" | "delivered" | "completed"; +} + +// Моковые данные для товаров +const mockGoodsSupplies: Supply[] = [ + { + id: "1", + number: 1, + deliveryDate: "2024-01-15", + createdDate: "2024-01-10", + status: "delivered", + plannedTotal: 180, + actualTotal: 173, + defectTotal: 2, + totalProductPrice: 3750000, + totalFulfillmentPrice: 43000, + totalLogisticsPrice: 27000, + grandTotal: 3820000, + routes: [ + { + id: "r1", + from: "Садовод", + fromAddress: "Москва, 14-й км МКАД", + to: "SFERAV Logistics", + toAddress: "Москва, ул. Складская, 15", + totalProductPrice: 3600000, + fulfillmentServicePrice: 25000, + logisticsPrice: 15000, + totalAmount: 3640000, + wholesalers: [ + { + id: "w1", + name: 'ООО "ТехноСнаб"', + inn: "7701234567", + contact: "+7 (495) 123-45-67", + address: "Москва, ул. Торговая, 1", + totalAmount: 3600000, + products: [ + { + id: "p1", + name: "Смартфон iPhone 15", + sku: "APL-IP15-128", + category: "Электроника", + plannedQty: 50, + actualQty: 48, + defectQty: 2, + productPrice: 75000, + parameters: [ + { id: "param1", name: "Цвет", value: "Черный" }, + { id: "param2", name: "Память", value: "128", unit: "ГБ" }, + { id: "param3", name: "Гарантия", value: "12", unit: "мес" }, + ], + }, + ], + }, + ], + }, + ], + }, + { + id: "2", + number: 2, + deliveryDate: "2024-01-20", + createdDate: "2024-01-12", + status: "in-transit", + plannedTotal: 30, + actualTotal: 30, + defectTotal: 0, + totalProductPrice: 750000, + totalFulfillmentPrice: 18000, + totalLogisticsPrice: 12000, + grandTotal: 780000, + routes: [ + { + id: "r3", + from: "Садовод", + fromAddress: "Москва, 14-й км МКАД", + to: "WB Подольск", + toAddress: "Подольск, ул. Складская, 25", + totalProductPrice: 750000, + fulfillmentServicePrice: 18000, + logisticsPrice: 12000, + totalAmount: 780000, + wholesalers: [ + { + id: "w3", + name: 'ООО "АудиоТех"', + inn: "7702345678", + contact: "+7 (495) 555-12-34", + address: "Москва, ул. Звуковая, 8", + totalAmount: 750000, + products: [ + { + id: "p3", + name: "Наушники AirPods Pro", + sku: "APL-AP-PRO2", + category: "Аудио", + plannedQty: 30, + actualQty: 30, + defectQty: 0, + productPrice: 25000, + parameters: [ + { id: "param6", name: "Тип", value: "Беспроводные" }, + { id: "param7", name: "Шумоподавление", value: "Активное" }, + { id: "param8", name: "Время работы", value: "6", unit: "ч" }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export function SuppliesGoodsTab() { + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); + const [expandedWholesalers, setExpandedWholesalers] = useState>( + new Set() + ); + const [expandedProducts, setExpandedProducts] = useState>( + new Set() + ); + + const toggleSupplyExpansion = (supplyId: string) => { + const newExpanded = new Set(expandedSupplies); + if (newExpanded.has(supplyId)) { + newExpanded.delete(supplyId); + } else { + newExpanded.add(supplyId); + } + setExpandedSupplies(newExpanded); + }; + + const toggleRouteExpansion = (routeId: string) => { + const newExpanded = new Set(expandedRoutes); + if (newExpanded.has(routeId)) { + newExpanded.delete(routeId); + } else { + newExpanded.add(routeId); + } + setExpandedRoutes(newExpanded); + }; + + const toggleWholesalerExpansion = (wholesalerId: string) => { + const newExpanded = new Set(expandedWholesalers); + if (newExpanded.has(wholesalerId)) { + newExpanded.delete(wholesalerId); + } else { + newExpanded.add(wholesalerId); + } + setExpandedWholesalers(newExpanded); + }; + + const toggleProductExpansion = (productId: string) => { + const newExpanded = new Set(expandedProducts); + if (newExpanded.has(productId)) { + newExpanded.delete(productId); + } else { + newExpanded.add(productId); + } + setExpandedProducts(newExpanded); + }; + + const getStatusBadge = (status: Supply["status"]) => { + const statusMap = { + planned: { + label: "Запланирована", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + "in-transit": { + label: "В пути", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + }, + delivered: { + label: "Доставлена", + color: "bg-green-500/20 text-green-300 border-green-500/30", + }, + completed: { + label: "Завершена", + color: "bg-purple-500/20 text-purple-300 border-purple-500/30", + }, + }; + const { label, color } = statusMap[status]; + return {label}; + }; + + 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 calculateProductTotal = (product: Product) => { + return product.actualQty * product.productPrice; + }; + + const getEfficiencyBadge = ( + planned: number, + actual: number, + defect: number + ) => { + const efficiency = ((actual - defect) / planned) * 100; + if (efficiency >= 95) { + return ( + + Отлично + + ); + } else if (efficiency >= 90) { + return ( + + Хорошо + + ); + } else { + return ( + + Проблемы + + ); + } + }; + + return ( +
+ {/* Статистика товаров */} +
+ +
+
+ +
+
+

Поставок товаров

+

+ {mockGoodsSupplies.length} +

+
+
+
+ + +
+
+ +
+
+

Сумма товаров

+

+ {formatCurrency( + mockGoodsSupplies.reduce( + (sum, supply) => sum + supply.grandTotal, + 0 + ) + )} +

+
+
+
+ + +
+
+ +
+
+

В пути

+

+ { + mockGoodsSupplies.filter( + (supply) => supply.status === "in-transit" + ).length + } +

+
+
+
+ + +
+
+ +
+
+

С браком

+

+ { + mockGoodsSupplies.filter((supply) => supply.defectTotal > 0) + .length + } +

+
+
+
+
+ + {/* Таблица поставок товаров */} + +
+ + + + + + + + + + + + + + + + + + {mockGoodsSupplies.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Уровень 1: Основная строка поставки */} + + + + + + + + + + + + + + + {/* Развернутые уровни */} + {isSupplyExpanded && + supply.routes.map((route) => { + const isRouteExpanded = expandedRoutes.has(route.id); + return ( + + + + + + + + + + + + + + + {/* Дальнейшие уровни развертывания */} + {isRouteExpanded && + route.wholesalers.map((wholesaler) => { + const isWholesalerExpanded = + expandedWholesalers.has(wholesaler.id); + return ( + + + + + + + + + + + + + + {/* Товары */} + {isWholesalerExpanded && + wholesaler.products.map((product) => { + const isProductExpanded = + expandedProducts.has(product.id); + return ( + + + + + + + + + + + + + + {/* Параметры товара */} + {isProductExpanded && ( + + + + )} + + ); + })} + + ); + })} + + ); + })} + + ); + })} + +
+ Дата поставки + + Дата создания + ПланФактБрак + Цена товаров + + Услуги ФФ + + Логистика до ФФ + + Итого сумма + + Статус +
+
+ + + #{supply.number} + +
+
+
+ + + {formatDate(supply.deliveryDate)} + +
+
+ + {formatDate(supply.createdDate)} + + + + {supply.plannedTotal} + + + + {supply.actualTotal} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {supply.defectTotal} + + + + {formatCurrency(supply.totalProductPrice)} + + + + {formatCurrency(supply.totalFulfillmentPrice)} + + + + {formatCurrency(supply.totalLogisticsPrice)} + + +
+ + + {formatCurrency(supply.grandTotal)} + +
+
{getStatusBadge(supply.status)}
+
+ + + + Маршрут + +
+
+
+
+ + {route.from} + + + + {route.to} + +
+
+ {route.fromAddress} → {route.toAddress} +
+
+
+ + {route.wholesalers.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.plannedQty, + 0 + ), + 0 + )} + + + + {route.wholesalers.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.actualQty, + 0 + ), + 0 + )} + + + + {route.wholesalers.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.defectQty, + 0 + ), + 0 + )} + + + + {formatCurrency(route.totalProductPrice)} + + + + {formatCurrency( + route.fulfillmentServicePrice + )} + + + + {formatCurrency(route.logisticsPrice)} + + + + {formatCurrency(route.totalAmount)} + +
+
+ + + + Оптовик + +
+
+
+
+ {wholesaler.name} +
+
+ ИНН: {wholesaler.inn} +
+
+ {wholesaler.address} +
+
+ {wholesaler.contact} +
+
+
+ + {wholesaler.products.reduce( + (sum, p) => sum + p.plannedQty, + 0 + )} + + + + {wholesaler.products.reduce( + (sum, p) => sum + p.actualQty, + 0 + )} + + + + {wholesaler.products.reduce( + (sum, p) => sum + p.defectQty, + 0 + )} + + + + {formatCurrency( + wholesaler.products.reduce( + (sum, p) => + sum + calculateProductTotal(p), + 0 + ) + )} + + + + {formatCurrency( + wholesaler.totalAmount + )} + +
+
+ + + + Товар + +
+
+
+
+ {product.name} +
+
+ Артикул: {product.sku} +
+ + {product.category} + +
+
+ + {product.plannedQty} + + + + {product.actualQty} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {product.defectQty} + + +
+
+ {formatCurrency( + calculateProductTotal( + product + ) + )} +
+
+ {formatCurrency( + product.productPrice + )}{" "} + за шт. +
+
+
+ {getEfficiencyBadge( + product.plannedQty, + product.actualQty, + product.defectQty + )} + + + {formatCurrency( + calculateProductTotal( + product + ) + )} + +
+
+
+

+ + 📋 Параметры товара: + +

+
+ {product.parameters.map( + (param) => ( +
+
+ {param.name} +
+
+ {param.value}{" "} + {param.unit || + ""} +
+
+ ) + )} +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/supplies/supplies-dashboard.tsx b/src/components/supplies/supplies-dashboard.tsx index 463c8cf..897f3c1 100644 --- a/src/components/supplies/supplies-dashboard.tsx +++ b/src/components/supplies/supplies-dashboard.tsx @@ -1,726 +1,68 @@ -"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 { Sidebar } from '@/components/dashboard/sidebar' -import { useSidebar } from '@/hooks/useSidebar' - -import { - ChevronDown, - ChevronRight, - Plus, - Calendar, - Package, - MapPin, - Building2, - TrendingUp, - AlertTriangle, - DollarSign -} from 'lucide-react' - - -// Типы данных для 5-уровневой структуры - - - -interface ProductParameter { - id: string - name: string - value: string - unit?: string -} - -interface Product { - id: string - name: string - sku: string - category: string - plannedQty: number - actualQty: number - defectQty: number - productPrice: number - parameters: ProductParameter[] -} - -interface Wholesaler { - id: string - name: string - inn: string - contact: string - address: string - products: Product[] - totalAmount: number -} - -interface Route { - id: string - from: string - fromAddress: string - to: string - toAddress: string - wholesalers: Wholesaler[] - totalProductPrice: number - fulfillmentServicePrice: number - logisticsPrice: number - totalAmount: number -} - -interface Supply { - id: string - number: number - deliveryDate: string - createdDate: string - routes: Route[] - plannedTotal: number - actualTotal: number - defectTotal: number - totalProductPrice: number - totalFulfillmentPrice: number - totalLogisticsPrice: number - grandTotal: number - status: 'planned' | 'in-transit' | 'delivered' | 'completed' -} - -// Моковые данные для 5-уровневой структуры -const mockSupplies: Supply[] = [ - { - id: '1', - number: 1, - deliveryDate: '2024-01-15', - createdDate: '2024-01-10', - status: 'delivered', - plannedTotal: 180, - actualTotal: 173, - defectTotal: 2, - totalProductPrice: 3750000, - totalFulfillmentPrice: 43000, - totalLogisticsPrice: 27000, - grandTotal: 3820000, - routes: [ - { - id: 'r1', - from: 'Садовод', - fromAddress: 'Москва, 14-й км МКАД', - to: 'SFERAV Logistics', - toAddress: 'Москва, ул. Складская, 15', - totalProductPrice: 3600000, - fulfillmentServicePrice: 25000, - logisticsPrice: 15000, - totalAmount: 3640000, - wholesalers: [ - { - id: 'w1', - name: 'ООО "ТехноСнаб"', - inn: '7701234567', - contact: '+7 (495) 123-45-67', - address: 'Москва, ул. Торговая, 1', - totalAmount: 3600000, - products: [ - { - id: 'p1', - name: 'Смартфон iPhone 15', - sku: 'APL-IP15-128', - category: 'Электроника', - plannedQty: 50, - actualQty: 48, - defectQty: 2, - productPrice: 75000, - parameters: [ - { id: 'param1', name: 'Цвет', value: 'Черный' }, - { id: 'param2', name: 'Память', value: '128', unit: 'ГБ' }, - { id: 'param3', name: 'Гарантия', value: '12', unit: 'мес' } - ] - } - ] - } - ] - }, - { - id: 'r2', - from: 'ТЯК Москва', - fromAddress: 'Москва, Алтуфьевское шоссе, 27', - to: 'MegaFulfillment', - toAddress: 'Подольск, ул. Индустриальная, 42', - totalProductPrice: 150000, - fulfillmentServicePrice: 18000, - logisticsPrice: 12000, - totalAmount: 180000, - wholesalers: [ - { - id: 'w2', - name: 'ИП Петров А.В.', - inn: '123456789012', - contact: '+7 (499) 987-65-43', - address: 'Москва, пр-т Мира, 45', - totalAmount: 150000, - products: [ - { - id: 'p2', - name: 'Чехол для iPhone 15', - sku: 'ACC-IP15-CASE', - category: 'Аксессуары', - plannedQty: 100, - actualQty: 95, - defectQty: 0, - productPrice: 1500, - parameters: [ - { id: 'param4', name: 'Материал', value: 'Силикон' }, - { id: 'param5', name: 'Цвет', value: 'Прозрачный' } - ] - } - ] - } - ] - } - ] - }, - { - id: '2', - number: 2, - deliveryDate: '2024-01-20', - createdDate: '2024-01-12', - status: 'in-transit', - plannedTotal: 30, - actualTotal: 30, - defectTotal: 0, - totalProductPrice: 750000, - totalFulfillmentPrice: 18000, - totalLogisticsPrice: 12000, - grandTotal: 780000, - routes: [ - { - id: 'r3', - from: 'Садовод', - fromAddress: 'Москва, 14-й км МКАД', - to: 'WB Подольск', - toAddress: 'Подольск, ул. Складская, 25', - totalProductPrice: 750000, - fulfillmentServicePrice: 18000, - logisticsPrice: 12000, - totalAmount: 780000, - wholesalers: [ - { - id: 'w3', - name: 'ООО "АудиоТех"', - inn: '7702345678', - contact: '+7 (495) 555-12-34', - address: 'Москва, ул. Звуковая, 8', - totalAmount: 750000, - products: [ - { - id: 'p3', - name: 'Наушники AirPods Pro', - sku: 'APL-AP-PRO2', - category: 'Аудио', - plannedQty: 30, - actualQty: 30, - defectQty: 0, - productPrice: 25000, - parameters: [ - { id: 'param6', name: 'Тип', value: 'Беспроводные' }, - { id: 'param7', name: 'Шумоподавление', value: 'Активное' }, - { id: 'param8', name: 'Время работы', value: '6', unit: 'ч' } - ] - } - ] - } - ] - } - ] - } -] +import React, { useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { Plus } from "lucide-react"; +import { SuppliesGoodsTab } from "./goods-supplies/goods-supplies-tab"; +import { SuppliesConsumablesTab } from "./consumables-supplies/consumables-supplies-tab"; export function SuppliesDashboard() { - const { getSidebarMargin } = useSidebar() - const [expandedSupplies, setExpandedSupplies] = useState>(new Set()) - const [expandedRoutes, setExpandedRoutes] = useState>(new Set()) - const [expandedWholesalers, setExpandedWholesalers] = useState>(new Set()) - const [expandedProducts, setExpandedProducts] = useState>(new Set()) - - - const toggleSupplyExpansion = (supplyId: string) => { - const newExpanded = new Set(expandedSupplies) - if (newExpanded.has(supplyId)) { - newExpanded.delete(supplyId) - } else { - newExpanded.add(supplyId) - } - setExpandedSupplies(newExpanded) - } - - const toggleRouteExpansion = (routeId: string) => { - const newExpanded = new Set(expandedRoutes) - if (newExpanded.has(routeId)) { - newExpanded.delete(routeId) - } else { - newExpanded.add(routeId) - } - setExpandedRoutes(newExpanded) - } - - const toggleWholesalerExpansion = (wholesalerId: string) => { - const newExpanded = new Set(expandedWholesalers) - if (newExpanded.has(wholesalerId)) { - newExpanded.delete(wholesalerId) - } else { - newExpanded.add(wholesalerId) - } - setExpandedWholesalers(newExpanded) - } - - const toggleProductExpansion = (productId: string) => { - const newExpanded = new Set(expandedProducts) - if (newExpanded.has(productId)) { - newExpanded.delete(productId) - } else { - newExpanded.add(productId) - } - setExpandedProducts(newExpanded) - } - - const getStatusBadge = (status: Supply['status']) => { - const statusMap = { - planned: { label: 'Запланирована', color: 'bg-blue-500/20 text-blue-300 border-blue-500/30' }, - 'in-transit': { label: 'В пути', color: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' }, - delivered: { label: 'Доставлена', color: 'bg-green-500/20 text-green-300 border-green-500/30' }, - completed: { label: 'Завершена', color: 'bg-purple-500/20 text-purple-300 border-purple-500/30' } - } - const { label, color } = statusMap[status] - return {label} - } - - 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 calculateProductTotal = (product: Product) => { - return product.actualQty * product.productPrice - } - - - - - - - - const getEfficiencyBadge = (planned: number, actual: number, defect: number) => { - const efficiency = ((actual - defect) / planned) * 100 - if (efficiency >= 95) { - return Отлично - } else if (efficiency >= 90) { - return Хорошо - } else { - return Проблемы - } - } + const { getSidebarMargin } = useSidebar(); + const [activeTab, setActiveTab] = useState("goods"); return (
-
-
- {/* Заголовок */} -
-
-

Поставки

-

Управление поставками товаров

+
+
+ {/* Вкладки с кнопкой создания */} + +
+ + + Товар + + + Расходники + + + +
- -
- {/* Статистика */} -
- -
-
- -
-
-

Всего поставок

-

{mockSupplies.length}

-
-
-
- - -
-
- -
-
-

Общая сумма

-

- {formatCurrency(mockSupplies.reduce((sum, supply) => sum + supply.grandTotal, 0))} -

-
-
-
+ + + - -
-
- -
-
-

В пути

-

- {mockSupplies.filter(supply => supply.status === 'in-transit').length} -

-
-
-
- - -
-
- -
-
-

С браком

-

- {mockSupplies.filter(supply => supply.defectTotal > 0).length} -

-
-
-
-
- - {/* Многоуровневая таблица поставок */} - -
- - - - - - - - - - - - - - - - - - {mockSupplies.map((supply) => { - const isSupplyExpanded = expandedSupplies.has(supply.id) - - return ( - - {/* Уровень 1: Основная строка поставки */} - - - - - - - - - - - - - - - {/* Уровень 2: Маршруты */} - {isSupplyExpanded && supply.routes.map((route) => { - const isRouteExpanded = expandedRoutes.has(route.id) - return ( - - - - - - - - - - - - - - - {/* Уровень 3: Оптовики */} - {isRouteExpanded && route.wholesalers.map((wholesaler) => { - const isWholesalerExpanded = expandedWholesalers.has(wholesaler.id) - return ( - - - - - - - - - - - - - - {/* Уровень 4: Товары */} - {isWholesalerExpanded && wholesaler.products.map((product) => { - const isProductExpanded = expandedProducts.has(product.id) - return ( - - - - - - - - - - - - - - {/* Уровень 5: Параметры товара */} - {isProductExpanded && ( - - - - )} - - ) - })} - - ) - })} - - ) - })} - - ) - })} - -
Дата поставкиДата созданияПланФактБракЦена товаровУслуги ФФЛогистика до ФФИтого суммаСтатус
-
- - #{supply.number} -
-
-
- - {formatDate(supply.deliveryDate)} -
-
- {formatDate(supply.createdDate)} - - {supply.plannedTotal} - - {supply.actualTotal} - - 0 ? 'text-red-400' : 'text-white'}`}> - {supply.defectTotal} - - - {formatCurrency(supply.totalProductPrice)} - - {formatCurrency(supply.totalFulfillmentPrice)} - - {formatCurrency(supply.totalLogisticsPrice)} - -
- - {formatCurrency(supply.grandTotal)} -
-
- {getStatusBadge(supply.status)} -
-
- - - Маршрут -
-
-
-
- {route.from} - - {route.to} -
-
{route.fromAddress} → {route.toAddress}
-
-
- - {route.wholesalers.reduce((sum, w) => - sum + w.products.reduce((pSum, p) => pSum + p.plannedQty, 0), 0 - )} - - - - {route.wholesalers.reduce((sum, w) => - sum + w.products.reduce((pSum, p) => pSum + p.actualQty, 0), 0 - )} - - - - {route.wholesalers.reduce((sum, w) => - sum + w.products.reduce((pSum, p) => pSum + p.defectQty, 0), 0 - )} - - - {formatCurrency(route.totalProductPrice)} - - {formatCurrency(route.fulfillmentServicePrice)} - - {formatCurrency(route.logisticsPrice)} - - {formatCurrency(route.totalAmount)} -
-
- - - Оптовик -
-
-
-
{wholesaler.name}
-
ИНН: {wholesaler.inn}
-
{wholesaler.address}
-
{wholesaler.contact}
-
-
- - {wholesaler.products.reduce((sum, p) => sum + p.plannedQty, 0)} - - - - {wholesaler.products.reduce((sum, p) => sum + p.actualQty, 0)} - - - - {wholesaler.products.reduce((sum, p) => sum + p.defectQty, 0)} - - - - {formatCurrency(wholesaler.products.reduce((sum, p) => sum + calculateProductTotal(p), 0))} - - - {formatCurrency(wholesaler.totalAmount)} -
-
- - - Товар -
-
-
-
{product.name}
-
Артикул: {product.sku}
- - {product.category} - -
-
- {product.plannedQty} - - {product.actualQty} - - 0 ? 'text-red-400' : 'text-white'}`}> - {product.defectQty} - - -
-
{formatCurrency(calculateProductTotal(product))}
-
{formatCurrency(product.productPrice)} за шт.
-
-
- {getEfficiencyBadge(product.plannedQty, product.actualQty, product.defectQty)} - - {formatCurrency(calculateProductTotal(product))} -
-
-
-

- 📋 Параметры товара: -

-
- {product.parameters.map((param) => ( -
-
{param.name}
-
- {param.value} {param.unit || ''} -
-
- ))} -
-
-
-
-
-
+ + + +
- ) -} \ No newline at end of file + ); +}