переделаны счетчки фильтр рэндж, настроены выборы категорий и подкатегорий
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
@ -27,6 +27,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 +43,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 +58,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(',', '.');
|
||||
@ -144,8 +162,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="Количество"
|
||||
/>
|
||||
|
@ -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, { 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";
|
||||
@ -52,8 +52,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,16 +91,35 @@ 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;
|
||||
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 handleInputBlur = (idx: number) => {
|
||||
if (inputValues[idx] === "") {
|
||||
setInputValues(prev => ({ ...prev, [idx]: "1" }));
|
||||
setQuantities(prev => ({ ...prev, [idx]: 1 }));
|
||||
}
|
||||
setQuantities(prev => ({ ...prev, [index]: num }));
|
||||
};
|
||||
|
||||
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 = (offer: CoreProductCardOffer, index: number) => {
|
||||
@ -291,6 +318,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 +345,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"
|
||||
|
@ -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>
|
||||
|
@ -325,36 +325,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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -137,10 +137,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 +149,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,11 @@ 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]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -117,26 +118,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 +193,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 +261,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 +279,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 +304,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 +314,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className="dropdown-link-3 w-dropdown-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleQuickGroupClick(group);
|
||||
handleQuickGroupToggle(group.quickgroupid, 0);
|
||||
}}
|
||||
>
|
||||
{group.name}
|
||||
@ -357,7 +331,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 +343,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 +353,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className="dropdown-link-3 w-dropdown-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleQuickGroupClick(child);
|
||||
handleQuickGroupToggle(child.quickgroupid, 1);
|
||||
}}
|
||||
>
|
||||
{child.name}
|
||||
@ -393,7 +370,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 +387,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
||||
className="dropdown-link-3 w-dropdown-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleQuickGroupClick(subChild);
|
||||
handleQuickGroupToggle(subChild.quickgroupid, 3);
|
||||
}}
|
||||
>
|
||||
{subChild.name}
|
||||
@ -434,7 +414,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 +427,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>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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 {
|
||||
@ -482,7 +488,7 @@ input#VinSearchInput {
|
||||
|
||||
.dropdown-toggle-3.active, .dropdown-toggle-card.active {
|
||||
background-color: var(--background);
|
||||
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dropdown-toggle-3.active {
|
||||
@ -690,3 +696,27 @@ 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;
|
||||
|
||||
}
|
Reference in New Issue
Block a user