9 Commits

Author SHA1 Message Date
7d9f611fe5 Merge pull request 'демо исправление карточки' (#14) from 1318 into main
Reviewed-on: #14
2025-07-06 13:20:06 +03:00
8820f4e835 демо исправление карточки 2025-07-06 13:18:53 +03:00
ac7b2de49f Обновлена логика добавления товаров в корзину во всех компонентах. Теперь добавление происходит асинхронно с обработкой успешных и ошибочных результатов. Добавлена информация о наличии товара при добавлении в корзину. Улучшены уведомления о добавлении товара с учетом статуса операции. 2025-07-06 02:21:33 +03:00
a8c8ae60bb Добавлен компонент CartIcon и обновлены уведомления о добавлении товара в корзину во всех соответствующих компонентах. Изменены стили текста и иконки в уведомлениях для улучшения визуального восприятия. 2025-07-05 18:38:12 +03:00
78e17a94ab Исправлены размеры SVG в компонентах CartRecommendedProductCard, Footer и Header. Обновлены атрибуты width и height для корректного отображения и устранения опечаток. В компоненте VinQuick улучшена уникальность ключей при отображении деталей. 2025-07-05 18:00:31 +03:00
36c5990921 Улучшена обработка SSD в компонентах QuickDetailSection, UnitDetailsSection и KnotIn. Добавлены отладочные логи для отслеживания значений SSD и состояния загрузки данных. Обновлены условия пропуска запросов в зависимости от наличия SSD. Исправлена логика передачи SSD в компонент KnotIn с использованием значения узла или родительского SSD. 2025-07-05 13:35:49 +03:00
e989d402a3 Добавлены отладочные логи в компонент WizardSearchForm для отслеживания обновлений и сброса параметров. Обновлена логика автовыбора параметров с учетом состояния загрузки. В компонент VinLeftbar добавлена загрузка единиц для категории при открытии. Улучшена обработка SSD при сбросе параметров. 2025-07-05 12:55:08 +03:00
65710a35be Merge pull request 'newpravki' (#13) from newpravki into main
Reviewed-on: #13
2025-07-04 21:52:14 +03:00
9a604b39b3 переделаны счетчки фильтр рэндж, настроены выборы категорий и подкатегорий 2025-07-04 21:51:28 +03:00
30 changed files with 1093 additions and 532 deletions

View File

@ -1,6 +1,7 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import CartIcon from "./CartIcon";
interface BestPriceCardProps { interface BestPriceCardProps {
bestOfferType: string; bestOfferType: string;
@ -27,6 +28,11 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
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;
const [count, setCount] = useState(1); const [count, setCount] = useState(1);
const [inputValue, setInputValue] = useState("1");
useEffect(() => {
setInputValue(count.toString());
}, [count]);
const handleMinus = () => setCount(prev => Math.max(1, prev - 1)); const handleMinus = () => setCount(prev => Math.max(1, prev - 1));
const handlePlus = () => { const handlePlus = () => {
@ -38,7 +44,13 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
}; };
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = parseInt(e.target.value, 10); const val = e.target.value;
setInputValue(val);
if (val === "") {
// Не обновляем count, пока не будет blur
return;
}
let value = parseInt(val, 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) {
toast.error(`Максимум ${maxCount} шт.`); toast.error(`Максимум ${maxCount} шт.`);
@ -47,6 +59,13 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
setCount(value); setCount(value);
}; };
const handleInputBlur = () => {
if (inputValue === "") {
setInputValue("1");
setCount(1);
}
};
// Функция для парсинга цены из строки // Функция для парсинга цены из строки
const parsePrice = (priceStr: string): number => { const parsePrice = (priceStr: string): number => {
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.'); const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
@ -54,7 +73,7 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
}; };
// Обработчик добавления в корзину // Обработчик добавления в корзину
const handleAddToCart = (e: React.MouseEvent) => { const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -69,14 +88,8 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
return; return;
} }
// Проверяем наличие
if (maxCount !== undefined && count > maxCount) {
toast.error(`Недостаточно товара в наличии. Доступно: ${maxCount} шт.`);
return;
}
try { try {
addItem({ const result = await addItem({
productId: offer.productId, productId: offer.productId,
offerKey: offer.offerKey, offerKey: offer.offerKey,
name: description, name: description,
@ -86,6 +99,7 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
price: numericPrice, price: numericPrice,
currency: offer.currency || 'RUB', currency: offer.currency || 'RUB',
quantity: count, quantity: count,
stock: maxCount, // передаем информацию о наличии
deliveryTime: delivery, deliveryTime: delivery,
warehouse: offer.warehouse || 'Склад', warehouse: offer.warehouse || 'Склад',
supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'), supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'),
@ -93,17 +107,22 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
image: offer.image, image: offer.image,
}); });
// Показываем тоастер об успешном добавлении if (result.success) {
toast.success( // Показываем тоастер об успешном добавлении
<div> toast.success(
<div className="font-semibold">Товар добавлен в корзину!</div> <div>
<div className="text-sm text-gray-600">{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div> <div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
</div>, <div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
{ </div>,
duration: 3000, {
icon: '🛒', duration: 3000,
} icon: <CartIcon size={20} color="#fff" />,
); }
);
} else {
// Показываем ошибку
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
}
} catch (error) { } catch (error) {
console.error('Ошибка добавления в корзину:', error); console.error('Ошибка добавления в корзину:', error);
toast.error('Ошибка добавления товара в корзину'); toast.error('Ошибка добавления товара в корзину');
@ -144,8 +163,9 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
type="number" type="number"
min={1} min={1}
max={maxCount} max={maxCount}
value={count} value={inputValue}
onChange={handleInput} onChange={handleInput}
onBlur={handleInputBlur}
className="text-block-26 w-full text-center outline-none" className="text-block-26 w-full text-center outline-none"
aria-label="Количество" aria-label="Количество"
/> />

View File

@ -0,0 +1,25 @@
import React from 'react';
interface CartIconProps {
size?: number;
color?: string;
}
const CartIcon: React.FC<CartIconProps> = ({ size = 24, color = '#fff' }) => {
return (
<svg
width={size}
height={size}
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={color}
/>
</svg>
);
};
export default CartIcon;

View File

@ -38,160 +38,180 @@ const CartItem: React.FC<CartItemProps> = ({
onRemove, onRemove,
isSummaryStep = false, isSummaryStep = false,
itemNumber, itemNumber,
}) => ( }) => {
<div className="w-layout-hflex cart-item"> // --- Фикс для input: можно стереть, при blur пустое = 1 ---
<div className="w-layout-hflex info-block-search-copy"> const [inputValue, setInputValue] = React.useState(count.toString());
{isSummaryStep ? ( React.useEffect(() => {
<div style={{ marginRight: 12, minWidth: 24, textAlign: 'center', fontWeight: 600, fontSize: 14 }}>{itemNumber}</div> setInputValue(count.toString());
) : ( }, [count]);
<div
className={"div-block-7" + (selected ? " active" : "")} return (
onClick={onSelect} <div className="w-layout-hflex cart-item">
style={{ marginRight: 12, cursor: 'pointer' }} <div className="w-layout-hflex info-block-search-copy">
>
{selected && (
<svg width="14" height="10" viewBox="0 0 14 10" fill="none">
<path d="M2 5.5L6 9L12 2" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</div>
)}
<div className="w-layout-hflex block-name">
<h4 className="heading-9-copy">{name}</h4>
<div
className={
"text-block-21-copy" +
(isSummaryStep && itemNumber === 1 ? " border-t-0" : "")
}
style={
isSummaryStep && itemNumber === 1
? { borderTop: 'none' }
: undefined
}
>
{description}
</div>
</div>
<div className="form-block-copy w-form">
<form className="form-copy" onSubmit={e => e.preventDefault()}>
<input
className="text-field-copy w-input"
maxLength={256}
name="Search-5"
data-name="Search 5"
placeholder="Комментарий"
type="text"
id="Search-5"
value={comment}
onChange={e => onComment(e.target.value)}
disabled={isSummaryStep}
/>
</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 className="w-layout-hflex add-to-cart-block">
<div className="w-layout-hflex flex-block-39-copy">
<h4 className="delivery-cart-s1">{delivery}</h4>
<div className="text-block-21-copy-copy">{deliveryDate}</div>
</div>
<div className="w-layout-hflex pcs-cart-s1">
{isSummaryStep ? ( {isSummaryStep ? (
<div className="text-block-26" style={{ fontWeight: 600, fontSize: 14 }}>{count} шт.</div> <div style={{ marginRight: 12, minWidth: 24, textAlign: 'center', fontWeight: 600, fontSize: 14 }}>{itemNumber}</div>
) : ( ) : (
<> <div
<div className={"div-block-7" + (selected ? " active" : "")}
className="minus-plus" onClick={onSelect}
onClick={() => onCountChange && onCountChange(count - 1)} style={{ marginRight: 12, cursor: 'pointer' }}
style={{ cursor: 'pointer' }} >
aria-label="Уменьшить количество" {selected && (
tabIndex={0} <svg width="14" height="10" viewBox="0 0 14 10" fill="none">
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)} <path d="M2 5.5L6 9L12 2" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
role="button" </svg>
> )}
<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">
<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
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 className="w-layout-hflex block-name">
<div className="w-layout-hflex flex-block-39-copy-copy"> <h4 className="heading-9-copy">{name}</h4>
<h4 className="price-in-cart-s1">{price}</h4>
<div className="price-1-pcs-cart-s1">{pricePerItem}</div>
</div>
{!isSummaryStep && (
<div className="w-layout-hflex control-element">
<div className="favorite-icon w-embed" onClick={onFavorite} style={{ cursor: 'pointer', color: favorite ? '#e53935' : undefined }}>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>
</div>
<div <div
className="bdel" className={
role="button" "text-block-21-copy" +
tabIndex={0} (isSummaryStep && itemNumber === 1 ? " border-t-0" : "")
aria-label="Удалить из корзины" }
onClick={onRemove} style={
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onRemove && onRemove()} isSummaryStep && itemNumber === 1
style={{ display: 'inline-flex', cursor: 'pointer', transition: 'color 0.2s' }} ? { borderTop: 'none' }
onMouseEnter={e => { : undefined
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');
}}
> >
{description}
</div>
</div>
<div className="form-block-copy w-form">
<form className="form-copy" onSubmit={e => e.preventDefault()}>
<input
className="text-field-copy w-input"
maxLength={256}
name="Search-5"
data-name="Search 5"
placeholder="Комментарий"
type="text"
id="Search-5"
value={comment}
onChange={e => onComment(e.target.value)}
disabled={isSummaryStep}
/>
</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 className="w-layout-hflex add-to-cart-block">
<div className="w-layout-hflex flex-block-39-copy">
<h4 className="delivery-cart-s1">{delivery}</h4>
<div className="text-block-21-copy-copy">{deliveryDate}</div>
</div>
<div className="w-layout-hflex pcs-cart-s1">
{isSummaryStep ? (
<div className="text-block-26" style={{ fontWeight: 600, fontSize: 14 }}>{count} шт.</div>
) : (
<>
<div
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 className="input-pcs">
<input
type="number"
min={1}
value={inputValue}
onChange={e => {
const val = e.target.value;
setInputValue(val);
if (val === "") {
// Не обновляем count, пока не будет blur
return;
}
const valueNum = Math.max(1, parseInt(val, 10) || 1);
onCountChange && onCountChange(valueNum);
}}
onBlur={() => {
if (inputValue === "") {
setInputValue("1");
onCountChange && onCountChange(1);
}
}}
className="text-block-26 w-full text-center outline-none"
aria-label="Количество"
style={{ width: 40 }}
/>
</div>
<div
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 className="w-layout-hflex flex-block-39-copy-copy">
<h4 className="price-in-cart-s1">{price}</h4>
<div className="price-1-pcs-cart-s1">{pricePerItem}</div>
</div>
{!isSummaryStep && (
<div className="w-layout-hflex control-element">
<div className="favorite-icon w-embed" onClick={onFavorite} style={{ cursor: 'pointer', color: favorite ? '#e53935' : undefined }}>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <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"} />
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> </svg>
</div> </div>
<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> );
); };
export default CartItem; export default CartItem;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import CartItem from "./CartItem"; import CartItem from "./CartItem";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext"; import { useFavorites } from "@/contexts/FavoritesContext";
@ -8,7 +8,7 @@ interface CartListProps {
} }
const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => { const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart(); const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity, clearError } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const { items } = state; const { items } = state;
@ -73,8 +73,40 @@ const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
// На втором шаге показываем только выбранные товары // На втором шаге показываем только выбранные товары
const displayItems = isSummaryStep ? items.filter(item => item.selected) : items; const displayItems = isSummaryStep ? items.filter(item => item.selected) : items;
// Автоматически очищаем ошибки через 5 секунд
useEffect(() => {
if (state.error) {
const timer = setTimeout(() => {
clearError();
}, 5000);
return () => clearTimeout(timer);
}
}, [state.error, clearError]);
return ( return (
<div className="w-layout-vflex flex-block-48"> <div className="w-layout-vflex flex-block-48">
{/* Отображение ошибок корзины */}
{state.error && (
<div className="alert alert-error mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
<div className="flex items-center justify-between">
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<span>{state.error}</span>
</div>
<button
onClick={clearError}
className="ml-2 text-red-500 hover:text-red-700"
aria-label="Закрыть уведомление"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)}
<div className="w-layout-vflex product-list-cart"> <div className="w-layout-vflex product-list-cart">
{!isSummaryStep && ( {!isSummaryStep && (
<div className="w-layout-hflex multi-control"> <div className="w-layout-hflex multi-control">

View File

@ -3,6 +3,7 @@ import CatalogProductCard from "./CatalogProductCard";
import { useArticleImage } from "@/hooks/useArticleImage"; import { useArticleImage } from "@/hooks/useArticleImage";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import CartIcon from "./CartIcon";
interface CartRecommendedProps { interface CartRecommendedProps {
recommendedProducts?: any[]; recommendedProducts?: any[];
@ -36,13 +37,14 @@ const RecommendedProductCard: React.FC<{
} }
// Добавляем товар в корзину // Добавляем товар в корзину
addItem({ const result = await addItem({
productId: String(item.artId) || undefined, productId: String(item.artId) || undefined,
name: item.name || `${item.brand} ${item.articleNumber}`, name: item.name || `${item.brand} ${item.articleNumber}`,
description: item.name || `${item.brand} ${item.articleNumber}`, description: item.name || `${item.brand} ${item.articleNumber}`,
price: numericPrice, price: numericPrice,
currency: 'RUB', currency: 'RUB',
quantity: 1, quantity: 1,
stock: undefined, // информация о наличии не доступна для рекомендуемых товаров
image: displayImage, image: displayImage,
brand: item.brand, brand: item.brand,
article: item.articleNumber, article: item.articleNumber,
@ -51,17 +53,22 @@ const RecommendedProductCard: React.FC<{
isExternal: true isExternal: true
}); });
// Показываем успешный тоастер if (result.success) {
toast.success( // Показываем успешный тоастер
<div> toast.success(
<div className="font-semibold">Товар добавлен в корзину!</div> <div>
<div className="text-sm text-gray-600">{item.name || `${item.brand} ${item.articleNumber}`}</div> <div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
</div>, <div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{item.name || `${item.brand} ${item.articleNumber}`}</div>
{ </div>,
duration: 3000, {
icon: '🛒', duration: 3000,
} icon: <CartIcon size={20} color="#fff" />,
); }
);
} else {
// Показываем ошибку
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
}
} catch (error) { } catch (error) {
console.error('Ошибка добавления в корзину:', error); console.error('Ошибка добавления в корзину:', error);
toast.error('Ошибка при добавлении товара в корзину'); toast.error('Ошибка при добавлении товара в корзину');

View File

@ -42,7 +42,7 @@ const CartRecommendedProductCard: React.FC<CartRecommendedProductCardProps> = ({
<Link href="/cart" className="link-block-4-copy w-inline-block"> <Link href="/cart" className="link-block-4-copy w-inline-block">
<div className="div-block-25"> <div className="div-block-25">
<span className="icon-setting w-embed"> <span className="icon-setting w-embed">
<svg width="currentWidht" height="currentHight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="30" height="30" 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" /> <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> </svg>
</span> </span>

View File

@ -1,7 +1,8 @@
import React, { useState } from "react"; import React, { useState, useEffect } 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"; import toast from "react-hot-toast";
import CartIcon from "./CartIcon";
const INITIAL_OFFERS_LIMIT = 5; const INITIAL_OFFERS_LIMIT = 5;
@ -52,8 +53,16 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
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 }), {})
); );
const [inputValues, setInputValues] = useState<{ [key: number]: string }>(
offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {})
);
const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({}); const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({});
useEffect(() => {
setInputValues(offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {}));
setQuantities(offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {}));
}, [offers.length]);
const displayedOffers = offers.slice(0, visibleOffersCount); const displayedOffers = offers.slice(0, visibleOffersCount);
const hasMoreOffers = visibleOffersCount < offers.length; const hasMoreOffers = visibleOffersCount < offers.length;
@ -83,31 +92,44 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
return match ? parseInt(match[0]) : 0; return match ? parseInt(match[0]) : 0;
}; };
const handleQuantityInput = (index: number, value: string) => { const handleInputChange = (idx: number, val: string) => {
const offer = offers[index]; setInputValues(prev => ({ ...prev, [idx]: val }));
const availableStock = parseStock(offer.pcs); if (val === "") return;
let num = parseInt(value, 10); const valueNum = Math.max(1, parseInt(val, 10) || 1);
if (isNaN(num) || num < 1) num = 1; setQuantities(prev => ({ ...prev, [idx]: valueNum }));
if (num > availableStock) {
toast.error(`Максимум ${availableStock} шт.`);
return;
}
setQuantities(prev => ({ ...prev, [index]: num }));
}; };
const handleAddToCart = (offer: CoreProductCardOffer, index: number) => { const handleInputBlur = (idx: number) => {
if (inputValues[idx] === "") {
setInputValues(prev => ({ ...prev, [idx]: "1" }));
setQuantities(prev => ({ ...prev, [idx]: 1 }));
}
};
const handleMinus = (idx: number) => {
setQuantities(prev => {
const newVal = Math.max(1, (prev[idx] || 1) - 1);
setInputValues(vals => ({ ...vals, [idx]: newVal.toString() }));
return { ...prev, [idx]: newVal };
});
};
const handlePlus = (idx: number, maxCount?: number) => {
setQuantities(prev => {
let newVal = (prev[idx] || 1) + 1;
if (maxCount !== undefined) newVal = Math.min(newVal, maxCount);
setInputValues(vals => ({ ...vals, [idx]: newVal.toString() }));
return { ...prev, [idx]: newVal };
});
};
const handleAddToCart = async (offer: CoreProductCardOffer, index: number) => {
const quantity = quantities[index] || 1; const quantity = quantities[index] || 1;
const availableStock = parseStock(offer.pcs); const availableStock = parseStock(offer.pcs);
// Проверяем наличие
if (quantity > availableStock) {
toast.error(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
return;
}
const numericPrice = parsePrice(offer.price); const numericPrice = parsePrice(offer.price);
addItem({ const result = await addItem({
productId: offer.productId, productId: offer.productId,
offerKey: offer.offerKey, offerKey: offer.offerKey,
name: name, name: name,
@ -117,6 +139,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
price: numericPrice, price: numericPrice,
currency: offer.currency || 'RUB', currency: offer.currency || 'RUB',
quantity: quantity, quantity: quantity,
stock: availableStock, // передаем информацию о наличии
deliveryTime: parseDeliveryTime(offer.days), deliveryTime: parseDeliveryTime(offer.days),
warehouse: offer.warehouse || 'Склад', warehouse: offer.warehouse || 'Склад',
supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'), supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'),
@ -124,17 +147,22 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
image: image, image: image,
}); });
// Показываем тоастер вместо alert if (result.success) {
toast.success( // Показываем тоастер вместо alert
<div> toast.success(
<div className="font-semibold">Товар добавлен в корзину!</div> <div>
<div className="text-sm text-gray-600">{`${brand} ${article} (${quantity} шт.)`}</div> <div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
</div>, <div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${brand} ${article} (${quantity} шт.)`}</div>
{ </div>,
duration: 3000, {
icon: '🛒', duration: 3000,
} icon: <CartIcon size={20} color="#fff" />,
); }
);
} else {
// Показываем ошибку
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
}
}; };
// Обработчик клика по сердечку // Обработчик клика по сердечку
@ -291,6 +319,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
<div className="w-layout-vflex product-list-search-s1"> <div className="w-layout-vflex product-list-search-s1">
{displayedOffers.map((offer, idx) => { {displayedOffers.map((offer, idx) => {
const isLast = idx === displayedOffers.length - 1; const isLast = idx === displayedOffers.length - 1;
const maxCount = parseStock(offer.pcs);
return ( return (
<div <div
className="w-layout-hflex product-item-search-s1" className="w-layout-hflex product-item-search-s1"
@ -317,43 +346,48 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
<div className="w-layout-hflex add-to-cart-block-s1"> <div className="w-layout-hflex add-to-cart-block-s1">
<div className="w-layout-hflex flex-block-82"> <div className="w-layout-hflex flex-block-82">
<div className="w-layout-hflex pcs-cart-s1"> <div className="w-layout-hflex pcs-cart-s1">
<button <div
type="button"
className="minus-plus" className="minus-plus"
onClick={() => handleQuantityInput(idx, ((quantities[idx] || 1) - 1).toString())} onClick={() => handleMinus(idx)}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
aria-label="Уменьшить количество" aria-label="Уменьшить количество"
tabIndex={0}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleMinus(idx)}
role="button"
> >
<div className="pluspcs w-embed"> <div className="pluspcs w-embed">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor" />
</svg> </svg>
</div> </div>
</button> </div>
<div className="input-pcs"> <div className="input-pcs">
<input <input
type="number" type="number"
min={1} min={1}
max={parseStock(offer.pcs)} max={maxCount}
value={quantities[idx] || 1} value={inputValues[idx]}
onChange={e => handleQuantityInput(idx, e.target.value)} onChange={e => handleInputChange(idx, e.target.value)}
onBlur={() => handleInputBlur(idx)}
className="text-block-26 w-full text-center outline-none" className="text-block-26 w-full text-center outline-none"
aria-label="Количество" aria-label="Количество"
/> />
</div> </div>
<button <div
type="button"
className="minus-plus" className="minus-plus"
onClick={() => handleQuantityInput(idx, ((quantities[idx] || 1) + 1).toString())} onClick={() => handlePlus(idx, maxCount)}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
aria-label="Увеличить количество" aria-label="Увеличить количество"
tabIndex={0}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handlePlus(idx, maxCount)}
role="button"
> >
<div className="pluspcs w-embed"> <div className="pluspcs w-embed">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <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" /> <path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor" />
</svg> </svg>
</div> </div>
</button> </div>
</div> </div>
<button <button
type="button" type="button"

View File

@ -0,0 +1,25 @@
import React from 'react';
interface DeleteCartIconProps {
size?: number;
color?: string;
}
const DeleteCartIcon: React.FC<DeleteCartIconProps> = ({ size = 24, color = '#ec1c24' }) => {
return (
<svg
width={size}
height={size}
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={color}
/>
</svg>
);
};
export default DeleteCartIcon;

View File

@ -24,7 +24,7 @@ const Footer = () => (
<div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown"> <div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown">
<div className="dropdown-toggle-2 w-dropdown-toggle"> <div className="dropdown-toggle-2 w-dropdown-toggle">
<div className="text-block-17">Покупателям</div> <div className="text-block-17">Покупателям</div>
<div className="code-embed-10 w-embed"><svg width="currentwight" height="currentheight" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentcolor"></path></svg></div> <div className="code-embed-10 w-embed"><svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentColor"></path></svg></div>
</div> </div>
<nav className="dropdown-list-3 w-dropdown-list"> <nav className="dropdown-list-3 w-dropdown-list">
<a href="#" className="dropdown-link-2 w-dropdown-link">Оплата</a> <a href="#" className="dropdown-link-2 w-dropdown-link">Оплата</a>
@ -43,7 +43,7 @@ const Footer = () => (
<div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown"> <div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown">
<div className="dropdown-toggle-2 w-dropdown-toggle"> <div className="dropdown-toggle-2 w-dropdown-toggle">
<div className="text-block-17">Сотрудничество</div> <div className="text-block-17">Сотрудничество</div>
<div className="code-embed-10 w-embed"><svg width="currentwight" height="currentheight" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentcolor"></path></svg></div> <div className="code-embed-10 w-embed"><svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentColor"></path></svg></div>
</div> </div>
<nav className="dropdown-list-3 w-dropdown-list"> <nav className="dropdown-list-3 w-dropdown-list">
<a href="#" className="dropdown-link-2 w-dropdown-link">Поставщикам</a> <a href="#" className="dropdown-link-2 w-dropdown-link">Поставщикам</a>
@ -62,7 +62,7 @@ const Footer = () => (
<div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown"> <div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown">
<div className="dropdown-toggle-2 w-dropdown-toggle"> <div className="dropdown-toggle-2 w-dropdown-toggle">
<div className="text-block-17">PROTEK</div> <div className="text-block-17">PROTEK</div>
<div className="code-embed-10 w-embed"><svg width="currentwight" height="currentheight" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentcolor"></path></svg></div> <div className="code-embed-10 w-embed"><svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentColor"></path></svg></div>
</div> </div>
<nav className="dropdown-list-3 w-dropdown-list"> <nav className="dropdown-list-3 w-dropdown-list">
<a href="#" className="dropdown-link-2 w-dropdown-link">Вакансии</a> <a href="#" className="dropdown-link-2 w-dropdown-link">Вакансии</a>
@ -81,7 +81,7 @@ const Footer = () => (
<div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown"> <div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown">
<div className="dropdown-toggle-2 w-dropdown-toggle"> <div className="dropdown-toggle-2 w-dropdown-toggle">
<div className="text-block-17">Оферта</div> <div className="text-block-17">Оферта</div>
<div className="code-embed-10 w-embed"><svg width="currentwight" height="currentheight" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentcolor"></path></svg></div> <div className="code-embed-10 w-embed"><svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M2 6.74036L3.28446 5.5L9 11.0193L14.7155 5.5L16 6.74036L9 13.5L2 6.74036Z" fill="currentColor"></path></svg></div>
</div> </div>
<nav className="dropdown-list-3 w-dropdown-list"> <nav className="dropdown-list-3 w-dropdown-list">
<a href="#" className="dropdown-link-2 w-dropdown-link">Поставщикам</a> <a href="#" className="dropdown-link-2 w-dropdown-link">Поставщикам</a>

View File

@ -37,10 +37,8 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
// Если мы находимся на странице search-result, восстанавливаем поисковый запрос // Если мы находимся на странице search-result, восстанавливаем поисковый запрос
if (router.pathname === '/search-result') { if (router.pathname === '/search-result') {
const { article, brand } = router.query; const { article, brand } = router.query;
if (article && brand && typeof article === 'string' && typeof brand === 'string') { if (article && typeof article === 'string') {
// Формируем поисковый запрос из артикула и бренда // Отображаем только артикул, без бренда
setSearchQuery(`${brand} ${article}`);
} else if (article && typeof article === 'string') {
setSearchQuery(article); setSearchQuery(article);
} }
} }
@ -375,7 +373,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
<div className="w-layout-hflex flex-block-2"> <div className="w-layout-hflex flex-block-2">
<div className="w-layout-hflex flex-block-3"> <div className="w-layout-hflex flex-block-3">
<div className="w-layout-hflex flex-block-77-copy"> <div className="w-layout-hflex flex-block-77-copy">
<div className="code-embed-4 w-embed"><svg width="currentwidth" height="currenthight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.51667 8.99167C6.71667 11.35 8.65 13.275 11.0083 14.4833L12.8417 12.65C13.0667 12.425 13.4 12.35 13.6917 12.45C14.625 12.7583 15.6333 12.925 16.6667 12.925C17.125 12.925 17.5 13.3 17.5 13.7583V16.6667C17.5 17.125 17.125 17.5 16.6667 17.5C8.84167 17.5 2.5 11.1583 2.5 3.33333C2.5 2.875 2.875 2.5 3.33333 2.5H6.25C6.70833 2.5 7.08333 2.875 7.08333 3.33333C7.08333 4.375 7.25 5.375 7.55833 6.30833C7.65 6.6 7.58333 6.925 7.35 7.15833L5.51667 8.99167Z" fill="currentColor" /></svg></div> <div className="code-embed-4 w-embed"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.51667 8.99167C6.71667 11.35 8.65 13.275 11.0083 14.4833L12.8417 12.65C13.0667 12.425 13.4 12.35 13.6917 12.45C14.625 12.7583 15.6333 12.925 16.6667 12.925C17.125 12.925 17.5 13.3 17.5 13.7583V16.6667C17.5 17.125 17.125 17.5 16.6667 17.5C8.84167 17.5 2.5 11.1583 2.5 3.33333C2.5 2.875 2.875 2.5 3.33333 2.5H6.25C6.70833 2.5 7.08333 2.875 7.08333 3.33333C7.08333 4.375 7.25 5.375 7.55833 6.30833C7.65 6.6 7.58333 6.925 7.35 7.15833L5.51667 8.99167Z" fill="currentColor" /></svg></div>
<div className="phone-copy">+7 (495) 260-20-60</div> <div className="phone-copy">+7 (495) 260-20-60</div>
</div> </div>
</div> </div>
@ -393,7 +391,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
onClick={() => setMenuOpen((open) => !open)} onClick={() => setMenuOpen((open) => !open)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<div className="code-embed-5 w-embed"><svg width="currentwidth" height="currenthight" viewBox="0 0 30 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <div className="code-embed-5 w-embed"><svg width="30" height="18" viewBox="0 0 30 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0H30V3H0V0Z" fill="currentColor"></path> <path d="M0 0H30V3H0V0Z" fill="currentColor"></path>
<path d="M0 7.5H30V10.5H0V7.5Z" fill="currentColor"></path> <path d="M0 7.5H30V10.5H0V7.5Z" fill="currentColor"></path>
<path d="M0 15H30V18H0V15Z" fill="currentColor"></path> <path d="M0 15H30V18H0V15Z" fill="currentColor"></path>

View File

@ -59,19 +59,13 @@ const ProductListCard: React.FC<ProductListCardProps> = ({
return match ? parseInt(match[0]) : 0; return match ? parseInt(match[0]) : 0;
}; };
const handleAddToCart = () => { const handleAddToCart = async () => {
const availableStock = parseStock(stock); const availableStock = parseStock(stock);
// Проверяем наличие
if (count > availableStock) {
alert(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
return;
}
const numericPrice = parsePrice(price); const numericPrice = parsePrice(price);
const numericOldPrice = oldPrice ? parsePrice(oldPrice) : undefined; const numericOldPrice = oldPrice ? parsePrice(oldPrice) : undefined;
addItem({ const result = await addItem({
productId: productId, productId: productId,
offerKey: offerKey, offerKey: offerKey,
name: title, name: title,
@ -81,6 +75,7 @@ const ProductListCard: React.FC<ProductListCardProps> = ({
originalPrice: numericOldPrice, originalPrice: numericOldPrice,
currency: currency, currency: currency,
quantity: count, quantity: count,
stock: availableStock, // передаем информацию о наличии
deliveryTime: deliveryTime || delivery, deliveryTime: deliveryTime || delivery,
warehouse: warehouse || address, warehouse: warehouse || address,
supplier: supplier, supplier: supplier,
@ -88,8 +83,13 @@ const ProductListCard: React.FC<ProductListCardProps> = ({
image: image, image: image,
}); });
// Показываем уведомление о добавлении if (result.success) {
alert(`Товар "${title}" добавлен в корзину (${count} шт.)`); // Показываем уведомление о добавлении
alert(`Товар "${title}" добавлен в корзину (${count} шт.)`);
} else {
// Показываем ошибку
alert(result.error || 'Ошибка при добавлении товара в корзину');
}
}; };
return ( return (

View File

@ -156,10 +156,15 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
}; };
const handleUnitClick = (unit: LaximoUnit) => { const handleUnitClick = (unit: LaximoUnit) => {
setSelectedUnit({ // ИСПРАВЛЕНИЕ: Сохраняем SSD узла из API ответа
...unit, console.log('🔍 handleUnitClick - сохраняем узел с SSD:', {
ssd: unit.ssd || ssd // Сохраняем правильный SSD в selectedUnit unitId: unit.unitid,
unitName: unit.name,
unitSsd: unit.ssd ? `${unit.ssd.substring(0, 50)}...` : 'отсутствует',
unitSsdLength: unit.ssd?.length
}); });
setSelectedUnit(unit); // Сохраняем полный объект узла с его SSD
}; };
const handleBackFromUnit = () => { const handleBackFromUnit = () => {
@ -209,21 +214,23 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
// Если выбран узел для детального просмотра, показываем UnitDetailsSection // Если выбран узел для детального просмотра, показываем UnitDetailsSection
if (selectedUnit) { if (selectedUnit) {
const unitSsd = selectedUnit.ssd || ssd; // ИСПРАВЛЕНИЕ: Используем SSD узла из API ответа, а не родительский SSD
// API Laximo возвращает для каждого узла свой собственный SSD
console.log('🔍 QuickDetailSection передает в UnitDetailsSection:', { console.log('🔍 QuickDetailSection передает в UnitDetailsSection:', {
unitSsd: unitSsd ? `${unitSsd.substring(0, 50)}...` : 'отсутствует', parentSsd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
unitSsdLength: unitSsd?.length, parentSsdLength: ssd?.length,
selectedUnitSsd: selectedUnit.ssd ? `${selectedUnit.ssd.substring(0, 50)}...` : 'отсутствует', selectedUnitSsd: selectedUnit.ssd ? `${selectedUnit.ssd.substring(0, 50)}...` : 'отсутствует',
fallbackSsd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует', selectedUnitSsdLength: selectedUnit.ssd?.length,
unitId: selectedUnit.unitid, unitId: selectedUnit.unitid,
unitName: selectedUnit.name unitName: selectedUnit.name,
note: 'Используем SSD УЗЛА из API ответа'
}); });
return ( return (
<UnitDetailsSection <UnitDetailsSection
catalogCode={catalogCode} catalogCode={catalogCode}
vehicleId={vehicleId} vehicleId={vehicleId}
ssd={unitSsd} // Используем SSD узла ssd={selectedUnit.ssd || ssd} // Используем SSD узла, fallback на родительский SSD
unitId={selectedUnit.unitid} unitId={selectedUnit.unitid}
unitName={selectedUnit.name} unitName={selectedUnit.name}
onBack={handleBackFromUnit} onBack={handleBackFromUnit}

View File

@ -42,7 +42,8 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует', ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
ssdLength: ssd?.length, ssdLength: ssd?.length,
unitId, unitId,
unitName unitName,
note: 'Используем SSD узла для API запросов'
}); });
const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery<{ laximoUnitInfo: LaximoUnitInfo }>( const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery<{ laximoUnitInfo: LaximoUnitInfo }>(
@ -52,11 +53,11 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
catalogCode, catalogCode,
vehicleId, vehicleId,
unitId, unitId,
ssd: ssd || '' ssd
}, },
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId || !ssd || ssd.trim() === '',
errorPolicy: 'all', errorPolicy: 'all',
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуальных данных
notifyOnNetworkStatusChange: true notifyOnNetworkStatusChange: true
} }
); );
@ -76,9 +77,9 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
catalogCode, catalogCode,
vehicleId, vehicleId,
unitId, unitId,
ssd: ssd || '' ssd
}, },
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId || !ssd || ssd.trim() === '',
errorPolicy: 'all', errorPolicy: 'all',
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD
notifyOnNetworkStatusChange: true notifyOnNetworkStatusChange: true
@ -100,9 +101,9 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
catalogCode, catalogCode,
vehicleId, vehicleId,
unitId, unitId,
ssd: ssd || '' ssd
}, },
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId || !ssd || ssd.trim() === '',
errorPolicy: 'all', errorPolicy: 'all',
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD
notifyOnNetworkStatusChange: true notifyOnNetworkStatusChange: true

View File

@ -68,49 +68,49 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
Найдено автомобилей: {results.length} Найдено автомобилей: {results.length}
</h3> </h3>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="flex flex-wrap flex-1 gap-5 size-full max-md:max-w-full">
{results.map((vehicle, index) => ( {results.map((vehicle, index) => (
<div <div
key={`${vehicle.vehicleid}-${index}`} key={`${vehicle.vehicleid}-${index}`}
className="bg-white rounded-lg shadow-md border border-gray-200 p-4 hover:shadow-lg transition-shadow cursor-pointer" className="flex flex-col flex-1 shrink p-8 bg-white rounded-lg border border-solid basis-0 border-stone-300 max-w-[504px] md:min-w-[370px] sm:min-w-[340px] min-w-[200px] max-md:px-5 cursor-pointer transition-shadow hover:shadow-lg"
onClick={() => handleSelectVehicle(vehicle)} onClick={() => handleSelectVehicle(vehicle)}
> >
{/* Заголовок автомобиля */} {/* Заголовок автомобиля */}
<div className="mb-3"> <div className="">
<h4 className="text-lg font-semibold text-blue-600 mb-1"> <h4 className="text-lg font-semibold text-red-600 mb-1 truncate">
{vehicle.name || `${vehicle.brand} ${vehicle.model}`} {vehicle.name || `${vehicle.brand} ${vehicle.model}`}
</h4> </h4>
<p className="text-sm text-gray-500"> {/* <p className="text-sm text-gray-500 truncate">
{vehicle.modification} ({vehicle.year}) {vehicle.modification} ({vehicle.year})
</p> </p> */}
</div> </div>
{/* Основные характеристики */} {/* Основные характеристики */}
<div className="space-y-1 mb-4"> <div className="space-y-1 mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-2">Основные характеристики</h5> <h5 className="text-base font-semibold text-gray-900 mb-2">Основные характеристики</h5>
{renderAttribute('Марка', vehicle.brand)} {renderAttribute('Марка', vehicle.brand)}
{renderAttribute('Модель', vehicle.model)} {renderAttribute('Модель', vehicle.model)}
{renderAttribute('Двигатель', vehicle.engine)} {renderAttribute('Двигатель', vehicle.engine)}
</div> </div>
{/* Все атрибуты из API */} {/* Все атрибуты из API */}
{vehicle.attributes && vehicle.attributes.length > 0 && ( {vehicle.attributes && vehicle.attributes.length > 0 && (
<div className="space-y-1 mb-4"> <div className="space-y-1 mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-2">Дополнительные характеристики</h5> <h5 className="text-base font-semibold text-gray-900 mb-2">Дополнительные характеристики</h5>
{vehicle.attributes.map((attr, attrIndex) => ( {vehicle.attributes.map((attr, attrIndex) => (
<div key={attrIndex} className="flex justify-between py-1 border-b border-gray-100"> <div key={attrIndex} className="flex justify-between py-1 border-b border-gray-100">
<span className="text-sm text-gray-600 font-medium">{attr.name || attr.key}:</span> <span className="text-sm text-gray-600 font-medium">{attr.name || attr.key}:</span>
<span className="text-sm text-gray-900">{attr.value}</span> <span className="text-sm text-gray-900">{attr.value}</span>
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Технические характеристики (fallback для старых данных) */} {/* Технические характеристики (fallback для старых данных) */}
{(!vehicle.attributes || vehicle.attributes.length === 0) && ( {(!vehicle.attributes || vehicle.attributes.length === 0) && (
<> <>
<div className="space-y-1 mb-4"> <div className="space-y-1 mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-2">Дополнительные характеристики</h5> <h5 className="text-base font-semibold text-gray-900 mb-2">Дополнительные характеристики</h5>
{renderAttribute('Год', vehicle.year)} {renderAttribute('Год', vehicle.year)}
{renderAttribute('Кузов', vehicle.bodytype)} {renderAttribute('Кузов', vehicle.bodytype)}
{renderAttribute('Трансмиссия', vehicle.transmission)} {renderAttribute('Трансмиссия', vehicle.transmission)}
@ -123,7 +123,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
</div> </div>
<div className="space-y-1 mb-4"> <div className="space-y-1 mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-2">Технические характеристики</h5> <h5 className="text-base font-semibold text-gray-900 mb-2">Технические характеристики</h5>
{renderAttribute('Информация о двигателе', vehicle.engine_info)} {renderAttribute('Информация о двигателе', vehicle.engine_info)}
{renderAttribute('Номер двигателя', vehicle.engineno)} {renderAttribute('Номер двигателя', vehicle.engineno)}
{renderAttribute('Дата производства', vehicle.date)} {renderAttribute('Дата производства', vehicle.date)}
@ -133,7 +133,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
</div> </div>
<div className="space-y-1 mb-4"> <div className="space-y-1 mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-2">Даты и периоды</h5> <h5 className="text-base font-semibold text-gray-900 mb-2">Даты и периоды</h5>
{renderAttribute('Дата с', vehicle.datefrom)} {renderAttribute('Дата с', vehicle.datefrom)}
{renderAttribute('Дата по', vehicle.dateto)} {renderAttribute('Дата по', vehicle.dateto)}
{renderAttribute('Модельный год с', vehicle.modelyearfrom)} {renderAttribute('Модельный год с', vehicle.modelyearfrom)}
@ -143,7 +143,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
{/* Опции и описание */} {/* Опции и описание */}
{(vehicle.options || vehicle.description || vehicle.notes) && ( {(vehicle.options || vehicle.description || vehicle.notes) && (
<div className="space-y-1 mb-4"> <div className="space-y-1 mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-2">Опции и описание</h5> <h5 className="text-base font-semibold text-gray-900 mb-2">Опции и описание</h5>
{renderAttribute('Опции', vehicle.options)} {renderAttribute('Опции', vehicle.options)}
{renderAttribute('Описание', vehicle.description)} {renderAttribute('Описание', vehicle.description)}
{renderAttribute('Примечания', vehicle.notes)} {renderAttribute('Примечания', vehicle.notes)}
@ -153,25 +153,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
)} )}
{/* Системная информация */} {/* Системная информация */}
<div className="mt-4 pt-3 border-t border-gray-200">
<div className="text-xs text-gray-400 space-y-1">
<div>ID: {vehicle.vehicleid}</div>
{vehicle.catalog && <div>Каталог: {vehicle.catalog}</div>}
{vehicle.ssd && (
<div>SSD: {vehicle.ssd.length > 50 ? `${vehicle.ssd.substring(0, 50)}...` : vehicle.ssd}</div>
)}
</div>
</div>
{/* Debug информация (только в development) */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-4 p-2 bg-gray-100 rounded text-xs">
<div className="font-semibold text-gray-700 mb-1">Debug Info:</div>
<pre className="text-gray-600 whitespace-pre-wrap">
{JSON.stringify(vehicle, null, 2)}
</pre>
</div>
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -26,6 +26,11 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
const [getWizard2] = useLazyQuery(GET_LAXIMO_WIZARD2, { const [getWizard2] = useLazyQuery(GET_LAXIMO_WIZARD2, {
onCompleted: (data) => { onCompleted: (data) => {
if (data.laximoWizard2) { if (data.laximoWizard2) {
console.log('🔄 Wizard обновлен:', {
steps: data.laximoWizard2.length,
selectedParams: Object.keys(selectedParams).length,
currentSsd
});
setWizardSteps(data.laximoWizard2); setWizardSteps(data.laximoWizard2);
setIsLoading(false); setIsLoading(false);
} }
@ -76,18 +81,28 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
// --- Автовыбор единственного варианта для всех шагов --- // --- Автовыбор единственного варианта для всех шагов ---
React.useEffect(() => { React.useEffect(() => {
// Предотвращаем автовыбор во время загрузки
if (isLoading) return;
wizardSteps.forEach(step => { wizardSteps.forEach(step => {
const options = step.options || []; const options = step.options || [];
const selectedKey = selectedParams[step.conditionid]?.key || (step.determined ? options.find(o => o.value === step.value)?.key : ''); const selectedKey = selectedParams[step.conditionid]?.key || (step.determined ? options.find(o => o.value === step.value)?.key : '');
if (options.length === 1 && selectedKey !== options[0].key) {
// Автовыбираем только если есть единственный вариант и он еще не выбран
if (options.length === 1 && selectedKey !== options[0].key && !selectedParams[step.conditionid]) {
handleParamSelect(step, options[0].key, options[0].value); handleParamSelect(step, options[0].key, options[0].value);
} }
}); });
// eslint-disable-next-line // eslint-disable-next-line
}, [wizardSteps, selectedParams]); }, [wizardSteps, selectedParams, isLoading]);
// Обработка выбора параметра // Обработка выбора параметра
const handleParamSelect = async (step: LaximoWizardStep, optionKey: string, optionValue: string) => { const handleParamSelect = async (step: LaximoWizardStep, optionKey: string, optionValue: string) => {
// Проверяем, не выбран ли уже этот параметр
if (selectedParams[step.conditionid]?.key === optionKey) {
return;
}
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -118,6 +133,13 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
// Сброс параметра // Сброс параметра
const handleParamReset = async (step: LaximoWizardStep) => { const handleParamReset = async (step: LaximoWizardStep) => {
console.log('🔄 Сброс параметра:', {
stepName: step.name,
conditionId: step.conditionid,
currentSsd,
selectedParamsBefore: Object.keys(selectedParams)
});
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
@ -126,8 +148,33 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
delete newSelectedParams[step.conditionid]; delete newSelectedParams[step.conditionid];
setSelectedParams(newSelectedParams); setSelectedParams(newSelectedParams);
// Используем SSD для сброса параметра, если он есть // Находим правильный SSD для сброса этого параметра
const resetSsd = step.ssd || ''; // Нужно найти SSD, который соответствует состоянию до выбора этого параметра
let resetSsd = '';
// Ищем среди шагов wizard тот, который имеет правильный SSD для восстановления
const currentStepIndex = wizardSteps.findIndex(s => s.conditionid === step.conditionid);
// Если есть предыдущие шаги с выбранными параметрами, используем их SSD
for (let i = currentStepIndex - 1; i >= 0; i--) {
const prevStep = wizardSteps[i];
if (newSelectedParams[prevStep.conditionid]) {
resetSsd = newSelectedParams[prevStep.conditionid].key;
break;
}
}
// Если не нашли предыдущий SSD, используем step.ssd или пустую строку
if (!resetSsd) {
resetSsd = step.ssd || '';
}
console.log('🔄 Новый SSD для сброса:', {
resetSsd,
selectedParamsAfter: Object.keys(newSelectedParams),
stepSsd: step.ssd
});
setCurrentSsd(resetSsd); setCurrentSsd(resetSsd);
try { try {
@ -325,36 +372,36 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
</div> </div>
)} )}
{/* Кнопка поиска автомобилей */} {/* Информация о недостаточности параметров и кнопка поиска */}
{!isLoading && canListVehicles && showSearchButton && ( {!isLoading && wizardSteps.length > 0 && (
<div className="pt-4 border-t"> <div className="flex flex-row gap-4 items-center w-full mx-auto max-sm:flex-col max-sm:items-stretch">
<button <button
onClick={() => { onClick={() => {
handleFindVehicles(); handleFindVehicles();
setShowSearchButton(false); setShowSearchButton(false);
}} }}
disabled={isLoading} disabled={!canListVehicles || isLoading}
className="w-full sm:w-auto px-8 py-3 bg-red-600 !text-white font-medium rounded-lg shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center" className="w-full sm:w-auto px-8 py-3 bg-red-600 !text-white font-medium rounded-lg shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center max-sm:w-full"
style={{ minWidth: 180 }}
> >
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> Найти
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
Найти автомобили
</button> </button>
<div className="mt-3 text-sm text-gray-600"> <div
Определено параметров: {wizardSteps.filter(s => s.determined).length} из {wizardSteps.length} layer-name="Выберите больше параметров для поиска автомобилей"
className="box-border inline-flex gap-5 items-center px-10 py-4 rounded-xl bg-slate-50 h-[52px] max-md:px-8 max-md:py-3.5 max-md:w-full max-md:h-auto max-md:max-w-[524px] max-md:min-h-[52px] max-sm:gap-3 max-sm:px-5 max-sm:py-3 max-sm:w-full max-sm:rounded-lg max-sm:justify-center"
>
<div>
<img src="/images/info.svg" alt="info" style={{ width: 18, height: 20, flexShrink: 0 }} />
</div>
<div
layer-name="Выберите больше параметров для поиска автомобилей"
className="relative text-base font-medium leading-5 text-center text-gray-950 max-md:text-sm max-sm:text-sm max-sm:leading-4 max-sm:text-center"
>
Выберите больше параметров для поиска автомобилей
</div>
</div> </div>
</div> </div>
)} )}
{/* Информация о недостаточности параметров */}
{!isLoading && !canListVehicles && wizardSteps.length > 0 && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-blue-800 text-sm">
Выберите больше параметров для поиска автомобилей
</p>
</div>
)}
</div> </div>
); );
}; };

View File

@ -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 { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import CartIcon from "../CartIcon";
interface ProductBuyBlockProps { interface ProductBuyBlockProps {
offer?: any; offer?: any;
@ -37,7 +38,7 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
} }
// Добавляем товар в корзину // Добавляем товар в корзину
addItem({ const result = await addItem({
productId: offer.id ? String(offer.id) : undefined, productId: offer.id ? String(offer.id) : undefined,
offerKey: offer.offerKey || undefined, offerKey: offer.offerKey || undefined,
name: offer.name || `${offer.brand} ${offer.articleNumber}`, name: offer.name || `${offer.brand} ${offer.articleNumber}`,
@ -45,6 +46,7 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
price: offer.price, price: offer.price,
currency: 'RUB', currency: 'RUB',
quantity: quantity, quantity: quantity,
stock: offer.quantity, // передаем информацию о наличии
image: offer.image || undefined, image: offer.image || undefined,
brand: offer.brand, brand: offer.brand,
article: offer.articleNumber, article: offer.articleNumber,
@ -53,17 +55,22 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
isExternal: offer.type === 'external' isExternal: offer.type === 'external'
}); });
// Показываем успешный тоастер if (result.success) {
toast.success( // Показываем успешный тоастер
<div> toast.success(
<div className="font-semibold">Товар добавлен в корзину!</div> <div>
<div className="text-sm text-gray-600">{offer.name || `${offer.brand} ${offer.articleNumber}`}</div> <div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
</div>, <div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{offer.name || `${offer.brand} ${offer.articleNumber}`}</div>
{ </div>,
duration: 3000, {
icon: '🛒', duration: 3000,
} icon: <CartIcon size={20} color="#fff" />,
); }
);
} else {
// Показываем ошибку
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
}
} catch (error) { } catch (error) {
console.error('Ошибка добавления в корзину:', error); console.error('Ошибка добавления в корзину:', error);
toast.error('Ошибка при добавлении товара в корзину'); toast.error('Ошибка при добавлении товара в корзину');

View File

@ -15,8 +15,10 @@ const DEFAULT_MAX = 32000;
const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(v, max)); const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(v, max));
const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max = DEFAULT_MAX, isMobile = false, value = null, onChange }) => { const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max = DEFAULT_MAX, isMobile = false, value = null, onChange }) => {
const [from, setFrom] = useState(value ? value[0] : min); const [from, setFrom] = useState<string>(value ? String(value[0]) : String(min));
const [to, setTo] = useState(value ? value[1] : max); const [to, setTo] = useState<string>(value ? String(value[1]) : String(max));
const [confirmedFrom, setConfirmedFrom] = useState<number>(value ? value[0] : min);
const [confirmedTo, setConfirmedTo] = useState<number>(value ? value[1] : max);
const [dragging, setDragging] = useState<null | "from" | "to">(null); const [dragging, setDragging] = useState<null | "from" | "to">(null);
const [trackWidth, setTrackWidth] = useState(0); const [trackWidth, setTrackWidth] = useState(0);
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@ -25,11 +27,15 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
// Обновляем локальное состояние при изменении внешнего значения // Обновляем локальное состояние при изменении внешнего значения
useEffect(() => { useEffect(() => {
if (value) { if (value) {
setFrom(value[0]); setFrom(String(value[0]));
setTo(value[1]); setTo(String(value[1]));
setConfirmedFrom(value[0]);
setConfirmedTo(value[1]);
} else { } else {
setFrom(min); setFrom(String(min));
setTo(max); setTo(String(max));
setConfirmedFrom(min);
setConfirmedTo(max);
} }
}, [value, min, max]); }, [value, min, max]);
@ -61,15 +67,15 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
x = clamp(x, 0, trackWidth); x = clamp(x, 0, trackWidth);
const value = clamp(pxToValue(x), min, max); const value = clamp(pxToValue(x), min, max);
if (dragging === "from") { if (dragging === "from") {
setFrom(v => clamp(Math.min(value, to), min, to)); setFrom(v => String(clamp(Math.min(value, Number(to)), min, Number(to))));
} else { } else {
setTo(v => clamp(Math.max(value, from), from, max)); setTo(v => String(clamp(Math.max(value, Number(from)), Number(from), max)));
} }
}; };
const onUp = () => { const onUp = () => {
setDragging(null); setDragging(null);
if (onChange) { if (onChange) {
onChange([from, to]); onChange([Number(from), Number(to)]);
} }
}; };
window.addEventListener("mousemove", onMove); window.addEventListener("mousemove", onMove);
@ -82,25 +88,48 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
// Input handlers // Input handlers
const handleFromInput = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFromInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = Number(e.target.value.replace(/\D/g, "")); let v = e.target.value.replace(/\D/g, "");
if (isNaN(v)) v = min; setFrom(v);
setFrom(clamp(Math.min(v, to), min, to));
}; };
const handleToInput = (e: React.ChangeEvent<HTMLInputElement>) => { const handleToInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = Number(e.target.value.replace(/\D/g, "")); let v = e.target.value.replace(/\D/g, "");
if (isNaN(v)) v = max; setTo(v);
setTo(clamp(Math.max(v, from), from, max));
}; };
const handleInputBlur = () => { const handleFromBlur = () => {
if (onChange) { let v = Number(from);
onChange([from, to]); if (isNaN(v) || v < min) v = min;
// если больше max — оставлять как есть
setFrom(String(v));
if (onChange) onChange([v, to === "" ? max : Number(to)]);
setConfirmedFrom(v);
};
const handleToBlur = () => {
let v = Number(to);
if (isNaN(v) || v < min) v = min;
if (v > max) v = max;
setTo(String(v));
if (onChange) onChange([from === "" ? min : Number(from), v]);
setConfirmedTo(v);
};
const handleFromKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleFromBlur();
(e.target as HTMLInputElement).blur();
}
};
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleToBlur();
(e.target as HTMLInputElement).blur();
} }
}; };
// px позиции для точек // px позиции для точек
const pxFrom = valueToPx(from); const pxFrom = valueToPx(dragging ? Number(from) : confirmedFrom);
const pxTo = valueToPx(to); const pxTo = valueToPx(dragging ? Number(to) : confirmedTo);
// Мобильная версия - без dropdown // Мобильная версия - без dropdown
if (isMobile) { if (isMobile) {
@ -124,7 +153,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
id="from" id="from"
value={from} value={from}
onChange={handleFromInput} onChange={handleFromInput}
onBlur={handleInputBlur} onBlur={handleFromBlur}
onKeyDown={handleFromKeyDown}
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }} style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
/> />
</div> </div>
@ -139,7 +169,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
id="to" id="to"
value={to} value={to}
onChange={handleToInput} onChange={handleToInput}
onBlur={handleInputBlur} onBlur={handleToBlur}
onKeyDown={handleToKeyDown}
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }} style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
/> />
</div> </div>
@ -214,7 +245,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
id="from" id="from"
value={from} value={from}
onChange={handleFromInput} onChange={handleFromInput}
onBlur={handleInputBlur} onBlur={handleFromBlur}
onKeyDown={handleFromKeyDown}
/> />
</div> </div>
<div className="div-block-5"> <div className="div-block-5">
@ -228,7 +260,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
id="to" id="to"
value={to} value={to}
onChange={handleToInput} onChange={handleToInput}
onBlur={handleInputBlur} onBlur={handleToBlur}
onKeyDown={handleToKeyDown}
/> />
</div> </div>
</form> </form>

View File

@ -43,30 +43,72 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
const router = useRouter(); const router = useRouter();
// Получаем инфо об узле (для картинки) // Получаем инфо об узле (для картинки)
console.log('🔍 KnotIn - GET_LAXIMO_UNIT_INFO запрос:', {
catalogCode,
vehicleId,
unitId,
ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
ssdLength: ssd?.length,
skipCondition: !catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === ''
});
const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery( const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery(
GET_LAXIMO_UNIT_INFO, GET_LAXIMO_UNIT_INFO,
{ {
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', unitId: unitId || '', ssd: ssd || '' }, variables: {
skip: !catalogCode || !vehicleId || !unitId, catalogCode,
vehicleId,
unitId,
ssd
},
skip: !catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === '',
errorPolicy: 'all', errorPolicy: 'all',
} }
); );
// Получаем карту координат // Получаем карту координат
console.log('🔍 KnotIn - GET_LAXIMO_UNIT_IMAGE_MAP запрос:', {
catalogCode,
vehicleId,
unitId,
ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
ssdLength: ssd?.length,
skipCondition: !catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === ''
});
const { data: imageMapData, loading: imageMapLoading, error: imageMapError } = useQuery( const { data: imageMapData, loading: imageMapLoading, error: imageMapError } = useQuery(
GET_LAXIMO_UNIT_IMAGE_MAP, GET_LAXIMO_UNIT_IMAGE_MAP,
{ {
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', unitId: unitId || '', ssd: ssd || '' }, variables: {
skip: !catalogCode || !vehicleId || !unitId, catalogCode,
vehicleId,
unitId,
ssd
},
skip: !catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === '',
errorPolicy: 'all', errorPolicy: 'all',
} }
); );
// Если нет необходимых данных, показываем заглушку // Если нет необходимых данных, показываем заглушку
if (!catalogCode || !vehicleId || !unitId) { if (!catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === '') {
console.log('⚠️ KnotIn: отсутствуют необходимые данные:', {
catalogCode: !!catalogCode,
vehicleId: !!vehicleId,
unitId: !!unitId,
ssd: !!ssd,
ssdValid: ssd ? ssd.trim() !== '' : false
});
return ( return (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
<div className="text-lg font-medium mb-2">Схема узла</div> <div className="text-lg font-medium mb-2">Схема узла</div>
<div className="text-sm">Выберите узел для отображения схемы</div> <div className="text-sm">Выберите узел для отображения схемы</div>
{process.env.NODE_ENV === 'development' && (
<div className="text-xs text-red-500 mt-2">
Debug: catalogCode={catalogCode}, vehicleId={vehicleId}, unitId={unitId}, ssd={ssd ? 'есть' : 'нет'}
</div>
)}
</div> </div>
); );
} }
@ -75,6 +117,29 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
const coordinates = imageMapData?.laximoUnitImageMap?.coordinates || []; const coordinates = imageMapData?.laximoUnitImageMap?.coordinates || [];
const imageUrl = unitInfo?.imageurl ? getImageUrl(unitInfo.imageurl, selectedImageSize) : ''; const imageUrl = unitInfo?.imageurl ? getImageUrl(unitInfo.imageurl, selectedImageSize) : '';
// Логируем успешную загрузку данных
React.useEffect(() => {
if (unitInfo) {
console.log('✅ KnotIn: данные узла загружены:', {
unitName: unitInfo.name,
hasImage: !!unitInfo.imageurl,
imageUrl: unitInfo.imageurl,
processedImageUrl: imageUrl
});
}
}, [unitInfo, imageUrl]);
React.useEffect(() => {
if (coordinates.length > 0) {
console.log('✅ KnotIn: координаты карты загружены:', {
coordinatesCount: coordinates.length,
firstCoordinate: coordinates[0]
});
} else if (imageMapData) {
console.log('⚠️ KnotIn: карта изображений загружена, но координаты пустые:', imageMapData);
}
}, [coordinates, imageMapData]);
// Масштабируем точки после загрузки картинки // Масштабируем точки после загрузки картинки
const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => { const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
const img = e.currentTarget; const img = e.currentTarget;
@ -110,13 +175,49 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
}, [parts, coordinates]); }, [parts, coordinates]);
if (unitInfoLoading || imageMapLoading) { if (unitInfoLoading || imageMapLoading) {
console.log('🔄 KnotIn: загрузка данных...', {
unitInfoLoading,
imageMapLoading,
unitInfoError: unitInfoError?.message,
imageMapError: imageMapError?.message
});
return <div className="text-center py-8 text-gray-500">Загружаем схему узла...</div>; return <div className="text-center py-8 text-gray-500">Загружаем схему узла...</div>;
} }
if (unitInfoError) { if (unitInfoError) {
return <div className="text-center py-8 text-red-600">Ошибка загрузки схемы: {unitInfoError.message}</div>; console.error('❌ KnotIn: ошибка загрузки информации об узле:', unitInfoError);
return (
<div className="text-center py-8 text-red-600">
Ошибка загрузки схемы: {unitInfoError.message}
{process.env.NODE_ENV === 'development' && (
<div className="text-xs mt-2 text-gray-500">
GraphQL Error: {JSON.stringify(unitInfoError, null, 2)}
</div>
)}
</div>
);
} }
if (imageMapError) {
console.error('❌ KnotIn: ошибка загрузки карты изображений:', imageMapError);
}
if (!imageUrl) { if (!imageUrl) {
return <div className="text-center py-8 text-gray-400">Нет изображения для этого узла</div>; console.log('⚠️ KnotIn: нет URL изображения:', {
unitInfo: !!unitInfo,
imageurl: unitInfo?.imageurl,
unitInfoData: !!unitInfoData
});
return (
<div className="text-center py-8 text-gray-400">
Нет изображения для этого узла
{process.env.NODE_ENV === 'development' && unitInfo && (
<div className="text-xs mt-2 text-gray-500">
Debug: unitInfo.imageurl = {unitInfo.imageurl || 'отсутствует'}
</div>
)}
</div>
);
} }
return ( return (
@ -137,10 +238,10 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
/> />
{/* Точки/области */} {/* Точки/области */}
{coordinates.map((coord: any, idx: number) => { {coordinates.map((coord: any, idx: number) => {
const scaledX = coord.x * imageScale.x; // Кружки всегда 32x32px, центрируем по координате
const scaledY = coord.y * imageScale.y; const size = 22;
const scaledWidth = coord.width * imageScale.x; const scaledX = coord.x * imageScale.x - size / 2;
const scaledHeight = coord.height * imageScale.y; const scaledY = coord.y * imageScale.y - size / 2;
return ( return (
<div <div
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`} key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
@ -149,19 +250,29 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
onKeyDown={e => { onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage); if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
}} }}
className="absolute flex items-center justify-center border-2 border-red-600 bg-white rounded-full cursor-pointer" className="absolute flex items-center justify-center cursor-pointer transition-colors"
style={{ style={{
left: scaledX, left: scaledX,
top: scaledY, top: scaledY,
width: scaledWidth, width: size,
height: scaledHeight, height: size,
background: '#B7CAE2',
borderRadius: '50%', borderRadius: '50%',
pointerEvents: 'auto', pointerEvents: 'auto',
}} }}
title={coord.codeonimage} title={coord.codeonimage}
onClick={() => handlePointClick(coord.codeonimage)} onClick={() => handlePointClick(coord.codeonimage)}
onMouseEnter={e => {
(e.currentTarget as HTMLDivElement).style.background = '#EC1C24';
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#fff';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLDivElement).style.background = '#B7CAE2';
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#000';
}}
> >
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none"> <span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none" style={{color: '#000'}}>
{coord.codeonimage} {coord.codeonimage}
</span> </span>
</div> </div>

View File

@ -10,18 +10,14 @@ interface VinCategoryProps {
activeTab?: 'uzly' | 'manufacturer'; activeTab?: 'uzly' | 'manufacturer';
onQuickGroupSelect?: (group: any) => void; onQuickGroupSelect?: (group: any) => void;
onCategoryClick?: (e?: React.MouseEvent) => void; onCategoryClick?: (e?: React.MouseEvent) => void;
openedPath?: string[];
setOpenedPath?: (path: string[]) => void;
} }
const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick }) => { const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick, openedPath = [], setOpenedPath = () => {} }) => {
const [selectedCategory, setSelectedCategory] = useState<any>(null);
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({}); const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
const lastCategoryIdRef = useRef<string | null>(null); const lastCategoryIdRef = useRef<string | null>(null);
// Сброс выбранной категории при смене вкладки
useEffect(() => {
setSelectedCategory(null);
}, [activeTab]);
// Запрос для "Общие" (QuickGroups) // Запрос для "Общие" (QuickGroups)
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, { const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', ssd: ssd || '' }, variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', ssd: ssd || '' },
@ -51,50 +47,41 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
} }
}); });
const categories = activeTab === 'uzly' ? (quickGroupsData?.laximoQuickGroups || []) : (categoriesData?.laximoCategories || []); // categories теперь зависят от activeTab
let categories = activeTab === 'uzly' ? (quickGroupsData?.laximoQuickGroups || []) : (categoriesData?.laximoCategories || []);
let selectedCategory: any = null;
let currentLevel = 0;
let currentList = categories;
while (openedPath[currentLevel]) {
const found = currentList.find((cat: any) => (cat.quickgroupid || cat.categoryid || cat.id) === openedPath[currentLevel]);
if (!found) break;
selectedCategory = found;
currentList = found.children || [];
currentLevel++;
}
const loading = activeTab === 'uzly' ? quickGroupsLoading : categoriesLoading; const loading = activeTab === 'uzly' ? quickGroupsLoading : categoriesLoading;
const error = activeTab === 'uzly' ? quickGroupsError : categoriesError; const error = activeTab === 'uzly' ? quickGroupsError : categoriesError;
const handleBack = () => { const handleBack = () => {
setSelectedCategory(null); setOpenedPath(openedPath.slice(0, openedPath.length - 1));
}; };
const handleCategoryClick = (category: any) => { const handleCategoryClick = (category: any, level: number) => {
// Если передан onCategoryClick, используем его
if (onCategoryClick) { if (onCategoryClick) {
onCategoryClick(); onCategoryClick();
return; return;
} }
if (category.children && category.children.length > 0) {
if (activeTab === 'uzly') { if (openedPath[level] === (category.quickgroupid || category.categoryid || category.id)) {
// Логика для вкладки "Общие" (QuickGroups) setOpenedPath(openedPath.slice(0, level));
if (category.children && category.children.length > 0) {
setSelectedCategory(category);
} else if (category.link && onQuickGroupSelect) {
onQuickGroupSelect(category);
} else if (onNodeSelect) {
onNodeSelect(category);
}
} else {
// Логика для вкладки "От производителя" (Categories)
if (category.children && category.children.length > 0) {
setSelectedCategory(category);
} else { } else {
// Если нет children, грузим units (подкатегории) setOpenedPath([...openedPath.slice(0, level), (category.quickgroupid || category.categoryid || category.id)]);
const categoryId = category.categoryid || category.quickgroupid || category.id;
if (!unitsByCategory[categoryId] && catalogCode && vehicleId) {
lastCategoryIdRef.current = categoryId;
getUnits({
variables: {
catalogCode,
vehicleId,
ssd: ssd || '',
categoryId
}
});
}
setSelectedCategory(category);
} }
} else if (category.link && onQuickGroupSelect) {
onQuickGroupSelect(category);
} else if (onNodeSelect) {
onNodeSelect(category);
} }
}; };
@ -106,7 +93,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
unitid: subcat.unitid || subcat.categoryid || subcat.quickgroupid || subcat.id unitid: subcat.unitid || subcat.categoryid || subcat.quickgroupid || subcat.id
}); });
} else { } else {
handleCategoryClick(subcat); handleCategoryClick(subcat, 0);
} }
}; };
@ -150,7 +137,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
<div <div
className="div-block-131" className="div-block-131"
key={cat.quickgroupid || cat.categoryid || cat.id || idx} key={cat.quickgroupid || cat.categoryid || cat.id || idx}
onClick={() => handleCategoryClick(cat)} onClick={() => handleCategoryClick(cat, 0)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<div className="text-block-57">{cat.name}</div> <div className="text-block-57">{cat.name}</div>
@ -165,32 +152,37 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
) : ( ) : (
// Список подкатегорий // Список подкатегорий
<> <>
{/* <div className="div-block-131" onClick={handleBack} style={{ cursor: "pointer", fontWeight: 500 }}> {(() => {
<div className="text-block-57">← Назад</div> // Найти текущий уровень вложенности для selectedCategory
<div className="w-embed"> let level = 0;
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> let list = categories;
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect> while (openedPath[level] && list) {
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path> const found = list.find((cat: any) => (cat.quickgroupid || cat.categoryid || cat.id) === openedPath[level]);
</svg> if (!found) break;
</div> if (found === selectedCategory) break;
</div> */} list = found.children || [];
{subcategories.length === 0 && <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>} level++;
{subcategories.map((subcat: any, idx: number) => ( }
<div // Теперь level - это уровень selectedCategory, подкатегории будут на level+1
className="div-block-131" const subcategories = selectedCategory.children || [];
key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx} if (subcategories.length === 0) return <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>;
onClick={() => handleSubcategoryClick(subcat)} return subcategories.map((subcat: any, idx: number) => (
style={{ cursor: "pointer" }} <div
> className="div-block-131"
<div className="text-block-57">{subcat.name}</div> key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx}
<div className="w-embed"> onClick={() => handleCategoryClick(subcat, level + 1)}
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> style={{ cursor: "pointer" }}
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect> >
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path> <div className="text-block-57">{subcat.name}</div>
</svg> <div className="w-embed">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
</svg>
</div>
</div> </div>
</div> ));
))} })()}
</> </>
)} )}
</div> </div>

