Добавлены модели и функциональность для управления логистикой, включая создание, обновление и удаление логистических маршрутов через GraphQL. Обновлены компоненты для отображения и управления логистикой, улучшен интерфейс взаимодействия с пользователем. Реализованы новые типы данных и интерфейсы для логистики, а также улучшена обработка ошибок.
This commit is contained in:
425
src/components/supplies/wholesaler-products.tsx
Normal file
425
src/components/supplies/wholesaler-products.tsx
Normal file
@ -0,0 +1,425 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Package,
|
||||
Plus,
|
||||
Minus,
|
||||
ShoppingCart,
|
||||
Eye,
|
||||
Info
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Wholesaler {
|
||||
id: string
|
||||
inn: string
|
||||
name: string
|
||||
fullName: string
|
||||
address: string
|
||||
phone?: string
|
||||
email?: string
|
||||
rating: number
|
||||
productCount: number
|
||||
avatar?: string
|
||||
specialization: string[]
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
article: string
|
||||
description: string
|
||||
price: number
|
||||
quantity: number
|
||||
category: string
|
||||
brand?: string
|
||||
color?: string
|
||||
size?: string
|
||||
weight?: number
|
||||
dimensions?: string
|
||||
material?: string
|
||||
images: string[]
|
||||
mainImage?: string
|
||||
}
|
||||
|
||||
interface SelectedProduct extends Product {
|
||||
selectedQuantity: number
|
||||
}
|
||||
|
||||
interface WholesalerProductsProps {
|
||||
wholesaler: Wholesaler
|
||||
onBack: () => void
|
||||
onClose: () => void
|
||||
onSupplyCreated: () => void
|
||||
}
|
||||
|
||||
// Моковые данные товаров
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Смартфон Samsung Galaxy A54',
|
||||
article: 'SGX-A54-128',
|
||||
description: 'Смартфон с экраном 6.4", камерой 50 МП, 128 ГБ памяти',
|
||||
price: 28900,
|
||||
quantity: 150,
|
||||
category: 'Смартфоны',
|
||||
brand: 'Samsung',
|
||||
color: 'Черный',
|
||||
size: '6.4"',
|
||||
weight: 202,
|
||||
dimensions: '158.2 x 76.7 x 8.2 мм',
|
||||
material: 'Алюминий, стекло',
|
||||
images: ['/api/placeholder/300/300?text=Samsung+A54'],
|
||||
mainImage: '/api/placeholder/300/300?text=Samsung+A54'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Наушники Sony WH-1000XM4',
|
||||
article: 'SNY-WH1000XM4',
|
||||
description: 'Беспроводные наушники с шумоподавлением',
|
||||
price: 24900,
|
||||
quantity: 85,
|
||||
category: 'Наушники',
|
||||
brand: 'Sony',
|
||||
color: 'Черный',
|
||||
weight: 254,
|
||||
material: 'Пластик, кожа',
|
||||
images: ['/api/placeholder/300/300?text=Sony+WH1000XM4'],
|
||||
mainImage: '/api/placeholder/300/300?text=Sony+WH1000XM4'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Планшет iPad Air 10.9"',
|
||||
article: 'APL-IPADAIR-64',
|
||||
description: 'Планшет Apple iPad Air с чипом M1, 64 ГБ',
|
||||
price: 54900,
|
||||
quantity: 45,
|
||||
category: 'Планшеты',
|
||||
brand: 'Apple',
|
||||
color: 'Серый космос',
|
||||
size: '10.9"',
|
||||
weight: 461,
|
||||
dimensions: '247.6 x 178.5 x 6.1 мм',
|
||||
material: 'Алюминий',
|
||||
images: ['/api/placeholder/300/300?text=iPad+Air'],
|
||||
mainImage: '/api/placeholder/300/300?text=iPad+Air'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Ноутбук Lenovo ThinkPad E15',
|
||||
article: 'LNV-TE15-I5',
|
||||
description: 'Ноутбук 15.6" Intel Core i5, 8 ГБ ОЗУ, 256 ГБ SSD',
|
||||
price: 45900,
|
||||
quantity: 25,
|
||||
category: 'Ноутбуки',
|
||||
brand: 'Lenovo',
|
||||
color: 'Черный',
|
||||
size: '15.6"',
|
||||
weight: 1700,
|
||||
dimensions: '365 x 240 x 19.9 мм',
|
||||
material: 'Пластик',
|
||||
images: ['/api/placeholder/300/300?text=ThinkPad+E15'],
|
||||
mainImage: '/api/placeholder/300/300?text=ThinkPad+E15'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Умные часы Apple Watch SE',
|
||||
article: 'APL-AWSE-40',
|
||||
description: 'Умные часы Apple Watch SE 40 мм',
|
||||
price: 21900,
|
||||
quantity: 120,
|
||||
category: 'Умные часы',
|
||||
brand: 'Apple',
|
||||
color: 'Белый',
|
||||
size: '40 мм',
|
||||
weight: 30,
|
||||
dimensions: '40 x 34 x 10.7 мм',
|
||||
material: 'Алюминий',
|
||||
images: ['/api/placeholder/300/300?text=Apple+Watch+SE'],
|
||||
mainImage: '/api/placeholder/300/300?text=Apple+Watch+SE'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Клавиатура Logitech MX Keys',
|
||||
article: 'LGT-MXKEYS',
|
||||
description: 'Беспроводная клавиатура для продуктивной работы',
|
||||
price: 8900,
|
||||
quantity: 75,
|
||||
category: 'Клавиатуры',
|
||||
brand: 'Logitech',
|
||||
color: 'Графит',
|
||||
weight: 810,
|
||||
dimensions: '430.2 x 20.5 x 131.6 мм',
|
||||
material: 'Пластик, металл',
|
||||
images: ['/api/placeholder/300/300?text=MX+Keys'],
|
||||
mainImage: '/api/placeholder/300/300?text=MX+Keys'
|
||||
}
|
||||
]
|
||||
|
||||
export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreated }: WholesalerProductsProps) {
|
||||
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
|
||||
const [showSummary, setShowSummary] = useState(false)
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const updateProductQuantity = (productId: string, quantity: number) => {
|
||||
const product = mockProducts.find(p => p.id === productId)
|
||||
if (!product) return
|
||||
|
||||
setSelectedProducts(prev => {
|
||||
const existing = prev.find(p => p.id === productId)
|
||||
|
||||
if (quantity === 0) {
|
||||
// Удаляем продукт если количество 0
|
||||
return prev.filter(p => p.id !== productId)
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
// Обновляем количество существующего продукта
|
||||
return prev.map(p =>
|
||||
p.id === productId ? { ...p, selectedQuantity: quantity } : p
|
||||
)
|
||||
} else {
|
||||
// Добавляем новый продукт
|
||||
return [...prev, { ...product, selectedQuantity: quantity }]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectedQuantity = (productId: string): number => {
|
||||
const selected = selectedProducts.find(p => p.id === productId)
|
||||
return selected ? selected.selectedQuantity : 0
|
||||
}
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return selectedProducts.reduce((sum, product) =>
|
||||
sum + (product.price * product.selectedQuantity), 0
|
||||
)
|
||||
}
|
||||
|
||||
const getTotalItems = () => {
|
||||
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
|
||||
}
|
||||
|
||||
const handleCreateSupply = () => {
|
||||
console.log('Создание поставки с товарами:', selectedProducts)
|
||||
// TODO: Здесь будет реальное создание поставки
|
||||
onSupplyCreated()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onBack}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Назад
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Товары оптовика</h2>
|
||||
<p className="text-white/60">{wholesaler.name} • {mockProducts.length} товаров</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowSummary(!showSummary)}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<Info className="h-4 w-4 mr-2" />
|
||||
Резюме ({selectedProducts.length})
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSummary && selectedProducts.length > 0 && (
|
||||
<Card className="bg-purple-500/10 backdrop-blur border-purple-500/30 p-6">
|
||||
<h3 className="text-white font-semibold text-lg mb-4">Резюме заказа</h3>
|
||||
<div className="space-y-3">
|
||||
{selectedProducts.map((product) => (
|
||||
<div key={product.id} className="flex justify-between items-center">
|
||||
<div>
|
||||
<span className="text-white">{product.name}</span>
|
||||
<span className="text-white/60 text-sm ml-2">× {product.selectedQuantity}</span>
|
||||
</div>
|
||||
<span className="text-white font-medium">
|
||||
{formatCurrency(product.price * product.selectedQuantity)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="border-t border-white/20 pt-3 flex justify-between items-center">
|
||||
<span className="text-white font-semibold">
|
||||
Итого: {getTotalItems()} товаров
|
||||
</span>
|
||||
<span className="text-white font-bold text-xl">
|
||||
{formatCurrency(getTotalAmount())}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
|
||||
onClick={handleCreateSupply}
|
||||
disabled={selectedProducts.length === 0}
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Создать поставку
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{mockProducts.map((product) => {
|
||||
const selectedQuantity = getSelectedQuantity(product.id)
|
||||
|
||||
return (
|
||||
<Card key={product.id} className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
||||
<div className="aspect-square relative bg-white/5">
|
||||
<Image
|
||||
src={product.mainImage || '/api/placeholder/300/300'}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute top-2 right-2">
|
||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30">
|
||||
В наличии: {product.quantity}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-1 line-clamp-2">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className="text-white/60 text-xs mb-2">
|
||||
Артикул: {product.article}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30 text-xs">
|
||||
{product.category}
|
||||
</Badge>
|
||||
{product.brand && (
|
||||
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 text-xs">
|
||||
{product.brand}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-white/60 text-sm line-clamp-2">
|
||||
{product.description}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{product.color && (
|
||||
<div className="text-white/60 text-xs">
|
||||
Цвет: <span className="text-white">{product.color}</span>
|
||||
</div>
|
||||
)}
|
||||
{product.size && (
|
||||
<div className="text-white/60 text-xs">
|
||||
Размер: <span className="text-white">{product.size}</span>
|
||||
</div>
|
||||
)}
|
||||
{product.weight && (
|
||||
<div className="text-white/60 text-xs">
|
||||
Вес: <span className="text-white">{product.weight} г</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-white/10">
|
||||
<div>
|
||||
<div className="text-white font-bold text-lg">
|
||||
{formatCurrency(product.price)}
|
||||
</div>
|
||||
<div className="text-white/60 text-xs">за штуку</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateProductQuantity(product.id, Math.max(0, selectedQuantity - 1))}
|
||||
disabled={selectedQuantity === 0}
|
||||
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedQuantity}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(0, Math.min(product.quantity, parseInt(e.target.value) || 0))
|
||||
updateProductQuantity(product.id, value)
|
||||
}}
|
||||
className="h-8 w-16 text-center bg-white/10 border-white/20 text-white"
|
||||
min={0}
|
||||
max={product.quantity}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateProductQuantity(product.id, Math.min(product.quantity, selectedQuantity + 1))}
|
||||
disabled={selectedQuantity >= product.quantity}
|
||||
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedQuantity > 0 && (
|
||||
<div className="bg-green-500/20 border border-green-500/30 rounded-lg p-2">
|
||||
<div className="text-green-300 text-sm font-medium">
|
||||
Сумма: {formatCurrency(product.price * selectedQuantity)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{selectedProducts.length > 0 && (
|
||||
<div className="fixed bottom-6 right-6">
|
||||
<Button
|
||||
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-lg"
|
||||
onClick={() => setShowSummary(!showSummary)}
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Корзина ({selectedProducts.length}) • {formatCurrency(getTotalAmount())}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user