Добавлен новый компонент для отображения бизнес-процессов в интерфейсе управления. Обновлен компонент UIKitSection для интеграции нового демо и улучшения навигации. Оптимизирована логика отображения данных и улучшена читаемость кода. Исправлены текстовые метки для повышения удобства использования.

This commit is contained in:
Veronika Smirnova
2025-07-27 20:10:39 +03:00
parent f198994400
commit ec28803549
17 changed files with 4304 additions and 1205 deletions

View File

@ -1,261 +1,295 @@
"use client"
"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'
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
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
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[]
big?: string;
c246x328?: string;
c516x688?: string;
square?: string;
tm?: string;
}>;
mediaFiles?: string[];
characteristics?: Array<{
name: string
value: string | string[]
}>
subjectName: string
description: string
name: string;
value: string | string[];
}>;
subjectName: string;
description: string;
}
interface WBWarehouse {
id: number
name: string
cargoType: number
deliveryType: number
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<any[]>([])
const { user } = useAuth();
const { isCollapsed, getSidebarMargin } = useSidebar();
const hasWBApiKey = user?.wildberriesApiKey
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;
// Комбинирование карточек с индивидуальными данными аналитики
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const combineCardsWithIndividualAnalytics = (cards: any[], analyticsResults: any[]): WBStock[] => {
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)
})
const combineCardsWithAnalytics = (
cards: unknown[],
analyticsResults: unknown[]
) => {
const stocksMap = new Map<number, WBStock>();
cards.forEach(card => {
// Создаем карту аналитических данных для быстрого поиска
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 || ''),
vendorCode: String(card.vendorCode || card.supplierVendorCode || ""),
title: String(card.title || card.object || `Товар ${card.nmID}`),
brand: String(card.brand || ''),
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 || '')
}
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
stock.price =
Number(card.sizes[0].price || card.sizes[0].discountedPrice) || 0;
}
const analyticsData = analyticsMap.get(card.nmID)
const analyticsData = analyticsMap.get(card.nmID);
if (analyticsData?.data?.regions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
analyticsData.data.regions.forEach((region: any) => {
(
analyticsData.data.regions as {
offices: {
officeID: number;
officeName: string;
metrics: { stockCount: number };
}[];
}[]
).forEach((region) => {
if (region.offices && region.offices.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
region.offices.forEach((office: any) => {
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
})
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)
}
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 => {
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
})
deliveryType: 1,
});
}
})
})
return Array.from(warehousesMap.values())
}
});
});
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 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)
}
stocksData.flatMap((item) => item.stocks.map((s) => s.warehouseId))
);
setActiveWarehouses(warehousesWithStock.size);
};
// Загрузка данных склада
const loadWarehouseData = async () => {
if (!user?.wildberriesApiKey) return
if (!user?.wildberriesApiKey) return;
setLoading(true)
setLoading(true);
try {
const apiToken = user.wildberriesApiKey
const wbService = new WildberriesService(apiToken)
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)
const cards = await WildberriesService.getAllCards(apiToken).catch(
() => []
);
console.log("WB Warehouse: Loaded cards:", cards.length);
if (cards.length === 0) {
toast.error('Нет карточек товаров в WB')
return
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)
const nmIds = cards.map((card) => card.nmID).filter((id) => id > 0);
console.log("WB Warehouse: NM IDs to process:", nmIds.length);
// 2. Получаем аналитику для каждого товара индивидуально
const analyticsResults = []
const analyticsResults = [];
for (const nmId of nmIds) {
try {
console.log(`WB Warehouse: Fetching analytics for nmId ${nmId}`)
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))
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.error(
`WB Warehouse: Error fetching analytics for nmId ${nmId}:`,
error
);
}
}
console.log('WB Warehouse: Analytics results:', analyticsResults.length)
console.log("WB Warehouse: Analytics results:", analyticsResults.length);
// 3. Комбинируем данные
const combinedStocks = combineCardsWithIndividualAnalytics(cards, analyticsResults)
console.log('WB Warehouse: Combined stocks:', combinedStocks.length)
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)
const extractedWarehouses = extractWarehousesFromStocks(combinedStocks);
toast.success(`Загружено товаров: ${combinedStocks.length}`)
} catch (error: any) {
console.error('WB Warehouse: Error loading data:', error)
toast.error('Ошибка загрузки данных: ' + (error.message || 'Неизвестная ошибка'))
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)
setLoading(false);
}
}
};
useEffect(() => {
if (hasWBApiKey) {
loadWarehouseData()
loadWarehouseData();
}
}, [hasWBApiKey])
}, [hasWBApiKey]);
// Фильтрация товаров
const filteredStocks = stocks.filter(item => {
if (!searchTerm) return true
const search = searchTerm.toLowerCase()
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`}>
<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}
@ -275,16 +309,25 @@ export function WBWarehouseDashboard() {
</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>
<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>
<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>
<span className="text-orange-400 font-medium">
{warehouse.fromClient}
</span>
</div>
</div>
</Card>
@ -294,10 +337,7 @@ export function WBWarehouseDashboard() {
)}
{/* Поиск */}
<SearchBar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
/>
<SearchBar searchTerm={searchTerm} onSearchChange={setSearchTerm} />
{/* Список товаров */}
<div className="flex-1 overflow-hidden">
@ -309,10 +349,15 @@ export function WBWarehouseDashboard() {
) : !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'}
<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"
>
Перейти в настройки
@ -321,13 +366,17 @@ export function WBWarehouseDashboard() {
) : 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>
<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) => (
@ -340,5 +389,5 @@ export function WBWarehouseDashboard() {
</div>
</main>
</div>
)
}
);
}