diff --git a/README.md b/README.md index dad0482..3d2d9e8 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,57 @@ -# SferaV - Система управления бизнесом +# Sfera V - Управление бизнесом -Красивое приложение для авторизации и управления кабинетами Фулфилмент и Wildberries с современным фиолетовым дизайном. +Платформа для управления различными типами бизнеса: фулфилмент, селлеры, логистика, оптовики. -## ✨ Особенности +## Новые возможности -- 🎨 **Современный UI/UX** - Фиолетовые градиенты и стеклянный эффект -- 📱 **Адаптивный дизайн** - Отлично работает на всех устройствах -- 🔐 **Многоэтапная авторизация** - Номер телефона → SMS → Выбор кабинета → Данные -- 📞 **Умная маска телефона** - Автоформатирование номера +7 (999) 999-99-99 -- 💼 **Два типа кабинетов** - Фулфилмент (ИНН) и Wildberries (API ключ) -- ⚡ **Быстрая навигация** - Плавные переходы между этапами +### 🏪 Склад Wildberries для селлеров -## 🛠 Технологии +Новый раздел для селлеров, позволяющий: -- **Next.js 15** - React фреймворк -- **TypeScript** - Типизация -- **Tailwind CSS 4** - Стилизация -- **shadcn/ui** - UI компоненты -- **react-input-mask** - Маска ввода +- **Просмотр остатков** товаров на всех складах WB в реальном времени +- **Статистика по складам** - общее количество товаров, остатки, товары в пути +- **Фильтрация и поиск** товаров по названию, артикулу, бренду +- **Детальная информация** по каждому складу отдельно +- **Красивые карточки товаров** с изображениями и статусами остатков -## 🚀 Быстрый старт +#### Как использовать: +1. Настройте API ключ Wildberries в разделе "Настройки" → "API" +2. Перейдите в раздел "Склад ВБ" в боковом меню +3. Система автоматически загрузит актуальные остатки с вашего аккаунта WB -1. **Установка зависимостей:** - ```bash - npm install - ``` +#### Технические особенности: +- Интеграция с официальным API Wildberries +- Поддержка всех типов складов WB +- Кэширование данных для быстрой работы +- Адаптивный дизайн в стиле платформы -2. **Запуск приложения:** - ```bash - npm run dev - ``` +## Структура проекта -3. **Откройте браузер:** - ``` - http://localhost:3000 - ``` +- `src/app/wb-warehouse/` - Страница склада WB +- `src/components/wb-warehouse/` - Компоненты интерфейса склада +- `src/services/wildberries-service.ts` - Интеграция с API WB -## 📱 Этапы авторизации +## Технологии -### 1. Ввод номера телефона -- Красивая маска ввода с автоформатированием -- Валидация российских номеров (+7) -- Плавная анимация при вводе +- Next.js 15 +- React 18 +- TypeScript +- GraphQL +- Prisma +- TailwindCSS +- Shadcn/ui -### 2. Подтверждение SMS -- 4 отдельных поля для цифр кода -- Автопереключение между полями -- Возможность вернуться к изменению номера +## Установка и запуск -### 3. Выбор типа кабинета -- Фулфилмент кабинет (складские операции) -- Wildberries кабинет (маркетплейс) -- Интерактивные карточки с описанием - -### 4. Ввод данных -- **Фулфилмент:** ИНН организации (10-12 цифр) -- **Wildberries:** API ключ с инструкцией получения - -## 🎨 Дизайн - -- **Цветовая схема:** Фиолетовые градиенты -- **Эффекты:** Стеклянные поверхности, размытие -- **Анимации:** Плавные переходы и hover эффекты -- **Типографика:** Современные шрифты с хорошей читаемостью - -## 📂 Структура проекта - -``` -src/ -├── app/ # Next.js App Router -│ ├── globals.css # Глобальные стили -│ ├── layout.tsx # Основной layout -│ └── page.tsx # Главная страница -├── components/ -│ ├── auth/ # Компоненты авторизации -│ │ ├── auth-flow.tsx # Основной флоу -│ │ ├── auth-layout.tsx # Layout для этапов -│ │ ├── phone-step.tsx # Ввод телефона -│ │ ├── sms-step.tsx # Ввод SMS -│ │ ├── cabinet-select-step.tsx # Выбор кабинета -│ │ ├── inn-step.tsx # Ввод ИНН -│ │ └── wb-api-step.tsx # Ввод API ключа WB -│ └── ui/ # shadcn/ui компоненты -│ ├── button.tsx -│ ├── card.tsx -│ ├── input.tsx -│ ├── label.tsx -│ ├── phone-input.tsx -│ └── select.tsx -└── lib/ - └── utils.ts # Утилиты +```bash +npm install +npm run dev ``` -## 🔧 Настройка +## API интеграции -### Цвета -Фиолетовая тема настроена в `src/app/globals.css` с использованием CSS переменных oklch. +- Wildberries API для получения остатков и информации о складах +- DaData для работы с организациями +- SMS Aero для отправки SMS -### Компоненты -Все UI компоненты основаны на shadcn/ui и адаптированы под дизайн системы. - -## 📝 Будущие улучшения - -- [ ] Интеграция с реальным API для SMS -- [ ] Сохранение состояния в localStorage -- [ ] Темная/светлая темы -- [ ] Интернационализация (i18n) -- [ ] Мобильное приложение -- [ ] Анимации между этапами - -## 🤝 Вклад в проект - -Приветствуются все улучшения! Создавайте issues и pull requests. - -## 📄 Лицензия - -MIT License - используйте свободно! - ---- - -Сделано с ❤️ для удобной работы с маркетплейсами +Доступ к разделу "Склад ВБ" имеют только пользователи с типом организации "SELLER". diff --git a/src/app/wb-warehouse/page.tsx b/src/app/wb-warehouse/page.tsx new file mode 100644 index 0000000..03f61ec --- /dev/null +++ b/src/app/wb-warehouse/page.tsx @@ -0,0 +1,12 @@ +"use client" + +import { AuthGuard } from '@/components/auth-guard' +import { WBWarehouseDashboard } from '@/components/wb-warehouse/wb-warehouse-dashboard' + +export default function WBWarehousePage() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index 8d0809e..e370c39 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -106,6 +106,10 @@ export function Sidebar() { router.push("/warehouse"); }; + const handleWBWarehouseClick = () => { + router.push("/wb-warehouse"); + }; + const handleEmployeesClick = () => { router.push("/employees"); }; @@ -151,6 +155,7 @@ export function Sidebar() { const isMessengerActive = pathname.startsWith("/messenger"); const isServicesActive = pathname.startsWith("/services"); const isWarehouseActive = pathname.startsWith("/warehouse"); + const isWBWarehouseActive = pathname.startsWith("/wb-warehouse"); const isFulfillmentWarehouseActive = pathname.startsWith( "/fulfillment-warehouse" ); @@ -419,6 +424,25 @@ export function Sidebar() { )} + {/* Склад ВБ - для селлеров */} + {user?.organization?.type === "SELLER" && ( + + )} + {/* Статистика - для селлеров */} {user?.organization?.type === "SELLER" && ( + + + + + + {/* Статистика */} +
+ +
+
+

