diff --git a/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx b/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx index 99d4499..9487f13 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies-dashboard.tsx @@ -1,54 +1,66 @@ -"use client" +"use client"; -import { useState } from 'react' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Card } from '@/components/ui/card' -import { Sidebar } from '@/components/dashboard/sidebar' -import { useSidebar } from '@/hooks/useSidebar' -import { Package, Truck, Wrench, ArrowLeftRight } from 'lucide-react' +import { useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card } from "@/components/ui/card"; +import { Sidebar } from "@/components/dashboard/sidebar"; +import { useSidebar } from "@/hooks/useSidebar"; +import { Building2, ShoppingCart } from "lucide-react"; // Импорты компонентов подразделов -import { GoodsSuppliesTab } from './goods-supplies/goods-supplies-tab' -import { MaterialsSuppliesTab } from './materials-supplies/materials-supplies-tab' +import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab"; +import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab"; export function FulfillmentSuppliesDashboard() { - const { getSidebarMargin } = useSidebar() - const [activeTab, setActiveTab] = useState('goods') + const { getSidebarMargin } = useSidebar(); + const [activeTab, setActiveTab] = useState("fulfillment"); return (
-
+
{/* Основной контент с табами */}
- + - - - Товары + + Поставки на ФФ - - - Расходники + + Поставки на маркетплейсы - + - + - + - + @@ -56,5 +68,5 @@ export function FulfillmentSuppliesDashboard() {
- ) -} \ No newline at end of file + ); +} diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx new file mode 100644 index 0000000..525f5e1 --- /dev/null +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-goods-tab.tsx @@ -0,0 +1,881 @@ +"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 Seller { + 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; + sellers: Seller[]; + 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 mockFulfillmentGoodsSupplies: 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, + sellers: [ + { + id: "ffs1", + name: 'ООО "ТехноСнаб ФФ"', + inn: "7701234567", + contact: "+7 (495) 123-45-67", + address: "Москва, ул. Торговая, 1", + totalAmount: 3600000, + products: [ + { + id: "ffp1", + name: "Смартфон iPhone 15 для ФФ", + sku: "APL-IP15-128-FF", + category: "Электроника ФФ", + plannedQty: 50, + actualQty: 48, + defectQty: 2, + productPrice: 75000, + parameters: [ + { id: "ffparam1", name: "Цвет", value: "Черный" }, + { id: "ffparam2", name: "Память", value: "128", unit: "ГБ" }, + { + id: "ffparam3", + name: "Гарантия", + value: "12", + unit: "мес", + }, + { id: "ffparam4", name: "Упаковка ФФ", value: "Усиленная" }, + ], + }, + ], + }, + ], + }, + ], + }, + { + 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: "WB Подольск ФФ", + toAddress: "Подольск, ул. Складская, 25", + totalProductPrice: 750000, + fulfillmentServicePrice: 18000, + logisticsPrice: 12000, + totalAmount: 780000, + sellers: [ + { + id: "ffs2", + name: 'ООО "АудиоТех ФФ"', + inn: "7702345678", + contact: "+7 (495) 555-12-34", + address: "Москва, ул. Звуковая, 8", + totalAmount: 750000, + products: [ + { + id: "ffp2", + name: "Наушники AirPods Pro для ФФ", + sku: "APL-AP-PRO2-FF", + category: "Аудио ФФ", + plannedQty: 30, + actualQty: 30, + defectQty: 0, + productPrice: 25000, + parameters: [ + { id: "ffparam5", name: "Тип", value: "Беспроводные" }, + { id: "ffparam6", name: "Шумоподавление", value: "Активное" }, + { + id: "ffparam7", + name: "Время работы", + value: "6", + unit: "ч", + }, + { + id: "ffparam8", + name: "Сертификация ФФ", + value: "Пройдена", + }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export function FulfillmentDetailedGoodsTab() { + const [expandedSupplies, setExpandedSupplies] = useState>( + new Set() + ); + const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); + const [expandedSellers, setExpandedSellers] = 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 toggleSellerExpansion = (sellerId: string) => { + const newExpanded = new Set(expandedSellers); + if (newExpanded.has(sellerId)) { + newExpanded.delete(sellerId); + } else { + newExpanded.add(sellerId); + } + setExpandedSellers(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 ( +
+ {/* Статистика товаров ФФ */} +
+ +
+
+ +
+
+

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

