'use client' import { useQuery, useMutation } from '@apollo/client' import { ArrowLeft, Building2, Search, Package, Plus, Minus, ShoppingCart, Calendar, Truck, Box, FileText, AlertCircle, Settings, DollarSign, X, } from 'lucide-react' import Image from 'next/image' import { useRouter } from 'next/navigation' import React, { useState } from 'react' import { toast } from 'sonner' import { Sidebar } from '@/components/dashboard/sidebar' import { OrganizationAvatar } from '@/components/market/organization-avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { DatePicker } from '@/components/ui/date-picker' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations' import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_PRODUCTS, GET_COUNTERPARTY_SERVICES, GET_COUNTERPARTY_SUPPLIES, GET_AVAILABLE_SUPPLIES_FOR_RECIPE, } from '@/graphql/queries' import { useAuth } from '@/hooks/useAuth' import { useSidebar } from '@/hooks/useSidebar' import { AddGoodsModal } from './add-goods-modal' // Интерфейсы согласно rules2.md 9.7 interface GoodsSupplier { id: string inn: string name?: string fullName?: string type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE' address?: string phones?: Array<{ value: string }> emails?: Array<{ value: string }> users?: Array<{ id: string; avatar?: string; managerName?: string }> createdAt: string rating?: number market?: string // Принадлежность к рынку согласно rules-complete.md v10.0 } interface GoodsProduct { id: string name: string description?: string price: number category?: { name: string } images: string[] mainImage?: string article: string // Артикул поставщика organization: { id: string name: string } quantity?: number unit?: string weight?: number dimensions?: { length: number width: number height: number } } interface SelectedGoodsItem { id: string name: string sku: string price: number selectedQuantity: number unit?: string category?: string supplierId: string supplierName: string completeness?: string // Комплектность согласно rules2.md 9.7.2 recipe?: string // Рецептура/состав specialRequirements?: string // Особые требования parameters?: Array<{ name: string; value: string }> // Параметры товара } interface LogisticsCompany { id: string name: string estimatedCost: number deliveryDays: number type: 'EXPRESS' | 'STANDARD' | 'ECONOMY' } // Новые интерфейсы для компонентов рецептуры interface FulfillmentService { id: string name: string description?: string price: number category?: string } interface FulfillmentConsumable { id: string name: string price: number quantity: number unit?: string } interface SellerConsumable { id: string name: string pricePerUnit: number warehouseStock: number unit?: string } interface WBCard { id: string title: string nmID: string vendorCode?: string brand?: string } interface ProductRecipe { productId: string selectedServices: string[] selectedFFConsumables: string[] selectedSellerConsumables: string[] selectedWBCard?: string } export function CreateSuppliersSupplyPage() { const router = useRouter() const { user: _user } = useAuth() const { getSidebarMargin } = useSidebar() // Основные состояния const [selectedSupplier, setSelectedSupplier] = useState(null) const [selectedGoods, setSelectedGoods] = useState([]) const [searchQuery, setSearchQuery] = useState('') const [productSearchQuery] = useState('') // Обязательные поля согласно rules2.md 9.7.8 const [deliveryDate, setDeliveryDate] = useState('') // Выбор логистики согласно rules2.md 9.7.7 const [selectedLogistics, setSelectedLogistics] = useState('auto') // "auto" или ID компании // Выбор фулфилмента согласно rules2.md 9.7.2 const [selectedFulfillment, setSelectedFulfillment] = useState('') // Модальное окно для детального добавления товара const [selectedProductForModal, setSelectedProductForModal] = useState(null) const [isModalOpen, setIsModalOpen] = useState(false) const [isCreatingSupply, setIsCreatingSupply] = useState(false) // Состояния для компонентов рецептуры const [productRecipes, setProductRecipes] = useState>({}) const [productQuantities, setProductQuantities] = useState>({}) // Все выбранные товары для персистентности согласно rules-complete.md 9.2.2.1 const [allSelectedProducts, setAllSelectedProducts] = useState< (GoodsProduct & { selectedQuantity: number; supplierId: string; supplierName: string })[] >([]) // Загружаем партнеров-поставщиков согласно rules2.md 13.3 const { data: counterpartiesData, loading: counterpartiesLoading, error: counterpartiesError, } = useQuery(GET_MY_COUNTERPARTIES, { errorPolicy: 'all', // Показываем все ошибки, но не прерываем работу onError: (error) => { try { console.warn('🚨 GET_MY_COUNTERPARTIES ERROR:', { errorMessage: error?.message || 'Unknown error', hasGraphQLErrors: !!error?.graphQLErrors?.length, hasNetworkError: !!error?.networkError, }) } catch (logError) { console.warn('❌ Error in counterparties error handler:', logError) } }, }) // Загружаем каталог товаров согласно rules2.md 13.3 // Товары поставщика загружаются из Product таблицы where organizationId = поставщик.id const { data: productsData, loading: productsLoading, error: productsError, } = useQuery(GET_ORGANIZATION_PRODUCTS, { variables: { organizationId: selectedSupplier?.id || '', // Избегаем undefined для обязательного параметра search: productSearchQuery, // Используем поисковый запрос для фильтрации category: '', // Пока без фильтра по категории type: 'PRODUCT', // КРИТИЧЕСКИ ВАЖНО: показываем только PRODUCT, не CONSUMABLE согласно development-checklist.md }, skip: !selectedSupplier || !selectedSupplier.id, // Более строгая проверка fetchPolicy: 'network-only', // Обходим кеш для получения актуальных данных errorPolicy: 'all', // Показываем все ошибки, но не прерываем работу onError: (error) => { try { console.warn('🚨 GET_ORGANIZATION_PRODUCTS ERROR:', { errorMessage: error?.message || 'Unknown error', hasGraphQLErrors: !!error?.graphQLErrors?.length, hasNetworkError: !!error?.networkError, variables: { organizationId: selectedSupplier?.id || 'not_selected', search: productSearchQuery || 'empty', category: '', type: 'PRODUCT', }, selectedSupplier: selectedSupplier ? { id: selectedSupplier.id, name: selectedSupplier.name || selectedSupplier.fullName || 'Unknown', } : 'not_selected', }) } catch (logError) { console.warn('❌ Error in error handler:', logError) } }, }) // Мутация создания поставки const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER) // Запросы для компонентов рецептуры const { data: fulfillmentServicesData } = useQuery(GET_COUNTERPARTY_SERVICES, { variables: { organizationId: selectedFulfillment || '' }, skip: !selectedFulfillment, errorPolicy: 'all', }) const { data: fulfillmentConsumablesData } = useQuery(GET_COUNTERPARTY_SUPPLIES, { variables: { organizationId: selectedFulfillment || '' }, skip: !selectedFulfillment, errorPolicy: 'all', }) const { data: sellerConsumablesData } = useQuery(GET_AVAILABLE_SUPPLIES_FOR_RECIPE, { skip: !selectedFulfillment, errorPolicy: 'all', }) // TODO: Нужен запрос для получения карточек товаров селлера // const { data: wbCardsData } = useQuery(GET_MY_WILDBERRIES_SUPPLIES, { // skip: !user?.organization?.id, // errorPolicy: 'all', // }) // Фильтруем только партнеров-поставщиков согласно rules2.md 13.3 const allCounterparties = counterpartiesData?.myCounterparties || [] // Извлекаем данные для компонентов рецептуры const fulfillmentServices: FulfillmentService[] = fulfillmentServicesData?.counterpartyServices || [] const fulfillmentConsumables: FulfillmentConsumable[] = fulfillmentConsumablesData?.counterpartySupplies || [] const sellerConsumables: SellerConsumable[] = sellerConsumablesData?.getAvailableSuppliesForRecipe || [] const _wbCards: WBCard[] = [] // Временно отключено // Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3 const wholesaleSuppliers = allCounterparties.filter((cp: any) => { try { return cp && cp.type === 'WHOLESALE' } catch (error) { console.warn('❌ Error filtering wholesale suppliers:', error) return false } }) const suppliers = wholesaleSuppliers.filter((cp: GoodsSupplier) => { try { if (!cp) return false const searchLower = searchQuery.toLowerCase() return ( cp.name?.toLowerCase().includes(searchLower) || cp.fullName?.toLowerCase().includes(searchLower) || cp.inn?.includes(searchQuery) || cp.phones?.some((phone) => phone.value?.includes(searchQuery)) ) } catch (error) { console.warn('❌ Error filtering suppliers by search:', error) return false } }) const isLoading = counterpartiesLoading // Получаем товары выбранного поставщика согласно rules2.md 13.3 // Теперь фильтрация происходит на сервере через GraphQL запрос const products = (productsData?.organizationProducts || []).filter((product: any) => { try { return product && product.id && product.name } catch (error) { console.warn('❌ Error filtering products:', error) return false } }) // Отладочные логи согласно development-checklist.md console.warn('🛒 CREATE_SUPPLIERS_SUPPLY DEBUG:', { selectedSupplier: selectedSupplier ? { id: selectedSupplier.id, name: selectedSupplier.name, type: selectedSupplier.type, } : null, counterpartiesStatus: { loading: counterpartiesLoading, error: counterpartiesError?.message, dataCount: counterpartiesData?.myCounterparties?.length || 0, }, productsStatus: { loading: productsLoading, error: productsError?.message, dataCount: products.length, hasData: !!productsData?.organizationProducts, productSample: products.slice(0, 3).map((p) => ({ id: p.id, name: p.name, article: p.article })), }, }) // Моковые логистические компании согласно rules2.md 9.7.7 // Функции для работы с рынками согласно rules-complete.md v10.0 const getMarketLabel = (market?: string) => { const marketLabels = { sadovod: 'Садовод', 'tyak-moscow': 'ТЯК Москва', 'opt-market': 'ОПТ Маркет', } return marketLabels[market as keyof typeof marketLabels] || market } const getMarketBadgeStyle = (market?: string) => { const styles = { sadovod: 'bg-green-500/20 text-green-300 border-green-500/30', 'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30', 'opt-market': 'bg-purple-500/20 text-purple-300 border-purple-500/30', } return styles[market as keyof typeof styles] || 'bg-gray-500/20 text-gray-300 border-gray-500/30' } // Получаем логистические компании из партнеров const logisticsCompanies = allCounterparties?.filter((partner) => partner.type === 'LOGIST') || [] // Моковые фулфилмент-центры согласно rules2.md 9.7.2 const fulfillmentCenters = [ { id: 'ff1', name: 'СФ Центр Москва', address: 'г. Москва, ул. Складская 10' }, { id: 'ff2', name: 'СФ Центр СПб', address: 'г. Санкт-Петербург, пр. Логистический 5' }, { id: 'ff3', name: 'СФ Центр Екатеринбург', address: 'г. Екатеринбург, ул. Промышленная 15' }, ] // Функции для работы с количеством товаров в карточках согласно rules2.md 13.3 const getProductQuantity = (productId: string): number => { return productQuantities[productId] || 0 } const setProductQuantity = (productId: string, quantity: number): void => { setProductQuantities((prev) => ({ ...prev, [productId]: Math.max(0, quantity), })) } // Removed unused updateProductQuantity function // Добавление товара в корзину из карточки с заданным количеством const addToCart = (product: GoodsProduct) => { const quantity = getProductQuantity(product.id) if (quantity <= 0) { toast.error('Укажите количество товара') return } // Проверка остатков согласно rules2.md 9.7.9 if (product.quantity !== undefined && quantity > product.quantity) { toast.error(`Недостаточно товара на складе. Доступно: ${product.quantity} ${product.unit || 'шт'}`) return } if (!selectedSupplier) { toast.error('Не выбран поставщик') return } const newGoodsItem: SelectedGoodsItem = { id: product.id, name: product.name, sku: product.article, price: product.price, selectedQuantity: quantity, unit: product.unit, category: product.category?.name, supplierId: selectedSupplier.id, supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Неизвестный поставщик', } // Проверяем, есть ли уже такой товар в корзине const existingItemIndex = selectedGoods.findIndex((item) => item.id === product.id) if (existingItemIndex >= 0) { // Обновляем количество существующего товара const updatedGoods = [...selectedGoods] updatedGoods[existingItemIndex] = { ...updatedGoods[existingItemIndex], selectedQuantity: quantity, } setSelectedGoods(updatedGoods) toast.success(`Количество товара "${product.name}" обновлено в корзине`) } else { // Добавляем новый товар setSelectedGoods((prev) => [...prev, newGoodsItem]) // Инициализируем рецептуру для нового товара initializeProductRecipe(product.id) toast.success(`Товар "${product.name}" добавлен в корзину`) } // Сбрасываем количество в карточке setProductQuantity(product.id, 0) } // Removed unused openAddModal function // Функции для работы с рецептурой const initializeProductRecipe = (productId: string) => { if (!productRecipes[productId]) { setProductRecipes((prev) => ({ ...prev, [productId]: { productId, selectedServices: [], selectedFFConsumables: [], selectedSellerConsumables: [], selectedWBCard: undefined, }, })) } } const toggleService = (productId: string, serviceId: string) => { initializeProductRecipe(productId) setProductRecipes((prev) => { const recipe = prev[productId] const isSelected = recipe.selectedServices.includes(serviceId) return { ...prev, [productId]: { ...recipe, selectedServices: isSelected ? recipe.selectedServices.filter((id) => id !== serviceId) : [...recipe.selectedServices, serviceId], }, } }) } const toggleFFConsumable = (productId: string, consumableId: string) => { initializeProductRecipe(productId) setProductRecipes((prev) => { const recipe = prev[productId] const isSelected = recipe.selectedFFConsumables.includes(consumableId) return { ...prev, [productId]: { ...recipe, selectedFFConsumables: isSelected ? recipe.selectedFFConsumables.filter((id) => id !== consumableId) : [...recipe.selectedFFConsumables, consumableId], }, } }) } const toggleSellerConsumable = (productId: string, consumableId: string) => { initializeProductRecipe(productId) setProductRecipes((prev) => { const recipe = prev[productId] const isSelected = recipe.selectedSellerConsumables.includes(consumableId) return { ...prev, [productId]: { ...recipe, selectedSellerConsumables: isSelected ? recipe.selectedSellerConsumables.filter((id) => id !== consumableId) : [...recipe.selectedSellerConsumables, consumableId], }, } }) } const _setWBCard = (productId: string, cardId: string) => { initializeProductRecipe(productId) setProductRecipes((prev) => ({ ...prev, [productId]: { ...prev[productId], selectedWBCard: cardId, }, })) } // Расчет стоимости компонентов рецептуры const calculateRecipeCost = (productId: string) => { const recipe = productRecipes[productId] if (!recipe) return { services: 0, consumables: 0, total: 0 } const servicesTotal = recipe.selectedServices.reduce((sum, serviceId) => { const service = fulfillmentServices.find((s) => s.id === serviceId) return sum + (service?.price || 0) }, 0) const consumablesTotal = recipe.selectedFFConsumables.reduce((sum, consumableId) => { const consumable = fulfillmentConsumables.find((c) => c.id === consumableId) return sum + (consumable?.price || 0) }, 0) return { services: servicesTotal, consumables: consumablesTotal, total: servicesTotal + consumablesTotal, } } // Добавление товара в корзину из модального окна с дополнительными данными const addToCartFromModal = ( product: GoodsProduct, quantity: number, additionalData?: { completeness?: string recipe?: string specialRequirements?: string parameters?: Array<{ name: string; value: string }> customPrice?: number }, ) => { // Проверка остатков согласно rules2.md 9.7.9 if (product.quantity !== undefined && quantity > product.quantity) { toast.error(`Недостаточно товара на складе. Доступно: ${product.quantity} ${product.unit || 'шт'}`) return } const existingItem = selectedGoods.find((item) => item.id === product.id) const finalPrice = additionalData?.customPrice || product.price if (existingItem) { // Обновляем существующий товар setSelectedGoods((prev) => prev.map((item) => item.id === product.id ? { ...item, selectedQuantity: quantity, price: finalPrice, completeness: additionalData?.completeness, recipe: additionalData?.recipe, specialRequirements: additionalData?.specialRequirements, parameters: additionalData?.parameters, } : item, ), ) } else { // Добавляем новый товар const newItem: SelectedGoodsItem = { id: product.id, name: product.name, sku: product.article, price: finalPrice, selectedQuantity: quantity, unit: product.unit, category: product.category?.name, supplierId: selectedSupplier!.id, supplierName: selectedSupplier!.name || selectedSupplier!.fullName || '', completeness: additionalData?.completeness, recipe: additionalData?.recipe, specialRequirements: additionalData?.specialRequirements, parameters: additionalData?.parameters, } setSelectedGoods((prev) => [...prev, newItem]) } toast.success('Товар добавлен в корзину') } // Удаление из корзины const removeFromCart = (productId: string) => { setSelectedGoods((prev) => prev.filter((item) => item.id !== productId)) // Удаляем рецептуру товара setProductRecipes((prev) => { const updated = { ...prev } delete updated[productId] return updated }) toast.success('Товар удален из корзины') } // Функция расчета полной стоимости товара с рецептурой const getProductTotalWithRecipe = (productId: string, quantity: number) => { const product = allSelectedProducts.find(p => p.id === productId) if (!product) return 0 const baseTotal = product.price * quantity const recipe = productRecipes[productId] if (!recipe) return baseTotal // Услуги ФФ const servicesCost = (recipe.selectedServices || []).reduce((sum, serviceId) => { const service = fulfillmentServices.find(s => s.id === serviceId) return sum + (service ? service.price * quantity : 0) }, 0) // Расходники ФФ const ffConsumablesCost = (recipe.selectedFFConsumables || []).reduce((sum, consumableId) => { const consumable = fulfillmentConsumables.find(c => c.id === consumableId) // Используем такую же логику как в карточке - только price return sum + (consumable ? consumable.price * quantity : 0) }, 0) // Расходники селлера const sellerConsumablesCost = (recipe.selectedSellerConsumables || []).reduce((sum, consumableId) => { const consumable = sellerConsumables.find(c => c.id === consumableId) return sum + (consumable ? (consumable.pricePerUnit || 0) * quantity : 0) }, 0) return baseTotal + servicesCost + ffConsumablesCost + sellerConsumablesCost } // Расчеты для корзины - используем функцию расчета const totalGoodsAmount = selectedGoods.reduce((sum, item) => { return sum + getProductTotalWithRecipe(item.id, item.selectedQuantity) }, 0) const totalQuantity = selectedGoods.reduce((sum, item) => sum + item.selectedQuantity, 0) const totalAmount = totalGoodsAmount // Валидация формы согласно rules2.md 9.7.6 // Проверяем обязательность услуг фулфилмента согласно rules-complete.md const hasRequiredServices = selectedGoods.every((item) => productRecipes[item.id]?.selectedServices?.length > 0) const isFormValid = selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment && hasRequiredServices // Обязательно: каждый товар должен иметь услуги // Создание поставки const handleCreateSupply = async () => { if (!isFormValid) { if (!hasRequiredServices) { toast.error('Каждый товар должен иметь минимум 1 услугу фулфилмента') } else { toast.error('Заполните все обязательные поля') } return } setIsCreatingSupply(true) try { await createSupplyOrder({ variables: { supplierId: selectedSupplier!.id, fulfillmentCenterId: selectedFulfillment, items: selectedGoods.map((item) => ({ productId: item.id, quantity: item.selectedQuantity, recipe: productRecipes[item.id] ? { services: productRecipes[item.id].selectedServices, fulfillmentConsumables: productRecipes[item.id].selectedFFConsumables, sellerConsumables: productRecipes[item.id].selectedSellerConsumables, marketplaceCardId: productRecipes[item.id].selectedWBCard, } : undefined, })), deliveryDate, logisticsCompany: selectedLogistics === 'auto' ? null : selectedLogistics, type: 'ТОВАР', creationMethod: 'suppliers', }, }) toast.success('Поставка успешно создана') router.push('/supplies?tab=goods&subTab=suppliers') } catch (error) { console.error('Ошибка создания поставки:', error) toast.error('Ошибка при создании поставки') } finally { setIsCreatingSupply(false) } } // Получение минимальной и максимальной даты согласно rules2.md 9.7.8 const tomorrow = new Date() tomorrow.setDate(tomorrow.getDate() + 1) const maxDate = new Date() maxDate.setDate(maxDate.getDate() + 90) const minDateString = tomorrow.toISOString().split('T')[0] const maxDateString = maxDate.toISOString().split('T')[0] return (
{/* СТРУКТУРА ИЗ 4 БЛОКОВ согласно rules-complete.md 9.2 - кабинет селлера */}
{/* ЛЕВЫЙ БЛОК: ПОСТАВЩИКИ, КАРТОЧКИ ТОВАРОВ И ДЕТАЛЬНЫЙ КАТАЛОГ */}
{/* БЛОК 1: ПОСТАВЩИКИ - обязательный блок согласно rules-complete.md 9.2.1 */}
{/* Навигация и заголовок в одном блоке */}

