Compare commits
4 Commits
pravki29
...
cartandfav
Author | SHA1 | Date | |
---|---|---|---|
e8f1fecb47 | |||
7f91da525f | |||
d268bb3359 | |||
936a08aa11 |
@ -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 React, { useState } from "react";
|
||||||
|
import { useCart } from "@/contexts/CartContext";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
interface BestPriceCardProps {
|
interface BestPriceCardProps {
|
||||||
bestOfferType: string;
|
bestOfferType: string;
|
||||||
@ -7,9 +9,20 @@ interface BestPriceCardProps {
|
|||||||
price: string;
|
price: string;
|
||||||
delivery: string;
|
delivery: string;
|
||||||
stock: 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 в число, если возможно
|
// Парсим stock в число, если возможно
|
||||||
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
||||||
const maxCount = isNaN(parsedStock) ? undefined : parsedStock;
|
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);
|
let value = parseInt(e.target.value, 10);
|
||||||
if (isNaN(value) || value < 1) value = 1;
|
if (isNaN(value) || value < 1) value = 1;
|
||||||
if (maxCount !== undefined && value > maxCount) {
|
if (maxCount !== undefined && value > maxCount) {
|
||||||
window.alert(`Максимум ${maxCount} шт.`);
|
toast.error(`Максимум ${maxCount} шт.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCount(value);
|
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 (
|
return (
|
||||||
<div className="w-layout-vflex flex-block-44">
|
<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>
|
<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>
|
</div>
|
||||||
<div className="w-layout-hflex flex-block-42">
|
<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="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 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>
|
</div>
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ const CartInfo: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<section className="section-info">
|
||||||
<div className="w-layout-blockcontainer container info w-container">
|
<div className="w-layout-blockcontainer container info w-container">
|
||||||
<div className="w-layout-vflex flex-block-9">
|
<div className="w-layout-vflex flex-block-9">
|
||||||
<div className="w-layout-hflex flex-block-7">
|
<div className="w-layout-hflex flex-block-7">
|
||||||
@ -44,6 +45,7 @@ const CartInfo: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,14 +80,49 @@ const CartItem: React.FC<CartItemProps> = ({
|
|||||||
<div className="text-block-21-copy-copy">{deliveryDate}</div>
|
<div className="text-block-21-copy-copy">{deliveryDate}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex pcs-cart-s1">
|
<div className="w-layout-hflex pcs-cart-s1">
|
||||||
<div className="minus-plus" onClick={() => onCountChange && onCountChange(count - 1)} style={{ cursor: 'pointer' }}>
|
<div
|
||||||
<img loading="lazy" src="/images/minus_icon.svg" alt="-" />
|
className="minus-plus"
|
||||||
|
onClick={() => onCountChange && onCountChange(count - 1)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
aria-label="Уменьшить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div className="pluspcs w-embed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="input-pcs">
|
<div className="input-pcs">
|
||||||
<div className="text-block-26">{count}</div>
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={count}
|
||||||
|
onChange={e => {
|
||||||
|
const value = Math.max(1, parseInt(e.target.value, 10) || 1);
|
||||||
|
onCountChange && onCountChange(value);
|
||||||
|
}}
|
||||||
|
className="text-block-26 w-full text-center outline-none"
|
||||||
|
aria-label="Количество"
|
||||||
|
style={{ width: 40 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="minus-plus" onClick={() => onCountChange && onCountChange(count + 1)} style={{ cursor: 'pointer' }}>
|
<div
|
||||||
<img loading="lazy" src="/images/plus_icon.svg" alt="+" />
|
className="minus-plus"
|
||||||
|
onClick={() => onCountChange && onCountChange(count + 1)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
aria-label="Увеличить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count + 1)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div className="pluspcs w-embed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||||
@ -100,7 +135,31 @@ const CartItem: React.FC<CartItemProps> = ({
|
|||||||
<path d="M9 16.5L7.84 15.4929C3.72 11.93 1 9.57248 1 6.69619C1 4.33869 2.936 2.5 5.4 2.5C6.792 2.5 8.128 3.11798 9 4.08692C9.872 3.11798 11.208 2.5 12.6 2.5C15.064 2.5 17 4.33869 17 6.69619C17 9.57248 14.28 11.93 10.16 15.4929L9 16.5Z" fill={favorite ? "#e53935" : "currentColor"} />
|
<path d="M9 16.5L7.84 15.4929C3.72 11.93 1 9.57248 1 6.69619C1 4.33869 2.936 2.5 5.4 2.5C6.792 2.5 8.128 3.11798 9 4.08692C9.872 3.11798 11.208 2.5 12.6 2.5C15.064 2.5 17 4.33869 17 6.69619C17 9.57248 14.28 11.93 10.16 15.4929L9 16.5Z" fill={favorite ? "#e53935" : "currentColor"} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<img src="/images/delete.svg" loading="lazy" alt="" className="image-13" style={{ cursor: 'pointer' }} onClick={onRemove} />
|
<div
|
||||||
|
className="bdel"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Удалить из корзины"
|
||||||
|
onClick={onRemove}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onRemove && onRemove()}
|
||||||
|
style={{ display: 'inline-flex', cursor: 'pointer', transition: 'color 0.2s' }}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
const path = e.currentTarget.querySelector('path');
|
||||||
|
if (path) path.setAttribute('fill', '#ec1c24');
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
const path = e.currentTarget.querySelector('path');
|
||||||
|
if (path) path.setAttribute('fill', '#D0D0D0');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M4.625 17.5C4.14375 17.5 3.73192 17.3261 3.3895 16.9782C3.04708 16.6304 2.87558 16.2117 2.875 15.7222V4.16667H2V2.38889H6.375V1.5H11.625V2.38889H16V4.16667H15.125V15.7222C15.125 16.2111 14.9538 16.6298 14.6114 16.9782C14.269 17.3267 13.8568 17.5006 13.375 17.5H4.625ZM6.375 13.9444H8.125V5.94444H6.375V13.9444ZM9.875 13.9444H11.625V5.94444H9.875V13.9444Z"
|
||||||
|
fill="#D0D0D0"
|
||||||
|
style={{ transition: 'fill 0.2s' }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@ import { useFavorites } from "@/contexts/FavoritesContext";
|
|||||||
|
|
||||||
const CartList: React.FC = () => {
|
const CartList: React.FC = () => {
|
||||||
const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart();
|
const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart();
|
||||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||||
const { items } = state;
|
const { items } = state;
|
||||||
|
|
||||||
const allSelected = items.length > 0 && items.every((item) => item.selected);
|
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);
|
const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand);
|
||||||
|
|
||||||
if (isInFavorites) {
|
if (isInFavorites) {
|
||||||
// Удаляем из избранного
|
// Находим товар в избранном по правильному ID
|
||||||
const favoriteId = `${item.productId || item.offerKey || ''}:${item.article}:${item.brand}`;
|
const favoriteItem = favorites.find((fav: any) => {
|
||||||
removeFromFavorites(favoriteId);
|
// Проверяем по разным комбинациям идентификаторов
|
||||||
|
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 {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
addToFavorites({
|
addToFavorites({
|
||||||
@ -81,9 +90,24 @@ const CartList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-block-30">Выделить всё</div>
|
<div className="text-block-30">Выделить всё</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex select-all-block" onClick={handleRemoveSelected} style={{ cursor: 'pointer' }}>
|
<div className="w-layout-hflex select-all-block" onClick={handleRemoveSelected} style={{ cursor: 'pointer' }}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
const path = (e.currentTarget.querySelector('path'));
|
||||||
|
if (path) path.setAttribute('fill', '#ec1c24');
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
const path = (e.currentTarget.querySelector('path'));
|
||||||
|
if (path) path.setAttribute('fill', '#D0D0D0');
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="text-block-30">Удалить выбранные</div>
|
<div className="text-block-30">Удалить выбранные</div>
|
||||||
<img src="/images/delete.svg" loading="lazy" alt="" className="image-13" />
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg" className="image-13">
|
||||||
|
<path
|
||||||
|
d="M4.625 17.5C4.14375 17.5 3.73192 17.3261 3.3895 16.9782C3.04708 16.6304 2.87558 16.2117 2.875 15.7222V4.16667H2V2.38889H6.375V1.5H11.625V2.38889H16V4.16667H15.125V15.7222C15.125 16.2111 14.9538 16.6298 14.6114 16.9782C14.269 17.3267 13.8568 17.5006 13.375 17.5H4.625ZM6.375 13.9444H8.125V5.94444H6.375V13.9444ZM9.875 13.9444H11.625V5.94444H9.875V13.9444Z"
|
||||||
|
fill="#D0D0D0"
|
||||||
|
style={{ transition: 'fill 0.2s' }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
|
@ -1,88 +1,79 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
|
import { useCart } from "@/contexts/CartContext";
|
||||||
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: "",
|
|
||||||
},
|
|
||||||
// ...ещё товары
|
|
||||||
];
|
|
||||||
|
|
||||||
const CartList2: React.FC = () => {
|
const CartList2: React.FC = () => {
|
||||||
const [items, setItems] = useState(initialItems);
|
const { state, updateComment } = useCart();
|
||||||
|
const { items } = state;
|
||||||
|
|
||||||
const handleComment = (id: number, comment: string) => {
|
const handleComment = (id: string, comment: string) => {
|
||||||
setItems((prev) => prev.map((item) => item.id === id ? { ...item, comment } : item));
|
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 (
|
return (
|
||||||
<div className="w-layout-vflex flex-block-48">
|
<div className="w-layout-vflex flex-block-48">
|
||||||
<div className="w-layout-vflex product-list-cart-check">
|
<div className="w-layout-vflex product-list-cart-check">
|
||||||
{items.map((item) => (
|
{selectedItems.length === 0 ? (
|
||||||
<div className="div-block-21-copy" key={item.id}>
|
<div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
|
||||||
<div className="w-layout-hflex cart-item-check">
|
<p>Не выбрано товаров для заказа</p>
|
||||||
<div className="w-layout-hflex info-block-search">
|
<p>Вернитесь на предыдущий шаг и выберите товары</p>
|
||||||
<div className="text-block-35">{item.count}</div>
|
</div>
|
||||||
<div className="w-layout-hflex block-name">
|
) : (
|
||||||
<h4 className="heading-9-copy">{item.name}</h4>
|
selectedItems.map((item) => (
|
||||||
<div className="text-block-21-copy">{item.description}</div>
|
<div className="div-block-21-copy" key={item.id}>
|
||||||
</div>
|
<div className="w-layout-hflex cart-item-check">
|
||||||
<div className="form-block-copy w-form">
|
<div className="w-layout-hflex info-block-search">
|
||||||
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
<div className="text-block-35">{item.quantity}</div>
|
||||||
<input
|
<div className="w-layout-hflex block-name">
|
||||||
className="text-field-copy w-input"
|
<h4 className="heading-9-copy">{item.name}</h4>
|
||||||
maxLength={256}
|
<div className="text-block-21-copy">{item.description}</div>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="error-message w-form-fail">
|
<div className="form-block-copy w-form">
|
||||||
<div>Oops! Something went wrong while submitting the form.</div>
|
<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>
|
||||||
</div>
|
<div className="w-layout-hflex add-to-cart-block">
|
||||||
<div className="w-layout-hflex add-to-cart-block">
|
<div className="w-layout-hflex flex-block-39-copy">
|
||||||
<div className="w-layout-hflex flex-block-39-copy">
|
<h4 className="heading-9-copy">{item.deliveryTime || 'Уточняется'}</h4>
|
||||||
<h4 className="heading-9-copy">{item.delivery}</h4>
|
<div className="text-block-21-copy">{item.deliveryDate || ''}</div>
|
||||||
<div className="text-block-21-copy">{item.deliveryDate}</div>
|
</div>
|
||||||
</div>
|
<div className="w-layout-hflex pcs">
|
||||||
<div className="w-layout-hflex pcs">
|
<div className="pcs-text">{item.quantity} шт.</div>
|
||||||
<div className="pcs-text">{item.count} шт.</div>
|
</div>
|
||||||
</div>
|
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
<h4 className="heading-9-copy-copy">{formatPrice(item.price * item.quantity, item.currency)}</h4>
|
||||||
<h4 className="heading-9-copy-copy">{item.price}</h4>
|
<div className="text-block-21-copy-copy">{formatPrice(item.price, item.currency)}/шт</div>
|
||||||
<div className="text-block-21-copy-copy">{item.pricePerItem}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 Link from "next/link";
|
||||||
import { useCart } from "@/contexts/CartContext";
|
import { useCart } from "@/contexts/CartContext";
|
||||||
import { useMutation, useQuery } from "@apollo/client";
|
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 CartSummary: React.FC = () => {
|
||||||
const { state, updateDelivery, updateOrderComment, clearCart } = useCart();
|
const { state, updateDelivery, updateOrderComment, clearCart } = useCart();
|
||||||
@ -15,7 +16,7 @@ const CartSummary: React.FC = () => {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [showAuthWarning, setShowAuthWarning] = useState(false);
|
const [showAuthWarning, setShowAuthWarning] = useState(false);
|
||||||
const [currentStep, setCurrentStep] = useState(1); // 1 - первый шаг, 2 - второй шаг
|
const [step, setStep] = useState(1);
|
||||||
|
|
||||||
// Новые состояния для первого шага
|
// Новые состояния для первого шага
|
||||||
const [selectedLegalEntity, setSelectedLegalEntity] = useState<string>("");
|
const [selectedLegalEntity, setSelectedLegalEntity] = useState<string>("");
|
||||||
@ -32,22 +33,17 @@ const CartSummary: React.FC = () => {
|
|||||||
const [paymentMethod, setPaymentMethod] = useState<string>("yookassa");
|
const [paymentMethod, setPaymentMethod] = useState<string>("yookassa");
|
||||||
const [showPaymentDropdown, setShowPaymentDropdown] = useState(false);
|
const [showPaymentDropdown, setShowPaymentDropdown] = useState(false);
|
||||||
|
|
||||||
// Состояния для офферов доставки
|
// Упрощенный тип доставки - только курьер или самовывоз
|
||||||
const [deliveryOffers, setDeliveryOffers] = useState<any[]>([]);
|
// const [deliveryType, setDeliveryType] = useState<'courier' | 'pickup'>('courier');
|
||||||
const [selectedDeliveryOffer, setSelectedDeliveryOffer] = useState<any>(null);
|
|
||||||
const [loadingOffers, setLoadingOffers] = useState(false);
|
|
||||||
const [offersError, setOffersError] = useState<string>("");
|
|
||||||
|
|
||||||
const [createOrder] = useMutation(CREATE_ORDER);
|
const [createOrder] = useMutation(CREATE_ORDER);
|
||||||
const [createPayment] = useMutation(CREATE_PAYMENT);
|
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: clientData, loading: clientLoading } = useQuery(GET_CLIENT_ME);
|
||||||
const { data: addressesData, loading: addressesLoading } = useQuery(GET_CLIENT_DELIVERY_ADDRESSES);
|
const { data: addressesData, loading: addressesLoading } = useQuery(GET_CLIENT_DELIVERY_ADDRESSES);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Получаем пользователя из localStorage для проверки авторизации
|
// Получаем пользователя из localStorage для проверки авторизации
|
||||||
const [userData, setUserData] = useState<any>(null);
|
const [userData, setUserData] = useState<any>(null);
|
||||||
|
|
||||||
@ -67,7 +63,7 @@ const CartSummary: React.FC = () => {
|
|||||||
if (savedCartSummaryState) {
|
if (savedCartSummaryState) {
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(savedCartSummaryState);
|
const state = JSON.parse(savedCartSummaryState);
|
||||||
setCurrentStep(state.currentStep || 1);
|
setStep(state.step || 1);
|
||||||
setSelectedLegalEntity(state.selectedLegalEntity || '');
|
setSelectedLegalEntity(state.selectedLegalEntity || '');
|
||||||
setSelectedLegalEntityId(state.selectedLegalEntityId || '');
|
setSelectedLegalEntityId(state.selectedLegalEntityId || '');
|
||||||
setIsIndividual(state.isIndividual ?? true);
|
setIsIndividual(state.isIndividual ?? true);
|
||||||
@ -87,7 +83,7 @@ const CartSummary: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const stateToSave = {
|
const stateToSave = {
|
||||||
currentStep,
|
step,
|
||||||
selectedLegalEntity,
|
selectedLegalEntity,
|
||||||
selectedLegalEntityId,
|
selectedLegalEntityId,
|
||||||
isIndividual,
|
isIndividual,
|
||||||
@ -99,7 +95,7 @@ const CartSummary: React.FC = () => {
|
|||||||
};
|
};
|
||||||
localStorage.setItem('cartSummaryState', JSON.stringify(stateToSave));
|
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(() => {
|
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 = () => {
|
const handleProceedToStep2 = () => {
|
||||||
if (!selectedDeliveryAddress) {
|
if (!recipientName.trim()) {
|
||||||
setError("Пожалуйста, выберите адрес доставки.");
|
toast.error('Пожалуйста, введите имя получателя');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summary.totalItems === 0) {
|
if (!recipientPhone.trim()) {
|
||||||
setError("Корзина пуста. Добавьте товары для оформления заказа.");
|
toast.error('Пожалуйста, введите телефон получателя');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedDeliveryAddress.trim()) {
|
||||||
|
toast.error('Пожалуйста, выберите адрес доставки');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedDeliveryOffer) {
|
// Обновляем данные доставки без стоимости
|
||||||
setError("Пожалуйста, выберите способ доставки.");
|
updateDelivery({
|
||||||
return;
|
address: selectedDeliveryAddress,
|
||||||
}
|
cost: 0, // Стоимость включена в товары
|
||||||
|
date: 'Включена в стоимость товаров',
|
||||||
|
time: 'Способ доставки указан в адресе'
|
||||||
|
});
|
||||||
|
|
||||||
// Проверяем достаточность средств для оплаты с баланса
|
setStep(2);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToStep1 = () => {
|
const handleBackToStep1 = () => {
|
||||||
setCurrentStep(1);
|
setStep(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@ -339,7 +194,7 @@ const CartSummary: React.FC = () => {
|
|||||||
deliveryAddress: selectedDeliveryAddress || delivery.address,
|
deliveryAddress: selectedDeliveryAddress || delivery.address,
|
||||||
legalEntityId: !isIndividual ? selectedLegalEntityId : null,
|
legalEntityId: !isIndividual ? selectedLegalEntityId : null,
|
||||||
paymentMethod: paymentMethod,
|
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 => ({
|
items: selectedItems.map(item => ({
|
||||||
productId: item.productId,
|
productId: item.productId,
|
||||||
externalId: item.offerKey,
|
externalId: item.offerKey,
|
||||||
@ -367,14 +222,6 @@ const CartSummary: React.FC = () => {
|
|||||||
localStorage.removeItem('cartSummaryState');
|
localStorage.removeItem('cartSummaryState');
|
||||||
}
|
}
|
||||||
window.location.href = `/payment/success?orderId=${order.id}&orderNumber=${order.orderNumber}&paymentMethod=balance`;
|
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 {
|
} else {
|
||||||
// Для ЮКассы - создаем платеж и переходим на оплату
|
// Для ЮКассы - создаем платеж и переходим на оплату
|
||||||
const paymentResult = await createPayment({
|
const paymentResult = await createPayment({
|
||||||
@ -421,14 +268,12 @@ const CartSummary: React.FC = () => {
|
|||||||
return 'ЮКасса (банковские карты)';
|
return 'ЮКасса (банковские карты)';
|
||||||
case 'balance':
|
case 'balance':
|
||||||
return 'Оплата с баланса';
|
return 'Оплата с баланса';
|
||||||
case 'invoice':
|
|
||||||
return 'Оплата по реквизитам';
|
|
||||||
default:
|
default:
|
||||||
return 'Выберите способ оплаты';
|
return 'ЮКасса (банковские карты)';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentStep === 1) {
|
if (step === 1) {
|
||||||
// Первый шаг - настройка доставки
|
// Первый шаг - настройка доставки
|
||||||
return (
|
return (
|
||||||
<div className="w-layout-vflex cart-ditail">
|
<div className="w-layout-vflex cart-ditail">
|
||||||
@ -650,107 +495,6 @@ const CartSummary: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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="w-layout-vflex flex-block-58" style={{ position: 'relative' }} ref={paymentDropdownRef}>
|
||||||
<div className="text-block-31">Способ оплаты</div>
|
<div className="text-block-31">Способ оплаты</div>
|
||||||
@ -856,24 +600,19 @@ const CartSummary: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contracts = clientData?.clientMe?.contracts || [];
|
const activeContracts = clientData?.clientMe?.contracts?.filter((contract: any) => contract.isActive) || [];
|
||||||
const defaultContract = contracts.find((contract: any) => contract.isDefault && contract.isActive);
|
const defaultContract = activeContracts.find((contract: any) => contract.isDefault) || activeContracts[0];
|
||||||
|
|
||||||
if (!defaultContract) {
|
if (!defaultContract) {
|
||||||
const anyActiveContract = contracts.find((contract: any) => contract.isActive);
|
return (
|
||||||
|
<span style={{ color: '#EF4444', fontWeight: 500 }}>
|
||||||
if (!anyActiveContract) {
|
Активный договор не найден
|
||||||
return (
|
</span>
|
||||||
<span style={{ fontWeight: 500, color: '#e74c3c' }}>
|
);
|
||||||
Нет активных контрактов
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const contract = defaultContract || contracts.find((contract: any) => contract.isActive);
|
const balance = defaultContract.balance || 0;
|
||||||
const balance = contract?.balance || 0;
|
const creditLimit = defaultContract.creditLimit || 0;
|
||||||
const creditLimit = contract?.creditLimit || 0;
|
|
||||||
const totalAvailable = balance + creditLimit;
|
const totalAvailable = balance + creditLimit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -884,59 +623,10 @@ const CartSummary: React.FC = () => {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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>
|
||||||
|
|
||||||
<div className="px-line"></div>
|
<div className="px-line"></div>
|
||||||
@ -958,10 +648,7 @@ const CartSummary: React.FC = () => {
|
|||||||
<div className="w-layout-hflex flex-block-59">
|
<div className="w-layout-hflex flex-block-59">
|
||||||
<div className="text-block-21-copy-copy">Доставка</div>
|
<div className="text-block-21-copy-copy">Доставка</div>
|
||||||
<div className="text-block-33">
|
<div className="text-block-33">
|
||||||
{selectedDeliveryOffer?.cost === 0
|
Включена в стоимость товаров
|
||||||
? 'Бесплатно'
|
|
||||||
: formatPrice(selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -972,7 +659,7 @@ const CartSummary: React.FC = () => {
|
|||||||
<div className="text-block-32">Итого</div>
|
<div className="text-block-32">Итого</div>
|
||||||
<h4 className="heading-9-copy-copy">
|
<h4 className="heading-9-copy-copy">
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
summary.totalPrice - summary.totalDiscount + (selectedDeliveryAddress ? 0 : summary.deliveryPrice)
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -980,10 +667,10 @@ const CartSummary: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
className="submit-button fill w-button"
|
className="submit-button fill w-button"
|
||||||
onClick={handleProceedToStep2}
|
onClick={handleProceedToStep2}
|
||||||
disabled={summary.totalItems === 0}
|
disabled={summary.totalItems === 0 || !consent}
|
||||||
style={{
|
style={{
|
||||||
opacity: summary.totalItems === 0 ? 0.5 : 1,
|
opacity: summary.totalItems === 0 || !consent ? 0.5 : 1,
|
||||||
cursor: summary.totalItems === 0 ? 'not-allowed' : 'pointer'
|
cursor: summary.totalItems === 0 || !consent ? 'not-allowed' : 'pointer'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Оформить заказ
|
Оформить заказ
|
||||||
@ -1035,10 +722,9 @@ const CartSummary: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
border: '1px solid #D0D0D0',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
outline: 'none',
|
||||||
fontSize: '14px',
|
boxShadow: 'none',
|
||||||
fontFamily: 'inherit'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1051,10 +737,9 @@ const CartSummary: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
border: '1px solid #D0D0D0',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
outline: 'none',
|
||||||
fontSize: '14px',
|
boxShadow: 'none',
|
||||||
fontFamily: 'inherit'
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1075,9 +760,19 @@ const CartSummary: React.FC = () => {
|
|||||||
{paymentMethod === 'balance' && !isIndividual && (
|
{paymentMethod === 'balance' && !isIndividual && (
|
||||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const defaultContract = clientData?.clientMe?.contracts?.find((contract: any) => contract.isDefault && contract.isActive);
|
const activeContracts = clientData?.clientMe?.contracts?.filter((contract: any) => contract.isActive) || [];
|
||||||
const balance = defaultContract?.balance || 0;
|
const defaultContract = activeContracts.find((contract: any) => contract.isDefault) || activeContracts[0];
|
||||||
const creditLimit = defaultContract?.creditLimit || 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;
|
const totalAvailable = balance + creditLimit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1131,10 +826,7 @@ const CartSummary: React.FC = () => {
|
|||||||
<div className="w-layout-hflex flex-block-59">
|
<div className="w-layout-hflex flex-block-59">
|
||||||
<div className="text-block-21-copy-copy">Доставка</div>
|
<div className="text-block-21-copy-copy">Доставка</div>
|
||||||
<div className="text-block-33">
|
<div className="text-block-33">
|
||||||
{selectedDeliveryOffer?.cost === 0
|
Включена в стоимость товаров
|
||||||
? 'Бесплатно'
|
|
||||||
: formatPrice(selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1145,7 +837,7 @@ const CartSummary: React.FC = () => {
|
|||||||
<div className="text-block-32">Итого</div>
|
<div className="text-block-32">Итого</div>
|
||||||
<h4 className="heading-9-copy-copy">
|
<h4 className="heading-9-copy-copy">
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
summary.totalPrice - summary.totalDiscount + (selectedDeliveryOffer?.cost || summary.deliveryPrice)
|
summary.totalPrice - summary.totalDiscount + (selectedDeliveryAddress ? 0 : summary.deliveryPrice)
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@ -1188,22 +880,21 @@ const CartSummary: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
className="submit-button fill w-button"
|
className="submit-button fill w-button"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim()}
|
disabled={summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim() || !consent}
|
||||||
style={{
|
style={{
|
||||||
opacity: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim()) ? 0.5 : 1,
|
opacity: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim() || !consent) ? 0.5 : 1,
|
||||||
cursor: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim()) ? 'not-allowed' : 'pointer'
|
cursor: (summary.totalItems === 0 || isProcessing || !recipientName.trim() || !recipientPhone.trim() || !consent) ? 'not-allowed' : 'pointer'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isProcessing ? 'Оформляем заказ...' :
|
{isProcessing ? 'Оформляем заказ...' :
|
||||||
paymentMethod === 'balance' ? 'Оплатить с баланса' :
|
paymentMethod === 'balance' ? 'Оплатить с баланса' :
|
||||||
paymentMethod === 'invoice' ? 'Выставить счёт' :
|
|
||||||
'Оплатить'}
|
'Оплатить'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{error && <div style={{ color: 'red', marginTop: 10 }}>{error}</div>}
|
{error && <div style={{ color: 'red', marginTop: 10 }}>{error}</div>}
|
||||||
|
|
||||||
{/* Кнопка "Назад" */}
|
{/* Кнопка "Назад" */}
|
||||||
<button
|
{/* <button
|
||||||
onClick={handleBackToStep1}
|
onClick={handleBackToStep1}
|
||||||
style={{
|
style={{
|
||||||
background: 'none',
|
background: 'none',
|
||||||
@ -1217,7 +908,7 @@ const CartSummary: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
← Назад к настройкам доставки
|
← Назад к настройкам доставки
|
||||||
</button>
|
</button> */}
|
||||||
|
|
||||||
<div className="w-layout-hflex privacy-consent" style={{ cursor: 'pointer' }} onClick={() => setConsent((v) => !v)}>
|
<div className="w-layout-hflex privacy-consent" style={{ cursor: 'pointer' }} onClick={() => setConsent((v) => !v)}>
|
||||||
<div
|
<div
|
||||||
|
@ -35,7 +35,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
|
|||||||
priceElement,
|
priceElement,
|
||||||
onAddToCart,
|
onAddToCart,
|
||||||
}) => {
|
}) => {
|
||||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||||
|
|
||||||
// Обрабатываем пустое изображение - используем SVG-заглушку вместо мокап-фотки
|
// Обрабатываем пустое изображение - используем SVG-заглушку вместо мокап-фотки
|
||||||
const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=';
|
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;
|
const numericPrice = parseFloat(price.replace(/[^\d.,]/g, '').replace(',', '.')) || 0;
|
||||||
|
|
||||||
if (isItemFavorite) {
|
if (isItemFavorite) {
|
||||||
// Создаем ID для удаления
|
// Находим товар в избранном по правильному ID
|
||||||
const id = `${productId || offerKey || ''}:${articleNumber}:${brandName || brand}`;
|
const favoriteItem = favorites.find((fav: any) => {
|
||||||
removeFromFavorites(id);
|
// Проверяем по разным комбинациям идентификаторов
|
||||||
|
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 {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
addToFavorites({
|
addToFavorites({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useCart } from "@/contexts/CartContext";
|
import { useCart } from "@/contexts/CartContext";
|
||||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const INITIAL_OFFERS_LIMIT = 5;
|
const INITIAL_OFFERS_LIMIT = 5;
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
partsIndexPowered = false
|
partsIndexPowered = false
|
||||||
}) => {
|
}) => {
|
||||||
const { addItem } = useCart();
|
const { addItem } = useCart();
|
||||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||||
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_LIMIT);
|
||||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||||
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
||||||
@ -88,7 +89,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
let num = parseInt(value, 10);
|
let num = parseInt(value, 10);
|
||||||
if (isNaN(num) || num < 1) num = 1;
|
if (isNaN(num) || num < 1) num = 1;
|
||||||
if (num > availableStock) {
|
if (num > availableStock) {
|
||||||
window.alert(`Максимум ${availableStock} шт.`);
|
toast.error(`Максимум ${availableStock} шт.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setQuantities(prev => ({ ...prev, [index]: num }));
|
setQuantities(prev => ({ ...prev, [index]: num }));
|
||||||
@ -100,7 +101,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
|
|
||||||
// Проверяем наличие
|
// Проверяем наличие
|
||||||
if (quantity > availableStock) {
|
if (quantity > availableStock) {
|
||||||
alert(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
toast.error(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,8 +124,17 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
image: image,
|
image: image,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Показываем уведомление о добавлении
|
// Показываем тоастер вместо alert
|
||||||
alert(`Товар "${brand} ${article}" добавлен в корзину (${quantity} шт.)`);
|
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();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isItemFavorite) {
|
if (isItemFavorite) {
|
||||||
// Создаем ID для удаления
|
// Находим товар в избранном и удаляем по правильному ID
|
||||||
const id = `${offers[0]?.productId || offers[0]?.offerKey || ''}:${article}:${brand}`;
|
const favoriteItem = favorites.find((item: any) => {
|
||||||
removeFromFavorites(id);
|
// Проверяем по разным комбинациям идентификаторов
|
||||||
|
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 {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
const bestOffer = offers[0]; // Берем первое предложение как лучшее
|
const bestOffer = offers[0]; // Берем первое предложение как лучшее
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import Filters, { FilterConfig } from "./Filters";
|
import Filters, { FilterConfig } from "./Filters";
|
||||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||||
|
|
||||||
@ -8,9 +9,9 @@ interface FavoriteListProps {
|
|||||||
onFilterChange?: (type: string, value: any) => void;
|
onFilterChange?: (type: string, value: any) => void;
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
onSearchChange?: (value: string) => void;
|
onSearchChange?: (value: string) => void;
|
||||||
sortBy?: 'name' | 'brand' | 'price' | 'date';
|
sortBy?: 'name' | 'brand' | 'date';
|
||||||
sortOrder?: 'asc' | 'desc';
|
sortOrder?: 'asc' | 'desc';
|
||||||
onSortChange?: (sortBy: 'name' | 'brand' | 'price' | 'date') => void;
|
onSortChange?: (sortBy: 'name' | 'brand' | 'date') => void;
|
||||||
onSortOrderChange?: (sortOrder: 'asc' | 'desc') => void;
|
onSortOrderChange?: (sortOrder: 'asc' | 'desc') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
|||||||
onSortChange,
|
onSortChange,
|
||||||
onSortOrderChange
|
onSortOrderChange
|
||||||
}) => {
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
const { favorites, removeFromFavorites, clearFavorites } = useFavorites();
|
const { favorites, removeFromFavorites, clearFavorites } = useFavorites();
|
||||||
|
|
||||||
const handleRemove = (id: string) => {
|
const handleRemove = (id: string) => {
|
||||||
@ -35,6 +37,14 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
|||||||
clearFavorites();
|
clearFavorites();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функция для поиска товара
|
||||||
|
const handleSearchItem = (item: any) => {
|
||||||
|
// Формируем поисковый запрос из бренда и артикула
|
||||||
|
const searchQuery = `${item.brand} ${item.article}`;
|
||||||
|
// Переходим на страницу поиска с результатами
|
||||||
|
router.push(`/search-result?article=${encodeURIComponent(item.article)}&brand=${encodeURIComponent(item.brand)}`);
|
||||||
|
};
|
||||||
|
|
||||||
// Состояние для hover на иконке удаления всех
|
// Состояние для hover на иконке удаления всех
|
||||||
const [removeAllHover, setRemoveAllHover] = useState(false);
|
const [removeAllHover, setRemoveAllHover] = useState(false);
|
||||||
// Состояние для hover на корзине отдельного товара
|
// Состояние для hover на корзине отдельного товара
|
||||||
@ -82,11 +92,6 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
|||||||
case 'brand':
|
case 'brand':
|
||||||
comparison = a.brand.localeCompare(b.brand);
|
comparison = a.brand.localeCompare(b.brand);
|
||||||
break;
|
break;
|
||||||
case 'price':
|
|
||||||
const priceA = a.price || 0;
|
|
||||||
const priceB = b.price || 0;
|
|
||||||
comparison = priceA - priceB;
|
|
||||||
break;
|
|
||||||
case 'date':
|
case 'date':
|
||||||
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
break;
|
break;
|
||||||
@ -97,29 +102,19 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
|||||||
return sortOrder === 'asc' ? comparison : -comparison;
|
return sortOrder === 'asc' ? comparison : -comparison;
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatPrice = (price?: number, currency?: string) => {
|
const handleSortClick = (newSortBy: 'name' | 'brand' | 'date') => {
|
||||||
if (!price) {
|
|
||||||
return 'Цена не указана';
|
|
||||||
}
|
|
||||||
if (currency === 'RUB') {
|
|
||||||
return `от ${price.toLocaleString('ru-RU')} ₽`;
|
|
||||||
}
|
|
||||||
return `от ${price} ${currency || ''}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSortClick = (newSortBy: 'name' | 'brand' | 'price' | 'date') => {
|
|
||||||
if (sortBy === newSortBy) {
|
if (sortBy === newSortBy) {
|
||||||
// Если тот же столбец, меняем порядок
|
// Если тот же столбец, меняем порядок
|
||||||
onSortOrderChange?.(sortOrder === 'asc' ? 'desc' : 'asc');
|
onSortOrderChange?.(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||||
} else {
|
} else {
|
||||||
// Если новый столбец, устанавливаем его и порядок по умолчанию
|
// Если новый столбец, устанавливаем его и порядок по умолчанию
|
||||||
onSortChange?.(newSortBy);
|
onSortChange?.(newSortBy);
|
||||||
onSortOrderChange?.(newSortBy === 'price' ? 'asc' : 'desc');
|
onSortOrderChange?.(newSortBy === 'date' ? 'desc' : 'asc');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// SVG-галочки для сортировки — всегда видны у всех колонок
|
// SVG-галочки для сортировки — всегда видны у всех колонок
|
||||||
const getSortIcon = (columnSort: 'name' | 'brand' | 'price' | 'date') => {
|
const getSortIcon = (columnSort: 'name' | 'brand' | 'date') => {
|
||||||
const isActive = sortBy === columnSort;
|
const isActive = sortBy === columnSort;
|
||||||
const isAsc = sortOrder === 'asc';
|
const isAsc = sortOrder === 'asc';
|
||||||
const color = isActive ? 'var(--_button---primary)' : '#94a3b8';
|
const color = isActive ? 'var(--_button---primary)' : '#94a3b8';
|
||||||
@ -176,6 +171,9 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="sort-item-comments">Комментарий</div>
|
<div className="sort-item-comments">Комментарий</div>
|
||||||
</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 && (
|
{favorites.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="w-layout-hflex select-all-block"
|
className="w-layout-hflex select-all-block"
|
||||||
@ -233,12 +231,24 @@ const FavoriteList: React.FC<FavoriteListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-layout-hflex add-to-cart-block-copy">
|
<div className="w-layout-hflex add-to-cart-block-copy">
|
||||||
<h4
|
<button
|
||||||
className="heading-9-copy-copy cursor-pointer hover:text-blue-600 flex items-center gap-1"
|
onClick={() => handleSearchItem(item)}
|
||||||
onClick={() => handleSortClick('price')}
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors duration-200 flex items-center gap-2 text-sm font-medium"
|
||||||
>
|
style={{color: '#fff'}}
|
||||||
{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">
|
<div className="w-layout-hflex control-element-copy">
|
||||||
{/* Корзина с hover-эффектом для удаления товара */}
|
{/* Корзина с hover-эффектом для удаления товара */}
|
||||||
<span
|
<span
|
||||||
|
@ -58,6 +58,15 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
setSearchQuery(q);
|
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 {
|
else {
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
@ -321,11 +330,31 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
|||||||
const catalogCode = (vehicle as any).catalog || vehicle.brand.toLowerCase();
|
const catalogCode = (vehicle as any).catalog || vehicle.brand.toLowerCase();
|
||||||
console.log('🚗 Переход на страницу автомобиля:', { catalogCode, vehicleId: vehicle.vehicleid, ssd: vehicle.ssd });
|
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 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);
|
router.push(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
||||||
|
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ label: 'Заказы', href: '/profile-orders', icon: 'https://cdn.builder.io/api/v1/image/assets/TEMP/22ecd7e6251abe04521d03f0ac09f73018a8c2c8?placeholderIfAbsent=true&apiKey=f5bc5a2dc9b841d0aba1cc6c74a35920' },
|
{ 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 router = useRouter();
|
||||||
const isClient = useIsClient();
|
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 = () => {
|
const handleLogout = () => {
|
||||||
if (isClient) {
|
if (isClient) {
|
||||||
localStorage.removeItem('authToken');
|
localStorage.removeItem('authToken');
|
||||||
@ -66,31 +77,37 @@ const LKMenu = React.forwardRef<HTMLDivElement>((props, ref) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="gap-2.5 self-start px-2.5 pt-2.5 mt-3 whitespace-nowrap text-gray-950">
|
|
||||||
Финансы
|
{/* Раздел "Финансы" показываем только если есть юридические лица */}
|
||||||
</div>
|
{hasLegalEntities && (
|
||||||
<div className="flex flex-col mt-3 w-full text-base leading-snug text-gray-600">
|
<>
|
||||||
{financeItems.map((item) => {
|
<div className="gap-2.5 self-start px-2.5 pt-2.5 mt-3 whitespace-nowrap text-gray-950">
|
||||||
const isActive = normalizePath(router.asPath) === normalizePath(item.href);
|
Финансы
|
||||||
return (
|
</div>
|
||||||
<Link href={item.href} key={item.href}>
|
<div className="flex flex-col mt-3 w-full text-base leading-snug text-gray-600">
|
||||||
<div
|
{financeItems.map((item) => {
|
||||||
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
const isActive = normalizePath(router.asPath) === normalizePath(item.href);
|
||||||
isActive ? 'bg-slate-200' : 'bg-white'
|
return (
|
||||||
}`}
|
<Link href={item.href} key={item.href}>
|
||||||
>
|
<div
|
||||||
<img
|
className={`flex gap-2.5 items-center px-2.5 py-2 w-full whitespace-nowrap rounded-lg ${
|
||||||
loading="lazy"
|
isActive ? 'bg-slate-200' : 'bg-white'
|
||||||
src={item.icon}
|
}`}
|
||||||
className="object-contain shrink-0 self-stretch my-auto w-5 aspect-square"
|
>
|
||||||
alt={item.label}
|
<img
|
||||||
/>
|
loading="lazy"
|
||||||
<div className="self-stretch my-auto text-gray-600">{item.label}</div>
|
src={item.icon}
|
||||||
</div>
|
className="object-contain shrink-0 self-stretch my-auto w-5 aspect-square"
|
||||||
</Link>
|
alt={item.label}
|
||||||
);
|
/>
|
||||||
})}
|
<div className="self-stretch my-auto text-gray-600">{item.label}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Кнопка выхода */}
|
{/* Кнопка выхода */}
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
import { useIsClient } from '@/lib/useIsomorphicLayoutEffect';
|
||||||
|
import { GET_CLIENT_ME } from '@/lib/graphql';
|
||||||
|
|
||||||
interface ProfileSidebarProps {
|
interface ProfileSidebarProps {
|
||||||
activeItem: string;
|
activeItem: string;
|
||||||
@ -8,6 +10,15 @@ interface ProfileSidebarProps {
|
|||||||
const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
||||||
const isClient = useIsClient();
|
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 = [
|
const menuItems = [
|
||||||
{ id: 'orders', icon: 'order', label: 'Заказы', href: '/profile-orders' },
|
{ id: 'orders', icon: 'order', label: 'Заказы', href: '/profile-orders' },
|
||||||
{ id: 'history', icon: 'history', label: 'История поиска', href: '/profile-history' },
|
{ id: 'history', icon: 'history', label: 'История поиска', href: '/profile-history' },
|
||||||
@ -133,25 +144,28 @@ const ProfileSidebar: React.FC<ProfileSidebarProps> = ({ activeItem }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sidebar-section">
|
{/* Раздел "Финансы" показываем только если есть юридические лица */}
|
||||||
<div className="sidebar-header">
|
{hasLegalEntities && (
|
||||||
<h3 className="sidebar-title">Финансы</h3>
|
<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>
|
||||||
<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">
|
<div className="logout-section">
|
||||||
|
@ -23,7 +23,7 @@ export default function InfoCard({
|
|||||||
currency = 'RUB',
|
currency = 'RUB',
|
||||||
image
|
image
|
||||||
}: InfoCardProps) {
|
}: InfoCardProps) {
|
||||||
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();
|
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
|
||||||
|
|
||||||
// Проверяем, есть ли товар в избранном
|
// Проверяем, есть ли товар в избранном
|
||||||
const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brand);
|
const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brand);
|
||||||
@ -34,9 +34,18 @@ export default function InfoCard({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isItemFavorite) {
|
if (isItemFavorite) {
|
||||||
// Создаем ID для удаления
|
// Находим товар в избранном по правильному ID
|
||||||
const id = `${productId || offerKey || ''}:${articleNumber}:${brand}`;
|
const favoriteItem = favorites.find((fav: any) => {
|
||||||
removeFromFavorites(id);
|
// Проверяем по разным комбинациям идентификаторов
|
||||||
|
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 {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
addToFavorites({
|
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 {
|
interface ProductCharacteristicsProps {
|
||||||
result?: any;
|
result?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
||||||
|
const [partsIndexData, setPartsIndexData] = useState<any>(null);
|
||||||
|
|
||||||
// Функция для рендеринга характеристик из нашей базы данных
|
// Запрос к Parts Index для получения дополнительных характеристик
|
||||||
const renderInternalCharacteristics = () => {
|
const { data: partsIndexResult, loading: partsIndexLoading } = useQuery(PARTS_INDEX_SEARCH_BY_ARTICLE, {
|
||||||
if (!result?.characteristics || result.characteristics.length === 0) return null;
|
variables: {
|
||||||
|
articleNumber: result?.articleNumber || '',
|
||||||
|
brandName: result?.brand || '',
|
||||||
|
lang: 'ru'
|
||||||
|
},
|
||||||
|
skip: !result?.articleNumber || !result?.brand,
|
||||||
|
errorPolicy: 'ignore'
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="w-layout-vflex flex-block-53">
|
if (partsIndexResult?.partsIndexSearchByArticle) {
|
||||||
<div className="w-layout-hflex flex-block-55">
|
setPartsIndexData(partsIndexResult.partsIndexSearchByArticle);
|
||||||
<span className="text-block-29" style={{ fontWeight: 'bold', color: '#333' }}>
|
}
|
||||||
Характеристики товара:
|
}, [partsIndexResult]);
|
||||||
</span>
|
|
||||||
</div>
|
// Функция для рендеринга параметров из Parts Index
|
||||||
{result.characteristics.map((char: any, index: number) => (
|
const renderPartsIndexParameters = () => {
|
||||||
<div key={index} className="w-layout-hflex flex-block-55">
|
if (!partsIndexData?.parameters) return null;
|
||||||
<span className="text-block-29">{char.characteristic.name}:</span>
|
|
||||||
<span className="text-block-28">{char.value}</span>
|
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>
|
||||||
))}
|
))}
|
||||||
</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 (
|
return (
|
||||||
@ -111,27 +62,33 @@ const ProductCharacteristics = ({ result }: ProductCharacteristicsProps) => {
|
|||||||
<span className="text-block-29">Название:</span>
|
<span className="text-block-29">Название:</span>
|
||||||
<span className="text-block-28">{result.name}</span>
|
<span className="text-block-28">{result.name}</span>
|
||||||
</div>
|
</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">
|
<div className="w-layout-hflex flex-block-55">
|
||||||
<span className="text-block-29">Описание:</span>
|
<span className="text-block-29">Описание:</span>
|
||||||
<span className="text-block-28" style={{ maxWidth: '400px', wordWrap: 'break-word' }}>
|
<span className="text-block-28">{partsIndexData.description}</span>
|
||||||
{result.description}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Характеристики из нашей базы данных */}
|
|
||||||
{renderInternalCharacteristics()}
|
|
||||||
|
|
||||||
{/* Дополнительные характеристики из Parts Index */}
|
{/* Дополнительные характеристики из 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>
|
</div>
|
||||||
|
|
||||||
{/* Изображения из нашей базы данных */}
|
|
||||||
{renderInternalImages()}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,53 +2,55 @@ import React from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const AvailableParts = () => (
|
const AvailableParts = () => (
|
||||||
<section>
|
<section>
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
<div className="w-layout-vflex flex-block-5">
|
<div className="w-layout-vflex flex-block-5">
|
||||||
<div className="w-layout-hflex flex-block-31">
|
<div className="w-layout-hflex flex-block-31">
|
||||||
<h2 className="heading-4">Автозапчасти в наличии</h2>
|
<h2 className="heading-4">Автозапчасти в наличии</h2>
|
||||||
<div className="w-layout-hflex flex-block-29">
|
<div className="w-layout-hflex flex-block-29">
|
||||||
<Link href="/catalog" className="text-block-18">
|
<Link href="/catalog" className="text-block-18">
|
||||||
Ко всем автозапчастям
|
Ко всем автозапчастям
|
||||||
</Link>
|
|
||||||
<img src="/images/Arrow_right.svg" loading="lazy" alt="" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex flex-block-6">
|
|
||||||
<Link href="/catalog" className="div-block-12" id="w-node-bc394713-4b8e-44e3-8ddf-3edc1c31a743-3b3232bc">
|
|
||||||
<h1 className="heading-7">Аксессуары</h1>
|
|
||||||
<img src="/images/IMG_1.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/catalog" className="div-block-12-copy">
|
<img src="/images/Arrow_right.svg" loading="lazy" alt="" />
|
||||||
<h1 className="heading-7">Воздушные фильтры</h1>
|
|
||||||
<img src="/images/IMG_2.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
|
||||||
<Link href="/catalog" className="div-block-12">
|
|
||||||
<h1 className="heading-7">Шины</h1>
|
|
||||||
<img src="/images/IMG_3.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
|
||||||
<Link href="/catalog" className="div-block-123">
|
|
||||||
<h1 className="heading-7-white">Аккумуляторы</h1>
|
|
||||||
<img src="/images/IMG_4.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
|
||||||
<div className="w-layout-hflex flex-block-35" id="w-node-_8908a890-8c8f-e12c-999f-08d5da3bcc01-3b3232bc">
|
|
||||||
<Link href="/catalog" className="div-block-12 small">
|
|
||||||
<h1 className="heading-7">Диски</h1>
|
|
||||||
<img src="/images/IMG_5.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
|
||||||
<Link href="/catalog" className="div-block-12 small">
|
|
||||||
<h1 className="heading-7">Свечи</h1>
|
|
||||||
<img src="/images/IMG_6.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
|
||||||
<Link href="/catalog" className="div-block-red small">
|
|
||||||
<h1 className="heading-7-white">Масла</h1>
|
|
||||||
<img src="/images/IMG_7.png" loading="lazy" alt="" className="image-22" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-6">
|
||||||
|
<Link href="/catalog" className="div-block-12">
|
||||||
|
<h1 className="heading-7">Аксессуары</h1>
|
||||||
|
<img src="/images/IMG_1.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-12-copy">
|
||||||
|
<h1 className="heading-7">Воздушные фильтры</h1>
|
||||||
|
<img src="/images/IMG_2.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-12">
|
||||||
|
<h1 className="heading-7">Шины</h1>
|
||||||
|
<img src="/images/IMG_3.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-123">
|
||||||
|
<h1 className="heading-7-white">Аккумуляторы</h1>
|
||||||
|
<img src="/images/IMG_4.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-12 small">
|
||||||
|
<h1 className="heading-7">Диски</h1>
|
||||||
|
<img src="/images/IMG_5.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-12 small">
|
||||||
|
<h1 className="heading-7">Свечи</h1>
|
||||||
|
<img src="/images/IMG_6.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-red small">
|
||||||
|
<h1 className="heading-7-white">Масла</h1>
|
||||||
|
<img src="/images/IMG_7.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
<Link href="/catalog" className="div-block-12 small">
|
||||||
|
<h1 className="heading-7">Диски</h1>
|
||||||
|
<img src="/images/IMG_5.png" loading="lazy" alt="" className="image-22" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default AvailableParts;
|
export default AvailableParts;
|
@ -15,6 +15,7 @@ import {
|
|||||||
const ProfileHistoryMain = () => {
|
const ProfileHistoryMain = () => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState("Все");
|
const [activeTab, setActiveTab] = useState("Все");
|
||||||
|
const [selectedManufacturer, setSelectedManufacturer] = useState("Все");
|
||||||
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||||
@ -105,6 +106,14 @@ const ProfileHistoryMain = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let filtered = [...getFilteredByTime(historyItems, activeTab)];
|
let filtered = [...getFilteredByTime(historyItems, activeTab)];
|
||||||
|
|
||||||
|
// Фильтрация по производителю
|
||||||
|
if (selectedManufacturer !== "Все") {
|
||||||
|
filtered = filtered.filter(item =>
|
||||||
|
item.brand === selectedManufacturer ||
|
||||||
|
item.vehicleInfo?.brand === selectedManufacturer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Поиск
|
// Поиск
|
||||||
if (search.trim()) {
|
if (search.trim()) {
|
||||||
const searchLower = search.toLowerCase();
|
const searchLower = search.toLowerCase();
|
||||||
@ -152,7 +161,7 @@ const ProfileHistoryMain = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFilteredItems(filtered);
|
setFilteredItems(filtered);
|
||||||
}, [historyItems, search, activeTab, sortField, sortOrder]);
|
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
||||||
|
|
||||||
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
@ -272,6 +281,18 @@ const ProfileHistoryMain = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<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 && (
|
{historyItems.length === 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateTestData}
|
onClick={handleCreateTestData}
|
||||||
@ -297,7 +318,14 @@ const ProfileHistoryMain = () => {
|
|||||||
</div>
|
</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">
|
<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>
|
||||||
|
|
||||||
<div className="flex flex-col mt-5 w-full text-gray-400 max-md:max-w-full flex-1 h-full">
|
<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 && (
|
{filteredItems.length > 0 && (
|
||||||
<div className="mt-4 text-center text-sm text-gray-500">
|
<div className="mt-4 text-center text-sm text-gray-500">
|
||||||
Показано {filteredItems.length} из {historyItems.length} записей
|
Показано {filteredItems.length} из {historyItems.length} записей
|
||||||
|
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||||
|
<span className="ml-2 text-blue-600">
|
||||||
|
(применены фильтры)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,22 +1,47 @@
|
|||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
|
import { PartsSearchHistoryItem } from '@/lib/graphql/search-history';
|
||||||
|
|
||||||
interface ProfileHistoryTabsProps {
|
interface ProfileHistoryTabsProps {
|
||||||
tabs: string[];
|
tabs: string[];
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
onTabChange: (tab: string) => void;
|
onTabChange: (tab: string) => void;
|
||||||
|
historyItems: PartsSearchHistoryItem[];
|
||||||
|
selectedManufacturer: string;
|
||||||
|
onManufacturerChange: (manufacturer: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manufacturers = ["Все", "VAG", "Toyota", "Ford", "BMW"];
|
|
||||||
|
|
||||||
const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
||||||
tabs,
|
tabs,
|
||||||
activeTab,
|
activeTab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
|
historyItems,
|
||||||
|
selectedManufacturer,
|
||||||
|
onManufacturerChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedManufacturer, setSelectedManufacturer] = useState(manufacturers[0]);
|
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
React.useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
@ -34,6 +59,11 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isDropdownOpen]);
|
}, [isDropdownOpen]);
|
||||||
|
|
||||||
|
const handleManufacturerSelect = (manufacturer: string) => {
|
||||||
|
onManufacturerChange(manufacturer);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-5 w-full max-md:max-w-full">
|
<div className="flex flex-wrap gap-5 w-full max-md:max-w-full">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
@ -69,20 +99,41 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
|
|||||||
>
|
>
|
||||||
<span className="truncate">{selectedManufacturer}</span>
|
<span className="truncate">{selectedManufacturer}</span>
|
||||||
<span className="ml-2 flex-shrink-0 flex items-center">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isDropdownOpen && (
|
{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">
|
<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.map((option) => (
|
{manufacturers.length === 0 ? (
|
||||||
<li
|
<li className="px-6 py-4 text-gray-400 text-center">
|
||||||
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}
|
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useLazyQuery, useQuery } from '@apollo/client';
|
import { useLazyQuery, useQuery } from '@apollo/client';
|
||||||
import { SEARCH_LAXIMO_FULLTEXT, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS } from '@/lib/graphql/laximo';
|
import { SEARCH_LAXIMO_FULLTEXT, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo';
|
||||||
import VinPartCard from './VinPartCard';
|
import VinPartCard from './VinPartCard';
|
||||||
|
|
||||||
interface VinLeftbarProps {
|
interface VinLeftbarProps {
|
||||||
catalogCode?: string;
|
vehicleInfo: {
|
||||||
vehicleId?: string;
|
catalog: string;
|
||||||
ssd?: string;
|
vehicleid: string;
|
||||||
|
ssd: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
onSearchResults?: (results: any[]) => void;
|
onSearchResults?: (results: any[]) => void;
|
||||||
onNodeSelect?: (node: any) => void;
|
onNodeSelect?: (node: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, onSearchResults, onNodeSelect }) => {
|
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect }) => {
|
||||||
|
const catalogCode = vehicleInfo.catalog;
|
||||||
|
const vehicleId = vehicleInfo.vehicleid;
|
||||||
|
const ssd = vehicleInfo.ssd;
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
||||||
@ -88,6 +94,92 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, on
|
|||||||
const showNotFound = isSearchAvailable && searchQuery.trim() && !loading && data && searchResults && searchResults.details && searchResults.details.length === 0;
|
const showNotFound = isSearchAvailable && searchQuery.trim() && !loading && data && searchResults && searchResults.details && searchResults.details.length === 0;
|
||||||
const showTips = isSearchAvailable && !searchQuery.trim() && !loading;
|
const showTips = isSearchAvailable && !searchQuery.trim() && !loading;
|
||||||
|
|
||||||
|
// --- QuickGroups (от производителя) ---
|
||||||
|
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
|
||||||
|
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
|
||||||
|
variables: { catalogCode, vehicleId, ssd },
|
||||||
|
skip: !catalogCode || !vehicleId || activeTab !== 'manufacturer',
|
||||||
|
errorPolicy: 'all'
|
||||||
|
});
|
||||||
|
const quickGroups = quickGroupsData?.laximoQuickGroups || [];
|
||||||
|
|
||||||
|
const [expandedQuickGroups, setExpandedQuickGroups] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const handleQuickGroupToggle = (groupId: string) => {
|
||||||
|
setExpandedQuickGroups(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(groupId)) {
|
||||||
|
newSet.delete(groupId);
|
||||||
|
} else {
|
||||||
|
newSet.add(groupId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickGroupClick = (group: any) => {
|
||||||
|
if (group.link) {
|
||||||
|
setSelectedQuickGroup(group);
|
||||||
|
} else {
|
||||||
|
handleQuickGroupToggle(group.quickgroupid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Детали выбранной группы (если link: true)
|
||||||
|
console.log('QuickDetail QUERY VARS', {
|
||||||
|
catalogCode,
|
||||||
|
vehicleId,
|
||||||
|
quickGroupId: selectedQuickGroup?.quickgroupid,
|
||||||
|
ssd
|
||||||
|
});
|
||||||
|
|
||||||
|
const skipQuickDetail =
|
||||||
|
!selectedQuickGroup ||
|
||||||
|
!catalogCode ||
|
||||||
|
!vehicleId ||
|
||||||
|
!selectedQuickGroup.quickgroupid ||
|
||||||
|
!ssd;
|
||||||
|
|
||||||
|
const { data: quickDetailData, loading: quickDetailLoading, error: quickDetailError } = useQuery(GET_LAXIMO_QUICK_DETAIL, {
|
||||||
|
variables: selectedQuickGroup ? {
|
||||||
|
catalogCode,
|
||||||
|
vehicleId,
|
||||||
|
quickGroupId: selectedQuickGroup.quickgroupid,
|
||||||
|
ssd
|
||||||
|
} : undefined,
|
||||||
|
skip: skipQuickDetail,
|
||||||
|
errorPolicy: 'all'
|
||||||
|
});
|
||||||
|
const quickDetail = quickDetailData?.laximoQuickDetail;
|
||||||
|
|
||||||
|
const renderQuickGroupTree = (groups: any[], level = 0): React.ReactNode => (
|
||||||
|
<div>
|
||||||
|
{groups.map(group => (
|
||||||
|
<div key={group.quickgroupid} style={{ marginLeft: level * 16, marginBottom: 8 }}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center p-2 rounded cursor-pointer border ${group.link ? 'bg-white hover:bg-red-50 border-gray-200 hover:border-red-300' : 'bg-gray-50 hover:bg-gray-100 border-gray-200'}`}
|
||||||
|
onClick={() => handleQuickGroupClick(group)}
|
||||||
|
>
|
||||||
|
{group.children && group.children.length > 0 && (
|
||||||
|
<svg className={`w-4 h-4 text-gray-400 mr-2 transition-transform ${expandedQuickGroups.has(group.quickgroupid) ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<span className={`font-medium ${group.link ? 'text-gray-900' : 'text-gray-600'}`}>{group.name}</span>
|
||||||
|
{group.link && (
|
||||||
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">Доступен поиск</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{group.children && group.children.length > 0 && expandedQuickGroups.has(group.quickgroupid) && (
|
||||||
|
<div className="mt-1">
|
||||||
|
{renderQuickGroupTree(group.children, level + 1)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-layout-vflex vinleftbar">
|
<div className="w-layout-vflex vinleftbar">
|
||||||
<div className="div-block-2">
|
<div className="div-block-2">
|
||||||
@ -217,8 +309,47 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ catalogCode, vehicleId, ssd, on
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
// Manufacturer tab content (заглушка)
|
// Manufacturer tab content (QuickGroups)
|
||||||
<div style={{ padding: '16px', color: '#888' }}>Здесь будет контент "От производителя"</div>
|
<div style={{ padding: '16px' }}>
|
||||||
|
{quickGroupsLoading ? (
|
||||||
|
<div style={{ textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
|
||||||
|
) : quickGroupsError ? (
|
||||||
|
<div style={{ color: 'red' }}>Ошибка загрузки групп: {quickGroupsError.message}</div>
|
||||||
|
) : selectedQuickGroup ? (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setSelectedQuickGroup(null)} className="mb-4 px-3 py-1 bg-gray-200 rounded">Назад к группам</button>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">{selectedQuickGroup.name}</h3>
|
||||||
|
{quickDetailLoading ? (
|
||||||
|
<div>Загружаем детали...</div>
|
||||||
|
) : quickDetailError ? (
|
||||||
|
<div style={{ color: 'red' }}>Ошибка загрузки деталей: {quickDetailError.message}</div>
|
||||||
|
) : quickDetail && quickDetail.units && quickDetail.units.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{quickDetail.units.map((unit: any) => (
|
||||||
|
<div key={unit.unitid} className="p-3 bg-gray-50 rounded border border-gray-200">
|
||||||
|
<div className="font-medium text-gray-900">{unit.name}</div>
|
||||||
|
{unit.details && unit.details.length > 0 && (
|
||||||
|
<ul className="mt-2 text-sm text-gray-700 list-disc pl-5">
|
||||||
|
{unit.details.map((detail: any) => (
|
||||||
|
<li key={detail.detailid}>
|
||||||
|
<span className="font-medium">{detail.name}</span> <span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">OEM: {detail.oem}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>Нет деталей для этой группы</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : quickGroups.length > 0 ? (
|
||||||
|
renderQuickGroupTree(quickGroups)
|
||||||
|
) : (
|
||||||
|
<div>Нет доступных групп быстрого поиска</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Tab content end */}
|
{/* Tab content end */}
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@ export const useArticleImage = (
|
|||||||
artId: string | undefined | null,
|
artId: string | undefined | null,
|
||||||
options: UseArticleImageOptions = {}
|
options: UseArticleImageOptions = {}
|
||||||
): UseArticleImageReturn => {
|
): UseArticleImageReturn => {
|
||||||
const { enabled = true, fallbackImage = '' } = options;
|
const { enabled = true, fallbackImage = '/images/image-10.png' } = options;
|
||||||
const [imageUrl, setImageUrl] = useState<string>(fallbackImage);
|
const [imageUrl, setImageUrl] = useState<string>(fallbackImage);
|
||||||
|
|
||||||
// Проверяем что artId валидный
|
// Проверяем что artId валидный
|
||||||
|
@ -7,7 +7,6 @@ import CartSummary from "@/components/CartSummary";
|
|||||||
import CartRecommended from "../components/CartRecommended";
|
import CartRecommended from "../components/CartRecommended";
|
||||||
import CatalogSubscribe from "@/components/CatalogSubscribe";
|
import CatalogSubscribe from "@/components/CatalogSubscribe";
|
||||||
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
|
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
|
||||||
import CartDebug from "@/components/CartDebug";
|
|
||||||
|
|
||||||
export default function CartPage() {
|
export default function CartPage() {
|
||||||
|
|
||||||
@ -39,7 +38,6 @@ export default function CartPage() {
|
|||||||
</section>
|
</section>
|
||||||
<Footer />
|
<Footer />
|
||||||
<MobileMenuBottomSection />
|
<MobileMenuBottomSection />
|
||||||
<CartDebug />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ export default function Favorite() {
|
|||||||
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
|
const [showFiltersMobile, setShowFiltersMobile] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [filterValues, setFilterValues] = useState<{[key: string]: any}>({});
|
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');
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||||
|
|
||||||
// Создаем динамические фильтры на основе данных избранного
|
// Создаем динамические фильтры на основе данных избранного
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Header from "@/components/Header";
|
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMutation, ApolloProvider } from "@apollo/client";
|
import { useMutation, ApolloProvider } from "@apollo/client";
|
||||||
import { gql } from "@apollo/client";
|
import { gql } from "@apollo/client";
|
||||||
import { apolloClient } from "@/lib/apollo";
|
import { apolloClient } from "@/lib/apollo";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const CONFIRM_PAYMENT = gql`
|
const CONFIRM_PAYMENT = gql`
|
||||||
mutation ConfirmPayment($orderId: ID!) {
|
mutation ConfirmPayment($orderId: ID!) {
|
||||||
@ -51,8 +51,15 @@ function PaymentSuccessContent() {
|
|||||||
}
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
console.log('Оплата заказа подтверждена');
|
console.log('Оплата заказа подтверждена');
|
||||||
|
toast.success('Оплата успешно подтверждена!', {
|
||||||
|
duration: 3000,
|
||||||
|
icon: '✅',
|
||||||
|
});
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
console.error('Ошибка подтверждения оплаты:', error);
|
console.error('Ошибка подтверждения оплаты:', error);
|
||||||
|
toast.error('Ошибка подтверждения оплаты: ' + error.message, {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [router.query, confirmPayment]);
|
}, [router.query, confirmPayment]);
|
||||||
@ -76,7 +83,7 @@ function PaymentSuccessContent() {
|
|||||||
<link href="/images/webclip.png" rel="apple-touch-icon" />
|
<link href="/images/webclip.png" rel="apple-touch-icon" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<div className="w-layout-blockcontainer container info w-container">
|
<div className="w-layout-blockcontainer container info w-container">
|
||||||
<div className="w-layout-vflex flex-block-9">
|
<div className="w-layout-vflex flex-block-9">
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import * as React from "react";
|
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 Header from '@/components/Header';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import ProfileSidebar from '@/components/ProfileSidebar';
|
import ProfileSidebar from '@/components/ProfileSidebar';
|
||||||
@ -11,6 +15,48 @@ import NotificationMane from "@/components/profile/NotificationMane";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
const ProfileActsPage = () => {
|
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 (
|
return (
|
||||||
<div className="page-wrapper">
|
<div className="page-wrapper">
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import * as React from "react";
|
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 Header from '@/components/Header';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import CatalogSubscribe from '@/components/CatalogSubscribe';
|
import CatalogSubscribe from '@/components/CatalogSubscribe';
|
||||||
@ -8,9 +12,49 @@ import ProfileRequisitiesMain from '@/components/profile/ProfileRequisitiesMain'
|
|||||||
import ProfileInfo from '@/components/profile/ProfileInfo';
|
import ProfileInfo from '@/components/profile/ProfileInfo';
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ProfileRequisitiesPage = () => {
|
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 (
|
return (
|
||||||
<div className="page-wrapper">
|
<div className="page-wrapper">
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -424,12 +424,12 @@ export default function SearchResult() {
|
|||||||
<title>Поиск предложений {searchQuery} - Protek</title>
|
<title>Поиск предложений {searchQuery} - Protek</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 bg-gray-50 bg-opacity-90 flex items-center justify-center min-h-screen" aria-live="polite">
|
||||||
<div className="text-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mb-4"></div>
|
||||||
<p className="mt-4 text-lg text-gray-600">Поиск предложений...</p>
|
<p className="text-lg text-gray-600">Поиск предложений...</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -502,6 +502,7 @@ export default function SearchResult() {
|
|||||||
price={`${offer.price.toLocaleString()} ₽`}
|
price={`${offer.price.toLocaleString()} ₽`}
|
||||||
delivery={`${offer.deliveryDuration} ${offer.deliveryDuration === 1 ? 'день' : 'дней'}`}
|
delivery={`${offer.deliveryDuration} ${offer.deliveryDuration === 1 ? 'день' : 'дней'}`}
|
||||||
stock={`${offer.quantity} шт.`}
|
stock={`${offer.quantity} шт.`}
|
||||||
|
offer={offer}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,6 +105,14 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
|||||||
ssdLength: ssd.length
|
ssdLength: ssd.length
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Создаем базовые параметры URL
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// Добавляем VIN-номер в URL, если он есть
|
||||||
|
if (searchQuery && searchType === 'vin') {
|
||||||
|
urlParams.set('vin', searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
// Если есть SSD, сохраняем его в localStorage для безопасной передачи
|
// Если есть SSD, сохраняем его в localStorage для безопасной передачи
|
||||||
if (ssd && ssd.trim() !== '') {
|
if (ssd && ssd.trim() !== '') {
|
||||||
const vehicleKey = `vehicle_ssd_${catalogCode}_${vehicleId}`;
|
const vehicleKey = `vehicle_ssd_${catalogCode}_${vehicleId}`;
|
||||||
@ -121,25 +129,22 @@ const VehicleSearchResultsPage: React.FC<VehicleSearchResultsPageProps> = () =>
|
|||||||
|
|
||||||
localStorage.setItem(vehicleKey, ssd);
|
localStorage.setItem(vehicleKey, ssd);
|
||||||
|
|
||||||
// Выбираем URL в зависимости от того, нужно ли пропустить промежуточную страницу
|
urlParams.set('use_storage', '1');
|
||||||
const url = skipToCategories
|
urlParams.set('ssd_length', ssd.length.toString());
|
||||||
? `/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);
|
|
||||||
}
|
}
|
||||||
}, [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, если найден только один автомобиль
|
// Предзагрузка и автоматический переход при поиске по VIN, если найден только один автомобиль
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -269,13 +269,13 @@ const VehicleDetailsPage = () => {
|
|||||||
<div className="w-layout-blockcontainer container-vin w-container">
|
<div className="w-layout-blockcontainer container-vin w-container">
|
||||||
{!selectedNode ? (
|
{!selectedNode ? (
|
||||||
<div className="w-layout-hflex flex-block-13">
|
<div className="w-layout-hflex flex-block-13">
|
||||||
<VinLeftbar
|
{vehicleInfo && vehicleInfo.catalog && vehicleInfo.vehicleid && vehicleInfo.ssd && (
|
||||||
catalogCode={vehicleInfo.catalog}
|
<VinLeftbar
|
||||||
vehicleId={vehicleInfo.vehicleid}
|
vehicleInfo={vehicleInfo}
|
||||||
ssd={vehicleInfo.ssd}
|
onSearchResults={setFoundParts}
|
||||||
onSearchResults={setFoundParts}
|
onNodeSelect={setSelectedNode}
|
||||||
onNodeSelect={setSelectedNode}
|
/>
|
||||||
/>
|
)}
|
||||||
{/* Категории или Knot или карточки */}
|
{/* Категории или Knot или карточки */}
|
||||||
{foundParts.length > 0 ? (
|
{foundParts.length > 0 ? (
|
||||||
<div className="knot-parts">
|
<div className="knot-parts">
|
||||||
|
Reference in New Issue
Block a user