Files
protekauto-frontend/src/components/vin/VinLeftbar.tsx

536 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from "react";
import { useLazyQuery, useQuery } from '@apollo/client';
import { GET_LAXIMO_FULLTEXT_SEARCH, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo';
import { useRouter } from 'next/router';
interface VinLeftbarProps {
vehicleInfo?: {
catalog: string;
vehicleid: string;
ssd: string;
[key: string]: any;
};
onSearchResults?: (data: {
results: any[];
loading: boolean;
error: any;
query: string;
isSearching?: boolean;
}) => void;
onNodeSelect?: (node: any) => void;
onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void;
onQuickGroupSelect?: (group: any) => void;
activeTab?: 'uzly' | 'manufacturer';
openedPath?: string[];
setOpenedPath?: (path: string[]) => void;
}
interface QuickGroup {
quickgroupid: string;
name: string;
link?: boolean;
children?: QuickGroup[];
}
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect, activeTab: activeTabProp, openedPath = [], setOpenedPath = () => {} }) => {
const router = useRouter();
const catalogCode = vehicleInfo?.catalog || '';
const vehicleId = vehicleInfo?.vehicleid || '';
const ssd = vehicleInfo?.ssd || '';
const [searchQuery, setSearchQuery] = useState('');
const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || vehicleId === undefined || vehicleId === null,
errorPolicy: 'all'
});
const categories = categoriesData?.laximoCategories || [];
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
const [getUnits] = useLazyQuery(GET_LAXIMO_UNITS, {
onCompleted: (data) => {
if (data && data.laximoUnits && lastCategoryIdRef.current) {
setUnitsByCategory(prev => ({
...prev,
[lastCategoryIdRef.current!]: data.laximoUnits || []
}));
}
}
});
const lastCategoryIdRef = React.useRef<string | null>(null);
// --- Синхронизация openedPath с URL ---
// Обновляем openedPath и URL
const setOpenedPathAndUrl = (newPath: string[]) => {
setOpenedPath(newPath);
const params = new URLSearchParams(router.query as any);
if (newPath.length > 0) {
params.set('catpath', newPath.join(','));
} else {
params.delete('catpath');
}
router.push(
{ pathname: router.pathname, query: { ...router.query, catpath: newPath.join(',') } },
undefined,
{ shallow: true }
);
};
// Восстанавливаем openedPath из URL
React.useEffect(() => {
if (typeof window === 'undefined') return;
const catpath = (router.query.catpath as string) || '';
if (catpath) {
setOpenedPath(catpath.split(',').filter(Boolean));
} else {
setOpenedPath([]);
}
}, [router.query.catpath]);
const handleToggle = (categoryId: string, level: number) => {
if (openedPath[level] === categoryId) {
setOpenedPathAndUrl(openedPath.slice(0, level));
} else {
setOpenedPathAndUrl([...openedPath.slice(0, level), categoryId]);
// Загружаем units для категории, если они еще не загружены
if (activeTabProp === 'manufacturer' && !unitsByCategory[categoryId]) {
lastCategoryIdRef.current = categoryId;
getUnits({
variables: {
catalogCode,
vehicleId,
ssd,
categoryId
}
});
}
}
};
const handleSearch = () => {
if (!searchQuery.trim()) return;
if (!ssd || ssd.trim() === '') {
console.error('SSD обязателен для поиска по названию');
return;
}
executeSearch({
variables: {
catalogCode,
vehicleId,
searchQuery: searchQuery.trim(),
ssd
}
});
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSearch();
}
};
const searchResults = data?.laximoFulltextSearch;
useEffect(() => {
if (onSearchResults) {
onSearchResults({
results: searchResults?.details || [],
loading: loading,
error: error,
query: searchQuery
});
}
}, [searchResults, loading, error, searchQuery]);
// --- Новый блок: вычисляем доступность поиска ---
const isSearchAvailable = !!catalogCode && vehicleId !== undefined && vehicleId !== null && !!ssd && ssd.trim() !== '';
const showWarning = !isSearchAvailable;
const showError = !!error && isSearchAvailable && searchQuery.trim();
const showNotFound = isSearchAvailable && searchQuery.trim() && !loading && data && searchResults && searchResults.details && searchResults.details.length === 0;
const showTips = isSearchAvailable && !searchQuery.trim() && !loading;
// --- QuickGroups (от производителя) ---
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || vehicleId === undefined || vehicleId === null,
errorPolicy: 'all'
});
const quickGroups = quickGroupsData?.laximoQuickGroups || [];
const handleQuickGroupToggle = (groupId: string, level: number) => {
if (openedPath[level] === groupId) {
setOpenedPathAndUrl(openedPath.slice(0, level));
} else {
setOpenedPathAndUrl([...openedPath.slice(0, level), groupId]);
}
};
// === Полнотекстовый поиск деталей (аналогично FulltextSearchSection) ===
const [fulltextQuery, setFulltextQuery] = useState('');
const [executeFulltextSearch, { data: fulltextData, loading: fulltextLoading, error: fulltextError }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
const handleFulltextSearch = () => {
if (!fulltextQuery.trim()) {
if (onSearchResults) {
onSearchResults({
results: [],
loading: false,
error: null,
query: '',
isSearching: false
});
}
return;
}
if (!ssd || ssd.trim() === '') {
console.error('SSD обязателен для поиска по названию');
return;
}
// Отправляем начальное состояние поиска родителю
if (onSearchResults) {
onSearchResults({
results: [],
loading: true,
error: null,
query: fulltextQuery.trim(),
isSearching: true
});
}
executeFulltextSearch({
variables: {
catalogCode,
vehicleId,
searchQuery: fulltextQuery.trim(),
ssd
}
});
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setFulltextQuery(newValue);
};
useEffect(() => {
if (onSearchResults && (fulltextData || fulltextLoading || fulltextError)) {
onSearchResults({
results: fulltextData?.laximoFulltextSearch?.details || [],
loading: fulltextLoading,
error: fulltextError,
query: fulltextQuery,
isSearching: true
});
}
}, [fulltextData, fulltextLoading, fulltextError, fulltextQuery]);
const handleFulltextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleFulltextSearch();
}
};
const fulltextResults = fulltextData?.laximoFulltextSearch?.details || [];
// Если нет данных о транспортном средстве, показываем заглушку
if (!vehicleInfo) {
return (
<div className="w-layout-vflex vinleftbar">
<div className="text-center py-8 text-gray-500">
<div className="text-lg font-medium mb-2">Поиск запчастей</div>
<div className="text-sm">Выберите автомобиль для поиска запчастей</div>
</div>
</div>
);
}
return (
<div className="w-layout-vflex vinleftbar">
{/* === Форма полнотекстового поиска === */}
<div className="div-block-2">
<div className="form-block w-form">
<form id="vin-form-search" name="vin-form-search" data-name="vin-form-search" action="#" method="post" className="form" onSubmit={e => { e.preventDefault(); handleFulltextSearch(); }}>
<a href="#" className="link-block-3 w-inline-block" onClick={e => { e.preventDefault(); handleFulltextSearch(); }}>
<div className="code-embed-6 w-embed">
{/* SVG */}
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 17.5L13.8834 13.8833" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
<path d="M9.16667 15.8333C12.8486 15.8333 15.8333 12.8486 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
</div>
</a>
<input
className="text-field w-input"
maxLength={256}
name="VinSearch"
data-name="VinSearch"
placeholder="Поиск по названию детали"
type="text"
id="VinSearchInput"
required
value={fulltextQuery}
onChange={handleInputChange}
onKeyDown={handleFulltextKeyDown}
disabled={fulltextLoading}
/>
</form>
{(!ssd || ssd.trim() === '') && (
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex">
<svg className="h-5 w-5 text-yellow-400 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
Полнотекстовый поиск недоступен
</h3>
<p className="text-sm text-yellow-700 mt-1">
Для поиска по названию деталей необходимо сначала выбрать конкретный автомобиль через поиск по VIN или мастер подбора.
</p>
</div>
</div>
</div>
)}
</div>
</div>
<div className="w-layout-vflex flex-block-113">
<div className="w-layout-hflex flex-block-114">
<a
href="#"
className={
searchQuery
? 'button-23 w-button'
: activeTabProp === 'uzly'
? 'button-3 w-button'
: 'button-23 w-button'
}
onClick={e => {
e.preventDefault();
if (searchQuery) setSearchQuery('');
if (onActiveTabChange) onActiveTabChange('uzly');
if (onQuickGroupSelect) onQuickGroupSelect(null);
}}
>
Узлы
</a>
<a
href="#"
className={
searchQuery
? 'button-23 w-button'
: activeTabProp === 'manufacturer'
? 'button-3 w-button'
: 'button-23 w-button'
}
onClick={e => {
e.preventDefault();
if (searchQuery) setSearchQuery('');
if (onActiveTabChange) onActiveTabChange('manufacturer');
if (onQuickGroupSelect) onQuickGroupSelect(null);
}}
>
От производителя
</a>
</div>
{/* Tab content start */}
{activeTabProp === 'uzly' ? (
// Общие (QuickGroups - бывшие "От производителя")
quickGroupsLoading ? (
<div style={{ padding: 16, textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
) : quickGroupsError ? (
<div style={{ color: 'red', padding: 16 }}>Ошибка загрузки групп: {quickGroupsError.message}</div>
) : (
<>
{(quickGroups as QuickGroup[]).map((group: QuickGroup) => {
const hasChildren = group.children && group.children.length > 0;
const isOpen = openedPath.includes(group.quickgroupid);
if (!hasChildren) {
return (
<a
href="#"
key={group.quickgroupid}
className="dropdown-link-3 w-dropdown-link"
onClick={(e) => {
e.preventDefault();
// Если это конечная группа с link=true, открываем QuickGroup
if (group.link && onQuickGroupSelect) {
onQuickGroupSelect(group);
} else {
handleQuickGroupToggle(group.quickgroupid, 0);
}
}}
>
{group.name}
</a>
);
}
return (
<div
key={group.quickgroupid}
data-hover="false"
data-delay="0"
className={`dropdown-4 w-dropdown${isOpen ? " w--open" : ""}`}
>
<div
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open active" : ""}`}
onClick={(e) => {
e.preventDefault();
handleQuickGroupToggle(group.quickgroupid, 0);
}}
style={{ cursor: "pointer" }}
>
<div className="w-icon-dropdown-toggle"></div>
<div className="text-block-56">{group.name}</div>
</div>
<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 = openedPath.includes(child.quickgroupid);
if (!hasSubChildren) {
return (
<a
href="#"
key={child.quickgroupid}
className="dropdown-link-3 w-dropdown-link"
onClick={(e) => {
e.preventDefault();
// Если это конечная группа с link=true, открываем QuickGroup
if (child.link && onQuickGroupSelect) {
onQuickGroupSelect(child);
} else {
handleQuickGroupToggle(child.quickgroupid, 1);
}
}}
>
{child.name}
</a>
);
}
return (
<div
key={child.quickgroupid}
data-hover="false"
data-delay="0"
className={`dropdown-4 w-dropdown pl-0${isChildOpen ? " w--open" : ""}`}
>
<div
className={`dropdown-toggle-card w-dropdown-toggle pl-0${isChildOpen ? " w--open active" : ""}`}
onClick={(e) => {
e.preventDefault();
handleQuickGroupToggle(child.quickgroupid, 2);
}}
style={{ cursor: "pointer" }}
>
<div className="w-icon-dropdown-toggle"></div>
<div className="text-block-56">{child.name}</div>
</div>
<nav className={`dropdown-list-4 w-dropdown-list pl-0${isChildOpen ? " w--open" : ""}`}>
{child.children?.map((subChild: QuickGroup) => (
<a
href="#"
key={subChild.quickgroupid}
className="dropdown-link-3 w-dropdown-link"
onClick={(e) => {
e.preventDefault();
// Если это конечная группа с link=true, открываем QuickGroup
if (subChild.link && onQuickGroupSelect) {
onQuickGroupSelect(subChild);
} else {
handleQuickGroupToggle(subChild.quickgroupid, 3);
}
}}
>
{subChild.name}
</a>
))}
</nav>
</div>
);
})}
</nav>
</div>
);
})}
</>
)
) : (
// От производителя (Categories - узлы)
categoriesLoading ? (
<div style={{ padding: 16, textAlign: 'center' }}>Загружаем категории...</div>
) : categoriesError ? (
<div style={{ color: 'red', padding: 16 }}>Ошибка загрузки категорий: {categoriesError.message}</div>
) : (
<>
{categories.map((category: any, idx: number) => {
const isOpen = openedPath.includes(category.quickgroupid);
const subcategories = category.children && category.children.length > 0
? category.children
: unitsByCategory[category.quickgroupid] || [];
return (
<div
key={category.quickgroupid}
data-hover="false"
data-delay="0"
className={`dropdown-4 w-dropdown${isOpen ? " w--open" : ""}`}
>
<div
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`}
onClick={(e) => {
e.preventDefault();
handleToggle(category.quickgroupid, 0);
}}
style={{ cursor: "pointer" }}
>
<div className="w-icon-dropdown-toggle"></div>
<div className="text-block-56">{category.name}</div>
</div>
<nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}>
{subcategories.length > 0 ? (
subcategories.map((subcat: any) => (
<a
href="#"
key={subcat.quickgroupid || subcat.unitid}
className="dropdown-link-3 w-dropdown-link pl-0"
onClick={e => {
e.preventDefault();
// Если это конечная категория с link=true, открываем QuickGroup
if (subcat.link && onQuickGroupSelect) {
onQuickGroupSelect(subcat);
} else if (onNodeSelect) {
onNodeSelect({
...subcat,
unitid: subcat.unitid || subcat.quickgroupid || subcat.id
});
}
}}
>
{subcat.name}
</a>
))
) : (
<span style={{ color: '#888', padding: 8 }}>Нет подкатегорий</span>
)}
</nav>
</div>
);
})}
</>
)
)}
{/* Tab content end */}
</div>
</div>
);
};
export default VinLeftbar;