
- 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>
2113 lines
96 KiB
TypeScript
2113 lines
96 KiB
TypeScript
"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>
|
||
);
|
||
}
|