Compare commits
9 Commits
homenewpag
...
7d9f611fe5
Author | SHA1 | Date | |
---|---|---|---|
7d9f611fe5 | |||
8820f4e835 | |||
ac7b2de49f | |||
a8c8ae60bb | |||
78e17a94ab | |||
36c5990921 | |||
e989d402a3 | |||
65710a35be | |||
9a604b39b3 |
@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import toast from "react-hot-toast";
|
||||
import CartIcon from "./CartIcon";
|
||||
|
||||
interface BestPriceCardProps {
|
||||
bestOfferType: string;
|
||||
@ -27,6 +28,11 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
||||
const maxCount = isNaN(parsedStock) ? undefined : parsedStock;
|
||||
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 handlePlus = () => {
|
||||
@ -38,7 +44,13 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
};
|
||||
|
||||
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 (maxCount !== undefined && value > maxCount) {
|
||||
toast.error(`Максимум ${maxCount} шт.`);
|
||||
@ -47,6 +59,13 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
setCount(value);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
if (inputValue === "") {
|
||||
setInputValue("1");
|
||||
setCount(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для парсинга цены из строки
|
||||
const parsePrice = (priceStr: string): number => {
|
||||
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.stopPropagation();
|
||||
|
||||
@ -69,14 +88,8 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем наличие
|
||||
if (maxCount !== undefined && count > maxCount) {
|
||||
toast.error(`Недостаточно товара в наличии. Доступно: ${maxCount} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
addItem({
|
||||
const result = await addItem({
|
||||
productId: offer.productId,
|
||||
offerKey: offer.offerKey,
|
||||
name: description,
|
||||
@ -86,6 +99,7 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
price: numericPrice,
|
||||
currency: offer.currency || 'RUB',
|
||||
quantity: count,
|
||||
stock: maxCount, // передаем информацию о наличии
|
||||
deliveryTime: delivery,
|
||||
warehouse: offer.warehouse || 'Склад',
|
||||
supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'),
|
||||
@ -93,17 +107,22 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
image: offer.image,
|
||||
});
|
||||
|
||||
// Показываем тоастер об успешном добавлении
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
if (result.success) {
|
||||
// Показываем тоастер об успешном добавлении
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
||||
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: <CartIcon size={20} color="#fff" />,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Показываем ошибку
|
||||
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка добавления товара в корзину');
|
||||
@ -144,8 +163,9 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
||||
type="number"
|
||||
min={1}
|
||||
max={maxCount}
|
||||
value={count}
|
||||
value={inputValue}
|
||||
onChange={handleInput}
|
||||
onBlur={handleInputBlur}
|
||||
className="text-block-26 w-full text-center outline-none"
|
||||
aria-label="Количество"
|
||||
/>
|
||||
|
25
src/components/CartIcon.tsx
Normal file
25
src/components/CartIcon.tsx
Normal 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;
|
@ -38,160 +38,180 @@ const CartItem: React.FC<CartItemProps> = ({
|
||||
onRemove,
|
||||
isSummaryStep = false,
|
||||
itemNumber,
|
||||
}) => (
|
||||
<div className="w-layout-hflex cart-item">
|
||||
<div className="w-layout-hflex info-block-search-copy">
|
||||
{isSummaryStep ? (
|
||||
<div style={{ marginRight: 12, minWidth: 24, textAlign: 'center', fontWeight: 600, fontSize: 14 }}>{itemNumber}</div>
|
||||
) : (
|
||||
<div
|
||||
className={"div-block-7" + (selected ? " active" : "")}
|
||||
onClick={onSelect}
|
||||
style={{ marginRight: 12, cursor: 'pointer' }}
|
||||
>
|
||||
{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">
|
||||
}) => {
|
||||
// --- Фикс для input: можно стереть, при blur пустое = 1 ---
|
||||
const [inputValue, setInputValue] = React.useState(count.toString());
|
||||
React.useEffect(() => {
|
||||
setInputValue(count.toString());
|
||||
}, [count]);
|
||||
|
||||
return (
|
||||
<div className="w-layout-hflex cart-item">
|
||||
<div className="w-layout-hflex info-block-search-copy">
|
||||
{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
|
||||
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
|
||||
className={"div-block-7" + (selected ? " active" : "")}
|
||||
onClick={onSelect}
|
||||
style={{ marginRight: 12, cursor: 'pointer' }}
|
||||
>
|
||||
{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="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 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">
|
||||
<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 className="w-layout-hflex block-name">
|
||||
<h4 className="heading-9-copy">{name}</h4>
|
||||
<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');
|
||||
}}
|
||||
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 ? (
|
||||
<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">
|
||||
<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' }}
|
||||
/>
|
||||
<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
|
||||
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>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default CartItem;
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import CartItem from "./CartItem";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
@ -8,7 +8,7 @@ interface CartListProps {
|
||||
}
|
||||
|
||||
const CartList: React.FC<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 { items } = state;
|
||||
|
||||
@ -73,8 +73,40 @@ const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
|
||||
// На втором шаге показываем только выбранные товары
|
||||
const displayItems = isSummaryStep ? items.filter(item => item.selected) : items;
|
||||
|
||||
// Автоматически очищаем ошибки через 5 секунд
|
||||
useEffect(() => {
|
||||
if (state.error) {
|
||||
const timer = setTimeout(() => {
|
||||
clearError();
|
||||
}, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [state.error, clearError]);
|
||||
|
||||
return (
|
||||
<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">
|
||||
{!isSummaryStep && (
|
||||
<div className="w-layout-hflex multi-control">
|
||||
|
@ -3,6 +3,7 @@ import CatalogProductCard from "./CatalogProductCard";
|
||||
import { useArticleImage } from "@/hooks/useArticleImage";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { toast } from "react-hot-toast";
|
||||
import CartIcon from "./CartIcon";
|
||||
|
||||
interface CartRecommendedProps {
|
||||
recommendedProducts?: any[];
|
||||
@ -36,13 +37,14 @@ const RecommendedProductCard: React.FC<{
|
||||
}
|
||||
|
||||
// Добавляем товар в корзину
|
||||
addItem({
|
||||
const result = await addItem({
|
||||
productId: String(item.artId) || undefined,
|
||||
name: item.name || `${item.brand} ${item.articleNumber}`,
|
||||
description: item.name || `${item.brand} ${item.articleNumber}`,
|
||||
price: numericPrice,
|
||||
currency: 'RUB',
|
||||
quantity: 1,
|
||||
stock: undefined, // информация о наличии не доступна для рекомендуемых товаров
|
||||
image: displayImage,
|
||||
brand: item.brand,
|
||||
article: item.articleNumber,
|
||||
@ -51,17 +53,22 @@ const RecommendedProductCard: React.FC<{
|
||||
isExternal: true
|
||||
});
|
||||
|
||||
// Показываем успешный тоастер
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{item.name || `${item.brand} ${item.articleNumber}`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
if (result.success) {
|
||||
// Показываем успешный тоастер
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
||||
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{item.name || `${item.brand} ${item.articleNumber}`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: <CartIcon size={20} color="#fff" />,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Показываем ошибку
|
||||
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка при добавлении товара в корзину');
|
||||
|
@ -42,7 +42,7 @@ const CartRecommendedProductCard: React.FC<CartRecommendedProductCardProps> = ({
|
||||
<Link href="/cart" className="link-block-4-copy w-inline-block">
|
||||
<div className="div-block-25">
|
||||
<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" />
|
||||
</svg>
|
||||
</span>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useFavorites } from "@/contexts/FavoritesContext";
|
||||
import toast from "react-hot-toast";
|
||||
import CartIcon from "./CartIcon";
|
||||
|
||||
const INITIAL_OFFERS_LIMIT = 5;
|
||||
|
||||
@ -52,8 +53,16 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||
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 }>({});
|
||||
|
||||
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 hasMoreOffers = visibleOffersCount < offers.length;
|
||||
|
||||
@ -83,31 +92,44 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
return match ? parseInt(match[0]) : 0;
|
||||
};
|
||||
|
||||
const handleQuantityInput = (index: number, value: string) => {
|
||||
const offer = offers[index];
|
||||
const availableStock = parseStock(offer.pcs);
|
||||
let num = parseInt(value, 10);
|
||||
if (isNaN(num) || num < 1) num = 1;
|
||||
if (num > availableStock) {
|
||||
toast.error(`Максимум ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
setQuantities(prev => ({ ...prev, [index]: num }));
|
||||
const handleInputChange = (idx: number, val: string) => {
|
||||
setInputValues(prev => ({ ...prev, [idx]: val }));
|
||||
if (val === "") return;
|
||||
const valueNum = Math.max(1, parseInt(val, 10) || 1);
|
||||
setQuantities(prev => ({ ...prev, [idx]: valueNum }));
|
||||
};
|
||||
|
||||
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 availableStock = parseStock(offer.pcs);
|
||||
|
||||
// Проверяем наличие
|
||||
if (quantity > availableStock) {
|
||||
toast.error(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const numericPrice = parsePrice(offer.price);
|
||||
|
||||
addItem({
|
||||
const result = await addItem({
|
||||
productId: offer.productId,
|
||||
offerKey: offer.offerKey,
|
||||
name: name,
|
||||
@ -117,6 +139,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
price: numericPrice,
|
||||
currency: offer.currency || 'RUB',
|
||||
quantity: quantity,
|
||||
stock: availableStock, // передаем информацию о наличии
|
||||
deliveryTime: parseDeliveryTime(offer.days),
|
||||
warehouse: offer.warehouse || 'Склад',
|
||||
supplier: offer.supplier || (offer.isExternal ? 'AutoEuro' : 'Protek'),
|
||||
@ -124,17 +147,22 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
||||
image: image,
|
||||
});
|
||||
|
||||
// Показываем тоастер вместо alert
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{`${brand} ${article} (${quantity} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
if (result.success) {
|
||||
// Показываем тоастер вместо alert
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
||||
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${brand} ${article} (${quantity} шт.)`}</div>
|
||||
</div>,
|
||||
{
|
||||
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">
|
||||
{displayedOffers.map((offer, idx) => {
|
||||
const isLast = idx === displayedOffers.length - 1;
|
||||
const maxCount = parseStock(offer.pcs);
|
||||
return (
|
||||
<div
|
||||
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 flex-block-82">
|
||||
<div className="w-layout-hflex pcs-cart-s1">
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
className="minus-plus"
|
||||
onClick={() => handleQuantityInput(idx, ((quantities[idx] || 1) - 1).toString())}
|
||||
onClick={() => handleMinus(idx)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
aria-label="Уменьшить количество"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleMinus(idx)}
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
<div className="input-pcs">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={parseStock(offer.pcs)}
|
||||
value={quantities[idx] || 1}
|
||||
onChange={e => handleQuantityInput(idx, e.target.value)}
|
||||
max={maxCount}
|
||||
value={inputValues[idx]}
|
||||
onChange={e => handleInputChange(idx, e.target.value)}
|
||||
onBlur={() => handleInputBlur(idx)}
|
||||
className="text-block-26 w-full text-center outline-none"
|
||||
aria-label="Количество"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
className="minus-plus"
|
||||
onClick={() => handleQuantityInput(idx, ((quantities[idx] || 1) + 1).toString())}
|
||||
onClick={() => handlePlus(idx, maxCount)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
aria-label="Увеличить количество"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handlePlus(idx, maxCount)}
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
25
src/components/DeleteCartIcon.tsx
Normal file
25
src/components/DeleteCartIcon.tsx
Normal 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;
|
@ -24,7 +24,7 @@ const Footer = () => (
|
||||
<div data-hover="false" data-delay="0" className="dropdown-3 w-dropdown">
|
||||
<div className="dropdown-toggle-2 w-dropdown-toggle">
|
||||
<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>
|
||||
<nav className="dropdown-list-3 w-dropdown-list">
|
||||
<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 className="dropdown-toggle-2 w-dropdown-toggle">
|
||||
<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>
|
||||
<nav className="dropdown-list-3 w-dropdown-list">
|
||||
<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 className="dropdown-toggle-2 w-dropdown-toggle">
|
||||
<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>
|
||||
<nav className="dropdown-list-3 w-dropdown-list">
|
||||
<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 className="dropdown-toggle-2 w-dropdown-toggle">
|
||||
<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>
|
||||
<nav className="dropdown-list-3 w-dropdown-list">
|
||||
<a href="#" className="dropdown-link-2 w-dropdown-link">Поставщикам</a>
|
||||
|
@ -37,10 +37,8 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
// Если мы находимся на странице search-result, восстанавливаем поисковый запрос
|
||||
if (router.pathname === '/search-result') {
|
||||
const { article, brand } = router.query;
|
||||
if (article && brand && typeof article === 'string' && typeof brand === 'string') {
|
||||
// Формируем поисковый запрос из артикула и бренда
|
||||
setSearchQuery(`${brand} ${article}`);
|
||||
} else if (article && typeof article === 'string') {
|
||||
if (article && typeof article === 'string') {
|
||||
// Отображаем только артикул, без бренда
|
||||
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-3">
|
||||
<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>
|
||||
</div>
|
||||
@ -393,7 +391,7 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
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 7.5H30V10.5H0V7.5Z" fill="currentColor"></path>
|
||||
<path d="M0 15H30V18H0V15Z" fill="currentColor"></path>
|
||||
|
@ -59,19 +59,13 @@ const ProductListCard: React.FC<ProductListCardProps> = ({
|
||||
return match ? parseInt(match[0]) : 0;
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
const handleAddToCart = async () => {
|
||||
const availableStock = parseStock(stock);
|
||||
|
||||
// Проверяем наличие
|
||||
if (count > availableStock) {
|
||||
alert(`Недостаточно товара в наличии. Доступно: ${availableStock} шт.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const numericPrice = parsePrice(price);
|
||||
const numericOldPrice = oldPrice ? parsePrice(oldPrice) : undefined;
|
||||
|
||||
addItem({
|
||||
const result = await addItem({
|
||||
productId: productId,
|
||||
offerKey: offerKey,
|
||||
name: title,
|
||||
@ -81,6 +75,7 @@ const ProductListCard: React.FC<ProductListCardProps> = ({
|
||||
originalPrice: numericOldPrice,
|
||||
currency: currency,
|
||||
quantity: count,
|
||||
stock: availableStock, // передаем информацию о наличии
|
||||
deliveryTime: deliveryTime || delivery,
|
||||
warehouse: warehouse || address,
|
||||
supplier: supplier,
|
||||
@ -88,8 +83,13 @@ const ProductListCard: React.FC<ProductListCardProps> = ({
|
||||
image: image,
|
||||
});
|
||||
|
||||
// Показываем уведомление о добавлении
|
||||
alert(`Товар "${title}" добавлен в корзину (${count} шт.)`);
|
||||
if (result.success) {
|
||||
// Показываем уведомление о добавлении
|
||||
alert(`Товар "${title}" добавлен в корзину (${count} шт.)`);
|
||||
} else {
|
||||
// Показываем ошибку
|
||||
alert(result.error || 'Ошибка при добавлении товара в корзину');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -156,10 +156,15 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
|
||||
};
|
||||
|
||||
const handleUnitClick = (unit: LaximoUnit) => {
|
||||
setSelectedUnit({
|
||||
...unit,
|
||||
ssd: unit.ssd || ssd // Сохраняем правильный SSD в selectedUnit
|
||||
// ИСПРАВЛЕНИЕ: Сохраняем SSD узла из API ответа
|
||||
console.log('🔍 handleUnitClick - сохраняем узел с SSD:', {
|
||||
unitId: unit.unitid,
|
||||
unitName: unit.name,
|
||||
unitSsd: unit.ssd ? `${unit.ssd.substring(0, 50)}...` : 'отсутствует',
|
||||
unitSsdLength: unit.ssd?.length
|
||||
});
|
||||
|
||||
setSelectedUnit(unit); // Сохраняем полный объект узла с его SSD
|
||||
};
|
||||
|
||||
const handleBackFromUnit = () => {
|
||||
@ -209,21 +214,23 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
|
||||
|
||||
// Если выбран узел для детального просмотра, показываем UnitDetailsSection
|
||||
if (selectedUnit) {
|
||||
const unitSsd = selectedUnit.ssd || ssd;
|
||||
// ИСПРАВЛЕНИЕ: Используем SSD узла из API ответа, а не родительский SSD
|
||||
// API Laximo возвращает для каждого узла свой собственный SSD
|
||||
console.log('🔍 QuickDetailSection передает в UnitDetailsSection:', {
|
||||
unitSsd: unitSsd ? `${unitSsd.substring(0, 50)}...` : 'отсутствует',
|
||||
unitSsdLength: unitSsd?.length,
|
||||
parentSsd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
|
||||
parentSsdLength: ssd?.length,
|
||||
selectedUnitSsd: selectedUnit.ssd ? `${selectedUnit.ssd.substring(0, 50)}...` : 'отсутствует',
|
||||
fallbackSsd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
|
||||
selectedUnitSsdLength: selectedUnit.ssd?.length,
|
||||
unitId: selectedUnit.unitid,
|
||||
unitName: selectedUnit.name
|
||||
unitName: selectedUnit.name,
|
||||
note: 'Используем SSD УЗЛА из API ответа'
|
||||
});
|
||||
|
||||
return (
|
||||
<UnitDetailsSection
|
||||
catalogCode={catalogCode}
|
||||
vehicleId={vehicleId}
|
||||
ssd={unitSsd} // Используем SSD узла
|
||||
ssd={selectedUnit.ssd || ssd} // Используем SSD узла, fallback на родительский SSD
|
||||
unitId={selectedUnit.unitid}
|
||||
unitName={selectedUnit.name}
|
||||
onBack={handleBackFromUnit}
|
||||
|
@ -42,7 +42,8 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
ssd: ssd ? `${ssd.substring(0, 50)}...` : 'отсутствует',
|
||||
ssdLength: ssd?.length,
|
||||
unitId,
|
||||
unitName
|
||||
unitName,
|
||||
note: 'Используем SSD узла для API запросов'
|
||||
});
|
||||
|
||||
const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery<{ laximoUnitInfo: LaximoUnitInfo }>(
|
||||
@ -52,11 +53,11 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
unitId,
|
||||
ssd: ssd || ''
|
||||
ssd
|
||||
},
|
||||
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
|
||||
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId || !ssd || ssd.trim() === '',
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD
|
||||
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуальных данных
|
||||
notifyOnNetworkStatusChange: true
|
||||
}
|
||||
);
|
||||
@ -76,9 +77,9 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
unitId,
|
||||
ssd: ssd || ''
|
||||
ssd
|
||||
},
|
||||
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
|
||||
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId || !ssd || ssd.trim() === '',
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD
|
||||
notifyOnNetworkStatusChange: true
|
||||
@ -100,9 +101,9 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
unitId,
|
||||
ssd: ssd || ''
|
||||
ssd
|
||||
},
|
||||
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
|
||||
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId || !ssd || ssd.trim() === '',
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'no-cache', // Отключаем кэширование для получения актуального SSD
|
||||
notifyOnNetworkStatusChange: true
|
||||
|
@ -68,49 +68,49 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
||||
Найдено автомобилей: {results.length}
|
||||
</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) => (
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
{/* Заголовок автомобиля */}
|
||||
<div className="mb-3">
|
||||
<h4 className="text-lg font-semibold text-blue-600 mb-1">
|
||||
<div className="">
|
||||
<h4 className="text-lg font-semibold text-red-600 mb-1 truncate">
|
||||
{vehicle.name || `${vehicle.brand} ${vehicle.model}`}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
</h4>
|
||||
{/* <p className="text-sm text-gray-500 truncate">
|
||||
{vehicle.modification} ({vehicle.year})
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
|
||||
{/* Основные характеристики */}
|
||||
<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.model)}
|
||||
{renderAttribute('Двигатель', vehicle.engine)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Все атрибуты из API */}
|
||||
{vehicle.attributes && vehicle.attributes.length > 0 && (
|
||||
<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) => (
|
||||
<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-900">{attr.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Технические характеристики (fallback для старых данных) */}
|
||||
{(!vehicle.attributes || vehicle.attributes.length === 0) && (
|
||||
<>
|
||||
<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.bodytype)}
|
||||
{renderAttribute('Трансмиссия', vehicle.transmission)}
|
||||
@ -123,7 +123,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
||||
</div>
|
||||
|
||||
<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.engineno)}
|
||||
{renderAttribute('Дата производства', vehicle.date)}
|
||||
@ -133,7 +133,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
||||
</div>
|
||||
|
||||
<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.dateto)}
|
||||
{renderAttribute('Модельный год с', vehicle.modelyearfrom)}
|
||||
@ -143,7 +143,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
||||
{/* Опции и описание */}
|
||||
{(vehicle.options || vehicle.description || vehicle.notes) && (
|
||||
<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.description)}
|
||||
{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>
|
||||
|
@ -26,6 +26,11 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
|
||||
const [getWizard2] = useLazyQuery(GET_LAXIMO_WIZARD2, {
|
||||
onCompleted: (data) => {
|
||||
if (data.laximoWizard2) {
|
||||
console.log('🔄 Wizard обновлен:', {
|
||||
steps: data.laximoWizard2.length,
|
||||
selectedParams: Object.keys(selectedParams).length,
|
||||
currentSsd
|
||||
});
|
||||
setWizardSteps(data.laximoWizard2);
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -76,18 +81,28 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
|
||||
|
||||
// --- Автовыбор единственного варианта для всех шагов ---
|
||||
React.useEffect(() => {
|
||||
// Предотвращаем автовыбор во время загрузки
|
||||
if (isLoading) return;
|
||||
|
||||
wizardSteps.forEach(step => {
|
||||
const options = step.options || [];
|
||||
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);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line
|
||||
}, [wizardSteps, selectedParams]);
|
||||
}, [wizardSteps, selectedParams, isLoading]);
|
||||
|
||||
// Обработка выбора параметра
|
||||
const handleParamSelect = async (step: LaximoWizardStep, optionKey: string, optionValue: string) => {
|
||||
// Проверяем, не выбран ли уже этот параметр
|
||||
if (selectedParams[step.conditionid]?.key === optionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
@ -118,6 +133,13 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
|
||||
|
||||
// Сброс параметра
|
||||
const handleParamReset = async (step: LaximoWizardStep) => {
|
||||
console.log('🔄 Сброс параметра:', {
|
||||
stepName: step.name,
|
||||
conditionId: step.conditionid,
|
||||
currentSsd,
|
||||
selectedParamsBefore: Object.keys(selectedParams)
|
||||
});
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
@ -126,8 +148,33 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
|
||||
delete newSelectedParams[step.conditionid];
|
||||
setSelectedParams(newSelectedParams);
|
||||
|
||||
// Используем SSD для сброса параметра, если он есть
|
||||
const resetSsd = step.ssd || '';
|
||||
// Находим правильный 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);
|
||||
|
||||
try {
|
||||
@ -325,36 +372,36 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка поиска автомобилей */}
|
||||
{!isLoading && canListVehicles && showSearchButton && (
|
||||
<div className="pt-4 border-t">
|
||||
{/* Информация о недостаточности параметров и кнопка поиска */}
|
||||
{!isLoading && wizardSteps.length > 0 && (
|
||||
<div className="flex flex-row gap-4 items-center w-full mx-auto max-sm:flex-col max-sm:items-stretch">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleFindVehicles();
|
||||
setShowSearchButton(false);
|
||||
}}
|
||||
disabled={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"
|
||||
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 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>
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
Определено параметров: {wizardSteps.filter(s => s.determined).length} из {wizardSteps.length}
|
||||
<div
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* Информация о недостаточности параметров */}
|
||||
{!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>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { toast } from "react-hot-toast";
|
||||
import CartIcon from "../CartIcon";
|
||||
|
||||
interface ProductBuyBlockProps {
|
||||
offer?: any;
|
||||
@ -37,7 +38,7 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
|
||||
}
|
||||
|
||||
// Добавляем товар в корзину
|
||||
addItem({
|
||||
const result = await addItem({
|
||||
productId: offer.id ? String(offer.id) : undefined,
|
||||
offerKey: offer.offerKey || undefined,
|
||||
name: offer.name || `${offer.brand} ${offer.articleNumber}`,
|
||||
@ -45,6 +46,7 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
|
||||
price: offer.price,
|
||||
currency: 'RUB',
|
||||
quantity: quantity,
|
||||
stock: offer.quantity, // передаем информацию о наличии
|
||||
image: offer.image || undefined,
|
||||
brand: offer.brand,
|
||||
article: offer.articleNumber,
|
||||
@ -53,17 +55,22 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
|
||||
isExternal: offer.type === 'external'
|
||||
});
|
||||
|
||||
// Показываем успешный тоастер
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold">Товар добавлен в корзину!</div>
|
||||
<div className="text-sm text-gray-600">{offer.name || `${offer.brand} ${offer.articleNumber}`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: '🛒',
|
||||
}
|
||||
);
|
||||
if (result.success) {
|
||||
// Показываем успешный тоастер
|
||||
toast.success(
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
||||
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{offer.name || `${offer.brand} ${offer.articleNumber}`}</div>
|
||||
</div>,
|
||||
{
|
||||
duration: 3000,
|
||||
icon: <CartIcon size={20} color="#fff" />,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Показываем ошибку
|
||||
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
toast.error('Ошибка при добавлении товара в корзину');
|
||||
|
@ -15,8 +15,10 @@ const DEFAULT_MAX = 32000;
|
||||
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 [from, setFrom] = useState(value ? value[0] : min);
|
||||
const [to, setTo] = useState(value ? value[1] : max);
|
||||
const [from, setFrom] = useState<string>(value ? String(value[0]) : String(min));
|
||||
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 [trackWidth, setTrackWidth] = useState(0);
|
||||
const [open, setOpen] = useState(true);
|
||||
@ -25,11 +27,15 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
// Обновляем локальное состояние при изменении внешнего значения
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setFrom(value[0]);
|
||||
setTo(value[1]);
|
||||
setFrom(String(value[0]));
|
||||
setTo(String(value[1]));
|
||||
setConfirmedFrom(value[0]);
|
||||
setConfirmedTo(value[1]);
|
||||
} else {
|
||||
setFrom(min);
|
||||
setTo(max);
|
||||
setFrom(String(min));
|
||||
setTo(String(max));
|
||||
setConfirmedFrom(min);
|
||||
setConfirmedTo(max);
|
||||
}
|
||||
}, [value, min, max]);
|
||||
|
||||
@ -61,15 +67,15 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
x = clamp(x, 0, trackWidth);
|
||||
const value = clamp(pxToValue(x), min, max);
|
||||
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 {
|
||||
setTo(v => clamp(Math.max(value, from), from, max));
|
||||
setTo(v => String(clamp(Math.max(value, Number(from)), Number(from), max)));
|
||||
}
|
||||
};
|
||||
const onUp = () => {
|
||||
setDragging(null);
|
||||
if (onChange) {
|
||||
onChange([from, to]);
|
||||
onChange([Number(from), Number(to)]);
|
||||
}
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
@ -82,25 +88,48 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
|
||||
// Input handlers
|
||||
const handleFromInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = Number(e.target.value.replace(/\D/g, ""));
|
||||
if (isNaN(v)) v = min;
|
||||
setFrom(clamp(Math.min(v, to), min, to));
|
||||
let v = e.target.value.replace(/\D/g, "");
|
||||
setFrom(v);
|
||||
};
|
||||
const handleToInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let v = Number(e.target.value.replace(/\D/g, ""));
|
||||
if (isNaN(v)) v = max;
|
||||
setTo(clamp(Math.max(v, from), from, max));
|
||||
let v = e.target.value.replace(/\D/g, "");
|
||||
setTo(v);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
if (onChange) {
|
||||
onChange([from, to]);
|
||||
const handleFromBlur = () => {
|
||||
let v = Number(from);
|
||||
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 позиции для точек
|
||||
const pxFrom = valueToPx(from);
|
||||
const pxTo = valueToPx(to);
|
||||
const pxFrom = valueToPx(dragging ? Number(from) : confirmedFrom);
|
||||
const pxTo = valueToPx(dragging ? Number(to) : confirmedTo);
|
||||
|
||||
// Мобильная версия - без dropdown
|
||||
if (isMobile) {
|
||||
@ -124,7 +153,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
id="from"
|
||||
value={from}
|
||||
onChange={handleFromInput}
|
||||
onBlur={handleInputBlur}
|
||||
onBlur={handleFromBlur}
|
||||
onKeyDown={handleFromKeyDown}
|
||||
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@ -139,7 +169,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
id="to"
|
||||
value={to}
|
||||
onChange={handleToInput}
|
||||
onBlur={handleInputBlur}
|
||||
onBlur={handleToBlur}
|
||||
onKeyDown={handleToKeyDown}
|
||||
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@ -214,7 +245,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
id="from"
|
||||
value={from}
|
||||
onChange={handleFromInput}
|
||||
onBlur={handleInputBlur}
|
||||
onBlur={handleFromBlur}
|
||||
onKeyDown={handleFromKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="div-block-5">
|
||||
@ -228,7 +260,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
||||
id="to"
|
||||
value={to}
|
||||
onChange={handleToInput}
|
||||
onBlur={handleInputBlur}
|
||||
onBlur={handleToBlur}
|
||||
onKeyDown={handleToKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -43,30 +43,72 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
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(
|
||||
GET_LAXIMO_UNIT_INFO,
|
||||
{
|
||||
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', unitId: unitId || '', ssd: ssd || '' },
|
||||
skip: !catalogCode || !vehicleId || !unitId,
|
||||
variables: {
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
unitId,
|
||||
ssd
|
||||
},
|
||||
skip: !catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === '',
|
||||
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(
|
||||
GET_LAXIMO_UNIT_IMAGE_MAP,
|
||||
{
|
||||
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', unitId: unitId || '', ssd: ssd || '' },
|
||||
skip: !catalogCode || !vehicleId || !unitId,
|
||||
variables: {
|
||||
catalogCode,
|
||||
vehicleId,
|
||||
unitId,
|
||||
ssd
|
||||
},
|
||||
skip: !catalogCode || !vehicleId || !unitId || !ssd || ssd.trim() === '',
|
||||
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 (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="text-lg font-medium mb-2">Схема узла</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>
|
||||
);
|
||||
}
|
||||
@ -75,6 +117,29 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
const coordinates = imageMapData?.laximoUnitImageMap?.coordinates || [];
|
||||
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 img = e.currentTarget;
|
||||
@ -110,13 +175,49 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
}, [parts, coordinates]);
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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) {
|
||||
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 (
|
||||
@ -137,10 +238,10 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
/>
|
||||
{/* Точки/области */}
|
||||
{coordinates.map((coord: any, idx: number) => {
|
||||
const scaledX = coord.x * imageScale.x;
|
||||
const scaledY = coord.y * imageScale.y;
|
||||
const scaledWidth = coord.width * imageScale.x;
|
||||
const scaledHeight = coord.height * imageScale.y;
|
||||
// Кружки всегда 32x32px, центрируем по координате
|
||||
const size = 22;
|
||||
const scaledX = coord.x * imageScale.x - size / 2;
|
||||
const scaledY = coord.y * imageScale.y - size / 2;
|
||||
return (
|
||||
<div
|
||||
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
||||
@ -149,19 +250,29 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
||||
onKeyDown={e => {
|
||||
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={{
|
||||
left: scaledX,
|
||||
top: scaledY,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
width: size,
|
||||
height: size,
|
||||
background: '#B7CAE2',
|
||||
borderRadius: '50%',
|
||||
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
title={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}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -10,18 +10,14 @@ interface VinCategoryProps {
|
||||
activeTab?: 'uzly' | 'manufacturer';
|
||||
onQuickGroupSelect?: (group: any) => 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 [selectedCategory, setSelectedCategory] = useState<any>(null);
|
||||
const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick, openedPath = [], setOpenedPath = () => {} }) => {
|
||||
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
|
||||
const lastCategoryIdRef = useRef<string | null>(null);
|
||||
|
||||
// Сброс выбранной категории при смене вкладки
|
||||
useEffect(() => {
|
||||
setSelectedCategory(null);
|
||||
}, [activeTab]);
|
||||
|
||||
// Запрос для "Общие" (QuickGroups)
|
||||
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
|
||||
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 error = activeTab === 'uzly' ? quickGroupsError : categoriesError;
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedCategory(null);
|
||||
setOpenedPath(openedPath.slice(0, openedPath.length - 1));
|
||||
};
|
||||
|
||||
const handleCategoryClick = (category: any) => {
|
||||
// Если передан onCategoryClick, используем его
|
||||
const handleCategoryClick = (category: any, level: number) => {
|
||||
if (onCategoryClick) {
|
||||
onCategoryClick();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTab === 'uzly') {
|
||||
// Логика для вкладки "Общие" (QuickGroups)
|
||||
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);
|
||||
if (category.children && category.children.length > 0) {
|
||||
if (openedPath[level] === (category.quickgroupid || category.categoryid || category.id)) {
|
||||
setOpenedPath(openedPath.slice(0, level));
|
||||
} else {
|
||||
// Если нет children, грузим units (подкатегории)
|
||||
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);
|
||||
setOpenedPath([...openedPath.slice(0, level), (category.quickgroupid || category.categoryid || category.id)]);
|
||||
}
|
||||
} 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
|
||||
});
|
||||
} else {
|
||||
handleCategoryClick(subcat);
|
||||
handleCategoryClick(subcat, 0);
|
||||
}
|
||||
};
|
||||
|
||||
@ -150,7 +137,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
|
||||
<div
|
||||
className="div-block-131"
|
||||
key={cat.quickgroupid || cat.categoryid || cat.id || idx}
|
||||
onClick={() => handleCategoryClick(cat)}
|
||||
onClick={() => handleCategoryClick(cat, 0)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<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>
|
||||
<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> */}
|
||||
{subcategories.length === 0 && <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>}
|
||||
{subcategories.map((subcat: any, idx: number) => (
|
||||
<div
|
||||
className="div-block-131"
|
||||
key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx}
|
||||
onClick={() => handleSubcategoryClick(subcat)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className="text-block-57">{subcat.name}</div>
|
||||
<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>
|
||||
{(() => {
|
||||
// Найти текущий уровень вложенности для selectedCategory
|
||||
let level = 0;
|
||||
let list = categories;
|
||||
while (openedPath[level] && list) {
|
||||
const found = list.find((cat: any) => (cat.quickgroupid || cat.categoryid || cat.id) === openedPath[level]);
|
||||
if (!found) break;
|
||||
if (found === selectedCategory) break;
|
||||
list = found.children || [];
|
||||
level++;
|
||||
}
|
||||
// Теперь level - это уровень selectedCategory, подкатегории будут на level+1
|
||||
const subcategories = selectedCategory.children || [];
|
||||
if (subcategories.length === 0) return <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>;
|
||||
return subcategories.map((subcat: any, idx: number) => (
|
||||
<div
|
||||
className="div-block-131"
|
||||
key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx}
|
||||
onClick={() => handleCategoryClick(subcat, level + 1)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className="text-block-57">{subcat.name}</div>
|
||||
<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>
|
||||
|
@ -19,6 +19,9 @@ interface VinLeftbarProps {
|
||||
onNodeSelect?: (node: any) => void;
|
||||
onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void;
|
||||
onQuickGroupSelect?: (group: any) => void;
|
||||
activeTab?: 'uzly' | 'manufacturer';
|
||||
openedPath?: string[];
|
||||
setOpenedPath?: (path: string[]) => void;
|
||||
}
|
||||
|
||||
interface QuickGroup {
|
||||
@ -28,13 +31,11 @@ interface 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 vehicleId = vehicleInfo?.vehicleid || '';
|
||||
const ssd = vehicleInfo?.ssd || '';
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
||||
const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
|
||||
|
||||
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 handleToggle = (idx: number, categoryId: string) => {
|
||||
setOpenIndex(openIndex === idx ? null : idx);
|
||||
if (openIndex !== idx && !unitsByCategory[categoryId]) {
|
||||
lastCategoryIdRef.current = categoryId;
|
||||
getUnits({ variables: { catalogCode, vehicleId, ssd, categoryId } });
|
||||
const handleToggle = (categoryId: string, level: number) => {
|
||||
if (openedPath[level] === categoryId) {
|
||||
setOpenedPath(openedPath.slice(0, level));
|
||||
} else {
|
||||
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 [expandedQuickGroup, setExpandedQuickGroup] = useState<string | null>(null);
|
||||
const [expandedSubQuickGroup, setExpandedSubQuickGroup] = useState<string | null>(null);
|
||||
|
||||
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);
|
||||
}
|
||||
const handleQuickGroupToggle = (groupId: string, level: number) => {
|
||||
if (openedPath[level] === groupId) {
|
||||
setOpenedPath(openedPath.slice(0, level));
|
||||
} 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 || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (onActiveTabChange) {
|
||||
onActiveTabChange(activeTab);
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
// Если нет данных о транспортном средстве, показываем заглушку
|
||||
if (!vehicleInfo) {
|
||||
return (
|
||||
@ -281,18 +274,15 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className={
|
||||
searchQuery
|
||||
? 'button-23 w-button'
|
||||
: activeTab === 'uzly'
|
||||
: activeTabProp === 'uzly'
|
||||
? 'button-3 w-button'
|
||||
: 'button-23 w-button'
|
||||
}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (searchQuery) setSearchQuery('');
|
||||
setActiveTab('uzly');
|
||||
// Очищаем выбранную группу при смене таба
|
||||
if (onQuickGroupSelect) {
|
||||
onQuickGroupSelect(null);
|
||||
}
|
||||
if (onActiveTabChange) onActiveTabChange('uzly');
|
||||
if (onQuickGroupSelect) onQuickGroupSelect(null);
|
||||
}}
|
||||
>
|
||||
Узлы
|
||||
@ -302,25 +292,22 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className={
|
||||
searchQuery
|
||||
? 'button-23 w-button'
|
||||
: activeTab === 'manufacturer'
|
||||
: activeTabProp === 'manufacturer'
|
||||
? 'button-3 w-button'
|
||||
: 'button-23 w-button'
|
||||
}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (searchQuery) setSearchQuery('');
|
||||
setActiveTab('manufacturer');
|
||||
// Очищаем выбранную группу при смене таба
|
||||
if (onQuickGroupSelect) {
|
||||
onQuickGroupSelect(null);
|
||||
}
|
||||
if (onActiveTabChange) onActiveTabChange('manufacturer');
|
||||
if (onQuickGroupSelect) onQuickGroupSelect(null);
|
||||
}}
|
||||
>
|
||||
От производителя
|
||||
</a>
|
||||
</div>
|
||||
{/* Tab content start */}
|
||||
{activeTab === 'uzly' ? (
|
||||
{activeTabProp === 'uzly' ? (
|
||||
// Общие (QuickGroups - бывшие "От производителя")
|
||||
quickGroupsLoading ? (
|
||||
<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) => {
|
||||
const hasChildren = group.children && group.children.length > 0;
|
||||
const isOpen = expandedQuickGroup === group.quickgroupid;
|
||||
const isOpen = openedPath.includes(group.quickgroupid);
|
||||
|
||||
if (!hasChildren) {
|
||||
return (
|
||||
@ -340,7 +327,12 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className="dropdown-link-3 w-dropdown-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleQuickGroupClick(group);
|
||||
// Если это конечная группа с link=true, открываем QuickGroup
|
||||
if (group.link && onQuickGroupSelect) {
|
||||
onQuickGroupSelect(group);
|
||||
} else {
|
||||
handleQuickGroupToggle(group.quickgroupid, 0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{group.name}
|
||||
@ -357,7 +349,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
>
|
||||
<div
|
||||
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" }}
|
||||
>
|
||||
<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" : ""}`}>
|
||||
{group.children?.map((child: QuickGroup) => {
|
||||
const hasSubChildren = child.children && child.children.length > 0;
|
||||
const isChildOpen = expandedSubQuickGroup === child.quickgroupid;
|
||||
const isChildOpen = openedPath.includes(child.quickgroupid);
|
||||
|
||||
if (!hasSubChildren) {
|
||||
return (
|
||||
@ -376,7 +371,12 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className="dropdown-link-3 w-dropdown-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleQuickGroupClick(child);
|
||||
// Если это конечная группа с link=true, открываем QuickGroup
|
||||
if (child.link && onQuickGroupSelect) {
|
||||
onQuickGroupSelect(child);
|
||||
} else {
|
||||
handleQuickGroupToggle(child.quickgroupid, 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{child.name}
|
||||
@ -393,7 +393,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
>
|
||||
<div
|
||||
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" }}
|
||||
>
|
||||
<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"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleQuickGroupClick(subChild);
|
||||
// Если это конечная группа с link=true, открываем QuickGroup
|
||||
if (subChild.link && onQuickGroupSelect) {
|
||||
onQuickGroupSelect(subChild);
|
||||
} else {
|
||||
handleQuickGroupToggle(subChild.quickgroupid, 3);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{subChild.name}
|
||||
@ -434,7 +442,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
) : (
|
||||
<>
|
||||
{categories.map((category: any, idx: number) => {
|
||||
const isOpen = openIndex === idx;
|
||||
const isOpen = openedPath.includes(category.quickgroupid);
|
||||
const subcategories = category.children && category.children.length > 0
|
||||
? category.children
|
||||
: unitsByCategory[category.quickgroupid] || [];
|
||||
@ -447,7 +455,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
>
|
||||
<div
|
||||
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" }}
|
||||
>
|
||||
<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"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (onNodeSelect) {
|
||||
// Если это конечная категория с link=true, открываем QuickGroup
|
||||
if (subcat.link && onQuickGroupSelect) {
|
||||
onQuickGroupSelect(subcat);
|
||||
} else if (onNodeSelect) {
|
||||
onNodeSelect({
|
||||
...subcat,
|
||||
unitid: subcat.unitid || subcat.quickgroupid || subcat.id
|
||||
|
@ -72,8 +72,8 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
|
||||
<div className="knot-img">
|
||||
<h1 className="heading-19">{unit.name}</h1>
|
||||
|
||||
{unit.details && unit.details.length > 0 && unit.details.map((detail: any) => (
|
||||
<div className="w-layout-hflex flex-block-115" key={detail.detailid}>
|
||||
{unit.details && unit.details.length > 0 && unit.details.map((detail: any, index: number) => (
|
||||
<div className="w-layout-hflex flex-block-115" key={`${unit.unitid}-${detail.detailid || index}`}>
|
||||
<div className="oemnuber">{detail.oem}</div>
|
||||
<div className="partsname">{detail.name}</div>
|
||||
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
|
||||
|
@ -25,6 +25,8 @@ const initialState: CartState = {
|
||||
// Типы действий
|
||||
type CartAction =
|
||||
| { 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: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
|
||||
| { type: 'TOGGLE_SELECT'; payload: string }
|
||||
@ -44,6 +46,16 @@ type CartAction =
|
||||
// Функция для генерации ID
|
||||
const generateId = () => Math.random().toString(36).substr(2, 9)
|
||||
|
||||
// Утилитарная функция для парсинга количества в наличии
|
||||
const parseStock = (stockStr: string | number | undefined): number => {
|
||||
if (typeof stockStr === 'number') return stockStr;
|
||||
if (typeof stockStr === 'string') {
|
||||
const match = stockStr.match(/\d+/);
|
||||
return match ? parseInt(match[0]) : 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Функция для расчета итогов
|
||||
const calculateSummary = (items: CartItem[], deliveryPrice: number) => {
|
||||
const selectedItems = items.filter(item => item.selected)
|
||||
@ -78,9 +90,12 @@ const cartReducer = (state: CartState, action: CartAction): CartState => {
|
||||
|
||||
if (existingItemIndex >= 0) {
|
||||
// Увеличиваем количество существующего товара
|
||||
const existingItem = state.items[existingItemIndex];
|
||||
const totalQuantity = existingItem.quantity + action.payload.quantity;
|
||||
|
||||
newItems = state.items.map((item, index) =>
|
||||
index === existingItemIndex
|
||||
? { ...item, quantity: item.quantity + action.payload.quantity }
|
||||
? { ...item, quantity: totalQuantity }
|
||||
: item
|
||||
)
|
||||
} else {
|
||||
@ -335,8 +350,31 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}, [state.items, state.delivery, state.orderComment, isInitialized])
|
||||
|
||||
// Функции для работы с корзиной
|
||||
const addItem = (item: Omit<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 })
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const removeItem = (id: string) => {
|
||||
@ -344,6 +382,17 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
|
||||
const updateQuantity = (id: string, quantity: number) => {
|
||||
// Найдем товар для проверки наличия
|
||||
const item = state.items.find(item => item.id === id);
|
||||
if (item) {
|
||||
const availableStock = parseStock(item.stock);
|
||||
if (availableStock > 0 && quantity > availableStock) {
|
||||
// Показываем ошибку, но не изменяем количество
|
||||
dispatch({ type: 'SET_ERROR', payload: `Недостаточно товара в наличии. Доступно: ${availableStock} шт.` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
|
||||
}
|
||||
|
||||
@ -388,6 +437,10 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
}
|
||||
|
||||
const clearError = () => {
|
||||
dispatch({ type: 'SET_ERROR', payload: '' })
|
||||
}
|
||||
|
||||
const contextValue: CartContextType = {
|
||||
state,
|
||||
addItem,
|
||||
@ -401,7 +454,8 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
removeAll,
|
||||
removeSelected,
|
||||
updateDelivery,
|
||||
clearCart
|
||||
clearCart,
|
||||
clearError
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -4,6 +4,7 @@ import React, { createContext, useContext, useReducer, useEffect, ReactNode } fr
|
||||
import { useMutation, useQuery } from '@apollo/client'
|
||||
import toast from 'react-hot-toast'
|
||||
import { GET_FAVORITES, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES, CLEAR_FAVORITES } from '@/lib/favorites-queries'
|
||||
import DeleteCartIcon from '@/components/DeleteCartIcon'
|
||||
|
||||
// Типы
|
||||
export interface FavoriteItem {
|
||||
@ -133,7 +134,9 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
|
||||
|
||||
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
||||
onCompleted: () => {
|
||||
toast.success('Товар удален из избранного')
|
||||
toast.success('Товар удален из избранного', {
|
||||
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка удаления из избранного:', error)
|
||||
|
@ -231,6 +231,7 @@ export const useCatalogPrices = (): UseCatalogPricesReturn => {
|
||||
price: cheapestOffer.price,
|
||||
currency: cheapestOffer.currency || 'RUB',
|
||||
quantity: 1,
|
||||
stock: cheapestOffer.quantity, // передаем информацию о наличии
|
||||
deliveryTime: cheapestOffer.deliveryDays?.toString() || '0',
|
||||
warehouse: cheapestOffer.warehouse || 'Склад',
|
||||
supplier: cheapestOffer.supplierName || 'Неизвестный поставщик',
|
||||
@ -238,10 +239,14 @@ export const useCatalogPrices = (): UseCatalogPricesReturn => {
|
||||
image: '', // Убираем мокап-фотку, изображения будут загружаться отдельно
|
||||
};
|
||||
|
||||
addItem(itemToAdd);
|
||||
const result = await addItem(itemToAdd);
|
||||
|
||||
// Показываем уведомление
|
||||
toast.success(`Товар "${brand} ${articleNumber}" добавлен в корзину за ${cheapestOffer.price} ₽`);
|
||||
if (result.success) {
|
||||
// Показываем уведомление
|
||||
toast.success(`Товар "${brand} ${articleNumber}" добавлен в корзину за ${cheapestOffer.price} ₽`);
|
||||
} else {
|
||||
toast.error(result.error || 'Ошибка добавления товара в корзину');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка добавления в корзину:', error);
|
||||
|
@ -60,8 +60,12 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
},
|
||||
success: {
|
||||
duration: 3000,
|
||||
style: {
|
||||
background: '#22c55e', // Зеленый фон для успешных уведомлений
|
||||
color: '#fff', // Белый текст
|
||||
},
|
||||
iconTheme: {
|
||||
primary: '#4ade80',
|
||||
primary: '#22c55e',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
|
@ -23,6 +23,7 @@ import { useProductPrices } from '@/hooks/useProductPrices';
|
||||
import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
import toast from 'react-hot-toast';
|
||||
import CartIcon from '@/components/CartIcon';
|
||||
|
||||
const mockData = Array(12).fill({
|
||||
image: "",
|
||||
@ -720,7 +721,7 @@ export default function Catalog() {
|
||||
productId={entity.id}
|
||||
artId={entity.id}
|
||||
offerKey={priceData?.offerKey}
|
||||
onAddToCart={() => {
|
||||
onAddToCart={async () => {
|
||||
// Если цена не загружена, загружаем её и добавляем в корзину
|
||||
if (!priceData && !isLoadingPriceData) {
|
||||
loadPriceOnDemand(productForPrice);
|
||||
@ -740,6 +741,7 @@ export default function Catalog() {
|
||||
price: priceData.price,
|
||||
currency: priceData.currency || 'RUB',
|
||||
quantity: 1,
|
||||
stock: undefined, // информация о наличии не доступна для PartsIndex
|
||||
deliveryTime: '1-3 дня',
|
||||
warehouse: 'Parts Index',
|
||||
supplier: 'Parts Index',
|
||||
@ -747,10 +749,23 @@ export default function Catalog() {
|
||||
image: entity.images?.[0] || '',
|
||||
};
|
||||
|
||||
addItem(itemToAdd);
|
||||
const result = await addItem(itemToAdd);
|
||||
|
||||
// Показываем уведомление
|
||||
toast.success(`Товар "${entity.brand.name} ${entity.code}" добавлен в корзину за ${priceData.price.toLocaleString('ru-RU')} ₽`);
|
||||
if (result.success) {
|
||||
// Показываем уведомление
|
||||
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 {
|
||||
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
|
||||
}
|
||||
|
@ -606,12 +606,11 @@ export default function SearchResult() {
|
||||
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 analogOffers.length > 0;
|
||||
return hasInternalOffers || hasExternalOffers;
|
||||
});
|
||||
|
||||
// Если нет аналогов с предложениями, не показываем секцию
|
||||
@ -625,11 +624,79 @@ export default function SearchResult() {
|
||||
const analogKey = `${analog.brand}-${analog.articleNumber}`;
|
||||
const loadedAnalogData = loadedAnalogs[analogKey];
|
||||
|
||||
const analogOffers = loadedAnalogData
|
||||
? transformOffersForCard(
|
||||
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
|
||||
)
|
||||
: [];
|
||||
// Если данные аналога загружены, формируем предложения из всех его данных
|
||||
const analogOffers = loadedAnalogData ? (() => {
|
||||
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 (
|
||||
<CoreProductCard
|
||||
@ -647,20 +714,7 @@ export default function SearchResult() {
|
||||
{(() => {
|
||||
// Проверяем, есть ли еще аналоги с предложениями для загрузки
|
||||
const remainingAnalogs = result.analogs.slice(visibleAnalogsCount);
|
||||
const hasMoreAnalogsWithOffers = remainingAnalogs.some((analog: any) => {
|
||||
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;
|
||||
});
|
||||
const hasMoreAnalogsWithOffers = remainingAnalogs.length > 0;
|
||||
|
||||
return hasMoreAnalogsWithOffers && (
|
||||
<div className="w-layout-hflex pagination">
|
||||
|
@ -58,6 +58,7 @@ const VehicleDetailsPage = () => {
|
||||
const [showKnot, setShowKnot] = useState(false);
|
||||
const [foundParts, setFoundParts] = useState<any[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
||||
const [openedPath, setOpenedPath] = useState<string[]>([]);
|
||||
const [searchState, setSearchState] = useState<{
|
||||
loading: boolean;
|
||||
error: any;
|
||||
@ -329,6 +330,9 @@ const VehicleDetailsPage = () => {
|
||||
onNodeSelect={setSelectedNode}
|
||||
onActiveTabChange={(tab) => setActiveTab(tab)}
|
||||
onQuickGroupSelect={setSelectedQuickGroup}
|
||||
activeTab={activeTab}
|
||||
openedPath={openedPath}
|
||||
setOpenedPath={setOpenedPath}
|
||||
/>
|
||||
{searchState.isSearching ? (
|
||||
<div className="knot-parts">
|
||||
@ -399,6 +403,8 @@ const VehicleDetailsPage = () => {
|
||||
onNodeSelect={setSelectedNode}
|
||||
activeTab={activeTab}
|
||||
onQuickGroupSelect={setSelectedQuickGroup}
|
||||
openedPath={openedPath}
|
||||
setOpenedPath={setOpenedPath}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -408,10 +414,22 @@ const VehicleDetailsPage = () => {
|
||||
<div className="w-layout-hflex flex-block-13">
|
||||
<div className="w-layout-vflex flex-block-14-copy-copy">
|
||||
{/* <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
|
||||
catalogCode={vehicleInfo.catalog}
|
||||
vehicleId={vehicleInfo.vehicleid}
|
||||
ssd={vehicleInfo.ssd}
|
||||
ssd={selectedNode.ssd || vehicleInfo.ssd} // ИСПРАВЛЕНИЕ: Используем SSD узла, fallback на родительский SSD
|
||||
unitId={selectedNode.unitid}
|
||||
unitName={selectedNode.name}
|
||||
parts={unitDetails}
|
||||
|
@ -469,7 +469,13 @@ input#VinSearchInput {
|
||||
|
||||
|
||||
.dropdown-toggle-card {
|
||||
align-self: stretch;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 6px 15px;
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.dropdown-link-3 {
|
||||
@ -480,11 +486,17 @@ input#VinSearchInput {
|
||||
max-width: 230px;
|
||||
}
|
||||
|
||||
.dropdown-toggle-3.active, .dropdown-toggle-card.active {
|
||||
.dropdown-toggle-3.active{
|
||||
background-color: var(--background);
|
||||
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dropdown-toggle-card.active {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dropdown-toggle-3.active {
|
||||
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 {
|
||||
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;
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ export interface CartItem {
|
||||
originalPrice?: number
|
||||
currency: string
|
||||
quantity: number
|
||||
stock?: string | number // количество товара в наличии на складе
|
||||
deliveryTime?: string
|
||||
deliveryDate?: string
|
||||
warehouse?: string
|
||||
@ -52,7 +53,7 @@ export interface CartState {
|
||||
|
||||
export interface CartContextType {
|
||||
state: CartState
|
||||
addItem: (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => void
|
||||
addItem: (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => Promise<{ success: boolean; error?: string }>
|
||||
removeItem: (id: string) => void
|
||||
updateQuantity: (id: string, quantity: number) => void
|
||||
toggleSelect: (id: string) => void
|
||||
@ -64,4 +65,5 @@ export interface CartContextType {
|
||||
removeSelected: () => void
|
||||
updateDelivery: (delivery: Partial<DeliveryInfo>) => void
|
||||
clearCart: () => void
|
||||
clearError: () => void
|
||||
}
|
Reference in New Issue
Block a user