"use client"; import { useState, useMemo } from "react"; import { useRouter } from "next/navigation"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Sidebar } from "@/components/dashboard/sidebar"; import { useSidebar } from "@/hooks/useSidebar"; import { useAuth } from "@/hooks/useAuth"; import { useQuery } from "@apollo/client"; import { GET_MY_COUNTERPARTIES, GET_SUPPLY_ORDERS, GET_WAREHOUSE_PRODUCTS, GET_MY_SUPPLIES, // Расходники селлеров GET_MY_FULFILLMENT_SUPPLIES, // Расходники фулфилмента GET_FULFILLMENT_WAREHOUSE_STATS, // Статистика склада с изменениями за сутки } from "@/graphql/queries"; import { toast } from "sonner"; import { Package, TrendingUp, TrendingDown, AlertTriangle, RotateCcw, Wrench, Users, Box, Search, ArrowUpDown, Store, Package2, Eye, EyeOff, ChevronRight, ChevronDown, Layers, Truck, Clock, } from "lucide-react"; // Типы данных interface ProductVariant { id: string; name: string; // Размер, характеристика, вариант упаковки // Места и количества для каждого типа на уровне варианта productPlace?: string; productQuantity: number; goodsPlace?: string; goodsQuantity: number; defectsPlace?: string; defectsQuantity: number; sellerSuppliesPlace?: string; sellerSuppliesQuantity: number; pvzReturnsPlace?: string; pvzReturnsQuantity: number; } interface ProductItem { id: string; name: string; article: string; // Места и количества для каждого типа productPlace?: string; productQuantity: number; goodsPlace?: string; goodsQuantity: number; defectsPlace?: string; defectsQuantity: number; sellerSuppliesPlace?: string; sellerSuppliesQuantity: number; pvzReturnsPlace?: string; pvzReturnsQuantity: number; // Третий уровень - варианты товара variants?: ProductVariant[]; } interface StoreData { id: string; name: string; logo?: string; avatar?: string; // Аватар пользователя организации products: number; goods: number; defects: number; sellerSupplies: number; pvzReturns: number; // Изменения за сутки productsChange: number; goodsChange: number; defectsChange: number; sellerSuppliesChange: number; pvzReturnsChange: number; // Детализация по товарам items: ProductItem[]; } interface WarehouseStats { products: { current: number; change: number }; goods: { current: number; change: number }; defects: { current: number; change: number }; pvzReturns: { current: number; change: number }; fulfillmentSupplies: { current: number; change: number }; sellerSupplies: { current: number; change: number }; } interface Supply { id: string; name: string; description?: string; price: number; quantity: number; unit: string; category: string; status: string; date: string; supplier: string; minStock: number; currentStock: number; } interface SupplyOrder { id: string; status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; deliveryDate: string; totalAmount: number; totalItems: number; partner: { id: string; name: string; fullName: string; }; items: Array<{ id: string; quantity: number; product: { id: string; name: string; article: string; }; }>; } /** * Цветовая схема уровней: * 🔵 Уровень 1: Магазины - УНИКАЛЬНЫЕ ЦВЕТА для каждого магазина: * - ТехноМир: Синий (blue-400/500) - технологии * - Стиль и Комфорт: Розовый (pink-400/500) - мода/одежда * - Зелёный Дом: Изумрудный (emerald-400/500) - природа/сад * - Усиленная видимость: жирная левая граница (8px), тень, светлый текст * 🟢 Уровень 2: Товары - Зеленый (green-500) * 🟠 Уровень 3: Варианты товаров - Оранжевый (orange-500) * * Каждый уровень имеет: * - Цветной индикатор (круглая точка увеличивающегося размера) * - Цветную левую границу с увеличивающимся отступом и толщиной * - Соответствующий цвет фона и границ * - Скроллбары в цвете уровня * - Контрастный цвет текста для лучшей читаемости */ export function FulfillmentWarehouseDashboard() { const router = useRouter(); const { getSidebarMargin } = useSidebar(); const { user } = useAuth(); // Состояния для поиска и фильтрации const [searchTerm, setSearchTerm] = useState(""); const [sortField, setSortField] = useState("name"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [expandedStores, setExpandedStores] = useState>(new Set()); const [expandedItems, setExpandedItems] = useState>(new Set()); const [showAdditionalValues, setShowAdditionalValues] = useState(true); // Загружаем данные из GraphQL const { data: counterpartiesData, loading: counterpartiesLoading, error: counterpartiesError, refetch: refetchCounterparties, } = useQuery(GET_MY_COUNTERPARTIES, { fetchPolicy: "cache-and-network", // Всегда проверяем актуальные данные }); const { data: ordersData, loading: ordersLoading, error: ordersError, refetch: refetchOrders, } = useQuery(GET_SUPPLY_ORDERS, { fetchPolicy: "cache-and-network", }); const { data: productsData, loading: productsLoading, error: productsError, refetch: refetchProducts, } = useQuery(GET_WAREHOUSE_PRODUCTS, { fetchPolicy: "cache-and-network", }); // Загружаем расходники селлеров const { data: suppliesData, loading: suppliesLoading, error: suppliesError, refetch: refetchSupplies, } = useQuery(GET_MY_SUPPLIES, { fetchPolicy: "cache-and-network", }); // Загружаем расходники фулфилмента const { data: fulfillmentSuppliesData, loading: fulfillmentSuppliesLoading, error: fulfillmentSuppliesError, refetch: refetchFulfillmentSupplies, } = useQuery(GET_MY_FULFILLMENT_SUPPLIES, { fetchPolicy: "cache-and-network", }); // Загружаем статистику склада с изменениями за сутки const { data: warehouseStatsData, loading: warehouseStatsLoading, error: warehouseStatsError, refetch: refetchWarehouseStats, } = useQuery(GET_FULFILLMENT_WAREHOUSE_STATS, { fetchPolicy: "no-cache", // Принудительно обходим кеш pollInterval: 60000, // Обновляем каждую минуту }); // Логируем статистику склада для отладки console.log("📊 WAREHOUSE STATS DEBUG:", { loading: warehouseStatsLoading, error: warehouseStatsError?.message, data: warehouseStatsData, hasData: !!warehouseStatsData?.fulfillmentWarehouseStats, }); // Детальное логирование данных статистики if (warehouseStatsData?.fulfillmentWarehouseStats) { console.log("📈 DETAILED WAREHOUSE STATS:", { products: warehouseStatsData.fulfillmentWarehouseStats.products, goods: warehouseStatsData.fulfillmentWarehouseStats.goods, defects: warehouseStatsData.fulfillmentWarehouseStats.defects, pvzReturns: warehouseStatsData.fulfillmentWarehouseStats.pvzReturns, fulfillmentSupplies: warehouseStatsData.fulfillmentWarehouseStats.fulfillmentSupplies, sellerSupplies: warehouseStatsData.fulfillmentWarehouseStats.sellerSupplies, }); } // Получаем данные магазинов, заказов и товаров const allCounterparties = counterpartiesData?.myCounterparties || []; const sellerPartners = allCounterparties.filter( (partner: { type: string }) => partner.type === "SELLER" ); const supplyOrders: SupplyOrder[] = ordersData?.supplyOrders || []; const allProducts = productsData?.warehouseProducts || []; const mySupplies = suppliesData?.mySupplies || []; // Расходники селлеров const myFulfillmentSupplies = fulfillmentSuppliesData?.myFulfillmentSupplies || []; // Расходники фулфилмента // Логирование для отладки console.log("🏪 Данные склада фулфилмента:", { allCounterpartiesCount: allCounterparties.length, sellerPartnersCount: sellerPartners.length, sellerPartners: sellerPartners.map((p: any) => ({ id: p.id, name: p.name, fullName: p.fullName, type: p.type, })), ordersCount: supplyOrders.length, deliveredOrders: supplyOrders.filter((o) => o.status === "DELIVERED") .length, productsCount: allProducts.length, suppliesCount: mySupplies.length, // Добавляем логирование расходников supplies: mySupplies.map((s: any) => ({ id: s.id, name: s.name, currentStock: s.currentStock, category: s.category, supplier: s.supplier, })), products: allProducts.map((p: any) => ({ id: p.id, name: p.name, article: p.article, organizationName: p.organization?.name || p.organization?.fullName, organizationType: p.organization?.type, })), // Добавляем анализ соответствия товаров и расходников productSupplyMatching: allProducts.map((product: any) => { const matchingSupply = mySupplies.find((supply: any) => { return ( supply.name.toLowerCase() === product.name.toLowerCase() || supply.name .toLowerCase() .includes(product.name.toLowerCase().split(" ")[0]) ); }); return { productName: product.name, matchingSupplyName: matchingSupply?.name, matchingSupplyStock: matchingSupply?.currentStock, hasMatch: !!matchingSupply, }; }), counterpartiesLoading, ordersLoading, productsLoading, suppliesLoading, // Добавляем статус загрузки расходников counterpartiesError: counterpartiesError?.message, ordersError: ordersError?.message, productsError: productsError?.message, suppliesError: suppliesError?.message, // Добавляем ошибки загрузки расходников }); // Расчет поступлений расходников за сутки (выносим отдельно для использования в storeData) const suppliesReceivedToday = useMemo(() => { const deliveredOrders = supplyOrders.filter( (o) => o.status === "DELIVERED" ); // Подсчитываем расходники селлера из доставленных заказов за последние сутки const oneDayAgo = new Date(); oneDayAgo.setDate(oneDayAgo.getDate() - 1); const recentDeliveredOrders = deliveredOrders.filter((order) => { const deliveryDate = new Date(order.deliveryDate); return deliveryDate >= oneDayAgo && order.fulfillmentCenter?.id; // За последние сутки }); const realSuppliesReceived = recentDeliveredOrders.reduce( (sum, order) => sum + order.totalItems, 0 ); // Логирование для отладки console.log("📦 Анализ поставок расходников за сутки:", { totalDeliveredOrders: deliveredOrders.length, recentDeliveredOrders: recentDeliveredOrders.length, recentOrders: recentDeliveredOrders.map((order) => ({ id: order.id, deliveryDate: order.deliveryDate, totalItems: order.totalItems, status: order.status, })), realSuppliesReceived, oneDayAgo: oneDayAgo.toISOString(), }); // Возвращаем реальное значение без fallback return realSuppliesReceived; }, [supplyOrders]); // Расчет использованных расходников за сутки (пока всегда 0, так как нет данных об использовании) const suppliesUsedToday = useMemo(() => { // TODO: Здесь должна быть логика подсчета использованных расходников // Пока возвращаем 0, так как нет данных об использовании return 0; }, []); // Расчет изменений товаров за сутки (реальные данные) const productsReceivedToday = useMemo(() => { // Товары, поступившие за сутки из доставленных заказов const deliveredOrders = supplyOrders.filter( (o) => o.status === "DELIVERED" ); const oneDayAgo = new Date(); oneDayAgo.setDate(oneDayAgo.getDate() - 1); const recentDeliveredOrders = deliveredOrders.filter((order) => { const deliveryDate = new Date(order.deliveryDate); return deliveryDate >= oneDayAgo && order.fulfillmentCenter?.id; }); const realProductsReceived = recentDeliveredOrders.reduce( (sum, order) => sum + (order.totalItems || 0), 0 ); // Логирование для отладки console.log("📦 Анализ поставок товаров за сутки:", { totalDeliveredOrders: deliveredOrders.length, recentDeliveredOrders: recentDeliveredOrders.length, recentOrders: recentDeliveredOrders.map((order) => ({ id: order.id, deliveryDate: order.deliveryDate, totalItems: order.totalItems, status: order.status, })), realProductsReceived, oneDayAgo: oneDayAgo.toISOString(), }); return realProductsReceived; }, [supplyOrders]); const productsUsedToday = useMemo(() => { // Товары, отправленные/использованные за сутки (пока 0, нет данных) return 0; }, []); // Логирование статистики расходников для отладки console.log("📊 Статистика расходников селлера:", { suppliesReceivedToday, suppliesUsedToday, totalSellerSupplies: mySupplies.reduce( (sum: number, supply: any) => sum + (supply.currentStock || 0), 0 ), netChange: suppliesReceivedToday - suppliesUsedToday, }); // Получаем статистику склада из GraphQL (с реальными изменениями за сутки) const warehouseStats: WarehouseStats = useMemo(() => { // Если данные еще загружаются, возвращаем нули if (warehouseStatsLoading || !warehouseStatsData?.fulfillmentWarehouseStats) { return { products: { current: 0, change: 0 }, goods: { current: 0, change: 0 }, defects: { current: 0, change: 0 }, pvzReturns: { current: 0, change: 0 }, fulfillmentSupplies: { current: 0, change: 0 }, sellerSupplies: { current: 0, change: 0 }, }; } // Используем данные из GraphQL резолвера const stats = warehouseStatsData.fulfillmentWarehouseStats; return { products: { current: stats.products.current, change: stats.products.change, }, goods: { current: stats.goods.current, change: stats.goods.change, }, defects: { current: stats.defects.current, change: stats.defects.change, }, pvzReturns: { current: stats.pvzReturns.current, change: stats.pvzReturns.change, }, fulfillmentSupplies: { current: stats.fulfillmentSupplies.current, change: stats.fulfillmentSupplies.change, }, sellerSupplies: { current: stats.sellerSupplies.current, change: stats.sellerSupplies.change, }, }; }, [warehouseStatsData, warehouseStatsLoading]); // Создаем структурированные данные склада на основе уникальных товаров const storeData: StoreData[] = useMemo(() => { if (!sellerPartners.length && !allProducts.length) return []; // Группируем товары по названию, суммируя количества из разных поставок const groupedProducts = new Map< string, { name: string; totalQuantity: number; suppliers: string[]; categories: string[]; prices: number[]; articles: string[]; originalProducts: any[]; } >(); // Группируем товары из allProducts allProducts.forEach((product: any) => { const productName = product.name; const quantity = product.orderedQuantity || 0; if (groupedProducts.has(productName)) { const existing = groupedProducts.get(productName)!; existing.totalQuantity += quantity; existing.suppliers.push( product.organization?.name || product.organization?.fullName || "Неизвестно" ); existing.categories.push(product.category?.name || "Без категории"); existing.prices.push(product.price || 0); existing.articles.push(product.article || ""); existing.originalProducts.push(product); } else { groupedProducts.set(productName, { name: productName, totalQuantity: quantity, suppliers: [ product.organization?.name || product.organization?.fullName || "Неизвестно", ], categories: [product.category?.name || "Без категории"], prices: [product.price || 0], articles: [product.article || ""], originalProducts: [product], }); } }); // Группируем расходники по названию const groupedSupplies = new Map(); mySupplies.forEach((supply: any) => { const supplyName = supply.name; const currentStock = supply.currentStock || 0; if (groupedSupplies.has(supplyName)) { groupedSupplies.set( supplyName, groupedSupplies.get(supplyName)! + currentStock ); } else { groupedSupplies.set(supplyName, currentStock); } }); // Логирование группировки console.log("📊 Группировка товаров и расходников:", { groupedProductsCount: groupedProducts.size, groupedSuppliesCount: groupedSupplies.size, groupedProducts: Array.from(groupedProducts.entries()).map( ([name, data]) => ({ name, totalQuantity: data.totalQuantity, suppliersCount: data.suppliers.length, uniqueSuppliers: [...new Set(data.suppliers)], }) ), groupedSupplies: Array.from(groupedSupplies.entries()).map( ([name, quantity]) => ({ name, totalQuantity: quantity, }) ), }); // Создаем виртуальных "партнеров" на основе уникальных товаров const uniqueProductNames = Array.from(groupedProducts.keys()); const virtualPartners = Math.max( 1, Math.min(sellerPartners.length, Math.ceil(uniqueProductNames.length / 8)) ); return Array.from({ length: virtualPartners }, (_, index) => { const startIndex = index * 8; const endIndex = Math.min(startIndex + 8, uniqueProductNames.length); const partnerProductNames = uniqueProductNames.slice( startIndex, endIndex ); const items: ProductItem[] = partnerProductNames.map( (productName, itemIndex) => { const productData = groupedProducts.get(productName)!; const itemProducts = productData.totalQuantity; // Ищем соответствующий расходник по названию const matchingSupplyQuantity = groupedSupplies.get(productName) || 0; // Если нет точного совпадения, ищем частичное совпадение let itemSuppliesQuantity = matchingSupplyQuantity; if (itemSuppliesQuantity === 0) { for (const [supplyName, quantity] of groupedSupplies.entries()) { if ( supplyName.toLowerCase().includes(productName.toLowerCase()) || productName.toLowerCase().includes(supplyName.toLowerCase()) ) { itemSuppliesQuantity = quantity; break; } } } // Fallback к процентному соотношению if (itemSuppliesQuantity === 0) { itemSuppliesQuantity = Math.floor(itemProducts * 0.1); } console.log(`📦 Товар "${productName}":`, { totalQuantity: itemProducts, suppliersCount: productData.suppliers.length, uniqueSuppliers: [...new Set(productData.suppliers)], matchingSupplyQuantity: matchingSupplyQuantity, finalSuppliesQuantity: itemSuppliesQuantity, usedFallback: matchingSupplyQuantity === 0 && itemSuppliesQuantity > 0, }); return { id: `grouped-${productName}-${itemIndex}`, // Уникальный ID для группированного товара name: productName, article: productData.articles[0] || `ART${(index + 1).toString().padStart(2, "0")}${(itemIndex + 1) .toString() .padStart(2, "0")}`, productPlace: `A${index + 1}-${itemIndex + 1}`, productQuantity: itemProducts, // Суммированное количество (реальные данные) goodsPlace: `B${index + 1}-${itemIndex + 1}`, goodsQuantity: 0, // Нет реальных данных о готовых товарах defectsPlace: `C${index + 1}-${itemIndex + 1}`, defectsQuantity: 0, // Нет реальных данных о браке sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}`, sellerSuppliesQuantity: itemSuppliesQuantity, // Суммированное количество расходников (реальные данные) pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах с ПВЗ // Создаем варианты товара variants: Math.random() > 0.5 ? [ { id: `grouped-${productName}-${itemIndex}-1`, name: `Размер S`, productPlace: `A${index + 1}-${itemIndex + 1}-1`, productQuantity: Math.floor(itemProducts * 0.4), // Часть от общего количества goodsPlace: `B${index + 1}-${itemIndex + 1}-1`, goodsQuantity: 0, // Нет реальных данных о готовых товарах defectsPlace: `C${index + 1}-${itemIndex + 1}-1`, defectsQuantity: 0, // Нет реальных данных о браке sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-1`, sellerSuppliesQuantity: Math.floor( itemSuppliesQuantity * 0.4 ), // Часть от расходников pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-1`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах }, { id: `grouped-${productName}-${itemIndex}-2`, name: `Размер M`, productPlace: `A${index + 1}-${itemIndex + 1}-2`, productQuantity: Math.floor(itemProducts * 0.4), // Часть от общего количества goodsPlace: `B${index + 1}-${itemIndex + 1}-2`, goodsQuantity: 0, // Нет реальных данных о готовых товарах defectsPlace: `C${index + 1}-${itemIndex + 1}-2`, defectsQuantity: 0, // Нет реальных данных о браке sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-2`, sellerSuppliesQuantity: Math.floor( itemSuppliesQuantity * 0.4 ), // Часть от расходников pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-2`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах }, { id: `grouped-${productName}-${itemIndex}-3`, name: `Размер L`, productPlace: `A${index + 1}-${itemIndex + 1}-3`, productQuantity: Math.floor(itemProducts * 0.2), // Оставшаяся часть goodsPlace: `B${index + 1}-${itemIndex + 1}-3`, goodsQuantity: 0, // Нет реальных данных о готовых товарах defectsPlace: `C${index + 1}-${itemIndex + 1}-3`, defectsQuantity: 0, // Нет реальных данных о браке sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}-3`, sellerSuppliesQuantity: Math.floor( itemSuppliesQuantity * 0.2 ), // Оставшаяся часть расходников pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-3`, pvzReturnsQuantity: 0, // Нет реальных данных о возвратах }, ] : [], }; } ); // Подсчитываем реальные суммы на основе товаров партнера const totalProducts = items.reduce( (sum, item) => sum + item.productQuantity, 0 ); const totalGoods = items.reduce( (sum, item) => sum + item.goodsQuantity, 0 ); const totalDefects = items.reduce( (sum, item) => sum + item.defectsQuantity, 0 ); // Используем реальные данные из товаров для расходников селлера const totalSellerSupplies = items.reduce( (sum, item) => sum + item.sellerSuppliesQuantity, 0 ); const totalPvzReturns = items.reduce( (sum, item) => sum + item.pvzReturnsQuantity, 0 ); // Логирование общих сумм виртуального партнера const partnerName = sellerPartners[index] ? sellerPartners[index].name || sellerPartners[index].fullName || `Селлер ${index + 1}` : `Склад ${index + 1}`; console.log(`🏪 Партнер "${partnerName}":`, { totalProducts, totalGoods, totalDefects, totalSellerSupplies, totalPvzReturns, itemsCount: items.length, itemsWithSupplies: items.filter( (item) => item.sellerSuppliesQuantity > 0 ).length, productNames: items.map((item) => item.name), hasRealPartner: !!sellerPartners[index], }); // Рассчитываем изменения расходников для этого партнера // Распределяем общие поступления пропорционально количеству расходников партнера const totalVirtualPartners = Math.max( 1, Math.min( sellerPartners.length, Math.ceil(uniqueProductNames.length / 8) ) ); // Нет данных об изменениях продуктов для этого партнера const partnerProductsChange = 0; // Реальные изменения расходников селлера для этого партнера const partnerSuppliesChange = totalSellerSupplies > 0 ? Math.floor( (totalSellerSupplies / (mySupplies.reduce( (sum: number, supply: any) => sum + (supply.currentStock || 0), 0 ) || 1)) * (suppliesReceivedToday - suppliesUsedToday) ) : Math.floor( (suppliesReceivedToday - suppliesUsedToday) / totalVirtualPartners ); return { id: `virtual-partner-${index + 1}`, name: sellerPartners[index] ? sellerPartners[index].name || sellerPartners[index].fullName || `Селлер ${index + 1}` : `Склад ${index + 1}`, // Только если нет реального партнера avatar: sellerPartners[index]?.users?.[0]?.avatar || `https://images.unsplash.com/photo-15312974840${ index + 1 }?w=100&h=100&fit=crop&crop=face`, products: totalProducts, // Реальная сумма товаров goods: totalGoods, // Реальная сумма готовых к отправке defects: totalDefects, // Реальная сумма брака sellerSupplies: totalSellerSupplies, // Реальная сумма расходников селлера pvzReturns: totalPvzReturns, // Реальная сумма возвратов productsChange: partnerProductsChange, // Реальные изменения товаров goodsChange: 0, // Нет реальных данных о готовых товарах defectsChange: 0, // Нет реальных данных о браке sellerSuppliesChange: partnerSuppliesChange, // Реальные изменения расходников pvzReturnsChange: 0, // Нет реальных данных о возвратах items, }; }); }, [sellerPartners, allProducts, mySupplies, suppliesReceivedToday]); // Функции для аватаров магазинов const getInitials = (name: string): string => { return name .split(" ") .map((word) => word.charAt(0)) .join("") .toUpperCase() .slice(0, 2); }; const getColorForStore = (storeId: string): string => { const colors = [ "bg-blue-500", "bg-green-500", "bg-purple-500", "bg-orange-500", "bg-pink-500", "bg-indigo-500", "bg-teal-500", "bg-red-500", "bg-yellow-500", "bg-cyan-500", ]; const hash = storeId .split("") .reduce((acc, char) => acc + char.charCodeAt(0), 0); return colors[hash % colors.length]; }; // Уникальные цветовые схемы для каждого магазина const getColorScheme = (storeId: string) => { const colorSchemes = { "1": { // Первый поставщик - Синий bg: "bg-blue-500/5", border: "border-blue-500/30", borderLeft: "border-l-blue-400", text: "text-blue-100", indicator: "bg-blue-400 border-blue-300", hover: "hover:bg-blue-500/10", header: "bg-blue-500/20 border-blue-500/40", }, "2": { // Второй поставщик - Розовый bg: "bg-pink-500/5", border: "border-pink-500/30", borderLeft: "border-l-pink-400", text: "text-pink-100", indicator: "bg-pink-400 border-pink-300", hover: "hover:bg-pink-500/10", header: "bg-pink-500/20 border-pink-500/40", }, "3": { // Третий поставщик - Зеленый bg: "bg-emerald-500/5", border: "border-emerald-500/30", borderLeft: "border-l-emerald-400", text: "text-emerald-100", indicator: "bg-emerald-400 border-emerald-300", hover: "hover:bg-emerald-500/10", header: "bg-emerald-500/20 border-emerald-500/40", }, "4": { // Четвертый поставщик - Фиолетовый bg: "bg-purple-500/5", border: "border-purple-500/30", borderLeft: "border-l-purple-400", text: "text-purple-100", indicator: "bg-purple-400 border-purple-300", hover: "hover:bg-purple-500/10", header: "bg-purple-500/20 border-purple-500/40", }, "5": { // Пятый поставщик - Оранжевый bg: "bg-orange-500/5", border: "border-orange-500/30", borderLeft: "border-l-orange-400", text: "text-orange-100", indicator: "bg-orange-400 border-orange-300", hover: "hover:bg-orange-500/10", header: "bg-orange-500/20 border-orange-500/40", }, "6": { // Шестой поставщик - Индиго bg: "bg-indigo-500/5", border: "border-indigo-500/30", borderLeft: "border-l-indigo-400", text: "text-indigo-100", indicator: "bg-indigo-400 border-indigo-300", hover: "hover:bg-indigo-500/10", header: "bg-indigo-500/20 border-indigo-500/40", }, }; // Если у нас больше поставщиков чем цветовых схем, используем циклический выбор const schemeKeys = Object.keys(colorSchemes); const schemeIndex = (parseInt(storeId) - 1) % schemeKeys.length; const selectedKey = schemeKeys[schemeIndex] || "1"; return ( colorSchemes[selectedKey as keyof typeof colorSchemes] || colorSchemes["1"] ); }; // Фильтрация и сортировка данных const filteredAndSortedStores = useMemo(() => { console.log("🔍 Фильтрация поставщиков:", { storeDataLength: storeData.length, searchTerm, sortField, sortOrder, }); const filtered = storeData.filter((store) => store.name.toLowerCase().includes(searchTerm.toLowerCase()) ); console.log("📋 Отфильтрованные поставщики:", { filteredLength: filtered.length, storeNames: filtered.map((s) => s.name), }); filtered.sort((a, b) => { const aValue = a[sortField]; const bValue = b[sortField]; if (typeof aValue === "string" && typeof bValue === "string") { return sortOrder === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } if (typeof aValue === "number" && typeof bValue === "number") { return sortOrder === "asc" ? aValue - bValue : bValue - aValue; } return 0; }); return filtered; }, [searchTerm, sortField, sortOrder, storeData]); // Подсчет общих сумм const totals = useMemo(() => { return filteredAndSortedStores.reduce( (acc, store) => ({ products: acc.products + store.products, goods: acc.goods + store.goods, defects: acc.defects + store.defects, sellerSupplies: acc.sellerSupplies + store.sellerSupplies, pvzReturns: acc.pvzReturns + store.pvzReturns, productsChange: acc.productsChange + store.productsChange, goodsChange: acc.goodsChange + store.goodsChange, defectsChange: acc.defectsChange + store.defectsChange, sellerSuppliesChange: acc.sellerSuppliesChange + store.sellerSuppliesChange, pvzReturnsChange: acc.pvzReturnsChange + store.pvzReturnsChange, }), { products: 0, goods: 0, defects: 0, sellerSupplies: 0, pvzReturns: 0, productsChange: 0, goodsChange: 0, defectsChange: 0, sellerSuppliesChange: 0, pvzReturnsChange: 0, } ); }, [filteredAndSortedStores]); const formatNumber = (num: number) => { return num.toLocaleString("ru-RU"); }; const formatChange = (change: number) => { const sign = change > 0 ? "+" : ""; return `${sign}${change}`; }; const toggleStoreExpansion = (storeId: string) => { const newExpanded = new Set(expandedStores); if (newExpanded.has(storeId)) { newExpanded.delete(storeId); } else { newExpanded.add(storeId); } setExpandedStores(newExpanded); }; const toggleItemExpansion = (itemId: string) => { const newExpanded = new Set(expandedItems); if (newExpanded.has(itemId)) { newExpanded.delete(itemId); } else { newExpanded.add(itemId); } setExpandedItems(newExpanded); }; const handleSort = (field: keyof StoreData) => { if (sortField === field) { setSortOrder(sortOrder === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortOrder("asc"); } }; // Компонент компактной статистической карточки const StatCard = ({ title, icon: Icon, current, change, percentChange, description, onClick, }: { title: string; icon: React.ComponentType<{ className?: string }>; current: number; change: number; percentChange?: number; description: string; onClick?: () => void; }) => { // Используем percentChange из GraphQL, если доступно, иначе вычисляем локально const displayPercentChange = percentChange !== undefined && percentChange !== null && !isNaN(percentChange) ? percentChange : (current > 0 ? (change / current) * 100 : 0); return (
{title}
{/* Процентное изменение - всегда показываем */}
{change >= 0 ? ( ) : ( )} = 0 ? "text-green-400" : "text-red-400" }`} > {displayPercentChange.toFixed(1)}%
{formatNumber(current)}
{/* Изменения - всегда показываем */}
= 0 ? "bg-green-500/20" : "bg-red-500/20" }`} > = 0 ? "text-green-400" : "text-red-400" }`} > {change >= 0 ? "+" : ""} {change}
{description}
{onClick && (
)}
); }; // Компонент заголовка таблицы const TableHeader = ({ field, children, sortable = false, }: { field?: keyof StoreData; children: React.ReactNode; sortable?: boolean; }) => (
handleSort(field) : undefined} > {children} {sortable && field && ( )} {field === "pvzReturns" && ( )}
); // Индикатор загрузки if ( counterpartiesLoading || ordersLoading || productsLoading || suppliesLoading ) { return (
Загрузка данных склада...
); } // Индикатор ошибки if (counterpartiesError || ordersError || productsError) { return (

Ошибка загрузки данных склада

{counterpartiesError?.message || ordersError?.message || productsError?.message}

); } return (
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}

Статистика склада

{/* Индикатор обновления данных */}
Обновлено из поставок {supplyOrders.filter((o) => o.status === "DELIVERED").length > 0 && ( { supplyOrders.filter((o) => o.status === "DELIVERED") .length }{" "} поставок получено )}
router.push("/fulfillment-warehouse/supplies")} />
{/* Основная скроллируемая часть - оставшиеся 70% экрана */}
{/* Компактная шапка таблицы - максимум 10% экрана */}

Детализация по Магазинам
Магазины
Товары

{/* Компактный поиск */}
setSearchTerm(e.target.value)} className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40 flex-1" />
{filteredAndSortedStores.length} магазинов
{/* Фиксированные заголовки таблицы - Уровень 1 (Поставщики) */}
№ / Магазин Продукты Товары Брак Расходники селлера Возвраты с ПВЗ
{/* Строка с суммами - Уровень 1 (Поставщики) */}
ИТОГО ({filteredAndSortedStores.length})
{formatNumber(totals.products)}
{totals.productsChange >= 0 ? ( ) : ( )} = 0 ? "text-green-400" : "text-red-400" }`} > {totals.products > 0 ? ( (totals.productsChange / totals.products) * 100 ).toFixed(1) : "0.0"} %
{showAdditionalValues && (
+0 {/* ТЕСТ: Временно захардкожено для проверки */}
-0 {/* ТЕСТ: Временно захардкожено для проверки */}
{Math.abs(totals.productsChange)}
)}
{formatNumber(totals.goods)}
{totals.goodsChange >= 0 ? ( ) : ( )} = 0 ? "text-green-400" : "text-red-400" }`} > {totals.goods > 0 ? ((totals.goodsChange / totals.goods) * 100).toFixed( 1 ) : "0.0"} %
{showAdditionalValues && (
+0 {/* Нет реальных данных о готовых товарах */}
-0 {/* Нет реальных данных о готовых товарах */}
{Math.abs(totals.goodsChange)}
)}
{formatNumber(totals.defects)}
{totals.defectsChange >= 0 ? ( ) : ( )} = 0 ? "text-green-400" : "text-red-400" }`} > {totals.defects > 0 ? ( (totals.defectsChange / totals.defects) * 100 ).toFixed(1) : "0.0"} %
{showAdditionalValues && (
+0 {/* Нет реальных данных о браке */}
-0 {/* Нет реальных данных о браке */}
{Math.abs(totals.defectsChange)}
)}
{formatNumber(totals.sellerSupplies)}
{totals.sellerSuppliesChange >= 0 ? ( ) : ( )} = 0 ? "text-green-400" : "text-red-400" }`} > {totals.sellerSupplies > 0 ? ( (totals.sellerSuppliesChange / totals.sellerSupplies) * 100 ).toFixed(1) : "0.0"} %
{showAdditionalValues && (
+{Math.max(totals.sellerSuppliesChange, 0)}
-{Math.max(-totals.sellerSuppliesChange, 0)}
{Math.abs(totals.sellerSuppliesChange)}
)}
{formatNumber(totals.pvzReturns)}
{totals.pvzReturnsChange >= 0 ? ( ) : ( )} = 0 ? "text-green-400" : "text-red-400" }`} > {totals.pvzReturns > 0 ? ( (totals.pvzReturnsChange / totals.pvzReturns) * 100 ).toFixed(1) : "0.0"} %
{showAdditionalValues && (
+0 {/* Нет реальных данных о возвратах с ПВЗ */}
-0 {/* Нет реальных данных о возвратах с ПВЗ */}
{Math.abs(totals.pvzReturnsChange)}
)}
{/* Скроллируемый контент таблицы - оставшееся пространство */}
{filteredAndSortedStores.length === 0 ? (

{sellerPartners.length === 0 ? "Нет магазинов" : allProducts.length === 0 ? "Нет товаров на складе" : "Магазины не найдены"}

{sellerPartners.length === 0 ? "Добавьте магазины для отображения данных склада" : allProducts.length === 0 ? "Добавьте товары на склад для отображения данных" : searchTerm ? "Попробуйте изменить поисковый запрос" : "Данные о магазинах будут отображены здесь"}

) : ( filteredAndSortedStores.map((store, index) => { const colorScheme = getColorScheme(store.id); return (
{/* Основная строка поставщика */}
toggleStoreExpansion(store.id)} >
{filteredAndSortedStores.length - index}
{store.avatar && ( )} {getInitials(store.name)}
{store.name}
{formatNumber(store.products)}
{showAdditionalValues && (
+{Math.max(0, store.productsChange)}{" "} {/* Поступило товаров */}
-{Math.max(0, -store.productsChange)}{" "} {/* Использовано товаров */}
{Math.abs(store.productsChange)}
)}
{formatNumber(store.goods)}
{showAdditionalValues && (
+0{" "} {/* Нет реальных данных о готовых товарах */}
-0{" "} {/* Нет реальных данных о готовых товарах */}
{Math.abs(store.goodsChange)}
)}
{formatNumber(store.defects)}
{showAdditionalValues && (
+0 {/* Нет реальных данных о браке */}
-0 {/* Нет реальных данных о браке */}
{Math.abs(store.defectsChange)}
)}
{formatNumber(store.sellerSupplies)}
{showAdditionalValues && (
+{Math.max(0, store.sellerSuppliesChange)}{" "} {/* Поступило расходников */}
-{Math.max(0, -store.sellerSuppliesChange)}{" "} {/* Использовано расходников */}
{Math.abs(store.sellerSuppliesChange)}
)}
{formatNumber(store.pvzReturns)}
{showAdditionalValues && (
+0{" "} {/* Нет реальных данных о возвратах с ПВЗ */}
-0{" "} {/* Нет реальных данных о возвратах с ПВЗ */}
{Math.abs(store.pvzReturnsChange)}
)}
{/* Второй уровень - детализация по товарам */} {expandedStores.has(store.id) && (
{/* Статическая часть - заголовки столбцов второго уровня */}
Наименование
Кол-во
Место
Кол-во
Место
Кол-во
Место
Кол-во
Место
Кол-во
Место
{/* Динамическая часть - данные по товарам (скроллируемая) */}
{store.items?.map((item) => (
{/* Основная строка товара */}
toggleItemExpansion(item.id)} >
{/* Наименование */}
{item.name} {item.variants && item.variants.length > 0 && ( {item.variants.length} вар. )}
{item.article}
{/* Продукты */}
{formatNumber(item.productQuantity)}
{item.productPlace || "-"}
{/* Товары */}
{formatNumber(item.goodsQuantity)}
{item.goodsPlace || "-"}
{/* Брак */}
{formatNumber(item.defectsQuantity)}
{item.defectsPlace || "-"}
{/* Расходники селлера */}
{formatNumber( item.sellerSuppliesQuantity )}
{item.sellerSuppliesPlace || "-"}
{/* Возвраты с ПВЗ */}
{formatNumber(item.pvzReturnsQuantity)}
{item.pvzReturnsPlace || "-"}
{/* Третий уровень - варианты товара */} {expandedItems.has(item.id) && item.variants && item.variants.length > 0 && (
{/* Заголовки для вариантов */}
Вариант
Кол-во
Место
Кол-во
Место
Кол-во
Место
Кол-во
Место
Кол-во
Место
{/* Данные по вариантам */}
{item.variants.map((variant) => (
{/* Название варианта */}
{variant.name}
{/* Продукты */}
{formatNumber( variant.productQuantity )}
{variant.productPlace || "-"}
{/* Товары */}
{formatNumber( variant.goodsQuantity )}
{variant.goodsPlace || "-"}
{/* Брак */}
{formatNumber( variant.defectsQuantity )}
{variant.defectsPlace || "-"}
{/* Расходники селлера */}
{formatNumber( variant.sellerSuppliesQuantity )}
{variant.sellerSuppliesPlace || "-"}
{/* Возвраты с ПВЗ */}
{formatNumber( variant.pvzReturnsQuantity )}
{variant.pvzReturnsPlace || "-"}
))}
)}
))}
)}
); }) )}
); }