Поставщики

setSearchQuery(e.target.value)} className="bg-white/10 border-white/20 text-white placeholder:text-white/60 pl-10 h-9 text-sm rounded-full transition-all duration-200 focus:border-white/30 backdrop-blur-sm" />
{/* Кнопка поиска в маркете */} {!isLoading && allCounterparties.length === 0 && (
)} {/* Контейнер скролла поставщиков согласно rules-complete.md 9.2.1 */}
{isLoading ? (
Загрузка поставщиков...
) : suppliers.length === 0 ? (

Поставщики товаров не найдены

{allCounterparties.length === 0 ? "У вас нет партнеров. Найдите поставщиков в маркете или добавьте их через раздел 'Партнеры'" : wholesaleSuppliers.length === 0 ? `Найдено ${allCounterparties.length} партнеров, но среди них нет поставщиков (тип WHOLESALE)` : searchQuery && suppliers.length === 0 ? 'Поставщики-партнеры не найдены по вашему запросу' : `Найдено ${suppliers.length} поставщиков-партнеров`}

) : (
{suppliers.map((supplier: GoodsSupplier) => (
setSelectedSupplier(supplier)} className={`flex-shrink-0 p-3 rounded-lg cursor-pointer group transition-all duration-200 w-[184px] md:w-[200px] lg:w-[216px] h-[92px] ${ selectedSupplier?.id === supplier.id ? 'bg-green-500/20 border border-green-400/60 shadow-lg ring-1 ring-green-400/30' : 'bg-white/5 border border-white/10 hover:border-white/20 hover:bg-white/10 hover:shadow-md' }`} >

{supplier.name || supplier.fullName}

ИНН: {supplier.inn}

{supplier.market && (
{getMarketLabel(supplier.market)}
)}
))}
)}
{/* БЛОК 2: КАРТОЧКИ ТОВАРОВ */}
{selectedSupplier && products.length > 0 && products.map((product: GoodsProduct) => { return (
{ // Добавляем товар в детальный каталог (блок 3) if (!allSelectedProducts.find((p) => p.id === product.id)) { setAllSelectedProducts((prev) => [ ...prev, { ...product, selectedQuantity: 0, supplierId: selectedSupplier.id, supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Поставщик', }, ]) // Инициализируем рецептуру для нового товара initializeProductRecipe(product.id) } }} > {product.mainImage ? ( {product.name} ) : (
)}
) })}
{/* БЛОК 3: КАТАЛОГ ТОВАРОВ согласно rules-complete.md 9.2.3 */}
{/* Верхняя панель каталога товаров согласно правилам 9.2.3.1 */} {!counterpartiesLoading && (
setProductSearchQuery(e.target.value)} className="pl-10 glass-input" />
)} {counterpartiesLoading && (
Загрузка партнеров...
)} {counterpartiesError && (
Ошибка загрузки партнеров: {counterpartiesError.message}
)}
{allSelectedProducts.length === 0 ? (

Каталог товаров пуст

Выберите поставщика для просмотра товаров

) : (
{allSelectedProducts.map((product) => { // Расчет стоимостей для каждого блока рецептуры const recipe = productRecipes[product.id] const selectedServicesIds = recipe?.selectedServices || [] const selectedFFConsumablesIds = recipe?.selectedFFConsumables || [] const selectedSellerConsumablesIds = recipe?.selectedSellerConsumables || [] // Стоимость услуг ФФ const servicesCost = selectedServicesIds.reduce((sum, serviceId) => { const service = fulfillmentServices.find((s) => s.id === serviceId) return sum + (service ? service.price * product.selectedQuantity : 0) }, 0) // Стоимость расходников ФФ const ffConsumablesCost = selectedFFConsumablesIds.reduce((sum, consumableId) => { const consumable = fulfillmentConsumables.find((c) => c.id === consumableId) return sum + (consumable ? consumable.price * product.selectedQuantity : 0) }, 0) // Стоимость расходников селлера const sellerConsumablesCost = selectedSellerConsumablesIds.reduce((sum, consumableId) => { const consumable = sellerConsumables.find((c) => c.id === consumableId) return sum + (consumable ? (consumable.pricePerUnit || 0) * product.selectedQuantity : 0) }, 0) // Общая стоимость товара с рецептурой const totalWithRecipe = product.price * product.selectedQuantity + servicesCost + ffConsumablesCost + sellerConsumablesCost // Debug: сравниваем с функцией расчета корзины const cartTotal = getProductTotalWithRecipe(product.id, product.selectedQuantity) if (Math.abs(totalWithRecipe - cartTotal) > 0.01) { console.log(`РАЗНИЦА для ${product.name}:`, { карточка: totalWithRecipe, корзина: cartTotal, базовая_цена: product.price * product.selectedQuantity, услуги: servicesCost, расходники_ФФ: ffConsumablesCost, расходники_селлера: sellerConsumablesCost, }) } return (
{/* Элегантный крестик удаления - согласно visual-design-rules.md */} {/* 7 модулей согласно rules-complete.md 9.2.3.2 + visual-design-rules.md */}
{/* 1. ИЗОБРАЖЕНИЕ (80px фиксированная ширина) */}
{product.mainImage ? ( {product.name} ) : (
)}
{/* 2. ОБЩАЯ ИНФОРМАЦИЯ (flex-1) - Правильная типографика согласно 2.2 */}

{product.name}

{product.price.toLocaleString('ru-RU')} ₽
{product.category && ( {product.category.name} )}

От: {product.supplierName}

Артикул: {product.article}

{/* 3. КОЛИЧЕСТВО/СУММА/ОСТАТОК (flex-1) */}
{product.quantity !== undefined && (
0 ? 'bg-green-400' : 'bg-red-400'}`} >
0 ? 'text-green-400' : 'text-red-400'}`}> {product.quantity > 0 ? `${product.quantity} шт` : 'Нет в наличии'}
)}
{ const inputValue = e.target.value const newQuantity = inputValue === '' ? 0 : Math.max(0, parseInt(inputValue) || 0) setAllSelectedProducts((prev) => prev.map((p) => p.id === product.id ? { ...p, selectedQuantity: newQuantity } : p, ), ) // Автоматическое добавление/удаление из корзины if (newQuantity > 0) { // Добавляем в корзину const existingItem = selectedGoods.find(item => item.id === product.id) if (!existingItem) { // Добавляем новый товар setSelectedGoods(prev => [...prev, { id: product.id, name: product.name, sku: product.article, price: product.price, category: product.category?.name || '', selectedQuantity: newQuantity, unit: product.unit || 'шт', supplierId: selectedSupplier?.id || '', supplierName: selectedSupplier?.name || selectedSupplier?.fullName || 'Поставщик', }]) // Инициализируем рецептуру initializeProductRecipe(product.id) } else { // Обновляем количество setSelectedGoods(prev => prev.map(item => item.id === product.id ? { ...item, selectedQuantity: newQuantity } : item, ), ) } } else { // Удаляем из корзины при количестве 0 setSelectedGoods(prev => prev.filter(item => item.id !== product.id)) } }} className="glass-input w-16 h-8 text-sm text-center text-white placeholder:text-white/50" placeholder="0" /> шт
{(product.price * product.selectedQuantity).toLocaleString('ru-RU')} ₽
{/* 4. УСЛУГИ ФФ (flex-1) - Правильные цвета согласно 1.2 */}
{servicesCost > 0 && (
{servicesCost.toLocaleString('ru-RU')} ₽
)}
🛠️ Услуги ФФ
{fulfillmentServices.length > 0 ? ( fulfillmentServices.map((service) => { const isSelected = selectedServicesIds.includes(service.id) return ( ) }) ) : (
{selectedFulfillment ? 'Загрузка...' : 'Выберите ФФ'}
)}
{/* 5. РАСХОДНИКИ ФФ (flex-1) */}
{ffConsumablesCost > 0 && (
{ffConsumablesCost.toLocaleString('ru-RU')} ₽
)}
📦 Расходники ФФ
{fulfillmentConsumables.length > 0 ? ( fulfillmentConsumables.map((consumable) => { const isSelected = selectedFFConsumablesIds.includes(consumable.id) return ( ) }) ) : (
{selectedFulfillment ? 'Загрузка...' : 'Выберите ФФ'}
)}
{/* 6. РАСХОДНИКИ СЕЛЛЕРА (flex-1) */}
{sellerConsumablesCost > 0 && (
{sellerConsumablesCost.toLocaleString('ru-RU')} ₽
)}
🏪 Расходники сел.
{sellerConsumables.length > 0 ? ( sellerConsumables.map((consumable) => { const isSelected = selectedSellerConsumablesIds.includes(consumable.id) return ( ) }) ) : (
Загрузка...
)}
{/* 7. МП + ИТОГО (flex-1) */}
Итого: {totalWithRecipe.toLocaleString('ru-RU')} ₽
) })}
)}
{/* БЛОК 4: КОРЗИНА И НАСТРОЙКИ - правый блок согласно rules-complete.md 9.2 */}

