Добавлены модели товаров и корзины для оптовиков, реализованы соответствующие мутации и запросы в 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,219 @@
"use client"
import { useState } from 'react'
import { useMutation } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { DELETE_PRODUCT } from '@/graphql/mutations'
import { Edit3, Trash2, Package, Eye, EyeOff } from 'lucide-react'
import { toast } from 'sonner'
interface Product {
id: string
name: string
article: string
description: string
price: number
quantity: number
category: { id: string; name: string } | null
brand: string
color: string
size: string
weight: number
dimensions: string
material: string
images: string[]
mainImage: string
isActive: boolean
createdAt: string
updatedAt: string
}
interface ProductCardProps {
product: Product
onEdit: (product: Product) => void
onDeleted: () => void
}
export function ProductCard({ product, onEdit, onDeleted }: ProductCardProps) {
const [deleteProduct, { loading: deleting }] = useMutation(DELETE_PRODUCT)
const handleDelete = async () => {
try {
await deleteProduct({
variables: { id: product.id }
})
toast.success('Товар успешно удален')
onDeleted()
} catch (error) {
console.error('Error deleting product:', error)
toast.error('Ошибка при удалении товара')
}
}
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(price)
}
const getStatusColor = () => {
if (!product.isActive) return 'bg-gray-500/20 text-gray-300 border-gray-400/30'
if (product.quantity === 0) return 'bg-red-500/20 text-red-300 border-red-400/30'
if (product.quantity < 10) return 'bg-yellow-500/20 text-yellow-300 border-yellow-400/30'
return 'bg-green-500/20 text-green-300 border-green-400/30'
}
const getStatusText = () => {
if (!product.isActive) return 'Неактивен'
if (product.quantity === 0) return 'Нет в наличии'
if (product.quantity < 10) return 'Мало на складе'
return 'В наличии'
}
return (
<Card className="glass-card group relative overflow-hidden transition-all duration-300 hover:scale-[1.02] hover:shadow-xl hover:shadow-purple-500/20">
{/* Изображение товара */}
<div className="relative h-48 bg-white/5 overflow-hidden">
{product.mainImage || product.images[0] ? (
<img
src={product.mainImage || product.images[0]}
alt={product.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Package className="h-16 w-16 text-white/30" />
</div>
)}
{/* Статус товара */}
<div className="absolute top-2 left-2">
<Badge className={`text-xs px-2 py-1 ${getStatusColor()}`}>
{getStatusText()}
</Badge>
</div>
{/* Индикатор активности */}
<div className="absolute top-2 right-2">
{product.isActive ? (
<Eye className="h-4 w-4 text-green-300" />
) : (
<EyeOff className="h-4 w-4 text-gray-400" />
)}
</div>
{/* Кнопки управления */}
<div className="absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
<Button
size="sm"
variant="outline"
onClick={() => onEdit(product)}
className="p-1 h-7 w-7 bg-white/20 border-white/30 hover:bg-white/30 backdrop-blur"
>
<Edit3 className="h-3 w-3 text-white" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="p-1 h-7 w-7 bg-red-500/20 border-red-400/30 hover:bg-red-500/30 backdrop-blur"
>
<Trash2 className="h-3 w-3 text-white" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent className="glass-card border-white/10">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">Удалить товар?</AlertDialogTitle>
<AlertDialogDescription className="text-white/70">
Вы уверены, что хотите удалить товар &quot;{product.name}&quot;?
Это действие нельзя отменить.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="glass-secondary text-white hover:text-white">
Отмена
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={deleting}
className="bg-red-600 hover:bg-red-700 text-white"
>
{deleting ? 'Удаление...' : 'Удалить'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
{/* Информация о товаре */}
<div className="p-4 space-y-3">
{/* Название и артикул */}
<div>
<h3 className="text-white font-medium text-sm line-clamp-2 leading-tight">
{product.name}
</h3>
<p className="text-white/60 text-xs mt-1">
Арт. {product.article}
</p>
</div>
{/* Цена и количество */}
<div className="flex items-center justify-between">
<div className="text-white font-semibold">
{formatPrice(product.price)}
</div>
<div className="text-white/70 text-sm">
{product.quantity} шт.
</div>
</div>
{/* Дополнительная информация */}
<div className="space-y-1">
{product.category && (
<div className="flex items-center gap-2">
<Badge variant="outline" className="glass-secondary text-white/60 border-white/20 text-xs">
{product.category.name}
</Badge>
</div>
)}
<div className="flex flex-wrap gap-1">
{product.brand && (
<span className="text-white/50 text-xs bg-white/10 px-2 py-1 rounded">
{product.brand}
</span>
)}
{product.color && (
<span className="text-white/50 text-xs bg-white/10 px-2 py-1 rounded">
{product.color}
</span>
)}
{product.size && (
<span className="text-white/50 text-xs bg-white/10 px-2 py-1 rounded">
{product.size}
</span>
)}
</div>
</div>
{/* Описание (если есть) */}
{product.description && (
<p className="text-white/60 text-xs line-clamp-2 leading-relaxed">
{product.description}
</p>
)}
</div>
{/* Эффект градиента при наведении */}
<div className="absolute inset-0 bg-gradient-to-t from-purple-600/0 via-transparent to-transparent opacity-0 group-hover:opacity-20 transition-opacity duration-300 pointer-events-none" />
</Card>
)
}

View File

@ -0,0 +1,487 @@
"use client"
import { useState, useRef } from 'react'
import { useMutation, useQuery } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Card } from '@/components/ui/card'
import { CREATE_PRODUCT, UPDATE_PRODUCT } from '@/graphql/mutations'
import { GET_CATEGORIES } from '@/graphql/queries'
import { Upload, X, Star, Plus, Image as ImageIcon } from 'lucide-react'
import { toast } from 'sonner'
interface Product {
id: string
name: string
article: string
description: string
price: number
quantity: number
category: { id: string; name: string } | null
brand: string
color: string
size: string
weight: number
dimensions: string
material: string
images: string[]
mainImage: string
isActive: boolean
}
interface ProductFormProps {
product?: Product | null
onSave: () => void
onCancel: () => void
}
export function ProductForm({ product, onSave, onCancel }: ProductFormProps) {
const [formData, setFormData] = useState({
name: product?.name || '',
article: product?.article || '',
description: product?.description || '',
price: product?.price || 0,
quantity: product?.quantity || 0,
categoryId: product?.category?.id || 'none',
brand: product?.brand || '',
color: product?.color || '',
size: product?.size || '',
weight: product?.weight || 0,
dimensions: product?.dimensions || '',
material: product?.material || '',
images: product?.images || [],
mainImage: product?.mainImage || '',
isActive: product?.isActive ?? true
})
const [isUploading, setIsUploading] = useState(false)
const [uploadingImages, setUploadingImages] = useState<Set<number>>(new Set())
const fileInputRef = useRef<HTMLInputElement>(null)
const [createProduct, { loading: creating }] = useMutation(CREATE_PRODUCT)
const [updateProduct, { loading: updating }] = useMutation(UPDATE_PRODUCT)
// Загружаем категории
const { data: categoriesData } = useQuery(GET_CATEGORIES)
const loading = creating || updating
const handleInputChange = (field: string, value: string | number | boolean) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
const handleImageUpload = async (files: FileList) => {
const newUploadingIndexes = new Set<number>()
const startIndex = formData.images.length
// Добавляем плейсхолдеры для загружаемых изображений
const placeholders = Array.from(files).map((_, index) => {
newUploadingIndexes.add(startIndex + index)
return '' // Пустой URL как плейсхолдер
})
setUploadingImages(prev => new Set([...prev, ...newUploadingIndexes]))
setFormData(prev => ({
...prev,
images: [...prev.images, ...placeholders]
}))
try {
// Загружаем каждое изображение
const uploadPromises = Array.from(files).map(async (file, index) => {
const actualIndex = startIndex + index
const formData = new FormData()
formData.append('file', file)
formData.append('type', 'product')
const response = await fetch('/api/upload-file', {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error('Ошибка загрузки изображения')
}
const result = await response.json()
return { index: actualIndex, url: result.fileUrl }
})
const results = await Promise.all(uploadPromises)
// Обновляем URLs загруженных изображений
setFormData(prev => {
const newImages = [...prev.images]
results.forEach(({ index, url }) => {
newImages[index] = url
})
return {
...prev,
images: newImages,
mainImage: prev.mainImage || results[0]?.url || '' // Устанавливаем первое изображение как главное
}
})
toast.success('Изображения успешно загружены')
} catch (error) {
console.error('Error uploading images:', error)
toast.error('Ошибка загрузки изображений')
// Удаляем неудачные плейсхолдеры
setFormData(prev => ({
...prev,
images: prev.images.slice(0, startIndex)
}))
} finally {
// Убираем индикаторы загрузки
setUploadingImages(prev => {
const updated = new Set(prev)
newUploadingIndexes.forEach(index => updated.delete(index))
return updated
})
}
}
const handleRemoveImage = (indexToRemove: number) => {
setFormData(prev => {
const newImages = prev.images.filter((_, index) => index !== indexToRemove)
const removedImageUrl = prev.images[indexToRemove]
return {
...prev,
images: newImages,
mainImage: prev.mainImage === removedImageUrl ? (newImages[0] || '') : prev.mainImage
}
})
}
const handleSetMainImage = (imageUrl: string) => {
setFormData(prev => ({
...prev,
mainImage: imageUrl
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.name || !formData.article || formData.price <= 0) {
toast.error('Пожалуйста, заполните все обязательные поля')
return
}
try {
const input = {
name: formData.name,
article: formData.article,
description: formData.description || undefined,
price: formData.price,
quantity: formData.quantity,
categoryId: formData.categoryId && formData.categoryId !== 'none' ? formData.categoryId : undefined,
brand: formData.brand || undefined,
color: formData.color || undefined,
size: formData.size || undefined,
weight: formData.weight || undefined,
dimensions: formData.dimensions || undefined,
material: formData.material || undefined,
images: formData.images.filter(img => img), // Убираем пустые строки
mainImage: formData.mainImage || undefined,
isActive: formData.isActive
}
if (product) {
await updateProduct({
variables: { id: product.id, input }
})
toast.success('Товар успешно обновлен')
} else {
await createProduct({
variables: { input }
})
toast.success('Товар успешно создан')
}
onSave()
} catch (error: unknown) {
console.error('Error saving product:', error)
toast.error((error as Error).message || 'Ошибка при сохранении товара')
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Основная информация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Основная информация</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Название товара <span className="text-red-400">*</span>
</Label>
<Input
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="iPhone 15 Pro Max"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">
Артикул <span className="text-red-400">*</span>
</Label>
<Input
value={formData.article}
onChange={(e) => handleInputChange('article', e.target.value)}
placeholder="IP15PM-256-BLU"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">Описание</Label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="Подробное описание товара..."
className="glass-input text-white placeholder:text-white/40 w-full resize-none"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">
Цена () <span className="text-red-400">*</span>
</Label>
<Input
type="number"
step="0.01"
min="0"
value={formData.price || ''}
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)}
placeholder="99999.99"
className="glass-input text-white placeholder:text-white/40 h-10"
required
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Количество</Label>
<Input
type="number"
min="0"
value={formData.quantity || ''}
onChange={(e) => handleInputChange('quantity', parseInt(e.target.value) || 0)}
placeholder="100"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
</Card>
{/* Категоризация */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Категоризация</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">Категория</Label>
<Select
value={formData.categoryId}
onValueChange={(value) => handleInputChange('categoryId', value)}
>
<SelectTrigger className="glass-input text-white h-10">
<SelectValue placeholder="Выберите категорию" />
</SelectTrigger>
<SelectContent className="glass-card">
<SelectItem value="none">Без категории</SelectItem>
{categoriesData?.categories?.map((category: { id: string; name: string }) => (
<SelectItem key={category.id} value={category.id} className="text-white">
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Бренд</Label>
<Input
value={formData.brand}
onChange={(e) => handleInputChange('brand', e.target.value)}
placeholder="Apple"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
</Card>
{/* Характеристики */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Характеристики</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-white/80 text-sm mb-2 block">Цвет</Label>
<Input
value={formData.color}
onChange={(e) => handleInputChange('color', e.target.value)}
placeholder="Синий"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Размер</Label>
<Input
value={formData.size}
onChange={(e) => handleInputChange('size', e.target.value)}
placeholder="L, XL, 42"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Вес (кг)</Label>
<Input
type="number"
step="0.001"
min="0"
value={formData.weight || ''}
onChange={(e) => handleInputChange('weight', parseFloat(e.target.value) || 0)}
placeholder="0.221"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
<div>
<Label className="text-white/80 text-sm mb-2 block">Габариты</Label>
<Input
value={formData.dimensions}
onChange={(e) => handleInputChange('dimensions', e.target.value)}
placeholder="159.9 × 76.7 × 8.25 мм"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
<div className="mt-4">
<Label className="text-white/80 text-sm mb-2 block">Материал</Label>
<Input
value={formData.material}
onChange={(e) => handleInputChange('material', e.target.value)}
placeholder="Титан, стекло"
className="glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</Card>
{/* Изображения */}
<Card className="bg-white/5 backdrop-blur border-white/10 p-4">
<h3 className="text-white font-medium mb-4">Изображения товара</h3>
{/* Кнопка загрузки */}
<div className="mb-4">
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={(e) => e.target.files && handleImageUpload(e.target.files)}
className="hidden"
/>
<Button
type="button"
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="glass-secondary text-white hover:text-white cursor-pointer"
>
<Upload className="h-4 w-4 mr-2" />
{isUploading ? 'Загрузка...' : 'Добавить изображения'}
</Button>
</div>
{/* Галерея изображений */}
{formData.images.length > 0 && (
<div className="grid grid-cols-3 gap-4">
{formData.images.map((imageUrl, index) => (
<div key={index} className="relative group">
{uploadingImages.has(index) ? (
<div className="aspect-square bg-white/10 rounded-lg flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
</div>
) : (
<>
<img
src={imageUrl}
alt={`Товар ${index + 1}`}
className="w-full aspect-square object-cover rounded-lg"
/>
{/* Индикатор главного изображения */}
{formData.mainImage === imageUrl && (
<div className="absolute top-2 left-2">
<Star className="h-5 w-5 text-yellow-400 fill-current" />
</div>
)}
{/* Кнопки управления */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
{formData.mainImage !== imageUrl && (
<Button
type="button"
size="sm"
variant="outline"
onClick={() => handleSetMainImage(imageUrl)}
className="p-1 h-7 w-7 bg-white/20 border-white/30 hover:bg-white/30"
>
<Star className="h-3 w-3 text-white" />
</Button>
)}
<Button
type="button"
size="sm"
variant="outline"
onClick={() => handleRemoveImage(index)}
className="p-1 h-7 w-7 bg-red-500/20 border-red-400/30 hover:bg-red-500/30"
>
<X className="h-3 w-3 text-white" />
</Button>
</div>
</>
)}
</div>
))}
</div>
)}
</Card>
{/* Кнопки управления */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1 border-purple-400/30 text-purple-200 hover:bg-purple-500/10 hover:border-purple-300 transition-all duration-300"
>
Отмена
</Button>
<Button
type="submit"
disabled={loading || isUploading}
className="flex-1 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white border-0 shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all duration-300"
>
{loading ? 'Сохранение...' : (product ? 'Сохранить изменения' : 'Создать товар')}
</Button>
</div>
</form>
)
}

View File

@ -0,0 +1,207 @@
"use client"
import { useState } from 'react'
import { useQuery } from '@apollo/client'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Sidebar } from '@/components/dashboard/sidebar'
import { ProductForm } from './product-form'
import { ProductCard } from './product-card'
import { GET_MY_PRODUCTS } from '@/graphql/queries'
import { Plus, Search, Package } from 'lucide-react'
import { Input } from '@/components/ui/input'
interface Product {
id: string
name: string
article: string
description: string
price: number
quantity: number
category: { id: string; name: string } | null
brand: string
color: string
size: string
weight: number
dimensions: string
material: string
images: string[]
mainImage: string
isActive: boolean
createdAt: string
updatedAt: string
}
export function WarehouseDashboard() {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const { data, loading, error, refetch } = useQuery(GET_MY_PRODUCTS, {
errorPolicy: 'all'
})
const products: Product[] = data?.myProducts || []
// Фильтрация товаров по поисковому запросу
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.article.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.category?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.brand?.toLowerCase().includes(searchQuery.toLowerCase())
)
const handleCreateProduct = () => {
setEditingProduct(null)
setIsDialogOpen(true)
}
const handleEditProduct = (product: Product) => {
setEditingProduct(product)
setIsDialogOpen(true)
}
const handleProductSaved = () => {
setIsDialogOpen(false)
setEditingProduct(null)
refetch()
}
const handleProductDeleted = () => {
refetch()
}
if (error) {
return (
<div className="h-screen bg-gradient-smooth flex overflow-hidden">
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
<Card className="flex-1 bg-white/5 backdrop-blur border-white/10 p-6">
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">Ошибка загрузки</h3>
<p className="text-white/60 text-sm mb-4">
{error.message || 'Не удалось загрузить товары'}
</p>
<Button
onClick={() => refetch()}
className="bg-purple-600 hover:bg-purple-700 text-white"
>
Попробовать снова
</Button>
</div>
</div>
</Card>
</div>
</main>
</div>
)
}
return (
<div className="h-screen bg-gradient-smooth flex overflow-hidden">
<Sidebar />
<main className="flex-1 ml-56 px-6 py-4 overflow-hidden">
<div className="h-full w-full flex flex-col">
{/* Заголовок и поиск */}
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Склад товаров</h1>
<p className="text-white/70 text-sm">Управление ассортиментом вашего склада</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
onClick={handleCreateProduct}
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white border-0 shadow-lg shadow-purple-500/25 transition-all duration-300"
>
<Plus className="w-4 h-4 mr-2" />
Добавить товар
</Button>
</DialogTrigger>
<DialogContent className="glass-card max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white">
{editingProduct ? 'Редактировать товар' : 'Добавить новый товар'}
</DialogTitle>
</DialogHeader>
<ProductForm
product={editingProduct}
onSave={handleProductSaved}
onCancel={() => setIsDialogOpen(false)}
/>
</DialogContent>
</Dialog>
</div>
{/* Поиск */}
<div className="mb-4 flex-shrink-0">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/50" />
<Input
type="text"
placeholder="Поиск по названию, артикулу, категории..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="glass-input text-white placeholder:text-white/50 pl-10 h-10"
/>
</div>
</div>
{/* Основной контент */}
<Card className="flex-1 bg-white/5 backdrop-blur border-white/10 p-6 overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-full">
<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>
) : filteredProducts.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<Package className="h-16 w-16 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-medium text-white mb-2">
{searchQuery ? 'Товары не найдены' : 'Склад пуст'}
</h3>
<p className="text-white/60 text-sm mb-4">
{searchQuery
? 'Попробуйте изменить критерии поиска'
: 'Добавьте ваш первый товар на склад'
}
</p>
{!searchQuery && (
<Button
onClick={handleCreateProduct}
className="bg-purple-600 hover:bg-purple-700 text-white"
>
<Plus className="w-4 h-4 mr-2" />
Добавить товар
</Button>
)}
</div>
</div>
) : (
<div className="h-full overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onEdit={handleEditProduct}
onDeleted={handleProductDeleted}
/>
))}
</div>
</div>
)}
</Card>
</div>
</main>
</div>
)
}