Files
sfera/src/components/fulfillment-warehouse/fulfillment-warehouse-dashboard.tsx
Bivekich 8b66793ae7 feat: Implement real-time warehouse statistics with GraphQL resolver
- Added fulfillmentWarehouseStats GraphQL query and resolver
- Updated StatCard component to use percentChange from GraphQL
- Added comprehensive logging for debugging warehouse statistics
- Implemented 24-hour change tracking for warehouse metrics
- Added polling for real-time statistics updates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-31 16:05:57 +03:00

2113 lines
96 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<keyof StoreData>("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [expandedStores, setExpandedStores] = useState<Set<string>>(new Set());
const [expandedItems, setExpandedItems] = useState<Set<string>>(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<string, number>();
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 (
<div
className={`glass-card p-3 hover:bg-white/15 transition-all duration-300 relative overflow-hidden ${
onClick ? "cursor-pointer hover:scale-105" : ""
}`}
onClick={onClick}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-white/10 rounded-lg">
<Icon className="h-3 w-3 text-white" />
</div>
<span className="text-white text-xs font-semibold">{title}</span>
</div>
{/* Процентное изменение - всегда показываем */}
<div className="flex items-center space-x-0.5 px-1.5 py-0.5 rounded bg-blue-500/20">
{change >= 0 ? (
<TrendingUp className="h-3 w-3 text-green-400" />
) : (
<TrendingDown className="h-3 w-3 text-red-400" />
)}
<span
className={`text-xs font-bold ${
change >= 0 ? "text-green-400" : "text-red-400"
}`}
>
{displayPercentChange.toFixed(1)}%
</span>
</div>
</div>
<div className="flex items-center justify-between mb-1">
<div className="text-lg font-bold text-white">
{formatNumber(current)}
</div>
{/* Изменения - всегда показываем */}
<div className="flex items-center space-x-1">
<div
className={`flex items-center space-x-0.5 px-1 py-0.5 rounded ${
change >= 0 ? "bg-green-500/20" : "bg-red-500/20"
}`}
>
<span
className={`text-xs font-bold ${
change >= 0 ? "text-green-400" : "text-red-400"
}`}
>
{change >= 0 ? "+" : ""}
{change}
</span>
</div>
</div>
</div>
<div className="text-white/60 text-[10px]">{description}</div>
{onClick && (
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<ChevronRight className="h-3 w-3 text-white/60" />
</div>
)}
</div>
);
};
// Компонент заголовка таблицы
const TableHeader = ({
field,
children,
sortable = false,
}: {
field?: keyof StoreData;
children: React.ReactNode;
sortable?: boolean;
}) => (
<div
className={`px-3 py-2 text-left text-xs font-medium text-blue-100 uppercase tracking-wider ${
sortable ? "cursor-pointer hover:text-white hover:bg-blue-500/10" : ""
} flex items-center space-x-1`}
onClick={sortable && field ? () => handleSort(field) : undefined}
>
<span>{children}</span>
{sortable && field && (
<ArrowUpDown
className={`h-3 w-3 ${
sortField === field ? "text-blue-400" : "text-white/40"
}`}
/>
)}
{field === "pvzReturns" && (
<button
onClick={(e) => {
e.stopPropagation();
setShowAdditionalValues(!showAdditionalValues);
}}
className="p-1 rounded hover:bg-orange-500/20 transition-colors border border-orange-500/30 bg-orange-500/10 ml-2"
title={
showAdditionalValues
? "Скрыть дополнительные значения"
: "Показать дополнительные значения"
}
>
{showAdditionalValues ? (
<Eye className="h-3 w-3 text-orange-400 hover:text-orange-300" />
) : (
<EyeOff className="h-3 w-3 text-orange-400 hover:text-orange-300" />
)}
</button>
)}
</div>
);
// Индикатор загрузки
if (
counterpartiesLoading ||
ordersLoading ||
productsLoading ||
suppliesLoading
) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}
>
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
<span className="text-white/60">Загрузка данных склада...</span>
</div>
</main>
</div>
);
}
// Индикатор ошибки
if (counterpartiesError || ordersError || productsError) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex items-center justify-center`}
>
<div className="text-center">
<AlertTriangle className="h-12 w-12 text-red-400 mx-auto mb-4" />
<p className="text-red-400 font-medium">
Ошибка загрузки данных склада
</p>
<p className="text-white/60 text-sm mt-2">
{counterpartiesError?.message ||
ordersError?.message ||
productsError?.message}
</p>
</div>
</main>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300`}
>
{/* Компактная статичная верхняя секция со статистикой - максимум 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<div className="glass-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-base font-semibold text-blue-400">
Статистика склада
</h2>
{/* Индикатор обновления данных */}
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 text-xs text-white/60">
<Clock className="h-3 w-3" />
<span>Обновлено из поставок</span>
{supplyOrders.filter((o) => o.status === "DELIVERED").length >
0 && (
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 text-xs">
{
supplyOrders.filter((o) => o.status === "DELIVERED")
.length
}{" "}
поставок получено
</Badge>
)}
</div>
<Button
variant="outline"
size="sm"
className="h-7 text-xs bg-white/10 border-white/20 text-white hover:bg-white/20"
onClick={() => {
refetchCounterparties();
refetchOrders();
refetchProducts();
refetchSupplies(); // Добавляем обновление расходников
toast.success("Данные склада обновлены");
}}
disabled={
counterpartiesLoading ||
ordersLoading ||
productsLoading ||
suppliesLoading
}
>
<RotateCcw className="h-3 w-3 mr-1" />
Обновить
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<StatCard
title="Продукты"
icon={Box}
current={warehouseStats.products.current}
change={warehouseStats.products.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.products?.percentChange}
description="Готовые к отправке"
/>
<StatCard
title="Товары"
icon={Package}
current={warehouseStats.goods.current}
change={warehouseStats.goods.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.goods?.percentChange}
description="В обработке"
/>
<StatCard
title="Брак"
icon={AlertTriangle}
current={warehouseStats.defects.current}
change={warehouseStats.defects.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.defects?.percentChange}
description="Требует утилизации"
/>
<StatCard
title="Возвраты с ПВЗ"
icon={RotateCcw}
current={warehouseStats.pvzReturns.current}
change={warehouseStats.pvzReturns.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.pvzReturns?.percentChange}
description="К обработке"
/>
<StatCard
title="Расходники фулфилмента"
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.fulfillmentSupplies?.percentChange}
description="Расходники, этикетки"
onClick={() => router.push("/fulfillment-warehouse/supplies")}
/>
<StatCard
title="Расходники селлеров"
icon={Users}
current={warehouseStats.sellerSupplies.current}
change={warehouseStats.sellerSupplies.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.sellerSupplies?.percentChange}
description="Материалы клиентов"
/>
</div>
</div>
</div>
{/* Основная скроллируемая часть - оставшиеся 70% экрана */}
<div
className="flex-1 flex flex-col overflow-hidden"
style={{ minHeight: "60vh" }}
>
<div className="glass-card flex-1 flex flex-col overflow-hidden">
{/* Компактная шапка таблицы - максимум 10% экрана */}
<div
className="p-4 border-b border-white/10 flex-shrink-0"
style={{ maxHeight: "10vh" }}
>
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-white flex items-center space-x-2">
<Store className="h-4 w-4 text-blue-400" />
<span>Детализация по Магазинам</span>
<div className="flex items-center space-x-1 text-xs text-white/60">
<div className="flex items-center space-x-1">
<div className="flex space-x-0.5">
<div className="w-2 h-2 bg-blue-400 rounded"></div>
<div className="w-2 h-2 bg-pink-400 rounded"></div>
<div className="w-2 h-2 bg-emerald-400 rounded"></div>
</div>
<span>Магазины</span>
</div>
<ChevronRight className="h-3 w-3" />
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-500 rounded"></div>
<span>Товары</span>
</div>
</div>
</h2>
{/* Компактный поиск */}
<div className="relative mx-2.5 flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
<div className="flex space-x-2">
<Input
placeholder="Поиск по магазинам..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40 flex-1"
/>
<Button
size="sm"
className="h-8 px-2 bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 border border-blue-500/30 text-xs"
>
Поиск
</Button>
</div>
</div>
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs"
>
{filteredAndSortedStores.length} магазинов
</Badge>
</div>
</div>
{/* Фиксированные заголовки таблицы - Уровень 1 (Поставщики) */}
<div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
<div className="grid grid-cols-6 gap-0">
<TableHeader field="name" sortable>
/ Магазин
</TableHeader>
<TableHeader field="products" sortable>
Продукты
</TableHeader>
<TableHeader field="goods" sortable>
Товары
</TableHeader>
<TableHeader field="defects" sortable>
Брак
</TableHeader>
<TableHeader field="sellerSupplies" sortable>
Расходники селлера
</TableHeader>
<TableHeader field="pvzReturns" sortable>
Возвраты с ПВЗ
</TableHeader>
</div>
</div>
{/* Строка с суммами - Уровень 1 (Поставщики) */}
<div className="flex-shrink-0 bg-blue-500/25 border-b border-blue-500/50">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300">
ИТОГО ({filteredAndSortedStores.length})
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.products)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.productsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.productsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{totals.products > 0
? (
(totals.productsChange / totals.products) *
100
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0 {/* ТЕСТ: Временно захардкожено для проверки */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0 {/* ТЕСТ: Временно захардкожено для проверки */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.productsChange)}
</span>
</div>
</div>
)}
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.goods)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.goodsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.goodsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{totals.goods > 0
? ((totals.goodsChange / totals.goods) * 100).toFixed(
1
)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0 {/* Нет реальных данных о готовых товарах */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0 {/* Нет реальных данных о готовых товарах */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.goodsChange)}
</span>
</div>
</div>
)}
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.defects)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.defectsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.defectsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{totals.defects > 0
? (
(totals.defectsChange / totals.defects) *
100
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0 {/* Нет реальных данных о браке */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0 {/* Нет реальных данных о браке */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.defectsChange)}
</span>
</div>
</div>
)}
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.sellerSupplies)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.sellerSuppliesChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.sellerSuppliesChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{totals.sellerSupplies > 0
? (
(totals.sellerSuppliesChange /
totals.sellerSupplies) *
100
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.max(totals.sellerSuppliesChange, 0)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.max(-totals.sellerSuppliesChange, 0)}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.sellerSuppliesChange)}
</span>
</div>
</div>
)}
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="flex items-center justify-between">
<span>{formatNumber(totals.pvzReturns)}</span>
<div className="flex items-center space-x-0.5 px-1 py-0.5 rounded bg-blue-500/20">
{totals.pvzReturnsChange >= 0 ? (
<TrendingUp className="h-2 w-2 text-green-400" />
) : (
<TrendingDown className="h-2 w-2 text-red-400" />
)}
<span
className={`text-[9px] font-bold ${
totals.pvzReturnsChange >= 0
? "text-green-400"
: "text-red-400"
}`}
>
{totals.pvzReturns > 0
? (
(totals.pvzReturnsChange / totals.pvzReturns) *
100
).toFixed(1)
: "0.0"}
%
</span>
</div>
</div>
{showAdditionalValues && (
<div className="flex items-center justify-end space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0 {/* Нет реальных данных о возвратах с ПВЗ */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0 {/* Нет реальных данных о возвратах с ПВЗ */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(totals.pvzReturnsChange)}
</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Скроллируемый контент таблицы - оставшееся пространство */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent">
{filteredAndSortedStores.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<p className="text-white/60 font-medium">
{sellerPartners.length === 0
? "Нет магазинов"
: allProducts.length === 0
? "Нет товаров на складе"
: "Магазины не найдены"}
</p>
<p className="text-white/40 text-sm mt-2">
{sellerPartners.length === 0
? "Добавьте магазины для отображения данных склада"
: allProducts.length === 0
? "Добавьте товары на склад для отображения данных"
: searchTerm
? "Попробуйте изменить поисковый запрос"
: "Данные о магазинах будут отображены здесь"}
</p>
</div>
</div>
) : (
filteredAndSortedStores.map((store, index) => {
const colorScheme = getColorScheme(store.id);
return (
<div
key={store.id}
className={`border-b ${colorScheme.border} ${colorScheme.hover} transition-colors border-l-8 ${colorScheme.borderLeft} ${colorScheme.bg} shadow-sm hover:shadow-md`}
>
{/* Основная строка поставщика */}
<div
className="grid grid-cols-6 gap-0 cursor-pointer"
onClick={() => toggleStoreExpansion(store.id)}
>
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">
{filteredAndSortedStores.length - index}
</span>
<div className="flex items-center space-x-2">
<Avatar className="w-6 h-6">
{store.avatar && (
<AvatarImage
src={store.avatar}
alt={store.name}
/>
)}
<AvatarFallback
className={`${getColorForStore(
store.id
)} text-white font-medium text-xs`}
>
{getInitials(store.name)}
</AvatarFallback>
</Avatar>
<div>
<div className="text-white font-medium text-xs flex items-center space-x-2">
<div
className={`w-3 h-3 ${colorScheme.indicator} rounded flex-shrink-0 border`}
></div>
<span className={colorScheme.text}>
{store.name}
</span>
</div>
</div>
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.products)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.max(0, store.productsChange)}{" "}
{/* Поступило товаров */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.max(0, -store.productsChange)}{" "}
{/* Использовано товаров */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.productsChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.goods)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0{" "}
{/* Нет реальных данных о готовых товарах */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0{" "}
{/* Нет реальных данных о готовых товарах */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.goodsChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.defects)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0 {/* Нет реальных данных о браке */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0 {/* Нет реальных данных о браке */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.defectsChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.sellerSupplies)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+{Math.max(0, store.sellerSuppliesChange)}{" "}
{/* Поступило расходников */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-{Math.max(0, -store.sellerSuppliesChange)}{" "}
{/* Использовано расходников */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.sellerSuppliesChange)}
</span>
</div>
</div>
)}
</div>
</div>
<div className="px-3 py-2.5">
<div className="flex items-center justify-between">
<div
className={`${colorScheme.text} font-bold text-sm`}
>
{formatNumber(store.pvzReturns)}
</div>
{showAdditionalValues && (
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-green-400">
+0{" "}
{/* Нет реальных данных о возвратах с ПВЗ */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-red-400">
-0{" "}
{/* Нет реальных данных о возвратах с ПВЗ */}
</span>
</div>
<div className="flex items-center space-x-0.5">
<span className="text-[9px] font-bold text-white">
{Math.abs(store.pvzReturnsChange)}
</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Второй уровень - детализация по товарам */}
{expandedStores.has(store.id) && (
<div className="bg-green-500/5 border-t border-green-500/20">
{/* Статическая часть - заголовки столбцов второго уровня */}
<div className="border-b border-green-500/20 bg-green-500/10">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-2 text-xs font-medium text-green-200 uppercase tracking-wider">
Наименование
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-2 text-xs font-medium text-green-200 uppercase tracking-wider text-center">
Место
</div>
</div>
</div>
</div>
{/* Динамическая часть - данные по товарам (скроллируемая) */}
<div className="max-h-64 overflow-y-auto scrollbar-thin scrollbar-thumb-green-500/30 scrollbar-track-transparent">
{store.items?.map((item) => (
<div key={item.id}>
{/* Основная строка товара */}
<div
className="border-b border-green-500/15 hover:bg-green-500/10 transition-colors cursor-pointer border-l-4 border-l-green-500/40 ml-4"
onClick={() => toggleItemExpansion(item.id)}
>
<div className="grid grid-cols-6 gap-0">
{/* Наименование */}
<div className="px-3 py-2 flex items-center">
<div className="flex-1">
<div className="text-white font-medium text-xs flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded flex-shrink-0"></div>
<span>{item.name}</span>
{item.variants &&
item.variants.length > 0 && (
<Badge
variant="secondary"
className="bg-orange-500/20 text-orange-300 text-[9px] px-1 py-0"
>
{item.variants.length} вар.
</Badge>
)}
</div>
<div className="text-white/60 text-[10px]">
{item.article}
</div>
</div>
</div>
{/* Продукты */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.productQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.productPlace || "-"}
</div>
</div>
{/* Товары */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.goodsQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.goodsPlace || "-"}
</div>
</div>
{/* Брак */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.defectsQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.defectsPlace || "-"}
</div>
</div>
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(
item.sellerSuppliesQuantity
)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.sellerSuppliesPlace || "-"}
</div>
</div>
{/* Возвраты с ПВЗ */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(item.pvzReturnsQuantity)}
</div>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.pvzReturnsPlace || "-"}
</div>
</div>
</div>
</div>
{/* Третий уровень - варианты товара */}
{expandedItems.has(item.id) &&
item.variants &&
item.variants.length > 0 && (
<div className="bg-orange-500/5 border-t border-orange-500/20">
{/* Заголовки для вариантов */}
<div className="border-b border-orange-500/20 bg-orange-500/10">
<div className="grid grid-cols-6 gap-0">
<div className="px-3 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider">
Вариант
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Кол-во
</div>
<div className="px-1 py-1.5 text-[10px] font-medium text-orange-200 uppercase tracking-wider text-center">
Место
</div>
</div>
</div>
</div>
{/* Данные по вариантам */}
<div className="max-h-32 overflow-y-auto scrollbar-thin scrollbar-thumb-orange-500/30 scrollbar-track-transparent">
{item.variants.map((variant) => (
<div
key={variant.id}
className="border-b border-orange-500/15 hover:bg-orange-500/10 transition-colors border-l-4 border-l-orange-500/50 ml-8"
>
<div className="grid grid-cols-6 gap-0">
{/* Название варианта */}
<div className="px-3 py-1.5">
<div className="text-white font-medium text-[10px] flex items-center space-x-2">
<div className="w-1.5 h-1.5 bg-orange-500 rounded flex-shrink-0"></div>
<span>{variant.name}</span>
</div>
</div>
{/* Продукты */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.productQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.productPlace || "-"}
</div>
</div>
{/* Товары */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.goodsQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.goodsPlace || "-"}
</div>
</div>
{/* Брак */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.defectsQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.defectsPlace || "-"}
</div>
</div>
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.sellerSuppliesQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.sellerSuppliesPlace ||
"-"}
</div>
</div>
{/* Возвраты с ПВЗ */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.pvzReturnsQuantity
)}
</div>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.pvzReturnsPlace ||
"-"}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
);
})
)}
</div>
</div>
</div>
</main>
</div>
);
}