'use client' import { useQuery, useMutation } from '@apollo/client' import { format } from 'date-fns' import { ru } from 'date-fns/locale' import { Search, Plus, Minus, ShoppingCart, Calendar as CalendarIcon, Package, ArrowLeft, Check, Eye, ChevronLeft, ChevronRight, } from 'lucide-react' import React, { useState, useEffect } from 'react' import DatePicker from 'react-datepicker' import 'react-datepicker/dist/react-datepicker.css' import { toast } from 'sonner' import { Sidebar } from '@/components/dashboard/sidebar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { ProductCardSkeletonGrid } from '@/components/ui/product-card-skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations' import { GET_MY_COUNTERPARTIES, GET_COUNTERPARTY_SERVICES, GET_COUNTERPARTY_SUPPLIES } from '@/graphql/queries' import { useAuth } from '@/hooks/useAuth' import { useSidebar } from '@/hooks/useSidebar' import { apolloClient } from '@/lib/apollo-client' import { WildberriesService } from '@/services/wildberries-service' import { SelectedCard, WildberriesCard } from '@/types/supplies' interface Organization { id: string name?: string fullName?: string type: string } interface WBProductCardsProps { onBack: () => void onComplete: (selectedCards: SelectedCard[]) => void showSummary?: boolean setShowSummary?: (show: boolean) => void selectedCards?: SelectedCard[] setSelectedCards?: (cards: SelectedCard[]) => void } export function WBProductCards({ _onBack, onComplete, showSummary: externalShowSummary, setShowSummary: externalSetShowSummary, selectedCards: externalSelectedCards, setSelectedCards: externalSetSelectedCards, }: WBProductCardsProps) { const { user } = useAuth() const { getSidebarMargin } = useSidebar() const [searchTerm, setSearchTerm] = useState('') const [loading, setLoading] = useState(false) const [wbCards, setWbCards] = useState([]) const [selectedCards, setSelectedCards] = useState([]) // Товары в корзине // Используем внешнее состояние если передано const actualSelectedCards = externalSelectedCards !== undefined ? externalSelectedCards : selectedCards const actualSetSelectedCards = externalSetSelectedCards || setSelectedCards const [preparingCards, setPreparingCards] = useState([]) // Товары, готовящиеся к добавлению const [showSummary, setShowSummary] = useState(false) // Используем внешнее состояние если передано const actualShowSummary = externalShowSummary !== undefined ? externalShowSummary : showSummary const actualSetShowSummary = externalSetShowSummary || setShowSummary const [globalDeliveryDate, setGlobalDeliveryDate] = useState(undefined) const [organizationServices, setOrganizationServices] = useState<{ [orgId: string]: Array<{ id: string; name: string; description?: string; price: number }> }>({}) const [organizationSupplies, setOrganizationSupplies] = useState<{ [orgId: string]: Array<{ id: string; name: string; description?: string; price: number }> }>({}) const [selectedCardForDetails, setSelectedCardForDetails] = useState(null) const [currentImageIndex, setCurrentImageIndex] = useState(0) // Загружаем реальные карточки WB const { data: wbCardsData, loading: wbCardsLoading } = useQuery(GET_MY_WILDBERRIES_SUPPLIES, { errorPolicy: 'all', }) // Используем реальные данные из GraphQL запроса const realWbCards: WildberriesCard[] = (wbCardsData?.myWildberriesSupplies || []) .flatMap((supply: any) => supply.cards || []) .map((card: any) => ({ nmID: card.nmId || card.nmID, vendorCode: card.vendorCode || '', title: card.title || 'Без названия', description: card.description || '', brand: card.brand || '', object: card.object || '', parent: card.parent || '', countryProduction: card.countryProduction || '', supplierVendorCode: card.supplierVendorCode || '', mediaFiles: card.mediaFiles || [], sizes: card.sizes || [], })) // Загружаем контрагентов-фулфилментов const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES) // Автоматически загружаем услуги и расходники для уже выбранных организаций useEffect(() => { actualSelectedCards.forEach((sc) => { if (sc.selectedFulfillmentOrg && !organizationServices[sc.selectedFulfillmentOrg]) { loadOrganizationServices(sc.selectedFulfillmentOrg) } if (sc.selectedConsumableOrg && !organizationSupplies[sc.selectedConsumableOrg]) { loadOrganizationSupplies(sc.selectedConsumableOrg) } }) }, [selectedCards]) // Функция для загрузки услуг организации const loadOrganizationServices = async (organizationId: string) => { if (organizationServices[organizationId]) return // Уже загружены try { const response = await apolloClient.query({ query: GET_COUNTERPARTY_SERVICES, variables: { organizationId }, }) if (response.data?.counterpartyServices) { setOrganizationServices((prev) => ({ ...prev, [organizationId]: response.data.counterpartyServices, })) } } catch (error) { console.error('Ошибка загрузки услуг организации:', error) } } // Функция для загрузки расходников организации const loadOrganizationSupplies = async (organizationId: string) => { if (organizationSupplies[organizationId]) return // Уже загружены try { const response = await apolloClient.query({ query: GET_COUNTERPARTY_SUPPLIES, variables: { organizationId }, }) if (response.data?.counterpartySupplies) { setOrganizationSupplies((prev) => ({ ...prev, [organizationId]: response.data.counterpartySupplies, })) } } catch (error) { console.error('Ошибка загрузки расходников организации:', error) } } // Мутация для создания поставки 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) }, }) // Загружаем карточки из GraphQL запроса useEffect(() => { if (!wbCardsLoading && wbCardsData) { setWbCards(realWbCards) console.warn('Загружено карточек из GraphQL:', realWbCards.length) } }, [wbCardsData, wbCardsLoading, realWbCards]) 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 const apiToken = validationData?.token || validationData?.apiKey || validationData?.key || (wbApiKey as { apiKey?: string }).apiKey if (apiToken) { console.warn('Загружаем все карточки из WB API...') const cards = await WildberriesService.getAllCards(apiToken, 100) setWbCards(cards) console.warn('Загружено карточек из WB API:', cards.length) return } } // Если API ключ не настроен, используем данные из GraphQL console.warn('API ключ WB не настроен, используем данные из GraphQL') setWbCards(realWbCards) console.warn('Используются данные из GraphQL:', realWbCards.length) } catch (error) { console.error('Ошибка загрузки всех карточек WB:', error) // При ошибке используем данные из GraphQL setWbCards(realWbCards) console.warn('Используются данные из GraphQL (fallback):', realWbCards.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 const apiToken = validationData?.token || validationData?.apiKey || validationData?.key || (wbApiKey as { apiKey?: string }).apiKey if (apiToken) { console.warn('Поиск в WB API:', searchTerm) const cards = await WildberriesService.searchCards(apiToken, searchTerm, 50) setWbCards(cards) console.warn('Найдено карточек в WB API:', cards.length) return } } // Если API ключ не настроен, ищем в данных из GraphQL console.warn('API ключ WB не настроен, поиск в данных GraphQL:', searchTerm) // Фильтруем товары по поисковому запросу const filteredCards = realWbCards.filter( (card) => card.title.toLowerCase().includes(searchTerm.toLowerCase()) || card.brand.toLowerCase().includes(searchTerm.toLowerCase()) || card.nmID.toString().includes(searchTerm.toLowerCase()) || card.object?.toLowerCase().includes(searchTerm.toLowerCase()), ) setWbCards(filteredCards) console.warn('Найдено товаров в GraphQL данных:', filteredCards.length) } catch (error) { console.error('Ошибка поиска карточек WB:', error) // При ошибке ищем в данных из GraphQL const filteredCards = realWbCards.filter( (card) => card.title.toLowerCase().includes(searchTerm.toLowerCase()) || card.brand.toLowerCase().includes(searchTerm.toLowerCase()) || card.nmID.toString().includes(searchTerm.toLowerCase()) || card.object?.toLowerCase().includes(searchTerm.toLowerCase()), ) setWbCards(filteredCards) console.warn('Найдено товаров в GraphQL данных (fallback):', filteredCards.length) } finally { setLoading(false) } } const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: string | number | string[]) => { setPreparingCards((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) { const updatedCard = { ...existing, [field]: value } // При изменении количества сбрасываем цену, чтобы пользователь ввел новую if (field === 'selectedQuantity' && typeof value === 'number' && existing.customPrice > 0) { updatedCard.customPrice = 0 } return prev.map((sc) => (sc.card.nmID === card.nmID ? updatedCard : sc)) } else if (field === 'selectedQuantity' && typeof value === 'number' && value > 0) { const newSelectedCard: SelectedCard = { card, selectedQuantity: value as number, customPrice: 0, selectedFulfillmentOrg: '', selectedFulfillmentServices: [], selectedConsumableOrg: '', selectedConsumableServices: [], deliveryDate: '', selectedMarket: '', selectedPlace: '', sellerName: '', sellerPhone: '', selectedServices: [], } return [...prev, newSelectedCard] } return prev }) } // Функция для получения цены за единицу товара const getSelectedUnitPrice = (card: WildberriesCard): number => { const selected = preparingCards.find((sc) => sc.card.nmID === card.nmID) if (!selected || selected.selectedQuantity === 0) return 0 return selected.customPrice / selected.selectedQuantity } // Функция для получения общей стоимости товара const getSelectedTotalPrice = (card: WildberriesCard): number => { const selected = preparingCards.find((sc) => sc.card.nmID === card.nmID) return selected ? selected.customPrice : 0 } const getSelectedQuantity = (card: WildberriesCard): number => { const selected = preparingCards.find((sc) => sc.card.nmID === card.nmID) return selected ? selected.selectedQuantity : 0 } // Функция для добавления подготовленных товаров в корзину const addToCart = () => { const validCards = preparingCards.filter((card) => card.selectedQuantity > 0 && card.customPrice > 0) if (validCards.length === 0) { toast.error('Выберите товары и укажите цены') return } if (!globalDeliveryDate) { toast.error('Выберите дату поставки') return } const newCards = [...actualSelectedCards] validCards.forEach((prepCard) => { const cardWithDate = { ...prepCard, deliveryDate: globalDeliveryDate.toISOString().split('T')[0], } const existingIndex = newCards.findIndex((sc) => sc.card.nmID === prepCard.card.nmID) if (existingIndex >= 0) { // Обновляем существующий товар newCards[existingIndex] = cardWithDate } else { // Добавляем новый товар newCards.push(cardWithDate) } }) actualSetSelectedCards(newCards) // Очищаем подготовленные товары setPreparingCards([]) toast.success(`Добавлено ${validCards.length} товар(ов) в корзину`) } // Функции подсчета для подготовленных товаров const getPreparingTotalItems = () => { return preparingCards.reduce((sum, card) => sum + card.selectedQuantity, 0) } const getPreparingTotalAmount = () => { return preparingCards.reduce((sum, card) => sum + card.customPrice, 0) } const formatCurrency = (amount: number) => { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', minimumFractionDigits: 0, }).format(amount) } // Функция для получения цены услуги по ID const getServicePrice = (orgId: string, serviceId: string): number => { const services = organizationServices[orgId] if (!services) return 0 const service = services.find((s) => s.id === serviceId) return service ? service.price : 0 } // Функция для получения цены расходника по ID const getSupplyPrice = (orgId: string, supplyId: string): number => { const supplies = organizationSupplies[orgId] if (!supplies) return 0 const supply = supplies.find((s) => s.id === supplyId) return supply ? supply.price : 0 } // Функция для расчета стоимости услуг и расходников за 1 штуку const calculateAdditionalCostPerUnit = (sc: SelectedCard): number => { let servicesCost = 0 let suppliesCost = 0 // Стоимость услуг фулфилмента if (sc.selectedFulfillmentOrg && sc.selectedFulfillmentServices.length > 0) { servicesCost = sc.selectedFulfillmentServices.reduce((sum, serviceId) => { return sum + getServicePrice(sc.selectedFulfillmentOrg, serviceId) }, 0) } // Стоимость расходных материалов if (sc.selectedConsumableOrg && sc.selectedConsumableServices.length > 0) { suppliesCost = sc.selectedConsumableServices.reduce((sum, supplyId) => { return sum + getSupplyPrice(sc.selectedConsumableOrg, supplyId) }, 0) } return servicesCost + suppliesCost } const getTotalAmount = () => { return actualSelectedCards.reduce((sum, sc) => { const additionalCostPerUnit = calculateAdditionalCostPerUnit(sc) const totalCostPerUnit = sc.customPrice / sc.selectedQuantity + additionalCostPerUnit const totalCostForAllItems = totalCostPerUnit * sc.selectedQuantity return sum + totalCostForAllItems }, 0) } const getTotalItems = () => { return actualSelectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0) } // Функция больше не нужна, так как услуги выбираются индивидуально const handleCardClick = (card: WildberriesCard) => { setSelectedCardForDetails(card) setCurrentImageIndex(0) } const closeDetailsModal = () => { setSelectedCardForDetails(null) setCurrentImageIndex(0) } const nextImage = () => { if (selectedCardForDetails) { const images = WildberriesService.getCardImages(selectedCardForDetails) if (images.length > 1) { setCurrentImageIndex((prev) => (prev + 1) % images.length) } } } const prevImage = () => { if (selectedCardForDetails) { const images = WildberriesService.getCardImages(selectedCardForDetails) if (images.length > 1) { setCurrentImageIndex((prev) => (prev - 1 + images.length) % images.length) } } } const handleCreateSupply = async () => { try { const supplyInput = { deliveryDate: selectedCards[0]?.deliveryDate || null, cards: actualSelectedCards.map((sc) => ({ nmId: sc.card.nmID.toString(), vendorCode: sc.card.vendorCode, title: sc.card.title, brand: sc.card.brand, selectedQuantity: sc.selectedQuantity, customPrice: sc.customPrice, selectedFulfillmentOrg: sc.selectedFulfillmentOrg, selectedFulfillmentServices: sc.selectedFulfillmentServices, selectedConsumableOrg: sc.selectedConsumableOrg, selectedConsumableServices: sc.selectedConsumableServices, deliveryDate: sc.deliveryDate || null, mediaFiles: sc.card.mediaFiles, })), } await createSupply({ variables: { input: supplyInput } }) } catch (error) { console.error('Error creating supply:', error) } } if (actualShowSummary) { return (

