diff --git a/src/components/BestPriceCard.tsx b/src/components/BestPriceCard.tsx index 250886f..426b3ba 100644 --- a/src/components/BestPriceCard.tsx +++ b/src/components/BestPriceCard.tsx @@ -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 = ({ 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 = ({ }; const handleInput = (e: React.ChangeEvent) => { - 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 = ({ 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 = ({ 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="Количество" /> diff --git a/src/components/CartItem.tsx b/src/components/CartItem.tsx index d5751e0..454f1f8 100644 --- a/src/components/CartItem.tsx +++ b/src/components/CartItem.tsx @@ -38,160 +38,180 @@ const CartItem: React.FC = ({ onRemove, isSummaryStep = false, itemNumber, -}) => ( -
-
- {isSummaryStep ? ( -
{itemNumber}
- ) : ( -
- {selected && ( - - - - )} -
- )} -
-

{name}

-
- {description} -
-
-
-
e.preventDefault()}> - onComment(e.target.value)} - disabled={isSummaryStep} - /> -
-
-
Thank you! Your submission has been received!
-
-
-
Oops! Something went wrong while submitting the form.
-
-
-
-
-
-

{delivery}

-
{deliveryDate}
-
-
+}) => { + // --- Фикс для input: можно стереть, при blur пустое = 1 --- + const [inputValue, setInputValue] = React.useState(count.toString()); + React.useEffect(() => { + setInputValue(count.toString()); + }, [count]); + + return ( +
+
{isSummaryStep ? ( -
{count} шт.
+
{itemNumber}
) : ( - <> -
onCountChange && onCountChange(count - 1)} - style={{ cursor: 'pointer' }} - aria-label="Уменьшить количество" - tabIndex={0} - onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)} - role="button" - > -
- - - -
+
+ {selected && ( + + + + )}
-
- { - 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 }} - /> -
-
onCountChange && onCountChange(count + 1)} - style={{ cursor: 'pointer' }} - aria-label="Увеличить количество" - tabIndex={0} - onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count + 1)} - role="button" - > -
- - - -
-
- )} -
-
-

{price}

-
{pricePerItem}
-
- {!isSummaryStep && ( -
-
- - - -
+
+

{name}

(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} +
+
+
+
e.preventDefault()}> + onComment(e.target.value)} + disabled={isSummaryStep} + /> +
+
+
Thank you! Your submission has been received!
+
+
+
Oops! Something went wrong while submitting the form.
+
+
+
+
+
+

{delivery}

+
{deliveryDate}
+
+
+ {isSummaryStep ? ( +
{count} шт.
+ ) : ( + <> +
onCountChange && onCountChange(count - 1)} + style={{ cursor: 'pointer' }} + aria-label="Уменьшить количество" + tabIndex={0} + onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)} + role="button" + > +
+ + + +
+
+
+ { + 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 }} + /> +
+
onCountChange && onCountChange(count + 1)} + style={{ cursor: 'pointer' }} + aria-label="Увеличить количество" + tabIndex={0} + onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count + 1)} + role="button" + > +
+ + + +
+
+ + )} +
+
+

{price}

