diff --git a/src/components/BestPriceCard.tsx b/src/components/BestPriceCard.tsx index b95fd76..21d02d6 100644 --- a/src/components/BestPriceCard.tsx +++ b/src/components/BestPriceCard.tsx @@ -73,7 +73,7 @@ const BestPriceCard: React.FC = ({ }; // Обработчик добавления в корзину - const handleAddToCart = (e: React.MouseEvent) => { + const handleAddToCart = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -88,14 +88,8 @@ const BestPriceCard: React.FC = ({ return; } - // Проверяем наличие - if (maxCount !== undefined && count > maxCount) { - toast.error(`Недостаточно товара в наличии. Доступно: ${maxCount} шт.`); - return; - } - try { - addItem({ + const result = await addItem({ productId: offer.productId, offerKey: offer.offerKey, name: description, @@ -105,6 +99,7 @@ const BestPriceCard: React.FC = ({ price: numericPrice, currency: offer.currency || 'RUB', quantity: count, + stock: maxCount, // передаем информацию о наличии deliveryTime: delivery, warehouse: offer.warehouse || 'Склад', supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'), @@ -112,17 +107,22 @@ const BestPriceCard: React.FC = ({ image: offer.image, }); - // Показываем тоастер об успешном добавлении - toast.success( -
-
Товар добавлен в корзину!
-
{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}
-
, - { - duration: 3000, - icon: , - } - ); + if (result.success) { + // Показываем тоастер об успешном добавлении + toast.success( +
+
Товар добавлен в корзину!
+
{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}
+
, + { + duration: 3000, + icon: , + } + ); + } else { + // Показываем ошибку + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); + } } catch (error) { console.error('Ошибка добавления в корзину:', error); toast.error('Ошибка добавления товара в корзину'); diff --git a/src/components/CartList.tsx b/src/components/CartList.tsx index 7ab981f..e837081 100644 --- a/src/components/CartList.tsx +++ b/src/components/CartList.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import CartItem from "./CartItem"; import { useCart } from "@/contexts/CartContext"; import { useFavorites } from "@/contexts/FavoritesContext"; @@ -8,7 +8,7 @@ interface CartListProps { } const CartList: React.FC = ({ isSummaryStep = false }) => { - const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart(); + const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity, clearError } = useCart(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { items } = state; @@ -73,8 +73,40 @@ const CartList: React.FC = ({ isSummaryStep = false }) => { // На втором шаге показываем только выбранные товары const displayItems = isSummaryStep ? items.filter(item => item.selected) : items; + // Автоматически очищаем ошибки через 5 секунд + useEffect(() => { + if (state.error) { + const timer = setTimeout(() => { + clearError(); + }, 5000); + return () => clearTimeout(timer); + } + }, [state.error, clearError]); + return (
+ {/* Отображение ошибок корзины */} + {state.error && ( +
+
+
+ + + + {state.error} +
+ +
+
+ )}
{!isSummaryStep && (
diff --git a/src/components/CartRecommended.tsx b/src/components/CartRecommended.tsx index a1383c4..56b6754 100644 --- a/src/components/CartRecommended.tsx +++ b/src/components/CartRecommended.tsx @@ -37,13 +37,14 @@ const RecommendedProductCard: React.FC<{ } // Добавляем товар в корзину - addItem({ + const result = await addItem({ productId: String(item.artId) || undefined, name: item.name || `${item.brand} ${item.articleNumber}`, description: item.name || `${item.brand} ${item.articleNumber}`, price: numericPrice, currency: 'RUB', quantity: 1, + stock: undefined, // информация о наличии не доступна для рекомендуемых товаров image: displayImage, brand: item.brand, article: item.articleNumber, @@ -52,17 +53,22 @@ const RecommendedProductCard: React.FC<{ isExternal: true }); - // Показываем успешный тоастер - toast.success( -
-
Товар добавлен в корзину!
-
{item.name || `${item.brand} ${item.articleNumber}`}
-
, - { - duration: 3000, - icon: , - } - ); + if (result.success) { + // Показываем успешный тоастер + toast.success( +
+
Товар добавлен в корзину!
+
{item.name || `${item.brand} ${item.articleNumber}`}
+
, + { + duration: 3000, + icon: , + } + ); + } else { + // Показываем ошибку + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); + } } catch (error) { console.error('Ошибка добавления в корзину:', error); toast.error('Ошибка при добавлении товара в корзину'); diff --git a/src/components/CoreProductCard.tsx b/src/components/CoreProductCard.tsx index 560b4e1..ffcedaf 100644 --- a/src/components/CoreProductCard.tsx +++ b/src/components/CoreProductCard.tsx @@ -123,19 +123,13 @@ const CoreProductCard: React.FC = ({ }); }; - const handleAddToCart = (offer: CoreProductCardOffer, index: number) => { + const handleAddToCart = async (offer: CoreProductCardOffer, index: number) => { const quantity = quantities[index] || 1; const availableStock = parseStock(offer.pcs); - // Проверяем наличие - if (quantity > availableStock) { - toast.error(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`); - return; - } - const numericPrice = parsePrice(offer.price); - addItem({ + const result = await addItem({ productId: offer.productId, offerKey: offer.offerKey, name: name, @@ -145,6 +139,7 @@ const CoreProductCard: React.FC = ({ price: numericPrice, currency: offer.currency || 'RUB', quantity: quantity, + stock: availableStock, // передаем информацию о наличии deliveryTime: parseDeliveryTime(offer.days), warehouse: offer.warehouse || 'Склад', supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'), @@ -152,17 +147,22 @@ const CoreProductCard: React.FC = ({ image: image, }); - // Показываем тоастер вместо alert - toast.success( -
-
Товар добавлен в корзину!
-
{`${brand} ${article} (${quantity} шт.)`}
-
, - { - duration: 3000, - icon: , - } - ); + if (result.success) { + // Показываем тоастер вместо alert + toast.success( +
+
Товар добавлен в корзину!
+
{`${brand} ${article} (${quantity} шт.)`}
+
, + { + duration: 3000, + icon: , + } + ); + } else { + // Показываем ошибку + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); + } }; // Обработчик клика по сердечку diff --git a/src/components/ProductListCard.tsx b/src/components/ProductListCard.tsx index 42cb12f..8b25202 100644 --- a/src/components/ProductListCard.tsx +++ b/src/components/ProductListCard.tsx @@ -59,19 +59,13 @@ const ProductListCard: React.FC = ({ return match ? parseInt(match[0]) : 0; }; - const handleAddToCart = () => { + const handleAddToCart = async () => { const availableStock = parseStock(stock); - // Проверяем наличие - if (count > availableStock) { - alert(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`); - return; - } - const numericPrice = parsePrice(price); const numericOldPrice = oldPrice ? parsePrice(oldPrice) : undefined; - addItem({ + const result = await addItem({ productId: productId, offerKey: offerKey, name: title, @@ -81,6 +75,7 @@ const ProductListCard: React.FC = ({ originalPrice: numericOldPrice, currency: currency, quantity: count, + stock: availableStock, // передаем информацию о наличии deliveryTime: deliveryTime || delivery, warehouse: warehouse || address, supplier: supplier, @@ -88,8 +83,13 @@ const ProductListCard: React.FC = ({ image: image, }); - // Показываем уведомление о добавлении - alert(`Товар "${title}" добавлен в корзину (${count} шт.)`); + if (result.success) { + // Показываем уведомление о добавлении + alert(`Товар "${title}" добавлен в корзину (${count} шт.)`); + } else { + // Показываем ошибку + alert(result.error || 'Ошибка при добавлении товара в корзину'); + } }; return ( diff --git a/src/components/card/ProductBuyBlock.tsx b/src/components/card/ProductBuyBlock.tsx index 958158b..dd43cbb 100644 --- a/src/components/card/ProductBuyBlock.tsx +++ b/src/components/card/ProductBuyBlock.tsx @@ -38,7 +38,7 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => { } // Добавляем товар в корзину - addItem({ + const result = await addItem({ productId: offer.id ? String(offer.id) : undefined, offerKey: offer.offerKey || undefined, name: offer.name || `${offer.brand} ${offer.articleNumber}`, @@ -46,6 +46,7 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => { price: offer.price, currency: 'RUB', quantity: quantity, + stock: offer.quantity, // передаем информацию о наличии image: offer.image || undefined, brand: offer.brand, article: offer.articleNumber, @@ -54,17 +55,22 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => { isExternal: offer.type === 'external' }); - // Показываем успешный тоастер - toast.success( -
-
Товар добавлен в корзину!
-
{offer.name || `${offer.brand} ${offer.articleNumber}`}
-
, - { - duration: 3000, - icon: , - } - ); + if (result.success) { + // Показываем успешный тоастер + toast.success( +
+
Товар добавлен в корзину!
+
{offer.name || `${offer.brand} ${offer.articleNumber}`}
+
, + { + duration: 3000, + icon: , + } + ); + } else { + // Показываем ошибку + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); + } } catch (error) { console.error('Ошибка добавления в корзину:', error); toast.error('Ошибка при добавлении товара в корзину'); diff --git a/src/contexts/CartContext.tsx b/src/contexts/CartContext.tsx index 3c25a28..324f6d2 100644 --- a/src/contexts/CartContext.tsx +++ b/src/contexts/CartContext.tsx @@ -25,6 +25,8 @@ const initialState: CartState = { // Типы действий type CartAction = | { type: 'ADD_ITEM'; payload: Omit } + | { type: 'ADD_ITEM_SUCCESS'; payload: { items: CartItem[]; summary: any } } + | { type: 'ADD_ITEM_ERROR'; payload: string } | { type: 'REMOVE_ITEM'; payload: string } | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } } | { type: 'TOGGLE_SELECT'; payload: string } @@ -44,6 +46,16 @@ type CartAction = // Функция для генерации ID const generateId = () => Math.random().toString(36).substr(2, 9) +// Утилитарная функция для парсинга количества в наличии +const parseStock = (stockStr: string | number | undefined): number => { + if (typeof stockStr === 'number') return stockStr; + if (typeof stockStr === 'string') { + const match = stockStr.match(/\d+/); + return match ? parseInt(match[0]) : 0; + } + return 0; +}; + // Функция для расчета итогов const calculateSummary = (items: CartItem[], deliveryPrice: number) => { const selectedItems = items.filter(item => item.selected) @@ -78,9 +90,12 @@ const cartReducer = (state: CartState, action: CartAction): CartState => { if (existingItemIndex >= 0) { // Увеличиваем количество существующего товара + const existingItem = state.items[existingItemIndex]; + const totalQuantity = existingItem.quantity + action.payload.quantity; + newItems = state.items.map((item, index) => index === existingItemIndex - ? { ...item, quantity: item.quantity + action.payload.quantity } + ? { ...item, quantity: totalQuantity } : item ) } else { @@ -335,8 +350,31 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }, [state.items, state.delivery, state.orderComment, isInitialized]) // Функции для работы с корзиной - const addItem = (item: Omit) => { + const addItem = async (item: Omit) => { + // Проверяем наличие товара на складе перед добавлением + const existingItemIndex = state.items.findIndex( + existingItem => + (existingItem.productId && existingItem.productId === item.productId) || + (existingItem.offerKey && existingItem.offerKey === item.offerKey) + ) + + let totalQuantity = item.quantity; + if (existingItemIndex >= 0) { + const existingItem = state.items[existingItemIndex]; + totalQuantity = existingItem.quantity + item.quantity; + } + + // Проверяем наличие товара на складе + const availableStock = parseStock(item.stock); + if (availableStock > 0 && totalQuantity > availableStock) { + const errorMessage = `Недостаточно товара в наличии. Доступно: ${availableStock} шт., запрошено: ${totalQuantity} шт.`; + dispatch({ type: 'SET_ERROR', payload: errorMessage }); + return { success: false, error: errorMessage }; + } + + // Если проверка прошла успешно, добавляем товар dispatch({ type: 'ADD_ITEM', payload: item }) + return { success: true } } const removeItem = (id: string) => { @@ -344,6 +382,17 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children } const updateQuantity = (id: string, quantity: number) => { + // Найдем товар для проверки наличия + const item = state.items.find(item => item.id === id); + if (item) { + const availableStock = parseStock(item.stock); + if (availableStock > 0 && quantity > availableStock) { + // Показываем ошибку, но не изменяем количество + dispatch({ type: 'SET_ERROR', payload: `Недостаточно товара в наличии. Доступно: ${availableStock} шт.` }); + return; + } + } + dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } }) } @@ -388,6 +437,10 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children } } + const clearError = () => { + dispatch({ type: 'SET_ERROR', payload: '' }) + } + const contextValue: CartContextType = { state, addItem, @@ -401,7 +454,8 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children removeAll, removeSelected, updateDelivery, - clearCart + clearCart, + clearError } return ( diff --git a/src/hooks/useCatalogPrices.ts b/src/hooks/useCatalogPrices.ts index 16e7f53..794548d 100644 --- a/src/hooks/useCatalogPrices.ts +++ b/src/hooks/useCatalogPrices.ts @@ -231,6 +231,7 @@ export const useCatalogPrices = (): UseCatalogPricesReturn => { price: cheapestOffer.price, currency: cheapestOffer.currency || 'RUB', quantity: 1, + stock: cheapestOffer.quantity, // передаем информацию о наличии deliveryTime: cheapestOffer.deliveryDays?.toString() || '0', warehouse: cheapestOffer.warehouse || 'Склад', supplier: cheapestOffer.supplierName || 'Неизвестный поставщик', @@ -238,10 +239,14 @@ export const useCatalogPrices = (): UseCatalogPricesReturn => { image: '', // Убираем мокап-фотку, изображения будут загружаться отдельно }; - addItem(itemToAdd); + const result = await addItem(itemToAdd); - // Показываем уведомление - toast.success(`Товар "${brand} ${articleNumber}" добавлен в корзину за ${cheapestOffer.price} ₽`); + if (result.success) { + // Показываем уведомление + toast.success(`Товар "${brand} ${articleNumber}" добавлен в корзину за ${cheapestOffer.price} ₽`); + } else { + toast.error(result.error || 'Ошибка добавления товара в корзину'); + } } catch (error) { console.error('Ошибка добавления в корзину:', error); diff --git a/src/pages/catalog.tsx b/src/pages/catalog.tsx index dad444a..7be5a09 100644 --- a/src/pages/catalog.tsx +++ b/src/pages/catalog.tsx @@ -721,7 +721,7 @@ export default function Catalog() { productId={entity.id} artId={entity.id} offerKey={priceData?.offerKey} - onAddToCart={() => { + onAddToCart={async () => { // Если цена не загружена, загружаем её и добавляем в корзину if (!priceData && !isLoadingPriceData) { loadPriceOnDemand(productForPrice); @@ -741,6 +741,7 @@ export default function Catalog() { price: priceData.price, currency: priceData.currency || 'RUB', quantity: 1, + stock: undefined, // информация о наличии не доступна для PartsIndex deliveryTime: '1-3 дня', warehouse: 'Parts Index', supplier: 'Parts Index', @@ -748,19 +749,23 @@ export default function Catalog() { image: entity.images?.[0] || '', }; - addItem(itemToAdd); + const result = await addItem(itemToAdd); - // Показываем уведомление - toast.success( -
-
Товар добавлен в корзину!
-
{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}
-
, - { - duration: 3000, - icon: , - } - ); + if (result.success) { + // Показываем уведомление + toast.success( +
+
Товар добавлен в корзину!
+
{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}
+
, + { + duration: 3000, + icon: , + } + ); + } else { + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); + } } else { toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.'); } diff --git a/src/types/cart.ts b/src/types/cart.ts index f7339ec..dd5a2cc 100644 --- a/src/types/cart.ts +++ b/src/types/cart.ts @@ -10,6 +10,7 @@ export interface CartItem { originalPrice?: number currency: string quantity: number + stock?: string | number // количество товара в наличии на складе deliveryTime?: string deliveryDate?: string warehouse?: string @@ -52,7 +53,7 @@ export interface CartState { export interface CartContextType { state: CartState - addItem: (item: Omit) => void + addItem: (item: Omit) => Promise<{ success: boolean; error?: string }> removeItem: (id: string) => void updateQuantity: (id: string, quantity: number) => void toggleSelect: (id: string) => void @@ -64,4 +65,5 @@ export interface CartContextType { removeSelected: () => void updateDelivery: (delivery: Partial) => void clearCart: () => void + clearError: () => void } \ No newline at end of file