Files
sfera-new/src/components/wb-warehouse/wb-warehouse-dashboard-refactored.tsx

394 lines
14 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 React, { useState, useEffect } from "react";
import { useAuth } from "@/hooks/useAuth";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { WildberriesService } from "@/services/wildberries-service";
import { toast } from "sonner";
import { StatsCards } from "./stats-cards";
import { SearchBar } from "./search-bar";
import { TableHeader } from "./table-header";
import { LoadingSkeleton } from "./loading-skeleton";
import { StockTableRow } from "./stock-table-row";
import { TrendingUp, Package } from "lucide-react";
interface WBStock {
nmId: number;
vendorCode: string;
title: string;
brand: string;
price: number;
stocks: Array<{
warehouseId: number;
warehouseName: string;
quantity: number;
quantityFull: number;
inWayToClient: number;
inWayFromClient: number;
}>;
totalQuantity: number;
totalReserved: number;
photos?: Array<{
big?: string;
c246x328?: string;
c516x688?: string;
square?: string;
tm?: string;
}>;
mediaFiles?: string[];
characteristics?: Array<{
name: string;
value: string | string[];
}>;
subjectName: string;
description: string;
}
interface WBWarehouse {
id: number;
name: string;
cargoType: number;
deliveryType: number;
}
export function WBWarehouseDashboard() {
const { user } = useAuth();
const { isCollapsed, getSidebarMargin } = useSidebar();
const [stocks, setStocks] = useState<WBStock[]>([]);
const [warehouses, setWarehouses] = useState<WBWarehouse[]>([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
// Статистика
const [totalProducts, setTotalProducts] = useState(0);
const [totalStocks, setTotalStocks] = useState(0);
const [totalReserved, setTotalReserved] = useState(0);
const [totalFromClient, setTotalFromClient] = useState(0);
const [activeWarehouses, setActiveWarehouses] = useState(0);
// Analytics data
const [analyticsData, setAnalyticsData] = useState<unknown[]>([]);
const hasWBApiKey = user?.wildberriesApiKey;
// Комбинирование карточек с индивидуальными данными аналитики
const combineCardsWithAnalytics = (
cards: unknown[],
analyticsResults: unknown[]
) => {
const stocksMap = new Map<number, WBStock>();
// Создаем карту аналитических данных для быстрого поиска
const analyticsMap = new Map(); // Map nmId to its analytics data
analyticsResults.forEach((result) => {
analyticsMap.set(result.nmId, result.data);
});
cards.forEach((card) => {
const stock: WBStock = {
nmId: card.nmID,
vendorCode: String(card.vendorCode || card.supplierVendorCode || ""),
title: String(card.title || card.object || `Товар ${card.nmID}`),
brand: String(card.brand || ""),
price: 0,
stocks: [],
totalQuantity: 0,
totalReserved: 0,
photos: Array.isArray(card.photos) ? card.photos : [],
mediaFiles: Array.isArray(card.mediaFiles) ? card.mediaFiles : [],
characteristics: Array.isArray(card.characteristics)
? card.characteristics
: [],
subjectName: String(card.subjectName || card.object || ""),
description: String(card.description || ""),
};
if (card.sizes && card.sizes.length > 0) {
stock.price =
Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0;
}
const analyticsData = analyticsMap.get(card.nmID);
if (analyticsData?.data?.regions) {
(
analyticsData.data.regions as {
offices: {
officeID: number;
officeName: string;
metrics: { stockCount: number };
}[];
}[]
).forEach((region) => {
if (region.offices && region.offices.length > 0) {
region.offices.forEach((office) => {
stock.stocks.push({
warehouseId: office.officeID,
warehouseName: office.officeName,
quantity: office.metrics?.stockCount || 0,
quantityFull: office.metrics?.stockCount || 0,
inWayToClient: office.metrics?.toClientCount || 0,
inWayFromClient: office.metrics?.fromClientCount || 0,
});
stock.totalQuantity += office.metrics?.stockCount || 0;
stock.totalReserved += office.metrics?.toClientCount || 0;
});
}
});
}
stocksMap.set(card.nmID, stock);
});
return Array.from(stocksMap.values()).sort(
(a, b) => b.totalQuantity - a.totalQuantity
);
};
// Извлечение информации о складах из данных
const extractWarehousesFromStocks = (
stocksData: WBStock[]
): WBWarehouse[] => {
const warehousesMap = new Map<number, WBWarehouse>();
stocksData.forEach((stock) => {
stock.stocks.forEach((stockInfo) => {
if (!warehousesMap.has(stockInfo.warehouseId)) {
warehousesMap.set(stockInfo.warehouseId, {
id: stockInfo.warehouseId,
name: stockInfo.warehouseName,
cargoType: 1,
deliveryType: 1,
});
}
});
});
return Array.from(warehousesMap.values());
};
// Обновление статистики
const updateStatistics = (
stocksData: WBStock[],
warehousesData: WBWarehouse[]
) => {
setTotalProducts(stocksData.length);
setTotalStocks(
stocksData.reduce((sum, item) => sum + item.totalQuantity, 0)
);
setTotalReserved(
stocksData.reduce((sum, item) => sum + item.totalReserved, 0)
);
const totalFromClientCount = stocksData.reduce(
(sum, item) =>
sum +
item.stocks.reduce(
(stockSum, stock) => stockSum + stock.inWayFromClient,
0
),
0
);
setTotalFromClient(totalFromClientCount);
const warehousesWithStock = new Set(
stocksData.flatMap((item) => item.stocks.map((s) => s.warehouseId))
);
setActiveWarehouses(warehousesWithStock.size);
};
// Загрузка данных склада
const loadWarehouseData = async () => {
if (!user?.wildberriesApiKey) return;
setLoading(true);
try {
const apiToken = user.wildberriesApiKey;
const wbService = new WildberriesService(apiToken);
// 1. Получаем карточки товаров
const cards = await WildberriesService.getAllCards(apiToken).catch(
() => []
);
console.log("WB Warehouse: Loaded cards:", cards.length);
if (cards.length === 0) {
toast.error("Нет карточек товаров в WB");
return;
}
const nmIds = cards.map((card) => card.nmID).filter((id) => id > 0);
console.log("WB Warehouse: NM IDs to process:", nmIds.length);
// 2. Получаем аналитику для каждого товара индивидуально
const analyticsResults = [];
for (const nmId of nmIds) {
try {
console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`);
const result = await wbService.getStocksReportByOffices({
nmIDs: [nmId],
stockType: "",
});
analyticsResults.push({ nmId, data: result });
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
console.error(
`WB Warehouse: Error fetching analytics for nmId ${nmId}:`,
error
);
}
}
console.log("WB Warehouse: Analytics results:", analyticsResults.length);
// 3. Комбинируем данные
const combinedStocks = combineCardsWithAnalytics(cards, analyticsResults);
console.log("WB Warehouse: Combined stocks:", combinedStocks.length);
// 4. Извлекаем склады и обновляем статистику
const extractedWarehouses = extractWarehousesFromStocks(combinedStocks);
setStocks(combinedStocks);
setWarehouses(extractedWarehouses);
updateStatistics(combinedStocks, extractedWarehouses);
toast.success(`Загружено товаров: ${combinedStocks.length}`);
} catch (error: unknown) {
console.error("Error loading warehouse data:", error);
toast.error("Ошибка при загрузке данных склада");
} finally {
setLoading(false);
}
};
useEffect(() => {
if (hasWBApiKey) {
loadWarehouseData();
}
}, [hasWBApiKey]);
// Фильтрация товаров
const filteredStocks = stocks.filter((item) => {
if (!searchTerm) return true;
const search = searchTerm.toLowerCase();
return (
item.title.toLowerCase().includes(search) ||
String(item.nmId).includes(search) ||
item.brand.toLowerCase().includes(search) ||
item.vendorCode.toLowerCase().includes(search)
);
});
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<div className="h-full w-full flex flex-col">
{/* Результирующие вкладки */}
<StatsCards
totalProducts={totalProducts}
totalStocks={totalStocks}
totalReserved={totalReserved}
totalFromClient={totalFromClient}
activeWarehouses={activeWarehouses}
loading={loading}
/>
{/* Аналитика по складам WB */}
{analyticsData.length > 0 && (
<Card className="glass-card border-white/10 p-4 mb-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<TrendingUp className="h-5 w-5 mr-2 text-blue-400" />
Движение товаров по складам WB
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{analyticsData.map((warehouse) => (
<Card
key={warehouse.warehouseId}
className="bg-white/5 border-white/10 p-3"
>
<div className="text-sm font-medium text-white mb-2">
{warehouse.warehouseName}
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span className="text-white/60">К клиенту:</span>
<span className="text-green-400 font-medium">
{warehouse.toClient}
</span>
</div>
<div className="flex justify-between text-xs">
<span className="text-white/60">От клиента:</span>
<span className="text-orange-400 font-medium">
{warehouse.fromClient}
</span>
</div>
</div>
</Card>
))}
</div>
</Card>
)}
{/* Поиск */}
<SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
{/* Список товаров */}
<div className="flex-1 overflow-hidden">
{loading ? (
<div className="overflow-y-auto pr-2 max-h-full">
<TableHeader />
<LoadingSkeleton />
</div>
) : !hasWBApiKey ? (
<Card className="glass-card border-white/10 p-8 text-center">
<Package className="h-12 w-12 text-blue-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">
Настройте API Wildberries
</h3>
<p className="text-white/60 mb-4">
Для просмотра остатков добавьте API ключ Wildberries в
настройках
</p>
<Button
onClick={() => (window.location.href = "/settings")}
className="bg-blue-600 hover:bg-blue-700"
>
Перейти в настройки
</Button>
</Card>
) : filteredStocks.length === 0 ? (
<Card className="glass-card border-white/10 p-8 text-center">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">
Товары не найдены
</h3>
<p className="text-white/60">
Попробуйте изменить параметры поиска
</p>
</Card>
) : (
<div className="overflow-y-auto pr-2 max-h-full">
<TableHeader />
{/* Таблица товаров */}
<div className="space-y-1">
{filteredStocks.map((item, index) => (
<StockTableRow key={`${item.nmId}-${index}`} item={item} />
))}
</div>
</div>
)}
</div>
</div>
</main>
</div>
);
}