Добавлены модели товаров и корзины для оптовиков, реализованы соответствующие мутации и запросы в GraphQL. Обновлен API для загрузки файлов с учетом новых типов данных. Улучшена обработка ошибок и добавлены новые функции для работы с категориями товаров.

This commit is contained in:
Bivekich
2025-07-17 16:36:07 +03:00
parent 6a94d51032
commit f377fbab5f
21 changed files with 3958 additions and 34 deletions

View File

@ -0,0 +1,205 @@
"use client"
import { useQuery } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { GET_CATEGORIES, GET_MY_CART } from '@/graphql/queries'
import { Package2, ArrowRight, Sparkles, ShoppingCart } from 'lucide-react'
interface Category {
id: string
name: string
createdAt: string
updatedAt: string
}
interface MarketCategoriesProps {
onSelectCategory: (categoryId: string, categoryName: string) => void
onShowCart?: () => void
}
export function MarketCategories({ onSelectCategory, onShowCart }: MarketCategoriesProps) {
const { data, loading, error } = useQuery(GET_CATEGORIES)
const { data: cartData } = useQuery(GET_MY_CART)
const categories: Category[] = data?.categories || []
const cart = cartData?.myCart
const uniqueItemsCount = cart?.items?.length || 0
if (loading) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-white border-t-transparent mx-auto mb-4"></div>
<p className="text-white/70">Загружаем категории...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<Package2 className="h-16 w-16 text-red-400/40 mx-auto mb-4" />
<p className="text-red-400">Ошибка загрузки категорий</p>
<p className="text-white/40 text-sm mt-2">{error.message}</p>
</div>
</div>
)
}
return (
<div className="h-full flex flex-col p-6">
{/* Заголовок */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center space-x-3">
<div className="p-3 rounded-xl bg-gradient-to-r from-purple-500/20 to-pink-500/20 border border-purple-500/30">
<Package2 className="h-8 w-8 text-purple-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-white mb-1">
Каталог товаров
</h1>
<p className="text-white/60">
Выберите категорию для просмотра товаров от оптовиков
</p>
</div>
</div>
{/* Кнопка корзины */}
{onShowCart && (
<Button
onClick={onShowCart}
className="bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 text-white border-purple-500/30 hover:border-purple-400/50 transition-all duration-200 shadow-lg px-6 py-3"
>
<ShoppingCart className="h-5 w-5 mr-2" />
Корзина {uniqueItemsCount > 0 && `(${uniqueItemsCount})`}
</Button>
)}
</div>
{/* Категории */}
<div className="flex-1 overflow-auto">
{categories.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<Package2 className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
Категории отсутствуют
</h3>
<p className="text-white/60">
Пока нет доступных категорий товаров
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{/* Карточка "Все товары" */}
<Card
onClick={() => onSelectCategory('', 'Все товары')}
className="group relative overflow-hidden bg-gradient-to-br from-indigo-500/10 via-purple-500/10 to-pink-500/10 backdrop-blur border-white/10 hover:border-white/20 transition-all duration-300 cursor-pointer hover:scale-105"
>
<div className="p-6 h-32 flex flex-col justify-between">
<div className="flex items-center justify-between">
<div className="p-3 rounded-lg bg-gradient-to-r from-indigo-500/20 to-purple-500/20 border border-indigo-500/30">
<Sparkles className="h-6 w-6 text-indigo-400" />
</div>
<ArrowRight className="h-5 w-5 text-white/40 group-hover:text-white/80 transition-colors" />
</div>
<div>
<h3 className="text-lg font-semibold text-white group-hover:text-white transition-colors">
Все товары
</h3>
<p className="text-white/60 text-sm">
Просмотреть весь каталог
</p>
</div>
</div>
{/* Эффект при наведении */}
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</Card>
{/* Карточки категорий */}
{categories.map((category, index) => {
// Разные градиенты для разных категорий
const gradients = [
'from-purple-500/10 via-pink-500/10 to-red-500/10',
'from-blue-500/10 via-cyan-500/10 to-teal-500/10',
'from-green-500/10 via-emerald-500/10 to-lime-500/10',
'from-yellow-500/10 via-orange-500/10 to-red-500/10',
'from-pink-500/10 via-rose-500/10 to-purple-500/10',
'from-indigo-500/10 via-blue-500/10 to-cyan-500/10',
'from-teal-500/10 via-green-500/10 to-emerald-500/10'
]
const borderColors = [
'border-purple-500/30',
'border-blue-500/30',
'border-green-500/30',
'border-orange-500/30',
'border-pink-500/30',
'border-indigo-500/30',
'border-teal-500/30'
]
const iconColors = [
'text-purple-400',
'text-blue-400',
'text-green-400',
'text-orange-400',
'text-pink-400',
'text-indigo-400',
'text-teal-400'
]
const bgColors = [
'from-purple-500/20 to-pink-500/20',
'from-blue-500/20 to-cyan-500/20',
'from-green-500/20 to-emerald-500/20',
'from-yellow-500/20 to-orange-500/20',
'from-pink-500/20 to-rose-500/20',
'from-indigo-500/20 to-blue-500/20',
'from-teal-500/20 to-green-500/20'
]
const gradient = gradients[index % gradients.length]
const borderColor = borderColors[index % borderColors.length]
const iconColor = iconColors[index % iconColors.length]
const bgColor = bgColors[index % bgColors.length]
return (
<Card
key={category.id}
onClick={() => onSelectCategory(category.id, category.name)}
className={`group relative overflow-hidden bg-gradient-to-br ${gradient} backdrop-blur border-white/10 hover:${borderColor} transition-all duration-300 cursor-pointer hover:scale-105`}
>
<div className="p-6 h-32 flex flex-col justify-between">
<div className="flex items-center justify-between">
<div className={`p-3 rounded-lg bg-gradient-to-r ${bgColor} border ${borderColor}`}>
<Package2 className={`h-6 w-6 ${iconColor}`} />
</div>
<ArrowRight className="h-5 w-5 text-white/40 group-hover:text-white/80 transition-colors" />
</div>
<div>
<h3 className="text-lg font-semibold text-white group-hover:text-white transition-colors">
{category.name}
</h3>
<p className="text-white/60 text-sm">
Товары категории
</p>
</div>
</div>
{/* Эффект при наведении */}
<div className={`absolute inset-0 bg-gradient-to-r ${gradient} opacity-0 group-hover:opacity-50 transition-opacity duration-300`} />
</Card>
)
})}
</div>
)}
</div>
</div>
)
}