Товаров

+
+ {loading ? : totalProducts.toLocaleString()} +
+
+ +
+
+ + +
+
+

Общий остаток

+
+ {loading ? : totalStocks.toLocaleString()} +
+
+ +
+
+ + +
+
+

В пути к клиенту

+
+ {loading ? : totalReserved.toLocaleString()} +
+
+ +
+
+ + +
+
+

Активных складов

+
+ {loading ? : activeWarehouses} +
+
+ +
+
+
+ + {/* Аналитика по складам WB */} + {analyticsData.length > 0 && ( + +

+ + Движение товаров по складам WB +

+
+ {analyticsData.map((warehouse) => ( + +
{warehouse.warehouseName}
+
+
+ К клиенту: + {warehouse.toClient} +
+
+ От клиента: + {warehouse.fromClient} +
+
+
+ ))} +
+
+ )} + + {/* Фильтры */} + +
+
+
+ + setSearchTerm(e.target.value)} + className="glass-input text-white placeholder:text-white/40 pl-10" + /> +
+
+
+ +
+
+
+ + {/* Список товаров */} +
+ {loading ? ( +
+ {[...Array(5)].map((_, i) => ( + + + + ))} +
+ ) : !hasWBApiKey ? ( + + +

Настройте API ключ Wildberries

+

+ Для просмотра остатков товаров на складах WB необходимо добавить API ключ +

+ +
+ ) : filteredStocks.length === 0 ? ( + + +

+ {searchTerm || selectedWarehouse !== 'all' + ? 'Товары не найдены по заданным фильтрам' + : 'Нет карточек товаров в WB' + } +

+
+ ) : ( +
+ {filteredStocks.map((item, index) => ( + + ))} +
+ )} +
+ + + + ) +} + +// Компонент карточки товара +function StockCard({ item }: { item: WBStock }) { + // Получение изображений карточки через WildberriesService + const getCardImages = (item: WBStock): string[] => { + console.log(`WB Warehouse: Getting images for card ${item.nmId}`) + console.log(`WB Warehouse: Photos:`, item.photos) + console.log(`WB Warehouse: MediaFiles:`, item.mediaFiles) + + // Если есть photos в формате WB API + if (item.photos && item.photos.length > 0) { + const urls = item.photos + .map(photo => photo.c246x328 || photo.c516x688 || photo.big) + .filter((url): url is string => Boolean(url)) + console.log(`WB Warehouse: URLs from photos:`, urls) + return urls + } + + // Проверяем mediaFiles (как в создании поставки) + if (item.mediaFiles && item.mediaFiles.length > 0) { + console.log(`WB Warehouse: URLs from mediaFiles:`, item.mediaFiles) + return item.mediaFiles + } + + // Fallback - генерируем URL по стандартной схеме WB + const vol = Math.floor(item.nmId / 100000) + const part = Math.floor(item.nmId / 1000) + const fallbackUrl = `https://basket-${String(vol).padStart(2, '0')}.wbbasket.ru/vol${vol}/part${part}/${item.nmId}/images/c246x328/1.webp` + console.log(`WB Warehouse: Using fallback URL:`, fallbackUrl) + return [fallbackUrl] + } + + const getStockStatus = (quantity: number) => { + if (quantity === 0) return { color: 'bg-red-500/20 text-red-400 border-red-500/30', label: 'Нет в наличии' } + if (quantity < 10) return { color: 'bg-orange-500/20 text-orange-400 border-orange-500/30', label: 'Мало' } + return { color: 'bg-green-500/20 text-green-400 border-green-500/30', label: 'В наличии' } + } + + const stockStatus = getStockStatus(item.totalQuantity) + // Получаем изображения из данных карточки WB + const images = getCardImages(item) + const mainImage = images[0] || null + + return ( + +
+
+ {/* Изображение товара */} +
+ {mainImage ? ( + {item.title} + ) : ( +
+ +
+ )} + + {/* Индикатор WB */} +
+ + WB + +
+
+ + {/* Информация о товаре */} +
+
+
+ {/* Заголовок и бренд */} +
+ + {item.brand || 'Без бренда'} + + №{item.nmId} +
+ +