Корзина

{actualSelectedCards.length} карточек товаров

{/* Массовое назначение поставщиков */}

Быстрое назначение

{ setGlobalDeliveryDate(date || undefined) if (date) { const dateString = date.toISOString().split('T')[0] actualSelectedCards.forEach((sc) => { updateCardSelection(sc.card, 'deliveryDate', dateString) }) } }} minDate={new Date()} inline locale="ru" />
{actualSelectedCards.map((sc) => { const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter( (org: Organization) => org.type === 'FULFILLMENT', ) const consumableOrgs = (counterpartiesData?.myCounterparties || []).filter( (org: Organization) => org.type === 'FULFILLMENT', ) return (
{sc.card.title}

{sc.card.title}

WB: {sc.card.nmID}

{/* Количество и цена */}
updateCardSelection(sc.card, 'selectedQuantity', parseInt(e.target.value) || 0) } className="bg-white/5 border-white/20 text-white mt-1" min="1" />
{ const pricePerUnit = e.target.value === '' ? 0 : parseFloat(e.target.value) || 0 const totalPrice = pricePerUnit * sc.selectedQuantity updateCardSelection(sc.card, 'customPrice', totalPrice) }} className="bg-white/5 border-white/20 text-white mt-1" placeholder="Введите цену за 1 штуку" /> {/* Показываем расчет дополнительных расходов */} {(() => { const additionalCost = calculateAdditionalCostPerUnit(sc) if (additionalCost > 0) { return (
Дополнительные расходы за 1 шт:
{sc.selectedFulfillmentServices.length > 0 && (
Услуги:{' '} {sc.selectedFulfillmentServices .map((serviceId) => { const price = getServicePrice(sc.selectedFulfillmentOrg, serviceId) const services = organizationServices[sc.selectedFulfillmentOrg] const service = services?.find((s) => s.id === serviceId) return service ? `${service.name} (${price}₽)` : '' }) .join(', ')}
)} {sc.selectedConsumableServices.length > 0 && (
Расходники:{' '} {sc.selectedConsumableServices .map((supplyId) => { const price = getSupplyPrice(sc.selectedConsumableOrg, supplyId) const supplies = organizationSupplies[sc.selectedConsumableOrg] const supply = supplies?.find((s) => s.id === supplyId) return supply ? `${supply.name} (${price}₽)` : '' }) .join(', ')}
)}
Итого доп. расходы: {formatCurrency(additionalCost)}
Полная стоимость за 1 шт:{' '} {formatCurrency(sc.customPrice / sc.selectedQuantity + additionalCost)}
) } return null })()}
{/* Услуги */}
{sc.selectedFulfillmentOrg && (
{organizationServices[sc.selectedFulfillmentOrg] ? ( organizationServices[sc.selectedFulfillmentOrg].length > 0 ? ( organizationServices[sc.selectedFulfillmentOrg].map((service) => { const isSelected = sc.selectedFulfillmentServices.includes(service.id) return ( ) }) ) : (
У данной организации нет услуг
) ) : (
Загрузка услуг...
)}
)}
{sc.selectedConsumableOrg && (
{organizationSupplies[sc.selectedConsumableOrg] ? ( organizationSupplies[sc.selectedConsumableOrg].length > 0 ? ( organizationSupplies[sc.selectedConsumableOrg].map((supply) => { const isSelected = sc.selectedConsumableServices.includes(supply.id) return ( ) }) ) : (
У данной организации нет расходников
) ) : (
Загрузка расходников...
)}
)}
{formatCurrency(sc.customPrice)} {sc.selectedQuantity > 0 && sc.customPrice > 0 && (

~{formatCurrency(sc.customPrice / sc.selectedQuantity)} за шт.

)}
) })}