+
{pricePerItem}
+
+ {!isSummaryStep && ( +
+
- +
+
(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'); + }} + > + + + +
+
+ )}
- )}
-
-); + ); +}; export default CartItem; \ No newline at end of file diff --git a/src/components/CoreProductCard.tsx b/src/components/CoreProductCard.tsx index 7abe4de..7c7cfc3 100644 --- a/src/components/CoreProductCard.tsx +++ b/src/components/CoreProductCard.tsx @@ -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 = ({ 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 = ({ 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 = ({
{displayedOffers.map((offer, idx) => { const isLast = idx === displayedOffers.length - 1; + const maxCount = parseStock(offer.pcs); return (
= ({
- +
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="Количество" />
- +
-
- Определено параметров: {wizardSteps.filter(s => s.determined).length} из {wizardSteps.length} +
+
+ info +
+
+ Выберите больше параметров для поиска автомобилей +
)} - - {/* Информация о недостаточности параметров */} - {!isLoading && !canListVehicles && wizardSteps.length > 0 && ( -
-

- Выберите больше параметров для поиска автомобилей -

-
- )}
); }; diff --git a/src/components/filters/FilterRange.tsx b/src/components/filters/FilterRange.tsx index 75a004e..86be6ce 100644 --- a/src/components/filters/FilterRange.tsx +++ b/src/components/filters/FilterRange.tsx @@ -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 = ({ 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(value ? String(value[0]) : String(min)); + const [to, setTo] = useState(value ? String(value[1]) : String(max)); + const [confirmedFrom, setConfirmedFrom] = useState(value ? value[0] : min); + const [confirmedTo, setConfirmedTo] = useState(value ? value[1] : max); const [dragging, setDragging] = useState(null); const [trackWidth, setTrackWidth] = useState(0); const [open, setOpen] = useState(true); @@ -25,11 +27,15 @@ const FilterRange: React.FC = ({ 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 = ({ 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 = ({ title, min = DEFAULT_MIN, max // Input handlers const handleFromInput = (e: React.ChangeEvent) => { - 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) => { - 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) => { + if (e.key === "Enter") { + handleFromBlur(); + (e.target as HTMLInputElement).blur(); + } + }; + const handleToKeyDown = (e: React.KeyboardEvent) => { + 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 = ({ 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%' }} />
@@ -139,7 +169,8 @@ const FilterRange: React.FC = ({ 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%' }} />
@@ -214,7 +245,8 @@ const FilterRange: React.FC = ({ title, min = DEFAULT_MIN, max id="from" value={from} onChange={handleFromInput} - onBlur={handleInputBlur} + onBlur={handleFromBlur} + onKeyDown={handleFromKeyDown} />
@@ -228,7 +260,8 @@ const FilterRange: React.FC = ({ title, min = DEFAULT_MIN, max id="to" value={to} onChange={handleToInput} - onBlur={handleInputBlur} + onBlur={handleToBlur} + onKeyDown={handleToKeyDown} />
diff --git a/src/components/vin/KnotIn.tsx b/src/components/vin/KnotIn.tsx index 3d308ff..bec0e16 100644 --- a/src/components/vin/KnotIn.tsx +++ b/src/components/vin/KnotIn.tsx @@ -137,10 +137,10 @@ const KnotIn: React.FC = ({ 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 (
= ({ 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'; + }} > - + {coord.codeonimage}
diff --git a/src/components/vin/VinCategory.tsx b/src/components/vin/VinCategory.tsx index 8c26a1d..af289ee 100644 --- a/src/components/vin/VinCategory.tsx +++ b/src/components/vin/VinCategory.tsx @@ -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 = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick }) => { - const [selectedCategory, setSelectedCategory] = useState(null); +const VinCategory: React.FC = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick, openedPath = [], setOpenedPath = () => {} }) => { const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({}); const lastCategoryIdRef = useRef(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 = ({ 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 = ({ 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 = ({ catalogCode, vehicleId, ssd,
handleCategoryClick(cat)} + onClick={() => handleCategoryClick(cat, 0)} style={{ cursor: "pointer" }} >
{cat.name}
@@ -165,32 +152,37 @@ const VinCategory: React.FC = ({ catalogCode, vehicleId, ssd, ) : ( // Список подкатегорий <> - {/*
-
← Назад
-
- - - - -
-
*/} - {subcategories.length === 0 &&
Нет подкатегорий
} - {subcategories.map((subcat: any, idx: number) => ( -
handleSubcategoryClick(subcat)} - style={{ cursor: "pointer" }} - > -
{subcat.name}
-
- - - - + {(() => { + // Найти текущий уровень вложенности для 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
Нет подкатегорий
; + return subcategories.map((subcat: any, idx: number) => ( +
handleCategoryClick(subcat, level + 1)} + style={{ cursor: "pointer" }} + > +
{subcat.name}
+
+ + + + +
-
- ))} + )); + })()} )}
diff --git a/src/components/vin/VinLeftbar.tsx b/src/components/vin/VinLeftbar.tsx index 2267871..182496b 100644 --- a/src/components/vin/VinLeftbar.tsx +++ b/src/components/vin/VinLeftbar.tsx @@ -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 = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect }) => { +const VinLeftbar: React.FC = ({ 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(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 = ({ vehicleInfo, onSearchResults, o const lastCategoryIdRef = React.useRef(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 = ({ vehicleInfo, onSearchResults, o }); const quickGroups = quickGroupsData?.laximoQuickGroups || []; - const [expandedQuickGroup, setExpandedQuickGroup] = useState(null); - const [expandedSubQuickGroup, setExpandedSubQuickGroup] = useState(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 = ({ 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 = ({ 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 = ({ 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); }} > От производителя
{/* Tab content start */} - {activeTab === 'uzly' ? ( + {activeTabProp === 'uzly' ? ( // Общие (QuickGroups - бывшие "От производителя") quickGroupsLoading ? (
Загружаем группы быстрого поиска...
@@ -330,7 +304,7 @@ const VinLeftbar: React.FC = ({ 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 = ({ 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 = ({ vehicleInfo, onSearchResults, o >
handleQuickGroupToggle(group.quickgroupid)} + onClick={(e) => { + e.preventDefault(); + handleQuickGroupToggle(group.quickgroupid, 0); + }} style={{ cursor: "pointer" }} >
@@ -366,7 +343,7 @@ const VinLeftbar: React.FC = ({ vehicleInfo, onSearchResults, o