import Head from "next/head"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import ProductListCard from "@/components/ProductListCard"; import Filters, { FilterConfig } from "@/components/Filters"; import FiltersWithSearch from "@/components/FiltersWithSearch"; import CatalogProductCard from "@/components/CatalogProductCard"; import CatalogPagination from "@/components/CatalogPagination"; import CatalogSubscribe from "@/components/CatalogSubscribe"; import CatalogInfoHeader from "@/components/CatalogInfoHeader"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { useRouter } from "next/router"; import { useQuery } from '@apollo/client'; import FiltersPanelMobile from '@/components/FiltersPanelMobile'; import MobileMenuBottomSection from '../components/MobileMenuBottomSection'; import { GET_PARTSAPI_ARTICLES, GET_PARTSAPI_MAIN_IMAGE, SEARCH_PRODUCT_OFFERS, GET_PARTSINDEX_CATALOG_ENTITIES, GET_PARTSINDEX_CATALOG_PARAMS } from '@/lib/graphql'; import { PartsAPIArticlesData, PartsAPIArticlesVariables, PartsAPIArticle, PartsAPIMainImageData, PartsAPIMainImageVariables } from '@/types/partsapi'; import { PartsIndexEntitiesData, PartsIndexEntitiesVariables, PartsIndexEntity, PartsIndexParamsData, PartsIndexParamsVariables } from '@/types/partsindex'; import LoadingSpinner from '@/components/LoadingSpinner'; import ArticleCard from '@/components/ArticleCard'; import CatalogEmptyState from '@/components/CatalogEmptyState'; import { useProductPrices } from '@/hooks/useProductPrices'; import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton'; import { useCart } from '@/contexts/CartContext'; import toast from 'react-hot-toast'; import CartIcon from '@/components/CartIcon'; import MetaTags from "@/components/MetaTags"; import { getMetaByPath, createCategoryMeta } from "@/lib/meta-config"; import JsonLdScript from "@/components/JsonLdScript"; import { generateBreadcrumbSchema, generateWebSiteSchema } from "@/lib/schema"; const mockData = Array(12).fill({ image: "", discount: "-35%", price: "от 17 087 ₽", oldPrice: "22 347 ₽", title: 'Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60', brand: "Borsehung", }); const ITEMS_PER_PAGE = 50; // Уменьшено для быстрой загрузки и лучшего UX const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально export default function Catalog() { const router = useRouter(); const { addItem } = useCart(); const { partsApiCategory: strId, categoryName, partsIndexCatalog: catalogId, partsIndexCategory: groupId } = router.query; const [showFiltersMobile, setShowFiltersMobile] = useState(false); const [showSortMobile, setShowSortMobile] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [selectedFilters, setSelectedFilters] = useState<{[key: string]: string[]}>({}); const [visibleArticles, setVisibleArticles] = useState([]); const [visibleEntities, setVisibleEntities] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [entitiesPage, setEntitiesPage] = useState(1); // Страница для PartsIndex const [isLoadingMore, setIsLoadingMore] = useState(false); const [hasMoreEntities, setHasMoreEntities] = useState(true); // Есть ли еще товары на сервере const [showAllBrands, setShowAllBrands] = useState(false); const [catalogFilters, setCatalogFilters] = useState([]); const [filtersLoading, setFiltersLoading] = useState(true); const [sortActive, setSortActive] = useState(0); const [visibleProductsCount, setVisibleProductsCount] = useState(0); // Счетчик товаров с предложениями const [filtersGenerating, setFiltersGenerating] = useState(false); // Состояние генерации фильтров const [targetVisibleCount, setTargetVisibleCount] = useState(ITEMS_PER_PAGE); // Целевое количество видимых товаров const [loadedArticlesCount, setLoadedArticlesCount] = useState(ITEMS_PER_PAGE); // Количество загруженных артикулов const [showEmptyState, setShowEmptyState] = useState(false); const [partsIndexPage, setPartsIndexPage] = useState(1); // Текущая страница для PartsIndex const [totalPages, setTotalPages] = useState(1); // Общее количество страниц // Новые состояния для логики автоподгрузки PartsIndex const [accumulatedEntities, setAccumulatedEntities] = useState([]); // Все накопленные товары const [entitiesWithOffers, setEntitiesWithOffers] = useState([]); // Товары с предложениями const [isAutoLoading, setIsAutoLoading] = useState(false); // Автоматическая подгрузка в процессе const [currentUserPage, setCurrentUserPage] = useState(1); // Текущая пользовательская страница const [entitiesCache, setEntitiesCache] = useState>(new Map()); // Кэш страниц // Карта видимости товаров по индексу const [visibilityMap, setVisibilityMap] = useState>(new Map()); // Обработчик изменения видимости товара const handleVisibilityChange = useCallback((index: number, isVisible: boolean) => { setVisibilityMap(prev => { const currentVisibility = prev.get(index); // Обновляем только если значение действительно изменилось if (currentVisibility === isVisible) { return prev; } const newMap = new Map(prev); newMap.set(index, isVisible); return newMap; }); }, []); // Пересчитываем количество видимых товаров при изменении карты видимости useEffect(() => { const visibleCount = Array.from(visibilityMap.values()).filter(Boolean).length; setVisibleProductsCount(visibleCount); }, [visibilityMap]); // Определяем режим работы const isPartsAPIMode = Boolean(strId && categoryName); const isPartsIndexMode = Boolean(catalogId && categoryName && groupId); // Требуем groupId для PartsIndex const isPartsIndexCatalogOnly = Boolean(catalogId && categoryName && !groupId); // Каталог без группы // Отладочная информация console.log('🔍 Режимы работы каталога:', { catalogId, groupId, categoryName, isPartsAPIMode, isPartsIndexMode, isPartsIndexCatalogOnly, 'router.query': router.query }); // Загружаем артикулы PartsAPI const { data: articlesData, loading: articlesLoading, error: articlesError } = useQuery( GET_PARTSAPI_ARTICLES, { variables: { strId: parseInt(strId as string), carId: 9877, carType: 'PC' }, skip: !isPartsAPIMode, fetchPolicy: 'cache-first' } ); const allArticles = articlesData?.partsAPIArticles || []; // Загружаем товары PartsIndex const { data: entitiesData, loading: entitiesLoading, error: entitiesError, refetch: refetchEntities } = useQuery( GET_PARTSINDEX_CATALOG_ENTITIES, { variables: { catalogId: catalogId as string, groupId: groupId as string, lang: 'ru', limit: PARTSINDEX_PAGE_SIZE, page: partsIndexPage, q: searchQuery || undefined, params: undefined // Будем обновлять через refetch }, skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId fetchPolicy: 'cache-and-network' } ); // Загружаем параметры фильтрации для PartsIndex const { data: paramsData, loading: paramsLoading, error: paramsError, refetch: refetchParams } = useQuery( GET_PARTSINDEX_CATALOG_PARAMS, { variables: { catalogId: catalogId as string, groupId: groupId as string, lang: 'ru', q: searchQuery || undefined, params: undefined // Будем обновлять через refetch }, skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId fetchPolicy: 'cache-first' } ); // allEntities больше не используется - используем allLoadedEntities // Хук для загрузки цен товаров PartsIndex const { getPrice, isLoadingPrice, ensurePriceLoaded } = useProductPrices(); // Загружаем цены для видимых товаров PartsIndex (для отображения конкретных цен) useEffect(() => { if (isPartsIndexMode && visibleEntities.length > 0) { // Загружаем цены только для видимых товаров для отображения точных цен visibleEntities.forEach((entity, index) => { const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; // Загружаем с небольшой задержкой setTimeout(() => { ensurePriceLoaded(productForPrice); }, index * 50); }); } }, [isPartsIndexMode, visibleEntities, ensurePriceLoaded]); useEffect(() => { if (articlesData?.partsAPIArticles) { // Загружаем изначально только ITEMS_PER_PAGE товаров const initialLoadCount = Math.min(ITEMS_PER_PAGE, articlesData.partsAPIArticles.length); setVisibleArticles(articlesData.partsAPIArticles.slice(0, initialLoadCount)); setLoadedArticlesCount(initialLoadCount); setTargetVisibleCount(ITEMS_PER_PAGE); setCurrentPage(1); } }, [articlesData]); useEffect(() => { if (entitiesData?.partsIndexCatalogEntities?.list) { console.log('📊 Обновляем entitiesData:', { listLength: entitiesData.partsIndexCatalogEntities.list.length, pagination: entitiesData.partsIndexCatalogEntities.pagination, currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1 }); const newEntities = entitiesData.partsIndexCatalogEntities.list; const pagination = entitiesData.partsIndexCatalogEntities.pagination; // Обновляем информацию о пагинации const currentPage = pagination?.page?.current || 1; const hasNext = pagination?.page?.next !== null; const hasPrev = pagination?.page?.prev !== null; setPartsIndexPage(currentPage); setHasMoreEntities(hasNext); // Сохраняем в кэш setEntitiesCache(prev => new Map(prev).set(currentPage, newEntities)); // Если это первая страница или сброс, заменяем накопленные товары if (currentPage === 1) { setAccumulatedEntities(newEntities); // Устанавливаем visibleEntities сразу, не дожидаясь проверки цен setVisibleEntities(newEntities); console.log('✅ Установлены visibleEntities для первой страницы:', newEntities.length); } else { // Добавляем к накопленным товарам setAccumulatedEntities(prev => [...prev, ...newEntities]); } // Вычисляем общее количество страниц (приблизительно) if (hasNext) { setTotalPages(currentPage + 1); // Минимум еще одна страница } else { setTotalPages(currentPage); // Это последняя страница } console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev }); } }, [entitiesData]); // Преобразование выбранных фильтров в формат PartsIndex API const convertFiltersToPartsIndexParams = useMemo((): Record => { if (!paramsData?.partsIndexCatalogParams?.list || Object.keys(selectedFilters).length === 0) { return {}; } const apiParams: Record = {}; paramsData.partsIndexCatalogParams.list.forEach((param: any) => { const selectedValues = selectedFilters[param.name]; if (selectedValues && selectedValues.length > 0) { // Находим соответствующие значения из API данных const matchingValues = param.values.filter((value: any) => selectedValues.includes(value.title || value.value) ); if (matchingValues.length > 0) { // Используем ID параметра из API и значения apiParams[param.id] = matchingValues.map((v: any) => v.value); } } }); return apiParams; }, [paramsData, selectedFilters]); // Функция автоматической подгрузки дополнительных страниц PartsIndex const autoLoadMoreEntities = useCallback(async () => { if (isAutoLoading || !hasMoreEntities || !isPartsIndexMode) { return; } console.log('🔄 Автоподгрузка: проверяем товары с предложениями...'); // Восстанавливаем автоподгрузку console.log('🔄 Автоподгрузка активна'); // Подсчитываем текущее количество товаров (все уже отфильтрованы на сервере) const currentEntitiesCount = accumulatedEntities.length; console.log('📊 Автоподгрузка: текущее состояние:', { накопленоТоваров: currentEntitiesCount, целевоеКоличество: ITEMS_PER_PAGE, естьЕщеТовары: hasMoreEntities }); // Если у нас уже достаточно товаров, не загружаем if (currentEntitiesCount >= ITEMS_PER_PAGE) { console.log('✅ Автоподгрузка: достаточно товаров'); return; } // Даем время на загрузку цен товаров, если их слишком много загружается const loadingCount = accumulatedEntities.filter(entity => { const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; return isLoadingPrice(productForPrice); }).length; // Ждем только если загружается больше 5 товаров одновременно if (loadingCount > 5) { console.log('⏳ Автоподгрузка: ждем загрузки цен для', loadingCount, 'товаров (больше 5)'); return; } // Если накопили уже много товаров, но мало с предложениями - прекращаем попытки if (accumulatedEntities.length >= ITEMS_PER_PAGE * 8) { // Увеличили лимит с 4 до 8 страниц console.log('⚠️ Автоподгрузка: достигли лимита попыток, прекращаем'); return; } setIsAutoLoading(true); try { console.log('🔄 Автоподгрузка: загружаем следующую страницу PartsIndex...'); const apiParams = convertFiltersToPartsIndexParams; const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined; const result = await refetchEntities({ catalogId: catalogId as string, groupId: groupId as string, lang: 'ru', limit: PARTSINDEX_PAGE_SIZE, page: partsIndexPage + 1, q: searchQuery || undefined, params: paramsString }); console.log('✅ Автоподгрузка: страница загружена, результат:', result.data?.partsIndexCatalogEntities?.list?.length || 0); } catch (error) { console.error('❌ Автоподгрузка: ошибка загрузки следующей страницы:', error); } finally { setIsAutoLoading(false); } }, [isAutoLoading, hasMoreEntities, isPartsIndexMode, accumulatedEntities.length, partsIndexPage, refetchEntities, catalogId, groupId, searchQuery]); // Генерация фильтров для PartsIndex на основе параметров API const generatePartsIndexFilters = useCallback((): FilterConfig[] => { if (!paramsData?.partsIndexCatalogParams?.list) { return []; } return paramsData.partsIndexCatalogParams.list.map((param: any) => { if (param.type === 'range') { // Для range фильтров ищем min и max значения const numericValues = param.values .map((v: any) => parseFloat(v.value)) .filter((v: number) => !isNaN(v)); const min = numericValues.length > 0 ? Math.min(...numericValues) : 0; const max = numericValues.length > 0 ? Math.max(...numericValues) : 100; return { type: 'range' as const, title: param.name, min, max }; } else { // Для dropdown фильтров return { type: 'dropdown' as const, title: param.name, options: param.values .filter((value: any) => value.available) // Показываем только доступные .map((value: any) => value.title || value.value), multi: true, showAll: true }; } }); }, [paramsData]); useEffect(() => { if (isPartsIndexMode) { // Для PartsIndex генерируем фильтры на основе параметров API const filters = generatePartsIndexFilters(); setCatalogFilters(filters); setFiltersLoading(paramsLoading); } else { // Для других режимов убираем запрос на catalog-filters setFiltersLoading(false); } }, [isPartsIndexMode, generatePartsIndexFilters, paramsLoading]); // Автоматическая подгрузка товаров с задержкой для загрузки цен useEffect(() => { if (!isPartsIndexMode || accumulatedEntities.length === 0 || isAutoLoading) { return; } // Даем время на загрузку цен (3 секунды после последнего изменения) const timer = setTimeout(() => { autoLoadMoreEntities(); }, 3000); return () => clearTimeout(timer); }, [isPartsIndexMode, accumulatedEntities.length, isAutoLoading]); // Дополнительный триггер автоподгрузки при изменении количества товаров с предложениями useEffect(() => { console.log('🔍 Проверка триггера автоподгрузки:', { isPartsIndexMode, entitiesWithOffersLength: entitiesWithOffers.length, isAutoLoading, hasMoreEntities, targetItemsPerPage: ITEMS_PER_PAGE }); if (!isPartsIndexMode || entitiesWithOffers.length === 0 || isAutoLoading) { return; } // Если товаров с предложениями мало, запускаем автоподгрузку через 1 секунду if (entitiesWithOffers.length < ITEMS_PER_PAGE && hasMoreEntities) { console.log('🚀 Запускаем автоподгрузку: товаров', entitiesWithOffers.length, 'из', ITEMS_PER_PAGE); const timer = setTimeout(() => { console.log('🚀 Дополнительная автоподгрузка: недостаточно товаров с предложениями'); autoLoadMoreEntities(); }, 1000); return () => clearTimeout(timer); } else { console.log('✅ Автоподгрузка не нужна: товаров достаточно или нет больше данных'); } }, [isPartsIndexMode, entitiesWithOffers.length, hasMoreEntities, isAutoLoading]); // Обновляем список товаров при изменении накопленных товаров (серверная фильтрация) useEffect(() => { if (!isPartsIndexMode) { return; } // Все товары уже отфильтрованы на сервере - показываем все накопленные const entitiesWithOffers = accumulatedEntities; console.log('📊 Обновляем entitiesWithOffers (серверная фильтрация):', { накопленоТоваров: accumulatedEntities.length, отображаемыхТоваров: entitiesWithOffers.length, целевоеКоличество: ITEMS_PER_PAGE }); setEntitiesWithOffers(entitiesWithOffers); // Показываем товары для текущей пользовательской страницы const startIndex = (currentUserPage - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; const visibleForCurrentPage = entitiesWithOffers.slice(startIndex, endIndex); console.log('📊 Обновляем visibleEntities:', { currentUserPage, startIndex, endIndex, visibleForCurrentPage: visibleForCurrentPage.length, entitiesWithOffers: entitiesWithOffers.length }); setVisibleEntities(visibleForCurrentPage); }, [isPartsIndexMode, accumulatedEntities, currentUserPage]); // Генерируем динамические фильтры для PartsAPI const generatePartsAPIFilters = useCallback((): FilterConfig[] => { if (!allArticles.length) return []; // Получаем список видимых товаров из карты видимости const visibleIndices = Array.from(visibilityMap.entries()) .filter(([_, isVisible]) => isVisible) .map(([index]) => index); // Если еще нет данных о видимости, используем все товары (для начальной загрузки) const articlesToProcess = visibilityMap.size === 0 ? allArticles : visibleIndices.map(index => allArticles[index]).filter(Boolean); const brandCounts = new Map(); const productGroups = new Set(); // Подсчитываем количество товаров для каждого бренда (только видимые) articlesToProcess.forEach(article => { if (article?.artSupBrand) { brandCounts.set(article.artSupBrand, (brandCounts.get(article.artSupBrand) || 0) + 1); } if (article?.productGroup) productGroups.add(article.productGroup); }); const filters: FilterConfig[] = []; if (brandCounts.size > 1) { // Сортируем бренды по количеству товаров (по убыванию) const sortedBrands = Array.from(brandCounts.entries()) .sort((a, b) => b[1] - a[1]) // Сортируем по количеству товаров .map(([brand]) => brand); // Показываем либо первые N брендов, либо все (если нажата кнопка "Показать еще") const brandsToShow = showAllBrands ? sortedBrands : sortedBrands.slice(0, MAX_BRANDS_DISPLAY); filters.push({ type: "dropdown", title: "Бренд", options: brandsToShow.sort(), // Сортируем по алфавиту для удобства multi: true, showAll: true, defaultOpen: true, hasMore: !showAllBrands && sortedBrands.length > MAX_BRANDS_DISPLAY, onShowMore: () => setShowAllBrands(true) }); } if (productGroups.size > 1) { filters.push({ type: "dropdown", title: "Группа товаров", options: Array.from(productGroups).sort(), multi: true, showAll: true, defaultOpen: true, }); } return filters; }, [allArticles, showAllBrands, visibilityMap]); const dynamicFilters = useMemo(() => { if (isPartsIndexMode) { return generatePartsIndexFilters(); } else if (isPartsAPIMode) { return generatePartsAPIFilters(); } return []; }, [isPartsIndexMode, isPartsAPIMode, generatePartsIndexFilters, generatePartsAPIFilters]); // Отдельный useEffect для управления состоянием загрузки фильтров useEffect(() => { if ((isPartsAPIMode && allArticles.length > 0) || (isPartsIndexMode && visibleEntities.length > 0)) { setFiltersGenerating(true); const timer = setTimeout(() => { setFiltersGenerating(false); }, 300); return () => clearTimeout(timer); } else { setFiltersGenerating(false); } }, [isPartsAPIMode, allArticles.length, isPartsIndexMode, visibleEntities.length]); const handleDesktopFilterChange = (filterTitle: string, value: string | string[]) => { setSelectedFilters(prev => ({ ...prev, [filterTitle]: Array.isArray(value) ? value : [value] })); }; const handleMobileFilterChange = (type: string, value: any) => { setSelectedFilters(prev => ({ ...prev, [type]: Array.isArray(value) ? value : [value] })); }; // Функция для сброса всех фильтров const handleResetFilters = useCallback(() => { setSearchQuery(''); setSelectedFilters({}); setShowAllBrands(false); setPartsIndexPage(1); // Сбрасываем страницу PartsIndex на первую }, []); // Фильтрация по поиску и фильтрам для PartsAPI const filteredArticles = useMemo(() => { return allArticles.filter(article => { // Фильтрация по поиску if (searchQuery.trim()) { const searchLower = searchQuery.toLowerCase(); const articleTitle = [ article.artSupBrand || '', article.artArticleNr || '', article.productGroup || '' ].join(' ').toLowerCase(); if (!articleTitle.includes(searchLower)) { return false; } } // Фильтрация по выбранным фильтрам const brandFilter = selectedFilters['Бренд'] || []; if (brandFilter.length > 0 && !brandFilter.includes(article.artSupBrand || '')) { return false; } const groupFilter = selectedFilters['Группа товаров'] || []; if (groupFilter.length > 0 && !groupFilter.includes(article.productGroup || '')) { return false; } return true; }); }, [allArticles, searchQuery, selectedFilters]); // Обновляем видимые артикулы при изменении поиска или фильтров для PartsAPI useEffect(() => { if (isPartsAPIMode) { setVisibleArticles(filteredArticles.slice(0, ITEMS_PER_PAGE)); setCurrentPage(1); setIsLoadingMore(false); setVisibilityMap(new Map()); // Сбрасываем карту видимости при изменении фильтров setVisibleProductsCount(0); // Сбрасываем счетчик setLoadedArticlesCount(ITEMS_PER_PAGE); // Сбрасываем счетчик загруженных setShowEmptyState(false); // Сбрасываем пустое состояние } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPartsAPIMode, searchQuery, JSON.stringify(selectedFilters), filteredArticles.length]); // Обновляем видимые товары при изменении поиска или фильтров для PartsIndex useEffect(() => { if (isPartsIndexMode) { // При изменении поиска или фильтров сбрасываем пагинацию setShowEmptyState(false); // Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) { console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию'); setPartsIndexPage(1); setCurrentUserPage(1); setHasMoreEntities(true); setAccumulatedEntities([]); setEntitiesWithOffers([]); setEntitiesCache(new Map()); // Перезагружаем данные с новыми параметрами фильтрации const apiParams = convertFiltersToPartsIndexParams; const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined; // Также обновляем параметры фильтрации refetchParams({ catalogId: catalogId as string, groupId: groupId as string, lang: 'ru', q: searchQuery || undefined, params: paramsString }); refetchEntities({ catalogId: catalogId as string, groupId: groupId as string, lang: 'ru', limit: PARTSINDEX_PAGE_SIZE, page: 1, q: searchQuery || undefined, params: paramsString }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), refetchEntities, refetchParams, convertFiltersToPartsIndexParams]); // Управляем показом пустого состояния с задержкой useEffect(() => { if (isPartsAPIMode && !articlesLoading && !articlesError) { // Если товаров вообще нет - показываем сразу if (allArticles.length === 0) { setShowEmptyState(true); return; } // Если товары есть, но нет видимых - ждем 2 секунды const timer = setTimeout(() => { setShowEmptyState(visibleProductsCount === 0 && allArticles.length > 0); }, 2000); // Даем 2 секунды на загрузку данных о предложениях return () => clearTimeout(timer); } else if (isPartsIndexMode && !entitiesLoading && !entitiesError) { // Для PartsIndex показываем пустое состояние если нет товаров И данные уже загружены const hasLoadedData = accumulatedEntities.length > 0 || Boolean(entitiesData?.partsIndexCatalogEntities?.list); // Показываем пустое состояние если данные загружены и нет видимых товаров // (товары уже отфильтрованы на сервере, поэтому не нужно ждать загрузки цен) const shouldShowEmpty = hasLoadedData && visibleEntities.length === 0; setShowEmptyState(shouldShowEmpty); console.log('📊 Определяем showEmptyState для PartsIndex (серверная фильтрация):', { hasLoadedData, visibleEntitiesLength: visibleEntities.length, accumulatedEntitiesLength: accumulatedEntities.length, shouldShowEmpty, showEmptyState: shouldShowEmpty }); } else { setShowEmptyState(false); } }, [isPartsAPIMode, articlesLoading, articlesError, visibleProductsCount, allArticles.length, isPartsIndexMode, entitiesLoading, entitiesError, visibleEntities.length, accumulatedEntities.length, entitiesData]); // Функции для навигации по пользовательским страницам const handleNextPage = useCallback(() => { const maxUserPage = Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE); console.log('🔄 Нажата кнопка "Вперед":', { currentUserPage, maxUserPage, accumulatedEntitiesLength: accumulatedEntities.length, ITEMS_PER_PAGE }); if (currentUserPage < maxUserPage) { setCurrentUserPage(prev => { console.log('✅ Переходим на страницу:', prev + 1); return prev + 1; }); } else { console.log('⚠️ Нельзя перейти вперед: уже на последней странице'); } }, [currentUserPage, accumulatedEntities.length]); const handlePrevPage = useCallback(() => { console.log('🔄 Нажата кнопка "Назад":', { currentUserPage, accumulatedEntitiesLength: accumulatedEntities.length }); if (currentUserPage > 1) { setCurrentUserPage(prev => { const newPage = prev - 1; console.log('✅ Переходим на страницу:', newPage); return newPage; }); } else { console.log('⚠️ Нельзя перейти назад: уже на первой странице'); } }, [currentUserPage, accumulatedEntities.length]); // Функция для загрузки следующей порции товаров по кнопке (только для PartsAPI) const handleLoadMorePartsAPI = useCallback(async () => { if (isLoadingMore || !isPartsAPIMode) { return; } setIsLoadingMore(true); try { const additionalCount = Math.min(ITEMS_PER_PAGE, filteredArticles.length - loadedArticlesCount); if (additionalCount > 0) { const newArticles = filteredArticles.slice(loadedArticlesCount, loadedArticlesCount + additionalCount); setVisibleArticles(prev => [...prev, ...newArticles]); setLoadedArticlesCount(prev => prev + additionalCount); setTargetVisibleCount(prev => prev + ITEMS_PER_PAGE); } } catch (error) { console.error('❌ Ошибка загрузки дополнительных товаров:', error); } finally { setIsLoadingMore(false); } }, [isPartsAPIMode, loadedArticlesCount, filteredArticles, isLoadingMore]); // Определяем есть ли еще товары для загрузки (только для PartsAPI) const hasMoreItems = useMemo(() => { if (isPartsAPIMode) { return loadedArticlesCount < filteredArticles.length; } return false; }, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]); if (filtersLoading) { return
Загрузка фильтров...
; } // Определяем meta-теги для каталога const categoryNameDecoded = decodeURIComponent(categoryName as string || 'Каталог'); const metaData = createCategoryMeta(categoryNameDecoded, visibleProductsCount || undefined); // Генерируем микроразметку для каталога const breadcrumbSchema = generateBreadcrumbSchema([ { name: "Главная", url: "https://protek.ru/" }, { name: "Каталог", url: "https://protek.ru/catalog" }, ...(categoryName ? [{ name: categoryNameDecoded, url: `https://protek.ru/catalog?categoryName=${categoryName}` }] : []) ]); const websiteSchema = generateWebSiteSchema( "Protek - Каталог автозапчастей", "https://protek.ru", "https://protek.ru/search" ); return ( <> 0 ? undefined : visibleProductsCount) : isPartsIndexMode ? entitiesWithOffers.length : 3587 } productName={ isPartsAPIMode ? "запчасть" : isPartsIndexMode ? "товар" : "аккумулятор" } breadcrumbs={[ { label: "Главная", href: "/" }, { label: "Каталог" }, ...((isPartsAPIMode || isPartsIndexMode) ? [{ label: decodeURIComponent(categoryName as string || 'Товары') }] : []) ]} showCount={true} showProductHelp={true} />
setShowFiltersMobile((v) => !v)}>
Фильтры
{isPartsAPIMode ? (
) : isPartsIndexMode ? (
) : (
)} setShowFiltersMobile(false)} filters={isPartsAPIMode ? dynamicFilters : catalogFilters} searchQuery={searchQuery} onSearchChange={setSearchQuery} filterValues={selectedFilters} onFilterChange={handleMobileFilterChange} />
{/* Индикатор загрузки для PartsAPI */} {isPartsAPIMode && articlesLoading && (
)} {/* Индикатор загрузки для PartsIndex */} {isPartsIndexMode && entitiesLoading && (
)} {/* Сообщение об ошибке */} {isPartsAPIMode && articlesError && (
Ошибка загрузки артикулов: {articlesError.message}
)} {/* Сообщение об ошибке для PartsIndex */} {isPartsIndexMode && entitiesError && (
Ошибка загрузки товаров: {entitiesError.message}
)} {/* Отображение артикулов PartsAPI */} {isPartsAPIMode && visibleArticles.length > 0 && ( <> {visibleArticles.map((article, idx) => ( ))} {/* Кнопка "Показать еще" */} {hasMoreItems && (
)} )} {/* Отображение товаров PartsIndex */} {isPartsIndexMode && (() => { console.log('🎯 Проверяем отображение PartsIndex товаров:', { isPartsIndexMode, visibleEntitiesLength: visibleEntities.length, visibleEntities: visibleEntities.map(e => ({ id: e.id, code: e.code, brand: e.brand.name })) }); return visibleEntities.length > 0; })() && ( <> {visibleEntities .map((entity, idx) => { const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; const priceData = getPrice(productForPrice); const isLoadingPriceData = isLoadingPrice(productForPrice); // Определяем цену для отображения (все товары уже отфильтрованы на сервере) let displayPrice = ""; let displayCurrency = "RUB"; let priceElement; if (isLoadingPriceData) { // Показываем скелетон загрузки вместо текста priceElement = ; } else if (priceData && priceData.price) { displayPrice = `${priceData.price.toLocaleString('ru-RU')} ₽`; displayCurrency = priceData.currency || "RUB"; } else { // Если нет данных о цене, показываем скелетон (товар должен загрузиться) priceElement = ; } return ( { // Если цена не загружена, загружаем её и добавляем в корзину if (!priceData && !isLoadingPriceData) { ensurePriceLoaded(productForPrice); console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name); return; } // Если цена есть, добавляем в корзину if (priceData && priceData.price) { const itemToAdd = { productId: entity.id, offerKey: priceData.offerKey, name: entity.originalName || entity.name?.name || 'Товар без названия', description: `${entity.brand.name} ${entity.code}`, brand: entity.brand.name, article: entity.code, price: priceData.price, currency: priceData.currency || 'RUB', quantity: 1, stock: undefined, // информация о наличии не доступна для PartsIndex deliveryTime: '1-3 дня', warehouse: 'Parts Index', supplier: 'Parts Index', isExternal: true, image: entity.images?.[0] || '', }; const result = await addItem(itemToAdd); if (result.success) { // Показываем уведомление toast.success(
Товар добавлен в корзину!
{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}
, { duration: 3000, icon: , } ); } else { toast.error(result.error || 'Ошибка при добавлении товара в корзину'); } } else { toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.'); } }} /> ); })} {/* Пагинация для PartsIndex */}
Страница {currentUserPage} из {Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE) || 1} {isAutoLoading && ' (загружаем...)'} (товаров: {accumulatedEntities.length})
{/* Отладочная информация */} {isPartsIndexMode && (
🔍 Отладка PartsIndex (исправленная логика):
• accumulatedEntities: {accumulatedEntities.length}
• entitiesWithOffers: {entitiesWithOffers.length}
• visibleEntities: {visibleEntities.length}
• currentUserPage: {currentUserPage}
• partsIndexPage (API): {partsIndexPage}
• isAutoLoading: {isAutoLoading ? 'да' : 'нет'}
• hasMoreEntities: {hasMoreEntities ? 'да' : 'нет'}
• entitiesLoading: {entitiesLoading ? 'да' : 'нет'}
• groupId: {groupId || 'отсутствует'}
• Target: {ITEMS_PER_PAGE} товаров на страницу
• showEmptyState: {showEmptyState ? 'да' : 'нет'}
)} )} {/* Пустое состояние для PartsAPI */} {isPartsAPIMode && !articlesLoading && !articlesError && showEmptyState && ( selectedFilters[key].length > 0)} onResetFilters={handleResetFilters} /> )} {/* Пустое состояние для PartsIndex */} {isPartsIndexMode && !entitiesLoading && !entitiesError && (() => { console.log('🎯 Проверяем пустое состояние PartsIndex:', { isPartsIndexMode, entitiesLoading, entitiesError, showEmptyState, visibleEntitiesLength: visibleEntities.length }); return showEmptyState; })() && ( selectedFilters[key].length > 0)} onResetFilters={handleResetFilters} /> )} {/* Каталог PartsIndex без группы */} {isPartsIndexCatalogOnly && (
Выберите подкатегорию
Для просмотра товаров необходимо выбрать конкретную подкатегорию из меню.
)} {/* Обычные товары (не PartsAPI/PartsIndex) */} {!isPartsAPIMode && !isPartsIndexMode && !isPartsIndexCatalogOnly && (
Раздел в разработке
Данные для этой категории скоро появятся.
)}
{!isPartsAPIMode && !isPartsIndexMode && }