diff --git a/frontend/src/app/requests/page.tsx b/frontend/src/app/requests/page.tsx index ce74892d..33a326e2 100644 --- a/frontend/src/app/requests/page.tsx +++ b/frontend/src/app/requests/page.tsx @@ -4,7 +4,7 @@ import { AppShell } from "../../components/AppShell"; import { apiClient } from "@/lib/api"; import { useUser } from "@/contexts/UserContext"; import { canManageRequests, canProcessRequests } from "@/lib/permissions"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import type { Request, RequestComment, User, Department } from "@/types/api"; import { Ban, Check, ChevronDown, FileSignature, Filter, MessageSquare, Paperclip, Pencil, Plus, Search, ThumbsDown, ThumbsUp, Trash2, X } from "lucide-react"; @@ -313,7 +313,7 @@ export default function RequestsPage() { } }; - const handleLoadMore = async () => { + const handleLoadMore = useCallback(async () => { if (!nextPage || loadingMore) return; try { setLoadingMore(true); @@ -331,7 +331,7 @@ export default function RequestsPage() { } finally { setLoadingMore(false); } - }; + }, [nextPage, loadingMore, view, typeFilter, statusFilter, employeeFilter, dateFromFilter, dateToFilter]); const handleApprove = async (id: number) => { try { @@ -431,6 +431,33 @@ export default function RequestsPage() { const isFinal = (status?: string) => ["approved", "rejected", "cancelled"].includes(String(status || "").toLowerCase()); const fileInputRef = useRef(null); + const loadMoreRef = useRef(null); + + // Автоматическая подгрузка при прокрутке + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const target = entries[0]; + if (target.isIntersecting && nextPage && !loadingMore && !loading) { + handleLoadMore(); + } + }, + { + rootMargin: '100px', // Начинаем загрузку за 100px до конца + } + ); + + const currentRef = loadMoreRef.current; + if (currentRef) { + observer.observe(currentRef); + } + + return () => { + if (currentRef) { + observer.unobserve(currentRef); + } + }; + }, [nextPage, loadingMore, loading, handleLoadMore]); /* ---- Searchable multi-select dropdown ---- */ const SearchableSelect = ({ label, items, selectedIds, onToggle, placeholder }: { @@ -916,18 +943,17 @@ export default function RequestsPage() { )} - {nextPage ? ( -
- + {/* Элемент-наблюдатель для автоматической подгрузки */} + {nextPage && ( +
+ {loadingMore && ( +
+
+ Загрузка... +
+ )}
- ) : null} + )} )} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 9e55e3fa..a376f3bc 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -551,13 +551,30 @@ class ApiClient { } // Заявки - async getRequests(params?: { status?: string; type?: string; search?: string; page?: number; limit?: number }): Promise { + async getRequests(params?: { + status?: string; + type?: string; + search?: string; + page?: number; + limit?: number; + view?: string; + addressed_to_me?: string; + employee_id?: string | number; + date_from?: string; + date_to?: string; + [key: string]: any; + }): Promise { const queryParams = new URLSearchParams(); if (params?.status) queryParams.append('status', params.status); if (params?.type) queryParams.append('type', params.type); if (params?.search) queryParams.append('search', params.search); if (params?.page) queryParams.append('page', params.page.toString()); if (params?.limit) queryParams.append('limit', params.limit.toString()); + if (params?.view) queryParams.append('view', params.view); + if (params?.addressed_to_me) queryParams.append('addressed_to_me', params.addressed_to_me); + if (params?.employee_id) queryParams.append('employee_id', params.employee_id.toString()); + if (params?.date_from) queryParams.append('date_from', params.date_from); + if (params?.date_to) queryParams.append('date_to', params.date_to); const url = `/api/v1/requests/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; return this.request(url);