+

+ {mockFulfillmentGoodsSupplies.length} +

+
+
+
+ + +
+
+ +
+
+

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

+

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

+
+
+
+ + +
+
+ +
+
+

В пути

+

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

+
+
+
+ + +
+
+ +
+
+

С браком

+

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

+
+
+
+
+ + {/* Таблица поставок товаров ФФ */} + +
+ + + + + + + + + + + + + + + + + + {mockFulfillmentGoodsSupplies.map((supply) => { + const isSupplyExpanded = expandedSupplies.has(supply.id); + + return ( + + {/* Уровень 1: Основная строка поставки ФФ */} + + + + + + + + + + + + + + + {/* Развернутые уровни */} + {isSupplyExpanded && + supply.routes.map((route) => { + const isRouteExpanded = expandedRoutes.has(route.id); + return ( + + + + + + + + + + + + + + + {/* Дальнейшие уровни развертывания */} + {isRouteExpanded && + route.sellers.map((seller) => { + const isSellerExpanded = expandedSellers.has( + seller.id + ); + return ( + + + + + + + + + + + + + + {/* Товары */} + {isSellerExpanded && + seller.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.sellers.reduce( + (sum, s) => + sum + + s.products.reduce( + (pSum, p) => pSum + p.plannedQty, + 0 + ), + 0 + )} + + + + {route.sellers.reduce( + (sum, s) => + sum + + s.products.reduce( + (pSum, p) => pSum + p.actualQty, + 0 + ), + 0 + )} + + + + {route.sellers.reduce( + (sum, s) => + sum + + s.products.reduce( + (pSum, p) => pSum + p.defectQty, + 0 + ), + 0 + )} + + + + {formatCurrency(route.totalProductPrice)} + + + + {formatCurrency( + route.fulfillmentServicePrice + )} + + + + {formatCurrency(route.logisticsPrice)} + + + + {formatCurrency(route.totalAmount)} + +
+
+ + + + Селлер ФФ + +
+
+
+
+ {seller.name} +
+
+ ИНН: {seller.inn} +
+
+ {seller.address} +
+
+ {seller.contact} +
+
+
+ + {seller.products.reduce( + (sum, p) => sum + p.plannedQty, + 0 + )} + + + + {seller.products.reduce( + (sum, p) => sum + p.actualQty, + 0 + )} + + + + {seller.products.reduce( + (sum, p) => sum + p.defectQty, + 0 + )} + + + + {formatCurrency( + seller.products.reduce( + (sum, p) => + sum + calculateProductTotal(p), + 0 + ) + )} + + + + {formatCurrency(seller.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/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-supplies-tab.tsx new file mode 100644 index 0000000..7d95d79 --- /dev/null +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-detailed-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 { StatsCard } from "../../supplies/ui/stats-card"; +import { StatsGrid } from "../../supplies/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 mockFulfillmentConsumablesDetailed: FulfillmentConsumableSupply[] = [ + { + id: "ffcd1", + 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: "ffcdr1", + from: "Склад расходников ФФ", + fromAddress: "Москва, ул. Промышленная, 12", + to: "SFERAV Logistics ФФ", + toAddress: "Москва, ул. Складская, 15", + totalConsumablesPrice: 125000, + logisticsPrice: 8000, + totalAmount: 133000, + suppliers: [ + { + id: "ffcds1", + name: 'ООО "УпакСервис ФФ Детально"', + inn: "7703456789", + contact: "+7 (495) 777-88-99", + address: "Москва, ул. Упаковочная, 5", + totalAmount: 75000, + consumables: [ + { + id: "ffcdcons1", + name: "Коробки для ФФ детально 40x30x15", + sku: "BOX-FFD-403015", + category: "Упаковка ФФ детально", + type: "packaging", + plannedQty: 2000, + actualQty: 1980, + defectQty: 20, + unitPrice: 45, + parameters: [ + { + id: "ffcdp1", + name: "Размер", + value: "40x30x15", + unit: "см", + }, + { + id: "ffcdp2", + name: "Материал", + value: "Гофрокартон усиленный ФФ", + }, + { + id: "ffcdp3", + name: "Плотность", + value: "5", + unit: "слоев", + }, + { id: "ffcdp4", name: "Сертификация ФФ", value: "Пройдена" }, + ], + }, + ], + }, + ], + }, + ], + }, + { + id: "ffcd2", + number: 2002, + deliveryDate: "2024-01-22", + createdDate: "2024-01-16", + status: "in-transit", + plannedTotal: 3000, + actualTotal: 3000, + defectTotal: 0, + totalConsumablesPrice: 85000, + totalLogisticsPrice: 5500, + grandTotal: 90500, + routes: [ + { + id: "ffcdr2", + from: "Склад расходников ФФ", + fromAddress: "Москва, ул. Промышленная, 12", + to: "WB Подольск ФФ", + toAddress: "Подольск, ул. Складская, 25", + totalConsumablesPrice: 85000, + logisticsPrice: 5500, + totalAmount: 90500, + suppliers: [ + { + id: "ffcds2", + name: 'ООО "ЭтикеткаПро ФФ"', + inn: "7704567890", + contact: "+7 (495) 888-99-00", + address: "Москва, ул. Этикеточная, 3", + totalAmount: 85000, + consumables: [ + { + id: "ffcdcons2", + name: "Этикетки самоклеящиеся ФФ 10x5", + sku: "LBL-FFD-105", + category: "Этикетки ФФ детально", + type: "labels", + plannedQty: 3000, + actualQty: 3000, + defectQty: 0, + unitPrice: 28, + parameters: [ + { + id: "ffcdp5", + name: "Размер", + value: "10x5", + unit: "см", + }, + { + id: "ffcdp6", + name: "Материал", + value: "Бумага самоклеящаяся ФФ", + }, + { id: "ffcdp7", name: "Клей", value: "Акриловый" }, + { id: "ffcdp8", name: "Качество ФФ", value: "Премиум" }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export function FulfillmentDetailedSuppliesTab() { + 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="Дефектные материалы ФФ" + /> + + + {/* Таблица поставок расходников ФФ детально */} + +
+ + + + + + + + + + + + + + + + + {mockFulfillmentConsumablesDetailed.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 + )}{" "} + за шт. +
+
+
+ + {formatCurrency( + calculateConsumableTotal( + consumable + ) + )} + +
+
+
+

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

+
+ {consumable.parameters.map( + (param) => ( +
+
+ {param.name} +
+
+ {param.value}{" "} + {param.unit || + ""} +
+
+ ) + )} +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx index 21bb77c..b0bf9d2 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-goods-tab.tsx @@ -1,61 +1,149 @@ "use client"; import { useState } from "react"; +import { useQuery } from "@apollo/client"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { GET_MY_EMPLOYEES, GET_MY_COUNTERPARTIES } from "@/graphql/queries"; import { Package, Plus, Search, - Filter, TrendingUp, - AlertCircle, Calendar, Eye, + User, + Truck, + Hash, + Package2, + Boxes, + ChevronDown, + ChevronRight, + Store, } from "lucide-react"; -// Мок данные для товаров -const mockGoodsSupplies = [ +// Интерфейсы для данных +interface Employee { + id: string; + firstName: string; + lastName: string; + position: string; + status: string; +} + +interface Organization { + id: string; + name?: string; + fullName?: string; + type: string; +} + +// Мок данные для поставок с новой структурой +const mockFulfillmentSupplies = [ { id: "1", - productName: "Смартфон iPhone 15", - sku: "IPH15-128-BLK", - seller: "TechStore LLC", - quantity: 50, - expectedDate: "2024-01-15", + supplyNumber: "ФФ-2024-001", + supplyDate: "2024-01-15", + seller: { + id: "seller1", + name: "TechStore LLC", + storeName: "ТехноМагазин", + managerName: "Иванов Иван Иванович", + phone: "+7 (495) 123-45-67", + email: "contact@techstore.ru", + inn: "7701234567", + }, + itemsQuantity: 150, + cargoPlaces: 5, + volume: 12.5, + responsibleEmployeeId: "emp1", + logisticsPartnerId: "log1", status: "planned", totalValue: 2500000, - warehouse: "Склад А1", }, { id: "2", - productName: "Ноутбук MacBook Air", - sku: "MBA-M2-256-SLV", - seller: "Apple Reseller", - quantity: 25, - expectedDate: "2024-01-12", + supplyNumber: "ФФ-2024-002", + supplyDate: "2024-01-12", + seller: { + id: "seller2", + name: "Apple Reseller", + storeName: "ЭплСтор", + managerName: "Петров Петр Петрович", + phone: "+7 (495) 987-65-43", + email: "orders@applereseller.ru", + inn: "7709876543", + }, + itemsQuantity: 75, + cargoPlaces: 3, + volume: 8.2, + responsibleEmployeeId: "emp2", + logisticsPartnerId: "log2", status: "in-transit", totalValue: 3750000, - warehouse: "Склад Б2", }, { id: "3", - productName: "Наушники AirPods Pro", - sku: "APP-2GEN-WHT", - seller: "Audio World", - quantity: 100, - expectedDate: "2024-01-10", + supplyNumber: "ФФ-2024-003", + supplyDate: "2024-01-10", + seller: { + id: "seller3", + name: "Audio World", + storeName: "АудиоМир", + managerName: "Сидоров Сидор Сидорович", + phone: "+7 (495) 555-12-34", + email: "info@audioworld.ru", + inn: "7705551234", + }, + itemsQuantity: 200, + cargoPlaces: 8, + volume: 15.7, + responsibleEmployeeId: "emp1", + logisticsPartnerId: "log1", status: "delivered", totalValue: 2800000, - warehouse: "Склад А1", }, ]; export function FulfillmentGoodsTab() { const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + const [expandedSellers, setExpandedSellers] = useState>( + new Set() + ); + + // Загружаем сотрудников для селектора ответственных + const { data: employeesData, loading: employeesLoading } = + useQuery(GET_MY_EMPLOYEES); + + // Загружаем партнеров-логистов + const { data: counterpartiesData, loading: counterpartiesLoading } = useQuery( + GET_MY_COUNTERPARTIES + ); + + const employees: Employee[] = employeesData?.myEmployees || []; + const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter( + (org: Organization) => org.type === "LOGIST" + ); + + const toggleSellerExpansion = (sellerId: string) => { + const newExpanded = new Set(expandedSellers); + if (newExpanded.has(sellerId)) { + newExpanded.delete(sellerId); + } else { + newExpanded.add(sellerId); + } + setExpandedSellers(newExpanded); + }; const formatCurrency = (amount: number) => { return new Intl.NumberFormat("ru-RU", { @@ -103,11 +191,30 @@ export function FulfillmentGoodsTab() { ); }; - const filteredSupplies = mockGoodsSupplies.filter((supply) => { + const getEmployeeName = (employeeId: string) => { + const employee = employees.find((emp) => emp.id === employeeId); + return employee + ? `${employee.firstName} ${employee.lastName}` + : "Не назначен"; + }; + + const getLogisticsPartnerName = (partnerId: string) => { + const partner = logisticsPartners.find( + (p: Organization) => p.id === partnerId + ); + return partner + ? partner.name || partner.fullName || "Без названия" + : "Не выбран"; + }; + + const filteredSupplies = mockFulfillmentSupplies.filter((supply) => { const matchesSearch = - supply.productName.toLowerCase().includes(searchTerm.toLowerCase()) || - supply.sku.toLowerCase().includes(searchTerm.toLowerCase()) || - supply.seller.toLowerCase().includes(searchTerm.toLowerCase()); + supply.supplyNumber.toLowerCase().includes(searchTerm.toLowerCase()) || + supply.seller.name.toLowerCase().includes(searchTerm.toLowerCase()) || + supply.seller.storeName + .toLowerCase() + .includes(searchTerm.toLowerCase()) || + supply.seller.inn.includes(searchTerm); const matchesStatus = statusFilter === "all" || supply.status === statusFilter; @@ -120,14 +227,21 @@ export function FulfillmentGoodsTab() { }; const getTotalQuantity = () => { - return filteredSupplies.reduce((sum, supply) => sum + supply.quantity, 0); + return filteredSupplies.reduce( + (sum, supply) => sum + supply.itemsQuantity, + 0 + ); + }; + + const getTotalVolume = () => { + return filteredSupplies.reduce((sum, supply) => sum + supply.volume, 0); }; return (
{/* Статистика с кнопкой */}
-
+
@@ -159,12 +273,26 @@ export function FulfillmentGoodsTab() {
- +

Товаров

- {getTotalQuantity()} + {getTotalQuantity()} ед. +

+
+
+
+ + +
+
+ +
+
+

Объём

+

+ {getTotalVolume().toFixed(1)} м³

@@ -185,7 +313,7 @@ export function FulfillmentGoodsTab() {
setSearchTerm(e.target.value)} className="glass-input pl-10 text-white placeholder:text-white/40" @@ -208,71 +336,240 @@ export function FulfillmentGoodsTab() { {/* Список поставок */}
- {filteredSupplies.map((supply) => ( - -
-
-
-

- {supply.productName} -

- {getStatusBadge(supply.status)} -
+ {filteredSupplies.map((supply) => { + const isSellerExpanded = expandedSellers.has(supply.seller.id); -
-
-

SKU

-

{supply.sku}

-
-
-

Селлер

-

{supply.seller}

-
-
-

Количество

-

- {supply.quantity} шт. -

-
-
-

Ожидается

-

- {formatDate(supply.expectedDate)} -

-
-
- -
-
- - Склад:{" "} - {supply.warehouse} - - - Стоимость:{" "} - - {formatCurrency(supply.totalValue)} + return ( + +
+ {/* Компактный блок с названием магазина */} +
+
+
+ +
+
+ + {supply.seller.storeName} - + + + {supply.seller.managerName} + + + + {supply.seller.phone} + + + +
+
+ +
+ + {/* Скрытая детальная информация о магазине */} + {isSellerExpanded && ( +
+
+
+

+ Юридическое название +

+

+ {supply.seller.name} +

+
+
+

ИНН

+

+ {supply.seller.inn} +

+
+
+
+

Email

+

+ {supply.seller.email} +

+
+
+ )} + + {/* Единый блок со всеми параметрами в одной строке */} +
+
+ {/* Номер поставки */} +
+

Номер

+

+ {supply.supplyNumber} +

+
+ + {/* Дата поставки */} +
+

Дата

+

+ {formatDate(supply.supplyDate)} +

+
+ + {/* Количество товаров */} +
+

Товаров

+

+ {supply.itemsQuantity} +

+
+ + {/* Количество мест */} +
+

Мест

+

+ {supply.cargoPlaces} +

+
+ + {/* Объём */} +
+

Объём

+

+ {supply.volume} м³ +

+
+ + {/* Стоимость */} +
+

Стоимость

+

+ {formatCurrency(supply.totalValue)} +

+
+ + {/* Ответственный сотрудник */} +
+

+ Ответственный +

+ +
+ + {/* Логистический партнер */} +
+

Логистика

+ +
+ + {/* Статус */} +
+

Статус

+
+ {getStatusBadge(supply.status)} +
+
- -
- -
-
- - ))} + + ); + })}
diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx index cd293a6..bd7a576 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-supplies-tab.tsx @@ -2,12 +2,16 @@ import { useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Package, Wrench, RotateCcw } from "lucide-react"; +import { Package, Wrench, RotateCcw, Package2, Building2 } from "lucide-react"; // Импорты компонентов подкатегорий import { FulfillmentGoodsTab } from "./fulfillment-goods-tab"; -import { SellerMaterialsTab } from "./seller-materials-tab"; import { PvzReturnsTab } from "./pvz-returns-tab"; +import { SuppliesConsumablesTab } from "../../supplies/consumables-supplies/consumables-supplies-tab"; + +// Новые компоненты для детального просмотра (копия из supplies модуля) +import { FulfillmentDetailedGoodsTab } from "./fulfillment-detailed-goods-tab"; +import { FulfillmentDetailedSuppliesTab } from "./fulfillment-detailed-supplies-tab"; export function FulfillmentSuppliesTab() { const [activeTab, setActiveTab] = useState("goods"); @@ -19,20 +23,34 @@ export function FulfillmentSuppliesTab() { onValueChange={setActiveTab} className="h-full flex flex-col" > - + - Товары + Поставки + + Товары детально + + + + Расходники детально + + - Расходники селлеров + Расходники С - - + +
+ +
+
+ + +
+ +
+
+ + +
+ +