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

730 lines
30 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 {
Search,
Plus,
Minus,
ShoppingCart,
Calendar,
Phone,
User,
MapPin,
Package,
Wrench,
ArrowLeft,
Check,
X
} 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 [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 { 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) {
const fulfillmentOrganizations = counterpartiesData.myCounterparties.filter(
(org: any) => org.type === 'FULFILLMENT'
)
// В реальном приложении здесь был бы запрос услуг для каждой организации
const mockServices: FulfillmentService[] = fulfillmentOrganizations.flatMap((org: any) => [
{
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])
const searchCards = async () => {
if (!searchTerm.trim()) return
setLoading(true)
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
if (!wbApiKey?.isActive) {
throw new Error('API ключ Wildberries не настроен')
}
const validationData = wbApiKey.validationData as Record<string, string>
const apiToken = validationData?.token || validationData?.apiKey
if (!apiToken) {
throw new Error('API токен не найден')
}
const cards = await WildberriesService.searchCards(apiToken, searchTerm)
setWbCards(cards)
} 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
}
]
}
])
} finally {
setLoading(false)
}
}
const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: any) => {
setSelectedCards(prev => {
const existing = prev.find(sc => sc.card.nmID === card.nmID)
if (field === 'selectedQuantity' && 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' && value > 0) {
const newSelectedCard: SelectedCard = {
card,
selectedQuantity: value,
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 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="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>
)
}
return (
<div className="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>
{/* Карточки товаров */}
{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)
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 ${isSelected ? 'ring-2 ring-purple-500/50' : ''}`}>
<div className="p-4 space-y-4">
{/* Изображение и основная информация */}
<div className="space-y-3">
<img
src={card.mediaFiles[0] || '/api/placeholder/300/200'}
alt={card.title}
className="w-full h-48 rounded-lg object-cover"
/>
<div>
<h3 className="text-white font-medium text-lg mb-1">{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>
<Badge className={`${maxQuantity > 10 ? 'bg-green-500/20 text-green-300' : maxQuantity > 0 ? 'bg-yellow-500/20 text-yellow-300' : 'bg-red-500/20 text-red-300'}`}>
{maxQuantity} шт.
</Badge>
</div>
</div>
</div>
{/* Количество */}
<div className="space-y-2">
<Label className="text-white text-sm">Количество для заказа</Label>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => updateCardSelection(card, 'selectedQuantity', Math.max(0, selectedQuantity - 1))}
disabled={selectedQuantity <= 0}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="number"
min="0"
max={maxQuantity}
value={selectedQuantity}
onChange={(e) => updateCardSelection(card, 'selectedQuantity', Math.min(maxQuantity, Math.max(0, parseInt(e.target.value) || 0)))}
className="bg-white/5 border-white/20 text-white text-center w-20 h-8"
/>
<Button
variant="ghost"
size="sm"
onClick={() => updateCardSelection(card, 'selectedQuantity', Math.min(maxQuantity, selectedQuantity + 1))}
disabled={selectedQuantity >= maxQuantity}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
{/* Детальные настройки для выбранных товаров */}
{isSelected && selectedCard && (
<div className="space-y-4 pt-4 border-t border-white/20">
{/* Рынок */}
<div className="space-y-2">
<Label className="text-white text-sm flex items-center">
<MapPin className="h-3 w-3 mr-1" />
Рынок
</Label>
<Select
value={selectedCard.selectedMarket}
onValueChange={(value) => updateCardSelection(card, 'selectedMarket', value)}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white">
<SelectValue placeholder="Выберите рынок" />
</SelectTrigger>
<SelectContent>
{markets.map((market) => (
<SelectItem key={market.value} value={market.value}>
{market.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Место на рынке */}
<div className="space-y-2">
<Label className="text-white text-sm">Место на рынке</Label>
<Input
placeholder="Например: Ряд 5, место 23"
value={selectedCard.selectedPlace}
onChange={(e) => updateCardSelection(card, 'selectedPlace', e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50"
/>
</div>
{/* Данные продавца */}
<div className="grid grid-cols-1 gap-2">
<div className="space-y-2">
<Label className="text-white text-sm flex items-center">
<User className="h-3 w-3 mr-1" />
Имя продавца
</Label>
<Input
placeholder="Имя продавца"
value={selectedCard.sellerName}
onChange={(e) => updateCardSelection(card, 'sellerName', e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50"
/>
</div>
<div className="space-y-2">
<Label className="text-white text-sm flex items-center">
<Phone className="h-3 w-3 mr-1" />
Телефон продавца
</Label>
<Input
placeholder="+7 (999) 123-45-67"
value={selectedCard.sellerPhone}
onChange={(e) => updateCardSelection(card, 'sellerPhone', e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50"
/>
</div>
</div>
{/* Дата поставки */}
<div className="space-y-2">
<Label className="text-white text-sm flex items-center">
<Calendar className="h-3 w-3 mr-1" />
Дата поставки
</Label>
<Input
type="date"
value={selectedCard.deliveryDate}
onChange={(e) => updateCardSelection(card, 'deliveryDate', e.target.value)}
className="bg-white/5 border-white/20 text-white"
/>
</div>
{/* Услуги фулфилмента */}
{fulfillmentServices.length > 0 && (
<div className="space-y-2">
<Label className="text-white text-sm flex items-center">
<Wrench className="h-3 w-3 mr-1" />
Услуги фулфилмента
</Label>
<div className="space-y-2 max-h-40 overflow-y-auto">
{fulfillmentServices.map((service) => (
<label key={service.id} className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={selectedCard.selectedServices.includes(service.id)}
onChange={(e) => {
const newServices = e.target.checked
? [...selectedCard.selectedServices, service.id]
: selectedCard.selectedServices.filter(id => id !== service.id)
updateCardSelection(card, 'selectedServices', newServices)
}}
className="rounded border-white/20 bg-white/5 text-purple-500"
/>
<div className="flex-1">
<div className="text-white text-sm">{service.name}</div>
<div className="text-white/60 text-xs">
{service.organizationName} {formatCurrency(service.price)}
</div>
</div>
</label>
))}
</div>
{selectedCards.length > 1 && (
<Button
size="sm"
variant="outline"
onClick={() => applyServicesToAll(selectedCard.selectedServices)}
className="w-full text-xs border-purple-500/30 text-purple-300 hover:bg-purple-500/10"
>
Применить ко всем карточкам
</Button>
)}
</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-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
</p>
</div>
</Card>
)}
</div>
)
}