View File

@ -19,6 +19,9 @@ interface VinLeftbarProps {
onNodeSelect?: (node: any) => void; onNodeSelect?: (node: any) => void;
onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void; onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void;
onQuickGroupSelect?: (group: any) => void; onQuickGroupSelect?: (group: any) => void;
activeTab?: 'uzly' | 'manufacturer';
openedPath?: string[];
setOpenedPath?: (path: string[]) => void;
} }
interface QuickGroup { interface QuickGroup {
@ -28,13 +31,11 @@ interface QuickGroup {
children?: QuickGroup[]; children?: QuickGroup[];
} }
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect }) => { const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect, activeTab: activeTabProp, openedPath = [], setOpenedPath = () => {} }) => {
const catalogCode = vehicleInfo?.catalog || ''; const catalogCode = vehicleInfo?.catalog || '';
const vehicleId = vehicleInfo?.vehicleid || ''; const vehicleId = vehicleInfo?.vehicleid || '';
const ssd = vehicleInfo?.ssd || ''; const ssd = vehicleInfo?.ssd || '';
const [openIndex, setOpenIndex] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' }); const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, { const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
@ -58,11 +59,24 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
const lastCategoryIdRef = React.useRef<string | null>(null); const lastCategoryIdRef = React.useRef<string | null>(null);
const handleToggle = (idx: number, categoryId: string) => { const handleToggle = (categoryId: string, level: number) => {
setOpenIndex(openIndex === idx ? null : idx); if (openedPath[level] === categoryId) {
if (openIndex !== idx && !unitsByCategory[categoryId]) { setOpenedPath(openedPath.slice(0, level));
lastCategoryIdRef.current = categoryId; } else {
getUnits({ variables: { catalogCode, vehicleId, ssd, categoryId } }); setOpenedPath([...openedPath.slice(0, level), categoryId]);
// Загружаем units для категории, если они еще не загружены
if (activeTabProp === 'manufacturer' && !unitsByCategory[categoryId]) {
lastCategoryIdRef.current = categoryId;
getUnits({
variables: {
catalogCode,
vehicleId,
ssd,
categoryId
}
});
}
} }
}; };
@ -117,26 +131,11 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
}); });
const quickGroups = quickGroupsData?.laximoQuickGroups || []; const quickGroups = quickGroupsData?.laximoQuickGroups || [];
const [expandedQuickGroup, setExpandedQuickGroup] = useState<string | null>(null); const handleQuickGroupToggle = (groupId: string, level: number) => {
const [expandedSubQuickGroup, setExpandedSubQuickGroup] = useState<string | null>(null); if (openedPath[level] === groupId) {
setOpenedPath(openedPath.slice(0, level));
const handleQuickGroupToggle = (groupId: string) => {
setExpandedQuickGroup(prev => (prev === groupId ? null : groupId));
setExpandedSubQuickGroup(null);
};
const handleSubQuickGroupToggle = (groupId: string) => {
setExpandedSubQuickGroup(prev => (prev === groupId ? null : groupId));
};
const handleQuickGroupClick = (group: any) => {
if (group.link) {
// Передаем выбранную группу в родительский компонент для отображения справа
if (onQuickGroupSelect) {
onQuickGroupSelect(group);
}
} else { } else {
handleQuickGroupToggle(group.quickgroupid); setOpenedPath([...openedPath.slice(0, level), groupId]);
} }
}; };
@ -207,12 +206,6 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
const fulltextResults = fulltextData?.laximoFulltextSearch?.details || []; const fulltextResults = fulltextData?.laximoFulltextSearch?.details || [];
useEffect(() => {
if (onActiveTabChange) {
onActiveTabChange(activeTab);
}
}, [activeTab]);
// Если нет данных о транспортном средстве, показываем заглушку // Если нет данных о транспортном средстве, показываем заглушку
if (!vehicleInfo) { if (!vehicleInfo) {
return ( return (
@ -281,18 +274,15 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
className={ className={
searchQuery searchQuery
? 'button-23 w-button' ? 'button-23 w-button'
: activeTab === 'uzly' : activeTabProp === 'uzly'
? 'button-3 w-button' ? 'button-3 w-button'
: 'button-23 w-button' : 'button-23 w-button'
} }
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
if (searchQuery) setSearchQuery(''); if (searchQuery) setSearchQuery('');
setActiveTab('uzly'); if (onActiveTabChange) onActiveTabChange('uzly');
// Очищаем выбранную группу при смене таба if (onQuickGroupSelect) onQuickGroupSelect(null);
if (onQuickGroupSelect) {
onQuickGroupSelect(null);
}
}} }}
> >
Узлы Узлы
@ -302,25 +292,22 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
className={ className={
searchQuery searchQuery
? 'button-23 w-button' ? 'button-23 w-button'
: activeTab === 'manufacturer' : activeTabProp === 'manufacturer'
? 'button-3 w-button' ? 'button-3 w-button'
: 'button-23 w-button' : 'button-23 w-button'
} }
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
if (searchQuery) setSearchQuery(''); if (searchQuery) setSearchQuery('');
setActiveTab('manufacturer'); if (onActiveTabChange) onActiveTabChange('manufacturer');
// Очищаем выбранную группу при смене таба if (onQuickGroupSelect) onQuickGroupSelect(null);
if (onQuickGroupSelect) {
onQuickGroupSelect(null);
}
}} }}
> >
От производителя От производителя
</a> </a>
</div> </div>
{/* Tab content start */} {/* Tab content start */}
{activeTab === 'uzly' ? ( {activeTabProp === 'uzly' ? (
// Общие (QuickGroups - бывшие "От производителя") // Общие (QuickGroups - бывшие "От производителя")
quickGroupsLoading ? ( quickGroupsLoading ? (
<div style={{ padding: 16, textAlign: 'center' }}>Загружаем группы быстрого поиска...</div> <div style={{ padding: 16, textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
@ -330,7 +317,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
<> <>
{(quickGroups as QuickGroup[]).map((group: QuickGroup) => { {(quickGroups as QuickGroup[]).map((group: QuickGroup) => {
const hasChildren = group.children && group.children.length > 0; const hasChildren = group.children && group.children.length > 0;
const isOpen = expandedQuickGroup === group.quickgroupid; const isOpen = openedPath.includes(group.quickgroupid);
if (!hasChildren) { if (!hasChildren) {
return ( return (
@ -340,7 +327,12 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
className="dropdown-link-3 w-dropdown-link" className="dropdown-link-3 w-dropdown-link"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleQuickGroupClick(group); // Если это конечная группа с link=true, открываем QuickGroup
if (group.link && onQuickGroupSelect) {
onQuickGroupSelect(group);
} else {
handleQuickGroupToggle(group.quickgroupid, 0);
}
}} }}
> >
{group.name} {group.name}
@ -357,7 +349,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
> >
<div <div
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open active" : ""}`} className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open active" : ""}`}
onClick={() => handleQuickGroupToggle(group.quickgroupid)} onClick={(e) => {
e.preventDefault();
handleQuickGroupToggle(group.quickgroupid, 0);
}}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<div className="w-icon-dropdown-toggle"></div> <div className="w-icon-dropdown-toggle"></div>
@ -366,7 +361,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
<nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}> <nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}>
{group.children?.map((child: QuickGroup) => { {group.children?.map((child: QuickGroup) => {
const hasSubChildren = child.children && child.children.length > 0; const hasSubChildren = child.children && child.children.length > 0;
const isChildOpen = expandedSubQuickGroup === child.quickgroupid; const isChildOpen = openedPath.includes(child.quickgroupid);
if (!hasSubChildren) { if (!hasSubChildren) {
return ( return (
@ -376,7 +371,12 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
className="dropdown-link-3 w-dropdown-link" className="dropdown-link-3 w-dropdown-link"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleQuickGroupClick(child); // Если это конечная группа с link=true, открываем QuickGroup
if (child.link && onQuickGroupSelect) {
onQuickGroupSelect(child);
} else {
handleQuickGroupToggle(child.quickgroupid, 1);
}
}} }}
> >
{child.name} {child.name}
@ -393,7 +393,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
> >
<div <div
className={`dropdown-toggle-card w-dropdown-toggle pl-0${isChildOpen ? " w--open active" : ""}`} className={`dropdown-toggle-card w-dropdown-toggle pl-0${isChildOpen ? " w--open active" : ""}`}
onClick={() => handleSubQuickGroupToggle(child.quickgroupid)} onClick={(e) => {
e.preventDefault();
handleQuickGroupToggle(child.quickgroupid, 2);
}}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<div className="w-icon-dropdown-toggle"></div> <div className="w-icon-dropdown-toggle"></div>
@ -407,7 +410,12 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
className="dropdown-link-3 w-dropdown-link" className="dropdown-link-3 w-dropdown-link"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleQuickGroupClick(subChild); // Если это конечная группа с link=true, открываем QuickGroup
if (subChild.link && onQuickGroupSelect) {
onQuickGroupSelect(subChild);
} else {
handleQuickGroupToggle(subChild.quickgroupid, 3);
}
}} }}
> >
{subChild.name} {subChild.name}
@ -434,7 +442,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
) : ( ) : (
<> <>
{categories.map((category: any, idx: number) => { {categories.map((category: any, idx: number) => {
const isOpen = openIndex === idx; const isOpen = openedPath.includes(category.quickgroupid);
const subcategories = category.children && category.children.length > 0 const subcategories = category.children && category.children.length > 0
? category.children ? category.children
: unitsByCategory[category.quickgroupid] || []; : unitsByCategory[category.quickgroupid] || [];
@ -447,7 +455,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
> >
<div <div
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`} className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`}
onClick={() => handleToggle(idx, category.quickgroupid)} onClick={(e) => {
e.preventDefault();
handleToggle(category.quickgroupid, 0);
}}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<div className="w-icon-dropdown-toggle"></div> <div className="w-icon-dropdown-toggle"></div>
@ -462,7 +473,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
className="dropdown-link-3 w-dropdown-link pl-0" className="dropdown-link-3 w-dropdown-link pl-0"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
if (onNodeSelect) { // Если это конечная категория с link=true, открываем QuickGroup
if (subcat.link && onQuickGroupSelect) {
onQuickGroupSelect(subcat);
} else if (onNodeSelect) {
onNodeSelect({ onNodeSelect({
...subcat, ...subcat,
unitid: subcat.unitid || subcat.quickgroupid || subcat.id unitid: subcat.unitid || subcat.quickgroupid || subcat.id

View File

@ -72,8 +72,8 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
<div className="knot-img"> <div className="knot-img">
<h1 className="heading-19">{unit.name}</h1> <h1 className="heading-19">{unit.name}</h1>
{unit.details && unit.details.length > 0 && unit.details.map((detail: any) => ( {unit.details && unit.details.length > 0 && unit.details.map((detail: any, index: number) => (
<div className="w-layout-hflex flex-block-115" key={detail.detailid}> <div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
<div className="oemnuber">{detail.oem}</div> <div className="oemnuber">{detail.oem}</div>
<div className="partsname">{detail.name}</div> <div className="partsname">{detail.name}</div>
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a> <a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>

View File

@ -25,6 +25,8 @@ const initialState: CartState = {
// Типы действий // Типы действий
type CartAction = type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'id' | 'selected' | 'favorite'> } | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'id' | 'selected' | 'favorite'> }
| { type: 'ADD_ITEM_SUCCESS'; payload: { items: CartItem[]; summary: any } }
| { type: 'ADD_ITEM_ERROR'; payload: string }
| { type: 'REMOVE_ITEM'; payload: string } | { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } } | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'TOGGLE_SELECT'; payload: string } | { type: 'TOGGLE_SELECT'; payload: string }
@ -44,6 +46,16 @@ type CartAction =
// Функция для генерации ID // Функция для генерации ID
const generateId = () => Math.random().toString(36).substr(2, 9) const generateId = () => Math.random().toString(36).substr(2, 9)
// Утилитарная функция для парсинга количества в наличии
const parseStock = (stockStr: string | number | undefined): number => {
if (typeof stockStr === 'number') return stockStr;
if (typeof stockStr === 'string') {
const match = stockStr.match(/\d+/);
return match ? parseInt(match[0]) : 0;
}
return 0;
};
// Функция для расчета итогов // Функция для расчета итогов
const calculateSummary = (items: CartItem[], deliveryPrice: number) => { const calculateSummary = (items: CartItem[], deliveryPrice: number) => {
const selectedItems = items.filter(item => item.selected) const selectedItems = items.filter(item => item.selected)
@ -78,9 +90,12 @@ const cartReducer = (state: CartState, action: CartAction): CartState => {
if (existingItemIndex >= 0) { if (existingItemIndex >= 0) {
// Увеличиваем количество существующего товара // Увеличиваем количество существующего товара
const existingItem = state.items[existingItemIndex];
const totalQuantity = existingItem.quantity + action.payload.quantity;
newItems = state.items.map((item, index) => newItems = state.items.map((item, index) =>
index === existingItemIndex index === existingItemIndex
? { ...item, quantity: item.quantity + action.payload.quantity } ? { ...item, quantity: totalQuantity }
: item : item
) )
} else { } else {
@ -335,8 +350,31 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
}, [state.items, state.delivery, state.orderComment, isInitialized]) }, [state.items, state.delivery, state.orderComment, isInitialized])
// Функции для работы с корзиной // Функции для работы с корзиной
const addItem = (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => { const addItem = async (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => {
// Проверяем наличие товара на складе перед добавлением
const existingItemIndex = state.items.findIndex(
existingItem =>
(existingItem.productId && existingItem.productId === item.productId) ||
(existingItem.offerKey && existingItem.offerKey === item.offerKey)
)
let totalQuantity = item.quantity;
if (existingItemIndex >= 0) {
const existingItem = state.items[existingItemIndex];
totalQuantity = existingItem.quantity + item.quantity;
}
// Проверяем наличие товара на складе
const availableStock = parseStock(item.stock);
if (availableStock > 0 && totalQuantity > availableStock) {
const errorMessage = `Недостаточно товара в наличии. Доступно: ${availableStock} шт., запрошено: ${totalQuantity} шт.`;
dispatch({ type: 'SET_ERROR', payload: errorMessage });
return { success: false, error: errorMessage };
}
// Если проверка прошла успешно, добавляем товар
dispatch({ type: 'ADD_ITEM', payload: item }) dispatch({ type: 'ADD_ITEM', payload: item })
return { success: true }
} }
const removeItem = (id: string) => { const removeItem = (id: string) => {
@ -344,6 +382,17 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
} }
const updateQuantity = (id: string, quantity: number) => { const updateQuantity = (id: string, quantity: number) => {
// Найдем товар для проверки наличия
const item = state.items.find(item => item.id === id);
if (item) {
const availableStock = parseStock(item.stock);
if (availableStock > 0 && quantity > availableStock) {
// Показываем ошибку, но не изменяем количество
dispatch({ type: 'SET_ERROR', payload: `Недостаточно товара в наличии. Доступно: ${availableStock} шт.` });
return;
}
}
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } }) dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
} }
@ -388,6 +437,10 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
} }
} }
const clearError = () => {
dispatch({ type: 'SET_ERROR', payload: '' })
}
const contextValue: CartContextType = { const contextValue: CartContextType = {
state, state,
addItem, addItem,
@ -401,7 +454,8 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
removeAll, removeAll,
removeSelected, removeSelected,
updateDelivery, updateDelivery,
clearCart clearCart,
clearError
} }
return ( return (

View File

@ -4,6 +4,7 @@ import React, { createContext, useContext, useReducer, useEffect, ReactNode } fr
import { useMutation, useQuery } from '@apollo/client' import { useMutation, useQuery } from '@apollo/client'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { GET_FAVORITES, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES, CLEAR_FAVORITES } from '@/lib/favorites-queries' import { GET_FAVORITES, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES, CLEAR_FAVORITES } from '@/lib/favorites-queries'
import DeleteCartIcon from '@/components/DeleteCartIcon'
// Типы // Типы
export interface FavoriteItem { export interface FavoriteItem {
@ -133,7 +134,9 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, { const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
onCompleted: () => { onCompleted: () => {
toast.success('Товар удален из избранного') toast.success('Товар удален из избранного', {
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
})
}, },
onError: (error) => { onError: (error) => {
console.error('Ошибка удаления из избранного:', error) console.error('Ошибка удаления из избранного:', error)

View File

@ -231,6 +231,7 @@ export const useCatalogPrices = (): UseCatalogPricesReturn => {
price: cheapestOffer.price, price: cheapestOffer.price,
currency: cheapestOffer.currency || 'RUB', currency: cheapestOffer.currency || 'RUB',
quantity: 1, quantity: 1,
stock: cheapestOffer.quantity, // передаем информацию о наличии
deliveryTime: cheapestOffer.deliveryDays?.toString() || '0', deliveryTime: cheapestOffer.deliveryDays?.toString() || '0',
warehouse: cheapestOffer.warehouse || 'Склад', warehouse: cheapestOffer.warehouse || 'Склад',
supplier: cheapestOffer.supplierName || 'Неизвестный поставщик', supplier: cheapestOffer.supplierName || 'Неизвестный поставщик',
@ -238,10 +239,14 @@ export const useCatalogPrices = (): UseCatalogPricesReturn => {
image: '', // Убираем мокап-фотку, изображения будут загружаться отдельно image: '', // Убираем мокап-фотку, изображения будут загружаться отдельно
}; };
addItem(itemToAdd); const result = await addItem(itemToAdd);
// Показываем уведомление if (result.success) {
toast.success(`Товар "${brand} ${articleNumber}" добавлен в корзину за ${cheapestOffer.price}`); // Показываем уведомление
toast.success(`Товар "${brand} ${articleNumber}" добавлен в корзину за ${cheapestOffer.price}`);
} else {
toast.error(result.error || 'Ошибка добавления товара в корзину');
}
} catch (error) { } catch (error) {
console.error('Ошибка добавления в корзину:', error); console.error('Ошибка добавления в корзину:', error);

View File

@ -60,8 +60,12 @@ export default function App({ Component, pageProps }: AppProps) {
}, },
success: { success: {
duration: 3000, duration: 3000,
style: {
background: '#22c55e', // Зеленый фон для успешных уведомлений
color: '#fff', // Белый текст
},
iconTheme: { iconTheme: {
primary: '#4ade80', primary: '#22c55e',
secondary: '#fff', secondary: '#fff',
}, },
}, },

View File

@ -23,6 +23,7 @@ import { useProductPrices } from '@/hooks/useProductPrices';
import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton'; import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton';
import { useCart } from '@/contexts/CartContext'; import { useCart } from '@/contexts/CartContext';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import CartIcon from '@/components/CartIcon';
const mockData = Array(12).fill({ const mockData = Array(12).fill({
image: "", image: "",
@ -720,7 +721,7 @@ export default function Catalog() {
productId={entity.id} productId={entity.id}
artId={entity.id} artId={entity.id}
offerKey={priceData?.offerKey} offerKey={priceData?.offerKey}
onAddToCart={() => { onAddToCart={async () => {
// Если цена не загружена, загружаем её и добавляем в корзину // Если цена не загружена, загружаем её и добавляем в корзину
if (!priceData && !isLoadingPriceData) { if (!priceData && !isLoadingPriceData) {
loadPriceOnDemand(productForPrice); loadPriceOnDemand(productForPrice);
@ -740,6 +741,7 @@ export default function Catalog() {
price: priceData.price, price: priceData.price,
currency: priceData.currency || 'RUB', currency: priceData.currency || 'RUB',
quantity: 1, quantity: 1,
stock: undefined, // информация о наличии не доступна для PartsIndex
deliveryTime: '1-3 дня', deliveryTime: '1-3 дня',
warehouse: 'Parts Index', warehouse: 'Parts Index',
supplier: 'Parts Index', supplier: 'Parts Index',
@ -747,10 +749,23 @@ export default function Catalog() {
image: entity.images?.[0] || '', image: entity.images?.[0] || '',
}; };
addItem(itemToAdd); const result = await addItem(itemToAdd);
// Показываем уведомление if (result.success) {
toast.success(`Товар "${entity.brand.name} ${entity.code}" добавлен в корзину за ${priceData.price.toLocaleString('ru-RU')}`); // Показываем уведомление
toast.success(
<div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')}`}</div>
</div>,
{
duration: 3000,
icon: <CartIcon size={20} color="#fff" />,
}
);
} else {
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
}
} else { } else {
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.'); toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
} }

View File

@ -606,12 +606,11 @@ export default function SearchResult() {
return true; // Показываем загружающиеся аналоги return true; // Показываем загружающиеся аналоги
} }
const analogOffers = transformOffersForCard( // Проверяем, есть ли предложения у аналога
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber) const hasInternalOffers = loadedAnalogData.internalOffers && loadedAnalogData.internalOffers.length > 0;
); const hasExternalOffers = loadedAnalogData.externalOffers && loadedAnalogData.externalOffers.length > 0;
// Показываем аналог только если у него есть предложения return hasInternalOffers || hasExternalOffers;
return analogOffers.length > 0;
}); });
// Если нет аналогов с предложениями, не показываем секцию // Если нет аналогов с предложениями, не показываем секцию
@ -625,11 +624,79 @@ export default function SearchResult() {
const analogKey = `${analog.brand}-${analog.articleNumber}`; const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey]; const loadedAnalogData = loadedAnalogs[analogKey];
const analogOffers = loadedAnalogData // Если данные аналога загружены, формируем предложения из всех его данных
? transformOffersForCard( const analogOffers = loadedAnalogData ? (() => {
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber) const allAnalogOffers: any[] = [];
)
: []; // Добавляем внутренние предложения
if (loadedAnalogData.internalOffers) {
loadedAnalogData.internalOffers.forEach((offer: any) => {
allAnalogOffers.push({
...offer,
type: 'internal',
brand: loadedAnalogData.brand,
articleNumber: loadedAnalogData.articleNumber,
name: loadedAnalogData.name,
isAnalog: true,
deliveryDuration: offer.deliveryDays
});
});
}
// Добавляем внешние предложения
if (loadedAnalogData.externalOffers) {
loadedAnalogData.externalOffers.forEach((offer: any) => {
allAnalogOffers.push({
...offer,
type: 'external',
brand: offer.brand || loadedAnalogData.brand,
articleNumber: offer.code || loadedAnalogData.articleNumber,
name: offer.name || loadedAnalogData.name,
isAnalog: true,
deliveryDuration: offer.deliveryTime
});
});
}
// Применяем фильтры только если они активны
const filteredAnalogOffers = allAnalogOffers.filter(offer => {
// Фильтр по бренду
if (selectedBrands.length > 0 && !selectedBrands.includes(offer.brand)) {
return false;
}
// Фильтр по цене
if (priceRange && (offer.price < priceRange[0] || offer.price > priceRange[1])) {
return false;
}
// Фильтр по сроку доставки
if (deliveryRange) {
const deliveryDays = offer.deliveryDuration;
if (deliveryDays < deliveryRange[0] || deliveryDays > deliveryRange[1]) {
return false;
}
}
// Фильтр по количеству наличия
if (quantityRange) {
const quantity = offer.quantity;
if (quantity < quantityRange[0] || quantity > quantityRange[1]) {
return false;
}
}
// Фильтр по поисковой строке
if (filterSearchTerm) {
const searchTerm = filterSearchTerm.toLowerCase();
const brandMatch = offer.brand.toLowerCase().includes(searchTerm);
const articleMatch = offer.articleNumber.toLowerCase().includes(searchTerm);
const nameMatch = offer.name.toLowerCase().includes(searchTerm);
if (!brandMatch && !articleMatch && !nameMatch) {
return false;
}
}
return true;
});
return transformOffersForCard(filteredAnalogOffers);
})() : [];
return ( return (
<CoreProductCard <CoreProductCard
@ -647,20 +714,7 @@ export default function SearchResult() {
{(() => { {(() => {
// Проверяем, есть ли еще аналоги с предложениями для загрузки // Проверяем, есть ли еще аналоги с предложениями для загрузки
const remainingAnalogs = result.analogs.slice(visibleAnalogsCount); const remainingAnalogs = result.analogs.slice(visibleAnalogsCount);
const hasMoreAnalogsWithOffers = remainingAnalogs.some((analog: any) => { const hasMoreAnalogsWithOffers = remainingAnalogs.length > 0;
const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey];
if (!loadedAnalogData) {
return true; // Могут быть предложения у незагруженных аналогов
}
const analogOffers = transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
);
return analogOffers.length > 0;
});
return hasMoreAnalogsWithOffers && ( return hasMoreAnalogsWithOffers && (
<div className="w-layout-hflex pagination"> <div className="w-layout-hflex pagination">

View File

@ -58,6 +58,7 @@ const VehicleDetailsPage = () => {
const [showKnot, setShowKnot] = useState(false); const [showKnot, setShowKnot] = useState(false);
const [foundParts, setFoundParts] = useState<any[]>([]); const [foundParts, setFoundParts] = useState<any[]>([]);
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly'); const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
const [openedPath, setOpenedPath] = useState<string[]>([]);
const [searchState, setSearchState] = useState<{ const [searchState, setSearchState] = useState<{
loading: boolean; loading: boolean;
error: any; error: any;
@ -329,6 +330,9 @@ const VehicleDetailsPage = () => {
onNodeSelect={setSelectedNode} onNodeSelect={setSelectedNode}
onActiveTabChange={(tab) => setActiveTab(tab)} onActiveTabChange={(tab) => setActiveTab(tab)}
onQuickGroupSelect={setSelectedQuickGroup} onQuickGroupSelect={setSelectedQuickGroup}
activeTab={activeTab}
openedPath={openedPath}
setOpenedPath={setOpenedPath}
/> />
{searchState.isSearching ? ( {searchState.isSearching ? (
<div className="knot-parts"> <div className="knot-parts">
@ -399,6 +403,8 @@ const VehicleDetailsPage = () => {
onNodeSelect={setSelectedNode} onNodeSelect={setSelectedNode}
activeTab={activeTab} activeTab={activeTab}
onQuickGroupSelect={setSelectedQuickGroup} onQuickGroupSelect={setSelectedQuickGroup}
openedPath={openedPath}
setOpenedPath={setOpenedPath}
/> />
)} )}
</> </>
@ -408,10 +414,22 @@ const VehicleDetailsPage = () => {
<div className="w-layout-hflex flex-block-13"> <div className="w-layout-hflex flex-block-13">
<div className="w-layout-vflex flex-block-14-copy-copy"> <div className="w-layout-vflex flex-block-14-copy-copy">
{/* <button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button> */} {/* <button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button> */}
{/* ОТЛАДКА: Логируем передачу SSD в KnotIn */}
{(() => {
const knotSsd = selectedNode.ssd || vehicleInfo.ssd;
console.log('🔍 [vehicleId].tsx передает в KnotIn:', {
selectedNodeSsd: selectedNode.ssd ? `${selectedNode.ssd.substring(0, 50)}...` : 'отсутствует',
vehicleInfoSsd: vehicleInfo.ssd ? `${vehicleInfo.ssd.substring(0, 50)}...` : 'отсутствует',
finalSsd: knotSsd ? `${knotSsd.substring(0, 50)}...` : 'отсутствует',
unitId: selectedNode.unitid,
unitName: selectedNode.name
});
return null;
})()}
<KnotIn <KnotIn
catalogCode={vehicleInfo.catalog} catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid} vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd} ssd={selectedNode.ssd || vehicleInfo.ssd} // ИСПРАВЛЕНИЕ: Используем SSD узла, fallback на родительский SSD
unitId={selectedNode.unitid} unitId={selectedNode.unitid}
unitName={selectedNode.name} unitName={selectedNode.name}
parts={unitDetails} parts={unitDetails}

View File

@ -469,7 +469,13 @@ input#VinSearchInput {
.dropdown-toggle-card { .dropdown-toggle-card {
align-self: stretch;
margin-bottom: 5px;
margin-left: 0;
margin-right: 0;
padding: 6px 15px;
padding-left: 0 !important; padding-left: 0 !important;
margin-left: 0 !important;
} }
.dropdown-link-3 { .dropdown-link-3 {
@ -480,11 +486,17 @@ input#VinSearchInput {
max-width: 230px; max-width: 230px;
} }
.dropdown-toggle-3.active, .dropdown-toggle-card.active { .dropdown-toggle-3.active{
background-color: var(--background); background-color: var(--background);
font-weight: 700;
} }
.dropdown-toggle-card.active {
background-color: var(--background);
}
.dropdown-toggle-3.active { .dropdown-toggle-3.active {
border-left: 2px solid var(--red); border-left: 2px solid var(--red);
@ -689,4 +701,43 @@ body {
} }
.flex-block-108::-webkit-scrollbar, .flex-block-108-copy::-webkit-scrollbar, .w-layout-hflex.flex-block-121::-webkit-scrollbar, .core-product-search::-webkit-scrollbar { .flex-block-108::-webkit-scrollbar, .flex-block-108-copy::-webkit-scrollbar, .w-layout-hflex.flex-block-121::-webkit-scrollbar, .core-product-search::-webkit-scrollbar {
display: none; /* Chrome, Safari */ display: none; /* Chrome, Safari */
}
.flex-block-44 {
max-width: 33%;
}
.text-block-21 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.flex-block-45 {
width: 80%;
}
.flex-block-39 {
max-width: 100%;
}
.heading-9-copy {
min-width: 100px;
}
@media screen and (max-width: 767px) {
.flex-block-15-copy {
grid-column-gap: 5px;
grid-row-gap: 5px;
width: 190px;
padding: 15px;
}
}
.flex-block-15-copy {
width: 230px;
} }

View File

@ -10,6 +10,7 @@ export interface CartItem {
originalPrice?: number originalPrice?: number
currency: string currency: string
quantity: number quantity: number
stock?: string | number // количество товара в наличии на складе
deliveryTime?: string deliveryTime?: string
deliveryDate?: string deliveryDate?: string
warehouse?: string warehouse?: string
@ -52,7 +53,7 @@ export interface CartState {
export interface CartContextType { export interface CartContextType {
state: CartState state: CartState
addItem: (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => void addItem: (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => Promise<{ success: boolean; error?: string }>
removeItem: (id: string) => void removeItem: (id: string) => void
updateQuantity: (id: string, quantity: number) => void updateQuantity: (id: string, quantity: number) => void
toggleSelect: (id: string) => void toggleSelect: (id: string) => void
@ -64,4 +65,5 @@ export interface CartContextType {
removeSelected: () => void removeSelected: () => void
updateDelivery: (delivery: Partial<DeliveryInfo>) => void updateDelivery: (delivery: Partial<DeliveryInfo>) => void
clearCart: () => void clearCart: () => void
clearError: () => void
} }