Files
protekauto-frontend/src/pages/catalog.tsx

860 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import 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';
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 = 20;
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<PartsAPIArticle[]>([]);
const [visibleEntities, setVisibleEntities] = useState<PartsIndexEntity[]>([]);
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<FilterConfig[]>([]);
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); // Общее количество страниц
// Карта видимости товаров по индексу
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(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
});
// Загружаем артикулы PartsAPI
const { data: articlesData, loading: articlesLoading, error: articlesError } = useQuery<PartsAPIArticlesData, PartsAPIArticlesVariables>(
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<PartsIndexEntitiesData, PartsIndexEntitiesVariables>(
GET_PARTSINDEX_CATALOG_ENTITIES,
{
variables: {
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
limit: ITEMS_PER_PAGE,
page: partsIndexPage,
q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
},
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-and-network'
}
);
// Загружаем параметры фильтрации для PartsIndex
const { data: paramsData, loading: paramsLoading, error: paramsError } = useQuery<PartsIndexParamsData, PartsIndexParamsVariables>(
GET_PARTSINDEX_CATALOG_PARAMS,
{
variables: {
catalogId: catalogId as string,
groupId: groupId as string,
lang: 'ru',
q: searchQuery || undefined,
params: Object.keys(selectedFilters).length > 0 ? JSON.stringify(selectedFilters) : undefined
},
skip: !isPartsIndexMode || !groupId, // Пропускаем запрос если нет groupId
fetchPolicy: 'cache-first'
}
);
// allEntities больше не используется - используем allLoadedEntities
// Хук для загрузки цен товаров PartsIndex
const productsForPrices = visibleEntities.map(entity => ({
id: entity.id,
code: entity.code,
brand: entity.brand.name
}));
const { getPrice, isLoadingPrice, loadPriceOnDemand } = useProductPrices(productsForPrices);
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;
// Обновляем список товаров
setVisibleEntities(newEntities);
// Обновляем информацию о пагинации
const currentPage = pagination?.page?.current || 1;
const hasNext = pagination?.page?.next !== null;
const hasPrev = pagination?.page?.prev !== null;
setPartsIndexPage(currentPage);
setHasMoreEntities(hasNext);
// Вычисляем общее количество страниц (приблизительно)
if (hasNext) {
setTotalPages(currentPage + 1); // Минимум еще одна страница
} else {
setTotalPages(currentPage); // Это последняя страница
}
console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev });
}
}, [entitiesData]);
// Генерация фильтров для PartsIndex на основе параметров API
const generatePartsIndexFilters = useCallback((): FilterConfig[] => {
if (!paramsData?.partsIndexCatalogParams?.list) {
return [];
}
return paramsData.partsIndexCatalogParams.list.map(param => {
if (param.type === 'range') {
// Для range фильтров ищем min и max значения
const numericValues = param.values
.map(v => parseFloat(v.value))
.filter(v => !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 => value.available) // Показываем только доступные
.map(value => 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]);
// Генерируем динамические фильтры для 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<string, number>();
const productGroups = new Set<string>();
// Подсчитываем количество товаров для каждого бренда (только видимые)
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]);
// Упрощенная логика - показываем все загруженные товары без клиентской фильтрации
const filteredEntities = visibleEntities;
// Обновляем видимые артикулы при изменении поиска или фильтров для 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);
setHasMoreEntities(true);
// refetch будет автоматически вызван при изменении partsIndexPage
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters)]);
// Управляем показом пустого состояния с задержкой
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 показываем пустое состояние если нет товаров
setShowEmptyState(visibleEntities.length === 0);
} else {
setShowEmptyState(false);
}
}, [isPartsAPIMode, articlesLoading, articlesError, visibleProductsCount, allArticles.length,
isPartsIndexMode, entitiesLoading, entitiesError, visibleEntities.length, filteredEntities.length]);
// Функции для навигации по страницам PartsIndex
const handleNextPage = useCallback(() => {
if (hasMoreEntities && !entitiesLoading) {
setPartsIndexPage(prev => prev + 1);
}
}, [hasMoreEntities, entitiesLoading]);
const handlePrevPage = useCallback(() => {
if (partsIndexPage > 1 && !entitiesLoading) {
setPartsIndexPage(prev => prev - 1);
}
}, [partsIndexPage, entitiesLoading]);
// Функция для загрузки следующей порции товаров по кнопке (только для 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 <div className="py-8 text-center">Загрузка фильтров...</div>;
}
return (
<>
<Head>
<title>Catalog</title>
<meta name="description" content="Catalog" />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<CatalogInfoHeader
title={
isPartsAPIMode ? decodeURIComponent(categoryName as string || 'Запчасти') :
isPartsIndexMode ? decodeURIComponent(categoryName as string || 'Товары') :
"Аккумуляторы"
}
count={
isPartsAPIMode ?
(visibilityMap.size === 0 && allArticles.length > 0 ? undefined : visibleProductsCount) :
isPartsIndexMode ?
(searchQuery.trim() || Object.keys(selectedFilters).length > 0 ?
filteredEntities.length :
entitiesData?.partsIndexCatalogEntities?.pagination?.limit || visibleEntities.length) :
3587
}
productName={
isPartsAPIMode ? "запчасть" :
isPartsIndexMode ? "товар" :
"аккумулятор"
}
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Каталог" },
...((isPartsAPIMode || isPartsIndexMode) ? [{ label: decodeURIComponent(categoryName as string || 'Товары') }] : [])
]}
showCount={true}
showProductHelp={true}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-hflex flex-block-13">
<div className="w-layout-hflex flex-block-84">
<div className="w-layout-hflex flex-block-85" onClick={() => setShowFiltersMobile((v) => !v)}>
<div className="code-embed-9 w-embed">
<svg width="currentwidth" height="currentheight" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 4H14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 4H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 12H12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 12H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 20H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 20H3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 2V6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8 10V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 18V22" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div>Фильтры</div>
</div>
</div>
{isPartsAPIMode ? (
<div className="filters-desktop">
<Filters
filters={dynamicFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersGenerating}
/>
</div>
) : isPartsIndexMode ? (
<div className="filters-desktop">
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersLoading}
/>
</div>
) : (
<div className="filters-desktop">
<Filters
filters={catalogFilters}
onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
isLoading={filtersLoading}
/>
</div>
)}
<FiltersPanelMobile
open={showFiltersMobile}
onClose={() => setShowFiltersMobile(false)}
filters={isPartsAPIMode ? dynamicFilters : catalogFilters}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filterValues={selectedFilters}
onFilterChange={handleMobileFilterChange}
/>
<div className="w-layout-vflex flex-block-14-copy-copy">
{/* Индикатор загрузки для PartsAPI */}
{isPartsAPIMode && articlesLoading && (
<div className="flex justify-center items-center py-8">
<LoadingSpinner size="lg" text="Загружаем артикулы..." />
</div>
)}
{/* Индикатор загрузки для PartsIndex */}
{isPartsIndexMode && entitiesLoading && (
<div className="flex justify-center items-center py-8">
<LoadingSpinner size="lg" text="Загружаем товары..." />
</div>
)}
{/* Сообщение об ошибке */}
{isPartsAPIMode && articlesError && (
<div className="flex justify-center items-center py-8">
<div className="text-lg text-red-600">Ошибка загрузки артикулов: {articlesError.message}</div>
</div>
)}
{/* Сообщение об ошибке для PartsIndex */}
{isPartsIndexMode && entitiesError && (
<div className="flex justify-center items-center py-8">
<div className="text-lg text-red-600">Ошибка загрузки товаров: {entitiesError.message}</div>
</div>
)}
{/* Отображение артикулов PartsAPI */}
{isPartsAPIMode && visibleArticles.length > 0 && (
<>
{visibleArticles.map((article, idx) => (
<ArticleCard
key={`${article.artId}_${idx}`}
article={article}
index={idx}
onVisibilityChange={handleVisibilityChange}
/>
))}
{/* Кнопка "Показать еще" */}
{hasMoreItems && (
<div className="w-layout-hflex pagination">
<button
onClick={handleLoadMorePartsAPI}
disabled={isLoadingMore}
className="button_strock w-button"
>
{isLoadingMore ? (
<>
Загружаем...
</>
) : (
<>
Показать еще
</>
)}
</button>
</div>
)}
</>
)}
{/* Отображение товаров PartsIndex */}
{isPartsIndexMode && filteredEntities.length > 0 && (
<>
{filteredEntities
.map((entity, idx) => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice);
const isLoadingPriceData = isLoadingPrice(productForPrice);
return {
entity,
idx,
productForPrice,
priceData,
isLoadingPriceData,
hasOffer: priceData !== null || isLoadingPriceData
};
})
.filter(item => item.hasOffer) // Показываем только товары с предложениями или загружающиеся
.map(({ entity, idx, productForPrice, priceData, isLoadingPriceData }) => {
// Определяем цену для отображения
let displayPrice = "Цена по запросу";
let displayCurrency = "RUB";
let priceElement;
if (isLoadingPriceData) {
priceElement = <PriceSkeleton />;
} else if (priceData && priceData.price) {
displayPrice = `${priceData.price.toLocaleString('ru-RU')}`;
displayCurrency = priceData.currency || "RUB";
}
return (
<CatalogProductCard
key={`${entity.id}_${idx}`}
title={entity.originalName || entity.name?.name || 'Товар без названия'}
brand={entity.brand.name}
articleNumber={entity.code}
brandName={entity.brand.name}
image={entity.images?.[0] || ''}
price={isLoadingPriceData ? "" : displayPrice}
priceElement={priceElement}
oldPrice=""
discount=""
currency={displayCurrency}
productId={entity.id}
artId={entity.id}
offerKey={priceData?.offerKey}
onAddToCart={() => {
// Если цена не загружена, загружаем её и добавляем в корзину
if (!priceData && !isLoadingPriceData) {
loadPriceOnDemand(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,
deliveryTime: '1-3 дня',
warehouse: 'Parts Index',
supplier: 'Parts Index',
isExternal: true,
image: entity.images?.[0] || '',
};
addItem(itemToAdd);
// Показываем уведомление
toast.success(
<div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')}`}</div>
</div>,
{
duration: 3000,
icon: <CartIcon size={20} color="#fff" />,
}
);
} else {
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
}
}}
/>
);
})}
{/* Пагинация для PartsIndex */}
<div className="w-layout-hflex pagination">
<button
onClick={handlePrevPage}
disabled={partsIndexPage <= 1 || entitiesLoading}
className="button_strock w-button mr-2"
>
Назад
</button>
<span className="flex items-center px-4 text-gray-600">
Страница {partsIndexPage} {totalPages > partsIndexPage && `из ${totalPages}+`}
</span>
<button
onClick={handleNextPage}
disabled={!hasMoreEntities || entitiesLoading}
className="button_strock w-button ml-2"
>
{entitiesLoading ? 'Загрузка...' : 'Вперед →'}
</button>
</div>
{/* Отладочная информация */}
{isPartsIndexMode && (
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<div>🔍 Отладка PartsIndex:</div>
<div> hasMoreItems: {hasMoreItems ? 'да' : 'нет'}</div>
<div> hasMoreEntities: {hasMoreEntities ? 'да' : 'нет'}</div>
<div> entitiesPage: {entitiesPage}</div>
<div> visibleEntities: {visibleEntities.length}</div>
<div> filteredEntities: {filteredEntities.length}</div>
<div> groupId: {groupId || 'отсутствует'}</div>
<div> isLoadingMore: {isLoadingMore ? 'да' : 'нет'}</div>
<div> entitiesLoading: {entitiesLoading ? 'да' : 'нет'}</div>
<div> catalogId: {catalogId || 'отсутствует'}</div>
<div> Пагинация: {JSON.stringify(entitiesData?.partsIndexCatalogEntities?.pagination)}</div>
</div>
)}
</>
)}
{/* Пустое состояние для PartsAPI */}
{isPartsAPIMode && !articlesLoading && !articlesError && showEmptyState && (
<CatalogEmptyState
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
onResetFilters={handleResetFilters}
/>
)}
{/* Пустое состояние для PartsIndex */}
{isPartsIndexMode && !entitiesLoading && !entitiesError && showEmptyState && (
<CatalogEmptyState
categoryName={decodeURIComponent(categoryName as string || 'товаров')}
hasFilters={searchQuery.trim() !== '' || Object.keys(selectedFilters).some(key => selectedFilters[key].length > 0)}
onResetFilters={handleResetFilters}
/>
)}
{/* Каталог PartsIndex без группы */}
{isPartsIndexCatalogOnly && (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-gray-500 text-lg mb-4">Выберите подкатегорию</div>
<div className="text-gray-400 text-sm">Для просмотра товаров необходимо выбрать конкретную подкатегорию из меню.</div>
</div>
)}
{/* Обычные товары (не PartsAPI/PartsIndex) */}
{!isPartsAPIMode && !isPartsIndexMode && !isPartsIndexCatalogOnly && (
<div className="flex flex-col items-center justify-center py-12">
<div className="text-gray-500 text-lg mb-4">Раздел в разработке</div>
<div className="text-gray-400 text-sm">Данные для этой категории скоро появятся.</div>
</div>
)}
</div>
</div>
</div>
</section>
{!isPartsAPIMode && !isPartsIndexMode && <CatalogPagination />}
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}