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

This commit is contained in:
Bivekich
2025-07-17 19:36:41 +03:00
parent f377fbab5f
commit 3d28051bde
12 changed files with 1074 additions and 141 deletions

View File

@ -55,6 +55,7 @@ interface CartItemsProps {
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 }],
@ -97,6 +98,12 @@ export function CartItems({ cart }: CartItemsProps) {
}
})
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
@ -182,9 +189,8 @@ export function CartItems({ cart }: CartItemsProps) {
{cart.items.length > 0 && (
<Button
onClick={handleClearCart}
variant="outline"
size="sm"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
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" />
Очистить заявки
@ -213,10 +219,10 @@ export function CartItems({ cart }: CartItemsProps) {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<OrganizationAvatar organization={group.organization} size="md" />
<div>
<div>
<h3 className="text-lg font-semibold text-white mb-1">
{group.organization.name || group.organization.fullName || `ИНН ${group.organization.inn}`}
</h3>
{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" />
@ -241,16 +247,16 @@ export function CartItems({ cart }: CartItemsProps) {
{/* Товары этого поставщика */}
<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
const isLoading = loadingItems.has(item.product.id)
const mainImage = item.product.images?.[0] || item.product.mainImage
return (
<div
key={item.id}
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' : ''
}`}
>
!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">
@ -262,23 +268,23 @@ export function CartItems({ cart }: CartItemsProps) {
</div>
</div>
{/* Основное содержимое карточки */}
<div className="p-5">
<div className="flex space-x-4">
{/* Основное содержимое карточки */}
<div className="p-4">
<div className="flex items-center space-x-4">
{/* Изображение товара */}
<div className="flex-shrink-0">
<div className="w-20 h-20 bg-white/5 rounded-xl overflow-hidden border border-white/10 shadow-lg">
<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={80}
height={80}
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-8 w-8 text-white/20" />
<Package className="h-6 w-6 text-white/20" />
</div>
)}
</div>
@ -287,99 +293,134 @@ export function CartItems({ cart }: CartItemsProps) {
{/* Информация о товаре */}
<div className="flex-1 min-w-0">
{/* Название и артикул */}
<div className="mb-3">
<h4 className="text-base font-semibold text-white mb-1 line-clamp-2">
{item.product.name}
</h4>
<p className="text-sm text-white/50">
Артикул: {item.product.article}
</p>
<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>
{/* Статус доступности */}
{!item.isAvailable && (
<Badge className="bg-red-500/20 text-red-300 text-xs mb-3 border border-red-500/30">
Недоступно
</Badge>
)}
{/* Управление количеством */}
<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 items-center justify-between">
{/* Управление количеством */}
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
<span className="text-sm text-white/60 font-medium">Количество:</span>
<div className="flex items-center space-x-2">
<Button
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
disabled={isLoading || !item.isAvailable || item.quantity <= 1}
variant="outline"
size="sm"
className="h-8 w-8 p-0 border-white/20 text-white/70 hover:bg-white/10"
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="number"
min="1"
max={item.availableQuantity}
value={item.quantity}
onChange={(e) => {
const value = parseInt(e.target.value) || 1
if (value >= 1 && value <= item.availableQuantity && !isLoading && item.isAvailable) {
updateQuantity(item.product.id, value)
}
}}
disabled={isLoading || !item.isAvailable}
className="w-16 h-8 text-sm text-center bg-white/5 border border-white/20 rounded-lg text-white focus:border-purple-400/50 focus:bg-white/10"
/>
<Button
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
disabled={isLoading || !item.isAvailable || item.quantity >= item.availableQuantity}
variant="outline"
size="sm"
className="h-8 w-8 p-0 border-white/20 text-white/70 hover:bg-white/10"
>
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
<span className="text-sm text-white/40">
из {item.availableQuantity} доступно
</span>
{/* Правая часть: цена и кнопка удаления */}
<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="flex items-center space-x-4">
<div className="text-right">
<div className="text-lg font-bold text-purple-300 mb-1">
{formatPrice(item.totalPrice)}
</div>
<div className="text-sm text-white/50">
{formatPrice(item.product.price)} за шт.
</div>
</div>
<Button
onClick={() => removeItem(item.product.id)}
disabled={isLoading}
variant="outline"
size="sm"
className="text-red-400 border-red-500/30 hover:bg-red-500/10 hover:text-red-300 h-9 w-9 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
<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>
))}