View File

@ -1,5 +1,6 @@
"use client"
import { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Card } from '@/components/ui/card'
import { Sidebar } from '@/components/dashboard/sidebar'
@ -8,8 +9,29 @@ import { MarketFulfillment } from './market-fulfillment'
import { MarketSellers } from './market-sellers'
import { MarketLogistics } from './market-logistics'
import { MarketWholesale } from './market-wholesale'
import { MarketProducts } from './market-products'
import { MarketCategories } from './market-categories'
import { MarketRequests } from './market-requests'
export function MarketDashboard() {
const [productsView, setProductsView] = useState<'categories' | 'products' | 'cart'>('categories')
const [selectedCategory, setSelectedCategory] = useState<{ id: string; name: string } | null>(null)
const handleSelectCategory = (categoryId: string, categoryName: string) => {
setSelectedCategory({ id: categoryId, name: categoryName })
setProductsView('products')
}
const handleBackToCategories = () => {
setProductsView('categories')
setSelectedCategory(null)
}
const handleShowCart = () => {
setProductsView('cart')
setSelectedCategory(null)
}
return (
<div className="h-screen bg-gradient-smooth flex overflow-hidden">
<Sidebar />
@ -17,8 +39,18 @@ export function MarketDashboard() {
<div className="h-full w-full flex flex-col">
{/* Основной контент с табами */}
<div className="flex-1 overflow-hidden">
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-5 bg-white/5 backdrop-blur border-white/10 flex-shrink-0">
<Tabs
defaultValue="counterparties"
className="h-full flex flex-col"
onValueChange={(value) => {
if (value === 'products') {
// Сбрасываем состояние когда переходим на вкладку товаров
setProductsView('categories')
setSelectedCategory(null)
}
}}
>
<TabsList className="grid w-full grid-cols-6 bg-white/5 backdrop-blur border-white/10 flex-shrink-0">
<TabsTrigger
value="counterparties"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
@ -49,6 +81,12 @@ export function MarketDashboard() {
>
Оптовик
</TabsTrigger>
<TabsTrigger
value="products"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70"
>
Товары
</TabsTrigger>
</TabsList>
<TabsContent value="counterparties" className="flex-1 overflow-hidden mt-6">
@ -80,6 +118,22 @@ export function MarketDashboard() {
<MarketWholesale />
</Card>
</TabsContent>
<TabsContent value="products" className="flex-1 overflow-hidden mt-6">
<Card className="glass-card h-full overflow-hidden p-0">
{productsView === 'categories' ? (
<MarketCategories onSelectCategory={handleSelectCategory} onShowCart={handleShowCart} />
) : productsView === 'products' ? (
<MarketProducts
selectedCategoryId={selectedCategory?.id}
selectedCategoryName={selectedCategory?.name}
onBackToCategories={handleBackToCategories}
/>
) : (
<MarketRequests />
)}
</Card>
</TabsContent>
</Tabs>
</div>
</div>

View File

@ -0,0 +1,243 @@
"use client"
import { useState, useMemo } from 'react'
import { useQuery } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Search, ShoppingBag, Package2, ArrowLeft } from 'lucide-react'
import { ProductCard } from './product-card'
import { GET_ALL_PRODUCTS } from '@/graphql/queries'
interface Product {
id: string
name: string
article: string
description?: string
price: number
quantity: number
category?: { id: string; name: string }
brand?: string
color?: string
size?: string
weight?: number
dimensions?: string
material?: string
images: string[]
mainImage?: string
isActive: boolean
createdAt: string
organization: {
id: string
inn: string
name?: string
fullName?: string
type: string
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
users?: Array<{ id: string, avatar?: string, managerName?: string }>
}
}
interface MarketProductsProps {
selectedCategoryId?: string
selectedCategoryName?: string
onBackToCategories?: () => void
}
export function MarketProducts({ selectedCategoryId, selectedCategoryName, onBackToCategories }: MarketProductsProps) {
const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string>('')
const [localSearch, setLocalSearch] = useState('')
const { data, loading, refetch } = useQuery(GET_ALL_PRODUCTS, {
variables: {
search: searchTerm || null,
category: selectedCategoryId || selectedCategory || null
}
})
const products: Product[] = data?.allProducts || []
// Получаем уникальные категории из товаров
const categories = useMemo(() => {
const allCategories = products
.map(product => product.category?.name)
.filter(Boolean)
.filter((category, index, arr) => arr.indexOf(category) === index)
.sort()
return allCategories
}, [products])
const handleSearch = () => {
setSearchTerm(localSearch.trim())
}
// Фильтруем товары по доступности
const availableProducts = products.filter(product => product.isActive && product.quantity > 0)
const totalProducts = products.length
const availableCount = availableProducts.length
return (
<div className="h-full flex flex-col space-y-4 overflow-hidden p-6">
{/* Кнопка назад и заголовок */}
{selectedCategoryName && onBackToCategories && (
<div className="flex items-center space-x-3 flex-shrink-0">
<Button
onClick={onBackToCategories}
variant="outline"
className="glass-secondary text-white hover:text-white border-white/20 hover:border-white/40"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Назад к категориям
</Button>
<div>
<h2 className="text-xl font-bold text-white">{selectedCategoryName}</h2>
<p className="text-white/60 text-sm">Товары выбранной категории</p>
</div>
</div>
)}
{/* Поиск */}
<div className="flex space-x-4 flex-shrink-0">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск товаров по названию, артикулу, бренду..."
value={localSearch}
onChange={(e) => setLocalSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<Button
onClick={handleSearch}
className="bg-purple-500/20 hover:bg-purple-500/30 text-purple-300 border-purple-500/30 cursor-pointer h-10"
>
<Search className="h-4 w-4 mr-2" />
Найти
</Button>
</div>
{/* Категории */}
<div className="flex-shrink-0">
<h4 className="text-white font-medium mb-3 flex items-center">
<Package2 className="h-4 w-4 mr-2" />
Категории
</h4>
<div className="flex flex-wrap gap-2">
<Button
onClick={() => setSelectedCategory('')}
variant={selectedCategory === '' ? 'default' : 'outline'}
className={`h-8 px-3 text-sm transition-all ${
selectedCategory === ''
? 'bg-purple-500 hover:bg-purple-600 text-white'
: 'bg-white/5 hover:bg-white/10 text-white/70 border-white/20'
}`}
>
Все категории
</Button>
{categories.map((category) => (
<Button
key={category}
onClick={() => setSelectedCategory(category!)}
variant={selectedCategory === category ? 'default' : 'outline'}
className={`h-8 px-3 text-sm transition-all ${
selectedCategory === category
? 'bg-purple-500 hover:bg-purple-600 text-white'
: 'bg-white/5 hover:bg-white/10 text-white/70 border-white/20'
}`}
>
{category}
</Button>
))}
</div>
</div>
{/* Заголовок с статистикой */}
<div className="flex items-center justify-between flex-shrink-0">
<div className="flex items-center space-x-3">
<ShoppingBag className="h-6 w-6 text-purple-400" />
<div>
<h3 className="text-lg font-semibold text-white">Товары оптовиков</h3>
<p className="text-white/60 text-sm">
Найдено {totalProducts} товаров, доступно {availableCount}
</p>
</div>
</div>
{/* Активные фильтры */}
<div className="flex items-center space-x-2">
{searchTerm && (
<div className="bg-purple-500/20 px-3 py-1 rounded-full text-purple-300 text-sm flex items-center">
<Search className="h-3 w-3 mr-1" />
{searchTerm}
<button
onClick={() => {
setSearchTerm('')
setLocalSearch('')
}}
className="ml-2 hover:text-white"
>
×
</button>
</div>
)}
{selectedCategory && (
<div className="bg-purple-500/20 px-3 py-1 rounded-full text-purple-300 text-sm flex items-center">
<Package2 className="h-3 w-3 mr-1" />
{selectedCategory}
<button
onClick={() => setSelectedCategory('')}
className="ml-2 hover:text-white"
>
×
</button>
</div>
)}
</div>
</div>
{/* Список товаров */}
<div className="flex-1 overflow-auto min-h-0">
{loading ? (
<div className="flex items-center justify-center p-8">
<div className="text-white/60">Загружаем товары...</div>
</div>
) : products.length === 0 ? (
<div className="glass-card p-8">
<div className="text-center">
<ShoppingBag className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchTerm || selectedCategory ? 'Товары не найдены' : 'Пока нет товаров для отображения'}
</p>
<p className="text-white/40 text-sm mt-2">
{searchTerm || selectedCategory
? 'Попробуйте изменить условия поиска или фильтры'
: 'Оптовики еще не добавили свои товары'
}
</p>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
)}
</div>
{/* Пагинация будет добавлена позже если понадобится */}
</div>
)
}

View File

@ -0,0 +1,84 @@
"use client"
import { useQuery } from '@apollo/client'
import { CartItems } from '../cart/cart-items'
import { CartSummary } from '../cart/cart-summary'
import { GET_MY_CART } from '@/graphql/queries'
import { ShoppingCart, Package } from 'lucide-react'
export function MarketRequests() {
const { data, loading, error } = useQuery(GET_MY_CART)
const cart = data?.myCart
const hasItems = cart?.items && cart.items.length > 0
if (loading) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-4 border-white border-t-transparent mx-auto mb-4"></div>
<p className="text-white/70">Загружаем заявки...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<ShoppingCart className="h-16 w-16 text-red-400/40 mx-auto mb-4" />
<p className="text-red-400">Ошибка загрузки заявок</p>
<p className="text-white/40 text-sm mt-2">{error.message}</p>
</div>
</div>
)
}
return (
<div className="h-full w-full flex flex-col">
{/* Заголовок */}
<div className="flex items-center space-x-3 p-6 border-b border-white/10">
<ShoppingCart className="h-6 w-6 text-purple-400" />
<div>
<h1 className="text-xl font-bold text-white">Мои заявки</h1>
<p className="text-white/60">
{hasItems
? `${cart.totalItems} заявок на сумму ${new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' }).format(cart.totalPrice)}`
: 'У вас пока нет заявок'
}
</p>
</div>
</div>
{/* Основной контент */}
<div className="flex-1 overflow-hidden">
{hasItems ? (
<div className="h-full grid grid-cols-1 lg:grid-cols-3 gap-6 p-6">
{/* Заявки */}
<div className="lg:col-span-2">
<div className="glass-card h-full overflow-hidden">
<CartItems cart={cart} />
</div>
</div>
{/* Сводка заявок */}
<div className="lg:col-span-1">
<div className="glass-card h-fit">
<CartSummary cart={cart} />
</div>
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center p-8">
<Package className="h-24 w-24 text-white/20 mb-6" />
<h2 className="text-xl font-semibold text-white mb-2">Нет заявок</h2>
<p className="text-white/60 mb-6 max-w-md">
Добавьте товары в заявки из раздела &quot;Товары&quot;, чтобы создать заявку для оптовика
</p>
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,292 @@
"use client"
import { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
ShoppingCart,
Eye,
ChevronLeft,
ChevronRight
} from 'lucide-react'
import { OrganizationAvatar } from './organization-avatar'
import { Input } from '@/components/ui/input'
import Image from 'next/image'
import { useMutation } from '@apollo/client'
import { ADD_TO_CART } from '@/graphql/mutations'
import { GET_MY_CART } from '@/graphql/queries'
import { toast } from 'sonner'
interface Product {
id: string
name: string
article: string
description?: string
price: number
quantity: number
category?: { id: string; name: string }
brand?: string
color?: string
size?: string
weight?: number
dimensions?: string
material?: string
images: string[]
mainImage?: string
isActive: boolean
createdAt: string
organization: {
id: string
inn: string
name?: string
fullName?: string
type: string
address?: string
phones?: Array<{ value: string }>
emails?: Array<{ value: string }>
users?: Array<{ id: string, avatar?: string, managerName?: string }>
}
}
interface ProductCardProps {
product: Product
}
export function ProductCard({ product }: ProductCardProps) {
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isImageDialogOpen, setIsImageDialogOpen] = useState(false)
const [quantity, setQuantity] = useState(1)
const [addToCart, { loading: addingToCart }] = useMutation(ADD_TO_CART, {
refetchQueries: [{ query: GET_MY_CART }],
onCompleted: (data) => {
if (data.addToCart.success) {
toast.success(data.addToCart.message)
setQuantity(1) // Сбрасываем количество после добавления
} else {
toast.error(data.addToCart.message)
}
},
onError: (error) => {
toast.error('Ошибка при добавлении в заявки')
console.error('Error adding to cart:', error)
}
})
const displayPrice = new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(product.price)
const displayName = product.organization.name || product.organization.fullName || 'Неизвестная организация'
const images = product.images.length > 0 ? product.images : [product.mainImage].filter(Boolean)
const hasMultipleImages = images.length > 1
const handleAddToCart = async () => {
try {
await addToCart({
variables: {
productId: product.id,
quantity: quantity
}
})
} catch (error) {
console.error('Error adding to cart:', error)
}
}
const nextImage = () => {
setCurrentImageIndex((prev) => (prev + 1) % images.length)
}
const prevImage = () => {
setCurrentImageIndex((prev) => (prev - 1 + images.length) % images.length)
}
const getStockStatus = () => {
if (product.quantity === 0) return { text: 'Нет в наличии', color: 'bg-red-500/20 text-red-300' }
if (product.quantity < 10) return { text: 'Мало', color: 'bg-orange-500/20 text-orange-300' }
return { text: 'В наличии', color: 'bg-green-500/20 text-green-300' }
}
const stockStatus = getStockStatus()
const canAddToCart = product.quantity > 0 && product.isActive
return (
<div className="glass-card p-3 hover:bg-white/10 transition-all duration-300 group">
{/* Изображения товара */}
<div className="relative mb-3 aspect-video bg-white/5 rounded-lg overflow-hidden">
{images.length > 0 ? (
<>
<Image
src={images[currentImageIndex] || '/placeholder-product.png'}
alt={product.name}
fill
className="object-cover cursor-pointer"
onClick={() => setIsImageDialogOpen(true)}
/>
{/* Навигация по изображениям */}
{hasMultipleImages && (
<>
<button
onClick={prevImage}
className="absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
onClick={nextImage}
className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<ChevronRight className="h-4 w-4" />
</button>
{/* Индикаторы изображений */}
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex space-x-1">
{images.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full transition-all ${
index === currentImageIndex ? 'bg-white' : 'bg-white/50'
}`}
/>
))}
</div>
</>
)}
{/* Кнопка увеличения */}
<button
onClick={() => setIsImageDialogOpen(true)}
className="absolute top-2 right-2 bg-black/50 hover:bg-black/70 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<Eye className="h-4 w-4" />
</button>
</>
) : (
<div className="absolute inset-0 flex items-center justify-center">
<ShoppingCart className="h-8 w-8 text-white/20" />
</div>
)}
</div>
{/* Информация о товаре */}
<div className="space-y-2">
{/* Название и цена */}
<div>
<h3 className="font-semibold text-white text-sm mb-1 line-clamp-1">{product.name}</h3>
<div className="flex items-center justify-between">
<span className="text-base font-bold text-purple-300">{displayPrice}</span>
<Badge className={`${stockStatus.color} text-xs`}>
{stockStatus.text}
</Badge>
</div>
</div>
{/* Краткая информация */}
<div className="flex items-center justify-between text-xs text-white/60">
<span>Арт: {product.article}</span>
{product.category && (
<span className="bg-white/10 px-2 py-1 rounded text-xs">{product.category.name}</span>
)}
</div>
{/* Информация о продавце */}
<div className="border-t border-white/10 pt-2">
<div className="flex items-center space-x-2">
<OrganizationAvatar organization={product.organization} size="sm" />
<div className="flex-1 min-w-0">
<p className="font-medium text-white text-xs truncate">{displayName}</p>
<p className="text-xs text-white/50">ИНН: {product.organization.inn}</p>
</div>
</div>
</div>
{/* Выбор количества и добавление в заявки */}
{canAddToCart ? (
<div className="space-y-2">
{/* Выбор количества */}
<div className="flex items-center justify-between">
<span className="text-xs text-white/70">Количество:</span>
<Input
type="number"
min="1"
max={product.quantity}
value={quantity}
onChange={(e) => {
const value = parseInt(e.target.value) || 1
if (value >= 1 && value <= product.quantity) {
setQuantity(value)
}
}}
className="w-16 h-6 text-xs text-center glass-input text-white border-white/20"
/>
</div>
{/* Кнопка добавления в заявки */}
<Button
onClick={handleAddToCart}
disabled={addingToCart}
className="w-full h-8 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white border-0 text-xs"
>
<ShoppingCart className="h-3 w-3 mr-1" />
{addingToCart ? 'Добавление...' : 'В заявки'}
</Button>
</div>
) : (
<Button
disabled
className="w-full h-8 bg-gray-500/20 text-gray-400 border-0 text-xs cursor-not-allowed"
>
<ShoppingCart className="h-3 w-3 mr-1" />
Недоступно
</Button>
)}
</div>
{/* Диалог просмотра изображений */}
<Dialog open={isImageDialogOpen} onOpenChange={setIsImageDialogOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] bg-black/90 backdrop-blur-xl border border-white/20 p-0">
<DialogHeader className="sr-only">
<DialogTitle>Просмотр изображения товара {product.name}</DialogTitle>
</DialogHeader>
<div className="relative">
{images.length > 0 && (
<div className="relative aspect-square">
<Image
src={images[currentImageIndex] || '/placeholder-product.png'}
alt={product.name}
fill
className="object-contain"
/>
{hasMultipleImages && (
<>
<button
onClick={prevImage}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full"
>
<ChevronLeft className="h-6 w-6" />
</button>
<button
onClick={nextImage}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-3 rounded-full"
>
<ChevronRight className="h-6 w-6" />
</button>
</>
)}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 px-3 py-1 rounded-full text-white text-sm">
{currentImageIndex + 1} из {images.length}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
)
}