13 Commits

Author SHA1 Message Date
6af1ed325c Merge pull request 'ура епте' (#10) from end into main
Reviewed-on: #10
2025-06-30 17:29:00 +03:00
1f1ea8baaf ура епте 2025-06-30 17:28:15 +03:00
8a953d32ae Merge pull request '1452' (#9) from vinleftbaandsearch into main
Reviewed-on: #9
2025-06-30 14:57:52 +03:00
69ccc786ea 1452 2025-06-30 14:52:30 +03:00
8cae029d7f Обновлены запросы GraphQL для поиска: заменен SEARCH_LAXIMO_FULLTEXT на GET_LAXIMO_FULLTEXT_SEARCH в компонентах FulltextSearchSection и VinLeftbar. Удален устаревший запрос SEARCH_LAXIMO_FULLTEXT из файла graphql.ts. 2025-06-30 00:48:55 +03:00
cbf50691c4 Merge pull request 'all cart' (#8) from frontcart into main
Reviewed-on: #8
2025-06-30 00:40:36 +03:00
4a3da4d5c5 Обновлены условия пропуска запросов в компонентах, чтобы учитывать случаи, когда vehicleId может быть undefined или null. Исправлены проверки в следующих компонентах: CatalogGroupsSection, CategoriesSection, GroupDetailsSection, QuickGroupsSection, UnitDetailsSection, UnitsSection, KnotIn, VinCategory, VinLeftbar, VehicleDetailsPage и PartDetailPage. 2025-06-30 00:39:55 +03:00
215853e8c7 all cart 2025-06-30 00:38:29 +03:00
f894b7e023 Merge pull request 'all fixes' (#7) from front into main
Reviewed-on: #7
2025-06-29 21:46:12 +03:00
a879e5e5e7 all fixes 2025-06-29 21:45:30 +03:00
85f7634158 Merge pull request 'checkbox' (#6) from numbers into main
Reviewed-on: #6
2025-06-29 15:45:16 +03:00
d62db55160 checkbox 2025-06-29 15:44:36 +03:00
5e454a7367 Merge pull request 'pravkiend cart' (#5) from cartandfavorite into main
Reviewed-on: #5
2025-06-29 12:37:29 +03:00
35 changed files with 1375 additions and 509 deletions

View File

@ -52,7 +52,7 @@ const BrandSelectionModal: React.FC<BrandSelectionModalProps> = ({
return ( return (
<div <div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" className="fixed inset-0 bg-black/10 bg-opacity-50 flex items-center justify-center z-50 p-4"
onClick={handleBackdropClick} onClick={handleBackdropClick}
> >
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden"> <div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden">

View File

@ -16,6 +16,8 @@ interface CartItemProps {
onComment: (comment: string) => void; onComment: (comment: string) => void;
onCountChange?: (count: number) => void; onCountChange?: (count: number) => void;
onRemove?: () => void; onRemove?: () => void;
isSummaryStep?: boolean;
itemNumber?: number;
} }
const CartItem: React.FC<CartItemProps> = ({ const CartItem: React.FC<CartItemProps> = ({
@ -34,9 +36,14 @@ const CartItem: React.FC<CartItemProps> = ({
onComment, onComment,
onCountChange, onCountChange,
onRemove, onRemove,
isSummaryStep = false,
itemNumber,
}) => ( }) => (
<div className="w-layout-hflex cart-item"> <div className="w-layout-hflex cart-item">
<div className="w-layout-hflex info-block-search-copy"> <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 <div
className={"div-block-7" + (selected ? " active" : "")} className={"div-block-7" + (selected ? " active" : "")}
onClick={onSelect} onClick={onSelect}
@ -48,9 +55,22 @@ const CartItem: React.FC<CartItemProps> = ({
</svg> </svg>
)} )}
</div> </div>
)}
<div className="w-layout-hflex block-name"> <div className="w-layout-hflex block-name">
<h4 className="heading-9-copy">{name}</h4> <h4 className="heading-9-copy">{name}</h4>
<div className="text-block-21-copy">{description}</div> <div
className={
"text-block-21-copy" +
(isSummaryStep && itemNumber === 1 ? " border-t-0" : "")
}
style={
isSummaryStep && itemNumber === 1
? { borderTop: 'none' }
: undefined
}
>
{description}
</div>
</div> </div>
<div className="form-block-copy w-form"> <div className="form-block-copy w-form">
<form className="form-copy" onSubmit={e => e.preventDefault()}> <form className="form-copy" onSubmit={e => e.preventDefault()}>
@ -64,6 +84,7 @@ const CartItem: React.FC<CartItemProps> = ({
id="Search-5" id="Search-5"
value={comment} value={comment}
onChange={e => onComment(e.target.value)} onChange={e => onComment(e.target.value)}
disabled={isSummaryStep}
/> />
</form> </form>
<div className="success-message w-form-done"> <div className="success-message w-form-done">
@ -80,6 +101,10 @@ const CartItem: React.FC<CartItemProps> = ({
<div className="text-block-21-copy-copy">{deliveryDate}</div> <div className="text-block-21-copy-copy">{deliveryDate}</div>
</div> </div>
<div className="w-layout-hflex pcs-cart-s1"> <div className="w-layout-hflex pcs-cart-s1">
{isSummaryStep ? (
<div className="text-block-26" style={{ fontWeight: 600, fontSize: 14 }}>{count} шт.</div>
) : (
<>
<div <div
className="minus-plus" className="minus-plus"
onClick={() => onCountChange && onCountChange(count - 1)} onClick={() => onCountChange && onCountChange(count - 1)}
@ -124,11 +149,14 @@ const CartItem: React.FC<CartItemProps> = ({
</svg> </svg>
</div> </div>
</div> </div>
</>
)}
</div> </div>
<div className="w-layout-hflex flex-block-39-copy-copy"> <div className="w-layout-hflex flex-block-39-copy-copy">
<h4 className="price-in-cart-s1">{price}</h4> <h4 className="price-in-cart-s1">{price}</h4>
<div className="price-1-pcs-cart-s1">{pricePerItem}</div> <div className="price-1-pcs-cart-s1">{pricePerItem}</div>
</div> </div>
{!isSummaryStep && (
<div className="w-layout-hflex control-element"> <div className="w-layout-hflex control-element">
<div className="favorite-icon w-embed" onClick={onFavorite} style={{ cursor: 'pointer', color: favorite ? '#e53935' : undefined }}> <div className="favorite-icon w-embed" onClick={onFavorite} style={{ cursor: 'pointer', color: favorite ? '#e53935' : undefined }}>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -161,6 +189,7 @@ const CartItem: React.FC<CartItemProps> = ({
</svg> </svg>
</div> </div>
</div> </div>
)}
</div> </div>
</div> </div>
); );

View File

@ -3,7 +3,11 @@ import CartItem from "./CartItem";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext"; import { useFavorites } from "@/contexts/FavoritesContext";
const CartList: React.FC = () => { interface CartListProps {
isSummaryStep?: boolean;
}
const CartList: React.FC<CartListProps> = ({ isSummaryStep = false }) => {
const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart(); const { state, toggleSelect, updateComment, removeItem, selectAll, removeSelected, updateQuantity } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const { items } = state; const { items } = state;
@ -25,24 +29,18 @@ const CartList: React.FC = () => {
const handleFavorite = (id: string) => { const handleFavorite = (id: string) => {
const item = items.find(item => item.id === id); const item = items.find(item => item.id === id);
if (!item) return; if (!item) return;
const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand); const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand);
if (isInFavorites) { if (isInFavorites) {
// Находим товар в избранном по правильному ID
const favoriteItem = favorites.find((fav: any) => { const favoriteItem = favorites.find((fav: any) => {
// Проверяем по разным комбинациям идентификаторов
if (item.productId && fav.productId === item.productId) return true; if (item.productId && fav.productId === item.productId) return true;
if (item.offerKey && fav.offerKey === item.offerKey) return true; if (item.offerKey && fav.offerKey === item.offerKey) return true;
if (fav.article === item.article && fav.brand === item.brand) return true; if (fav.article === item.article && fav.brand === item.brand) return true;
return false; return false;
}); });
if (favoriteItem) { if (favoriteItem) {
removeFromFavorites(favoriteItem.id); removeFromFavorites(favoriteItem.id);
} }
} else { } else {
// Добавляем в избранное
addToFavorites({ addToFavorites({
productId: item.productId, productId: item.productId,
offerKey: item.offerKey, offerKey: item.offerKey,
@ -68,14 +66,17 @@ const CartList: React.FC = () => {
updateQuantity(id, count); updateQuantity(id, count);
}; };
// Функция для форматирования цены
const formatPrice = (price: number, currency: string = 'RUB') => { const formatPrice = (price: number, currency: string = 'RUB') => {
return `${price.toLocaleString('ru-RU')} ${currency === 'RUB' ? '₽' : currency}`; return `${price.toLocaleString('ru-RU')} ${currency === 'RUB' ? '₽' : currency}`;
}; };
// На втором шаге показываем только выбранные товары
const displayItems = isSummaryStep ? items.filter(item => item.selected) : items;
return ( return (
<div className="w-layout-vflex flex-block-48"> <div className="w-layout-vflex flex-block-48">
<div className="w-layout-vflex product-list-cart"> <div className="w-layout-vflex product-list-cart">
{!isSummaryStep && (
<div className="w-layout-hflex multi-control"> <div className="w-layout-hflex multi-control">
<div className="w-layout-hflex select-all-block" onClick={handleSelectAll} style={{ cursor: 'pointer' }}> <div className="w-layout-hflex select-all-block" onClick={handleSelectAll} style={{ cursor: 'pointer' }}>
<div <div
@ -110,17 +111,28 @@ const CartList: React.FC = () => {
</svg> </svg>
</div> </div>
</div> </div>
{items.length === 0 ? ( )}
{displayItems.length === 0 ? (
<div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}> <div className="empty-cart-message" style={{ textAlign: 'center', padding: '2rem', color: '#666' }}>
<p>Ваша корзина пуста</p> <p>Ваша корзина пуста</p>
<p>Добавьте товары из каталога</p> <p>Добавьте товары из каталога</p>
</div> </div>
) : ( ) : (
items.map((item) => { displayItems.map((item, idx) => {
const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand); const isInFavorites = isFavorite(item.productId, item.offerKey, item.article, item.brand);
return ( return (
<div className="div-block-21" key={item.id}> <div
className={
"div-block-21" +
(isSummaryStep && idx === 0 ? " border-t-0" : "")
}
style={
isSummaryStep && idx === 0
? { borderTop: 'none' }
: undefined
}
key={item.id}
>
<CartItem <CartItem
name={item.name} name={item.name}
description={item.description} description={item.description}
@ -137,6 +149,8 @@ const CartList: React.FC = () => {
onComment={(comment) => handleComment(item.id, comment)} onComment={(comment) => handleComment(item.id, comment)}
onCountChange={(count) => handleCountChange(item.id, count)} onCountChange={(count) => handleCountChange(item.id, count)}
onRemove={() => handleRemove(item.id)} onRemove={() => handleRemove(item.id)}
isSummaryStep={isSummaryStep}
itemNumber={idx + 1}
/> />
</div> </div>
); );

View File

@ -26,8 +26,17 @@ const CartList2: React.FC = () => {
<p>Вернитесь на предыдущий шаг и выберите товары</p> <p>Вернитесь на предыдущий шаг и выберите товары</p>
</div> </div>
) : ( ) : (
selectedItems.map((item) => ( selectedItems.map((item, index) => (
<div className="div-block-21-copy" key={item.id}> <div
className={
"div-block-21-copy" +
(index === 0 ? " border-t-0" : "")
}
style={
index === 0 ? { borderTop: 'none' } : undefined
}
key={item.id}
>
<div className="w-layout-hflex cart-item-check"> <div className="w-layout-hflex cart-item-check">
<div className="w-layout-hflex info-block-search"> <div className="w-layout-hflex info-block-search">
<div className="text-block-35">{item.quantity}</div> <div className="text-block-35">{item.quantity}</div>

View File

@ -5,7 +5,12 @@ import { useMutation, useQuery } from "@apollo/client";
import { CREATE_ORDER, CREATE_PAYMENT, GET_CLIENT_ME, GET_CLIENT_DELIVERY_ADDRESSES } from "@/lib/graphql"; import { CREATE_ORDER, CREATE_PAYMENT, GET_CLIENT_ME, GET_CLIENT_DELIVERY_ADDRESSES } from "@/lib/graphql";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
const CartSummary: React.FC = () => { interface CartSummaryProps {
step: number;
setStep: (step: number) => void;
}
const CartSummary: React.FC<CartSummaryProps> = ({ step, setStep }) => {
const { state, updateDelivery, updateOrderComment, clearCart } = useCart(); const { state, updateDelivery, updateOrderComment, clearCart } = useCart();
const { summary, delivery, items, orderComment } = state; const { summary, delivery, items, orderComment } = state;
const legalEntityDropdownRef = useRef<HTMLDivElement>(null); const legalEntityDropdownRef = useRef<HTMLDivElement>(null);
@ -16,7 +21,6 @@ const CartSummary: React.FC = () => {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [showAuthWarning, setShowAuthWarning] = useState(false); const [showAuthWarning, setShowAuthWarning] = useState(false);
const [step, setStep] = useState(1);
// Новые состояния для первого шага // Новые состояния для первого шага
const [selectedLegalEntity, setSelectedLegalEntity] = useState<string>(""); const [selectedLegalEntity, setSelectedLegalEntity] = useState<string>("");
@ -135,25 +139,20 @@ const CartSummary: React.FC = () => {
toast.error('Пожалуйста, введите имя получателя'); toast.error('Пожалуйста, введите имя получателя');
return; return;
} }
if (!recipientPhone.trim()) { if (!recipientPhone.trim()) {
toast.error('Пожалуйста, введите телефон получателя'); toast.error('Пожалуйста, введите телефон получателя');
return; return;
} }
if (!selectedDeliveryAddress.trim()) { if (!selectedDeliveryAddress.trim()) {
toast.error('Пожалуйста, выберите адрес доставки'); toast.error('Пожалуйста, выберите адрес доставки');
return; return;
} }
// Обновляем данные доставки без стоимости
updateDelivery({ updateDelivery({
address: selectedDeliveryAddress, address: selectedDeliveryAddress,
cost: 0, // Стоимость включена в товары cost: 0,
date: 'Включена в стоимость товаров', date: 'Включена в стоимость товаров',
time: 'Способ доставки указан в адресе' time: 'Способ доставки указан в адресе'
}); });
setStep(2); setStep(2);
}; };
@ -894,7 +893,7 @@ const CartSummary: React.FC = () => {
{error && <div style={{ color: 'red', marginTop: 10 }}>{error}</div>} {error && <div style={{ color: 'red', marginTop: 10 }}>{error}</div>}
{/* Кнопка "Назад" */} {/* Кнопка "Назад" */}
{/* <button <button
onClick={handleBackToStep1} onClick={handleBackToStep1}
style={{ style={{
background: 'none', background: 'none',
@ -908,7 +907,7 @@ const CartSummary: React.FC = () => {
}} }}
> >
Назад к настройкам доставки Назад к настройкам доставки
</button> */} </button>
<div className="w-layout-hflex privacy-consent" style={{ cursor: 'pointer' }} onClick={() => setConsent((v) => !v)}> <div className="w-layout-hflex privacy-consent" style={{ cursor: 'pointer' }} onClick={() => setConsent((v) => !v)}>
<div <div

View File

@ -208,7 +208,7 @@ const CatalogGroupsSection: React.FC<CatalogGroupsSectionProps> = ({
vehicleId, vehicleId,
...(ssd && ssd.trim() !== '' && { ssd }) ...(ssd && ssd.trim() !== '' && { ssd })
}, },
skip: !catalogCode || !vehicleId || catalogType !== 'quickGroups', skip: !catalogCode || vehicleId === undefined || vehicleId === null || catalogType !== 'quickGroups',
errorPolicy: 'all' errorPolicy: 'all'
} }
); );

View File

@ -33,7 +33,7 @@ const CategoriesSection: React.FC<CategoriesSectionProps> = ({
vehicleId, vehicleId,
...(ssd && ssd.trim() !== '' && { ssd }) ...(ssd && ssd.trim() !== '' && { ssd })
}, },
skip: !catalogCode || !vehicleId, skip: !catalogCode || vehicleId === undefined || vehicleId === null,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useLazyQuery } from '@apollo/client'; import { useLazyQuery } from '@apollo/client';
import { LaximoFulltextSearchResult, LaximoFulltextDetail, LaximoOEMResult } from '@/types/laximo'; import { LaximoFulltextSearchResult, LaximoFulltextDetail, LaximoOEMResult } from '@/types/laximo';
import { SEARCH_LAXIMO_FULLTEXT, SEARCH_LAXIMO_OEM } from '@/lib/graphql'; import { GET_LAXIMO_FULLTEXT_SEARCH, SEARCH_LAXIMO_OEM } from '@/lib/graphql/laximo';
import PartDetailCard from './PartDetailCard'; import PartDetailCard from './PartDetailCard';
interface FulltextSearchSectionProps { interface FulltextSearchSectionProps {
@ -17,7 +17,7 @@ const FulltextSearchSection: React.FC<FulltextSearchSectionProps> = ({
}) => { }) => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [executeSearch, { data, loading, error }] = useLazyQuery(SEARCH_LAXIMO_FULLTEXT, { const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, {
errorPolicy: 'all' errorPolicy: 'all'
}); });

View File

@ -187,13 +187,13 @@ const GroupDetailsSection: React.FC<GroupDetailsSectionProps> = ({
const { data, loading, error } = useQuery<{ laximoQuickDetail: LaximoQuickDetail }>( const { data, loading, error } = useQuery<{ laximoQuickDetail: LaximoQuickDetail }>(
GET_LAXIMO_QUICK_DETAIL, GET_LAXIMO_QUICK_DETAIL,
{ {
variables: { variables: quickGroupId ? {
catalogCode, catalogCode,
vehicleId, vehicleId,
quickGroupId, quickGroupId,
ssd ssd
}, } : undefined,
skip: !catalogCode || !vehicleId || !quickGroupId || !ssd, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !quickGroupId || !ssd || ssd.trim() === '',
errorPolicy: 'all' errorPolicy: 'all'
} }
); );

View File

@ -30,7 +30,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
onSuccess={handleAuthSuccess} onSuccess={handleAuthSuccess}
/> />
</header> </header>
<main className="pt-[132px]">{children}</main> <main className="pt-[108px] md:pt-[131px]">{children}</main>
<MobileMenuBottomSection onOpenAuthModal={() => setAuthModalOpen(true)} /> <MobileMenuBottomSection onOpenAuthModal={() => setAuthModalOpen(true)} />
</> </>
); );

View File

@ -154,13 +154,13 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
const { data: quickDetailData, loading: quickDetailLoading, error: quickDetailError } = useQuery<{ laximoQuickDetail: LaximoQuickDetail }>( const { data: quickDetailData, loading: quickDetailLoading, error: quickDetailError } = useQuery<{ laximoQuickDetail: LaximoQuickDetail }>(
GET_LAXIMO_QUICK_DETAIL, GET_LAXIMO_QUICK_DETAIL,
{ {
variables: { variables: selectedGroup?.quickgroupid ? {
catalogCode, catalogCode,
vehicleId, vehicleId,
quickGroupId: selectedGroup.quickgroupid, quickGroupId: selectedGroup.quickgroupid,
ssd ssd
}, } : undefined,
skip: !catalogCode || !vehicleId || !selectedGroup.quickgroupid || !ssd, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !selectedGroup?.quickgroupid || !ssd || ssd.trim() === '',
errorPolicy: 'all', errorPolicy: 'all',
fetchPolicy: 'cache-and-network' // Принудительно запрашиваем данные fetchPolicy: 'cache-and-network' // Принудительно запрашиваем данные
} }
@ -169,11 +169,28 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
const quickDetail = quickDetailData?.laximoQuickDetail; const quickDetail = quickDetailData?.laximoQuickDetail;
// Добавляем отладочную информацию // Добавляем отладочную информацию
console.log('🔍 QuickDetailSection Debug:'); console.log('🔍 QuickDetailSection Debug:', {
console.log('📊 quickDetailData:', quickDetailData); catalogCode,
console.log('📋 quickDetail:', quickDetail); vehicleId,
console.log('🏗️ quickDetail.units:', quickDetail?.units); vehicleIdType: typeof vehicleId,
console.log('⚙️ Variables:', { catalogCode, vehicleId, quickGroupId: selectedGroup.quickgroupid, ssd }); quickGroupId: selectedGroup?.quickgroupid,
quickGroupIdType: typeof selectedGroup?.quickgroupid,
ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует',
ssdType: typeof ssd,
ssdLength: ssd?.length,
hasData: !!quickDetailData,
hasQuickDetail: !!quickDetail,
unitsCount: quickDetail?.units?.length || 0,
loading: quickDetailLoading,
error: quickDetailError?.message,
skipCondition: !catalogCode || vehicleId === undefined || vehicleId === null || !selectedGroup?.quickgroupid || !ssd,
skipDetails: {
noCatalogCode: !catalogCode,
noVehicleId: vehicleId === undefined || vehicleId === null,
noQuickGroupId: !selectedGroup?.quickgroupid,
noSsd: !ssd
}
});
// Если выбран узел для детального просмотра, показываем UnitDetailsSection // Если выбран узел для детального просмотра, показываем UnitDetailsSection
if (selectedUnit) { if (selectedUnit) {
@ -213,6 +230,20 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
} }
if (quickDetailError) { if (quickDetailError) {
console.error('🚨 QuickDetailSection Error Details:', {
message: quickDetailError.message,
graphQLErrors: quickDetailError.graphQLErrors,
networkError: quickDetailError.networkError,
extraInfo: quickDetailError.extraInfo,
selectedGroup: selectedGroup,
variables: selectedGroup?.quickgroupid ? {
catalogCode,
vehicleId,
quickGroupId: selectedGroup.quickgroupid,
ssd
} : 'undefined (no variables sent)'
});
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -231,6 +262,33 @@ const QuickDetailSection: React.FC<QuickDetailSectionProps> = ({
<h3 className="text-lg font-medium text-red-600 mb-2">Ошибка загрузки деталей</h3> <h3 className="text-lg font-medium text-red-600 mb-2">Ошибка загрузки деталей</h3>
<p className="text-red-700">Не удалось загрузить детали для группы "{selectedGroup.name}"</p> <p className="text-red-700">Не удалось загрузить детали для группы "{selectedGroup.name}"</p>
<p className="text-sm text-red-600 mt-2">Ошибка: {quickDetailError.message}</p> <p className="text-sm text-red-600 mt-2">Ошибка: {quickDetailError.message}</p>
{/* Отладочная информация */}
<details className="mt-4">
<summary className="text-sm text-red-700 cursor-pointer hover:text-red-800">
🔧 Показать отладочную информацию
</summary>
<div className="mt-2 p-3 bg-red-100 rounded text-xs">
<div><strong>Catalog Code:</strong> {catalogCode}</div>
<div><strong>Vehicle ID:</strong> {vehicleId} (type: {typeof vehicleId})</div>
<div><strong>Quick Group ID:</strong> {selectedGroup?.quickgroupid} (type: {typeof selectedGroup?.quickgroupid})</div>
<div><strong>SSD:</strong> {ssd ? `${ssd.substring(0, 100)}...` : 'отсутствует'} (length: {ssd?.length})</div>
<div className="mt-2">
<strong>GraphQL Errors:</strong>
<pre className="mt-1 text-xs overflow-auto">
{JSON.stringify(quickDetailError.graphQLErrors, null, 2)}
</pre>
</div>
{quickDetailError.networkError && (
<div className="mt-2">
<strong>Network Error:</strong>
<pre className="mt-1 text-xs overflow-auto">
{JSON.stringify(quickDetailError.networkError, null, 2)}
</pre>
</div>
)}
</div>
</details>
</div> </div>
</div> </div>
); );
@ -464,7 +522,7 @@ const QuickGroupsSection: React.FC<QuickGroupsSectionProps> = ({
vehicleId, vehicleId,
...(ssd && ssd.trim() !== '' && { ssd }) ...(ssd && ssd.trim() !== '' && { ssd })
}, },
skip: !catalogCode || !vehicleId, skip: !catalogCode || vehicleId === undefined || vehicleId === null,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );

View File

@ -39,7 +39,7 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
unitId, unitId,
ssd: ssd || '' ssd: ssd || ''
}, },
skip: !catalogCode || !vehicleId || !unitId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );
@ -54,7 +54,7 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
unitId, unitId,
ssd: ssd || '' ssd: ssd || ''
}, },
skip: !catalogCode || !vehicleId || !unitId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );
@ -69,7 +69,7 @@ const UnitDetailsSection: React.FC<UnitDetailsSectionProps> = ({
unitId, unitId,
ssd: ssd || '' ssd: ssd || ''
}, },
skip: !catalogCode || !vehicleId || !unitId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );

View File

@ -59,7 +59,7 @@ const UnitsSection: React.FC<UnitsSectionProps> = ({
categoryId, categoryId,
...(ssd && ssd.trim() !== '' && { ssd }) ...(ssd && ssd.trim() !== '' && { ssd })
}, },
skip: !catalogCode || !vehicleId || !categoryId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || !categoryId,
errorPolicy: 'all', errorPolicy: 'all',
fetchPolicy: 'no-cache', // Полностью отключаем кэширование для гарантии свежих данных fetchPolicy: 'no-cache', // Полностью отключаем кэширование для гарантии свежих данных
notifyOnNetworkStatusChange: true notifyOnNetworkStatusChange: true

View File

@ -110,7 +110,7 @@ const LegalEntityListBlock: React.FC<LegalEntityListBlockProps> = ({ legalEntiti
</div> </div>
<div <div
layer-name="link_control_element" layer-name="link_control_element"
className="flex relative gap-1.5 items-center cursor-pointer hover:text-red-600" className="flex relative gap-1.5 items-center cursor-pointer group"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => router.push('/profile-requisites')} onClick={() => router.push('/profile-requisites')}
@ -130,7 +130,7 @@ const LegalEntityListBlock: React.FC<LegalEntityListBlockProps> = ({ legalEntiti
</div> </div>
<div <div
layer-name="Редактировать" layer-name="Редактировать"
className="text-sm leading-5 text-gray-600" className="text-sm leading-5 text-gray-600 group-hover:text-red-600"
> >
Реквизиты компании Реквизиты компании
</div> </div>
@ -141,8 +141,9 @@ const LegalEntityListBlock: React.FC<LegalEntityListBlockProps> = ({ legalEntiti
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
className="flex relative gap-1.5 items-center cursor-pointer hover:text-red-600" className="flex relative gap-1.5 items-center cursor-pointer group"
onClick={() => onEdit && onEdit(entity)} onClick={() => onEdit && onEdit(entity)}
aria-label="Редактировать юридическое лицо"
> >
<div className="relative h-4 w-[18px]"> <div className="relative h-4 w-[18px]">
<Image <Image
@ -153,26 +154,37 @@ const LegalEntityListBlock: React.FC<LegalEntityListBlockProps> = ({ legalEntiti
className="absolute left-0.5 top-0" className="absolute left-0.5 top-0"
/> />
</div> </div>
<div className="text-sm leading-5 text-gray-600"> <div className="text-sm leading-5 text-gray-600 group-hover:text-red-600">
Редактировать Редактировать
</div> </div>
</div> </div>
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
className="flex relative gap-1.5 items-center cursor-pointer hover:text-red-600" className="flex relative gap-1.5 items-center cursor-pointer group"
aria-label="Удалить юридическое лицо"
onClick={() => handleDelete(entity.id, entity.shortName)} onClick={() => handleDelete(entity.id, entity.shortName)}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleDelete(entity.id, entity.shortName)}
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');
}}
> >
<div className="relative h-4 w-[18px]"> <div className="relative h-4 w-4">
<Image <svg width="16" height="16" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
src="/images/delete.svg" <path
alt="Удалить" 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"
width={16} fill="#D0D0D0"
height={16} style={{ transition: 'fill 0.2s' }}
className="absolute left-0.5 top-0"
/> />
</svg>
</div> </div>
<div className="text-sm leading-5 text-gray-600"> <div className="text-sm leading-5 text-gray-600 group-hover:text-red-600">
Удалить Удалить
</div> </div>
</div> </div>

View File

@ -52,14 +52,50 @@ const ProfileAddressCard: React.FC<ProfileAddressCardProps> = ({
</div> </div>
</div> </div>
)} )}
<div className="flex justify-between items-start self-stretch"> <div className="flex justify-between items-start self-stretch max-sm:flex-row max-sm:gap-4 max-sm:justify-start max-sm:items-center">
<div className="flex gap-1.5 items-center cursor-pointer group" onClick={onEdit}> <div
<img src="/images/edit.svg" alt="edit" width={18} height={18} className="mr-1.5 group-hover:filter-red" /> className="flex gap-1.5 items-center cursor-pointer group"
<div className="relative text-sm leading-5 text-gray-600">Редактировать</div> onClick={onEdit}
role="button"
tabIndex={0}
aria-label="Редактировать адрес"
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onEdit && onEdit()}
onMouseEnter={e => {
const svg = (e.currentTarget as HTMLElement).querySelector('img');
if (svg) (svg as HTMLImageElement).style.filter = 'invert(32%) sepia(97%) saturate(7490%) hue-rotate(355deg) brightness(97%) contrast(108%)';
}}
onMouseLeave={e => {
const svg = (e.currentTarget as HTMLElement).querySelector('img');
if (svg) (svg as HTMLImageElement).style.filter = '';
}}
>
<img src="/images/edit.svg" alt="edit" width={18} height={18} className="mr-1.5" />
<div className="relative text-sm leading-5 text-gray-600 group-hover:text-red-600 max-sm:hidden">Редактировать</div>
</div> </div>
<div className="flex gap-1.5 items-center cursor-pointer group" onClick={onDelete}> <div
<img src="/images/delete.svg" alt="delete" width={18} height={18} className="mr-1.5 group-hover:filter-red" /> className="flex gap-1.5 items-center cursor-pointer group"
<div className="relative text-sm leading-5 text-gray-600">Удалить</div> role="button"
tabIndex={0}
aria-label="Удалить адрес"
onClick={onDelete}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onDelete && onDelete()}
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="18" 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 className="relative text-sm leading-5 text-gray-600 group-hover:text-red-600 max-sm:hidden">Удалить</div>
</div> </div>
</div> </div>
{onSelectMain && ( {onSelectMain && (

View File

@ -89,11 +89,11 @@ const ProfileBalanceCard: React.FC<ProfileBalanceCardProps> = ({
{balance} {balance}
</div> </div>
</div> </div>
<div className="flex flex-row gap-5 items-end mt-5 w-full max-sm:flex-col"> <div className="flex flex-row gap-5 items-end mt-5 w-full max-sm:flex-col max-sm:items-start">
<div className="flex flex-col flex-1 shrink basis-0"> <div className="flex flex-col flex-1 shrink basis-0">
<div className="flex flex-col min-w-[160px]"> <div className="flex flex-col min-w-[160px]">
<div className="text-sm leading-snug text-gray-600">Лимит отсрочки</div> <div className="text-sm leading-snug text-gray-600">Лимит отсрочки</div>
<div className="flex flex-col self-start mt-2"> <div className="flex flex-col mt-2">
<div className="text-lg font-medium leading-none text-gray-950">{limit}</div> <div className="text-lg font-medium leading-none text-gray-950">{limit}</div>
<div className={`text-sm leading-snug ${isOverLimit ? 'text-red-600' : 'text-gray-600'}`}> <div className={`text-sm leading-snug ${isOverLimit ? 'text-red-600' : 'text-gray-600'}`}>
{limitLeft.includes('Не установлен') ? limitLeft : `Осталось ${limitLeft}`} {limitLeft.includes('Не установлен') ? limitLeft : `Осталось ${limitLeft}`}

View File

@ -179,7 +179,7 @@ const ProfileGarageMain = () => {
{!vehiclesLoading && filteredVehicles.map((vehicle) => ( {!vehiclesLoading && filteredVehicles.map((vehicle) => (
<div key={vehicle.id} className="mt-8"> <div key={vehicle.id} className="mt-8">
<div className="flex flex-col justify-center pr-5 py-3 w-full rounded-lg bg-slate-50 max-md:max-w-full"> <div className="flex flex-col justify-center px-5 py-3 w-full rounded-lg bg-slate-50 max-md:max-w-full">
<div className="flex flex-wrap gap-8 items-center w-full max-md:max-w-full"> <div className="flex flex-wrap gap-8 items-center w-full max-md:max-w-full">
<div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2"> <div className="flex gap-8 items-center self-stretch my-auto min-w-[240px] max-md:flex-col max-md:min-w-0 max-md:gap-2">
<div className="self-stretch my-auto text-xl font-bold leading-none text-gray-950"> <div className="self-stretch my-auto text-xl font-bold leading-none text-gray-950">
@ -203,15 +203,29 @@ const ProfileGarageMain = () => {
<div className="flex gap-5 items-center self-stretch pr-2.5 my-auto text-sm leading-snug text-gray-600 whitespace-nowrap"> <div className="flex gap-5 items-center self-stretch pr-2.5 my-auto text-sm leading-snug text-gray-600 whitespace-nowrap">
<button <button
type="button" type="button"
className="flex gap-1.5 items-center self-stretch my-auto cursor-pointer text-sm leading-snug text-gray-600 hover:text-red-600 transition-colors" className="flex gap-1.5 items-center self-stretch my-auto text-sm leading-snug text-gray-600 cursor-pointer bg-transparent group"
style={{ outline: 'none' }}
aria-label="Удалить автомобиль"
tabIndex={0}
onClick={() => handleDeleteVehicle(vehicle.id)} onClick={() => handleDeleteVehicle(vehicle.id)}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleDeleteVehicle(vehicle.id)}
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');
}}
> >
<img <svg width="16" height="16" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
loading="lazy" <path
src="/images/delete.svg" 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"
className="object-contain shrink-0 self-stretch my-auto aspect-[1.12] w-[18px]" fill="#D0D0D0"
style={{ transition: 'fill 0.2s' }}
/> />
<span className="self-stretch my-auto text-gray-600"> </svg>
<span className="self-stretch my-auto text-gray-600 group-hover:text-red-600">
Удалить Удалить
</span> </span>
</button> </button>
@ -418,15 +432,29 @@ const ProfileGarageMain = () => {
</div> </div>
<button <button
type="button" type="button"
className="flex gap-1.5 items-center self-stretch my-auto text-sm leading-snug text-gray-600 cursor-pointer bg-transparent hover:text-red-600 transition-colors" className="flex gap-1.5 items-center self-stretch my-auto text-sm leading-snug text-gray-600 cursor-pointer bg-transparent group"
style={{ outline: 'none' }}
aria-label="Удалить из истории поиска"
tabIndex={0}
onClick={() => handleDeleteFromHistory(historyItem.id)} onClick={() => handleDeleteFromHistory(historyItem.id)}
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleDeleteFromHistory(historyItem.id)}
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');
}}
> >
<img <svg width="16" height="16" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
loading="lazy" <path
src="/images/delete.svg" 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"
className="object-contain shrink-0 self-stretch my-auto aspect-[1.12] w-[18px]" fill="#D0D0D0"
style={{ transition: 'fill 0.2s' }}
/> />
<span className="self-stretch my-auto text-gray-600"> </svg>
<span className="self-stretch my-auto text-gray-600 group-hover:text-red-600">
Удалить Удалить
</span> </span>
</button> </button>

View File

@ -80,24 +80,25 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
<div className="w-16 text-center max-md:w-full"> <div className="w-16 text-center max-md:w-full">
<button <button
onClick={handleDeleteClick} onClick={handleDeleteClick}
className="p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors group" className="flex items-center p-2 group"
title="Удалить из истории" title="Удалить из истории"
aria-label="Удалить из истории" aria-label="Удалить из истории"
tabIndex={0}
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 <svg width="16" height="16" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
width="16" <path
height="16" 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"
viewBox="0 0 24 24" fill="#D0D0D0"
fill="none" style={{ transition: 'fill 0.2s' }}
stroke="currentColor" />
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-colors"
>
<path d="M3 6h18" className="group-hover:stroke-[#ec1c24]" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" className="group-hover:stroke-[#ec1c24]" />
<path d="M8 6V4c0-1 1-2 2-2h4c-1 0 2 1 2 2v2" className="group-hover:stroke-[#ec1c24]" />
</svg> </svg>
</button> </button>
</div> </div>

View File

@ -272,7 +272,7 @@ const ProfileHistoryMain = () => {
return ( return (
<div className="flex flex-col min-h-[526px]"> <div className="flex flex-col min-h-[526px]">
<div className="flex flex-wrap gap-5 items-center px-8 py-3 w-full leading-snug text-gray-400 whitespace-nowrap bg-white rounded-lg max-md:px-5 max-md:max-w-full max-md:flex-col"> <div className="flex gap-5 items-center px-8 py-3 w-full leading-snug text-gray-400 whitespace-nowrap bg-white rounded-lg max-md:px-5 max-md:max-w-full">
<div className="flex-1 shrink self-stretch my-auto text-gray-400 basis-0 text-ellipsis max-md:max-w-full max-md:w-full"> <div className="flex-1 shrink self-stretch my-auto text-gray-400 basis-0 text-ellipsis max-md:max-w-full max-md:w-full">
<SearchInput <SearchInput
value={search} value={search}
@ -280,7 +280,7 @@ const ProfileHistoryMain = () => {
placeholder="Поиск в истории..." placeholder="Поиск в истории..."
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2 max-sm:hidden">
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && ( {(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
<button <button
onClick={() => { onClick={() => {

View File

@ -89,7 +89,7 @@ const ProfileHistoryTabs: React.FC<ProfileHistoryTabsProps> = ({
</div> </div>
))} ))}
<div <div
className="relative w-[240px] max-w-full" className="relative w-[240px] max-w-full max-sm:w-full"
ref={dropdownRef} ref={dropdownRef}
tabIndex={0} tabIndex={0}
> >

View File

@ -10,7 +10,7 @@ const ProfileSettingsActionsBlock: React.FC<ProfileSettingsActionsBlockProps> =
Сохранить изменения Сохранить изменения
</div> </div>
<div className="gap-2.5 self-stretch px-5 py-4 my-auto rounded-xl border border-red-600 min-h-[50px] min-w-[240px] cursor-pointer bg-white text-gray-950" onClick={onAddLegalEntity}> <div className="gap-2.5 self-stretch px-5 py-4 my-auto rounded-xl border border-red-600 min-h-[50px] min-w-[240px] cursor-pointer bg-white text-gray-950" onClick={onAddLegalEntity}>
Добавить юридическое лицо Добавить юр лицо
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,27 @@
import React from "react"; import React, { useRef, useState } from "react";
import { useQuery } from '@apollo/client';
import { useRouter } from 'next/router';
import { GET_LAXIMO_UNIT_INFO, GET_LAXIMO_UNIT_IMAGE_MAP } from '@/lib/graphql';
import BrandSelectionModal from '../BrandSelectionModal';
interface KnotInProps {
catalogCode: string;
vehicleId: string;
ssd?: string;
unitId: string;
unitName?: string;
parts?: Array<{
detailid?: string;
codeonimage?: string | number;
oem?: string;
name?: string;
price?: string | number;
brand?: string;
availability?: string;
note?: string;
attributes?: Array<{ key: string; name?: string; value: string }>;
}>;
}
// Функция для корректного формирования URL изображения // Функция для корректного формирования URL изображения
const getImageUrl = (baseUrl: string, size: string) => { const getImageUrl = (baseUrl: string, size: string) => {
@ -11,26 +34,139 @@ const getImageUrl = (baseUrl: string, size: string) => {
.replace('%size%', size); .replace('%size%', size);
}; };
const KnotIn = ({ node }: { node: any }) => { const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, unitName, parts }) => {
if (!node) return null; const imgRef = useRef<HTMLImageElement>(null);
let imageUrl = ''; const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
if (node.imageurl) { const selectedImageSize = 'source';
imageUrl = getImageUrl(node.imageurl, '250'); const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
} else if (node.largeimageurl) { const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
imageUrl = node.largeimageurl; const router = useRouter();
// Получаем инфо об узле (для картинки)
const { data: unitInfoData, loading: unitInfoLoading, error: unitInfoError } = useQuery(
GET_LAXIMO_UNIT_INFO,
{
variables: { catalogCode, vehicleId, unitId, ssd: ssd || '' },
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
errorPolicy: 'all',
} }
);
// Получаем карту координат
const { data: imageMapData, loading: imageMapLoading, error: imageMapError } = useQuery(
GET_LAXIMO_UNIT_IMAGE_MAP,
{
variables: { catalogCode, vehicleId, unitId, ssd: ssd || '' },
skip: !catalogCode || vehicleId === undefined || vehicleId === null || !unitId,
errorPolicy: 'all',
}
);
const unitInfo = unitInfoData?.laximoUnitInfo;
const coordinates = imageMapData?.laximoUnitImageMap?.coordinates || [];
const imageUrl = unitInfo?.imageurl ? getImageUrl(unitInfo.imageurl, selectedImageSize) : '';
// Масштабируем точки после загрузки картинки
const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
const img = e.currentTarget;
if (!img.naturalWidth || !img.naturalHeight) return;
setImageScale({
x: img.offsetWidth / img.naturalWidth,
y: img.offsetHeight / img.naturalHeight,
});
};
// Клик по точке: найти part по codeonimage/detailid и открыть BrandSelectionModal
const handlePointClick = (codeonimage: string | number) => {
if (!parts) return;
console.log('Клик по точке:', codeonimage, 'Все детали:', parts);
const part = parts.find(
(p) =>
(p.codeonimage && p.codeonimage.toString() === codeonimage.toString()) ||
(p.detailid && p.detailid.toString() === codeonimage.toString())
);
console.log('Найдена деталь для точки:', part);
if (part?.oem) {
setSelectedDetail({ oem: part.oem, name: part.name || '' });
setIsBrandModalOpen(true);
} else {
console.warn('Нет артикула (oem) для выбранной точки:', codeonimage, part);
}
};
// Для отладки: вывести детали и координаты
React.useEffect(() => {
console.log('KnotIn parts:', parts);
console.log('KnotIn coordinates:', coordinates);
}, [parts, coordinates]);
if (unitInfoLoading || imageMapLoading) {
return <div className="text-center py-8 text-gray-500">Загружаем схему узла...</div>;
}
if (unitInfoError) {
return <div className="text-center py-8 text-red-600">Ошибка загрузки схемы: {unitInfoError.message}</div>;
}
if (!imageUrl) {
return <div className="text-center py-8 text-gray-400">Нет изображения для этого узла</div>;
}
return ( return (
<div className="knotin"> <>
{imageUrl ? ( <div className="relative inline-block">
<img src={imageUrl} loading="lazy" alt={node.name || "Изображение узла"} className="image-26" /> {/* ВРЕМЕННО: выводим количество точек для быстрой проверки */}
) : ( <div style={{ position: 'absolute', top: 4, left: 4, zIndex: 20, background: 'rgba(255,0,0,0.1)', color: '#c00', fontWeight: 700, fontSize: 14, padding: '2px 8px', borderRadius: 6 }}>
<div style={{ width: 200, height: 200, background: '#eee', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> {coordinates.length} точек
Нет изображения
</div> </div>
)} <img
{/* <div style={{ marginTop: 8, fontWeight: 500 }}>{node.name}</div> */} ref={imgRef}
src={imageUrl}
loading="lazy"
alt={unitName || unitInfo?.name || "Изображение узла"}
onLoad={handleImageLoad}
className="max-w-full h-auto mx-auto rounded"
style={{ maxWidth: 400, display: 'block' }}
/>
{/* Точки/области */}
{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;
return (
<div
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
tabIndex={0}
aria-label={`Деталь ${coord.codeonimage}`}
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"
style={{
left: scaledX,
top: scaledY,
width: scaledWidth,
height: scaledHeight,
borderRadius: '50%',
pointerEvents: 'auto',
}}
title={coord.codeonimage}
onClick={() => handlePointClick(coord.codeonimage)}
>
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none">
{coord.codeonimage}
</span>
</div> </div>
); );
})}
</div>
{/* Модалка выбора бренда */}
<BrandSelectionModal
isOpen={isBrandModalOpen}
onClose={() => setIsBrandModalOpen(false)}
articleNumber={selectedDetail?.oem || ''}
detailName={selectedDetail?.name || ''}
/>
</>
);
}; };
export default KnotIn; export default KnotIn;

View File

@ -1,5 +1,6 @@
import React from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import BrandSelectionModal from '../BrandSelectionModal';
interface KnotPartsProps { interface KnotPartsProps {
parts: Array<{ parts: Array<{
@ -13,14 +14,30 @@ interface KnotPartsProps {
note?: string; note?: string;
attributes?: Array<{ key: string; name?: string; value: string }>; attributes?: Array<{ key: string; name?: string; value: string }>;
}>; }>;
selectedCodeOnImage?: string | number;
} }
const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => { const KnotParts: React.FC<KnotPartsProps> = ({ parts, selectedCodeOnImage }) => {
const router = useRouter(); const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
const [selectedDetail, setSelectedDetail] = useState<{ oem: string; name: string } | null>(null);
const handlePriceClick = (part: any) => {
if (part.oem) {
setSelectedDetail({ oem: part.oem, name: part.name || '' });
setIsBrandModalOpen(true);
}
};
return ( return (
<>
<div className="knot-parts"> <div className="knot-parts">
{parts.map((part, idx) => ( {parts.map((part, idx) => {
<div className="w-layout-hflex knotlistitem" key={part.detailid || idx}> const isSelected = part.codeonimage && part.codeonimage === selectedCodeOnImage;
return (
<div
className={`w-layout-hflex knotlistitem border rounded transition-colors duration-150 ${isSelected ? 'bg-yellow-100 border-yellow-400' : 'border-transparent'}`}
key={part.detailid || idx}
>
<div className="w-layout-hflex flex-block-116"> <div className="w-layout-hflex flex-block-116">
<div className="nuberlist">{part.codeonimage || idx + 1}</div> <div className="nuberlist">{part.codeonimage || idx + 1}</div>
<div className="oemnuber">{part.oem}</div> <div className="oemnuber">{part.oem}</div>
@ -29,11 +46,7 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
<div className="w-layout-hflex flex-block-117"> <div className="w-layout-hflex flex-block-117">
<button <button
className="button-3 w-button" className="button-3 w-button"
onClick={() => { onClick={() => handlePriceClick(part)}
if (part.oem) {
router.push(`/search?q=${encodeURIComponent(part.oem)}&mode=parts`);
}
}}
> >
Цена Цена
</button> </button>
@ -44,8 +57,16 @@ const KnotParts: React.FC<KnotPartsProps> = ({ parts }) => {
</div> </div>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
<BrandSelectionModal
isOpen={isBrandModalOpen}
onClose={() => setIsBrandModalOpen(false)}
articleNumber={selectedDetail?.oem || ''}
detailName={selectedDetail?.name || ''}
/>
</>
); );
}; };

View File

@ -1,74 +1,129 @@
import React, { useState, useRef } from "react"; import React, { useState, useEffect, useRef } from 'react';
import { useQuery, useLazyQuery } from "@apollo/client"; import { useQuery, useLazyQuery } from '@apollo/client';
import { GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS } from "@/lib/graphql/laximo"; import { GET_LAXIMO_CATEGORIES, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_UNITS } from '@/lib/graphql/laximo';
interface VinCategoryProps { interface VinCategoryProps {
catalogCode: string; catalogCode: string;
vehicleId: string; vehicleId: string;
ssd?: string; ssd: string;
onNodeSelect?: (node: any) => void; onNodeSelect?: (node: any) => void;
activeTab: 'uzly' | 'manufacturer';
onQuickGroupSelect?: (group: any) => void;
} }
const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect }) => { const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab, onQuickGroupSelect }) => {
const [selectedCategory, setSelectedCategory] = useState<any>(null);
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
const lastCategoryIdRef = useRef<string | null>(null);
// Сброс выбранной категории при смене вкладки
useEffect(() => {
setSelectedCategory(null);
}, [activeTab]);
// Запрос для "Узлы"
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, { const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
variables: { catalogCode, vehicleId, ssd }, variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || !vehicleId, skip: !catalogCode || vehicleId === undefined || vehicleId === null || activeTab !== 'uzly',
errorPolicy: "all", errorPolicy: 'all'
}); });
const categories = categoriesData?.laximoCategories || [];
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({}); // Запрос для получения units (подкатегорий) в режиме "Узлы"
const [getUnits] = useLazyQuery(GET_LAXIMO_UNITS, { const [getUnits] = useLazyQuery(GET_LAXIMO_UNITS, {
onCompleted: (data) => { onCompleted: (data) => {
console.log('Units loaded:', data);
if (data && data.laximoUnits && lastCategoryIdRef.current) { if (data && data.laximoUnits && lastCategoryIdRef.current) {
setUnitsByCategory((prev) => ({ console.log('Setting units for category:', lastCategoryIdRef.current, data.laximoUnits);
setUnitsByCategory(prev => ({
...prev, ...prev,
[lastCategoryIdRef.current!]: data.laximoUnits || [], [lastCategoryIdRef.current!]: data.laximoUnits || []
})); }));
} }
}, },
onError: (error) => {
console.error('Error loading units:', error);
}
}); });
const [selectedCategory, setSelectedCategory] = useState<any | null>(null);
const lastCategoryIdRef = useRef<string | null>(null);
// Если выбрана категория — показываем подкатегории (children или units) // Запрос для "От производителя"
let subcategories: any[] = []; const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
if (selectedCategory) { variables: { catalogCode, vehicleId, ssd },
if (selectedCategory.children && selectedCategory.children.length > 0) { skip: !catalogCode || vehicleId === undefined || vehicleId === null || activeTab !== 'manufacturer',
subcategories = selectedCategory.children; errorPolicy: 'all'
} else { });
subcategories = unitsByCategory[selectedCategory.quickgroupid] || [];
}
}
const handleCategoryClick = (cat: any) => { const categories = activeTab === 'uzly' ? (categoriesData?.laximoCategories || []) : (quickGroupsData?.laximoQuickGroups || []);
if (cat.children && cat.children.length > 0) { const loading = activeTab === 'uzly' ? categoriesLoading : quickGroupsLoading;
setSelectedCategory(cat); const error = activeTab === 'uzly' ? categoriesError : quickGroupsError;
} else {
// Если нет children, грузим units (подкатегории)
if (!unitsByCategory[cat.quickgroupid]) {
lastCategoryIdRef.current = cat.quickgroupid;
getUnits({ variables: { catalogCode, vehicleId, ssd, categoryId: cat.quickgroupid } });
}
setSelectedCategory(cat);
}
};
const handleBack = () => { const handleBack = () => {
setSelectedCategory(null); setSelectedCategory(null);
}; };
const handleSubcategoryClick = (subcat: any) => { const handleCategoryClick = (category: any) => {
if (onNodeSelect) { if (activeTab === 'manufacturer') {
onNodeSelect({ if (category.children && category.children.length > 0) {
...subcat, setSelectedCategory(category);
unitid: subcat.unitid || subcat.quickgroupid || subcat.id, } else if (category.link && onQuickGroupSelect) {
onQuickGroupSelect(category);
} else if (onNodeSelect) {
onNodeSelect(category);
}
} else {
// Логика для вкладки "Узлы"
if (category.children && category.children.length > 0) {
setSelectedCategory(category);
} else {
// Если нет children, грузим units (подкатегории)
const categoryId = category.categoryid || category.quickgroupid || category.id;
if (!unitsByCategory[categoryId]) {
lastCategoryIdRef.current = categoryId;
console.log('Loading units for category:', { categoryId, category });
getUnits({
variables: {
catalogCode,
vehicleId,
ssd,
categoryId
}
}); });
} }
setSelectedCategory(category);
}
}
}; };
if (categoriesLoading) return <div>Загрузка категорий...</div>; const handleSubcategoryClick = (subcat: any) => {
if (categoriesError) return <div style={{ color: "red" }}>Ошибка: {categoriesError.message}</div>; if (activeTab === 'uzly' && onNodeSelect) {
// Для режима "Узлы" при клике на подкатегорию открываем KnotIn
onNodeSelect({
...subcat,
unitid: subcat.unitid || subcat.categoryid || subcat.quickgroupid || subcat.id
});
} else {
handleCategoryClick(subcat);
}
};
if (loading) return <div>Загрузка категорий...</div>;
if (error) return <div style={{ color: "red" }}>Ошибка: {error.message}</div>;
// Определяем, какие подкатегории показывать
let subcategories: any[] = [];
if (selectedCategory) {
if (activeTab === 'manufacturer') {
// Для вкладки "От производителя" используем children
subcategories = selectedCategory.children || [];
} else {
// Для вкладки "Узлы" используем либо children, либо units
if (selectedCategory.children && selectedCategory.children.length > 0) {
subcategories = selectedCategory.children;
} else {
const categoryId = selectedCategory.categoryid || selectedCategory.quickgroupid || selectedCategory.id;
subcategories = unitsByCategory[categoryId] || [];
}
}
}
return ( return (
<div className="w-layout-vflex flex-block-14-copy-copy"> <div className="w-layout-vflex flex-block-14-copy-copy">
@ -77,7 +132,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
categories.map((cat: any, idx: number) => ( categories.map((cat: any, idx: number) => (
<div <div
className="div-block-131" className="div-block-131"
key={cat.quickgroupid || cat.id || idx} key={cat.quickgroupid || cat.categoryid || cat.id || idx}
onClick={() => handleCategoryClick(cat)} onClick={() => handleCategoryClick(cat)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
@ -91,7 +146,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
</div> </div>
)) ))
) : ( ) : (
// Список подкатегорий (children или units) // Список подкатегорий
<> <>
<div className="div-block-131" onClick={handleBack} style={{ cursor: "pointer", fontWeight: 500 }}> <div className="div-block-131" onClick={handleBack} style={{ cursor: "pointer", fontWeight: 500 }}>
<div className="text-block-57"> Назад</div> <div className="text-block-57"> Назад</div>
@ -106,7 +161,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
{subcategories.map((subcat: any, idx: number) => ( {subcategories.map((subcat: any, idx: number) => (
<div <div
className="div-block-131" className="div-block-131"
key={subcat.quickgroupid || subcat.unitid || subcat.id || idx} key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx}
onClick={() => handleSubcategoryClick(subcat)} onClick={() => handleSubcategoryClick(subcat)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useLazyQuery, useQuery } from '@apollo/client'; import { useLazyQuery, useQuery } from '@apollo/client';
import { SEARCH_LAXIMO_FULLTEXT, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo'; import { GET_LAXIMO_FULLTEXT_SEARCH, GET_LAXIMO_CATEGORIES, GET_LAXIMO_UNITS, GET_LAXIMO_QUICK_GROUPS, GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo';
import VinPartCard from './VinPartCard'; import VinPartCard from './VinPartCard';
interface VinLeftbarProps { interface VinLeftbarProps {
@ -10,22 +10,37 @@ interface VinLeftbarProps {
ssd: string; ssd: string;
[key: string]: any; [key: string]: any;
}; };
onSearchResults?: (results: any[]) => void; onSearchResults?: (data: {
results: any[];
loading: boolean;
error: any;
query: string;
isSearching?: boolean;
}) => void;
onNodeSelect?: (node: any) => void; onNodeSelect?: (node: any) => void;
onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void;
onQuickGroupSelect?: (group: any) => void;
} }
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect }) => { interface QuickGroup {
quickgroupid: string;
name: string;
link?: boolean;
children?: QuickGroup[];
}
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect }) => {
const catalogCode = vehicleInfo.catalog; const catalogCode = vehicleInfo.catalog;
const vehicleId = vehicleInfo.vehicleid; const vehicleId = vehicleInfo.vehicleid;
const ssd = vehicleInfo.ssd; const ssd = vehicleInfo.ssd;
const [openIndex, setOpenIndex] = useState<number | null>(null); const [openIndex, setOpenIndex] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly'); const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
const [executeSearch, { data, loading, error }] = useLazyQuery(SEARCH_LAXIMO_FULLTEXT, { errorPolicy: 'all' }); const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, { const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
variables: { catalogCode, vehicleId, ssd }, variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || !vehicleId, skip: !catalogCode || vehicleId === undefined || vehicleId === null,
errorPolicy: 'all' errorPolicy: 'all'
}); });
const categories = categoriesData?.laximoCategories || []; const categories = categoriesData?.laximoCategories || [];
@ -79,16 +94,18 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
const searchResults = data?.laximoFulltextSearch; const searchResults = data?.laximoFulltextSearch;
useEffect(() => { useEffect(() => {
if (searchResults && onSearchResults) { if (onSearchResults) {
onSearchResults(searchResults.details || []); onSearchResults({
results: searchResults?.details || [],
loading: loading,
error: error,
query: searchQuery
});
} }
if (!searchQuery.trim() && onSearchResults) { }, [searchResults, loading, error, searchQuery, onSearchResults]);
onSearchResults([]);
}
}, [searchResults, searchQuery, onSearchResults]);
// --- Новый блок: вычисляем доступность поиска --- // --- Новый блок: вычисляем доступность поиска ---
const isSearchAvailable = !!catalogCode && !!vehicleId && !!ssd && ssd.trim() !== ''; const isSearchAvailable = !!catalogCode && vehicleId !== undefined && vehicleId !== null && !!ssd && ssd.trim() !== '';
const showWarning = !isSearchAvailable; const showWarning = !isSearchAvailable;
const showError = !!error && isSearchAvailable && searchQuery.trim(); const showError = !!error && isSearchAvailable && searchQuery.trim();
const showNotFound = isSearchAvailable && searchQuery.trim() && !loading && data && searchResults && searchResults.details && searchResults.details.length === 0; const showNotFound = isSearchAvailable && searchQuery.trim() && !loading && data && searchResults && searchResults.details && searchResults.details.length === 0;
@ -98,7 +115,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null); const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, { const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
variables: { catalogCode, vehicleId, ssd }, variables: { catalogCode, vehicleId, ssd },
skip: !catalogCode || !vehicleId || activeTab !== 'manufacturer', skip: !catalogCode || vehicleId === undefined || vehicleId === null || activeTab !== 'manufacturer',
errorPolicy: 'all' errorPolicy: 'all'
}); });
const quickGroups = quickGroupsData?.laximoQuickGroups || []; const quickGroups = quickGroupsData?.laximoQuickGroups || [];
@ -136,12 +153,30 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
const skipQuickDetail = const skipQuickDetail =
!selectedQuickGroup || !selectedQuickGroup ||
!catalogCode || !catalogCode ||
!vehicleId || vehicleId === undefined ||
!selectedQuickGroup.quickgroupid || vehicleId === null ||
!ssd; !selectedQuickGroup?.quickgroupid ||
!ssd ||
ssd.trim() === '';
console.log('QuickDetail QUERY VARS', {
catalogCode,
vehicleId,
quickGroupId: selectedQuickGroup?.quickgroupid,
ssd: ssd ? `${ssd.substring(0, 30)}...` : 'отсутствует'
});
console.log('QuickDetail SKIP CONDITIONS', {
hasSelectedQuickGroup: !!selectedQuickGroup,
hasCatalogCode: !!catalogCode,
hasVehicleId: vehicleId !== undefined && vehicleId !== null,
hasQuickGroupId: !!selectedQuickGroup?.quickgroupid,
hasSsd: !!ssd && ssd.trim() !== '',
skipQuickDetail
});
const { data: quickDetailData, loading: quickDetailLoading, error: quickDetailError } = useQuery(GET_LAXIMO_QUICK_DETAIL, { const { data: quickDetailData, loading: quickDetailLoading, error: quickDetailError } = useQuery(GET_LAXIMO_QUICK_DETAIL, {
variables: selectedQuickGroup ? { variables: selectedQuickGroup?.quickgroupid && !skipQuickDetail ? {
catalogCode, catalogCode,
vehicleId, vehicleId,
quickGroupId: selectedQuickGroup.quickgroupid, quickGroupId: selectedQuickGroup.quickgroupid,
@ -152,40 +187,86 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
}); });
const quickDetail = quickDetailData?.laximoQuickDetail; const quickDetail = quickDetailData?.laximoQuickDetail;
const renderQuickGroupTree = (groups: any[], level = 0): React.ReactNode => ( // === Полнотекстовый поиск деталей (аналогично FulltextSearchSection) ===
<div> const [fulltextQuery, setFulltextQuery] = useState('');
{groups.map(group => ( const [executeFulltextSearch, { data: fulltextData, loading: fulltextLoading, error: fulltextError }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
<div key={group.quickgroupid} style={{ marginLeft: level * 16, marginBottom: 8 }}>
<div const handleFulltextSearch = () => {
className={`flex items-center p-2 rounded cursor-pointer border ${group.link ? 'bg-white hover:bg-red-50 border-gray-200 hover:border-red-300' : 'bg-gray-50 hover:bg-gray-100 border-gray-200'}`} if (!fulltextQuery.trim()) {
onClick={() => handleQuickGroupClick(group)} if (onSearchResults) {
> onSearchResults({
{group.children && group.children.length > 0 && ( results: [],
<svg className={`w-4 h-4 text-gray-400 mr-2 transition-transform ${expandedQuickGroups.has(group.quickgroupid) ? 'rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> loading: false,
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> error: null,
</svg> query: '',
)} isSearching: false
<span className={`font-medium ${group.link ? 'text-gray-900' : 'text-gray-600'}`}>{group.name}</span> });
{group.link && ( }
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">Доступен поиск</span> return;
)} }
</div> if (!ssd || ssd.trim() === '') {
{group.children && group.children.length > 0 && expandedQuickGroups.has(group.quickgroupid) && ( console.error('SSD обязателен для поиска по названию');
<div className="mt-1"> return;
{renderQuickGroupTree(group.children, level + 1)} }
</div> // Отправляем начальное состояние поиска родителю
)} if (onSearchResults) {
</div> onSearchResults({
))} results: [],
</div> 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, onSearchResults]);
const handleFulltextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleFulltextSearch();
}
};
const fulltextResults = fulltextData?.laximoFulltextSearch?.details || [];
useEffect(() => {
if (onActiveTabChange) {
onActiveTabChange(activeTab);
}
}, [activeTab, onActiveTabChange]);
return ( return (
<div className="w-layout-vflex vinleftbar"> <div className="w-layout-vflex vinleftbar">
{/* === Форма полнотекстового поиска === */}
<div className="div-block-2"> <div className="div-block-2">
<div className="form-block w-form"> <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"> <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(); if (!ssd || ssd.trim() === '') { return; } handleSearch(); }}> <a href="#" className="link-block-3 w-inline-block" onClick={e => { e.preventDefault(); handleFulltextSearch(); }}>
<div className="code-embed-6 w-embed"> <div className="code-embed-6 w-embed">
{/* SVG */} {/* SVG */}
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -203,14 +284,29 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
type="text" type="text"
id="VinSearchInput" id="VinSearchInput"
required required
value={searchQuery} value={fulltextQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={handleInputChange}
onKeyDown={handleKeyDown} onKeyDown={handleFulltextKeyDown}
disabled={loading} disabled={fulltextLoading}
/> />
</form> </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> </div>
<div className="w-layout-vflex flex-block-113"> <div className="w-layout-vflex flex-block-113">
@ -260,7 +356,6 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
<> <>
{categories.map((category: any, idx: number) => { {categories.map((category: any, idx: number) => {
const isOpen = openIndex === idx; const isOpen = openIndex === idx;
// Подкатегории: сначала children, если нет — unitsByCategory
const subcategories = category.children && category.children.length > 0 const subcategories = category.children && category.children.length > 0
? category.children ? category.children
: unitsByCategory[category.quickgroupid] || []; : unitsByCategory[category.quickgroupid] || [];
@ -285,7 +380,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
<a <a
href="#" href="#"
key={subcat.quickgroupid || subcat.unitid} key={subcat.quickgroupid || subcat.unitid}
className="dropdown-link-3 w-dropdown-link" className="dropdown-link-3 w-dropdown-link pl-0"
onClick={e => { onClick={e => {
e.preventDefault(); e.preventDefault();
if (onNodeSelect) { if (onNodeSelect) {
@ -310,47 +405,159 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
) )
) : ( ) : (
// Manufacturer tab content (QuickGroups) // Manufacturer tab content (QuickGroups)
<div style={{ padding: '16px' }}> quickGroupsLoading ? (
{quickGroupsLoading ? ( <div style={{ padding: 16, textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
<div style={{ textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
) : quickGroupsError ? ( ) : quickGroupsError ? (
<div style={{ color: 'red' }}>Ошибка загрузки групп: {quickGroupsError.message}</div> <div style={{ color: 'red', padding: 16 }}>Ошибка загрузки групп: {quickGroupsError.message}</div>
) : selectedQuickGroup ? ( ) : (
<div> <>
<button onClick={() => setSelectedQuickGroup(null)} className="mb-4 px-3 py-1 bg-gray-200 rounded">Назад к группам</button> {(quickGroups as QuickGroup[]).map((group: QuickGroup) => {
<h3 className="text-lg font-semibold mb-2">{selectedQuickGroup.name}</h3> const hasChildren = group.children && group.children.length > 0;
const isOpen = expandedQuickGroups.has(group.quickgroupid);
if (!hasChildren) {
return (
<a
href="#"
key={group.quickgroupid}
className="dropdown-link-3 w-dropdown-link"
onClick={(e) => {
e.preventDefault();
if (group.link && onQuickGroupSelect) {
onQuickGroupSelect(group);
}
}}
>
{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" : ""}`}
onClick={() => handleQuickGroupToggle(group.quickgroupid)}
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 = expandedQuickGroups.has(child.quickgroupid);
if (!hasSubChildren) {
return (
<a
href="#"
key={child.quickgroupid}
className="dropdown-link-3 w-dropdown-link "
onClick={(e) => {
e.preventDefault();
if (child.link && onQuickGroupSelect) {
onQuickGroupSelect(child);
}
}}
>
{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" : ""}`}
onClick={() => handleQuickGroupToggle(child.quickgroupid)}
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();
if (subChild.link && onQuickGroupSelect) {
onQuickGroupSelect(subChild);
}
}}
>
{subChild.name}
</a>
))}
</nav>
</div>
);
})}
</nav>
</div>
);
})}
{/* Quick Detail Modal */}
{selectedQuickGroup && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">{selectedQuickGroup.name}</h3>
<button
onClick={() => setSelectedQuickGroup(null)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
{quickDetailLoading ? ( {quickDetailLoading ? (
<div>Загружаем детали...</div> <div className="text-center py-4">Загружаем детали...</div>
) : quickDetailError ? ( ) : quickDetailError ? (
<div style={{ color: 'red' }}>Ошибка загрузки деталей: {quickDetailError.message}</div> <div className="text-red-600 py-4">Ошибка загрузки деталей: {quickDetailError.message}</div>
) : quickDetail && quickDetail.units && quickDetail.units.length > 0 ? ( ) : quickDetail?.units?.length > 0 ? (
<div className="space-y-3"> <div className="space-y-4">
{quickDetail.units.map((unit: any) => ( {quickDetail.units.map((unit: any) => (
<div key={unit.unitid} className="p-3 bg-gray-50 rounded border border-gray-200"> <div key={unit.unitid} className="border border-gray-200 rounded-lg p-4">
<div className="font-medium text-gray-900">{unit.name}</div> <div className="font-medium text-gray-900 mb-2">{unit.name}</div>
{unit.details && unit.details.length > 0 && ( {unit.details && unit.details.length > 0 && (
<ul className="mt-2 text-sm text-gray-700 list-disc pl-5"> <div className="space-y-2">
{unit.details.map((detail: any) => ( {unit.details.map((detail: any) => (
<li key={detail.detailid}> <div key={detail.detailid} className="flex items-center justify-between bg-gray-50 p-2 rounded">
<span className="font-medium">{detail.name}</span> <span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">OEM: {detail.oem}</span> <span className="font-medium text-gray-700">{detail.name}</span>
</li> <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
OEM: {detail.oem}
</span>
</div>
))} ))}
</ul> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div>Нет деталей для этой группы</div> <div className="text-center text-gray-500 py-4">Нет деталей для этой группы</div>
)} )}
</div> </div>
) : quickGroups.length > 0 ? (
renderQuickGroupTree(quickGroups)
) : (
<div>Нет доступных групп быстрого поиска</div>
)}
</div> </div>
)} )}
</>
)
)}
{/* Tab content end */} {/* Tab content end */}
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import React from "react"; import React, { useState } from "react";
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import BrandSelectionModal from '../BrandSelectionModal';
interface VinPartCardProps { interface VinPartCardProps {
n?: number; n?: number;
@ -10,13 +11,16 @@ interface VinPartCardProps {
const VinPartCard: React.FC<VinPartCardProps> = ({ n, oem, name, onPriceClick }) => { const VinPartCard: React.FC<VinPartCardProps> = ({ n, oem, name, onPriceClick }) => {
const router = useRouter(); const router = useRouter();
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
const handlePriceClick = (e: React.MouseEvent) => { const handlePriceClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
if (onPriceClick) onPriceClick(); if (onPriceClick) onPriceClick();
if (oem) router.push(`/search?q=${encodeURIComponent(oem)}&mode=parts`); setIsBrandModalOpen(true);
}; };
return ( return (
<>
<div className="w-layout-hflex knotlistitem"> <div className="w-layout-hflex knotlistitem">
<div className="w-layout-hflex flex-block-116"> <div className="w-layout-hflex flex-block-116">
{n !== undefined && <div className="nuberlist">{n}</div>} {n !== undefined && <div className="nuberlist">{n}</div>}
@ -32,6 +36,13 @@ const VinPartCard: React.FC<VinPartCardProps> = ({ n, oem, name, onPriceClick })
</div> </div>
</div> </div>
</div> </div>
<BrandSelectionModal
isOpen={isBrandModalOpen}
onClose={() => setIsBrandModalOpen(false)}
articleNumber={oem}
detailName={name}
/>
</>
); );
}; };

View File

@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_LAXIMO_QUICK_DETAIL } from '@/lib/graphql/laximo';
import BrandSelectionModal from '../BrandSelectionModal';
interface VinQuickProps {
quickGroup: any;
catalogCode: string;
vehicleId: string;
ssd: string;
onBack: () => void;
onNodeSelect: (unit: any) => void;
}
const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId, ssd, onBack, onNodeSelect }) => {
const { data, loading, error } = useQuery(GET_LAXIMO_QUICK_DETAIL, {
variables: {
catalogCode,
vehicleId,
quickGroupId: quickGroup.quickgroupid,
ssd
},
skip: !quickGroup || !quickGroup.quickgroupid
});
const quickDetail = data?.laximoQuickDetail;
const [isBrandModalOpen, setIsBrandModalOpen] = useState(false);
const [selectedDetail, setSelectedDetail] = useState<any>(null);
const handleUnitClick = (unit: any) => {
onNodeSelect({
...unit,
unitid: unit.unitid,
name: unit.name,
catalogCode,
vehicleId,
ssd
});
};
const handleDetailClick = (detail: any) => {
setSelectedDetail(detail);
setIsBrandModalOpen(true);
};
const handleCloseBrandModal = () => {
setIsBrandModalOpen(false);
setSelectedDetail(null);
};
return (
<div className="w-full">
{/* <button onClick={onBack} className="mb-4 px-4 py-2 bg-gray-200 rounded self-start">Назад</button> */}
{loading ? (
<div className="text-center py-4">Загружаем детали...</div>
) : error ? (
<div className="text-red-600 py-4">Ошибка загрузки деталей: {error.message}</div>
) : quickDetail && quickDetail.units ? (
quickDetail.units.map((unit: any) => (
<div key={unit.unitid} className="w-layout-vflex flex-block-14-copy-copy">
<div className="knotinfo">
{unit.imageurl || unit.largeimageurl ? (
<img
src={unit.largeimageurl ? unit.largeimageurl.replace('%size%', '250') : unit.imageurl.replace('%size%', '250')}
alt={unit.name}
className="image-26"
onError={e => { (e.currentTarget as HTMLImageElement).src = '/images/image-44.jpg'; }}
/>
) : (
<img src="/images/image-44.jpg" alt="Нет изображения" className="image-26" />
)}
</div>
<div className="knot-img">
<h1 className="heading-19">{unit.name}</h1>
{unit.details && unit.details.length > 0 && unit.details.map((detail: any) => (
<div className="w-layout-hflex flex-block-115" key={detail.detailid}>
<div className="oemnuber">{detail.oem}</div>
<div className="partsname">{detail.name}</div>
<a href="#" className="button-3 w-button" onClick={e => { e.preventDefault(); handleDetailClick(detail); }}>Показать цены</a>
</div>
))}
<a href="#" className="showallparts w-button" onClick={e => { e.preventDefault(); handleUnitClick(unit); }}>Подробнее</a>
</div>
</div>
))
) : (
<div className="text-center text-gray-500 py-4">Нет деталей для этой группы</div>
)}
{isBrandModalOpen && selectedDetail && (
<BrandSelectionModal
isOpen={isBrandModalOpen}
onClose={handleCloseBrandModal}
articleNumber={selectedDetail.oem}
detailName={selectedDetail.name}
/>
)}
</div>
);
};
export default VinQuick;

View File

@ -180,23 +180,6 @@ export const SEARCH_LAXIMO_OEM = gql`
} }
`; `;
export const SEARCH_LAXIMO_FULLTEXT = gql`
query SearchLaximoFulltext($catalogCode: String!, $vehicleId: String!, $searchText: String!, $ssd: String!) {
laximoFulltextSearch(catalogCode: $catalogCode, vehicleId: $vehicleId, searchText: $searchText, ssd: $ssd) {
details {
codeonimage
code
name
note
filter
oem
price
availability
}
}
}
`;
export const GET_LAXIMO_FULLTEXT_SEARCH = gql` export const GET_LAXIMO_FULLTEXT_SEARCH = gql`
query GetLaximoFulltextSearch($catalogCode: String!, $vehicleId: String!, $searchQuery: String!, $ssd: String!) { query GetLaximoFulltextSearch($catalogCode: String!, $vehicleId: String!, $searchQuery: String!, $ssd: String!) {
laximoFulltextSearch(catalogCode: $catalogCode, vehicleId: $vehicleId, searchQuery: $searchQuery, ssd: $ssd) { laximoFulltextSearch(catalogCode: $catalogCode, vehicleId: $vehicleId, searchQuery: $searchQuery, ssd: $ssd) {
@ -208,6 +191,10 @@ export const GET_LAXIMO_FULLTEXT_SEARCH = gql`
parttype parttype
filter filter
note note
codeonimage
code
price
availability
attributes { attributes {
key key
name name

View File

@ -7,8 +7,10 @@ import CartSummary from "@/components/CartSummary";
import CartRecommended from "../components/CartRecommended"; import CartRecommended from "../components/CartRecommended";
import CatalogSubscribe from "@/components/CatalogSubscribe"; import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection"; import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import React, { useState } from "react";
export default function CartPage() { export default function CartPage() {
const [step, setStep] = useState(1);
return ( return (
<><Head> <><Head>
@ -26,8 +28,8 @@ export default function CartPage() {
<div className="w-layout-blockcontainer container w-container"> <div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex cart-list"> <div className="w-layout-vflex cart-list">
<div className="w-layout-hflex core-product-card"> <div className="w-layout-hflex core-product-card">
<CartList /> <CartList isSummaryStep={step === 2} />
<CartSummary /> <CartSummary step={step} setStep={setStep} />
</div> </div>
<CartRecommended /> <CartRecommended />
</div> </div>

View File

@ -58,7 +58,7 @@ const ProfileActsPage = () => {
); );
} }
return ( return (
<div className="page-wrapper"> <>
<Head> <Head>
<title>ProfileActs</title> <title>ProfileActs</title>
<meta content="ProfileActs" property="og:title" /> <meta content="ProfileActs" property="og:title" />
@ -78,7 +78,7 @@ const ProfileActsPage = () => {
</section> </section>
<MobileMenuBottomSection /> <MobileMenuBottomSection />
<Footer /> <Footer />
</div> </>
); );
}; };

View File

@ -16,6 +16,10 @@ import PartDetailCard from '@/components/PartDetailCard';
import VinPartCard from '@/components/vin/VinPartCard'; import VinPartCard from '@/components/vin/VinPartCard';
import KnotIn from '@/components/vin/KnotIn'; import KnotIn from '@/components/vin/KnotIn';
import KnotParts from '@/components/vin/KnotParts'; import KnotParts from '@/components/vin/KnotParts';
import VinQuick from '@/components/vin/VinQuick';
import CatalogSubscribe from '@/components/CatalogSubscribe';
import MobileMenuBottomSection from '@/components/MobileMenuBottomSection';
interface LaximoVehicleInfo { interface LaximoVehicleInfo {
vehicleid: string; vehicleid: string;
@ -53,7 +57,20 @@ const VehicleDetailsPage = () => {
const [searchType, setSearchType] = useState<'quickgroups' | 'categories' | 'fulltext'>(defaultSearchType); const [searchType, setSearchType] = useState<'quickgroups' | 'categories' | 'fulltext'>(defaultSearchType);
const [showKnot, setShowKnot] = useState(false); const [showKnot, setShowKnot] = useState(false);
const [foundParts, setFoundParts] = useState<any[]>([]); const [foundParts, setFoundParts] = useState<any[]>([]);
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
const [searchState, setSearchState] = useState<{
loading: boolean;
error: any;
query: string;
isSearching: boolean;
}>({
loading: false,
error: null,
query: '',
isSearching: false
});
const [selectedNode, setSelectedNode] = useState<any | null>(null); const [selectedNode, setSelectedNode] = useState<any | null>(null);
const [selectedQuickGroup, setSelectedQuickGroup] = useState<any | null>(null);
const handleCategoryClick = (e?: React.MouseEvent) => { const handleCategoryClick = (e?: React.MouseEvent) => {
if (e) e.preventDefault(); if (e) e.preventDefault();
setShowKnot(true); setShowKnot(true);
@ -123,7 +140,7 @@ const VehicleDetailsPage = () => {
...(finalSsd && { ssd: finalSsd }), ...(finalSsd && { ssd: finalSsd }),
localized: true localized: true
}, },
skip: !brand || !vehicleId, skip: !brand || vehicleId === undefined || vehicleId === null,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );
@ -194,8 +211,9 @@ const VehicleDetailsPage = () => {
); );
} }
// Если vehicleId невалидный (например, '0'), показываем предупреждение и не рендерим поиск // Если vehicleId отсутствует или пустой, показываем предупреждение
if (!vehicleId || vehicleId === '0') { // Важно: vehicleId может быть '0' для некоторых автомобилей, найденных по VIN
if (!vehicleId || vehicleId === '') {
return ( return (
<main className="min-h-screen bg-yellow-50 flex items-center justify-center"> <main className="min-h-screen bg-yellow-50 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
@ -214,7 +232,8 @@ const VehicleDetailsPage = () => {
// Гарантируем, что vehicleId — строка // Гарантируем, что vehicleId — строка
const vehicleIdStr = Array.isArray(vehicleId) ? (vehicleId[0] || '') : (vehicleId || ''); const vehicleIdStr = Array.isArray(vehicleId) ? (vehicleId[0] || '') : (vehicleId || '');
const fallbackVehicleId = (vehicleIdStr !== '0' ? vehicleIdStr : ''); // Для Laximo API vehicleId может быть '0' для автомобилей, найденных по VIN
const fallbackVehicleId = vehicleIdStr;
let vehicleInfo = vehicleData?.laximoVehicleInfo || { let vehicleInfo = vehicleData?.laximoVehicleInfo || {
vehicleid: fallbackVehicleId, vehicleid: fallbackVehicleId,
@ -225,8 +244,8 @@ const VehicleDetailsPage = () => {
attributes: [] as never[] attributes: [] as never[]
}; };
// Если вдруг с сервера пришёл vehicleid: '0', подменяем на корректный // Убеждаемся, что vehicleid соответствует параметру из URL
if (vehicleInfo.vehicleid === '0' && fallbackVehicleId) { if (vehicleInfo.vehicleid !== fallbackVehicleId && fallbackVehicleId) {
vehicleInfo = { ...vehicleInfo, vehicleid: fallbackVehicleId }; vehicleInfo = { ...vehicleInfo, vehicleid: fallbackVehicleId };
} }
@ -270,40 +289,101 @@ const VehicleDetailsPage = () => {
{!selectedNode ? ( {!selectedNode ? (
<div className="w-layout-hflex flex-block-13"> <div className="w-layout-hflex flex-block-13">
{vehicleInfo && vehicleInfo.catalog && vehicleInfo.vehicleid && vehicleInfo.ssd && ( {vehicleInfo && vehicleInfo.catalog && vehicleInfo.vehicleid && vehicleInfo.ssd && (
<>
<VinLeftbar <VinLeftbar
vehicleInfo={vehicleInfo} vehicleInfo={vehicleInfo}
onSearchResults={setFoundParts} onSearchResults={({ results, loading, error, query, isSearching }) => {
setFoundParts(results);
setSearchState({ loading, error, query, isSearching: isSearching || false });
}}
onNodeSelect={setSelectedNode} onNodeSelect={setSelectedNode}
onActiveTabChange={(tab) => setActiveTab(tab)}
onQuickGroupSelect={setSelectedQuickGroup}
/> />
)} {searchState.isSearching ? (
{/* Категории или Knot или карточки */}
{foundParts.length > 0 ? (
<div className="knot-parts"> <div className="knot-parts">
{foundParts.map((detail, idx) => ( {searchState.loading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Выполняется поиск...</p>
</div>
) : searchState.error ? (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mt-3">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
Ошибка поиска
</h3>
<div className="mt-2 text-sm text-red-700">
<p>{searchState.error.message}</p>
</div>
</div>
</div>
</div>
) : foundParts.length > 0 ? (
foundParts.map((detail, idx) => (
<VinPartCard <VinPartCard
key={detail.oem + idx} key={detail.oem + idx}
n={idx + 1} n={idx + 1}
name={detail.name} name={detail.name}
oem={detail.oem} oem={detail.oem}
/> />
))} ))
) : (
<div className="text-center py-12">
<svg className="w-12 h-12 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p className="text-gray-600">
По запросу "{searchState.query}" ничего не найдено
</p>
<p className="text-sm text-gray-500 mt-1">
Попробуйте изменить поисковый запрос
</p>
</div>
)}
</div> </div>
) : showKnot ? ( ) : showKnot ? (
<VinKnot /> <VinKnot />
) : selectedQuickGroup ? (
<VinQuick
quickGroup={selectedQuickGroup}
catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd}
onBack={() => setSelectedQuickGroup(null)}
onNodeSelect={setSelectedNode}
/>
) : ( ) : (
<VinCategory <VinCategory
catalogCode={vehicleInfo.catalog} catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid} vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd} ssd={vehicleInfo.ssd}
onNodeSelect={setSelectedNode} onNodeSelect={setSelectedNode}
activeTab={activeTab}
onQuickGroupSelect={setSelectedQuickGroup}
/> />
)} )}
</>
)}
</div> </div>
) : ( ) : (
<div className="w-layout-hflex flex-block-13"> <div className="w-layout-hflex flex-block-13">
<div className="w-layout-vflex flex-block-14-copy-copy"> <div className="w-layout-vflex flex-block-14-copy-copy">
<button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button> <button onClick={() => setSelectedNode(null)} style={{ marginBottom: 16 }}>Назад</button>
<KnotIn node={selectedNode} /> <KnotIn
catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid}
ssd={vehicleInfo.ssd}
unitId={selectedNode.unitid}
unitName={selectedNode.name}
parts={unitDetails}
/>
{unitDetailsLoading ? ( {unitDetailsLoading ? (
<div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div> <div style={{ padding: 24, textAlign: 'center' }}>Загружаем детали узла...</div>
) : unitDetailsError ? ( ) : unitDetailsError ? (
@ -317,10 +397,15 @@ const VehicleDetailsPage = () => {
</div> </div>
)} )}
</div> </div>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
{/* ====== ВРЕМЕННЫЙ МАКЕТ ДЛЯ ВЕРСТКИ (конец) ====== */} {/* ====== ВРЕМЕННЫЙ МАКЕТ ДЛЯ ВЕРСТКИ (конец) ====== */}
{/* Навигация */} {/* Навигация
<nav className="bg-white border-b"> <nav className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
@ -348,7 +433,7 @@ const VehicleDetailsPage = () => {
</div> </div>
</nav> </nav>
{/* Информация об автомобиле */} Информация об автомобиле
<div className="bg-white border-b"> <div className="bg-white border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center space-x-4 mb-6"> <div className="flex items-center space-x-4 mb-6">
@ -368,7 +453,7 @@ const VehicleDetailsPage = () => {
</div> </div>
</div> </div>
{/* Предупреждение об ошибке */} Предупреждение об ошибке
{hasError && ( {hasError && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6"> <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex"> <div className="flex">
@ -389,7 +474,7 @@ const VehicleDetailsPage = () => {
</div> </div>
)} )}
{/* Отладочная информация */} Отладочная информация
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6"> <div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
<h4 className="text-sm font-medium text-gray-900 mb-3"> <h4 className="text-sm font-medium text-gray-900 mb-3">
🔧 Отладочная информация 🔧 Отладочная информация
@ -420,7 +505,7 @@ const VehicleDetailsPage = () => {
</div> </div>
</div> </div>
{/* Характеристики автомобиля */} Характеристики автомобиля
{vehicleInfo.attributes && vehicleInfo.attributes.length > 0 && ( {vehicleInfo.attributes && vehicleInfo.attributes.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{vehicleInfo.attributes.map((attr, index) => ( {vehicleInfo.attributes.map((attr, index) => (
@ -434,7 +519,7 @@ const VehicleDetailsPage = () => {
</div> </div>
</div> </div>
{/* Способы поиска запчастей */} Способы поиска запчастей
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-xl font-bold text-gray-900 mb-2">Поиск запчастей</h2> <h2 className="text-xl font-bold text-gray-900 mb-2">Поиск запчастей</h2>
@ -443,7 +528,7 @@ const VehicleDetailsPage = () => {
</p> </p>
</div> </div>
{/* Диагностический компонент */} Диагностический компонент
<LaximoDiagnostic <LaximoDiagnostic
catalogCode={vehicleInfo.catalog} catalogCode={vehicleInfo.catalog}
vehicleId={vehicleInfo.vehicleid} vehicleId={vehicleInfo.vehicleid}
@ -456,7 +541,7 @@ const VehicleDetailsPage = () => {
searchType={searchType} searchType={searchType}
onSearchTypeChange={setSearchType} onSearchTypeChange={setSearchType}
/> />
</div> </div> */}
</> </>
); );
}; };

View File

@ -74,12 +74,12 @@ const PartDetailPage = () => {
oemNumber: oemNumber, oemNumber: oemNumber,
ssd: finalSsd ssd: finalSsd
}, },
skip: !brand || !vehicleId || !oemNumber || !finalSsd, skip: !brand || vehicleId === undefined || vehicleId === null || !oemNumber || !finalSsd,
errorPolicy: 'all' errorPolicy: 'all'
} }
); );
if (!brand || !vehicleId || !oemNumber) { if (!brand || vehicleId === undefined || vehicleId === null || !oemNumber) {
return ( return (
<> <>
<Head> <Head>

View File

@ -386,15 +386,17 @@ input.input-receiver:focus {
color: var(--_fonts---color--light-blue-grey); color: var(--_fonts---color--light-blue-grey);
} }
.knotin { /* .knotin {
max-width: 100%; max-width: 100%;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
} }
.knotin img { .knotin img {
max-width: 100%; max-width: 100%;
object-fit: contain; /* или cover */ object-fit: contain;
} } */
.tabs-menu.w-tab-menu { .tabs-menu.w-tab-menu {
scrollbar-width: none; scrollbar-width: none;
@ -429,3 +431,47 @@ input#VinSearchInput {
max-height: 2.8em; max-height: 2.8em;
line-height: 1.4em; line-height: 1.4em;
} }
.heading-9-copy,
.text-block-21-copy {
width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 767px) {
.w-layout-hflex.flex-block-6 {
flex-direction: column !important;
}
@media (max-width: 767px) {
.div-block-12,
.div-block-12.small,
.div-block-12-copy,
.div-block-12-copy.small,
.div-block-123,
.div-block-123.small,
.div-block-red,
.div-block-red.small {
width: 100% !important;
max-width: 100% !important;
}
}
}
@media (max-width: 767px) {
.flex-block-6 {
grid-template-columns: 1fr !important;
grid-template-rows: none !important;
grid-template-areas: none !important;
grid-auto-flow: row !important; /* <--- ВАЖНО! */
}
}
.dropdown-toggle-card {
padding-left: 0 !important;
}
.dropdown-link-3 {
margin-left: 0 !important;
}

View File

@ -787,6 +787,8 @@
.w-layout-blockcontainer { .w-layout-blockcontainer {
max-width: 728px; max-width: 728px;
} }
} }
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
@ -794,6 +796,8 @@
max-width: none; max-width: none;
} }
.w-commerce-commercelayoutcontainer { .w-commerce-commercelayoutcontainer {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@ -2844,6 +2848,7 @@ body {
flex-flow: column; flex-flow: column;
flex: 1; flex: 1;
display: flex; display: flex;
} }
.text-field-copy { .text-field-copy {
@ -3110,6 +3115,7 @@ body {
} }
.block-name { .block-name {
max-width: 300px;
flex-flow: column; flex-flow: column;
flex: 1; flex: 1;
display: flex; display: flex;
@ -3908,6 +3914,7 @@ body {
font-size: var(--_fonts---font-size--small-font-size); font-size: var(--_fonts---font-size--small-font-size);
height: 20px; height: 20px;
margin-top: 0; margin-top: 0;
max-width: 100%;
margin-bottom: 0; margin-bottom: 0;
font-weight: 700; font-weight: 700;
} }
@ -4024,6 +4031,7 @@ body {
position: relative; position: relative;
} }
.button-for-mobile-menu-block:hover { .button-for-mobile-menu-block:hover {
background-color: var(--_button---hover-dark_blue); background-color: var(--_button---hover-dark_blue);
} }
@ -4329,7 +4337,7 @@ body {
color: var(--_fonts---color--black); color: var(--_fonts---color--black);
font-size: var(--_fonts---font-size--bigger); font-size: var(--_fonts---font-size--bigger);
text-align: right; text-align: right;
max-width: 100px; max-width: 200px;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
font-weight: 700; font-weight: 700;
@ -5426,10 +5434,10 @@ body {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
} }
/*
.topmenuh { .bottom_head {
margin-top: 0; margin-top: 0;
} } */
.flex-block-4 { .flex-block-4 {
grid-column-gap: 40px; grid-column-gap: 40px;
@ -5519,7 +5527,7 @@ body {
} }
.flex-block-39-copy { .flex-block-39-copy {
width: 200px; width: 150px;
} }
.cart-ditail { .cart-ditail {
@ -5579,6 +5587,8 @@ body {
background-color: var(--white); background-color: var(--white);
} }
.button-for-mobile-menu-block { .button-for-mobile-menu-block {
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
@ -5680,9 +5690,9 @@ body {
margin-top: 12px; margin-top: 12px;
} }
.topmenub { /* .bottom_head {
margin-top: 0; margin-top: 0;
} } */
.vinleftbar { .vinleftbar {
width: 320px; width: 320px;
@ -6082,7 +6092,7 @@ body {
padding-bottom: 40px; padding-bottom: 40px;
} }
.top_head, .topmenuh { .top_head, .bottom_head {
padding-left: 30px; padding-left: 30px;
padding-right: 30px; padding-right: 30px;
} }
@ -6882,7 +6892,7 @@ body {
margin-left: 0; margin-left: 0;
} }
.topmenub { .bottom_head {
padding-left: 30px; padding-left: 30px;
padding-right: 30px; padding-right: 30px;
} }
@ -7329,7 +7339,7 @@ body {
} }
.flex-block-39-copy { .flex-block-39-copy {
width: 200px; width: 150px;
} }
.heading-9-copy-copy { .heading-9-copy-copy {
@ -7543,8 +7553,9 @@ body {
} }
.flex-block-18-copy-copy { .flex-block-18-copy-copy {
grid-column-gap: 10px; grid-column-gap: 40px;
grid-row-gap: 10px; grid-row-gap: 40px;
flex-flow: column;
} }
.link-block-4-copy { .link-block-4-copy {
@ -7602,8 +7613,9 @@ body {
} }
.flex-block-87 { .flex-block-87 {
grid-column-gap: 0px; grid-column-gap: 10px;
grid-row-gap: 0px; grid-row-gap: 10px;
flex: 1;
} }
.mobile-menu-bottom { .mobile-menu-bottom {
@ -7627,10 +7639,16 @@ body {
} }
.button-for-mobile-menu-block { .button-for-mobile-menu-block {
grid-column-gap: 0px; grid-column-gap: 2px;
grid-row-gap: 0px; grid-row-gap: 2px;
width: 60px; background-color: var(--_fonts---color--white);
padding-bottom: 5px; color: var(--_button---light-blue-grey);
flex-flow: column;
width: 70px;
}
.button-for-mobile-menu-block:hover {
background-color: var(--_button---light-blue);
} }
.section-3 { .section-3 {
@ -7643,7 +7661,8 @@ body {
} }
.flex-block-93 { .flex-block-93 {
margin-left: 0; align-self: auto;
min-height: 48px;
} }
.sort-list-card { .sort-list-card {
@ -7882,6 +7901,10 @@ body {
.container-copy.footer { .container-copy.footer {
padding-bottom: 90px; padding-bottom: 90px;
} }
.mobile-menu-buttom-section {
display: block;
}
} }
@media screen and (max-width: 479px) { @media screen and (max-width: 479px) {
@ -7951,7 +7974,7 @@ body {
grid-row-gap: 15px; grid-row-gap: 15px;
} }
.top_head, .topmenuh { .top_head, .bottom_head {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
@ -9132,7 +9155,7 @@ body {
top: 58px; top: 58px;
} }
.topmenub { .bottom_head {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
@ -9400,6 +9423,12 @@ body {
#w-node-_35f55517-cbe0-9ee3-13bb-a3ed00029bba-00029ba8, #w-node-_35f55517-cbe0-9ee3-13bb-a3ed00029bc7-00029ba8 { #w-node-_35f55517-cbe0-9ee3-13bb-a3ed00029bba-00029ba8, #w-node-_35f55517-cbe0-9ee3-13bb-a3ed00029bc7-00029ba8 {
justify-self: stretch; justify-self: stretch;
} }
.button-for-mobile-menu-block {
grid-column-gap: 0px;
grid-row-gap: 0px;
width: 60px;
padding-bottom: 5px;
}
} }
.flex-block-113 { .flex-block-113 {
@ -9457,7 +9486,7 @@ body {
display: flex; display: flex;
} }
.dropdown-toggle-3 { .dropdown-toggle-3, .dropdown-toggle-card {
border-top-right-radius: var(--_round---normal); border-top-right-radius: var(--_round---normal);
border-bottom-right-radius: var(--_round---normal); border-bottom-right-radius: var(--_round---normal);
border-left: 2px solid #0000; border-left: 2px solid #0000;