Итого

Товаров: {getTotalItems()}
Карточек: {actualSelectedCards.length}
Общая сумма: {formatCurrency(getTotalAmount())}
) } return (
{/* Поиск */} {/* Поиск товаров и выбор даты поставки */}
{/* Поиск */}
setSearchTerm(e.target.value)} className="bg-white/5 border-white/20 text-white placeholder-white/50 h-9" onKeyPress={(e) => e.key === 'Enter' && searchCards()} />
{/* Выбор даты поставки */}
setGlobalDeliveryDate(date || undefined)} minDate={new Date()} inline locale="ru" />
{/* Кнопка поиска */}
{/* Состояние загрузки с красивыми скелетонами */} {(loading || wbCardsLoading) && } {/* Карточки товаров */} {!loading && !wbCardsLoading && wbCards.length > 0 && (
{wbCards.map((card) => { const selectedQuantity = getSelectedQuantity(card) const isSelected = selectedQuantity > 0 return (
{/* Изображение и основная информация */}
{card.title} handleCardClick(card)} /> {/* Индикатор товара WB */}
◉ WB
{/* Индикатор выбранного товара */} {isSelected && (
Подготовлен
)} {/* Overlay с кнопкой */}
{/* Заголовок и бренд */}
{card.brand} №{card.nmID}

handleCardClick(card)} > {card.title}

{/* Информация о товаре */}
Добавьте в поставку для настройки
{/* Компактное управление */}
{/* Количество - компактно */}
{ const value = e.target.value.replace(/[^0-9]/g, '') const numValue = Math.max(0, parseInt(value) || 0) updateCardSelection(card, 'selectedQuantity', numValue) }} onFocus={(e) => e.target.select()} className="flex-1 h-6 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none min-w-0" placeholder="0" />
{/* Цена - компактно, показывается только если есть количество */} {selectedQuantity > 0 && ( { const value = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.') const totalPrice = parseFloat(value) || 0 updateCardSelection(card, 'customPrice', totalPrice) }} onFocus={(e) => e.target.select()} className="w-full h-6 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent" placeholder={`Цена за ${selectedQuantity} шт`} /> )} {/* Результат - очень компактно */} {selectedQuantity > 0 && getSelectedTotalPrice(card) > 0 && (
{formatCurrency(getSelectedTotalPrice(card))}
~{formatCurrency(getSelectedUnitPrice(card))}/шт
)} {/* Индикатор подготовки к добавлению */} {selectedQuantity > 0 && (
Подготовлен
)}
) })}
)} {/* Плавающая кнопка "В корзину" для подготовленных товаров */} {preparingCards.length > 0 && getPreparingTotalItems() > 0 && (
)} {wbCards.length === 0 && !loading && !wbCardsLoading && (

Нет товаров

{user?.organization?.apiKeys?.find((key) => key.marketplace === 'WILDBERRIES')?.isActive ? ( <>

Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries, или загрузите все доступные карточки

) : ( <>

Для работы с полным функционалом WB API необходимо настроить API ключ Wildberries

Загружены товары из вашего склада

)}
)} {/* Модальное окно с детальной информацией о товаре */} !open && closeDetailsModal()}> Информация о товаре {selectedCardForDetails && (
{/* Изображение */}
{selectedCardForDetails.title} {/* Навигация по изображениям */} {WildberriesService.getCardImages(selectedCardForDetails).length > 1 && ( <>
{currentImageIndex + 1} из {WildberriesService.getCardImages(selectedCardForDetails).length}
)}
{/* Основная информация */}

{selectedCardForDetails.title}

Артикул WB: {selectedCardForDetails.nmID}

Бренд: {selectedCardForDetails.brand}
Категория: {selectedCardForDetails.object}
{selectedCardForDetails.description && (

Описание

{selectedCardForDetails.description}

)}
{/* Миниатюры изображений */} {WildberriesService.getCardImages(selectedCardForDetails).length > 1 && (
{WildberriesService.getCardImages(selectedCardForDetails).map((image, index) => ( {`${selectedCardForDetails.title} setCurrentImageIndex(index)} /> ))}
)}
)}
) }