Files
protekauto-frontend/src/components/profile/ProfileHistoryMain.tsx
egortriston 2703137ca1 fix1607
2025-07-16 14:28:47 +03:00

540 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useState, useEffect } from "react";
import { useQuery, useMutation } from '@apollo/client';
import ProfileHistoryItem from "./ProfileHistoryItem";
import SearchInput from "./SearchInput";
import ProfileHistoryTabs from "./ProfileHistoryTabs";
import Pagination from '../Pagination';
import {
GET_PARTS_SEARCH_HISTORY,
DELETE_SEARCH_HISTORY_ITEM,
CLEAR_SEARCH_HISTORY,
CREATE_SEARCH_HISTORY_ITEM,
PartsSearchHistoryItem,
PartsSearchHistoryResponse
} from '@/lib/graphql/search-history';
const ProfileHistoryMain = () => {
const [search, setSearch] = useState("");
const [activeTab, setActiveTab] = useState("Все");
const [selectedManufacturer, setSelectedManufacturer] = useState("Все");
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
// Состояние пагинации
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10); // Количество элементов на странице
const tabOptions = ["Все", "Сегодня", "Вчера", "Эта неделя", "Этот месяц"];
// GraphQL запросы
const { data, loading, error, refetch } = useQuery<{ partsSearchHistory: PartsSearchHistoryResponse }>(
GET_PARTS_SEARCH_HISTORY,
{
variables: {
limit: 1000, // Загружаем больше для клиентской пагинации с фильтрами
offset: 0
},
fetchPolicy: 'cache-and-network',
onCompleted: (data) => {
console.log('История поиска загружена:', data);
},
onError: (error) => {
console.error('Ошибка загрузки истории поиска:', error);
}
}
);
const [deleteItem] = useMutation(DELETE_SEARCH_HISTORY_ITEM, {
onCompleted: () => {
refetch();
},
onError: (error) => {
console.error('Ошибка удаления элемента истории:', error);
}
});
const [clearHistory] = useMutation(CLEAR_SEARCH_HISTORY, {
onCompleted: () => {
refetch();
},
onError: (error) => {
console.error('Ошибка очистки истории:', error);
}
});
const [createHistoryItem] = useMutation(CREATE_SEARCH_HISTORY_ITEM, {
onCompleted: () => {
refetch();
},
onError: (error) => {
console.error('Ошибка создания записи истории:', error);
}
});
const historyItems = data?.partsSearchHistory?.items || [];
// Отладочная информация
console.log('ProfileHistoryMain состояние:', {
loading,
error: error?.message,
data,
historyItemsCount: historyItems.length
});
// Фильтрация по времени
const getFilteredByTime = (items: PartsSearchHistoryItem[], timeFilter: string) => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
const monthAgo = new Date(today);
monthAgo.setMonth(monthAgo.getMonth() - 1);
switch (timeFilter) {
case "Сегодня":
return items.filter(item => new Date(item.createdAt) >= today);
case "Вчера":
return items.filter(item => {
const itemDate = new Date(item.createdAt);
return itemDate >= yesterday && itemDate < today;
});
case "Эта неделя":
return items.filter(item => new Date(item.createdAt) >= weekAgo);
case "Этот месяц":
return items.filter(item => new Date(item.createdAt) >= monthAgo);
default:
return items;
}
};
// Фильтрация и сортировка
useEffect(() => {
let filtered = [...getFilteredByTime(historyItems, activeTab)];
// Фильтрация по производителю
if (selectedManufacturer !== "Все") {
filtered = filtered.filter(item =>
item.brand === selectedManufacturer ||
item.vehicleInfo?.brand === selectedManufacturer
);
}
// Поиск
if (search.trim()) {
const searchLower = search.toLowerCase();
filtered = filtered.filter(item =>
item.searchQuery.toLowerCase().includes(searchLower) ||
item.brand?.toLowerCase().includes(searchLower) ||
item.articleNumber?.toLowerCase().includes(searchLower) ||
item.vehicleInfo?.brand?.toLowerCase().includes(searchLower) ||
item.vehicleInfo?.model?.toLowerCase().includes(searchLower)
);
}
// Сортировка
if (sortField) {
filtered.sort((a, b) => {
let aValue: string | number = '';
let bValue: string | number = '';
switch (sortField) {
case 'date':
aValue = new Date(a.createdAt).getTime();
bValue = new Date(b.createdAt).getTime();
break;
case 'manufacturer':
aValue = a.brand || a.vehicleInfo?.brand || '';
bValue = b.brand || b.vehicleInfo?.brand || '';
break;
case 'name':
aValue = a.searchQuery;
bValue = b.searchQuery;
break;
}
if (typeof aValue === 'string' && typeof bValue === 'string') {
const comparison = aValue.localeCompare(bValue);
return sortOrder === 'asc' ? comparison : -comparison;
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
}
return 0;
});
}
setFilteredItems(filtered);
// Сбрасываем страницу на первую при изменении фильтров
setCurrentPage(1);
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
// Вычисляем элементы для текущей страницы
const totalPages = Math.ceil(filteredItems.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentPageItems = filteredItems.slice(startIndex, endIndex);
// Обработчик изменения страницы
const handlePageChange = (page: number) => {
setCurrentPage(page);
// Прокручиваем к началу списка при смене страницы
const historyContainer = document.querySelector('.flex.flex-col.mt-5.w-full.text-gray-400');
if (historyContainer) {
historyContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
// Обработчик изменения количества элементов на странице
const handleItemsPerPageChange = (newItemsPerPage: number) => {
setItemsPerPage(newItemsPerPage);
setCurrentPage(1); // Сбрасываем на первую страницу
};
const handleSort = (field: "date" | "manufacturer" | "name") => {
if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortField(field);
setSortOrder("desc"); // По умолчанию сначала новые
}
};
const handleDeleteItem = async (id: string) => {
if (window.confirm('Удалить этот элемент из истории?')) {
try {
await deleteItem({ variables: { id } });
} catch (error) {
console.error('Ошибка удаления:', error);
}
}
};
const handleClearHistory = async () => {
if (window.confirm('Очистить всю историю поиска? Это действие нельзя отменить.')) {
try {
await clearHistory();
} catch (error) {
console.error('Ошибка очистки истории:', error);
}
}
};
const handleCreateTestData = async () => {
const testItems = [
{
searchQuery: "тормозные колодки",
searchType: "TEXT" as const,
brand: "BREMBO",
resultCount: 15,
vehicleBrand: "BMW",
vehicleModel: "X5",
vehicleYear: 2020
},
{
searchQuery: "0986424781",
searchType: "ARTICLE" as const,
brand: "BOSCH",
articleNumber: "0986424781",
resultCount: 3
},
{
searchQuery: "масляный фильтр",
searchType: "TEXT" as const,
brand: "MANN",
resultCount: 22,
vehicleBrand: "AUDI",
vehicleModel: "A4",
vehicleYear: 2018
},
{
searchQuery: "34116858652",
searchType: "OEM" as const,
brand: "BMW",
articleNumber: "34116858652",
resultCount: 8,
vehicleBrand: "BMW",
vehicleModel: "3 Series",
vehicleYear: 2019
},
{
searchQuery: "свечи зажигания",
searchType: "TEXT" as const,
brand: "NGK",
resultCount: 45
}
];
try {
for (const item of testItems) {
await createHistoryItem({
variables: { input: item }
});
// Небольшая задержка между запросами
await new Promise(resolve => setTimeout(resolve, 200));
}
} catch (error) {
console.error('Ошибка создания тестовых данных:', error);
}
};
if (loading && historyItems.length === 0) {
return (
<div className="flex flex-col flex-1 shrink justify-center basis-0 w-full max-md:max-w-full min-h-[526px] h-full">
<div className="flex justify-center items-center h-40">
<div className="text-gray-500">Загрузка истории поиска...</div>
</div>
</div>
);
}
if (error) {
return (
<div className="flex flex-col flex-1 shrink justify-center basis-0 w-full max-md:max-w-full min-h-[526px]">
<div className="flex justify-center items-center h-40">
<div className="text-red-500">Ошибка загрузки истории поиска</div>
</div>
</div>
);
}
return (
<div className="flex flex-col flex-1 shrink justify-center basis-0 w-full max-md:max-w-full min-h-[526px]">
<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">
<SearchInput
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Поиск в истории..."
/>
</div>
<div className="flex gap-2 max-sm:hidden">
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
<button
onClick={() => {
setSelectedManufacturer("Все");
setSearch("");
setActiveTab("Все");
setCurrentPage(1);
}}
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
Сбросить фильтры
</button>
)}
{historyItems.length === 0 && (
<button
onClick={handleCreateTestData}
className="px-4 py-2 text-sm text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors"
>
Создать тестовые данные
</button>
)}
{historyItems.length > 0 && (
<button
onClick={handleClearHistory}
className="px-4 py-2 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50 transition-colors"
>
Очистить историю
</button>
)}
</div>
<img
loading="lazy"
src="https://cdn.builder.io/api/v1/image/assets/TEMP/02c9461c587bf477e8ee3187cb5faa1bccaf0900?placeholderIfAbsent=true&apiKey=f5bc5a2dc9b841d0aba1cc6c74a35920"
className="object-contain shrink-0 self-stretch my-auto w-5 rounded-sm aspect-square max-md:mt-2"
/>
</div>
<div className="flex flex-col mt-5 w-full text-lg font-medium leading-tight whitespace-nowrap text-gray-950 max-md:max-w-full">
<ProfileHistoryTabs
tabs={tabOptions}
activeTab={activeTab}
onTabChange={setActiveTab}
historyItems={historyItems}
selectedManufacturer={selectedManufacturer}
onManufacturerChange={setSelectedManufacturer}
/>
</div>
<div className="flex flex-col mt-5 w-full text-gray-400 max-md:max-w-full flex-1 h-full">
<div className="flex flex-col p-2 w-full bg-white rounded-xl h-full max-md:max-w-full min-h-[250px] ">
<div className="hidden md:flex gap-10 items-center px-5 py-2 w-full text-sm max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:px-2">
<div className="flex flex-wrap flex-1 shrink gap-5 items-center self-stretch pr-5 my-auto w-full basis-0 min-w-[240px] max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:p-0 max-md:min-w-0">
<div className={`flex gap-1.5 items-center self-stretch my-auto w-40 max-md:w-full ${sortField === 'date' ? 'text-[#ec1c24] font-semibold' : ''}`}>
<div
className="self-stretch my-auto cursor-pointer select-none hover:text-[#ec1c24] transition-colors"
onClick={() => handleSort('date')}
>
Дата и время
</div>
<svg
width="14"
height="14"
fill="none"
viewBox="0 0 20 20"
className="transition-transform duration-200"
style={{
transform: sortField === 'date' && sortOrder === 'asc' ? 'rotate(180deg)' : 'none',
opacity: sortField === 'date' ? 1 : 0.5,
stroke: sortField === 'date' ? '#ec1c24' : '#9CA3AF'
}}
>
<path d="M6 8l4 4 4-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div className={`flex gap-1.5 items-center self-stretch my-auto w-40 whitespace-nowrap max-md:w-full ${sortField === 'manufacturer' ? 'text-[#ec1c24] font-semibold' : ''}`}>
<div
className="self-stretch my-auto cursor-pointer select-none hover:text-[#ec1c24] transition-colors"
onClick={() => handleSort('manufacturer')}
>
Производитель
</div>
<svg
width="14"
height="14"
fill="none"
viewBox="0 0 20 20"
className="transition-transform duration-200"
style={{
transform: sortField === 'manufacturer' && sortOrder === 'asc' ? 'rotate(180deg)' : 'none',
opacity: sortField === 'manufacturer' ? 1 : 0.5,
stroke: sortField === 'manufacturer' ? '#ec1c24' : '#9CA3AF'
}}
>
<path d="M6 8l4 4 4-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div className="gap-1.5 self-stretch my-auto whitespace-nowrap w-[180px] max-md:w-full">
Артикул/Тип
</div>
<div className={`flex flex-wrap flex-1 shrink gap-1.5 items-center self-stretch my-auto whitespace-nowrap basis-0 max-md:max-w-full max-md:w-full ${sortField === 'name' ? 'text-[#ec1c24] font-semibold' : ''}`}>
<div
className="self-stretch my-auto cursor-pointer select-none hover:text-[#ec1c24] transition-colors"
onClick={() => handleSort('name')}
>
Поисковый запрос
</div>
<svg
width="14"
height="14"
fill="none"
viewBox="0 0 20 20"
className="transition-transform duration-200"
style={{
transform: sortField === 'name' && sortOrder === 'asc' ? 'rotate(180deg)' : 'none',
opacity: sortField === 'name' ? 1 : 0.5,
stroke: sortField === 'name' ? '#ec1c24' : '#9CA3AF'
}}
>
<path d="M6 8l4 4 4-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div className="w-16 text-center max-md:w-full">
Действия
</div>
</div>
</div>
{filteredItems.length === 0 ? (
<div className="flex justify-center items-center py-12 h-full max-md:h-full">
<div className="text-center text-gray-500 h-full">
{historyItems.length === 0 ? (
<>
<div className="text-lg mb-2 " >История поиска пуста</div>
<div className="text-sm">Ваши поисковые запросы будут отображаться здесь</div>
</>
) : (
<>
<div className="text-lg mb-2">Ничего не найдено</div>
<div className="text-sm">Попробуйте изменить фильтры или поисковый запрос</div>
</>
)}
</div>
</div>
) : (
currentPageItems.map((item) => (
<ProfileHistoryItem
key={item.id}
id={item.id}
date={new Date(item.createdAt).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
manufacturer={item.brand || item.vehicleInfo?.brand || 'Не указан'}
article={item.articleNumber || `${item.searchType} поиск`}
name={item.searchQuery}
vehicleInfo={item.vehicleInfo}
resultCount={item.resultCount}
onDelete={handleDeleteItem}
searchType={item.searchType}
articleNumber={item.articleNumber}
brand={item.brand}
/>
))
)}
</div>
{/* Пагинация */}
{filteredItems.length > 0 && (
<div className="mt-6 space-y-4">
{/* Селектор количества элементов на странице */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-2 sm:space-y-0">
<div className="flex items-center space-x-2 text-sm text-gray-500">
<span>Показывать по:</span>
<select
value={itemsPerPage}
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
className="px-2 py-1 border border-gray-200 rounded text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-[#ec1c24] focus:border-transparent"
style={{ cursor: 'pointer' }}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
<span>записей</span>
</div>
<div className="text-sm text-gray-500 text-center sm:text-right">
Показано {startIndex + 1}-{Math.min(endIndex, filteredItems.length)} из {filteredItems.length} записей
{filteredItems.length !== historyItems.length && (
<span className="ml-1">
(всего {historyItems.length})
</span>
)}
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
<span className="ml-2 text-blue-600">
(применены фильтры)
</span>
)}
</div>
</div>
{/* Компонент пагинации */}
{filteredItems.length > itemsPerPage && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
showPageInfo={true}
/>
)}
</div>
)}
</div>
</div>
);
};
export default ProfileHistoryMain;