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

This commit is contained in:
Bivekich
2025-06-29 03:36:21 +03:00
parent d268bb3359
commit 7f91da525f
23 changed files with 685 additions and 780 deletions

View File

@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { useCart } from "@/contexts/CartContext";
import { useMutation, useQuery } from "@apollo/client";
import { CREATE_ORDER, CREATE_PAYMENT, GET_CLIENT_ME, GET_CLIENT_DELIVERY_ADDRESSES, GET_DELIVERY_OFFERS } from "@/lib/graphql";
import { CREATE_ORDER, CREATE_PAYMENT, GET_CLIENT_ME, GET_CLIENT_DELIVERY_ADDRESSES } from "@/lib/graphql";
import toast from "react-hot-toast";
const CartSummary: React.FC = () => {
const { state, updateDelivery, updateOrderComment, clearCart } = useCart();
@ -15,7 +16,7 @@ const CartSummary: React.FC = () => {
const [error, setError] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [showAuthWarning, setShowAuthWarning] = useState(false);
const [currentStep, setCurrentStep] = useState(1); // 1 - первый шаг, 2 - второй шаг
const [step, setStep] = useState(1);
// Новые состояния для первого шага
const [selectedLegalEntity, setSelectedLegalEntity] = useState<string>("");
@ -32,22 +33,17 @@ const CartSummary: React.FC = () => {
const [paymentMethod, setPaymentMethod] = useState<string>("yookassa");
const [showPaymentDropdown, setShowPaymentDropdown] = useState(false);
// Состояния для офферов доставки
const [deliveryOffers, setDeliveryOffers] = useState<any[]>([]);
const [selectedDeliveryOffer, setSelectedDeliveryOffer] = useState<any>(null);
const [loadingOffers, setLoadingOffers] = useState(false);
const [offersError, setOffersError] = useState<string>("");
// Упрощенный тип доставки - только курьер или самовывоз
// const [deliveryType, setDeliveryType] = useState<'courier' | 'pickup'>('courier');
const [createOrder] = useMutation(CREATE_ORDER);
const [createPayment] = useMutation(CREATE_PAYMENT);
const [getDeliveryOffers] = useMutation(GET_DELIVERY_OFFERS);
// Убираем useMutation для GET_DELIVERY_OFFERS
// Получаем данные клиента
const { data: clientData, loading: clientLoading } = useQuery(GET_CLIENT_ME);
const { data: addressesData, loading: addressesLoading } = useQuery(GET_CLIENT_DELIVERY_ADDRESSES);
// Получаем пользователя из localStorage для проверки авторизации
const [userData, setUserData] = useState<any>(null);
@ -67,7 +63,7 @@ const CartSummary: React.FC = () => {
if (savedCartSummaryState) {
try {
const state = JSON.parse(savedCartSummaryState);
setCurrentStep(state.currentStep || 1);
setStep(state.step || 1);
setSelectedLegalEntity(state.selectedLegalEntity || '');
setSelectedLegalEntityId(state.selectedLegalEntityId || '');
setIsIndividual(state.isIndividual ?? true);
@ -87,7 +83,7 @@ const CartSummary: React.FC = () => {
useEffect(() => {
if (typeof window !== 'undefined') {
const stateToSave = {
currentStep,
step,
selectedLegalEntity,
selectedLegalEntityId,
isIndividual,
@ -99,7 +95,7 @@ const CartSummary: React.FC = () => {
};
localStorage.setItem('cartSummaryState', JSON.stringify(stateToSave));
}
}, [currentStep, selectedLegalEntity, selectedLegalEntityId, isIndividual, selectedDeliveryAddress, recipientName, recipientPhone, paymentMethod, consent]);
}, [step, selectedLegalEntity, selectedLegalEntityId, isIndividual, selectedDeliveryAddress, recipientName, recipientPhone, paymentMethod, consent]);
// Инициализация данных получателя
useEffect(() => {
@ -134,176 +130,35 @@ const CartSummary: React.FC = () => {
};
}, []);
// Функция для загрузки офферов доставки
const loadDeliveryOffers = async () => {
if (!selectedDeliveryAddress || !recipientName || !recipientPhone || items.length === 0) {
return;
}
setLoadingOffers(true);
setOffersError("");
try {
// Подготавливаем данные для API
const deliveryOffersInput = {
items: items.map(item => {
// Извлекаем срок поставки из deliveryTime товара
let deliveryDays = 0;
if (item.deliveryTime) {
const match = item.deliveryTime.match(/(\d+)/);
if (match) {
deliveryDays = parseInt(match[0]);
}
}
return {
name: item.name,
article: item.article || '',
brand: item.brand || '',
price: item.price,
quantity: item.quantity,
weight: item.weight || 500, // Примерный вес в граммах
dimensions: "10x10x5", // Примерные размеры
deliveryTime: deliveryDays, // Срок поставки товара в днях
offerKey: item.offerKey,
isExternal: item.isExternal
};
}),
deliveryAddress: selectedDeliveryAddress,
recipientName,
recipientPhone
};
const { data } = await getDeliveryOffers({
variables: { input: deliveryOffersInput }
});
if (data?.getDeliveryOffers?.success && data.getDeliveryOffers.offers && Array.isArray(data.getDeliveryOffers.offers) && data.getDeliveryOffers.offers.length > 0) {
setDeliveryOffers(data.getDeliveryOffers.offers);
setOffersError('');
// Автоматически выбираем первый оффер
const firstOffer = data.getDeliveryOffers.offers[0];
setSelectedDeliveryOffer(firstOffer);
// Обновляем стоимость доставки в корзине
updateDelivery({
address: selectedDeliveryAddress,
cost: firstOffer.cost,
date: firstOffer.deliveryDate,
time: firstOffer.deliveryTime
});
} else {
const errorMessage = data?.getDeliveryOffers?.error || 'Не удалось получить варианты доставки';
setOffersError(errorMessage);
// Добавляем стандартные варианты доставки как fallback
const standardOffers = data?.getDeliveryOffers?.offers || [
{
id: 'standard',
name: 'Стандартная доставка',
description: 'Доставка в течение 3-5 рабочих дней',
deliveryDate: 'в течение 3-5 рабочих дней',
deliveryTime: '',
cost: 500
},
{
id: 'express',
name: 'Экспресс доставка',
description: 'Доставка на следующий день',
deliveryDate: 'завтра',
deliveryTime: '10:00-18:00',
cost: 1000
}
];
setDeliveryOffers(standardOffers);
setSelectedDeliveryOffer(standardOffers[0]);
updateDelivery({
address: selectedDeliveryAddress,
cost: standardOffers[0].cost,
date: standardOffers[0].deliveryDate,
time: standardOffers[0].deliveryTime
});
}
} catch (error) {
setOffersError('Ошибка загрузки вариантов доставки');
// Добавляем стандартные варианты доставки как fallback при ошибке
const standardOffers = [
{
id: 'standard',
name: 'Стандартная доставка',
description: 'Доставка в течение 3-5 рабочих дней',
deliveryDate: 'в течение 3-5 рабочих дней',
deliveryTime: '',
cost: 500
}
];
setDeliveryOffers(standardOffers);
setSelectedDeliveryOffer(standardOffers[0]);
updateDelivery({
address: selectedDeliveryAddress,
cost: standardOffers[0].cost,
date: standardOffers[0].deliveryDate,
time: standardOffers[0].deliveryTime
});
} finally {
setLoadingOffers(false);
}
};
// Автоматическая загрузка офферов при изменении ключевых данных
useEffect(() => {
if (selectedDeliveryAddress && recipientName && recipientPhone && items.length > 0) {
// Загружаем офферы с небольшой задержкой для избежания множественных запросов
const timeoutId = setTimeout(() => {
loadDeliveryOffers();
}, 500);
return () => clearTimeout(timeoutId);
}
}, [selectedDeliveryAddress, recipientName, recipientPhone, items.length]);
const handleProceedToStep2 = () => {
if (!selectedDeliveryAddress) {
setError("Пожалуйста, выберите адрес доставки.");
if (!recipientName.trim()) {
toast.error('Пожалуйста, введите имя получателя');
return;
}
if (summary.totalItems === 0) {
setError("Корзина пуста. Добавьте товары для оформления заказа.");
if (!recipientPhone.trim()) {
toast.error('Пожалуйста, введите телефон получателя');
return;
}
if (!selectedDeliveryAddress.trim()) {
toast.error('Пожалуйста, выберите адрес доставки');
return;
}
if (!selectedDeliveryOffer) {
setError("Пожалуйста, выберите способ доставки.");
return;
}
// Обновляем данные доставки без стоимости
updateDelivery({
address: selectedDeliveryAddress,
cost: 0, // Стоимость включена в товары
date: 'Включена в стоимость товаров',
time: 'Способ доставки указан в адресе'
});
// Проверяем достаточность средств для оплаты с баланса
if (paymentMethod === 'balance' && !isIndividual) {
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
const finalAmount = summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice);
const availableBalance = (defaultContract?.balance || 0) + (defaultContract?.creditLimit || 0);
if (availableBalance < finalAmount) {
setError("Недостаточно средств на балансе для оплаты заказа. Выберите другой способ оплаты.");
return;
}
}
setError("");
setCurrentStep(2);
setStep(2);
};
const handleBackToStep1 = () => {
setCurrentStep(1);
setStep(1);
};
const handleSubmit = async () => {
@ -339,7 +194,7 @@ const CartSummary: React.FC = () => {
deliveryAddress: selectedDeliveryAddress || delivery.address,
legalEntityId: !isIndividual ? selectedLegalEntityId : null,
paymentMethod: paymentMethod,
comment: orderComment || `Адрес доставки: ${selectedDeliveryAddress}. ${!isIndividual && selectedLegalEntity ? `Юридическое лицо: ${selectedLegalEntity}.` : 'Физическое лицо.'} Способ оплаты: ${getPaymentMethodName(paymentMethod)}. Доставка: ${selectedDeliveryOffer?.name || 'Стандартная доставка'} (${selectedDeliveryOffer?.deliveryDate || ''} ${selectedDeliveryOffer?.deliveryTime || ''}).`,
comment: orderComment || `Адрес доставки: ${selectedDeliveryAddress}. ${!isIndividual && selectedLegalEntity ? `Юридическое лицо: ${selectedLegalEntity}.` : 'Физическое лицо.'} Способ оплаты: ${getPaymentMethodName(paymentMethod)}. Доставка: ${selectedDeliveryAddress}.`,
items: selectedItems.map(item => ({
productId: item.productId,
externalId: item.offerKey,
@ -367,14 +222,6 @@ const CartSummary: React.FC = () => {
localStorage.removeItem('cartSummaryState');
}
window.location.href = `/payment/success?orderId=${order.id}&orderNumber=${order.orderNumber}&paymentMethod=balance`;
} else if (paymentMethod === 'invoice') {
// Для оплаты по реквизитам - переходим на страницу с реквизитами
clearCart();
// Очищаем сохраненное состояние оформления заказа
if (typeof window !== 'undefined') {
localStorage.removeItem('cartSummaryState');
}
window.location.href = `/payment/invoice?orderId=${order.id}&orderNumber=${order.orderNumber}`;
} else {
// Для ЮКассы - создаем платеж и переходим на оплату
const paymentResult = await createPayment({
@ -421,14 +268,12 @@ const CartSummary: React.FC = () => {
return 'ЮКасса (банковские карты)';
case 'balance':
return 'Оплата с баланса';
case 'invoice':
return 'Оплата по реквизитам';
default:
return 'Выберите способ оплаты';
return 'ЮКасса (банковские карты)';
}
};
if (currentStep === 1) {
if (step === 1) {
// Первый шаг - настройка доставки
return (
<div className="w-layout-vflex cart-ditail">
@ -650,107 +495,6 @@ const CartSummary: React.FC = () => {
)}
</div>
{/* Варианты доставки */}
<div className="w-layout-vflex flex-block-66">
<div className="text-block-31" style={{ marginBottom: '12px' }}>Варианты доставки</div>
{loadingOffers && (
<div style={{
padding: '16px',
textAlign: 'center',
fontSize: '14px',
color: '#666'
}}>
Загружаем варианты доставки...
</div>
)}
{offersError && (
<div style={{
padding: '12px',
backgroundColor: '#FEF3C7',
border: '1px solid #F59E0B',
borderRadius: '4px',
fontSize: '12px',
color: '#92400E',
marginBottom: '12px'
}}>
{offersError}
</div>
)}
{deliveryOffers.length > 0 && !loadingOffers && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{deliveryOffers.map((offer, index) => (
<div
key={offer.id}
onClick={() => {
setSelectedDeliveryOffer(offer);
updateDelivery({
address: selectedDeliveryAddress,
cost: offer.cost,
date: offer.deliveryDate,
time: offer.deliveryTime
});
}}
style={{
padding: '12px',
border: selectedDeliveryOffer?.id === offer.id ? '2px solid #007bff' : '1px solid #dee2e6',
borderRadius: '8px',
cursor: 'pointer',
backgroundColor: selectedDeliveryOffer?.id === offer.id ? '#f8f9fa' : 'white',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
if (selectedDeliveryOffer?.id !== offer.id) {
e.currentTarget.style.backgroundColor = '#f8f9fa';
}
}}
onMouseLeave={(e) => {
if (selectedDeliveryOffer?.id !== offer.id) {
e.currentTarget.style.backgroundColor = 'white';
}
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500, fontSize: '14px', marginBottom: '4px' }}>
{offer.name}
</div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>
{offer.description}
</div>
<div style={{ fontSize: '12px', color: '#007bff' }}>
{offer.deliveryDate} {offer.deliveryTime}
</div>
</div>
<div style={{
fontWeight: 500,
fontSize: '14px',
color: offer.cost === 0 ? '#28a745' : '#333'
}}>
{offer.cost === 0 ? 'Бесплатно' : `${offer.cost}`}
</div>
</div>
</div>
))}
</div>
)}
{deliveryOffers.length === 0 && !loadingOffers && selectedDeliveryAddress && (
<div style={{
padding: '16px',
textAlign: 'center',
fontSize: '14px',
color: '#666',
border: '1px dashed #dee2e6',
borderRadius: '8px'
}}>
Выберите адрес доставки для просмотра вариантов
</div>
)}
</div>
{/* Способ оплаты */}
<div className="w-layout-vflex flex-block-58" style={{ position: 'relative' }} ref={paymentDropdownRef}>
<div className="text-block-31">Способ оплаты</div>
@ -856,24 +600,19 @@ const CartSummary: React.FC = () => {
);
}
const contracts = clientData?.clientMe?.contracts || [];
const defaultContract = contracts.find((contract: any) => contract.isDefault && contract.isActive);
const activeContracts = clientData?.clientMe?.contracts?.filter((contract: any) => contract.isActive) || [];
const defaultContract = activeContracts.find((contract: any) => contract.isDefault) || activeContracts[0];
if (!defaultContract) {
const anyActiveContract = contracts.find((contract: any) => contract.isActive);
if (!anyActiveContract) {
return (
<span style={{ fontWeight: 500, color: '#e74c3c' }}>
Нет активных контрактов
</span>
);
}
return (
<span style={{ color: '#EF4444', fontWeight: 500 }}>
Активный договор не найден
</span>
);
}
const contract = defaultContract || contracts.find((contract: any) => contract.isActive);
const balance = contract?.balance || 0;
const creditLimit = contract?.creditLimit || 0;
const balance = defaultContract.balance || 0;
const creditLimit = defaultContract.creditLimit || 0;
const totalAvailable = balance + creditLimit;
return (
@ -884,59 +623,10 @@ const CartSummary: React.FC = () => {
})()}
</div>
</div>
<div
onClick={() => {
setPaymentMethod('invoice');
setShowPaymentDropdown(false);
}}
style={{
padding: '12px 16px',
cursor: 'pointer',
backgroundColor: paymentMethod === 'invoice' ? '#f8f9fa' : 'white',
fontSize: '14px'
}}
onMouseEnter={(e) => {
if (paymentMethod !== 'invoice') {
e.currentTarget.style.backgroundColor = '#f8f9fa';
}
}}
onMouseLeave={(e) => {
if (paymentMethod !== 'invoice') {
e.currentTarget.style.backgroundColor = 'white';
}
}}
>
Оплата по реквизитам
</div>
</>
)}
</div>
)}
{/* Показываем предупреждение для оплаты с баланса если недостаточно средств */}
{paymentMethod === 'balance' && !isIndividual && (
(() => {
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
const availableBalance = (defaultContract?.balance || 0) + (defaultContract?.creditLimit || 0);
const finalAmount = summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice);
const isInsufficientFunds = availableBalance < finalAmount;
return isInsufficientFunds ? (
<div style={{
marginTop: '8px',
padding: '8px 12px',
backgroundColor: '#FEF3C7',
border: '1px solid #F59E0B',
borderRadius: '4px',
fontSize: '12px',
color: '#92400E'
}}>
Недостаточно средств на балансе для оплаты заказа
</div>
) : null;
})()
)}
</div>
<div className="px-line"></div>
@ -958,10 +648,7 @@ const CartSummary: React.FC = () => {
<div className="w-layout-hflex flex-block-59">
<div className="text-block-21-copy-copy">Доставка</div>
<div className="text-block-33">
{selectedDeliveryOffer?.cost === 0
? 'Бесплатно'
: formatPrice(selectedDeliveryOffer?.cost || summary.deliveryPrice)
}
Включена в стоимость товаров
</div>
</div>
</div>
@ -972,7 +659,7 @@ const CartSummary: React.FC = () => {
<div className="text-block-32">Итого</div>
<h4 className="heading-9-copy-copy">
{formatPrice(
summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice)
summary.totalPrice - summary.totalDiscount + (selectedDeliveryAddress ? 0 : summary.deliveryPrice)
)}
</h4>
</div>
@ -1075,9 +762,19 @@ const CartSummary: React.FC = () => {
{paymentMethod === 'balance' && !isIndividual && (
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
{(() => {
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
const balance = defaultContract?.balance || 0;
const creditLimit = defaultContract?.creditLimit || 0;
const activeContracts = clientData?.clientMe?.contracts?.filter((contract: any) => contract.isActive) || [];
const defaultContract = activeContracts.find((contract: any) => contract.isDefault) || activeContracts[0];
if (!defaultContract) {
return (
<span style={{ color: '#EF4444', fontWeight: 500 }}>
Активный договор не найден
</span>
);
}
const balance = defaultContract.balance || 0;
const creditLimit = defaultContract.creditLimit || 0;
const totalAvailable = balance + creditLimit;
return (
@ -1131,10 +828,7 @@ const CartSummary: React.FC = () => {
<div className="w-layout-hflex flex-block-59">
<div className="text-block-21-copy-copy">Доставка</div>
<div className="text-block-33">
{selectedDeliveryOffer?.cost === 0
? 'Бесплатно'
: formatPrice(selectedDeliveryOffer?.cost || summary.deliveryPrice)
}
Включена в стоимость товаров
</div>
</div>
</div>
@ -1145,7 +839,7 @@ const CartSummary: React.FC = () => {
<div className="text-block-32">Итого</div>
<h4 className="heading-9-copy-copy">
{formatPrice(
summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice)
summary.totalPrice - summary.totalDiscount + (selectedDeliveryAddress ? 0 : summary.deliveryPrice)
)}
</h4>
</div>
@ -1196,7 +890,6 @@ const CartSummary: React.FC = () => {
>
{isProcessing ? 'Оформляем заказ...' :
paymentMethod === 'balance' ? 'Оплатить с баланса' :
paymentMethod === 'invoice' ? 'Выставить счёт' :
'Оплатить'}
</button>