Files
sfera/src/components/supplies/wb-product-cards.tsx

1139 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import React, { useState, useEffect } from 'react'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
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 { Sidebar } from '@/components/dashboard/sidebar'
import { useSidebar } from '@/hooks/useSidebar'
import {
Search,
Plus,
Minus,
ShoppingCart,
Calendar,
Phone,
User,
MapPin,
Package,
Wrench,
ArrowLeft,
Check,
Eye,
ChevronLeft,
ChevronRight
} from 'lucide-react'
import { WildberriesService } from '@/services/wildberries-service'
import { useAuth } from '@/hooks/useAuth'
import { useQuery, useMutation } from '@apollo/client'
import { GET_MY_COUNTERPARTIES } from '@/graphql/queries'
import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations'
import { toast } from 'sonner'
interface WildberriesCard {
nmID: number
vendorCode: string
sizes: Array<{
chrtID: number
techSize: string
wbSize: string
price: number
discountedPrice: number
quantity: number
}>
mediaFiles: string[]
object: string
parent: string
countryProduction: string
supplierVendorCode: string
brand: string
title: string
description: string
}
interface SelectedCard {
card: WildberriesCard
selectedQuantity: number
selectedMarket: string
selectedPlace: string
sellerName: string
sellerPhone: string
deliveryDate: string
selectedServices: string[]
}
interface FulfillmentService {
id: string
name: string
description?: string
price: number
organizationName: string
}
interface WBProductCardsProps {
onBack: () => void
onComplete: (selectedCards: SelectedCard[]) => void
}
export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
const { user } = useAuth()
const { getSidebarMargin } = useSidebar()
const [searchTerm, setSearchTerm] = useState('')
const [loading, setLoading] = useState(false)
const [wbCards, setWbCards] = useState<WildberriesCard[]>([])
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)
// Мутация для создания поставки
const [createSupply, { loading: creatingSupply }] = useMutation(CREATE_WILDBERRIES_SUPPLY, {
onCompleted: (data) => {
if (data.createWildberriesSupply.success) {
toast.success(data.createWildberriesSupply.message)
onComplete(selectedCards)
} else {
toast.error(data.createWildberriesSupply.message)
}
},
onError: (error) => {
toast.error('Ошибка при создании поставки')
console.error('Error creating supply:', error)
}
})
// Моковые данные рынков
const markets = [
{ value: 'sadovod', label: 'Садовод' },
{ value: 'luzhniki', label: 'Лужники' },
{ value: 'tishinka', label: 'Тишинка' },
{ value: 'food-city', label: 'Фуд Сити' }
]
useEffect(() => {
// Загружаем услуги фулфилмента из контрагентов
if (counterpartiesData?.myCounterparties) {
interface Organization {
id: string
name?: string
fullName?: string
type: string
}
const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter(
(org: Organization) => org.type === 'FULFILLMENT'
)
// В реальном приложении здесь был бы запрос услуг для каждой организации
const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: Organization) => [
{
id: `${org.id}-packaging`,
name: 'Упаковка товаров',
description: 'Профессиональная упаковка товаров',
price: 50,
organizationName: org.name || org.fullName
},
{
id: `${org.id}-labeling`,
name: 'Маркировка товаров',
description: 'Нанесение этикеток и штрих-кодов',
price: 30,
organizationName: org.name || org.fullName
},
{
id: `${org.id}-quality-check`,
name: 'Контроль качества',
description: 'Проверка качества товаров',
price: 100,
organizationName: org.name || org.fullName
}
])
setFulfillmentServices(mockServices)
}
}, [counterpartiesData])
// Автоматически загружаем товары при открытии компонента
useEffect(() => {
const loadCards = async () => {
setLoading(true)
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === '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>
// 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
}
}
// Если API ключ не настроен, оставляем пустое состояние
console.log('API ключ WB не настроен, показываем пустое состояние')
setWbCards([])
} catch (error) {
console.error('Ошибка загрузки карточек WB:', error)
// При ошибке API показываем пустое состояние
setWbCards([])
} finally {
setLoading(false)
}
}
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' && typeof value === 'number' && value === 0) {
return prev.filter(sc => sc.card.nmID !== card.nmID)
}
if (existing) {
return prev.map(sc =>
sc.card.nmID === card.nmID
? { ...sc, [field]: value }
: sc
)
} else if (field === 'selectedQuantity' && typeof value === 'number' && value > 0) {
const newSelectedCard: SelectedCard = {
card,
selectedQuantity: value as number,
selectedMarket: '',
selectedPlace: '',
sellerName: '',
sellerPhone: '',
deliveryDate: '',
selectedServices: []
}
return [...prev, newSelectedCard]
}
return prev
})
}
const getSelectedQuantity = (card: WildberriesCard): number => {
const selected = selectedCards.find(sc => sc.card.nmID === card.nmID)
return selected ? selected.selectedQuantity : 0
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(amount)
}
const getTotalAmount = () => {
return selectedCards.reduce((sum, sc) => {
const cardPrice = sc.card.sizes[0]?.discountedPrice || sc.card.sizes[0]?.price || 0
const servicesPrice = sc.selectedServices.reduce((serviceSum, serviceId) => {
const service = fulfillmentServices.find(s => s.id === serviceId)
return serviceSum + (service?.price || 0)
}, 0)
return sum + (cardPrice + servicesPrice) * sc.selectedQuantity
}, 0)
}
const getTotalItems = () => {
return selectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0)
}
const applyServicesToAll = (serviceIds: string[]) => {
setSelectedCards(prev =>
prev.map(sc => ({ ...sc, selectedServices: serviceIds }))
)
}
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 = {
deliveryDate: selectedCards[0]?.deliveryDate || null,
cards: selectedCards.map(sc => ({
nmId: sc.card.nmID.toString(),
vendorCode: sc.card.vendorCode,
title: sc.card.title,
brand: sc.card.brand,
price: sc.card.sizes[0]?.price || 0,
discountedPrice: sc.card.sizes[0]?.discountedPrice || null,
quantity: sc.card.sizes[0]?.quantity || 0,
selectedQuantity: sc.selectedQuantity,
selectedMarket: sc.selectedMarket,
selectedPlace: sc.selectedPlace,
sellerName: sc.sellerName,
sellerPhone: sc.sellerPhone,
deliveryDate: sc.deliveryDate || null,
mediaFiles: sc.card.mediaFiles,
selectedServices: sc.selectedServices
}))
}
await createSupply({ variables: { input: supplyInput } })
} catch (error) {
console.error('Error creating supply:', error)
}
}
if (showSummary) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
size="sm"
onClick={() => setShowSummary(false)}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Назад к товарам
</Button>
<div>
<h2 className="text-2xl font-bold text-white mb-1">Сводка заказа</h2>
<p className="text-white/60">Проверьте данные перед созданием поставки</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-white/60 hover:text-white hover:bg-white/10"
>
Отмена
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
{selectedCards.map((sc) => {
const cardPrice = sc.card.sizes[0]?.discountedPrice || sc.card.sizes[0]?.price || 0
const servicesPrice = sc.selectedServices.reduce((sum, serviceId) => {
const service = fulfillmentServices.find(s => s.id === serviceId)
return sum + (service?.price || 0)
}, 0)
const totalPrice = (cardPrice + servicesPrice) * sc.selectedQuantity
return (
<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'}
alt={sc.card.title}
className="w-20 h-20 rounded-lg object-cover"
/>
<div className="flex-1 space-y-3">
<div>
<h3 className="text-white font-medium">{sc.card.title}</h3>
<p className="text-white/60 text-sm">{sc.card.vendorCode}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span className="text-white/60">Количество:</span>
<span className="text-white ml-2">{sc.selectedQuantity}</span>
</div>
<div>
<span className="text-white/60">Рынок:</span>
<span className="text-white ml-2">{markets.find(m => m.value === sc.selectedMarket)?.label || 'Не выбран'}</span>
</div>
<div>
<span className="text-white/60">Место:</span>
<span className="text-white ml-2">{sc.selectedPlace || 'Не указано'}</span>
</div>
<div>
<span className="text-white/60">Продавец:</span>
<span className="text-white ml-2">{sc.sellerName || 'Не указан'}</span>
</div>
<div>
<span className="text-white/60">Телефон:</span>
<span className="text-white ml-2">{sc.sellerPhone || 'Не указан'}</span>
</div>
<div>
<span className="text-white/60">Дата поставки:</span>
<span className="text-white ml-2">{sc.deliveryDate || 'Не выбрана'}</span>
</div>
</div>
{sc.selectedServices.length > 0 && (
<div>
<p className="text-white/60 text-sm mb-2">Услуги:</p>
<div className="flex flex-wrap gap-1">
{sc.selectedServices.map(serviceId => {
const service = fulfillmentServices.find(s => s.id === serviceId)
return service ? (
<Badge key={serviceId} className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs">
{service.name} ({formatCurrency(service.price)})
</Badge>
) : null
})}
</div>
</div>
)}
<div className="text-right">
<span className="text-white font-bold text-lg">{formatCurrency(totalPrice)}</span>
</div>
</div>
</div>
</Card>
)
})}
</div>
<div className="lg:col-span-1">
<Card className="bg-white/10 backdrop-blur border-white/20 p-6 sticky top-4">
<h3 className="text-white font-semibold text-lg mb-4">Итого</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-white/60">Товаров:</span>
<span className="text-white">{getTotalItems()}</span>
</div>
<div className="flex justify-between">
<span className="text-white/60">Карточек:</span>
<span className="text-white">{selectedCards.length}</span>
</div>
<div className="border-t border-white/20 pt-3 flex justify-between">
<span className="text-white font-semibold">Общая сумма:</span>
<span className="text-white font-bold text-xl">{formatCurrency(getTotalAmount())}</span>
</div>
<Button
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
onClick={handleCreateSupply}
disabled={creatingSupply}
>
<Check className="h-4 w-4 mr-2" />
{creatingSupply ? 'Создание...' : 'Создать поставку'}
</Button>
</div>
</Card>
</div>
</div>
</div>
</main>
</div>
)
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Назад
</Button>
<div>
<h2 className="text-2xl font-bold text-white mb-1">Карточки товаров Wildberries</h2>
<p className="text-white/60">Найдите и выберите товары для поставки</p>
</div>
</div>
{selectedCards.length > 0 && (
<Button
onClick={() => setShowSummary(true)}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
>
<ShoppingCart className="h-4 w-4 mr-2" />
Корзина ({getTotalItems()})
</Button>
)}
</div>
{/* Поиск */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex space-x-3">
<div className="flex-1">
<Input
placeholder="Поиск товаров по названию, артикулу или бренду..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50"
onKeyPress={(e) => e.key === 'Enter' && searchCards()}
/>
</div>
<Button
onClick={searchCards}
disabled={loading || !searchTerm.trim()}
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600"
>
<Search className="h-4 w-4 mr-2" />
{loading ? 'Поиск...' : 'Найти'}
</Button>
</div>
</Card>
{/* Состояние загрузки */}
{loading && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{[...Array(12)].map((_, i) => (
<Card key={i} className="bg-white/10 backdrop-blur border-white/20 p-3 animate-pulse">
<div className="space-y-3">
<div className="bg-white/20 rounded-lg aspect-square w-full"></div>
<div className="space-y-2">
<div className="bg-white/20 rounded h-3 w-3/4"></div>
<div className="bg-white/20 rounded h-3 w-1/2"></div>
<div className="bg-white/20 rounded h-4 w-2/3"></div>
</div>
<div className="bg-white/20 rounded h-7 w-full"></div>
</div>
</Card>
))}
</div>
)}
{/* Карточки товаров */}
{!loading && wbCards.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{wbCards.map((card) => {
const selectedQuantity = getSelectedQuantity(card)
const isSelected = selectedQuantity > 0
const selectedCard = selectedCards.find(sc => sc.card.nmID === card.nmID)
const mainSize = card.sizes[0]
const maxQuantity = mainSize?.quantity || 0
const price = mainSize?.discountedPrice || mainSize?.price || 0
return (
<Card key={card.nmID} className={`bg-white/10 backdrop-blur border-white/20 transition-all hover:scale-105 hover:shadow-2xl group ${isSelected ? 'ring-2 ring-purple-500/50 bg-purple-500/10' : ''}`}>
<div className="p-3 space-y-3">
{/* Изображение и основная информация */}
<div className="space-y-2">
<div className="relative">
<div className="aspect-square relative bg-white/5 overflow-hidden rounded-lg">
<img
src={card.mediaFiles?.[0] || '/api/placeholder/300/300'}
alt={card.title}
className="w-full h-full object-cover cursor-pointer group-hover:scale-110 transition-transform duration-500"
onClick={() => handleCardClick(card)}
/>
{/* Количество в наличии */}
<div className="absolute top-2 right-2">
<Badge className={`${maxQuantity > 10 ? 'bg-green-500/80' : maxQuantity > 0 ? 'bg-yellow-500/80' : 'bg-red-500/80'} text-white border-0 backdrop-blur text-xs`}>
{maxQuantity}
</Badge>
</div>
{/* Overlay с кнопкой */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<Button
size="sm"
variant="secondary"
onClick={() => handleCardClick(card)}
className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30"
>
<Eye className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="space-y-2">
{/* Заголовок и бренд */}
<div>
<div className="flex items-center justify-between mb-1">
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 text-xs">
{card.brand}
</Badge>
</div>
<h3 className="text-white font-semibold text-sm mb-1 line-clamp-2 leading-tight cursor-pointer hover:text-purple-300 transition-colors" onClick={() => handleCardClick(card)}>
{card.title}
</h3>
<p className="text-white/60 text-xs mb-1">{card.vendorCode}</p>
</div>
{/* Цена */}
<div className="pt-1 border-t border-white/10">
<div className="text-white font-bold text-base">
{formatCurrency(price)}
</div>
</div>
</div>
</div>
{/* Управление количеством */}
<div className="space-y-2">
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => updateCardSelection(card, 'selectedQuantity', Math.max(0, selectedQuantity - 1))}
disabled={selectedQuantity <= 0}
className="h-7 w-7 p-0 text-white/60 hover:text-white hover:bg-white/10 border border-white/20"
>
<Minus className="h-3 w-3" />
</Button>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={selectedQuantity}
onChange={(e) => {
const value = e.target.value.replace(/[^0-9]/g, '')
const numValue = Math.max(0, Math.min(maxQuantity, parseInt(value) || 0))
updateCardSelection(card, 'selectedQuantity', numValue)
}}
onFocus={(e) => e.target.select()}
className="h-7 w-12 text-center bg-white/10 border border-white/20 text-white text-sm rounded focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
<Button
variant="ghost"
size="sm"
onClick={() => updateCardSelection(card, 'selectedQuantity', Math.min(maxQuantity, selectedQuantity + 1))}
disabled={selectedQuantity >= maxQuantity}
className="h-7 w-7 p-0 text-white/60 hover:text-white hover:bg-white/10 border border-white/20"
>
<Plus className="h-3 w-3" />
</Button>
{selectedQuantity > 0 && (
<Badge className="bg-gradient-to-r from-purple-500 to-pink-500 text-white border-0 text-xs ml-auto">
<ShoppingCart className="h-3 w-3 mr-1" />
{selectedQuantity}
</Badge>
)}
</div>
{/* Сумма для выбранного товара */}
{selectedQuantity > 0 && (
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 rounded p-1">
<div className="text-green-300 text-xs font-medium text-center">
{formatCurrency(price * selectedQuantity)}
</div>
</div>
)}
</div>
</div>
</Card>
)
})}
</div>
)}
{/* Плавающая корзина */}
{selectedCards.length > 0 && !showSummary && (
<div className="fixed bottom-6 right-6 z-50">
<Button
onClick={() => setShowSummary(true)}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-lg h-12 px-6"
>
<ShoppingCart className="h-5 w-5 mr-2" />
Корзина ({getTotalItems()}) {formatCurrency(getTotalAmount())}
</Button>
</div>
)}
{wbCards.length === 0 && !loading && (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
<div className="text-center max-w-md mx-auto">
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">Карточки товаров Wildberries</h3>
{user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive ? (
<>
<p className="text-white/60 mb-4 text-sm">
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге 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-3 text-sm">
Для работы с реальными карточками необходимо настроить API ключ Wildberries
</p>
<p className="text-white/40 text-xs mb-4">
Показаны демонстрационные товары для тестирования
</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>
</main>
</div>
)
}