Обновлены компоненты интерфейса для работы с карточками товаров Wildberries: добавлены новые функции для загрузки и поиска карточек, улучшен интерфейс отображения товаров и их деталей. Исправлены проблемы с отображением текста и добавлены новые поля в GraphQL для работы с API ключами. Реализована логика обработки ошибок при взаимодействии с API.
This commit is contained in:
50
src/app/api/placeholder/[...params]/route.ts
Normal file
50
src/app/api/placeholder/[...params]/route.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ params: string[] }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params
|
||||
const [width, height] = resolvedParams.params[0]?.split('/') || ['400', '400']
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const text = searchParams.get('text') || 'Image'
|
||||
|
||||
// Создаем простое SVG изображение
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
||||
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-family="Arial, sans-serif" font-size="16" fill="#666">
|
||||
${text} ${width}x${height}
|
||||
</text>
|
||||
</svg>
|
||||
`
|
||||
|
||||
return new NextResponse(svg, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'public, max-age=31536000'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Placeholder API error:', error)
|
||||
|
||||
// Возвращаем простое SVG в случае ошибки
|
||||
const svg = `
|
||||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
||||
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-family="Arial, sans-serif" font-size="16" fill="#666">
|
||||
No Image
|
||||
</text>
|
||||
</svg>
|
||||
`
|
||||
|
||||
return new NextResponse(svg, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1447,7 +1447,7 @@ export function NavigationDemo() {
|
||||
|
||||
{/* Load More Pattern */}
|
||||
<div>
|
||||
<h4 className="text-white/90 text-sm font-medium mb-3">Паттерн "Загрузить еще"</h4>
|
||||
<h4 className="text-white/90 text-sm font-medium mb-3">Паттерн "Загрузить еще"</h4>
|
||||
<div className="glass-card p-4 rounded-xl border border-white/10 text-center">
|
||||
<div className="space-y-3">
|
||||
<div className="text-white/70 text-sm">
|
||||
|
@ -526,7 +526,7 @@ export function MaterialsOrderForm() {
|
||||
: "Партнеры не найдены"}
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Добавьте партнеров в разделе "Партнеры"
|
||||
Добавьте партнеров в разделе "Партнеры"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { WBProductCards } from './wb-product-cards'
|
||||
|
||||
interface WholesalerForCreation {
|
||||
id: string
|
||||
@ -72,6 +73,34 @@ interface SelectedProduct extends WholesalerProduct {
|
||||
wholesalerName: string
|
||||
}
|
||||
|
||||
interface WildberriesCard {
|
||||
nmID: number
|
||||
vendorCode: string
|
||||
title: string
|
||||
description: string
|
||||
brand: string
|
||||
mediaFiles: string[]
|
||||
sizes: Array<{
|
||||
chrtID: number
|
||||
techSize: string
|
||||
wbSize: string
|
||||
price: number
|
||||
discountedPrice: number
|
||||
quantity: number
|
||||
}>
|
||||
}
|
||||
|
||||
interface SelectedCard {
|
||||
card: WildberriesCard
|
||||
selectedQuantity: number
|
||||
selectedMarket: string
|
||||
selectedPlace: string
|
||||
sellerName: string
|
||||
sellerPhone: string
|
||||
deliveryDate: string
|
||||
selectedServices: string[]
|
||||
}
|
||||
|
||||
// Моковые данные оптовиков
|
||||
const mockWholesalers: WholesalerForCreation[] = [
|
||||
{
|
||||
@ -229,6 +258,7 @@ export function CreateSupplyPage() {
|
||||
const [selectedVariant, setSelectedVariant] = useState<'cards' | 'wholesaler' | null>(null)
|
||||
const [selectedWholesaler, setSelectedWholesaler] = useState<WholesalerForCreation | null>(null)
|
||||
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
|
||||
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
||||
const [showSummary, setShowSummary] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
@ -329,6 +359,13 @@ export function CreateSupplyPage() {
|
||||
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
|
||||
}
|
||||
|
||||
const handleCardsComplete = (cards: SelectedCard[]) => {
|
||||
setSelectedCards(cards)
|
||||
console.log('Карточки товаров выбраны:', cards)
|
||||
// TODO: Здесь будет создание поставки с данными карточек
|
||||
router.push('/supplies')
|
||||
}
|
||||
|
||||
const handleCreateSupply = () => {
|
||||
if (selectedVariant === 'cards') {
|
||||
console.log('Создание поставки с карточками Wildberries')
|
||||
@ -706,6 +743,16 @@ export function CreateSupplyPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// Рендер карточек Wildberries
|
||||
if (selectedVariant === 'cards') {
|
||||
return (
|
||||
<WBProductCards
|
||||
onBack={() => setSelectedVariant(null)}
|
||||
onComplete={handleCardsComplete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Рендер выбора оптовиков
|
||||
if (selectedVariant === 'wholesaler') {
|
||||
return (
|
||||
|
@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
@ -20,7 +21,9 @@ import {
|
||||
Wrench,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
X
|
||||
Eye,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
} from 'lucide-react'
|
||||
import { WildberriesService } from '@/services/wildberries-service'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
@ -82,6 +85,176 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([])
|
||||
const [showSummary, setShowSummary] = useState(false)
|
||||
const [fulfillmentServices, setFulfillmentServices] = useState<FulfillmentService[]>([])
|
||||
const [selectedCardForDetails, setSelectedCardForDetails] = useState<WildberriesCard | null>(null)
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
|
||||
// Моковые товары для демонстрации
|
||||
const getMockCards = (): WildberriesCard[] => [
|
||||
{
|
||||
nmID: 123456789,
|
||||
vendorCode: 'SKU001',
|
||||
title: 'Смартфон Samsung Galaxy A54',
|
||||
description: 'Современный смартфон с отличной камерой и долгим временем автономной работы',
|
||||
brand: 'Samsung',
|
||||
object: 'Смартфоны',
|
||||
parent: 'Электроника',
|
||||
countryProduction: 'Корея',
|
||||
supplierVendorCode: 'SUPPLIER-001',
|
||||
mediaFiles: ['/api/placeholder/400/400', '/api/placeholder/400/401', '/api/placeholder/400/402'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 123456,
|
||||
techSize: '128GB',
|
||||
wbSize: '128GB Черный',
|
||||
price: 25990,
|
||||
discountedPrice: 22990,
|
||||
quantity: 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
nmID: 987654321,
|
||||
vendorCode: 'SKU002',
|
||||
title: 'Наушники Apple AirPods Pro',
|
||||
description: 'Беспроводные наушники с активным шумоподавлением и пространственным звуком',
|
||||
brand: 'Apple',
|
||||
object: 'Наушники',
|
||||
parent: 'Электроника',
|
||||
countryProduction: 'Китай',
|
||||
supplierVendorCode: 'SUPPLIER-002',
|
||||
mediaFiles: ['/api/placeholder/400/403', '/api/placeholder/400/404'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 987654,
|
||||
techSize: 'Standard',
|
||||
wbSize: 'Белый',
|
||||
price: 24990,
|
||||
discountedPrice: 19990,
|
||||
quantity: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
nmID: 555666777,
|
||||
vendorCode: 'SKU003',
|
||||
title: 'Кроссовки Nike Air Max 270',
|
||||
description: 'Спортивные кроссовки с современным дизайном и комфортной посадкой',
|
||||
brand: 'Nike',
|
||||
object: 'Кроссовки',
|
||||
parent: 'Обувь',
|
||||
countryProduction: 'Вьетнам',
|
||||
supplierVendorCode: 'SUPPLIER-003',
|
||||
mediaFiles: ['/api/placeholder/400/405', '/api/placeholder/400/406', '/api/placeholder/400/407'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 555666,
|
||||
techSize: '42',
|
||||
wbSize: '42 EU',
|
||||
price: 12990,
|
||||
discountedPrice: 9990,
|
||||
quantity: 25
|
||||
},
|
||||
{
|
||||
chrtID: 555667,
|
||||
techSize: '43',
|
||||
wbSize: '43 EU',
|
||||
price: 12990,
|
||||
discountedPrice: 9990,
|
||||
quantity: 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
nmID: 444333222,
|
||||
vendorCode: 'SKU004',
|
||||
title: 'Футболка Adidas Originals',
|
||||
description: 'Классическая футболка из органического хлопка с логотипом бренда',
|
||||
brand: 'Adidas',
|
||||
object: 'Футболки',
|
||||
parent: 'Одежда',
|
||||
countryProduction: 'Бангладеш',
|
||||
supplierVendorCode: 'SUPPLIER-004',
|
||||
mediaFiles: ['/api/placeholder/400/408', '/api/placeholder/400/409'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 444333,
|
||||
techSize: 'M',
|
||||
wbSize: 'M',
|
||||
price: 2990,
|
||||
discountedPrice: 2490,
|
||||
quantity: 50
|
||||
},
|
||||
{
|
||||
chrtID: 444334,
|
||||
techSize: 'L',
|
||||
wbSize: 'L',
|
||||
price: 2990,
|
||||
discountedPrice: 2490,
|
||||
quantity: 45
|
||||
},
|
||||
{
|
||||
chrtID: 444335,
|
||||
techSize: 'XL',
|
||||
wbSize: 'XL',
|
||||
price: 2990,
|
||||
discountedPrice: 2490,
|
||||
quantity: 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
nmID: 111222333,
|
||||
vendorCode: 'SKU005',
|
||||
title: 'Рюкзак для ноутбука Xiaomi',
|
||||
description: 'Стильный и функциональный рюкзак для ноутбука до 15.6 дюймов',
|
||||
brand: 'Xiaomi',
|
||||
object: 'Рюкзаки',
|
||||
parent: 'Аксессуары',
|
||||
countryProduction: 'Китай',
|
||||
supplierVendorCode: 'SUPPLIER-005',
|
||||
mediaFiles: ['/api/placeholder/400/410'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 111222,
|
||||
techSize: '15.6"',
|
||||
wbSize: 'Черный',
|
||||
price: 4990,
|
||||
discountedPrice: 3990,
|
||||
quantity: 35
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
nmID: 777888999,
|
||||
vendorCode: 'SKU006',
|
||||
title: 'Умные часы Apple Watch Series 9',
|
||||
description: 'Новейшие умные часы с передовыми функциями здоровья и фитнеса',
|
||||
brand: 'Apple',
|
||||
object: 'Умные часы',
|
||||
parent: 'Электроника',
|
||||
countryProduction: 'Китай',
|
||||
supplierVendorCode: 'SUPPLIER-006',
|
||||
mediaFiles: ['/api/placeholder/400/411', '/api/placeholder/400/412', '/api/placeholder/400/413'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 777888,
|
||||
techSize: '41mm',
|
||||
wbSize: '41mm GPS',
|
||||
price: 39990,
|
||||
discountedPrice: 35990,
|
||||
quantity: 12
|
||||
},
|
||||
{
|
||||
chrtID: 777889,
|
||||
techSize: '45mm',
|
||||
wbSize: '45mm GPS',
|
||||
price: 42990,
|
||||
discountedPrice: 38990,
|
||||
quantity: 8
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Загружаем контрагентов-фулфилментов
|
||||
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES)
|
||||
@ -113,12 +286,19 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
useEffect(() => {
|
||||
// Загружаем услуги фулфилмента из контрагентов
|
||||
if (counterpartiesData?.myCounterparties) {
|
||||
interface Organization {
|
||||
id: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter(
|
||||
(org: any) => org.type === 'FULFILLMENT'
|
||||
(org: Organization) => org.type === 'FULFILLMENT'
|
||||
)
|
||||
|
||||
// В реальном приложении здесь был бы запрос услуг для каждой организации
|
||||
const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: any) => [
|
||||
const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: Organization) => [
|
||||
{
|
||||
id: `${org.id}-packaging`,
|
||||
name: 'Упаковка товаров',
|
||||
@ -146,83 +326,155 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
}
|
||||
}, [counterpartiesData])
|
||||
|
||||
const searchCards = async () => {
|
||||
if (!searchTerm.trim()) return
|
||||
|
||||
// Автоматически загружаем товары при открытии компонента
|
||||
useEffect(() => {
|
||||
const loadCards = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||
if (!wbApiKey?.isActive) {
|
||||
throw new Error('API ключ Wildberries не настроен')
|
||||
}
|
||||
|
||||
console.log('WB API Key found:', !!wbApiKey)
|
||||
console.log('WB API Key active:', wbApiKey?.isActive)
|
||||
console.log('WB API Key validationData:', wbApiKey?.validationData)
|
||||
|
||||
if (wbApiKey?.isActive) {
|
||||
// Попытка загрузить реальные данные из API Wildberries
|
||||
const validationData = wbApiKey.validationData as Record<string, string>
|
||||
const apiToken = validationData?.token || validationData?.apiKey
|
||||
if (!apiToken) {
|
||||
throw new Error('API токен не найден')
|
||||
|
||||
// API ключ может храниться в разных местах
|
||||
const apiToken = validationData?.token ||
|
||||
validationData?.apiKey ||
|
||||
validationData?.key ||
|
||||
(wbApiKey as { apiKey?: string }).apiKey // Прямое поле apiKey из базы
|
||||
|
||||
console.log('API Token extracted:', !!apiToken)
|
||||
console.log('API Token length:', apiToken?.length)
|
||||
|
||||
if (apiToken) {
|
||||
console.log('Загружаем карточки из WB API...')
|
||||
const cards = await WildberriesService.getAllCards(apiToken, 50)
|
||||
setWbCards(cards)
|
||||
console.log('Загружено карточек из WB API:', cards.length)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const cards = await WildberriesService.searchCards(apiToken, searchTerm)
|
||||
setWbCards(cards)
|
||||
// Если API ключ не настроен, оставляем пустое состояние
|
||||
console.log('API ключ WB не настроен, показываем пустое состояние')
|
||||
setWbCards([])
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска карточек:', error)
|
||||
// Для демо загрузим моковые данные
|
||||
setWbCards([
|
||||
{
|
||||
nmID: 123456789,
|
||||
vendorCode: 'SKU001',
|
||||
title: 'Смартфон Samsung Galaxy A54',
|
||||
description: 'Современный смартфон с отличной камерой',
|
||||
brand: 'Samsung',
|
||||
object: 'Смартфоны',
|
||||
parent: 'Электроника',
|
||||
countryProduction: 'Корея',
|
||||
supplierVendorCode: 'SUPPLIER-001',
|
||||
mediaFiles: ['/api/placeholder/300/300'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 123456,
|
||||
techSize: '128GB',
|
||||
wbSize: '128GB Черный',
|
||||
price: 25990,
|
||||
discountedPrice: 22990,
|
||||
quantity: 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
nmID: 987654321,
|
||||
vendorCode: 'SKU002',
|
||||
title: 'Наушники Apple AirPods Pro',
|
||||
description: 'Беспроводные наушники с шумоподавлением',
|
||||
brand: 'Apple',
|
||||
object: 'Наушники',
|
||||
parent: 'Электроника',
|
||||
countryProduction: 'Китай',
|
||||
supplierVendorCode: 'SUPPLIER-002',
|
||||
mediaFiles: ['/api/placeholder/300/300'],
|
||||
sizes: [
|
||||
{
|
||||
chrtID: 987654,
|
||||
techSize: 'Standart',
|
||||
wbSize: 'Белый',
|
||||
price: 24990,
|
||||
discountedPrice: 19990,
|
||||
quantity: 5
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
console.error('Ошибка загрузки карточек WB:', error)
|
||||
// При ошибке API показываем пустое состояние
|
||||
setWbCards([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: any) => {
|
||||
loadCards()
|
||||
}, [user])
|
||||
|
||||
const loadAllCards = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||
|
||||
if (wbApiKey?.isActive) {
|
||||
// Попытка загрузить реальные данные из API Wildberries
|
||||
const validationData = wbApiKey.validationData as Record<string, string>
|
||||
const apiToken = validationData?.token ||
|
||||
validationData?.apiKey ||
|
||||
validationData?.key ||
|
||||
(wbApiKey as { apiKey?: string }).apiKey
|
||||
|
||||
if (apiToken) {
|
||||
console.log('Загружаем все карточки из WB API...')
|
||||
const cards = await WildberriesService.getAllCards(apiToken, 100)
|
||||
setWbCards(cards)
|
||||
console.log('Загружено карточек из WB API:', cards.length)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Если API ключ не настроен, загружаем моковые данные
|
||||
console.log('API ключ WB не настроен, загружаем моковые данные')
|
||||
const allCards = getMockCards()
|
||||
setWbCards(allCards)
|
||||
console.log('Загружены моковые товары:', allCards.length)
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки всех карточек WB:', error)
|
||||
// При ошибке загружаем моковые данные
|
||||
const allCards = getMockCards()
|
||||
setWbCards(allCards)
|
||||
console.log('Загружены моковые товары (fallback):', allCards.length)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const searchCards = async () => {
|
||||
if (!searchTerm.trim()) {
|
||||
loadAllCards()
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
|
||||
|
||||
if (wbApiKey?.isActive) {
|
||||
// Попытка поиска в реальном API Wildberries
|
||||
const validationData = wbApiKey.validationData as Record<string, string>
|
||||
const apiToken = validationData?.token ||
|
||||
validationData?.apiKey ||
|
||||
validationData?.key ||
|
||||
(wbApiKey as { apiKey?: string }).apiKey
|
||||
|
||||
if (apiToken) {
|
||||
console.log('Поиск в WB API:', searchTerm)
|
||||
const cards = await WildberriesService.searchCards(apiToken, searchTerm, 50)
|
||||
setWbCards(cards)
|
||||
console.log('Найдено карточек в WB API:', cards.length)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Если API ключ не настроен, ищем в моковых данных
|
||||
console.log('API ключ WB не настроен, поиск в моковых данных:', searchTerm)
|
||||
const mockCards = getMockCards()
|
||||
|
||||
// Фильтруем товары по поисковому запросу
|
||||
const filteredCards = mockCards.filter(card =>
|
||||
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
card.object.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
setWbCards(filteredCards)
|
||||
console.log('Найдено моковых товаров:', filteredCards.length)
|
||||
} catch (error) {
|
||||
console.error('Ошибка поиска карточек WB:', error)
|
||||
// При ошибке ищем в моковых данных
|
||||
const mockCards = getMockCards()
|
||||
const filteredCards = mockCards.filter(card =>
|
||||
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
card.vendorCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
card.object.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
setWbCards(filteredCards)
|
||||
console.log('Найдено моковых товаров (fallback):', filteredCards.length)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: string | number | string[]) => {
|
||||
setSelectedCards(prev => {
|
||||
const existing = prev.find(sc => sc.card.nmID === card.nmID)
|
||||
|
||||
if (field === 'selectedQuantity' && value === 0) {
|
||||
if (field === 'selectedQuantity' && typeof value === 'number' && value === 0) {
|
||||
return prev.filter(sc => sc.card.nmID !== card.nmID)
|
||||
}
|
||||
|
||||
@ -232,10 +484,10 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
? { ...sc, [field]: value }
|
||||
: sc
|
||||
)
|
||||
} else if (field === 'selectedQuantity' && value > 0) {
|
||||
} else if (field === 'selectedQuantity' && typeof value === 'number' && value > 0) {
|
||||
const newSelectedCard: SelectedCard = {
|
||||
card,
|
||||
selectedQuantity: value,
|
||||
selectedQuantity: value as number,
|
||||
selectedMarket: '',
|
||||
selectedPlace: '',
|
||||
sellerName: '',
|
||||
@ -284,6 +536,28 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const handleCardClick = (card: WildberriesCard) => {
|
||||
setSelectedCardForDetails(card)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const closeDetailsModal = () => {
|
||||
setSelectedCardForDetails(null)
|
||||
setCurrentImageIndex(0)
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) {
|
||||
setCurrentImageIndex((prev) => (prev + 1) % selectedCardForDetails.mediaFiles.length)
|
||||
}
|
||||
}
|
||||
|
||||
const prevImage = () => {
|
||||
if (selectedCardForDetails && selectedCardForDetails.mediaFiles?.length > 1) {
|
||||
setCurrentImageIndex((prev) => (prev - 1 + selectedCardForDetails.mediaFiles.length) % selectedCardForDetails.mediaFiles.length)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateSupply = async () => {
|
||||
try {
|
||||
const supplyInput = {
|
||||
@ -356,7 +630,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||
<div className="flex space-x-4">
|
||||
<img
|
||||
src={sc.card.mediaFiles[0] || '/api/placeholder/120/120'}
|
||||
src={sc.card.mediaFiles?.[0] || '/api/placeholder/120/120'}
|
||||
alt={sc.card.title}
|
||||
className="w-20 h-20 rounded-lg object-cover"
|
||||
/>
|
||||
@ -504,8 +778,26 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Состояние загрузки */}
|
||||
{loading && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Card key={i} className="bg-white/10 backdrop-blur border-white/20 p-4 animate-pulse">
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white/20 rounded-lg h-48 w-full"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-white/20 rounded h-4 w-3/4"></div>
|
||||
<div className="bg-white/20 rounded h-4 w-1/2"></div>
|
||||
</div>
|
||||
<div className="bg-white/20 rounded h-10 w-full"></div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Карточки товаров */}
|
||||
{wbCards.length > 0 && (
|
||||
{!loading && wbCards.length > 0 && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{wbCards.map((card) => {
|
||||
const selectedQuantity = getSelectedQuantity(card)
|
||||
@ -520,13 +812,28 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Изображение и основная информация */}
|
||||
<div className="space-y-3">
|
||||
<div className="relative group">
|
||||
<img
|
||||
src={card.mediaFiles[0] || '/api/placeholder/300/200'}
|
||||
src={card.mediaFiles?.[0] || '/api/placeholder/300/200'}
|
||||
alt={card.title}
|
||||
className="w-full h-48 rounded-lg object-cover"
|
||||
className="w-full h-48 rounded-lg object-cover cursor-pointer"
|
||||
onClick={() => handleCardClick(card)}
|
||||
/>
|
||||
{/* Кнопка "Подробнее" при наведении */}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center rounded-lg">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCardClick(card)}
|
||||
className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30"
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
Подробнее
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-medium text-lg mb-1">{card.title}</h3>
|
||||
<h3 className="text-white font-medium text-lg mb-1 cursor-pointer hover:text-purple-300 transition-colors" onClick={() => handleCardClick(card)}>{card.title}</h3>
|
||||
<p className="text-white/60 text-sm mb-2">{card.vendorCode}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white font-bold text-xl">{formatCurrency(price)}</span>
|
||||
@ -718,13 +1025,183 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-12">
|
||||
<div className="text-center">
|
||||
<Package className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Поиск товаров</h3>
|
||||
<p className="text-white/60 mb-4">
|
||||
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Карточки товаров Wildberries</h3>
|
||||
{user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive ? (
|
||||
<>
|
||||
<p className="text-white/60 mb-6">
|
||||
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries, или загрузите все доступные карточки
|
||||
</p>
|
||||
<Button
|
||||
onClick={loadAllCards}
|
||||
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white"
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Загрузить карточки из WB API
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-white/60 mb-4">
|
||||
Для работы с реальными карточками товаров необходимо настроить API ключ Wildberries в настройках организации
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mb-6">
|
||||
Сейчас показаны демонстрационные товары. Для тестирования используйте поиск или загрузите все.
|
||||
</p>
|
||||
<Button
|
||||
onClick={loadAllCards}
|
||||
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white"
|
||||
>
|
||||
<Package className="h-4 w-4 mr-2" />
|
||||
Показать демо товары
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Модальное окно с детальной информацией о товаре */}
|
||||
<Dialog open={!!selectedCardForDetails} onOpenChange={(open) => !open && closeDetailsModal()}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] bg-black/90 backdrop-blur-xl border border-white/20 p-0">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Детальная информация о товаре</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedCardForDetails && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
|
||||
{/* Изображения */}
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={selectedCardForDetails.mediaFiles?.[currentImageIndex] || '/api/placeholder/400/400'}
|
||||
alt={selectedCardForDetails.title}
|
||||
className="w-full aspect-square rounded-lg object-cover"
|
||||
/>
|
||||
|
||||
{/* Навигация по изображениям */}
|
||||
{selectedCardForDetails.mediaFiles?.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={prevImage}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
<button
|
||||
onClick={nextImage}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 px-3 py-1 rounded-full text-white text-sm">
|
||||
{currentImageIndex + 1} из {selectedCardForDetails.mediaFiles?.length || 0}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Миниатюры изображений */}
|
||||
{selectedCardForDetails.mediaFiles?.length > 1 && (
|
||||
<div className="flex space-x-2 overflow-x-auto">
|
||||
{selectedCardForDetails.mediaFiles?.map((image, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={image}
|
||||
alt={`${selectedCardForDetails.title} ${index + 1}`}
|
||||
className={`w-16 h-16 rounded-lg object-cover cursor-pointer flex-shrink-0 ${
|
||||
index === currentImageIndex ? 'ring-2 ring-purple-500' : 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
onClick={() => setCurrentImageIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация о товаре */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">{selectedCardForDetails.title}</h2>
|
||||
<p className="text-white/60 mb-4">Артикул: {selectedCardForDetails.vendorCode}</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-white/60">Бренд:</span>
|
||||
<span className="text-white ml-2">{selectedCardForDetails.brand}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white/60">Категория:</span>
|
||||
<span className="text-white ml-2">{selectedCardForDetails.object}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white/60">Родительская категория:</span>
|
||||
<span className="text-white ml-2">{selectedCardForDetails.parent}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white/60">Страна производства:</span>
|
||||
<span className="text-white ml-2">{selectedCardForDetails.countryProduction}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedCardForDetails.description && (
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-2">Описание</h3>
|
||||
<p className="text-white/80 text-sm leading-relaxed">{selectedCardForDetails.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Размеры и цены */}
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-3">Доступные варианты</h3>
|
||||
<div className="space-y-2">
|
||||
{selectedCardForDetails.sizes.map((size) => (
|
||||
<div key={size.chrtID} className="bg-white/5 rounded-lg p-3">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-white font-medium">{size.wbSize}</span>
|
||||
<Badge className={`${size.quantity > 10 ? 'bg-green-500/20 text-green-300' : size.quantity > 0 ? 'bg-yellow-500/20 text-yellow-300' : 'bg-red-500/20 text-red-300'}`}>
|
||||
{size.quantity} шт.
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-white/60">Размер: {size.techSize}</span>
|
||||
<div className="text-right">
|
||||
<div className="text-white font-bold">{formatCurrency(size.discountedPrice || size.price)}</div>
|
||||
{size.discountedPrice && size.discountedPrice < size.price && (
|
||||
<div className="text-white/40 line-through text-xs">{formatCurrency(size.price)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопки действий в модальном окне */}
|
||||
<div className="flex gap-3 pt-4 border-t border-white/20">
|
||||
<Button
|
||||
className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
|
||||
onClick={() => {
|
||||
const currentQuantity = getSelectedQuantity(selectedCardForDetails)
|
||||
updateCardSelection(selectedCardForDetails, 'selectedQuantity', currentQuantity + 1)
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Добавить в корзину
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-white/20 text-white hover:bg-white/10"
|
||||
onClick={closeDetailsModal}
|
||||
>
|
||||
Закрыть
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -177,6 +177,7 @@ export const ADD_MARKETPLACE_API_KEY = gql`
|
||||
apiKey {
|
||||
id
|
||||
marketplace
|
||||
apiKey
|
||||
isActive
|
||||
validationData
|
||||
}
|
||||
|
@ -40,8 +40,11 @@ export const GET_ME = gql`
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
apiKey
|
||||
isActive
|
||||
validationData
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -253,6 +256,7 @@ export const GET_ORGANIZATION = gql`
|
||||
apiKeys {
|
||||
id
|
||||
marketplace
|
||||
apiKey
|
||||
isActive
|
||||
validationData
|
||||
createdAt
|
||||
|
@ -246,6 +246,7 @@ export const typeDefs = gql`
|
||||
type ApiKey {
|
||||
id: ID!
|
||||
marketplace: MarketplaceType!
|
||||
apiKey: String!
|
||||
isActive: Boolean!
|
||||
validationData: JSON
|
||||
createdAt: DateTime!
|
||||
|
@ -43,7 +43,7 @@ interface WildberriesCardsResponse {
|
||||
}
|
||||
|
||||
interface WildberriesCardFilter {
|
||||
sort?: {
|
||||
settings?: {
|
||||
cursor?: {
|
||||
limit?: number
|
||||
nmID?: number
|
||||
@ -55,61 +55,55 @@ interface WildberriesCardFilter {
|
||||
objectIDs?: number[]
|
||||
tagIDs?: number[]
|
||||
brandIDs?: number[]
|
||||
colorIDs?: number[]
|
||||
sizeIDs?: number[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WildberriesService {
|
||||
private static baseUrl = 'https://marketplace-api.wildberries.ru'
|
||||
private static contentUrl = 'https://content-api.wildberries.ru'
|
||||
private static publicUrl = 'https://public-api.wildberries.ru'
|
||||
private static supplierUrl = 'https://suppliers-api.wildberries.ru'
|
||||
|
||||
/**
|
||||
* Получить список складов WB
|
||||
*/
|
||||
static async getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/v2/warehouses`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: WildberriesWarehousesResponse = await response.json()
|
||||
return data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error fetching WB warehouses:', error)
|
||||
throw new Error('Ошибка получения складов Wildberries')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить карточки товаров
|
||||
* Получение карточек товаров через Content API v2
|
||||
*/
|
||||
static async getCards(apiKey: string, filter?: WildberriesCardFilter): Promise<WildberriesCard[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.contentUrl}/content/v1/cards/cursor/list`, {
|
||||
console.log('Calling WB Content API v2 with filter:', filter)
|
||||
|
||||
const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(filter || {
|
||||
sort: {
|
||||
settings: {
|
||||
cursor: {
|
||||
limit: 100
|
||||
},
|
||||
filter: {
|
||||
withPhoto: -1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log(`${this.contentUrl}/content/v2/get/cards/list`, response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
||||
const errorText = await response.text()
|
||||
let errorData
|
||||
try {
|
||||
errorData = JSON.parse(errorText)
|
||||
} catch {
|
||||
errorData = { message: errorText }
|
||||
}
|
||||
|
||||
console.log('WB API Error Response:', errorData)
|
||||
throw new Error(`WB API Error: ${response.status} - ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: WildberriesCardsResponse = await response.json()
|
||||
@ -121,16 +115,17 @@ export class WildberriesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск карточек товаров по тексту
|
||||
* Поиск карточек товаров
|
||||
*/
|
||||
static async searchCards(apiKey: string, searchText: string, limit: number = 100): Promise<WildberriesCard[]> {
|
||||
static async searchCards(apiKey: string, searchTerm: string, limit = 50): Promise<WildberriesCard[]> {
|
||||
const filter: WildberriesCardFilter = {
|
||||
sort: {
|
||||
settings: {
|
||||
cursor: {
|
||||
limit
|
||||
},
|
||||
filter: {
|
||||
textSearch: searchText
|
||||
textSearch: searchTerm,
|
||||
withPhoto: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,63 +134,70 @@ export class WildberriesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация API ключа WB
|
||||
* Получение всех карточек товаров с пагинацией
|
||||
*/
|
||||
static async getAllCards(apiKey: string, limit = 100): Promise<WildberriesCard[]> {
|
||||
const filter: WildberriesCardFilter = {
|
||||
settings: {
|
||||
cursor: {
|
||||
limit
|
||||
},
|
||||
filter: {
|
||||
withPhoto: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.getCards(apiKey, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение складов WB
|
||||
*/
|
||||
static async getWarehouses(apiKey: string): Promise<WildberriesWarehouse[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.supplierUrl}/api/v3/warehouses`, {
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data: WildberriesWarehousesResponse = await response.json()
|
||||
return data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error fetching warehouses:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка валидности API ключа
|
||||
*/
|
||||
static async validateApiKey(apiKey: string): Promise<boolean> {
|
||||
try {
|
||||
await this.getWarehouses(apiKey)
|
||||
return true
|
||||
const response = await fetch(`${this.contentUrl}/content/v2/get/cards/list`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
cursor: {
|
||||
limit: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
console.error('WB API key validation failed:', error)
|
||||
console.error('Error validating API key:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить информацию о поставке
|
||||
*/
|
||||
static async getSupplyInfo(apiKey: string, supplyId: string): Promise<unknown> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/v3/supplies/${supplyId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error fetching WB supply info:', error)
|
||||
throw new Error('Ошибка получения информации о поставке')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список поставок
|
||||
*/
|
||||
static async getSupplies(apiKey: string, limit: number = 1000, next: number = 0): Promise<unknown> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/v3/supplies?limit=${limit}&next=${next}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`WB API Error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Error fetching WB supplies:', error)
|
||||
throw new Error('Ошибка получения списка поставок')
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user