Files
sfera/src/components/supplies/create-suppliers-supply-page.tsx
Veronika Smirnova 4e8e217cdb Унификация дизайна корзины и обновление правил
- Убран текст "(с рецептурой)" из названий товаров в корзине
- Добавлен раздел 9.2.6 в rules-complete.md с единым стандартом корзины
- Определены обязательные размеры, структура и функциональность
- Запрещено отображение технических суффиксов в UI корзины

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-07 21:49:54 +03:00

1463 lines
71 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { 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<GoodsSupplier | null>(null)
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsItem[]>([])
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<string>('auto') // "auto" или ID компании
// Выбор фулфилмента согласно rules2.md 9.7.2
const [selectedFulfillment, setSelectedFulfillment] = useState<string>('')
// Модальное окно для детального добавления товара
const [selectedProductForModal, setSelectedProductForModal] = useState<GoodsProduct | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const [isCreatingSupply, setIsCreatingSupply] = useState(false)
// Состояния для компонентов рецептуры
const [productRecipes, setProductRecipes] = useState<Record<string, ProductRecipe>>({})
const [productQuantities, setProductQuantities] = useState<Record<string, number>>({})
// Все выбранные товары для персистентности согласно 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 (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} overflow-hidden transition-all duration-300 p-4`}>
<div className="h-full flex flex-col gap-4">
{/* СТРУКТУРА ИЗ 4 БЛОКОВ согласно rules-complete.md 9.2 - кабинет селлера */}
<div className="flex-1 flex gap-4 min-h-0">
{/* ЛЕВЫЙ БЛОК: ПОСТАВЩИКИ, КАРТОЧКИ ТОВАРОВ И ДЕТАЛЬНЫЙ КАТАЛОГ */}
<div className="flex-1 flex flex-col gap-4 min-h-0">
{/* БЛОК 1: ПОСТАВЩИКИ - обязательный блок согласно rules-complete.md 9.2.1 */}
<div
className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0 flex flex-col"
style={{ height: '180px' }}
>
<div className="p-4 flex-shrink-0">
{/* Навигация и заголовок в одном блоке */}
<div className="flex items-center justify-between gap-4 mb-4">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="sm"
onClick={() => router.push('/supplies?tab=goods&subTab=suppliers')}
className="glass-secondary hover:text-white/90 gap-2 transition-all duration-200 -ml-2"
>
<ArrowLeft className="h-3 w-3" />
Назад
</Button>
<div className="h-4 w-px bg-white/20"></div>
<Building2 className="h-5 w-5 text-blue-400" />
<h2 className="text-lg font-semibold text-white">Поставщики</h2>
</div>
<div className="w-64">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/90 h-4 w-4 drop-shadow-sm z-10" />
<Input
placeholder="Поиск поставщиков..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
</div>
</div>
{/* Кнопка поиска в маркете */}
{!isLoading && allCounterparties.length === 0 && (
<div className="mt-4">
<Button
variant="outline"
size="sm"
onClick={() => router.push('/market')}
className="glass-secondary hover:text-white/90 transition-all duration-200"
>
<Building2 className="h-3 w-3 mr-2" />
Найти поставщиков в маркете
</Button>
</div>
)}
{/* Контейнер скролла поставщиков согласно rules-complete.md 9.2.1 */}
<div className="flex-1 overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center h-44">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white/60"></div>
<span className="ml-3 text-white/70">Загрузка поставщиков...</span>
</div>
) : suppliers.length === 0 ? (
<div className="flex items-center justify-center h-44">
<div className="text-center space-y-3">
<div className="w-12 h-12 mx-auto bg-white/5 rounded-full flex items-center justify-center">
<Building2 className="h-6 w-6 text-white/40" />
</div>
<div>
<h3 className="text-base font-medium text-white mb-1">Поставщики товаров не найдены</h3>
<p className="text-white/60 text-xs max-w-md mx-auto">
{allCounterparties.length === 0
? "У вас нет партнеров. Найдите поставщиков в маркете или добавьте их через раздел 'Партнеры'"
: wholesaleSuppliers.length === 0
? `Найдено ${allCounterparties.length} партнеров, но среди них нет поставщиков (тип WHOLESALE)`
: searchQuery && suppliers.length === 0
? 'Поставщики-партнеры не найдены по вашему запросу'
: `Найдено ${suppliers.length} поставщиков-партнеров`}
</p>
</div>
</div>
</div>
) : (
<div className="h-44 overflow-hidden">
<div
className={`h-full ${
suppliers.length <= 4
? 'flex items-start gap-3 px-4'
: 'flex gap-3 overflow-x-auto px-4 pb-2 scrollbar-hide'
}`}
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
{suppliers.map((supplier: GoodsSupplier) => (
<div
key={supplier.id}
onClick={() => 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'
}`}
>
<div className="flex items-start gap-2 h-full">
<div className="flex-shrink-0">
<OrganizationAvatar organization={supplier} size="sm" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium text-sm truncate group-hover:text-white transition-colors">
{supplier.name || supplier.fullName}
</h4>
<p className="text-white/60 text-xs font-mono mt-1">ИНН: {supplier.inn}</p>
{supplier.market && (
<div className="mt-1">
<Badge
className={`text-xs font-medium border ${getMarketBadgeStyle(supplier.market)}`}
>
{getMarketLabel(supplier.market)}
</Badge>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* БЛОК 2: КАРТОЧКИ ТОВАРОВ */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0">
<div className="flex gap-3 overflow-x-auto p-4" style={{ scrollbarWidth: 'thin' }}>
{selectedSupplier &&
products.length > 0 &&
products.map((product: GoodsProduct) => {
return (
<div
key={product.id}
className="relative flex-shrink-0 bg-white/5 rounded-lg overflow-hidden border cursor-pointer transition-all duration-300 group w-20 h-28 border-white/10 hover:border-white/30"
onClick={() => {
// Добавляем товар в детальный каталог (блок 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 ? (
<Image
src={product.mainImage}
alt={product.name}
width={80}
height={112}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-6 w-6 text-white/40" />
</div>
)}
</div>
)
})}
</div>
</div>
{/* БЛОК 3: КАТАЛОГ ТОВАРОВ согласно rules-complete.md 9.2.3 */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-1 min-h-0 flex flex-col">
{/* Верхняя панель каталога товаров согласно правилам 9.2.3.1 */}
{!counterpartiesLoading && (
<div className="flex items-center gap-4 p-4 bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl mb-4">
<DatePicker
placeholder="Дата поставки"
value={deliveryDate}
onChange={setDeliveryDate}
className="min-w-[140px]"
/>
<Select value={selectedFulfillment} onValueChange={setSelectedFulfillment}>
<SelectTrigger className="glass-input min-w-[200px]">
<SelectValue placeholder="Выберите фулфилмент" />
</SelectTrigger>
<SelectContent>
{allCounterparties && allCounterparties.length > 0 ? (
allCounterparties
.filter((partner) => partner.type === 'FULFILLMENT')
.map((fulfillment) => (
<SelectItem key={fulfillment.id} value={fulfillment.id}>
{fulfillment.name || fulfillment.fullName}
</SelectItem>
))
) : (
<SelectItem value="" disabled>
Нет доступных фулфилмент-центров
</SelectItem>
)}
</SelectContent>
</Select>
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск товаров..."
value={productSearchQuery}
onChange={(e) => setProductSearchQuery(e.target.value)}
className="pl-10 glass-input"
/>
</div>
</div>
)}
{counterpartiesLoading && (
<div className="flex items-center justify-center p-4 bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl mb-4">
<div className="text-white/60 text-sm">Загрузка партнеров...</div>
</div>
)}
{counterpartiesError && (
<div className="flex items-center justify-center p-4 bg-red-500/10 backdrop-blur-xl border border-red-500/20 rounded-2xl mb-4">
<div className="text-red-300 text-sm">Ошибка загрузки партнеров: {counterpartiesError.message}</div>
</div>
)}
<div className="flex-1 overflow-y-auto p-6">
{allSelectedProducts.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-4">
<div className="w-24 h-24 mx-auto bg-blue-400/5 rounded-full flex items-center justify-center">
<Package className="h-12 w-12 text-blue-400/50" />
</div>
<div>
<h4 className="text-xl font-medium text-white mb-2">Каталог товаров пуст</h4>
<p className="text-white/60 max-w-sm mx-auto">Выберите поставщика для просмотра товаров</p>
</div>
</div>
</div>
) : (
<div className="space-y-4">
{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 (
<div
key={product.id}
className="glass-card border-white/10 hover:border-white/20 transition-all duration-300 group relative"
style={{ height: '140px' }}
>
{/* Элегантный крестик удаления - согласно visual-design-rules.md */}
<button
onClick={() => {
setAllSelectedProducts((prev) => prev.filter((p) => p.id !== product.id))
// Очищаем рецептуру
setProductRecipes((prev) => {
const updated = { ...prev }
delete updated[product.id]
return updated
})
}}
className="absolute top-3 right-3 z-10 w-7 h-7 flex items-center justify-center rounded-full bg-white/5 text-white/40 hover:bg-red-500/20 hover:text-red-400 transition-all duration-200 opacity-0 group-hover:opacity-100"
>
<Plus className="h-4 w-4 rotate-45" />
</button>
{/* 7 модулей согласно rules-complete.md 9.2.3.2 + visual-design-rules.md */}
<div className="flex h-full">
{/* 1. ИЗОБРАЖЕНИЕ (80px фиксированная ширина) */}
<div className="w-20 flex-shrink-0 p-3">
<div className="w-full h-full bg-white/5 rounded-lg overflow-hidden">
{product.mainImage ? (
<Image
src={product.mainImage}
alt={product.name}
width={80}
height={112}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-5 w-5 text-white/40" />
</div>
)}
</div>
</div>
{/* 2. ОБЩАЯ ИНФОРМАЦИЯ (flex-1) - Правильная типографика согласно 2.2 */}
<div className="flex-1 p-3 flex flex-col justify-center">
<div className="space-y-2">
<h4 className="text-white font-semibold text-sm truncate">{product.name}</h4>
<div className="text-white font-bold text-lg">
{product.price.toLocaleString('ru-RU')}
</div>
{product.category && (
<Badge className="bg-blue-500/20 text-blue-300 border-0 text-xs font-medium px-2 py-1">
{product.category.name}
</Badge>
)}
<p className="text-white/60 text-xs truncate">От: {product.supplierName}</p>
<p className="font-mono text-xs text-white/60 truncate">Артикул: {product.article}</p>
</div>
</div>
{/* 3. КОЛИЧЕСТВО/СУММА/ОСТАТОК (flex-1) */}
<div className="flex-1 p-3 flex flex-col justify-center">
<div className="space-y-3">
{product.quantity !== undefined && (
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'}`}
></div>
<span className={`text-xs ${product.quantity > 0 ? 'text-green-400' : 'text-red-400'}`}>
{product.quantity > 0 ? `${product.quantity} шт` : 'Нет в наличии'}
</span>
</div>
)}
<div className="flex items-center gap-2">
<Input
type="number"
min="0"
max={product.quantity}
value={product.selectedQuantity || ''}
onChange={(e) => {
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"
/>
<span className="text-white/60 text-sm">шт</span>
</div>
<div className="text-green-400 font-semibold text-sm">
{(product.price * product.selectedQuantity).toLocaleString('ru-RU')}
</div>
</div>
</div>
{/* 4. УСЛУГИ ФФ (flex-1) - Правильные цвета согласно 1.2 */}
<div className="flex-1 p-3 flex flex-col">
<div className="text-center mb-2">
{servicesCost > 0 && (
<div className="text-purple-400 font-semibold text-sm mb-1">
{servicesCost.toLocaleString('ru-RU')}
</div>
)}
<h6 className="text-purple-400 text-xs font-medium uppercase tracking-wider">🛠 Услуги ФФ</h6>
</div>
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
{fulfillmentServices.length > 0 ? (
fulfillmentServices.map((service) => {
const isSelected = selectedServicesIds.includes(service.id)
return (
<label
key={service.id}
className={`block w-full px-2 py-1.5 rounded-md text-xs cursor-pointer transition-all duration-200 hover:scale-105 ${
isSelected
? 'bg-purple-500/20 border-purple-500/30 text-purple-300 border'
: 'bg-white/5 text-white/70 hover:bg-white/10 hover:text-white'
}`}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => toggleService(product.id, service.id)}
className="sr-only"
/>
<div className="text-center">
<div className="truncate font-medium">{service.name}</div>
<div className="text-xs opacity-80 mt-1">
{service.price.toLocaleString('ru-RU')}
</div>
</div>
</label>
)
})
) : (
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">
{selectedFulfillment ? 'Загрузка...' : 'Выберите ФФ'}
</div>
)}
</div>
</div>
{/* 5. РАСХОДНИКИ ФФ (flex-1) */}
<div className="flex-1 p-3 flex flex-col">
<div className="text-center mb-2">
{ffConsumablesCost > 0 && (
<div className="text-orange-400 font-semibold text-sm mb-1">
{ffConsumablesCost.toLocaleString('ru-RU')}
</div>
)}
<h6 className="text-orange-400 text-xs font-medium uppercase tracking-wider">📦 Расходники ФФ</h6>
</div>
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
{fulfillmentConsumables.length > 0 ? (
fulfillmentConsumables.map((consumable) => {
const isSelected = selectedFFConsumablesIds.includes(consumable.id)
return (
<label
key={consumable.id}
className={`block w-full px-2 py-1.5 rounded-md text-xs cursor-pointer transition-all duration-200 hover:scale-105 ${
isSelected
? 'bg-orange-500/20 border-orange-500/30 text-orange-300 border'
: 'bg-white/5 text-white/70 hover:bg-white/10 hover:text-white'
}`}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => toggleFFConsumable(product.id, consumable.id)}
className="sr-only"
/>
<div className="text-center">
<div className="truncate font-medium">{consumable.name}</div>
<div className="text-xs opacity-80 mt-1">
{consumable.price.toLocaleString('ru-RU')}
</div>
</div>
</label>
)
})
) : (
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">
{selectedFulfillment ? 'Загрузка...' : 'Выберите ФФ'}
</div>
)}
</div>
</div>
{/* 6. РАСХОДНИКИ СЕЛЛЕРА (flex-1) */}
<div className="flex-1 p-3 flex flex-col">
<div className="text-center mb-2">
{sellerConsumablesCost > 0 && (
<div className="text-blue-400 font-semibold text-sm mb-1">
{sellerConsumablesCost.toLocaleString('ru-RU')}
</div>
)}
<h6 className="text-blue-400 text-xs font-medium uppercase tracking-wider">🏪 Расходники сел.</h6>
</div>
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
{sellerConsumables.length > 0 ? (
sellerConsumables.map((consumable) => {
const isSelected = selectedSellerConsumablesIds.includes(consumable.id)
return (
<label
key={consumable.id}
className={`block w-full px-2 py-1.5 rounded-md text-xs cursor-pointer transition-all duration-200 hover:scale-105 ${
isSelected
? 'bg-blue-500/20 border-blue-500/30 text-blue-300 border'
: 'bg-white/5 text-white/70 hover:bg-white/10 hover:text-white'
}`}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => toggleSellerConsumable(product.id, consumable.id)}
className="sr-only"
/>
<div className="text-center">
<div className="truncate font-medium">{consumable.name}</div>
<div className="text-xs opacity-80 mt-1">
{consumable.pricePerUnit} /{consumable.unit || 'шт'}
</div>
</div>
</label>
)
})
) : (
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">Загрузка...</div>
)}
</div>
</div>
{/* 7. МП + ИТОГО (flex-1) */}
<div className="flex-1 p-3 flex flex-col justify-between">
<div className="text-center">
<div className="text-green-400 font-bold text-lg mb-3">
Итого: {totalWithRecipe.toLocaleString('ru-RU')}
</div>
</div>
<div className="flex-1 flex flex-col justify-center">
<Select
value={recipe?.selectedWBCard || 'none'}
onValueChange={(value) => {
if (value !== 'none') {
_setWBCard(product.id, value)
}
}}
>
<SelectTrigger className="glass-input h-9 text-sm text-white">
<SelectValue placeholder="Не выбрано" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
Не выбрано
</SelectItem>
{/* TODO: Загружать из БД */}
<SelectItem value="card1">Карточка 1</SelectItem>
<SelectItem value="card2">Карточка 2</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
)
})}
</div>
)}
</div>
</div>
</div>
{/* БЛОК 4: КОРЗИНА И НАСТРОЙКИ - правый блок согласно rules-complete.md 9.2 */}
<div className="w-72 flex-shrink-0">
<div className="bg-white/10 backdrop-blur border-white/20 p-3 sticky top-0 rounded-2xl">
<h3 className="text-white font-semibold mb-3 flex items-center text-sm">
<ShoppingCart className="h-4 w-4 mr-2" />
Корзина ({selectedGoods.length} шт)
</h3>
{selectedGoods.length === 0 ? (
<div className="text-center py-6">
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-4 w-fit mx-auto mb-3">
<ShoppingCart className="h-8 w-8 text-purple-300" />
</div>
<p className="text-white/60 text-sm font-medium mb-2">Корзина пуста</p>
<p className="text-white/40 text-xs mb-3">Добавьте товары из каталога для создания поставки</p>
</div>
) : (
<div className="space-y-2 mb-4">
{selectedGoods.map((item) => {
// Используем единую функцию расчета
const itemTotalPrice = getProductTotalWithRecipe(item.id, item.selectedQuantity)
const basePrice = item.price
const priceWithRecipe = itemTotalPrice / item.selectedQuantity
return (
<div key={item.id} className="flex items-center justify-between bg-white/5 rounded-lg p-2">
<div className="flex-1 min-w-0">
<h4 className="text-white text-sm font-medium truncate">{item.name}</h4>
<p className="text-white/60 text-xs">
{priceWithRecipe.toLocaleString('ru-RU')} × {item.selectedQuantity}
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-green-400 font-bold text-sm">
{itemTotalPrice.toLocaleString('ru-RU')}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => removeFromCart(item.id)}
className="text-red-400 hover:text-red-300 hover:bg-red-500/20 p-1 h-6 w-6"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
)
})}
</div>
)}
{selectedGoods.length > 0 && (
<>
<div className="border-t border-white/10 pt-3 mb-3">
{deliveryDate && (
<div className="mb-2">
<p className="text-white/60 text-xs">Дата поставки:</p>
<p className="text-white text-xs font-medium">
{new Date(deliveryDate).toLocaleDateString('ru-RU')}
</p>
</div>
)}
{selectedFulfillment && (
<div className="mb-2">
<p className="text-white/60 text-xs">Фулфилмент-центр:</p>
<p className="text-white text-xs font-medium">
{allCounterparties?.find(c => c.id === selectedFulfillment)?.name ||
allCounterparties?.find(c => c.id === selectedFulfillment)?.fullName ||
'Выбранный центр'}
</p>
</div>
)}
<div className="mb-3">
<p className="text-white/60 text-xs mb-1">Логистическая компания:</p>
<select
value={selectedLogistics}
onChange={(e) => setSelectedLogistics(e.target.value)}
className="w-full bg-white/5 border-white/10 text-white h-7 text-xs rounded hover:border-white/30 focus:border-purple-400/50 transition-all duration-200"
>
<option value="auto" className="bg-gray-800 text-white">
Выбрать
</option>
{logisticsCompanies.length > 0 ? (
logisticsCompanies.map((logisticsPartner) => (
<option key={logisticsPartner.id} value={logisticsPartner.id} className="bg-gray-800 text-white">
{logisticsPartner.name || logisticsPartner.fullName}
</option>
))
) : (
<option value="" disabled className="bg-gray-800 text-white">
Нет доступных логистических партнеров
</option>
)}
</select>
</div>
</div>
<div className="flex items-center justify-between mb-3 pt-2 border-t border-white/10">
<span className="text-white font-semibold text-sm">Итого:</span>
<span className="text-green-400 font-bold text-lg">{totalAmount.toLocaleString('ru-RU')} </span>
</div>
<Button
onClick={handleCreateSupply}
disabled={!isFormValid || isCreatingSupply}
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white disabled:opacity-50 h-8 text-sm"
>
{isCreatingSupply ? 'Создание...' : 'Создать поставку'}
</Button>
</>
)}
</div>
</div>
</div>
</div>
</main>
{/* Модальное окно для детального добавления товара */}
<AddGoodsModal
product={selectedProductForModal}
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false)
setSelectedProductForModal(null)
}}
onAdd={addToCartFromModal}
/>
</div>
)
}