diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50323b6..81e0881 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -124,6 +124,9 @@ model Organization { // Корзины carts Cart[] + // Избранные товары + favorites Favorites[] + @@map("organizations") } @@ -337,6 +340,9 @@ model Product { // Связь с элементами корзины cartItems CartItem[] + // Избранные товары + favorites Favorites[] + // Уникальность артикула в рамках организации @@unique([organizationId, article]) @@map("products") @@ -383,3 +389,24 @@ model CartItem { @@unique([cartId, productId]) @@map("cart_items") } + +// Модель избранных товаров +model Favorites { + id String @id @default(cuid()) + + // Связь с организацией (пользователь может добавлять товары в избранное) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organizationId String + + // Связь с товаром + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + productId String + + // Временные метки + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Уникальность: один товар может быть только один раз в избранном у организации + @@unique([organizationId, productId]) + @@map("favorites") +} diff --git a/src/components/cart/cart-items.tsx b/src/components/cart/cart-items.tsx index 132fa0e..cf61dd3 100644 --- a/src/components/cart/cart-items.tsx +++ b/src/components/cart/cart-items.tsx @@ -55,6 +55,7 @@ interface CartItemsProps { export function CartItems({ cart }: CartItemsProps) { const [loadingItems, setLoadingItems] = useState>(new Set()) + const [quantities, setQuantities] = useState>({}) 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 && ( + + { + 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" + /> + + + + +
+ до {item.availableQuantity} +
+ - {/* Нижняя секция: управление количеством и цена */} -
- {/* Управление количеством */} -
-
- Количество: -
- - - { - 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" - /> - - -
-
- - - из {item.availableQuantity} доступно - + {/* Правая часть: цена и кнопка удаления */} +
+ {/* Цена */} +
+
+ {formatPrice(item.totalPrice)}
- - {/* Цена и кнопка удаления */} -
-
-
- {formatPrice(item.totalPrice)} -
-
- {formatPrice(item.product.price)} за шт. -
-
- - +
+ {formatPrice(item.product.price)} за шт.
+ + {/* Кнопка удаления */} +
-
- ) - })} + + ) + })} ))} diff --git a/src/components/favorites/favorites-dashboard.tsx b/src/components/favorites/favorites-dashboard.tsx new file mode 100644 index 0000000..fab7ec8 --- /dev/null +++ b/src/components/favorites/favorites-dashboard.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useQuery } from '@apollo/client' +import { Card } from '@/components/ui/card' +import { FavoritesItems } from './favorites-items' +import { GET_MY_FAVORITES } from '@/graphql/queries' +import { Heart } from 'lucide-react' + +interface FavoritesDashboardProps { + onBackToCategories?: () => void +} + +export function FavoritesDashboard({ onBackToCategories }: FavoritesDashboardProps) { + const { data, loading, error } = useQuery(GET_MY_FAVORITES) + + const favorites = data?.myFavorites || [] + + if (loading) { + return ( +
+
+
+

Загружаем избранное...

+
+
+ ) + } + + if (error) { + return ( +
+
+ +

Ошибка загрузки избранного

+

{error.message}

+
+
+ ) + } + + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/favorites/favorites-items.tsx b/src/components/favorites/favorites-items.tsx new file mode 100644 index 0000000..22857d6 --- /dev/null +++ b/src/components/favorites/favorites-items.tsx @@ -0,0 +1,387 @@ +"use client" + +import { useState } from 'react' +import { useMutation } from '@apollo/client' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { + Heart, + Package, + Store, + ShoppingCart, + Plus, + ArrowLeft +} from 'lucide-react' +import { OrganizationAvatar } from '@/components/market/organization-avatar' +import Image from 'next/image' +import { REMOVE_FROM_FAVORITES, ADD_TO_CART } from '@/graphql/mutations' +import { GET_MY_FAVORITES, GET_MY_CART } from '@/graphql/queries' +import { toast } from 'sonner' + +interface Product { + id: string + name: string + article: string + price: number + quantity: number + images: string[] + mainImage?: string + organization: { + id: string + name?: string + fullName?: string + inn: string + } + category?: { + id: string + name: string + } +} + +interface FavoritesItemsProps { + favorites: Product[] + onBackToCategories?: () => void +} + +export function FavoritesItems({ favorites, onBackToCategories }: FavoritesItemsProps) { + const [loadingItems, setLoadingItems] = useState>(new Set()) + const [quantities, setQuantities] = useState>({}) + + const [removeFromFavorites] = useMutation(REMOVE_FROM_FAVORITES, { + refetchQueries: [{ query: GET_MY_FAVORITES }], + onCompleted: (data) => { + if (data.removeFromFavorites.success) { + toast.success(data.removeFromFavorites.message) + } else { + toast.error(data.removeFromFavorites.message) + } + }, + onError: (error) => { + toast.error('Ошибка при удалении из избранного') + console.error('Error removing from favorites:', error) + } + }) + + const [addToCart] = useMutation(ADD_TO_CART, { + refetchQueries: [{ query: GET_MY_CART }], + onCompleted: (data) => { + if (data.addToCart.success) { + toast.success(data.addToCart.message) + } else { + toast.error(data.addToCart.message) + } + }, + onError: (error) => { + toast.error('Ошибка при добавлении в корзину') + console.error('Error adding to cart:', error) + } + }) + + const removeFromFavoritesList = async (productId: string) => { + setLoadingItems(prev => new Set(prev).add(productId)) + + try { + await removeFromFavorites({ + variables: { productId } + }) + } finally { + setLoadingItems(prev => { + const newSet = new Set(prev) + newSet.delete(productId) + return newSet + }) + } + } + + const getQuantity = (productId: string) => quantities[productId] || 1 + + const setQuantity = (productId: string, quantity: number) => { + setQuantities(prev => ({ ...prev, [productId]: quantity })) + } + + const addProductToCart = async (productId: string) => { + setLoadingItems(prev => new Set(prev).add(productId)) + + try { + const quantity = getQuantity(productId) + await addToCart({ + variables: { productId, quantity } + }) + } finally { + setLoadingItems(prev => { + const newSet = new Set(prev) + newSet.delete(productId) + return newSet + }) + } + } + + const formatPrice = (price: number) => { + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB' + }).format(price) + } + + // Группировка товаров по поставщикам + const groupedItems = favorites.reduce((groups, product) => { + const orgId = product.organization.id + if (!groups[orgId]) { + groups[orgId] = { + organization: product.organization, + products: [] + } + } + groups[orgId].products.push(product) + return groups + }, {} as Record) + + const supplierGroups = Object.values(groupedItems) + + return ( +
+ {/* Заголовок */} +
+ {onBackToCategories && ( + + )} +
+ +
+
+

+ Избранные товары +

+

+ {favorites.length > 0 + ? `${favorites.length} товаров в избранном` + : 'Ваш список избранного пуст' + } +

+
+
+ + {/* Содержимое */} +
+ {favorites.length === 0 ? ( +
+
+ +

+ Избранных товаров нет +

+

+ Добавляйте товары в избранное, чтобы быстро находить их в будущем +

+
+
+ ) : ( +
+ {supplierGroups.map((group) => ( +
+ {/* Заголовок поставщика */} +
+
+
+ +
+

+ {group.organization.name || group.organization.fullName || `ИНН ${group.organization.inn}`} +

+
+ + + {group.products.length} товаров + +
+
+
+ + {group.products.length} в избранном + +
+
+ + {/* Товары этого поставщика */} +
+ {group.products.map((product) => { + const isLoading = loadingItems.has(product.id) + const mainImage = product.images?.[0] || product.mainImage + + return ( +
+ {/* Информация о поставщике в карточке товара */} +
+
+ + Поставщик: + + {product.organization.name || product.organization.fullName || `ИНН ${product.organization.inn}`} + + {product.category && ( + <> + + + {product.category.name} + + + )} +
+
+ + {/* Основное содержимое карточки */} +
+
+ {/* Изображение товара */} +
+
+ {mainImage ? ( + {product.name} + ) : ( +
+ +
+ )} +
+
+ + {/* Информация о товаре */} +
+ {/* Название и артикул */} +

+ {product.name} +

+

+ Арт: {product.article} +

+ + {/* Статус наличия */} +
+ + {product.quantity} шт. + + {product.quantity > 0 ? ( + + В наличии + + ) : ( + + Нет в наличии + + )} +
+
+ + {/* Правая часть: цена и действия */} +
+ {/* Цена */} +
+
+ {formatPrice(product.price)} +
+
+ + {/* Количество и кнопки */} +
+ {/* Инпут количества */} + { + const value = e.target.value + + // Разрешаем только цифры и пустое поле + if (value === '' || /^\d+$/.test(value)) { + const numValue = value === '' ? 0 : parseInt(value) + + // Временно сохраняем даже если 0 или больше лимита для удобства ввода + if (value === '' || (numValue >= 0 && numValue <= 99999)) { + setQuantity(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 > product.quantity) { + value = product.quantity + } + setQuantity(product.id, value) + }} + onKeyDown={(e) => { + // Enter для быстрого добавления в корзину + if (e.key === 'Enter') { + addProductToCart(product.id) + } + }} + className="w-16 h-7 text-xs text-center glass-input text-white border-white/20 bg-white/5" + disabled={product.quantity === 0} + placeholder="1" + /> + + {/* Кнопка добавления в корзину */} + + + {/* Кнопка удаления из избранного */} + +
+
+
+
+
+ ) + })} +
+
+ ))} +
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/market/market-categories.tsx b/src/components/market/market-categories.tsx index 767ee22..3addff8 100644 --- a/src/components/market/market-categories.tsx +++ b/src/components/market/market-categories.tsx @@ -4,7 +4,7 @@ import { useQuery } from '@apollo/client' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { GET_CATEGORIES, GET_MY_CART } from '@/graphql/queries' -import { Package2, ArrowRight, Sparkles, ShoppingCart } from 'lucide-react' +import { Package2, ArrowRight, Sparkles, ShoppingCart, Heart } from 'lucide-react' interface Category { id: string @@ -16,9 +16,10 @@ interface Category { interface MarketCategoriesProps { onSelectCategory: (categoryId: string, categoryName: string) => void onShowCart?: () => void + onShowFavorites?: () => void } -export function MarketCategories({ onSelectCategory, onShowCart }: MarketCategoriesProps) { +export function MarketCategories({ onSelectCategory, onShowCart, onShowFavorites }: MarketCategoriesProps) { const { data, loading, error } = useQuery(GET_CATEGORIES) const { data: cartData } = useQuery(GET_MY_CART) @@ -67,16 +68,29 @@ export function MarketCategories({ onSelectCategory, onShowCart }: MarketCategor - {/* Кнопка корзины */} - {onShowCart && ( - - )} + {/* Кнопки корзины и избранного */} +
+ {onShowFavorites && ( + + )} + + {onShowCart && ( + + )} +
{/* Категории */} diff --git a/src/components/market/market-dashboard.tsx b/src/components/market/market-dashboard.tsx index c74e796..dee2215 100644 --- a/src/components/market/market-dashboard.tsx +++ b/src/components/market/market-dashboard.tsx @@ -12,9 +12,10 @@ import { MarketWholesale } from './market-wholesale' import { MarketProducts } from './market-products' import { MarketCategories } from './market-categories' import { MarketRequests } from './market-requests' +import { FavoritesDashboard } from '../favorites/favorites-dashboard' export function MarketDashboard() { - const [productsView, setProductsView] = useState<'categories' | 'products' | 'cart'>('categories') + const [productsView, setProductsView] = useState<'categories' | 'products' | 'cart' | 'favorites'>('categories') const [selectedCategory, setSelectedCategory] = useState<{ id: string; name: string } | null>(null) const handleSelectCategory = (categoryId: string, categoryName: string) => { @@ -32,6 +33,11 @@ export function MarketDashboard() { setSelectedCategory(null) } + const handleShowFavorites = () => { + setProductsView('favorites') + setSelectedCategory(null) + } + return (
@@ -122,15 +128,17 @@ export function MarketDashboard() { {productsView === 'categories' ? ( - + ) : productsView === 'products' ? ( + ) : productsView === 'cart' ? ( + ) : ( - + )} diff --git a/src/components/market/market-requests.tsx b/src/components/market/market-requests.tsx index 8558965..7070786 100644 --- a/src/components/market/market-requests.tsx +++ b/src/components/market/market-requests.tsx @@ -4,9 +4,14 @@ import { useQuery } from '@apollo/client' import { CartItems } from '../cart/cart-items' import { CartSummary } from '../cart/cart-summary' import { GET_MY_CART } from '@/graphql/queries' -import { ShoppingCart, Package } from 'lucide-react' +import { ShoppingCart, Package, ArrowLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' -export function MarketRequests() { +interface MarketRequestsProps { + onBackToCategories?: () => void +} + +export function MarketRequests({ onBackToCategories }: MarketRequestsProps) { const { data, loading, error } = useQuery(GET_MY_CART) const cart = data?.myCart @@ -39,6 +44,16 @@ export function MarketRequests() {
{/* Заголовок */}
+ {onBackToCategories && ( + + )}

Мои заявки

diff --git a/src/components/market/product-card.tsx b/src/components/market/product-card.tsx index b3444c7..f51e87b 100644 --- a/src/components/market/product-card.tsx +++ b/src/components/market/product-card.tsx @@ -8,14 +8,15 @@ import { ShoppingCart, Eye, ChevronLeft, - ChevronRight + ChevronRight, + Heart } from 'lucide-react' import { OrganizationAvatar } from './organization-avatar' import { Input } from '@/components/ui/input' import Image from 'next/image' -import { useMutation } from '@apollo/client' -import { ADD_TO_CART } from '@/graphql/mutations' -import { GET_MY_CART } from '@/graphql/queries' +import { useMutation, useQuery } from '@apollo/client' +import { ADD_TO_CART, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES } from '@/graphql/mutations' +import { GET_MY_CART, GET_MY_FAVORITES } from '@/graphql/queries' import { toast } from 'sonner' interface Product { @@ -58,6 +59,11 @@ export function ProductCard({ product }: ProductCardProps) { const [isImageDialogOpen, setIsImageDialogOpen] = useState(false) const [quantity, setQuantity] = useState(1) + // Запрос избранного для проверки статуса + const { data: favoritesData } = useQuery(GET_MY_FAVORITES) + const favorites = favoritesData?.myFavorites || [] + const isFavorite = favorites.some((fav: Product) => fav.id === product.id) + const [addToCart, { loading: addingToCart }] = useMutation(ADD_TO_CART, { refetchQueries: [{ query: GET_MY_CART }], onCompleted: (data) => { @@ -74,6 +80,36 @@ export function ProductCard({ product }: ProductCardProps) { } }) + const [addToFavorites, { loading: addingToFavorites }] = useMutation(ADD_TO_FAVORITES, { + refetchQueries: [{ query: GET_MY_FAVORITES }], + onCompleted: (data) => { + if (data.addToFavorites.success) { + toast.success(data.addToFavorites.message) + } else { + toast.error(data.addToFavorites.message) + } + }, + onError: (error) => { + toast.error('Ошибка при добавлении в избранное') + console.error('Error adding to favorites:', error) + } + }) + + const [removeFromFavorites, { loading: removingFromFavorites }] = useMutation(REMOVE_FROM_FAVORITES, { + refetchQueries: [{ query: GET_MY_FAVORITES }], + onCompleted: (data) => { + if (data.removeFromFavorites.success) { + toast.success(data.removeFromFavorites.message) + } else { + toast.error(data.removeFromFavorites.message) + } + }, + onError: (error) => { + toast.error('Ошибка при удалении из избранного') + console.error('Error removing from favorites:', error) + } + }) + const displayPrice = new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' @@ -96,6 +132,22 @@ export function ProductCard({ product }: ProductCardProps) { } } + const handleToggleFavorite = async () => { + try { + if (isFavorite) { + await removeFromFavorites({ + variables: { productId: product.id } + }) + } else { + await addToFavorites({ + variables: { productId: product.id } + }) + } + } catch (error) { + console.error('Error toggling favorite:', error) + } + } + const nextImage = () => { setCurrentImageIndex((prev) => (prev + 1) % images.length) } @@ -225,24 +277,59 @@ export function ProductCard({ product }: ProductCardProps) { />
- {/* Кнопка добавления в заявки */} - + {/* Кнопки действий */} +
+ {/* Кнопка добавления в заявки */} + + + {/* Кнопка избранного */} + +
) : ( - +
+ + + {/* Кнопка избранного (всегда доступна) */} + +
)}
diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts index 3392a8d..8ca354c 100644 --- a/src/graphql/mutations.ts +++ b/src/graphql/mutations.ts @@ -817,4 +817,61 @@ export const CLEAR_CART = gql` mutation ClearCart { clearCart } +` + +// Мутации для избранного +export const ADD_TO_FAVORITES = gql` + mutation AddToFavorites($productId: ID!) { + addToFavorites(productId: $productId) { + success + message + favorites { + id + name + article + price + quantity + images + mainImage + category { + id + name + } + organization { + id + name + fullName + inn + } + } + } + } +` + +export const REMOVE_FROM_FAVORITES = gql` + mutation RemoveFromFavorites($productId: ID!) { + removeFromFavorites(productId: $productId) { + success + message + favorites { + id + name + article + price + quantity + images + mainImage + category { + id + name + } + organization { + id + name + fullName + inn + } + } + } + } ` \ No newline at end of file diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 707a245..2acd8dc 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -436,4 +436,44 @@ export const GET_MY_CART = gql` updatedAt } } +` + +export const GET_MY_FAVORITES = gql` + query GetMyFavorites { + myFavorites { + id + name + article + description + price + quantity + brand + color + size + images + mainImage + isActive + createdAt + updatedAt + category { + id + name + } + organization { + id + inn + name + fullName + type + address + phones + emails + users { + id + avatar + managerName + } + } + } + } ` \ No newline at end of file diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 2f749f8..5b4ebab 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -638,6 +638,44 @@ export const resolvers = { } return cart + }, + + // Избранные товары пользователя + myFavorites: async (_: unknown, __: unknown, context: Context) => { + if (!context.user) { + throw new GraphQLError('Требуется авторизация', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true } + }) + + if (!currentUser?.organization) { + throw new GraphQLError('У пользователя нет организации') + } + + // Получаем избранные товары + const favorites = await prisma.favorites.findMany({ + where: { organizationId: currentUser.organization.id }, + include: { + product: { + include: { + category: true, + organization: { + include: { + users: true + } + } + } + } + }, + orderBy: { createdAt: 'desc' } + }) + + return favorites.map(favorite => favorite.product) } }, @@ -2844,6 +2882,165 @@ export const resolvers = { console.error('Error clearing cart:', error) return false } + }, + + // Добавить товар в избранное + addToFavorites: async (_: unknown, args: { productId: string }, context: Context) => { + if (!context.user) { + throw new GraphQLError('Требуется авторизация', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true } + }) + + if (!currentUser?.organization) { + throw new GraphQLError('У пользователя нет организации') + } + + // Проверяем, что товар существует и активен + const product = await prisma.product.findFirst({ + where: { + id: args.productId, + isActive: true + }, + include: { + organization: true + } + }) + + if (!product) { + return { + success: false, + message: 'Товар не найден или неактивен' + } + } + + // Проверяем, что пользователь не пытается добавить свой собственный товар + if (product.organizationId === currentUser.organization.id) { + return { + success: false, + message: 'Нельзя добавлять собственные товары в избранное' + } + } + + try { + // Проверяем, есть ли уже такой товар в избранном + const existingFavorite = await prisma.favorites.findUnique({ + where: { + organizationId_productId: { + organizationId: currentUser.organization.id, + productId: args.productId + } + } + }) + + if (existingFavorite) { + return { + success: false, + message: 'Товар уже в избранном' + } + } + + // Добавляем товар в избранное + await prisma.favorites.create({ + data: { + organizationId: currentUser.organization.id, + productId: args.productId + } + }) + + // Возвращаем обновленный список избранного + const favorites = await prisma.favorites.findMany({ + where: { organizationId: currentUser.organization.id }, + include: { + product: { + include: { + category: true, + organization: { + include: { + users: true + } + } + } + } + }, + orderBy: { createdAt: 'desc' } + }) + + return { + success: true, + message: 'Товар добавлен в избранное', + favorites: favorites.map(favorite => favorite.product) + } + } catch (error) { + console.error('Error adding to favorites:', error) + return { + success: false, + message: 'Ошибка при добавлении в избранное' + } + } + }, + + // Удалить товар из избранного + removeFromFavorites: async (_: unknown, args: { productId: string }, context: Context) => { + if (!context.user) { + throw new GraphQLError('Требуется авторизация', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + + const currentUser = await prisma.user.findUnique({ + where: { id: context.user.id }, + include: { organization: true } + }) + + if (!currentUser?.organization) { + throw new GraphQLError('У пользователя нет организации') + } + + try { + // Удаляем товар из избранного + await prisma.favorites.deleteMany({ + where: { + organizationId: currentUser.organization.id, + productId: args.productId + } + }) + + // Возвращаем обновленный список избранного + const favorites = await prisma.favorites.findMany({ + where: { organizationId: currentUser.organization.id }, + include: { + product: { + include: { + category: true, + organization: { + include: { + users: true + } + } + } + } + }, + orderBy: { createdAt: 'desc' } + }) + + return { + success: true, + message: 'Товар удален из избранного', + favorites: favorites.map(favorite => favorite.product) + } + } catch (error) { + console.error('Error removing from favorites:', error) + return { + success: false, + message: 'Ошибка при удалении из избранного' + } + } } }, diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index d6df787..2d7866e 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -40,6 +40,9 @@ export const typeDefs = gql` # Корзина пользователя myCart: Cart + + # Избранные товары пользователя + myFavorites: [Product!]! } type Mutation { @@ -100,6 +103,10 @@ export const typeDefs = gql` updateCartItem(productId: ID!, quantity: Int!): CartResponse! removeFromCart(productId: ID!): CartResponse! clearCart: Boolean! + + # Работа с избранным + addToFavorites(productId: ID!): FavoritesResponse! + removeFromFavorites(productId: ID!): FavoritesResponse! } # Типы данных @@ -456,6 +463,13 @@ export const typeDefs = gql` cart: Cart } + # Типы для избранного + type FavoritesResponse { + success: Boolean! + message: String! + favorites: [Product!] + } + # JSON скаляр scalar JSON ` \ No newline at end of file