Удален файл интеграции с Parts Index API и обновлены компоненты для работы с корзиной и избранным. Добавлены функции для обработки добавления товаров в корзину с уведомлениями, улучшена логика работы с избранным, а также добавлены фильтры для истории поиска по производителю.
This commit is contained in:
@ -1,117 +0,0 @@
|
||||
# Интеграция Parts Index API
|
||||
|
||||
## Описание
|
||||
|
||||
В проект добавлена интеграция с Parts Index API для отображения детальной информации о деталях, включая:
|
||||
- Главную фотографию детали
|
||||
- Штрих-коды
|
||||
- Технические характеристики
|
||||
- Категории
|
||||
- Дополнительные изображения
|
||||
- Логотип "powered by Parts Index"
|
||||
|
||||
## Реализованные компоненты
|
||||
|
||||
### 1. Типы (`src/types/partsindex.ts`)
|
||||
- `PartsIndexEntityInfo` - информация о детали
|
||||
- `PartsIndexEntityInfoResponse` - ответ API
|
||||
- `PartsIndexEntityInfoVariables` - параметры запроса
|
||||
|
||||
### 2. Сервис (`src/lib/partsindex-service.ts`)
|
||||
- `getEntityInfo(code, brand?, lang?)` - получение информации о детали
|
||||
|
||||
### 3. Хук (`src/hooks/usePartsIndex.ts`)
|
||||
- `usePartsIndexEntityInfo(code, brand)` - хук для получения данных
|
||||
|
||||
### 4. Компонент (`src/components/PartsIndexCard.tsx`)
|
||||
- Отображение карточки с информацией о детали
|
||||
- Поддержка состояния загрузки
|
||||
- Адаптивный дизайн
|
||||
|
||||
## Интеграция в страницу поиска
|
||||
|
||||
В файле `src/pages/search-result.tsx` добавлено:
|
||||
|
||||
```tsx
|
||||
import PartsIndexCard from "@/components/PartsIndexCard";
|
||||
import { usePartsIndexEntityInfo } from "@/hooks/usePartsIndex";
|
||||
|
||||
// В компоненте:
|
||||
const { entityInfo, loading: partsIndexLoading } = usePartsIndexEntityInfo(
|
||||
searchQuery || null,
|
||||
brandQuery || null
|
||||
);
|
||||
|
||||
// В JSX:
|
||||
{partsIndexLoading && (
|
||||
<PartsIndexCard
|
||||
entityInfo={null as any}
|
||||
loading={true}
|
||||
/>
|
||||
)}
|
||||
{entityInfo && !partsIndexLoading && (
|
||||
<PartsIndexCard
|
||||
entityInfo={entityInfo}
|
||||
loading={false}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
## API Parts Index
|
||||
|
||||
### Endpoint
|
||||
```
|
||||
GET https://api.parts-index.com/v1/entities
|
||||
```
|
||||
|
||||
### Параметры
|
||||
- `code` (обязательный) - артикул детали
|
||||
- `brand` (опциональный) - бренд
|
||||
- `lang` (опциональный) - язык (по умолчанию 'ru')
|
||||
|
||||
### Заголовки
|
||||
```
|
||||
Authorization: PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Пример запроса
|
||||
```bash
|
||||
curl -H "Authorization: PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE" \
|
||||
"https://api.parts-index.com/v1/entities?code=059198405B&brand=VAG&lang=ru"
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### URL для тестирования
|
||||
```
|
||||
http://localhost:3002/search-result?article=059198405B&brand=VAG
|
||||
```
|
||||
|
||||
### Тестовая HTML страница
|
||||
Создана страница `test-parts-index.html` для демонстрации работы API без React.
|
||||
|
||||
## Функциональность
|
||||
|
||||
1. **Автоматическая загрузка** - при переходе на страницу результатов поиска
|
||||
2. **Главная фотография** - отображается первое изображение из массива
|
||||
3. **Логотип Parts Index** - в правом верхнем углу карточки
|
||||
4. **Характеристики** - первые 6 параметров из API
|
||||
5. **Штрих-коды** - все доступные штрих-коды
|
||||
6. **Дополнительные изображения** - до 4 дополнительных фото
|
||||
7. **Обработка ошибок** - скрытие изображений при ошибке загрузки
|
||||
|
||||
## Стили
|
||||
|
||||
Компонент использует Tailwind CSS классы для стилизации:
|
||||
- Адаптивная сетка для характеристик
|
||||
- Скроллинг для дополнительных изображений
|
||||
- Состояние загрузки с анимацией
|
||||
- Обработка ошибок изображений
|
||||
|
||||
## Производительность
|
||||
|
||||
- Загрузка данных только при наличии артикула
|
||||
- Кэширование на уровне React Query (через Apollo Client)
|
||||
- Ленивая загрузка изображений
|
||||
- Обработка ошибок сети
|
@ -1,4 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface BestPriceCardProps {
|
||||
bestOfferType: string;
|
||||
@ -7,9 +9,20 @@ interface BestPriceCardProps {
|
||||
price: string;
|
||||
delivery: string;
|
||||
stock: string;
|
||||
offer?: any; // Добавляем полный объект предложения для корзины
|
||||
}
|
||||
|
||||
const BestPriceCard: React.FC<BestPriceCardProps> = ({ bestOfferType, title, description, price, delivery, stock }) => {
|
||||
const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
bestOfferType,
|
||||
title,
|
||||
description,
|
||||
price,
|
||||
delivery,
|
||||
stock,
|
||||
offer
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
|
||||
// Парсим stock в число, если возможно
|
||||
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
||||
const maxCount = isNaN(parsedStock) ? undefined : parsedStock;
|
||||
@ -28,12 +41,75 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({ bestOfferType, title, des
|
||||
let value = parseInt(e.target.value, 10);
|
||||
if (isNaN(value) || value < 1) value = 1;
|
||||
if (maxCount !== undefined && value > maxCount) {
|
||||
window.alert(`Максимум ${maxCount} шт.`);
|
||||
toast.error(`Максимум ${maxCount} шт.`);
|
||||
return;
|
||||
}
|
||||
setCount(value);
|
||||
};
|
||||
|
||||
// Функция для парсинга цены из строки
|
||||
const parsePrice = (priceStr: string): number => {
|
||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||
return parseFloat(cleanPrice) || 0;
|
||||
};
|
||||
|
||||
// Обработчик добавления в корзину
|
||||
const handleAddToCart = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!offer) {
|
||||
toast.error('Информация о товаре недоступна');
|
||||
return;
|
||||
}
|
||||
|
||||
const numericPrice = parsePrice(price);
|
||||
if (numericPrice <= 0) {
|
||||
toast.error('Цена товара не найдена');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем наличие
|
||||
if (maxCount !== undefined && count > maxCount) {
|
||||
toast.error(`Недостаточно товара в наличии. Доступно: ${maxCount} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
addItem({
|
||||
productId: offer.productId,
|
||||
offerKey: offer.offerKey,
|
||||
name: description,
|
||||
description: `${offer.brand} ${offer.articleNumber} - ${description}`,
|
||||
brand: offer.brand,
|
||||
article: offer.articleNumber,
|
||||
price: numericPrice,
|
||||
currency: offer.currency || 'RUB',
|
||||
quantity: count,
|
||||
deliveryTime: delivery,
|
||||
warehouse: offer.warehouse || 'Склад',
|
||||
supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'),
|
||||
isExternal: offer.isExternal || false,
|
||||
image: offer.image,
|
||||
});
|
||||
|
||||
// Показываем тоастер об успешном добавлении
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка добавления товара в корзину');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-44">
|
||||
<h3 className="heading-8-copy line-clamp-2 md:line-clamp-1 min-h-[2.5em] md:min-h-0">{bestOfferType}</h3>
|
||||
@ -80,11 +156,17 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({ bestOfferType, title, des
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-42">
|
||||
<a href="#" className="button-icon w-inline-block">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddToCart}
|
||||
className="button-icon w-inline-block"
|
||||
style={{ cursor: 'pointer', textDecoration: 'none' }}
|
||||
aria-label="Добавить в корзину"
|
||||
>
|
||||
<div className="div-block-26">
|
||||
<div className="icon-setting w-embed"><svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"/></svg></div>
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@ import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
|
||||
const CartList: React.FC = () => {
|
||||
const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const { items } = state;
|
||||
|
||||
const allSelected = items.length > 0 && items.every((item) => item.selected);
|
||||
@ -29,9 +29,18 @@ const CartList: React.FC = () => {
|
||||
const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand);
|
||||
|
||||
if (isInFavorites) {
|
||||
// Удаляем из избранного
|
||||
const favoriteId = `${item.productId || item.offerKey || ''}:${item.article}:${item.brand}`;
|
||||
removeFromFavorites(favoriteId);
|
||||
// Находим товар в избранном по правильному ID
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (item.productId && fav.productId === item.productId) return true;
|
||||
if (item.offerKey && fav.offerKey === item.offerKey) return true;
|
||||
if (fav.article === item.article && fav.brand === item.brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
addToFavorites({
|
||||
|
@ -1,88 +1,79 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const initialItems = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Ganz GIE37312",
|
||||
description: "Ролик ремня ГРМ VW AD GANZ GIE37312",
|
||||
delivery: "Послезавтра, курьером",
|
||||
deliveryDate: "пт, 7 февраля",
|
||||
price: "18 763 ₽",
|
||||
pricePerItem: "18 763 ₽/шт",
|
||||
count: 1,
|
||||
comment: "",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Ganz GIE37312",
|
||||
description: "Ролик ремня ГРМ VW AD GANZ GIE37312",
|
||||
delivery: "Послезавтра, курьером",
|
||||
deliveryDate: "пт, 7 февраля",
|
||||
price: "18 763 ₽",
|
||||
pricePerItem: "18 763 ₽/шт",
|
||||
count: 1,
|
||||
comment: "",
|
||||
},
|
||||
// ...ещё товары
|
||||
];
|
||||
import React from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
|
||||
const CartList2: React.FC = () => {
|
||||
const [items, setItems] = useState(initialItems);
|
||||
const { state, updateComment } = useCart();
|
||||
const { items } = state;
|
||||
|
||||
const handleComment = (id: number, comment: string) => {
|
||||
setItems((prev) => prev.map((item) => item.id === id ? { ...item, comment } : item));
|
||||
const handleComment = (id: string, comment: string) => {
|
||||
updateComment(id, comment);
|
||||
};
|
||||
|
||||
// Функция для форматирования цены
|
||||
const formatPrice = (price: number, currency: string = 'RUB') => {
|
||||
return `${price.toLocaleString('ru-RU')} ${currency === 'RUB' ? '₽' : currency}`;
|
||||
};
|
||||
|
||||
// Показываем только выбранные товары на втором этапе
|
||||
const selectedItems = items.filter(item => item.selected);
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-48">
|
||||
<div className="w-layout-vflex product-list-cart-check">
|
||||
{items.map((item) => (
|
||||
<div className="div-block-21-copy" key={item.id}>
|
||||
<div className="w-layout-hflex cart-item-check">
|
||||
<div className="w-layout-hflex info-block-search">
|
||||
<div className="text-block-35">{item.count}</div>
|
||||
<div className="w-layout-hflex block-name">
|
||||
<h4 className="heading-9-copy">{item.name}</h4>
|
||||
<div className="text-block-21-copy">{item.description}</div>
|
||||
</div>
|
||||
<div className="form-block-copy w-form">
|
||||
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
||||
<input
|
||||
className="text-field-copy w-input"
|
||||
maxLength={256}
|
||||
name="Search-5"
|
||||
data-name="Search 5"
|
||||
placeholder="Комментарий"
|
||||
type="text"
|
||||
id="Search-5"
|
||||
value={item.comment}
|
||||
onChange={e => handleComment(item.id, e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
<div className="success-message w-form-done">
|
||||
<div>Thank you! Your submission has been received!</div>
|
||||
{selectedItems.length === 0 ? (
|
||||
<div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
|
||||
<p>Не выбрано товаров для заказа</p>
|
||||
<p>Вернитесь на предыдущий шаг и выберите товары</p>
|
||||
</div>
|
||||
) : (
|
||||
selectedItems.map((item) => (
|
||||
<div className="div-block-21-copy" key={item.id}>
|
||||
<div className="w-layout-hflex cart-item-check">
|
||||
<div className="w-layout-hflex info-block-search">
|
||||
<div className="text-block-35">{item.quantity}</div>
|
||||
<div className="w-layout-hflex block-name">
|
||||
<h4 className="heading-9-copy">{item.name}</h4>
|
||||
<div className="text-block-21-copy">{item.description}</div>
|
||||
</div>
|
||||
<div className="error-message w-form-fail">
|
||||
<div>Oops! Something went wrong while submitting the form.</div>
|
||||
<div className="form-block-copy w-form">
|
||||
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
||||
<input
|
||||
className="text-field-copy w-input"
|
||||
maxLength={256}
|
||||
name="Search-5"
|
||||
data-name="Search 5"
|
||||
placeholder="Комментарий"
|
||||
type="text"
|
||||
id={`Search-${item.id}`}
|
||||
value={item.comment || ''}
|
||||
onChange={e => handleComment(item.id, e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
<div className="success-message w-form-done">
|
||||
<div>Thank you! Your submission has been received!</div>
|
||||
</div>
|
||||
<div className="error-message w-form-fail">
|
||||
<div>Oops! Something went wrong while submitting the form.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex add-to-cart-block">
|
||||
<div className="w-layout-hflex flex-block-39-copy">
|
||||
<h4 className="heading-9-copy">{item.delivery}</h4>
|
||||
<div className="text-block-21-copy">{item.deliveryDate}</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex pcs">
|
||||
<div className="pcs-text">{item.count} шт.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||
<h4 className="heading-9-copy-copy">{item.price}</h4>
|
||||
<div className="text-block-21-copy-copy">{item.pricePerItem}</div>
|
||||
<div className="w-layout-hflex add-to-cart-block">
|
||||
<div className="w-layout-hflex flex-block-39-copy">
|
||||
<h4 className="heading-9-copy">{item.deliveryTime || 'Уточняется'}</h4>
|
||||
<div className="text-block-21-copy">{item.deliveryDate || ''}</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex pcs">
|
||||
<div className="pcs-text">{item.quantity} шт.</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||
<h4 className="heading-9-copy-copy">{formatPrice(item.price * item.quantity, item.currency)}</h4>
|
||||
<div className="text-block-21-copy-copy">{formatPrice(item.price, item.currency)}/шт</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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>
|
||||
|
||||
|
@ -35,7 +35,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
||||
priceElement,
|
||||
onAddToCart,
|
||||
}) => {
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
|
||||
// Обрабатываем пустое изображение - используем SVG-заглушку вместо мокап-фотки
|
||||
const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=';
|
||||
@ -57,9 +57,18 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
||||
const numericPrice = parseFloat(price.replace(/[^\d.,]/g, '').replace(',', '.')) || 0;
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Создаем ID для удаления
|
||||
const id = `${productId || offerKey || ''}:${articleNumber}:${brandName || brand}`;
|
||||
removeFromFavorites(id);
|
||||
// Находим товар в избранном по правильному ID
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (productId && fav.productId === productId) return true;
|
||||
if (offerKey && fav.offerKey === offerKey) return true;
|
||||
if (fav.article === articleNumber && fav.brand === (brandName || brand)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
addToFavorites({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const INITIAL_OFFERS_LIMIT = 5;
|
||||
|
||||
@ -46,7 +47,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
partsIndexPowered = false
|
||||
}) => {
|
||||
const { addItem } = useCart();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
||||
@ -88,7 +89,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num) || num < 1) num = 1;
|
||||
if (num > availableStock) {
|
||||
window.alert(`Максимум ${availableStock} шт.`);
|
||||
toast.error(`Максимум ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
setQuantities(prev => ({ ...prev, [index]: num }));
|
||||
@ -100,7 +101,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
|
||||
// Проверяем наличие
|
||||
if (quantity > availableStock) {
|
||||
alert(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||
toast.error(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -123,8 +124,17 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
image: image,
|
||||
});
|
||||
|
||||
// Показываем уведомление о добавлении
|
||||
alert(`Товар "${brand} ${article}" добавлен в корзину (${quantity} шт.)`);
|
||||
// Показываем тоастер вместо alert
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{`${brand} ${article} (${quantity} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Обработчик клика по сердечку
|
||||
@ -133,9 +143,18 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Создаем ID для удаления
|
||||
const id = `${offers[0]?.productId || offers[0]?.offerKey || ''}:${article}:${brand}`;
|
||||
removeFromFavorites(id);
|
||||
// Находим товар в избранном и удаляем по правильному ID
|
||||
const favoriteItem = favorites.find((item: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (offers[0]?.productId && item.productId === offers[0].productId) return true;
|
||||
if (offers[0]?.offerKey && item.offerKey === offers[0].offerKey) return true;
|
||||
if (item.article === article && item.brand === brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
const bestOffer = offers[0]; // Берем первое предложение как лучшее
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Filters, { FilterConfig } from "./Filters";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
|
||||
@ -8,9 +9,9 @@ interface FavoriteListProps {
|
||||
onFilterChange?: (type: string, value: any) => void;
|
||||
searchQuery?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
sortBy?: 'name' | 'brand' | 'price' | 'date';
|
||||
sortBy?: 'name' | 'brand' | 'date';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
onSortChange?: (sortBy: 'name' | 'brand' | 'price' | 'date') => void;
|
||||
onSortChange?: (sortBy: 'name' | 'brand' | 'date') => void;
|
||||
onSortOrderChange?: (sortOrder: 'asc' | 'desc') => void;
|
||||
}
|
||||
|
||||
@ -25,6 +26,7 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
onSortChange,
|
||||
onSortOrderChange
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { favorites, removeFromFavorites, clearFavorites } = useFavorites();
|
||||
|
||||
const handleRemove = (id: string) => {
|
||||
@ -35,6 +37,14 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
clearFavorites();
|
||||
};
|
||||
|
||||
// Функция для поиска товара
|
||||
const handleSearchItem = (item: any) => {
|
||||
// Формируем поисковый запрос из бренда и артикула
|
||||
const searchQuery = `${item.brand} ${item.article}`;
|
||||
// Переходим на страницу поиска с результатами
|
||||
router.push(`/search-result?article=${encodeURIComponent(item.article)}&brand=${encodeURIComponent(item.brand)}`);
|
||||
};
|
||||
|
||||
// Состояние для hover на иконке удаления всех
|
||||
const [removeAllHover, setRemoveAllHover] = useState(false);
|
||||
// Состояние для hover на корзине отдельного товара
|
||||
@ -82,11 +92,6 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
case 'brand':
|
||||
comparison = a.brand.localeCompare(b.brand);
|
||||
break;
|
||||
case 'price':
|
||||
const priceA = a.price || 0;
|
||||
const priceB = b.price || 0;
|
||||
comparison = priceA - priceB;
|
||||
break;
|
||||
case 'date':
|
||||
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
break;
|
||||
@ -97,29 +102,19 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
return sortOrder === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
|
||||
const formatPrice = (price?: number, currency?: string) => {
|
||||
if (!price) {
|
||||
return 'Цена не указана';
|
||||
}
|
||||
if (currency === 'RUB') {
|
||||
return `от ${price.toLocaleString('ru-RU')} ₽`;
|
||||
}
|
||||
return `от ${price} ${currency || ''}`;
|
||||
};
|
||||
|
||||
const handleSortClick = (newSortBy: 'name' | 'brand' | 'price' | 'date') => {
|
||||
const handleSortClick = (newSortBy: 'name' | 'brand' | 'date') => {
|
||||
if (sortBy === newSortBy) {
|
||||
// Если тот же столбец, меняем порядок
|
||||
onSortOrderChange?.(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
// Если новый столбец, устанавливаем его и порядок по умолчанию
|
||||
onSortChange?.(newSortBy);
|
||||
onSortOrderChange?.(newSortBy === 'price' ? 'asc' : 'desc');
|
||||
onSortOrderChange?.(newSortBy === 'date' ? 'desc' : 'asc');
|
||||
}
|
||||
};
|
||||
|
||||
// SVG-галочки для сортировки — всегда видны у всех колонок
|
||||
const getSortIcon = (columnSort: 'name' | 'brand' | 'price' | 'date') => {
|
||||
const getSortIcon = (columnSort: 'name' | 'brand' | 'date') => {
|
||||
const isActive = sortBy === columnSort;
|
||||
const isAsc = sortOrder === 'asc';
|
||||
const color = isActive ? 'var(--_button---primary)' : '#94a3b8';
|
||||
@ -176,6 +171,9 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
</div>
|
||||
<div className="sort-item-comments">Комментарий</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex add-to-cart-block-copy">
|
||||
<div className="text-sm font-medium text-gray-600">Действия</div>
|
||||
</div>
|
||||
{favorites.length > 0 && (
|
||||
<div
|
||||
className="w-layout-hflex select-all-block"
|
||||
@ -233,12 +231,22 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-layout-hflex add-to-cart-block-copy">
|
||||
<h4
|
||||
className="heading-9-copy-copy cursor-pointer hover:text-blue-600 flex items-center gap-1"
|
||||
onClick={() => handleSortClick('price')}
|
||||
<button
|
||||
onClick={() => handleSearchItem(item)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors duration-200 flex items-center gap-2 text-sm font-medium"
|
||||
>
|
||||
{formatPrice(item.price, item.currency)} {getSortIcon('price')}
|
||||
</h4>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="m21 21-4.35-4.35" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
Поиск
|
||||
</button>
|
||||
<div className="w-layout-hflex control-element-copy">
|
||||
{/* Корзина с hover-эффектом для удаления товара */}
|
||||
<span
|
||||
|
@ -58,6 +58,15 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
setSearchQuery(q);
|
||||
}
|
||||
}
|
||||
// Если мы находимся на странице деталей автомобиля, восстанавливаем VIN из URL
|
||||
else if (router.pathname === '/vehicle-search/[brand]/[vehicleId]') {
|
||||
const { vin } = router.query;
|
||||
if (vin && typeof vin === 'string') {
|
||||
setSearchQuery(vin);
|
||||
} else {
|
||||
setSearchQuery('');
|
||||
}
|
||||
}
|
||||
// Для других страниц очищаем поисковый запрос
|
||||
else {
|
||||
setSearchQuery('');
|
||||
@ -321,11 +330,31 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
const catalogCode = (vehicle as any).catalog || vehicle.brand.toLowerCase();
|
||||
console.log('🚗 Переход на страницу автомобиля:', { catalogCode, vehicleId: vehicle.vehicleid, ssd: vehicle.ssd });
|
||||
|
||||
// Создаем параметры URL
|
||||
const urlParams = new URLSearchParams();
|
||||
|
||||
// Добавляем SSD если есть
|
||||
if (vehicle.ssd) {
|
||||
urlParams.set('ssd', vehicle.ssd);
|
||||
}
|
||||
|
||||
// Добавляем VIN-номер в URL, если поиск был по VIN
|
||||
if (searchType === 'vin' && searchQuery) {
|
||||
urlParams.set('vin', searchQuery);
|
||||
}
|
||||
|
||||
// Если переход происходит из поиска автомобилей по артикулу, передаем артикул для автоматического поиска
|
||||
const currentOEMNumber = oemSearchMode === 'vehicles' ? searchQuery.trim().toUpperCase() : '';
|
||||
const url = `/vehicle-search/${catalogCode}/${vehicle.vehicleid}?ssd=${vehicle.ssd || ''}${currentOEMNumber ? `&oemNumber=${encodeURIComponent(currentOEMNumber)}` : ''}`;
|
||||
if (currentOEMNumber) {
|
||||
urlParams.set('oemNumber', currentOEMNumber);
|
||||
}
|
||||
|
||||
setSearchQuery('');
|
||||
// Формируем URL
|
||||
const baseUrl = `/vehicle-search/${catalogCode}/${vehicle.vehicleid}`;
|
||||
const url = urlParams.toString() ? `${baseUrl}?${urlParams.toString()}` : baseUrl;
|
||||
|
||||
// НЕ очищаем поисковый запрос, чтобы он остался в строке поиска
|
||||
// setSearchQuery('');
|
||||
router.push(url);
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import * as React from "react";
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
||||
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||
|
||||
const menuItems = [
|
||||
{ label: 'Заказы', href: '/profile-orders', icon: 'https://cdn.builder.io/api/v1/image/assets/TEMP/22ecd7e6251abe04521d03f0ac09f73018a8c2c8?placeholderIfAbsent=true&apiKey=f5bc5a2dc9b841d0aba1cc6c74a35920' },
|
||||
@ -28,6 +30,15 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
||||
const router = useRouter();
|
||||
const isClient = useIsClient();
|
||||
|
||||
// Получаем данные клиента для проверки наличия юридических лиц
|
||||
const { data: clientData } = useQuery(GET_CLIENT_ME, {
|
||||
skip: !isClient,
|
||||
errorPolicy: 'ignore' // Игнорируем ошибки, чтобы не ломать интерфейс
|
||||
});
|
||||
|
||||
// Проверяем есть ли у клиента юридические лица
|
||||
const hasLegalEntities = clientData?.clientMe?.legalEntities && clientData.clientMe.legalEntities.length > 0;
|
||||
|
||||
const handleLogout = () => {
|
||||
if (isClient) {
|
||||
localStorage.removeItem('authToken');
|
||||
@ -66,31 +77,37 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="gap-2.5 self-start px-2.5 pt-2.5 mt-3 whitespace-nowrap text-gray-950">
|
||||
Финансы
|
||||
</div>
|
||||
<div className="flex flex-col mt-3 w-full text-base leading-snug text-gray-600">
|
||||
{financeItems.map((item) => {
|
||||
const isActive = normalizePath(router.asPath) === normalizePath(item.href);
|
||||
return (
|
||||
<Link href={item.href} key={item.href}>
|
||||
<div
|
||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||
isActive ? 'bg-slate-200' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
src={item.icon}
|
||||
className="object-contain shrink-0 self-stretch my-auto w-5 aspect-square"
|
||||
alt={item.label}
|
||||
/>
|
||||
<div className="self-stretch my-auto text-gray-600">{item.label}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Раздел "Финансы" показываем только если есть юридические лица */}
|
||||
{hasLegalEntities && (
|
||||
<>
|
||||
<div className="gap-2.5 self-start px-2.5 pt-2.5 mt-3 whitespace-nowrap text-gray-950">
|
||||
Финансы
|
||||
</div>
|
||||
<div className="flex flex-col mt-3 w-full text-base leading-snug text-gray-600">
|
||||
{financeItems.map((item) => {
|
||||
const isActive = normalizePath(router.asPath) === normalizePath(item.href);
|
||||
return (
|
||||
<Link href={item.href} key={item.href}>
|
||||
<div
|
||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||
isActive ? 'bg-slate-200' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
src={item.icon}
|
||||
className="object-contain shrink-0 self-stretch my-auto w-5 aspect-square"
|
||||
alt={item.label}
|
||||
/>
|
||||
<div className="self-stretch my-auto text-gray-600">{item.label}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Кнопка выхода */}
|
||||
<div className="mt-3">
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
||||
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||
|
||||
interface ProfileSidebarProps {
|
||||
activeItem: string;
|
||||
@ -8,6 +10,15 @@ interface ProfileSidebarProps {
|
||||
const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
||||
const isClient = useIsClient();
|
||||
|
||||
// Получаем данные клиента для проверки наличия юридических лиц
|
||||
const { data: clientData } = useQuery(GET_CLIENT_ME, {
|
||||
skip: !isClient,
|
||||
errorPolicy: 'ignore' // Игнорируем ошибки, чтобы не ломать интерфейс
|
||||
});
|
||||
|
||||
// Проверяем есть ли у клиента юридические лица
|
||||
const hasLegalEntities = clientData?.clientMe?.legalEntities && clientData.clientMe.legalEntities.length > 0;
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'orders', icon: 'order', label: 'Заказы', href: '/profile-orders' },
|
||||
{ id: 'history', icon: 'history', label: 'История поиска', href: '/profile-history' },
|
||||
@ -133,25 +144,28 @@ const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header">
|
||||
<h3 className="sidebar-title">Финансы</h3>
|
||||
{/* Раздел "Финансы" показываем только если есть юридические лица */}
|
||||
{hasLegalEntities && (
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-header">
|
||||
<h3 className="sidebar-title">Финансы</h3>
|
||||
</div>
|
||||
<div className="sidebar-menu">
|
||||
{financeItems.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className={`sidebar-item ${activeItem === item.id ? 'active' : ''}`}
|
||||
>
|
||||
<div className="sidebar-icon">
|
||||
{renderIcon(item.icon, activeItem === item.id)}
|
||||
</div>
|
||||
<span className="sidebar-label">{item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-menu">
|
||||
{financeItems.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={item.href}
|
||||
className={`sidebar-item ${activeItem === item.id ? 'active' : ''}`}
|
||||
>
|
||||
<div className="sidebar-icon">
|
||||
{renderIcon(item.icon, activeItem === item.id)}
|
||||
</div>
|
||||
<span className="sidebar-label">{item.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка выхода */}
|
||||
<div className="logout-section">
|
||||
|
@ -23,7 +23,7 @@ export default function InfoCard({
|
||||
currency = 'RUB',
|
||||
image
|
||||
}: InfoCardProps) {
|
||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
||||
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||
|
||||
// Проверяем, есть ли товар в избранном
|
||||
const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brand);
|
||||
@ -34,9 +34,18 @@ export default function InfoCard({
|
||||
e.stopPropagation();
|
||||
|
||||
if (isItemFavorite) {
|
||||
// Создаем ID для удаления
|
||||
const id = `${productId || offerKey || ''}:${articleNumber}:${brand}`;
|
||||
removeFromFavorites(id);
|
||||
// Находим товар в избранном по правильному ID
|
||||
const favoriteItem = favorites.find((fav: any) => {
|
||||
// Проверяем по разным комбинациям идентификаторов
|
||||
if (productId && fav.productId === productId) return true;
|
||||
if (offerKey && fav.offerKey === offerKey) return true;
|
||||
if (fav.article === articleNumber && fav.brand === brand) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (favoriteItem) {
|
||||
removeFromFavorites(favoriteItem.id);
|
||||
}
|
||||
} else {
|
||||
// Добавляем в избранное
|
||||
addToFavorites({
|
||||
|
@ -1,96 +1,47 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { PARTS_INDEX_SEARCH_BY_ARTICLE } from "@/lib/graphql";
|
||||
|
||||
interface ProductCharacteristicsProps {
|
||||
result?: any;
|
||||
}
|
||||
|
||||
const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
||||
const [partsIndexData, setPartsIndexData] = useState<any>(null);
|
||||
|
||||
// Функция для рендеринга характеристик из нашей базы данных
|
||||
const renderInternalCharacteristics = () => {
|
||||
if (!result?.characteristics || result.characteristics.length === 0) return null;
|
||||
// Запрос к Parts Index для получения дополнительных характеристик
|
||||
const { data: partsIndexResult, loading: partsIndexLoading } = useQuery(PARTS_INDEX_SEARCH_BY_ARTICLE, {
|
||||
variables: {
|
||||
articleNumber: result?.articleNumber || '',
|
||||
brandName: result?.brand || '',
|
||||
lang: 'ru'
|
||||
},
|
||||
skip: !result?.articleNumber || !result?.brand,
|
||||
errorPolicy: 'ignore'
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-53">
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
||||
Характеристики товара:
|
||||
</span>
|
||||
</div>
|
||||
{result.characteristics.map((char: any, index: number) => (
|
||||
<div key={index} className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">{char.characteristic.name}:</span>
|
||||
<span className="text-block-28">{char.value}</span>
|
||||
useEffect(() => {
|
||||
if (partsIndexResult?.partsIndexSearchByArticle) {
|
||||
setPartsIndexData(partsIndexResult.partsIndexSearchByArticle);
|
||||
}
|
||||
}, [partsIndexResult]);
|
||||
|
||||
// Функция для рендеринга параметров из Parts Index
|
||||
const renderPartsIndexParameters = () => {
|
||||
if (!partsIndexData?.parameters) return null;
|
||||
|
||||
return partsIndexData.parameters.map((paramGroup: any, groupIndex: number) => (
|
||||
<div key={groupIndex} className="w-layout-vflex flex-block-53">
|
||||
{paramGroup.params?.map((param: any, paramIndex: number) => (
|
||||
<div key={paramIndex} className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">{param.title}:</span>
|
||||
<span className="text-block-28">
|
||||
{param.values?.map((value: any) => value.value).join(', ') || 'Нет данных'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Функция для рендеринга характеристик из Parts Index
|
||||
const renderPartsIndexCharacteristics = () => {
|
||||
if (!result?.partsIndexCharacteristics || result.partsIndexCharacteristics.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-53">
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
||||
Дополнительные характеристики:
|
||||
</span>
|
||||
</div>
|
||||
{result.partsIndexCharacteristics.map((char: any, index: number) => (
|
||||
<div key={index} className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">{char.name}:</span>
|
||||
<span className="text-block-28">{char.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Функция для рендеринга изображений из нашей базы данных
|
||||
const renderInternalImages = () => {
|
||||
if (!result?.images || result.images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="w-layout-vflex flex-block-53" style={{ marginTop: '20px' }}>
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
||||
Изображения товара:
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-layout-hflex" style={{ flexWrap: 'wrap', gap: '10px', marginTop: '10px' }}>
|
||||
{result.images.slice(0, 6).map((image: any, index: number) => (
|
||||
<div key={image.id || index} style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f9f9f9'
|
||||
}}>
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.alt || `${result?.brand} ${result?.articleNumber} - изображение ${index + 1}`}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
onClick={() => window.open(image.url, '_blank')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -111,27 +62,33 @@ const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
||||
<span className="text-block-29">Название:</span>
|
||||
<span className="text-block-28">{result.name}</span>
|
||||
</div>
|
||||
{result?.description && (
|
||||
{partsIndexData?.originalName && (
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">Оригинальное название:</span>
|
||||
<span className="text-block-28">{partsIndexData.originalName}</span>
|
||||
</div>
|
||||
)}
|
||||
{partsIndexData?.description && (
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">Описание:</span>
|
||||
<span className="text-block-28" style={{ maxWidth: '400px', wordWrap: 'break-word' }}>
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="text-block-28">{partsIndexData.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Характеристики из нашей базы данных */}
|
||||
{renderInternalCharacteristics()}
|
||||
|
||||
{/* Дополнительные характеристики из Parts Index */}
|
||||
{renderPartsIndexCharacteristics()}
|
||||
{partsIndexLoading ? (
|
||||
<div className="w-layout-vflex flex-block-53">
|
||||
<div className="w-layout-hflex flex-block-55">
|
||||
<span className="text-block-29">Загрузка характеристик...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
renderPartsIndexParameters()
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Изображения из нашей базы данных */}
|
||||
{renderInternalImages()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
const ProfileHistoryMain = () => {
|
||||
const [search, setSearch] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("Все");
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState("Все");
|
||||
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||
@ -105,6 +106,14 @@ const ProfileHistoryMain = () => {
|
||||
useEffect(() => {
|
||||
let filtered = [...getFilteredByTime(historyItems, activeTab)];
|
||||
|
||||
// Фильтрация по производителю
|
||||
if (selectedManufacturer !== "Все") {
|
||||
filtered = filtered.filter(item =>
|
||||
item.brand === selectedManufacturer ||
|
||||
item.vehicleInfo?.brand === selectedManufacturer
|
||||
);
|
||||
}
|
||||
|
||||
// Поиск
|
||||
if (search.trim()) {
|
||||
const searchLower = search.toLowerCase();
|
||||
@ -152,7 +161,7 @@ const ProfileHistoryMain = () => {
|
||||
}
|
||||
|
||||
setFilteredItems(filtered);
|
||||
}, [historyItems, search, activeTab, sortField, sortOrder]);
|
||||
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
||||
|
||||
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
||||
if (sortField === field) {
|
||||
@ -272,6 +281,18 @@ const ProfileHistoryMain = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedManufacturer("Все");
|
||||
setSearch("");
|
||||
setActiveTab("Все");
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
)}
|
||||
{historyItems.length === 0 && (
|
||||
<button
|
||||
onClick={handleCreateTestData}
|
||||
@ -297,7 +318,14 @@ const ProfileHistoryMain = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-5 w-full text-lg font-medium leading-tight whitespace-nowrap text-gray-950 max-md:max-w-full">
|
||||
<ProfileHistoryTabs tabs={tabOptions} activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
<ProfileHistoryTabs
|
||||
tabs={tabOptions}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
historyItems={historyItems}
|
||||
selectedManufacturer={selectedManufacturer}
|
||||
onManufacturerChange={setSelectedManufacturer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-5 w-full text-gray-400 max-md:max-w-full flex-1 h-full">
|
||||
@ -421,6 +449,11 @@ const ProfileHistoryMain = () => {
|
||||
{filteredItems.length > 0 && (
|
||||
<div className="mt-4 text-center text-sm text-gray-500">
|
||||
Показано {filteredItems.length} из {historyItems.length} записей
|
||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||
<span className="ml-2 text-blue-600">
|
||||
(применены фильтры)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,22 +1,47 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { PartsSearchHistoryItem } from '@/lib/graphql/search-history';
|
||||
|
||||
interface ProfileHistoryTabsProps {
|
||||
tabs: string[];
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
historyItems: PartsSearchHistoryItem[];
|
||||
selectedManufacturer: string;
|
||||
onManufacturerChange: (manufacturer: string) => void;
|
||||
}
|
||||
|
||||
const manufacturers = ["Все", "VAG", "Toyota", "Ford", "BMW"];
|
||||
|
||||
const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
historyItems,
|
||||
selectedManufacturer,
|
||||
onManufacturerChange,
|
||||
}) => {
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState(manufacturers[0]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Получаем уникальных производителей из истории поиска
|
||||
const getUniqueManufacturers = () => {
|
||||
const manufacturersSet = new Set<string>();
|
||||
|
||||
historyItems.forEach(item => {
|
||||
// Добавляем бренд из поля brand
|
||||
if (item.brand) {
|
||||
manufacturersSet.add(item.brand);
|
||||
}
|
||||
// Добавляем бренд из информации об автомобиле
|
||||
if (item.vehicleInfo?.brand) {
|
||||
manufacturersSet.add(item.vehicleInfo.brand);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueManufacturers = Array.from(manufacturersSet).sort();
|
||||
return ["Все", ...uniqueManufacturers];
|
||||
};
|
||||
|
||||
const manufacturers = getUniqueManufacturers();
|
||||
|
||||
// Закрытие дропдауна при клике вне
|
||||
React.useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@ -34,6 +59,11 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
};
|
||||
}, [isDropdownOpen]);
|
||||
|
||||
const handleManufacturerSelect = (manufacturer: string) => {
|
||||
onManufacturerChange(manufacturer);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-5 w-full max-md:max-w-full">
|
||||
{tabs.map((tab) => (
|
||||
@ -69,20 +99,41 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||
>
|
||||
<span className="truncate">{selectedManufacturer}</span>
|
||||
<span className="ml-2 flex-shrink-0 flex items-center">
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 20 20"><path d="M6 8l4 4 4-4" stroke="#9CA3AF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
className={`transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''}`}
|
||||
>
|
||||
<path d="M6 8l4 4 4-4" stroke="#9CA3AF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{isDropdownOpen && (
|
||||
<ul className="absolute left-0 top-full z-10 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full">
|
||||
{manufacturers.map((option) => (
|
||||
<li
|
||||
key={option}
|
||||
className={`px-6 py-4 cursor-pointer hover:bg-blue-100 ${option === selectedManufacturer ? 'bg-blue-50 font-semibold' : ''}`}
|
||||
onMouseDown={() => { setSelectedManufacturer(option); setIsDropdownOpen(false); }}
|
||||
>
|
||||
{option}
|
||||
<ul className="absolute left-0 top-full z-10 bg-white border-x border-b border-stone-300 rounded-b-lg shadow-lg w-full max-h-60 overflow-y-auto">
|
||||
{manufacturers.length === 0 ? (
|
||||
<li className="px-6 py-4 text-gray-400 text-center">
|
||||
Нет данных
|
||||
</li>
|
||||
))}
|
||||
) : (
|
||||
manufacturers.map((manufacturer) => (
|
||||
<li
|
||||
key={manufacturer}
|
||||
className={`px-6 py-4 cursor-pointer hover:bg-blue-100 transition-colors ${manufacturer === selectedManufacturer ? 'bg-blue-50 font-semibold text-blue-600' : ''}`}
|
||||
onMouseDown={() => handleManufacturerSelect(manufacturer)}
|
||||
>
|
||||
{manufacturer}
|
||||
{manufacturer !== "Все" && (
|
||||
<span className="ml-2 text-xs text-gray-400">
|
||||
({historyItems.filter(item =>
|
||||
item.brand === manufacturer || item.vehicleInfo?.brand === manufacturer
|
||||
).length})
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@ export const useArticleImage = (
|
||||
artId: string | undefined | null,
|
||||
options: UseArticleImageOptions = {}
|
||||
): UseArticleImageReturn => {
|
||||
const { enabled = true, fallbackImage = '' } = options;
|
||||
const { enabled = true, fallbackImage = '/images/image-10.png' } = options;
|
||||
const [imageUrl, setImageUrl] = useState<string>(fallbackImage);
|
||||
|
||||
// Проверяем что artId валидный
|
||||
|
@ -7,7 +7,6 @@ import CartSummary from "@/components/CartSummary";
|
||||
import CartRecommended from "../components/CartRecommended";
|
||||
import CatalogSubscribe from "@/components/CatalogSubscribe";
|
||||
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
|
||||
import CartDebug from "@/components/CartDebug";
|
||||
|
||||
export default function CartPage() {
|
||||
|
||||
@ -39,7 +38,6 @@ export default function CartPage() {
|
||||
</section>
|
||||
<Footer />
|
||||
<MobileMenuBottomSection />
|
||||
<CartDebug />
|
||||
</>
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@ export default function Favorite() {
|
||||
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterValues, setFilterValues] = useState<{[key: string]: any}>({});
|
||||
const [sortBy, setSortBy] = useState<'name' | 'brand' | 'price' | 'date'>('date');
|
||||
const [sortBy, setSortBy] = useState<'name' | 'brand' | 'date'>('date');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
// Создаем динамические фильтры на основе данных избранного
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMutation, ApolloProvider } from "@apollo/client";
|
||||
import { gql } from "@apollo/client";
|
||||
import { apolloClient } from "@/lib/apollo";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const CONFIRM_PAYMENT = gql`
|
||||
mutation ConfirmPayment($orderId: ID!) {
|
||||
@ -51,8 +51,15 @@ function PaymentSuccessContent() {
|
||||
}
|
||||
}).then(() => {
|
||||
console.log('Оплата заказа подтверждена');
|
||||
toast.success('Оплата успешно подтверждена!', {
|
||||
duration: 3000,
|
||||
icon: '✅',
|
||||
});
|
||||
}).catch((error: any) => {
|
||||
console.error('Ошибка подтверждения оплаты:', error);
|
||||
toast.error('Ошибка подтверждения оплаты: ' + error.message, {
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [router.query, confirmPayment]);
|
||||
@ -76,7 +83,7 @@ function PaymentSuccessContent() {
|
||||
<link href="/images/webclip.png" rel="apple-touch-icon" />
|
||||
</Head>
|
||||
|
||||
<Header />
|
||||
|
||||
|
||||
<div className="w-layout-blockcontainer container info w-container">
|
||||
<div className="w-layout-vflex flex-block-9">
|
||||
|
@ -1,4 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import ProfileSidebar from '@/components/ProfileSidebar';
|
||||
@ -11,6 +15,48 @@ import NotificationMane from "@/components/profile/NotificationMane";
|
||||
import Head from "next/head";
|
||||
|
||||
const ProfileActsPage = () => {
|
||||
const router = useRouter();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
|
||||
const { data: clientData, loading: clientLoading } = useQuery(GET_CLIENT_ME, {
|
||||
skip: !isAuthenticated,
|
||||
onCompleted: (data) => {
|
||||
// Проверяем есть ли у клиента юридические лица
|
||||
if (!data?.clientMe?.legalEntities?.length) {
|
||||
// Если нет юридических лиц, перенаправляем на настройки
|
||||
router.push('/profile-settings?tab=legal');
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка загрузки данных клиента:', error);
|
||||
// Если ошибка авторизации, перенаправляем на главную
|
||||
router.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Проверяем авторизацию
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
setIsAuthenticated(true);
|
||||
}, [router]);
|
||||
|
||||
// Показываем загрузку пока проверяем авторизацию и данные
|
||||
if (!isAuthenticated || clientLoading) {
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<div className="flex flex-col justify-center items-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600"></div>
|
||||
<div className="mt-4 text-gray-600">Загрузка...</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<Head>
|
||||
|
@ -1,4 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import CatalogSubscribe from '@/components/CatalogSubscribe';
|
||||
@ -8,9 +12,49 @@ import ProfileRequisitiesMain from '@/components/profile/ProfileRequisitiesMain'
|
||||
import ProfileInfo from '@/components/profile/ProfileInfo';
|
||||
import Head from "next/head";
|
||||
|
||||
|
||||
|
||||
const ProfileRequisitiesPage = () => {
|
||||
const router = useRouter();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
|
||||
const { data: clientData, loading: clientLoading } = useQuery(GET_CLIENT_ME, {
|
||||
skip: !isAuthenticated,
|
||||
onCompleted: (data) => {
|
||||
// Проверяем есть ли у клиента юридические лица
|
||||
if (!data?.clientMe?.legalEntities?.length) {
|
||||
// Если нет юридических лиц, перенаправляем на настройки
|
||||
router.push('/profile-settings?tab=legal');
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка загрузки данных клиента:', error);
|
||||
// Если ошибка авторизации, перенаправляем на главную
|
||||
router.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Проверяем авторизацию
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
setIsAuthenticated(true);
|
||||
}, [router]);
|
||||
|
||||
// Показываем загрузку пока проверяем авторизацию и данные
|
||||
if (!isAuthenticated || clientLoading) {
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<div className="flex flex-col justify-center items-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600"></div>
|
||||
<div className="mt-4 text-gray-600">Загрузка...</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<Head>
|
||||
|
@ -502,6 +502,7 @@ export default function SearchResult() {
|
||||
price={`${offer.price.toLocaleString()} ₽`}
|
||||
delivery={`${offer.deliveryDuration} ${offer.deliveryDuration === 1 ? 'день' : 'дней'}`}
|
||||
stock={`${offer.quantity} шт.`}
|
||||
offer={offer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -105,6 +105,14 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
||||
ssdLength: ssd.length
|
||||
});
|
||||
|
||||
// Создаем базовые параметры URL
|
||||
const urlParams = new URLSearchParams();
|
||||
|
||||
// Добавляем VIN-номер в URL, если он есть
|
||||
if (searchQuery && searchType === 'vin') {
|
||||
urlParams.set('vin', searchQuery);
|
||||
}
|
||||
|
||||
// Если есть SSD, сохраняем его в localStorage для безопасной передачи
|
||||
if (ssd && ssd.trim() !== '') {
|
||||
const vehicleKey = `vehicle_ssd_${catalogCode}_${vehicleId}`;
|
||||
@ -121,25 +129,22 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
||||
|
||||
localStorage.setItem(vehicleKey, ssd);
|
||||
|
||||
// Выбираем URL в зависимости от того, нужно ли пропустить промежуточную страницу
|
||||
const url = skipToCategories
|
||||
? `/vehicle-search/${catalogCode}/${vehicleId}?use_storage=1&ssd_length=${ssd.length}&searchType=categories`
|
||||
: `/vehicle-search/${catalogCode}/${vehicleId}?use_storage=1&ssd_length=${ssd.length}`;
|
||||
|
||||
console.log('🔗 Переходим на URL с localStorage:', url);
|
||||
// Используем replace вместо push для моментального перехода
|
||||
router.replace(url);
|
||||
} else {
|
||||
// Выбираем URL в зависимости от того, нужно ли пропустить промежуточную страницу
|
||||
const url = skipToCategories
|
||||
? `/vehicle-search/${catalogCode}/${vehicleId}?searchType=categories`
|
||||
: `/vehicle-search/${catalogCode}/${vehicleId}`;
|
||||
|
||||
console.log('🔗 Переходим на URL без SSD:', url);
|
||||
// Используем replace вместо push для моментального перехода
|
||||
router.replace(url);
|
||||
urlParams.set('use_storage', '1');
|
||||
urlParams.set('ssd_length', ssd.length.toString());
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
if (skipToCategories) {
|
||||
urlParams.set('searchType', 'categories');
|
||||
}
|
||||
|
||||
// Формируем URL с параметрами
|
||||
const baseUrl = `/vehicle-search/${catalogCode}/${vehicleId}`;
|
||||
const url = urlParams.toString() ? `${baseUrl}?${urlParams.toString()}` : baseUrl;
|
||||
|
||||
console.log('🔗 Переходим на URL:', url);
|
||||
// Используем replace вместо push для моментального перехода
|
||||
router.replace(url);
|
||||
}, [router, searchQuery, searchType]);
|
||||
|
||||
// Предзагрузка и автоматический переход при поиске по VIN, если найден только один автомобиль
|
||||
useEffect(() => {
|
||||
|
Reference in New Issue
Block a user