Добавлен новый компонент для отображения бизнес-процессов в интерфейсе управления. Обновлен компонент UIKitSection для интеграции нового демо и улучшения навигации. Оптимизирована логика отображения данных и улучшена читаемость кода. Исправлены текстовые метки для повышения удобства использования.
This commit is contained in:
@ -1,98 +1,127 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import React from 'react'
|
||||
import { Package } from 'lucide-react'
|
||||
import React from "react";
|
||||
import { Package } from "lucide-react";
|
||||
|
||||
// Интерфейсы (можно будет вынести в отдельный файл types.ts)
|
||||
interface WBStockInfo {
|
||||
warehouseId: number
|
||||
warehouseName: string
|
||||
quantity: number
|
||||
quantityFull: number
|
||||
inWayToClient: number
|
||||
inWayFromClient: number
|
||||
warehouseId: number;
|
||||
warehouseName: string;
|
||||
quantity: number;
|
||||
quantityFull: number;
|
||||
inWayToClient: number;
|
||||
inWayFromClient: number;
|
||||
}
|
||||
|
||||
interface WBStock {
|
||||
nmId: number
|
||||
vendorCode: string
|
||||
title: string
|
||||
brand: string
|
||||
price: number
|
||||
stocks: WBStockInfo[]
|
||||
totalQuantity: number
|
||||
totalReserved: number
|
||||
photos: any[]
|
||||
mediaFiles: any[]
|
||||
characteristics: any[]
|
||||
subjectName: string
|
||||
description: string
|
||||
nmId: number;
|
||||
vendorCode: string;
|
||||
title: string;
|
||||
brand: string;
|
||||
price: number;
|
||||
stocks: WBStockInfo[];
|
||||
totalQuantity: number;
|
||||
totalReserved: number;
|
||||
photos: unknown[];
|
||||
mediaFiles: unknown[];
|
||||
characteristics: unknown[];
|
||||
subjectName: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface StockTableRowProps {
|
||||
item: WBStock
|
||||
item: WBStock;
|
||||
}
|
||||
|
||||
export function StockTableRow({ item }: StockTableRowProps) {
|
||||
// Функция для получения изображений карточки
|
||||
const getCardImages = (item: WBStock) => {
|
||||
const fallbackUrl = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp`
|
||||
|
||||
const fallbackUrl = `https://basket-${String(item.nmId).slice(
|
||||
0,
|
||||
2
|
||||
)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(
|
||||
item.nmId
|
||||
).slice(0, -3)}/${item.nmId}/images/big/1.webp`;
|
||||
|
||||
// Проверяем photos
|
||||
if (item.photos && item.photos.length > 0) {
|
||||
return item.photos.map(photo => photo.big || photo.c516x688 || photo.c246x328 || photo.square || photo.tm || fallbackUrl)
|
||||
return item.photos.map(
|
||||
(photo) =>
|
||||
photo.big ||
|
||||
photo.c516x688 ||
|
||||
photo.c246x328 ||
|
||||
photo.square ||
|
||||
photo.tm ||
|
||||
fallbackUrl
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Проверяем mediaFiles
|
||||
if (item.mediaFiles && item.mediaFiles.length > 0) {
|
||||
return item.mediaFiles.map(media => media.big || media.c516x688 || media.c246x328 || media.square || media.tm || fallbackUrl)
|
||||
return item.mediaFiles.map(
|
||||
(media) =>
|
||||
media.big ||
|
||||
media.c516x688 ||
|
||||
media.c246x328 ||
|
||||
media.square ||
|
||||
media.tm ||
|
||||
fallbackUrl
|
||||
);
|
||||
}
|
||||
|
||||
return [fallbackUrl]
|
||||
}
|
||||
|
||||
const getStockStatus = (quantity: number) => {
|
||||
if (quantity === 0) return {
|
||||
color: 'text-red-400',
|
||||
bgColor: 'bg-red-500/10',
|
||||
label: 'Нет в наличии'
|
||||
}
|
||||
if (quantity < 10) return {
|
||||
color: 'text-orange-400',
|
||||
bgColor: 'bg-orange-500/10',
|
||||
label: 'Мало'
|
||||
}
|
||||
return {
|
||||
color: 'text-green-400',
|
||||
bgColor: 'bg-green-500/10',
|
||||
label: 'В наличии'
|
||||
}
|
||||
}
|
||||
|
||||
const stockStatus = getStockStatus(item.totalQuantity)
|
||||
const images = getCardImages(item)
|
||||
const mainImage = images[0] || null
|
||||
return [fallbackUrl];
|
||||
};
|
||||
|
||||
const getStockStatus = (quantity: number) => {
|
||||
if (quantity === 0)
|
||||
return {
|
||||
color: "text-red-400",
|
||||
bgColor: "bg-red-500/10",
|
||||
label: "Нет в наличии",
|
||||
};
|
||||
if (quantity < 10)
|
||||
return {
|
||||
color: "text-orange-400",
|
||||
bgColor: "bg-orange-500/10",
|
||||
label: "Мало",
|
||||
};
|
||||
return {
|
||||
color: "text-green-400",
|
||||
bgColor: "bg-green-500/10",
|
||||
label: "В наличии",
|
||||
};
|
||||
};
|
||||
|
||||
const stockStatus = getStockStatus(item.totalQuantity);
|
||||
const images = getCardImages(item);
|
||||
const mainImage = images[0] || null;
|
||||
|
||||
// Отбираем ключевые характеристики для отображения в таблице
|
||||
const keyCharacteristics = item.characteristics?.slice(0, 3) || []
|
||||
const keyCharacteristics = item.characteristics?.slice(0, 3) || [];
|
||||
|
||||
return (
|
||||
<div className="group">
|
||||
{/* Основная строка товара */}
|
||||
<div
|
||||
className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300"
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/20 transition-all duration-300">
|
||||
{/* Товар (3 колонки) */}
|
||||
<div className="col-span-3 flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden bg-white/10 flex-shrink-0">
|
||||
{mainImage ? (
|
||||
<img
|
||||
<img
|
||||
src={mainImage}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.src = `https://basket-${String(item.nmId).slice(0, 2)}.wbbasket.ru/vol${String(item.nmId).slice(0, -5)}/part${String(item.nmId).slice(0, -3)}/${item.nmId}/images/big/1.webp`
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = `https://basket-${String(item.nmId).slice(
|
||||
0,
|
||||
2
|
||||
)}.wbbasket.ru/vol${String(item.nmId).slice(
|
||||
0,
|
||||
-5
|
||||
)}/part${String(item.nmId).slice(0, -3)}/${
|
||||
item.nmId
|
||||
}/images/big/1.webp`;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -104,16 +133,14 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded">
|
||||
{item.brand || 'Без бренда'}
|
||||
{item.brand || "Без бренда"}
|
||||
</span>
|
||||
<span className="text-white/40 text-xs">#{item.nmId}</span>
|
||||
</div>
|
||||
<h3 className="text-white text-sm font-medium line-clamp-1 mb-1">
|
||||
{item.title}
|
||||
</h3>
|
||||
<div className="text-white/60 text-xs">
|
||||
{item.vendorCode}
|
||||
</div>
|
||||
<div className="text-white/60 text-xs">{item.vendorCode}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -123,7 +150,9 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className={`text-lg font-bold ${stockStatus.color}`}>
|
||||
{item.totalQuantity.toLocaleString()}
|
||||
</div>
|
||||
<div className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}>
|
||||
<div
|
||||
className={`text-xs px-2 py-0.5 rounded ${stockStatus.bgColor} ${stockStatus.color}`}
|
||||
>
|
||||
{stockStatus.label}
|
||||
</div>
|
||||
</div>
|
||||
@ -164,16 +193,22 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className="space-y-1 w-full">
|
||||
{keyCharacteristics.map((char, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span className="text-white/60 truncate w-1/2">{char.name}:</span>
|
||||
<span className="text-white/60 truncate w-1/2">
|
||||
{char.name}:
|
||||
</span>
|
||||
<span className="text-white truncate w-1/2 text-right">
|
||||
{Array.isArray(char.value) ? char.value.join(', ') : String(char.value)}
|
||||
{Array.isArray(char.value)
|
||||
? char.value.join(", ")
|
||||
: String(char.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{item.subjectName && (
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-white/60">Категория:</span>
|
||||
<span className="text-blue-300 truncate text-right">{item.subjectName}</span>
|
||||
<span className="text-blue-300 truncate text-right">
|
||||
{item.subjectName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -184,38 +219,46 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
<div className="grid grid-cols-12 gap-4 p-3 bg-white/[0.02] border-l-2 border-blue-400/30">
|
||||
<div className="col-span-12 flex flex-wrap gap-3">
|
||||
{item.stocks.map((stock, stockIndex) => (
|
||||
<div
|
||||
key={`${stock.warehouseId}-${stockIndex}`}
|
||||
<div
|
||||
key={`${stock.warehouseId}-${stockIndex}`}
|
||||
className="bg-white/10 rounded-lg px-3 py-2 border border-white/20 hover:border-white/30 transition-colors"
|
||||
>
|
||||
{/* Название города */}
|
||||
<div className="text-white text-sm font-medium mb-1">
|
||||
{stock.warehouseName}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Цифры */}
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<div className="text-center">
|
||||
<div className={`font-bold ${stock.quantity > 0 ? 'text-green-400' : 'text-white/30'}`}>
|
||||
<div
|
||||
className={`font-bold ${
|
||||
stock.quantity > 0 ? "text-green-400" : "text-white/30"
|
||||
}`}
|
||||
>
|
||||
{stock.quantity}
|
||||
</div>
|
||||
<div className="text-white/50">остаток</div>
|
||||
</div>
|
||||
|
||||
|
||||
{(stock.inWayToClient > 0 || stock.inWayFromClient > 0) && (
|
||||
<>
|
||||
<div className="w-px h-6 bg-white/20" />
|
||||
|
||||
|
||||
{stock.inWayToClient > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-orange-400">{stock.inWayToClient}</div>
|
||||
<div className="font-bold text-orange-400">
|
||||
{stock.inWayToClient}
|
||||
</div>
|
||||
<div className="text-white/50">к клиенту</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{stock.inWayFromClient > 0 && (
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-red-400">{stock.inWayFromClient}</div>
|
||||
<div className="font-bold text-red-400">
|
||||
{stock.inWayFromClient}
|
||||
</div>
|
||||
<div className="text-white/50">от клиента</div>
|
||||
</div>
|
||||
)}
|
||||
@ -227,5 +270,5 @@ export function StockTableRow({ item }: StockTableRowProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user