Корзина ({selectedGoods.length} шт)

{selectedGoods.length === 0 ? (

Корзина пуста

Добавьте товары из каталога для создания поставки

) : (
{selectedGoods.map((item) => { // Используем единую функцию расчета const itemTotalPrice = getProductTotalWithRecipe(item.id, item.selectedQuantity) const basePrice = item.price const priceWithRecipe = itemTotalPrice / item.selectedQuantity return (

{item.name}

{priceWithRecipe.toLocaleString('ru-RU')} ₽ × {item.selectedQuantity}

{itemTotalPrice.toLocaleString('ru-RU')} ₽
) })}
)} {selectedGoods.length > 0 && ( <>
{deliveryDate && (

Дата поставки:

{new Date(deliveryDate).toLocaleDateString('ru-RU')}

)} {selectedFulfillment && (

Фулфилмент-центр:

{allCounterparties?.find(c => c.id === selectedFulfillment)?.name || allCounterparties?.find(c => c.id === selectedFulfillment)?.fullName || 'Выбранный центр'}

)}

Логистическая компания:

Итого: {totalAmount.toLocaleString('ru-RU')} ₽
)}
{/* Модальное окно для детального добавления товара */} { setIsModalOpen(false) setSelectedProductForModal(null) }} onAdd={addToCartFromModal} />
) }