Добавлены модели товаров и корзины для оптовиков, реализованы соответствующие мутации и запросы в 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,389 @@
"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 [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 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 availableItems = 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}
variant="outline"
size="sm"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
>
<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-5">
<div className="flex 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">
{mainImage ? (
<Image
src={mainImage}
alt={item.product.name}
width={80}
height={80}
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" />
</div>
)}
</div>
</div>
{/* Информация о товаре */}
<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>
</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 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>
{/* Цена и кнопка удаления */}
<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>
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
))}
</div>
</div>
)
}