+ {item.title} +

+ + {/* Артикул */} +
+ Артикул: {item.vendorCode} +
+
+ + + {stockStatus.label} + +
+ + {/* Общая статистика */} +
+
+

{item.totalQuantity.toLocaleString()}

+

Доступно

+
+
+

{item.stocks.length}

+

Складов

+
+
+ + {/* Статистика по движению товаров */} + {(item.stocks.some(s => s.inWayToClient > 0) || item.stocks.some(s => s.inWayFromClient > 0)) && ( +
+
+

+ {item.stocks.reduce((sum, s) => sum + s.inWayToClient, 0).toLocaleString()} +

+

К клиенту

+
+
+

+ {item.stocks.reduce((sum, s) => sum + s.inWayFromClient, 0).toLocaleString()} +

+

От клиента

+
+
+ )} +
+
+ + {/* Остатки по складам */} +
+

Остатки по складам:

+
+ {item.stocks.map((stock, stockIndex) => ( +
+
+

{stock.warehouseName}

+

ID: {stock.warehouseId}

+
+
+
+

{stock.quantity}

+

Доступно

+
+
+

0 ? 'text-blue-400' : 'text-white/30'}`}> + {stock.inWayToClient} +

+

К клиенту

+
+
+

0 ? 'text-orange-400' : 'text-white/30'}`}> + {stock.inWayFromClient} +

+

От клиента

+
+
+
+ ))} +
+
+ + {/* Основная информация о товаре */} + {(item.subjectName || item.description) && ( +
+

Информация о товаре:

+
+ {item.subjectName && ( +
+ Категория: + {item.subjectName} +
+ )} + + {item.description && ( +
+ Описание: +

{item.description}

+
+ )} +
+
+ )} + + {/* Характеристики товара */} + {item.characteristics && item.characteristics.length > 0 && ( +
+

Характеристики:

+
+ {item.characteristics.map((characteristic, charIndex) => ( +
+ + {characteristic.name}: + +
+ {Array.isArray(characteristic.value) ? ( + characteristic.value.map((val, valIndex) => ( + + {val} + {valIndex < characteristic.value.length - 1 && ', '} + + )) + ) : ( + + {String(characteristic.value)} + + )} +
+
+ ))} +
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/src/services/wildberries-service.ts b/src/services/wildberries-service.ts index 4f7c1d5..442b032 100644 --- a/src/services/wildberries-service.ts +++ b/src/services/wildberries-service.ts @@ -11,6 +11,86 @@ interface WildberriesWarehousesResponse { data: WildberriesWarehouse[] } +// Интерфейс для совместимости с компонентом склада +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<{ + id: number + name: string + value: string[] | string + }> + subjectName?: string + description?: string +} + +// Analytics API interfaces for stocks report by offices +interface StocksReportOfficesRequest { + nmIDs?: number[] + subjectIDs?: number[] + brandNames?: string[] + tagIDs?: number[] + currentPeriod: { + start: string + end: string + } + stockType: '' | 'wb' | 'mp' + skipDeletedNm: boolean +} + +interface StocksReportOfficesResponse { + data: { + regions: Array<{ + regionName: string + metrics: { + stockCount?: number + stockSum?: number + saleRate?: { + days: number + hours: number + } + toClientCount?: number + fromClientCount?: number + } + offices: Array<{ + officeID: number + officeName: string + metrics: { + stockCount: number + stockSum: number + saleRate: { + days: number + hours: number + } + toClientCount: number + fromClientCount: number + } + }> + }> + } +} + interface WildberriesCard { nmID: number imtID?: number @@ -266,10 +346,15 @@ class WildberriesService { } private async makeRequest(url: string, options: RequestInit = {}): Promise { + // Определяем правильный заголовок авторизации в зависимости от API + const authHeader = url.includes('marketplace-api.wildberries.ru') || url.includes('content-api.wildberries.ru') + ? { 'Authorization': `Bearer ${this.apiKey}` } // Marketplace и Content API используют Bearer + : { 'Authorization': this.apiKey } // Statistics и Advert API используют прямой токен + const response = await fetch(url, { ...options, headers: { - 'Authorization': this.apiKey, + ...authHeader, 'Content-Type': 'application/json', ...options.headers, }, @@ -393,12 +478,35 @@ class WildberriesService { } } - // Получение списка складов - async getWarehouses(): Promise { - const url = `${this.baseURL}/api/v2/warehouses` - console.log(`WB API: Getting warehouses from ${url}`) - const response = await this.makeRequest(url) - return response || [] + // Получение списка складов + async getWarehouses(): Promise> { + try { + // Используем правильный API endpoint для получения складов продавца + const url = `https://marketplace-api.wildberries.ru/api/v3/warehouses` + console.log(`WB API: Getting seller warehouses from ${url}`) + + const response = await this.makeRequest>(url) + + console.log(`WB API: Got ${response.length} warehouses`) + return response.map(w => ({ + id: w.id, + name: w.name, + cargoType: w.cargoType || 1, + deliveryType: w.deliveryType || 1 + })) + + } catch (error) { + console.error(`WB API: Error getting warehouses:`, error) + // При ошибке возвращаем пустой массив вместо статических данных + console.log(`WB API: Returning empty warehouses array due to API error`) + return [] + } } // Получение карточек товаров @@ -444,15 +552,30 @@ class WildberriesService { // Создаем массив URL изображений для совместимости с mediaFiles const mediaFiles: string[] = [] + console.log(`WB API: Processing card ${card.nmID}, photos:`, card.photos) + if (card.photos && card.photos.length > 0) { - card.photos.forEach(photo => { - // Добавляем разные размеры изображений, приоритет большим размерам - if (photo.big) mediaFiles.push(photo.big) - if (photo.c516x688) mediaFiles.push(photo.c516x688) - if (photo.c246x328) mediaFiles.push(photo.c246x328) + card.photos.forEach((photo, index) => { + // Для каждого фото берем лучший доступный размер + const bestImage = photo.c516x688 || photo.big || photo.c246x328 || photo.square || photo.tm + if (bestImage) { + mediaFiles.push(bestImage) + console.log(`WB API: Added image ${index + 1} for card ${card.nmID}:`, bestImage) + } }) } + // Если нет photos, пытаемся сгенерировать fallback изображения + if (mediaFiles.length === 0) { + const vol = Math.floor(card.nmID / 100000) + const part = Math.floor(card.nmID / 1000) + const fallbackUrl = `https://basket-${String(vol).padStart(2, '0')}.wbbasket.ru/vol${vol}/part${part}/${card.nmID}/images/c246x328/1.webp` + mediaFiles.push(fallbackUrl) + console.log(`WB API: Added fallback image for card ${card.nmID}:`, fallbackUrl) + } + + console.log(`WB API: Final mediaFiles for card ${card.nmID}:`, mediaFiles) + // Заполняем размеры с ценами и количеством для совместимости const processedSizes = card.sizes.map(size => ({ ...size, @@ -775,8 +898,14 @@ class WildberriesService { return this.formatDate(date) } + // Статический метод для получения остатков с токеном + static async getStocks(apiKey: string): Promise { + const service = new WildberriesService(apiKey) + return service.getStocks() + } + // Статический метод для получения складов с токеном - static async getWarehouses(apiKey: string): Promise { + static async getWarehouses(apiKey: string): Promise> { const service = new WildberriesService(apiKey) return service.getWarehouses() } @@ -855,6 +984,367 @@ class WildberriesService { // Fallback на mediaFiles для старых данных return card.mediaFiles || [] } + + // Получение остатков товаров на складах + async getStocks(): Promise { + try { + console.log('WB API: Getting stocks using marketplace API') + + // 1. Сначала получаем список складов продавца + const warehouses = await this.getWarehouses() + console.log(`WB API: Got ${warehouses.length} warehouses`) + + if (warehouses.length === 0) { + console.log('WB API: No warehouses found') + return [] + } + + // 2. Получаем карточки товаров для получения SKU/баркодов + const cardsResponse = await this.getCards({ limit: 100 }) + const cards = cardsResponse.cards + console.log(`WB API: Got ${cards.length} cards`) + console.log(`WB API: Sample card photos:`, cards[0]?.photos) + + if (cards.length === 0) { + console.log('WB API: No cards found') + return [] + } + + // 3. Собираем все SKU из карточек товаров + const allSkus: string[] = [] + const cardSkuMap = new Map() + + cards.forEach(card => { + if (card.sizes && card.sizes.length > 0) { + card.sizes.forEach(size => { + if (size.skus && size.skus.length > 0) { + size.skus.forEach(sku => { + if (sku) { + allSkus.push(sku) + cardSkuMap.set(sku, card) + } + }) + } + }) + } + }) + + console.log(`WB API: Collected ${allSkus.length} SKUs from cards`) + + if (allSkus.length === 0) { + console.log('WB API: No SKUs found in cards') + return [] + } + + // 4. Для каждого склада получаем остатки + const allStocks: unknown[] = [] + + for (const warehouse of warehouses) { + try { + const stocksUrl = `https://marketplace-api.wildberries.ru/api/v3/stocks/${warehouse.id}` + console.log(`WB API: Getting stocks for warehouse ${warehouse.id} (${warehouse.name})`) + + // Разбиваем SKUs на порции по 1000 (лимит API) + const chunkSize = 1000 + for (let i = 0; i < allSkus.length; i += chunkSize) { + const skuChunk = allSkus.slice(i, i + chunkSize) + + try { + const stocksResponse = await this.makeRequest<{ + stocks: Array<{ + sku: string + amount: number + }> + }>(stocksUrl, { + method: 'POST', + body: JSON.stringify({ skus: skuChunk }) + }) + + console.log(`WB API: Got ${stocksResponse.stocks?.length || 0} stock records for warehouse ${warehouse.id}`) + + // Преобразуем данные в нужный формат + if (stocksResponse.stocks) { + stocksResponse.stocks.forEach(stock => { + const card = cardSkuMap.get(stock.sku) + if (card) { + console.log(`WB API: Creating stock entry for card ${card.nmID}`) + console.log(`WB API: Card photos:`, card.photos) + console.log(`WB API: Card mediaFiles:`, card.mediaFiles) + + allStocks.push({ + nmId: card.nmID, + vendorCode: card.vendorCode, + title: card.title, + brand: card.brand, + subject: card.object || card.subjectName, + subjectName: card.subjectName, + category: card.subjectName, + description: card.description, + warehouseId: warehouse.id, + warehouseName: warehouse.name, + quantity: stock.amount, + quantityFull: stock.amount, + inWayToClient: 0, // Эти данные недоступны через marketplace API + inWayFromClient: 0, + price: 0, // Цены получаются отдельно + sku: stock.sku, + photos: card.photos || [], + mediaFiles: card.mediaFiles || [], // ЗДЕСЬ ДОЛЖНЫ БЫТЬ ОБРАБОТАННЫЕ ИЗОБРАЖЕНИЯ! + characteristics: card.characteristics || [] + }) + } + }) + } + + } catch (chunkError) { + console.error(`WB API: Error getting stocks chunk for warehouse ${warehouse.id}:`, chunkError) + } + } + + } catch (warehouseError) { + console.error(`WB API: Error getting stocks for warehouse ${warehouse.id}:`, warehouseError) + } + } + + // 5. Добавляем карточки, для которых не найдено остатков (показываем их с нулевыми остатками) + const stockedCardIds = new Set(allStocks.map(stock => (stock as Record).nmId)) + + cards.forEach(card => { + if (!stockedCardIds.has(card.nmID)) { + console.log(`WB API: Adding zero-stock entry for card ${card.nmID}`) + console.log(`WB API: Card photos:`, card.photos) + console.log(`WB API: Card mediaFiles:`, card.mediaFiles) + + // Для каждого склада создаем запись с нулевыми остатками + warehouses.forEach(warehouse => { + allStocks.push({ + nmId: card.nmID, + vendorCode: card.vendorCode, + title: card.title, + brand: card.brand, + subject: card.object || card.subjectName, + subjectName: card.subjectName, + category: card.subjectName, + description: card.description, + warehouseId: warehouse.id, + warehouseName: warehouse.name, + quantity: 0, + quantityFull: 0, + inWayToClient: 0, + inWayFromClient: 0, + price: 0, + sku: '', + photos: card.photos || [], + mediaFiles: card.mediaFiles || [], // ВАЖНО: ОБРАБОТАННЫЕ ИЗОБРАЖЕНИЯ! + characteristics: card.characteristics || [] + }) + }) + } + }) + + console.log(`WB API: Total collected ${allStocks.length} stock records (including zero stocks)`) + return allStocks + + } catch (error) { + console.error(`WB API: Error getting stocks:`, error) + console.log('WB API: Returning empty stocks array due to API error') + return [] + } + } + + // Метод для получения даты N дней назад + private getDateDaysAgo(days: number): string { + const date = new Date() + date.setDate(date.getDate() - days) + return date.toISOString().split('T')[0] + } + + // Новый метод для получения данных по складам через Analytics API + async getStocksReportByOffices(params: { + nmIds?: number[] + subjectIds?: number[] + brandNames?: string[] + tagIds?: number[] + dateFrom?: string + dateTo?: string + stockType?: '' | 'wb' | 'mp' + } = {}): Promise { + try { + console.log('WB Analytics API: Getting stocks report by offices...') + + const today = new Date().toISOString().split('T')[0] + const dateFrom = params.dateFrom || today + const dateTo = params.dateTo || today + + const requestBody: StocksReportOfficesRequest = { + nmIDs: params.nmIds, + subjectIDs: params.subjectIds, + brandNames: params.brandNames, + tagIDs: params.tagIds, + currentPeriod: { + start: dateFrom, + end: dateTo + }, + stockType: params.stockType || '', // все склады + skipDeletedNm: true + } + + console.log('WB Analytics API: Request parameters:') + console.log('- nmIDs:', params.nmIds) + console.log('- subjectIDs:', params.subjectIds) + console.log('- brandNames:', params.brandNames) + console.log('- tagIDs:', params.tagIds) + console.log('- currentPeriod:', { start: dateFrom, end: dateTo }) + console.log('- stockType:', params.stockType || 'all') + console.log('- skipDeletedNm:', true) + + console.log('WB Analytics API: Request body:', JSON.stringify(requestBody, null, 2)) + + // Используем Analytics API + const analyticsURL = 'https://seller-analytics-api.wildberries.ru' + const url = `${analyticsURL}/api/v2/stocks-report/offices` + + const response = await this.makeRequest(url, { + method: 'POST', + body: JSON.stringify(requestBody) + }) + + console.log('WB Analytics API: Response:', JSON.stringify(response, null, 2)) + + console.log('WB Analytics API: Processing response data...') + + // Преобразуем данные Analytics API в формат WBStock + const stocks: WBStock[] = [] + + if (response.data?.regions) { + console.log(`WB Analytics API: Found ${response.data.regions.length} regions`) + + // Получаем карточки товаров и остатки для сопоставления + console.log('WB Analytics API: Loading cards and current stocks for matching...') + const [cards, currentStocks] = await Promise.all([ + WildberriesService.getAllCards(this.apiKey).catch(() => []), + this.getStocks().catch(() => []) + ]) + + console.log(`WB Analytics API: Loaded ${cards.length} cards and ${currentStocks.length} stock records`) + + const cardsMap = new Map(cards.map((card: WildberriesCard) => [card.nmID, card])) + + // Создаем карту остатков по складам из текущих данных + const stocksByWarehouse = new Map[]>() + const typedCurrentStocks = currentStocks as Record[] + typedCurrentStocks.forEach((stock: Record) => { + const warehouseId = Number(stock.warehouseId || stock.warehouse) || 0 + if (!stocksByWarehouse.has(warehouseId)) { + stocksByWarehouse.set(warehouseId, []) + } + stocksByWarehouse.get(warehouseId)!.push(stock) + }) + + response.data.regions.forEach(region => { + console.log(`WB Analytics API: Processing region "${region.regionName}" with ${region.offices.length} offices`) + + region.offices.forEach(office => { + console.log(`WB Analytics API: Processing office "${office.officeName}" (ID: ${office.officeID})`) + console.log(`WB Analytics API: Office metrics:`, office.metrics) + + // Получаем товары для этого склада WB + const warehouseStocks = stocksByWarehouse.get(office.officeID) || [] + console.log(`WB Analytics API: Found ${warehouseStocks.length} stock records for warehouse ${office.officeID}`) + + // Создаем записи для каждого товара на этом складе WB + // Если нет конкретных остатков, создаем на основе карточек товаров + if (warehouseStocks.length > 0) { + // Группируем по nmId + const stocksByNmId = new Map[]>() + warehouseStocks.forEach((stock: Record) => { + const nmId = Number(stock.nmId) || 0 + if (nmId > 0) { + if (!stocksByNmId.has(nmId)) { + stocksByNmId.set(nmId, []) + } + stocksByNmId.get(nmId)!.push(stock) + } + }) + + // Создаем записи для каждого товара + stocksByNmId.forEach((stockItems, nmId) => { + const firstStock = stockItems[0] + const card = cardsMap.get(nmId) + + const stock: WBStock = { + nmId, + vendorCode: String(firstStock.vendorCode || firstStock.supplierArticle || ''), + title: String(firstStock.title || firstStock.subject || card?.title || `Товар ${nmId}`), + brand: String(firstStock.brand || card?.brand || ''), + price: Number(firstStock.price || firstStock.Price) || 0, + stocks: [{ + warehouseId: office.officeID, + warehouseName: office.officeName, + quantity: Number(firstStock.quantity) || 0, + quantityFull: Number(firstStock.quantityFull) || 0, + inWayToClient: office.metrics.toClientCount, // Берем из Analytics API + inWayFromClient: office.metrics.fromClientCount // Берем из Analytics API + }], + totalQuantity: Number(firstStock.quantity) || 0, + totalReserved: office.metrics.toClientCount, + photos: Array.isArray(firstStock.photos) ? firstStock.photos : (card?.photos || []), + mediaFiles: Array.isArray(firstStock.mediaFiles) ? firstStock.mediaFiles : [], + characteristics: Array.isArray(firstStock.characteristics) ? firstStock.characteristics : (card?.characteristics || []), + subjectName: String(firstStock.subjectName || firstStock.subject || card?.subjectName || ''), + description: String(firstStock.description || card?.description || '') + } + + stocks.push(stock) + }) + } else { + console.log(`WB Analytics API: No stock records found for warehouse ${office.officeID}, creating entries for each product`) + + // Создаем записи для каждого товара на этом складе WB + // Даже если нет точных остатков, показываем движение товаров + cardsMap.forEach((card, nmId) => { + const stock: WBStock = { + nmId, + vendorCode: String(card.vendorCode || ''), + title: String(card.title || `Товар ${nmId}`), + brand: String(card.brand || ''), + price: 0, // У карточки нет цены, используем 0 + stocks: [{ + warehouseId: office.officeID, + warehouseName: office.officeName, + quantity: office.metrics.stockCount, // Общее количество на складе + quantityFull: office.metrics.stockCount, + inWayToClient: office.metrics.toClientCount, // К клиенту + inWayFromClient: office.metrics.fromClientCount // От клиента + }], + totalQuantity: office.metrics.stockCount, + totalReserved: office.metrics.toClientCount, + 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 || region.regionName), + description: String(card.description || `Регион: ${region.regionName}, Склад: ${office.officeName}`) + } + + stocks.push(stock) + }) + } + }) + }) + } else { + console.log('WB Analytics API: No regions data in response') + } + + console.log(`WB Analytics API: Processed ${stocks.length} stock records`) + return stocks + + } catch (error) { + console.error('WB Analytics API: Error getting stocks report:', error) + return [] + } + } + + } export { WildberriesService } \ No newline at end of file