From 248548a4b4303821a5b2776f132a00535e983002 Mon Sep 17 00:00:00 2001 From: Veronika Smirnova Date: Mon, 21 Jul 2025 15:24:12 +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=B7=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=8B=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BD=D0=B0=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=BC=D0=B0=D1=80?= =?UTF-8?q?=D0=BA=D0=B5=D1=82=D0=BF=D0=BB=D0=B5=D0=B9=D1=81=D0=BE=D0=B2.?= =?UTF-8?q?=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9=20=D1=82=D0=B0=D0=B1=20?= =?UTF-8?q?=D0=BD=D0=B0=20'fulfillment',=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=BD=D0=B8=D0=BC=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F.=20=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=BE=D0=B4=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=B9=20=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fulfillment-goods-tab.tsx | 852 ++++++++++++++++++ .../fulfillment-supplies-sub-tab.tsx | 447 +++++++++ .../fulfillment-supplies-tab.tsx | 55 ++ .../fulfillment-supplies/pvz-returns-tab.tsx | 741 +++++++++++++++ .../marketplace-supplies-tab.tsx | 44 + .../ozon-supplies-tab.tsx | 779 ++++++++++++++++ .../wildberries-supplies-tab.tsx | 779 ++++++++++++++++ .../supplies/supplies-dashboard.tsx | 24 +- src/components/supplies/ui/stats-card.tsx | 83 ++ src/components/supplies/ui/stats-grid.tsx | 28 + 10 files changed, 3820 insertions(+), 12 deletions(-) create mode 100644 src/components/supplies/fulfillment-supplies/fulfillment-goods-tab.tsx create mode 100644 src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx create mode 100644 src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx create mode 100644 src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx create mode 100644 src/components/supplies/marketplace-supplies/marketplace-supplies-tab.tsx create mode 100644 src/components/supplies/marketplace-supplies/ozon-supplies-tab.tsx create mode 100644 src/components/supplies/marketplace-supplies/wildberries-supplies-tab.tsx create mode 100644 src/components/supplies/ui/stats-card.tsx create mode 100644 src/components/supplies/ui/stats-grid.tsx diff --git a/src/components/supplies/fulfillment-supplies/fulfillment-goods-tab.tsx b/src/components/supplies/fulfillment-supplies/fulfillment-goods-tab.tsx new file mode 100644 index 0000000..f6bd60e --- /dev/null +++ b/src/components/supplies/fulfillment-supplies/fulfillment-goods-tab.tsx @@ -0,0 +1,852 @@ +"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 { StatsCard } from "../ui/stats-card"; +import { StatsGrid } from "../ui/stats-grid"; +import { + ChevronDown, + ChevronRight, + Calendar, + Package, + MapPin, + Building2, + TrendingUp, + AlertTriangle, + DollarSign, + Warehouse, +} 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 FulfillmentSupply { + 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 mockFulfillmentGoods: FulfillmentSupply[] = [ + { + id: "ff1", + number: 1001, + 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: "ffr1", + from: "Садовод", + fromAddress: "Москва, 14-й км МКАД", + to: "SFERAV Logistics ФФ", + toAddress: "Москва, ул. Складская, 15", + totalProductPrice: 3600000, + fulfillmentServicePrice: 25000, + logisticsPrice: 15000, + totalAmount: 3640000, + wholesalers: [ + { + id: "ffw1", + name: 'ООО "ТехноСнаб"', + inn: "7701234567", + contact: "+7 (495) 123-45-67", + address: "Москва, ул. Торговая, 1", + totalAmount: 3600000, + products: [ + { + id: "ffp1", + name: "Смартфон iPhone 15 Pro", + sku: "APL-IP15P-256", + category: "Электроника", + plannedQty: 50, + actualQty: 48, + defectQty: 2, + productPrice: 75000, + parameters: [ + { id: "param1", name: "Цвет", value: "Титановый" }, + { id: "param2", name: "Память", value: "256", unit: "ГБ" }, + { id: "param3", name: "Гарантия", value: "12", unit: "мес" }, + ], + }, + ], + }, + ], + }, + ], + }, + { + id: "ff2", + number: 1002, + 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: "ffr2", + from: "Садовод", + fromAddress: "Москва, 14-й км МКАД", + to: "MegaFulfillment", + toAddress: "Подольск, ул. Складская, 25", + totalProductPrice: 750000, + fulfillmentServicePrice: 18000, + logisticsPrice: 12000, + totalAmount: 780000, + wholesalers: [ + { + id: "ffw2", + name: 'ООО "АудиоТех"', + inn: "7702345678", + contact: "+7 (495) 555-12-34", + address: "Москва, ул. Звуковая, 8", + totalAmount: 750000, + products: [ + { + id: "ffp2", + name: "Наушники AirPods Pro 2", + sku: "APL-AP-PRO2-USB", + category: "Аудио", + plannedQty: 30, + actualQty: 30, + defectQty: 0, + productPrice: 25000, + parameters: [ + { id: "param4", name: "Тип", value: "Беспроводные" }, + { id: "param5", name: "Шумоподавление", value: "Активное" }, + { id: "param6", name: "Время работы", value: "6", unit: "ч" }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export function FulfillmentGoodsTab() { + 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: FulfillmentSupply["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 ( +
+ {/* Статистика товаров ФФ */} + + + + sum + supply.grandTotal, + 0 + ) + )} + icon={TrendingUp} + iconColor="text-green-400" + iconBg="bg-green-500/20" + trend={{ value: 8, isPositive: true }} + subtitle="Общая стоимость" + /> + + supply.status === "in-transit" + ).length + } + icon={Calendar} + iconColor="text-yellow-400" + iconBg="bg-yellow-500/20" + subtitle="Активные поставки" + /> + + supply.defectTotal > 0) + .length + } + icon={AlertTriangle} + iconColor="text-red-400" + iconBg="bg-red-500/20" + trend={{ value: 3, isPositive: false }} + subtitle="Требуют внимания" + /> + + + {/* Таблица поставок товаров ФФ */} + +
+ + + + + + + + + + + + + + + + + + {mockFulfillmentGoods.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Основная строка поставки */} + + + + + + + + + + + + + + + {/* Развернутые уровни - аналогично оригинальному коду */} + {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/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx new file mode 100644 index 0000000..2c14511 --- /dev/null +++ b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-sub-tab.tsx @@ -0,0 +1,447 @@ +"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 { StatsCard } from "../ui/stats-card"; +import { StatsGrid } from "../ui/stats-grid"; +import { + ChevronDown, + ChevronRight, + Calendar, + MapPin, + Building2, + TrendingUp, + AlertTriangle, + DollarSign, + Wrench, + Box, + Package2, + Tags, +} 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 FulfillmentConsumableSupply { + 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 mockFulfillmentConsumables: FulfillmentConsumableSupply[] = [ + { + id: "ffc1", + number: 2001, + deliveryDate: "2024-01-18", + createdDate: "2024-01-14", + status: "delivered", + plannedTotal: 5000, + actualTotal: 4950, + defectTotal: 50, + totalConsumablesPrice: 125000, + totalLogisticsPrice: 8000, + grandTotal: 133000, + routes: [ + { + id: "ffcr1", + from: "Склад расходников", + fromAddress: "Москва, ул. Промышленная, 12", + to: "SFERAV Logistics ФФ", + toAddress: "Москва, ул. Складская, 15", + totalConsumablesPrice: 125000, + logisticsPrice: 8000, + totalAmount: 133000, + suppliers: [ + { + id: "ffcs1", + name: 'ООО "УпакСервис ФФ"', + inn: "7703456789", + contact: "+7 (495) 777-88-99", + address: "Москва, ул. Упаковочная, 5", + totalAmount: 75000, + consumables: [ + { + id: "ffcons1", + name: "Коробки для ФФ 40x30x15", + sku: "BOX-FF-403015", + category: "Упаковка ФФ", + type: "packaging", + plannedQty: 2000, + actualQty: 1980, + defectQty: 20, + unitPrice: 45, + parameters: [ + { + id: "ffcp1", + name: "Размер", + value: "40x30x15", + unit: "см", + }, + { + id: "ffcp2", + name: "Материал", + value: "Гофрокартон усиленный", + }, + { id: "ffcp3", name: "Плотность", value: "5", unit: "слоев" }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export function FulfillmentSuppliesTab() { + 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: FulfillmentConsumableSupply["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; + }; + + return ( +
+ {/* Статистика расходников ФФ */} + + + + sum + supply.grandTotal, + 0 + ) + )} + icon={TrendingUp} + iconColor="text-green-400" + iconBg="bg-green-500/20" + trend={{ value: 15, isPositive: true }} + subtitle="Общая стоимость" + /> + + supply.status === "in-transit" + ).length + } + icon={Calendar} + iconColor="text-yellow-400" + iconBg="bg-yellow-500/20" + subtitle="Активные поставки" + /> + + supply.defectTotal > 0 + ).length + } + icon={AlertTriangle} + iconColor="text-red-400" + iconBg="bg-red-500/20" + trend={{ value: 2, isPositive: false }} + subtitle="Дефектные материалы" + /> + + + {/* Таблица поставок расходников ФФ */} + +
+ + + + + + + + + + + + + + + + + {mockFulfillmentConsumables.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Основная строка поставки расходников ФФ */} + + + + + + + + + + + + + + ); + })} + +
+ Дата поставки + + Дата создания + ПланФактБрак + Цена расходников + + Логистика + + Итого сумма + + Статус +
+
+ + + #{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)}
+
+
+
+ ); +} diff --git a/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx new file mode 100644 index 0000000..354d4fe --- /dev/null +++ b/src/components/supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React, { useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { FulfillmentGoodsTab } from "./fulfillment-goods-tab"; +import { FulfillmentSuppliesTab as FulfillmentSuppliesSubTab } from "./fulfillment-supplies-sub-tab"; +import { PvzReturnsTab } from "./pvz-returns-tab"; + +export function FulfillmentSuppliesTab() { + const [activeSubTab, setActiveSubTab] = useState("goods"); + + return ( +
+ + {/* Подвкладки для ФФ */} + + + Товар + + + Расходники + + + Возвраты с ПВЗ + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx b/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx new file mode 100644 index 0000000..fcd0508 --- /dev/null +++ b/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx @@ -0,0 +1,741 @@ +"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 { StatsCard } from "../ui/stats-card"; +import { StatsGrid } from "../ui/stats-grid"; +import { + ChevronDown, + ChevronRight, + Calendar, + Package, + MapPin, + Building2, + TrendingDown, + AlertTriangle, + DollarSign, + RotateCcw, + RefreshCcw, +} from "lucide-react"; + +// Типы данных для возвратов с ПВЗ +interface ReturnProduct { + id: string; + name: string; + sku: string; + category: string; + returnQty: number; + defectQty: number; + returnPrice: number; + returnReason: + | "customer_return" + | "defect" + | "damage" + | "wrong_item" + | "other"; +} + +interface PvzPoint { + id: string; + name: string; + address: string; + contact: string; + products: ReturnProduct[]; + totalAmount: number; +} + +interface ReturnRoute { + id: string; + from: string; + fromAddress: string; + to: string; + toAddress: string; + pvzPoints: PvzPoint[]; + totalReturnPrice: number; + logisticsPrice: number; + totalAmount: number; +} + +interface PvzReturnSupply { + id: string; + number: number; + returnDate: string; + createdDate: string; + routes: ReturnRoute[]; + totalReturnQty: number; + totalDefectQty: number; + totalReturnPrice: number; + totalLogisticsPrice: number; + grandTotal: number; + status: "planned" | "in-transit" | "delivered" | "completed"; +} + +// Моковые данные для возвратов с ПВЗ +const mockPvzReturns: PvzReturnSupply[] = [ + { + id: "pvz1", + number: 3001, + returnDate: "2024-01-20", + createdDate: "2024-01-15", + status: "delivered", + totalReturnQty: 45, + totalDefectQty: 12, + totalReturnPrice: 890000, + totalLogisticsPrice: 15000, + grandTotal: 905000, + routes: [ + { + id: "pvzr1", + from: "ПВЗ Сеть", + fromAddress: "Москва, различные точки", + to: "SFERAV Logistics ФФ", + toAddress: "Москва, ул. Складская, 15", + totalReturnPrice: 890000, + logisticsPrice: 15000, + totalAmount: 905000, + pvzPoints: [ + { + id: "pvzp1", + name: 'ПВЗ "На Тверской"', + address: "Москва, ул. Тверская, 15", + contact: "+7 (495) 123-45-67", + totalAmount: 450000, + products: [ + { + id: "pvzprod1", + name: "Смартфон iPhone 15 (возврат)", + sku: "APL-IP15-128-RET", + category: "Электроника", + returnQty: 15, + defectQty: 3, + returnPrice: 70000, + returnReason: "customer_return", + }, + { + id: "pvzprod2", + name: "Наушники AirPods (брак)", + sku: "APL-AP-PRO2-DEF", + category: "Аудио", + returnQty: 8, + defectQty: 8, + returnPrice: 22000, + returnReason: "defect", + }, + ], + }, + { + id: "pvzp2", + name: 'ПВЗ "Арбатский"', + address: "Москва, ул. Арбат, 25", + contact: "+7 (495) 987-65-43", + totalAmount: 440000, + products: [ + { + id: "pvzprod3", + name: "Планшет iPad Air (повреждение)", + sku: "APL-IPAD-AIR-DMG", + category: "Планшеты", + returnQty: 12, + defectQty: 1, + returnPrice: 35000, + returnReason: "damage", + }, + ], + }, + ], + }, + ], + }, + { + id: "pvz2", + number: 3002, + returnDate: "2024-01-25", + createdDate: "2024-01-18", + status: "in-transit", + totalReturnQty: 28, + totalDefectQty: 5, + totalReturnPrice: 560000, + totalLogisticsPrice: 12000, + grandTotal: 572000, + routes: [ + { + id: "pvzr2", + from: "ПВЗ Сеть Подольск", + fromAddress: "Подольск, различные точки", + to: "MegaFulfillment", + toAddress: "Подольск, ул. Складская, 25", + totalReturnPrice: 560000, + logisticsPrice: 12000, + totalAmount: 572000, + pvzPoints: [ + { + id: "pvzp3", + name: 'ПВЗ "Центральный"', + address: "Подольск, ул. Центральная, 10", + contact: "+7 (4967) 55-66-77", + totalAmount: 560000, + products: [ + { + id: "pvzprod4", + name: "Ноутбук MacBook (возврат)", + sku: "APL-MBP-14-RET", + category: "Компьютеры", + returnQty: 8, + defectQty: 2, + returnPrice: 180000, + returnReason: "customer_return", + }, + ], + }, + ], + }, + ], + }, +]; + +export function PvzReturnsTab() { + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); + const [expandedPvzPoints, setExpandedPvzPoints] = 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 togglePvzPointExpansion = (pvzPointId: string) => { + const newExpanded = new Set(expandedPvzPoints); + if (newExpanded.has(pvzPointId)) { + newExpanded.delete(pvzPointId); + } else { + newExpanded.add(pvzPointId); + } + setExpandedPvzPoints(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: PvzReturnSupply["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 getReturnReasonBadge = (reason: ReturnProduct["returnReason"]) => { + const reasonMap = { + customer_return: { + label: "Возврат клиента", + color: "bg-blue-500/20 text-blue-300 border-blue-500/30", + }, + defect: { + label: "Брак", + color: "bg-red-500/20 text-red-300 border-red-500/30", + }, + damage: { + label: "Повреждение", + color: "bg-orange-500/20 text-orange-300 border-orange-500/30", + }, + wrong_item: { + label: "Неправильный товар", + color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", + }, + other: { + label: "Прочее", + color: "bg-gray-500/20 text-gray-300 border-gray-500/30", + }, + }; + const { label, color } = reasonMap[reason]; + 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: ReturnProduct) => { + return product.returnQty * product.returnPrice; + }; + + return ( +
+ {/* Статистика возвратов с ПВЗ */} + + + + sum + supply.grandTotal, 0) + )} + icon={TrendingDown} + iconColor="text-orange-400" + iconBg="bg-orange-500/20" + trend={{ value: 4, isPositive: false }} + subtitle="Общая стоимость" + /> + + supply.status === "in-transit") + .length + } + icon={Calendar} + iconColor="text-yellow-400" + iconBg="bg-yellow-500/20" + subtitle="Активные возвраты" + /> + + supply.totalDefectQty > 0).length + } + icon={AlertTriangle} + iconColor="text-red-400" + iconBg="bg-red-500/20" + trend={{ value: 15, isPositive: false }} + subtitle="Бракованные товары" + /> + + + {/* Таблица возвратов с ПВЗ */} + +
+ + + + + + + + + + + + + + + + {mockPvzReturns.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Основная строка возврата */} + + + + + + + + + + + + + {/* Развернутые уровни - аналогично другим компонентам */} + {isSupplyExpanded && + supply.routes.map((route) => { + const isRouteExpanded = expandedRoutes.has(route.id); + return ( + + + + + + + + + + + + + {/* ПВЗ точки */} + {isRouteExpanded && + route.pvzPoints.map((pvzPoint) => { + const isPvzPointExpanded = + expandedPvzPoints.has(pvzPoint.id); + return ( + + + + + + + + + + + + + {/* Возвращенные товары */} + {isPvzPointExpanded && + pvzPoint.products.map((product) => ( + + + + + + + + + + + ))} + + ); + })} + + ); + })} + + ); + })} + +
+ Дата возврата + + Дата создания + + Возвратов + + Дефектов + + Сумма возвратов + + Логистика + + Итого сумма + + Статус +
+
+ + + #{supply.number} + +
+
+
+ + + {formatDate(supply.returnDate)} + +
+
+ + {formatDate(supply.createdDate)} + + + + {supply.totalReturnQty} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {supply.totalDefectQty} + + + + {formatCurrency(supply.totalReturnPrice)} + + + + {formatCurrency(supply.totalLogisticsPrice)} + + +
+ + + {formatCurrency(supply.grandTotal)} + +
+
{getStatusBadge(supply.status)}
+
+ + + + Маршрут возврата + +
+
+
+
+ + {route.from} + + + + {route.to} + +
+
+ {route.fromAddress} → {route.toAddress} +
+
+
+ + {route.pvzPoints.reduce( + (sum, p) => + sum + + p.products.reduce( + (pSum, prod) => pSum + prod.returnQty, + 0 + ), + 0 + )} + + + + {route.pvzPoints.reduce( + (sum, p) => + sum + + p.products.reduce( + (pSum, prod) => pSum + prod.defectQty, + 0 + ), + 0 + )} + + + + {formatCurrency(route.totalReturnPrice)} + + + + {formatCurrency(route.logisticsPrice)} + + + + {formatCurrency(route.totalAmount)} + +
+
+ + + + ПВЗ + +
+
+
+
+ {pvzPoint.name} +
+
+ {pvzPoint.address} +
+
+ {pvzPoint.contact} +
+
+
+ + {pvzPoint.products.reduce( + (sum, p) => sum + p.returnQty, + 0 + )} + + + + {pvzPoint.products.reduce( + (sum, p) => sum + p.defectQty, + 0 + )} + + + + {formatCurrency( + pvzPoint.products.reduce( + (sum, p) => + sum + calculateProductTotal(p), + 0 + ) + )} + + + + {formatCurrency(pvzPoint.totalAmount)} + +
+
+ + + Возврат + +
+
+
+
+ {product.name} +
+
+ Артикул: {product.sku} +
+
+ + {product.category} + + {getReturnReasonBadge( + product.returnReason + )} +
+
+
+ + {product.returnQty} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {product.defectQty} + + +
+
+ {formatCurrency( + calculateProductTotal(product) + )} +
+
+ {formatCurrency( + product.returnPrice + )}{" "} + за шт. +
+
+
+ + {formatCurrency( + calculateProductTotal(product) + )} + +
+
+
+
+ ); +} diff --git a/src/components/supplies/marketplace-supplies/marketplace-supplies-tab.tsx b/src/components/supplies/marketplace-supplies/marketplace-supplies-tab.tsx new file mode 100644 index 0000000..e1cb132 --- /dev/null +++ b/src/components/supplies/marketplace-supplies/marketplace-supplies-tab.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React, { useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { WildberriesSuppliesTab } from "./wildberries-supplies-tab"; +import { OzonSuppliesTab } from "./ozon-supplies-tab"; + +export function MarketplaceSuppliesTab() { + const [activeSubTab, setActiveSubTab] = useState("wildberries"); + + return ( +
+ + {/* Подвкладки для Маркетплейсов */} + + + Поставки на Wildberries + + + Поставки на Ozon + + + + + + + + + + + +
+ ); +} diff --git a/src/components/supplies/marketplace-supplies/ozon-supplies-tab.tsx b/src/components/supplies/marketplace-supplies/ozon-supplies-tab.tsx new file mode 100644 index 0000000..51e393c --- /dev/null +++ b/src/components/supplies/marketplace-supplies/ozon-supplies-tab.tsx @@ -0,0 +1,779 @@ +"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 { StatsCard } from "../ui/stats-card"; +import { StatsGrid } from "../ui/stats-grid"; +import { + ChevronDown, + ChevronRight, + Calendar, + Package, + MapPin, + TrendingUp, + AlertTriangle, + DollarSign, + Truck, + Store, +} from "lucide-react"; + +// Типы данных для поставок на Ozon +interface OzonProduct { + id: string; + name: string; + sku: string; + offerId: string; + category: string; + plannedQty: number; + actualQty: number; + defectQty: number; + productPrice: number; +} + +interface OzonWarehouse { + id: string; + name: string; + address: string; + warehouseId: number; + products: OzonProduct[]; + totalAmount: number; +} + +interface OzonRoute { + id: string; + from: string; + fromAddress: string; + to: string; + toAddress: string; + warehouses: OzonWarehouse[]; + totalProductPrice: number; + logisticsPrice: number; + totalAmount: number; +} + +interface OzonSupply { + id: string; + number: number; + supplyId: string; + deliveryDate: string; + createdDate: string; + routes: OzonRoute[]; + plannedTotal: number; + actualTotal: number; + defectTotal: number; + totalProductPrice: number; + totalLogisticsPrice: number; + grandTotal: number; + status: "planned" | "in-transit" | "delivered" | "completed"; +} + +// Моковые данные для поставок на Ozon +const mockOzonSupplies: OzonSupply[] = [ + { + id: "oz1", + number: 5001, + supplyId: "OZ24010001", + deliveryDate: "2024-01-25", + createdDate: "2024-01-18", + status: "delivered", + plannedTotal: 90, + actualTotal: 87, + defectTotal: 3, + totalProductPrice: 1950000, + totalLogisticsPrice: 22000, + grandTotal: 1972000, + routes: [ + { + id: "ozr1", + from: "ТЯК Москва", + fromAddress: "Москва, Алтуфьевское шоссе, 27", + to: "Ozon Тверь", + toAddress: "Тверь, ул. Складская, 45", + totalProductPrice: 1950000, + logisticsPrice: 22000, + totalAmount: 1972000, + warehouses: [ + { + id: "ozw1", + name: "Склад Ozon Тверь", + address: "Тверь, ул. Складская, 45", + warehouseId: 22341172, + totalAmount: 1950000, + products: [ + { + id: "ozp1", + name: "Ноутбук ASUS VivoBook", + sku: "ASUS-VB-15-512", + offerId: "ASUS-001", + category: "Ноутбуки", + plannedQty: 15, + actualQty: 14, + defectQty: 1, + productPrice: 85000, + }, + { + id: "ozp2", + name: "Мышь беспроводная Logitech", + sku: "LOG-MX3-BLK", + offerId: "LOG-002", + category: "Компьютерные аксессуары", + plannedQty: 75, + actualQty: 73, + defectQty: 2, + productPrice: 4500, + }, + ], + }, + ], + }, + ], + }, + { + id: "oz2", + number: 5002, + supplyId: "OZ24010002", + deliveryDate: "2024-01-30", + createdDate: "2024-01-22", + status: "in-transit", + plannedTotal: 45, + actualTotal: 45, + defectTotal: 0, + totalProductPrice: 1125000, + totalLogisticsPrice: 18000, + grandTotal: 1143000, + routes: [ + { + id: "ozr2", + from: "Садовод", + fromAddress: "Москва, 14-й км МКАД", + to: "Ozon Рязань", + toAddress: "Рязань, ул. Промышленная, 15", + totalProductPrice: 1125000, + logisticsPrice: 18000, + totalAmount: 1143000, + warehouses: [ + { + id: "ozw2", + name: "Склад Ozon Рязань", + address: "Рязань, ул. Промышленная, 15", + warehouseId: 22341173, + totalAmount: 1125000, + products: [ + { + id: "ozp3", + name: "Планшет iPad Air", + sku: "APL-IPAD-AIR-64", + offerId: "APL-003", + category: "Планшеты", + plannedQty: 20, + actualQty: 20, + defectQty: 0, + productPrice: 45000, + }, + { + id: "ozp4", + name: "Клавиатура механическая", + sku: "KEYB-MECH-RGB", + offerId: "KEYB-004", + category: "Клавиатуры", + plannedQty: 25, + actualQty: 25, + defectQty: 0, + productPrice: 12000, + }, + ], + }, + ], + }, + ], + }, +]; + +export function OzonSuppliesTab() { + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); + const [expandedWarehouses, setExpandedWarehouses] = 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 toggleWarehouseExpansion = (warehouseId: string) => { + const newExpanded = new Set(expandedWarehouses); + if (newExpanded.has(warehouseId)) { + newExpanded.delete(warehouseId); + } else { + newExpanded.add(warehouseId); + } + setExpandedWarehouses(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: OzonSupply["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: OzonProduct) => { + 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 ( +
+ {/* Статистика поставок на Ozon */} + + + + sum + supply.grandTotal, 0) + )} + icon={TrendingUp} + iconColor="text-green-400" + iconBg="bg-green-500/20" + trend={{ value: 19, isPositive: true }} + subtitle="Общая стоимость" + /> + + supply.status === "in-transit") + .length + } + icon={Calendar} + iconColor="text-yellow-400" + iconBg="bg-yellow-500/20" + subtitle="Активные поставки" + /> + + supply.defectTotal > 0).length + } + icon={AlertTriangle} + iconColor="text-red-400" + iconBg="bg-red-500/20" + trend={{ value: 2, isPositive: false }} + subtitle="Требуют проверки" + /> + + + {/* Таблица поставок на Ozon */} + +
+ + + + + + + + + + + + + + + + + + {mockOzonSupplies.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Основная строка поставки на Ozon */} + + + + + + + + + + + + + + + {/* Развернутые уровни */} + {isSupplyExpanded && + supply.routes.map((route) => { + const isRouteExpanded = expandedRoutes.has(route.id); + return ( + + + + + + + + + + + + + + {/* Склады Ozon */} + {isRouteExpanded && + route.warehouses.map((warehouse) => { + const isWarehouseExpanded = + expandedWarehouses.has(warehouse.id); + return ( + + + + + + + + + + + + + + {/* Товары Ozon */} + {isWarehouseExpanded && + warehouse.products.map((product) => ( + + + + + + + + + + + + ))} + + ); + })} + + ); + })} + + ); + })} + +
+ ID поставки + + Дата поставки + + Дата создания + ПланФактБрак + Цена товаров + + Логистика + + Итого сумма + + Статус +
+
+ + + #{supply.number} + +
+
+ + {supply.supplyId} + + +
+ + + {formatDate(supply.deliveryDate)} + +
+
+ + {formatDate(supply.createdDate)} + + + + {supply.plannedTotal} + + + + {supply.actualTotal} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {supply.defectTotal} + + + + {formatCurrency(supply.totalProductPrice)} + + + + {formatCurrency(supply.totalLogisticsPrice)} + + +
+ + + {formatCurrency(supply.grandTotal)} + +
+
{getStatusBadge(supply.status)}
+
+ + + + Маршрут + +
+
+
+
+ + {route.from} + + + + {route.to} + +
+
+ {route.fromAddress} → {route.toAddress} +
+
+
+ + {route.warehouses.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.plannedQty, + 0 + ), + 0 + )} + + + + {route.warehouses.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.actualQty, + 0 + ), + 0 + )} + + + + {route.warehouses.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.defectQty, + 0 + ), + 0 + )} + + + + {formatCurrency(route.totalProductPrice)} + + + + {formatCurrency(route.logisticsPrice)} + + + + {formatCurrency(route.totalAmount)} + +
+
+ + + + Склад Ozon + +
+
+
+
+ {warehouse.name} +
+
+ ID: {warehouse.warehouseId} +
+
+ {warehouse.address} +
+
+
+ + {warehouse.products.reduce( + (sum, p) => sum + p.plannedQty, + 0 + )} + + + + {warehouse.products.reduce( + (sum, p) => sum + p.actualQty, + 0 + )} + + + + {warehouse.products.reduce( + (sum, p) => sum + p.defectQty, + 0 + )} + + + + {formatCurrency( + warehouse.products.reduce( + (sum, p) => + sum + calculateProductTotal(p), + 0 + ) + )} + + + + {formatCurrency( + warehouse.totalAmount + )} + +
+
+ + + Товар Ozon + +
+
+
+
+ {product.name} +
+
+ Артикул: {product.sku} +
+
+ Offer ID: {product.offerId} +
+ + {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) + )} + +
+
+
+
+ ); +} diff --git a/src/components/supplies/marketplace-supplies/wildberries-supplies-tab.tsx b/src/components/supplies/marketplace-supplies/wildberries-supplies-tab.tsx new file mode 100644 index 0000000..8c882fb --- /dev/null +++ b/src/components/supplies/marketplace-supplies/wildberries-supplies-tab.tsx @@ -0,0 +1,779 @@ +"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 { StatsCard } from "../ui/stats-card"; +import { StatsGrid } from "../ui/stats-grid"; +import { + ChevronDown, + ChevronRight, + Calendar, + Package, + MapPin, + TrendingUp, + AlertTriangle, + DollarSign, + Truck, + ShoppingBag, +} from "lucide-react"; + +// Типы данных для поставок на Wildberries +interface WbProduct { + id: string; + name: string; + sku: string; + nmId: number; + category: string; + plannedQty: number; + actualQty: number; + defectQty: number; + productPrice: number; +} + +interface WbWarehouse { + id: string; + name: string; + address: string; + warehouseId: number; + products: WbProduct[]; + totalAmount: number; +} + +interface WbRoute { + id: string; + from: string; + fromAddress: string; + to: string; + toAddress: string; + warehouses: WbWarehouse[]; + totalProductPrice: number; + logisticsPrice: number; + totalAmount: number; +} + +interface WbSupply { + id: string; + number: number; + supplyId: string; + deliveryDate: string; + createdDate: string; + routes: WbRoute[]; + plannedTotal: number; + actualTotal: number; + defectTotal: number; + totalProductPrice: number; + totalLogisticsPrice: number; + grandTotal: number; + status: "planned" | "in-transit" | "delivered" | "completed"; +} + +// Моковые данные для поставок на Wildberries +const mockWbSupplies: WbSupply[] = [ + { + id: "wb1", + number: 4001, + supplyId: "WB24010001", + deliveryDate: "2024-01-22", + createdDate: "2024-01-16", + status: "delivered", + plannedTotal: 120, + actualTotal: 118, + defectTotal: 2, + totalProductPrice: 2400000, + totalLogisticsPrice: 18000, + grandTotal: 2418000, + routes: [ + { + id: "wbr1", + from: "Садовод", + fromAddress: "Москва, 14-й км МКАД", + to: "WB Подольск", + toAddress: "Подольск, ул. Складская, 25", + totalProductPrice: 2400000, + logisticsPrice: 18000, + totalAmount: 2418000, + warehouses: [ + { + id: "wbw1", + name: "Склад WB Подольск", + address: "Подольск, ул. Складская, 25", + warehouseId: 117501, + totalAmount: 2400000, + products: [ + { + id: "wbp1", + name: "Смартфон Samsung Galaxy S24", + sku: "SAMS-GS24-256", + nmId: 123456789, + category: "Смартфоны и гаджеты", + plannedQty: 40, + actualQty: 39, + defectQty: 1, + productPrice: 65000, + }, + { + id: "wbp2", + name: "Чехол для Samsung Galaxy S24", + sku: "CASE-GS24-BLK", + nmId: 987654321, + category: "Аксессуары для телефонов", + plannedQty: 80, + actualQty: 79, + defectQty: 1, + productPrice: 1200, + }, + ], + }, + ], + }, + ], + }, + { + id: "wb2", + number: 4002, + supplyId: "WB24010002", + deliveryDate: "2024-01-28", + createdDate: "2024-01-20", + status: "in-transit", + plannedTotal: 60, + actualTotal: 60, + defectTotal: 0, + totalProductPrice: 1800000, + totalLogisticsPrice: 15000, + grandTotal: 1815000, + routes: [ + { + id: "wbr2", + from: "ТЯК Москва", + fromAddress: "Москва, Алтуфьевское шоссе, 27", + to: "WB Электросталь", + toAddress: "Электросталь, ул. Промышленная, 10", + totalProductPrice: 1800000, + logisticsPrice: 15000, + totalAmount: 1815000, + warehouses: [ + { + id: "wbw2", + name: "Склад WB Электросталь", + address: "Электросталь, ул. Промышленная, 10", + warehouseId: 117986, + totalAmount: 1800000, + products: [ + { + id: "wbp3", + name: "Наушники Sony WH-1000XM5", + sku: "SONY-WH1000XM5", + nmId: 555666777, + category: "Наушники и аудио", + plannedQty: 30, + actualQty: 30, + defectQty: 0, + productPrice: 35000, + }, + { + id: "wbp4", + name: "Кабель USB-C", + sku: "CABLE-USBC-2M", + nmId: 111222333, + category: "Кабели и адаптеры", + plannedQty: 30, + actualQty: 30, + defectQty: 0, + productPrice: 800, + }, + ], + }, + ], + }, + ], + }, +]; + +export function WildberriesSuppliesTab() { + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); + const [expandedWarehouses, setExpandedWarehouses] = 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 toggleWarehouseExpansion = (warehouseId: string) => { + const newExpanded = new Set(expandedWarehouses); + if (newExpanded.has(warehouseId)) { + newExpanded.delete(warehouseId); + } else { + newExpanded.add(warehouseId); + } + setExpandedWarehouses(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: WbSupply["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: WbProduct) => { + 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 ( +
+ {/* Статистика поставок на Wildberries */} + + + + sum + supply.grandTotal, 0) + )} + icon={TrendingUp} + iconColor="text-green-400" + iconBg="bg-green-500/20" + trend={{ value: 22, isPositive: true }} + subtitle="Общая стоимость" + /> + + supply.status === "in-transit") + .length + } + icon={Calendar} + iconColor="text-yellow-400" + iconBg="bg-yellow-500/20" + subtitle="Активные поставки" + /> + + supply.defectTotal > 0).length + } + icon={AlertTriangle} + iconColor="text-red-400" + iconBg="bg-red-500/20" + trend={{ value: 1, isPositive: false }} + subtitle="Требуют проверки" + /> + + + {/* Таблица поставок на Wildberries */} + +
+ + + + + + + + + + + + + + + + + + {mockWbSupplies.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Основная строка поставки на WB */} + + + + + + + + + + + + + + + {/* Развернутые уровни */} + {isSupplyExpanded && + supply.routes.map((route) => { + const isRouteExpanded = expandedRoutes.has(route.id); + return ( + + + + + + + + + + + + + + {/* Склады WB */} + {isRouteExpanded && + route.warehouses.map((warehouse) => { + const isWarehouseExpanded = + expandedWarehouses.has(warehouse.id); + return ( + + + + + + + + + + + + + + {/* Товары WB */} + {isWarehouseExpanded && + warehouse.products.map((product) => ( + + + + + + + + + + + + ))} + + ); + })} + + ); + })} + + ); + })} + +
+ ID поставки + + Дата поставки + + Дата создания + ПланФактБрак + Цена товаров + + Логистика + + Итого сумма + + Статус +
+
+ + + #{supply.number} + +
+
+ + {supply.supplyId} + + +
+ + + {formatDate(supply.deliveryDate)} + +
+
+ + {formatDate(supply.createdDate)} + + + + {supply.plannedTotal} + + + + {supply.actualTotal} + + + 0 + ? "text-red-400" + : "text-white" + }`} + > + {supply.defectTotal} + + + + {formatCurrency(supply.totalProductPrice)} + + + + {formatCurrency(supply.totalLogisticsPrice)} + + +
+ + + {formatCurrency(supply.grandTotal)} + +
+
{getStatusBadge(supply.status)}
+
+ + + + Маршрут + +
+
+
+
+ + {route.from} + + + + {route.to} + +
+
+ {route.fromAddress} → {route.toAddress} +
+
+
+ + {route.warehouses.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.plannedQty, + 0 + ), + 0 + )} + + + + {route.warehouses.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.actualQty, + 0 + ), + 0 + )} + + + + {route.warehouses.reduce( + (sum, w) => + sum + + w.products.reduce( + (pSum, p) => pSum + p.defectQty, + 0 + ), + 0 + )} + + + + {formatCurrency(route.totalProductPrice)} + + + + {formatCurrency(route.logisticsPrice)} + + + + {formatCurrency(route.totalAmount)} + +
+
+ + + + Склад WB + +
+
+
+
+ {warehouse.name} +
+
+ ID: {warehouse.warehouseId} +
+
+ {warehouse.address} +
+
+
+ + {warehouse.products.reduce( + (sum, p) => sum + p.plannedQty, + 0 + )} + + + + {warehouse.products.reduce( + (sum, p) => sum + p.actualQty, + 0 + )} + + + + {warehouse.products.reduce( + (sum, p) => sum + p.defectQty, + 0 + )} + + + + {formatCurrency( + warehouse.products.reduce( + (sum, p) => + sum + calculateProductTotal(p), + 0 + ) + )} + + + + {formatCurrency( + warehouse.totalAmount + )} + +
+
+ + + Товар WB + +
+
+
+
+ {product.name} +
+
+ Артикул: {product.sku} +
+
+ NM ID: {product.nmId} +
+ + {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) + )} + +
+
+
+
+ ); +} diff --git a/src/components/supplies/supplies-dashboard.tsx b/src/components/supplies/supplies-dashboard.tsx index 897f3c1..361bbef 100644 --- a/src/components/supplies/supplies-dashboard.tsx +++ b/src/components/supplies/supplies-dashboard.tsx @@ -6,12 +6,12 @@ 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"; +import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab"; +import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab"; export function SuppliesDashboard() { const { getSidebarMargin } = useSidebar(); - const [activeTab, setActiveTab] = useState("goods"); + const [activeTab, setActiveTab] = useState("fulfillment"); return (
@@ -20,7 +20,7 @@ export function SuppliesDashboard() { className={`flex-1 ${getSidebarMargin()} px-4 py-4 overflow-hidden transition-all duration-300`} >
- {/* Вкладки с кнопкой создания */} + {/* Главные вкладки с кнопкой создания */} - Товар + Поставки на ФФ - Расходники + Поставки на Маркетплейсы @@ -53,12 +53,12 @@ export function SuppliesDashboard() {
- - + + - - + +
diff --git a/src/components/supplies/ui/stats-card.tsx b/src/components/supplies/ui/stats-card.tsx new file mode 100644 index 0000000..0417fbc --- /dev/null +++ b/src/components/supplies/ui/stats-card.tsx @@ -0,0 +1,83 @@ +"use client"; + +import React from "react"; +import { Card } from "@/components/ui/card"; +import { LucideIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface StatsCardProps { + title: string; + value: string | number; + icon: LucideIcon; + iconColor: string; + iconBg: string; + trend?: { + value: number; + isPositive: boolean; + }; + subtitle?: string; + className?: string; +} + +export function StatsCard({ + title, + value, + icon: Icon, + iconColor, + iconBg, + trend, + subtitle, + className, +}: StatsCardProps) { + return ( + +
+
+
+ +
+
+

+ {title} +

+

+ {value} +

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ + {trend && ( +
+ + {trend.isPositive ? "↗" : "↘"} + + {Math.abs(trend.value)}% +
+ )} +
+
+ ); +} diff --git a/src/components/supplies/ui/stats-grid.tsx b/src/components/supplies/ui/stats-grid.tsx new file mode 100644 index 0000000..eb2d02f --- /dev/null +++ b/src/components/supplies/ui/stats-grid.tsx @@ -0,0 +1,28 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; + +interface StatsGridProps { + children: React.ReactNode; + columns?: 2 | 3 | 4; + className?: string; +} + +export function StatsGrid({ + children, + columns = 4, + className, +}: StatsGridProps) { + const gridCols = { + 2: "grid-cols-1 md:grid-cols-2", + 3: "grid-cols-1 md:grid-cols-3", + 4: "grid-cols-1 md:grid-cols-2 lg:grid-cols-4", + }; + + return ( +
+ {children} +
+ ); +}