Files
sfera-new/src/components/cart/cart-items.tsx

429 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { useState } from 'react'
import { useMutation } from '@apollo/client'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Trash2,
AlertTriangle,
Package,
Store,
Minus,
Plus
} from 'lucide-react'
import { OrganizationAvatar } from '@/components/market/organization-avatar'
import { Input } from '@/components/ui/input'
import Image from 'next/image'
import { UPDATE_CART_ITEM, REMOVE_FROM_CART, CLEAR_CART } from '@/graphql/mutations'
import { GET_MY_CART } from '@/graphql/queries'
import { toast } from 'sonner'
interface CartItem {
id: string
quantity: number
totalPrice: number
isAvailable: boolean
availableQuantity: number
product: {
id: string
name: string
article: string
price: number
quantity: number
images: string[]
mainImage?: string
organization: {
id: string
name?: string
fullName?: string
inn: string
}
}
}
interface Cart {
id: string
items: CartItem[]
totalPrice: number
totalItems: number
}
interface CartItemsProps {
cart: Cart
}
export function CartItems({ cart }: CartItemsProps) {
const [loadingItems, setLoadingItems] = useState<Set<string>>(new Set())
const [quantities, setQuantities] = useState<Record<string, number>>({})
const [updateCartItem] = useMutation(UPDATE_CART_ITEM, {
refetchQueries: [{ query: GET_MY_CART }],
onCompleted: (data) => {
if (data.updateCartItem.success) {
toast.success(data.updateCartItem.message)
} else {
toast.error(data.updateCartItem.message)
}
},
onError: (error) => {
toast.error('Ошибка при обновлении заявки')
console.error('Error updating cart item:', error)
}
})
const [removeFromCart] = useMutation(REMOVE_FROM_CART, {
refetchQueries: [{ query: GET_MY_CART }],
onCompleted: (data) => {
if (data.removeFromCart.success) {
toast.success(data.removeFromCart.message)
} else {
toast.error(data.removeFromCart.message)
}
},
onError: (error) => {
toast.error('Ошибка при удалении заявки')
console.error('Error removing from cart:', error)
}
})
const [clearCart] = useMutation(CLEAR_CART, {
refetchQueries: [{ query: GET_MY_CART }],
onCompleted: () => {
toast.success('Заявки очищены')
},
onError: (error) => {
toast.error('Ошибка при очистке заявок')
console.error('Error clearing cart:', error)
}
})
const getQuantity = (productId: string, defaultQuantity: number) => quantities[productId] || defaultQuantity
const setQuantity = (productId: string, quantity: number) => {
setQuantities(prev => ({ ...prev, [productId]: quantity }))
}
const updateQuantity = async (productId: string, newQuantity: number) => {
if (newQuantity <= 0) return
setLoadingItems(prev => new Set(prev).add(productId))
try {
await updateCartItem({
variables: {
productId,
quantity: newQuantity
}
})
} finally {
setLoadingItems(prev => {
const newSet = new Set(prev)
newSet.delete(productId)
return newSet
})
}
}
const removeItem = async (productId: string) => {
setLoadingItems(prev => new Set(prev).add(productId))
try {
await removeFromCart({
variables: { productId }
})
} finally {
setLoadingItems(prev => {
const newSet = new Set(prev)
newSet.delete(productId)
return newSet
})
}
}
const handleClearCart = async () => {
if (confirm('Вы уверены, что хотите очистить все заявки?')) {
await clearCart()
}
}
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(price)
}
const unavailableItems = cart.items.filter(item => !item.isAvailable)
// Группировка товаров по поставщикам
const groupedItems = cart.items.reduce((groups, item) => {
const orgId = item.product.organization.id
if (!groups[orgId]) {
groups[orgId] = {
organization: item.product.organization,
items: [],
totalPrice: 0,
totalItems: 0
}
}
groups[orgId].items.push(item)
groups[orgId].totalPrice += item.totalPrice
groups[orgId].totalItems += item.quantity
return groups
}, {} as Record<string, {
organization: CartItem['product']['organization'],
items: CartItem[],
totalPrice: number,
totalItems: number
}>)
const supplierGroups = Object.values(groupedItems)
return (
<div className="p-6 h-full flex flex-col">
{/* Заголовок с кнопкой очистки */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-white">Заявки на товары</h2>
{cart.items.length > 0 && (
<Button
onClick={handleClearCart}
size="sm"
className="bg-gradient-to-r from-red-500/20 to-pink-500/20 hover:from-red-500/30 hover:to-pink-500/30 border border-red-500/30 text-red-400 hover:text-white transition-all"
>
<Trash2 className="h-4 w-4 mr-2" />
Очистить заявки
</Button>
)}
</div>
{/* Предупреждение о недоступных товарах */}
{unavailableItems.length > 0 && (
<div className="mb-6 p-4 bg-orange-500/10 border border-orange-500/20 rounded-lg">
<div className="flex items-center space-x-2 text-orange-400">
<AlertTriangle className="h-4 w-4" />
<span className="text-sm font-medium">
{unavailableItems.length} заявок недоступно для оформления
</span>
</div>
</div>
)}
{/* Группы поставщиков */}
<div className="flex-1 overflow-auto space-y-8">
{supplierGroups.map((group) => (
<div key={group.organization.id} className="space-y-4">
{/* Заголовок поставщика */}
<div className="bg-gradient-to-r from-purple-600/20 via-purple-500/10 to-transparent border border-purple-500/20 rounded-xl p-5">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<OrganizationAvatar organization={group.organization} size="md" />
<div>
<h3 className="text-lg font-semibold text-white mb-1">
{group.organization.name || group.organization.fullName || `ИНН ${group.organization.inn}`}
</h3>
<div className="flex items-center space-x-3 text-sm text-white/60">
<span className="flex items-center space-x-1">
<Package className="h-4 w-4" />
<span>{group.totalItems} товаров</span>
</span>
<span></span>
<span className="font-medium text-purple-300">
{formatPrice(group.totalPrice)}
</span>
</div>
</div>
</div>
<Badge
variant="secondary"
className="bg-purple-500/20 text-purple-300 px-3 py-1 text-sm font-medium"
>
{group.items.length} заявок
</Badge>
</div>
</div>
{/* Товары этого поставщика */}
<div className="space-y-3">
{group.items.map((item) => {
const isLoading = loadingItems.has(item.product.id)
const mainImage = item.product.images?.[0] || item.product.mainImage
return (
<div
key={item.id}
className={`bg-white/5 backdrop-blur border border-white/10 rounded-xl transition-all hover:bg-white/8 hover:border-white/20 ${
!item.isAvailable ? 'opacity-60' : ''
}`}
>
{/* Информация о поставщике в карточке товара */}
<div className="px-4 py-2 bg-white/5 border-b border-white/10 rounded-t-xl">
<div className="flex items-center space-x-2 text-xs text-white/60">
<Store className="h-3 w-3" />
<span>Поставщик:</span>
<span className="text-white/80 font-medium">
{item.product.organization.name || item.product.organization.fullName || `ИНН ${item.product.organization.inn}`}
</span>
</div>
</div>
{/* Основное содержимое карточки */}
<div className="p-4">
<div className="flex items-center space-x-4">
{/* Изображение товара */}
<div className="flex-shrink-0">
<div className="w-16 h-16 bg-white/5 rounded-lg overflow-hidden border border-white/10 shadow-lg">
{mainImage ? (
<Image
src={mainImage}
alt={item.product.name}
width={64}
height={64}
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/20" />
</div>
)}
</div>
</div>
{/* Информация о товаре */}
<div className="flex-1 min-w-0">
{/* Название и артикул */}
<h4 className="text-sm font-semibold text-white mb-1 line-clamp-1">
{item.product.name}
</h4>
<p className="text-xs text-white/50 mb-2">
Арт: {item.product.article}
</p>
{/* Статус и наличие */}
<div className="flex items-center space-x-2">
<span className="text-xs text-white/60">
{item.availableQuantity} шт.
</span>
{item.isAvailable ? (
<Badge className="bg-green-500/20 text-green-300 text-xs border border-green-500/30 px-1 py-0">
В наличии
</Badge>
) : (
<Badge className="bg-red-500/20 text-red-300 text-xs border border-red-500/30 px-1 py-0">
Недоступно
</Badge>
)}
</div>
</div>
{/* Управление количеством */}
<div className="flex-shrink-0">
<div className="flex items-center space-x-1 mb-2">
<Button
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
disabled={isLoading || !item.isAvailable || item.quantity <= 1}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 text-purple-300 hover:text-white transition-all"
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="text"
value={getQuantity(item.product.id, item.quantity)}
onChange={(e) => {
const value = e.target.value
// Разрешаем только цифры и пустое поле
if (value === '' || /^\d+$/.test(value)) {
const numValue = value === '' ? 0 : parseInt(value)
// Временно сохраняем даже если 0 или больше лимита для удобства ввода
if (value === '' || (numValue >= 0 && numValue <= 99999)) {
setQuantity(item.product.id, numValue || 1)
}
}
}}
onFocus={(e) => {
// При фокусе выделяем весь текст для удобного редактирования
e.target.select()
}}
onBlur={(e) => {
// При потере фокуса проверяем и корректируем значение, отправляем запрос
let value = parseInt(e.target.value)
if (isNaN(value) || value < 1) {
value = 1
} else if (value > item.availableQuantity) {
value = item.availableQuantity
}
setQuantity(item.product.id, value)
updateQuantity(item.product.id, value)
}}
onKeyDown={(e) => {
// Enter для быстрого обновления
if (e.key === 'Enter') {
let value = parseInt(e.currentTarget.value)
if (isNaN(value) || value < 1) {
value = 1
} else if (value > item.availableQuantity) {
value = item.availableQuantity
}
setQuantity(item.product.id, value)
updateQuantity(item.product.id, value)
e.currentTarget.blur()
}
}}
disabled={isLoading || !item.isAvailable}
className="w-16 h-7 text-xs text-center glass-input text-white border-white/20 bg-white/5"
placeholder="1"
/>
<Button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
disabled={isLoading || !item.isAvailable || item.quantity >= item.availableQuantity}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-purple-500/20 to-pink-500/20 hover:from-purple-500/30 hover:to-pink-500/30 border border-purple-500/30 text-purple-300 hover:text-white transition-all"
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="text-xs text-white/40 text-center">
до {item.availableQuantity}
</div>
</div>
{/* Правая часть: цена и кнопка удаления */}
<div className="flex-shrink-0 text-right">
{/* Цена */}
<div className="mb-2">
<div className="text-base font-bold text-purple-300">
{formatPrice(item.totalPrice)}
</div>
<div className="text-xs text-white/50">
{formatPrice(item.product.price)} за шт.
</div>
</div>
{/* Кнопка удаления */}
<Button
onClick={() => removeItem(item.product.id)}
disabled={isLoading}
size="sm"
className="h-7 w-7 p-0 bg-gradient-to-r from-red-500/20 to-pink-500/20 hover:from-red-500/30 hover:to-pink-500/30 border border-red-500/30 text-red-400 hover:text-white transition-all"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
))}
</div>
</div>
)
}