/** * User All Requests Page - For Regular Users * * OPTIMIZED: Uses backend pagination (10 records per page) and backend stats API * Shows requests where the user is EITHER: * - The initiator (created by the user), OR * - A participant (approver/spectator) */ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { Pagination } from '@/components/common/Pagination'; import dashboardService from '@/services/dashboard.service'; import type { DateRange } from '@/services/dashboard.service'; import userApi from '@/services/userApi'; // Components import { RequestsHeader } from './components/RequestsHeader'; import { RequestsStats } from './components/RequestsStats'; import { RequestsList } from './components/RequestsList'; // Hooks import { useRequestsFilters } from './hooks/useRequestsFilters'; import { useUserSearch } from './hooks/useUserSearch'; // Utils import { transformRequests } from './utils/requestTransformers'; import { exportRequestsToCSV } from './utils/csvExports'; // Services import { fetchUserParticipantRequestsData, fetchAllRequestsForExport } from './services/userRequestsService'; // Types import type { RequestsProps, BackendStats } from './types/requests.types'; // Filter UI components import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react'; import { format } from 'date-fns'; export function UserAllRequests({ onViewRequest }: RequestsProps) { // Filters hook const filters = useRequestsFilters(); // State const [apiRequests, setApiRequests] = useState([]); const [loading, setLoading] = useState(false); const [exporting, setExporting] = useState(false); const [backendStats, setBackendStats] = useState(null); // Stats from backend API const [departments, setDepartments] = useState([]); const [loadingDepartments, setLoadingDepartments] = useState(false); const [allUsers, setAllUsers] = useState>([]); // Pagination const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalRecords, setTotalRecords] = useState(0); const [itemsPerPage] = useState(10); // User search hooks const initiatorSearch = useUserSearch({ allUsers, filterValue: filters.initiatorFilter, onFilterChange: filters.setInitiatorFilter }); const approverSearch = useUserSearch({ allUsers, filterValue: filters.approverFilter, onFilterChange: filters.setApproverFilter }); // Fetch backend stats using dashboard API // OPTIMIZED: Uses backend stats API instead of fetching 100 records // Stats reflect all filters EXCEPT status - total stays stable when only status changes const fetchBackendStats = useCallback(async ( statsDateRange?: DateRange, statsStartDate?: Date, statsEndDate?: Date, filtersWithoutStatus?: { priority?: string; department?: string; initiator?: string; approver?: string; approverType?: 'current' | 'any'; search?: string; slaCompliance?: string; } ) => { try { // Use dashboard stats API with viewAsUser=true for user-level stats const stats = await dashboardService.getRequestStats( statsDateRange, statsStartDate ? statsStartDate.toISOString() : undefined, statsEndDate ? statsEndDate.toISOString() : undefined, undefined, // status - stats should show all statuses filtersWithoutStatus?.priority, filtersWithoutStatus?.department, filtersWithoutStatus?.initiator, filtersWithoutStatus?.approver, filtersWithoutStatus?.approverType, filtersWithoutStatus?.search, filtersWithoutStatus?.slaCompliance, true // viewAsUser: always true for user-level ); setBackendStats({ total: stats.totalRequests || 0, pending: stats.openRequests || 0, paused: stats.pausedRequests || 0, approved: stats.approvedRequests || 0, rejected: stats.rejectedRequests || 0, draft: stats.draftRequests || 0, closed: stats.closedRequests || 0 }); } catch (error) { console.error('Failed to fetch backend stats:', error); // Keep previous stats on error } }, []); // Fetch departments const fetchDepartments = useCallback(async () => { try { setLoadingDepartments(true); const depts = await dashboardService.getDepartments(); setDepartments(depts); } catch (error) { // Leave departments empty on error } finally { setLoadingDepartments(false); } }, []); // Fetch users const fetchUsers = useCallback(async () => { try { const usersData = await userApi.getAllUsers(); const usersList = usersData.map((user: any) => ({ userId: user.userId, email: user.email, displayName: user.displayName || user.email })); setAllUsers(usersList); } catch (error) { console.error('Failed to fetch users:', error); } }, []); // Use refs to store stable callbacks to prevent infinite loops const filtersRef = useRef(filters); const fetchBackendStatsRef = useRef(fetchBackendStats); // Update refs on each render useEffect(() => { filtersRef.current = filters; fetchBackendStatsRef.current = fetchBackendStats; }, [filters, fetchBackendStats]); // Fetch requests - OPTIMIZED: Only fetches 10 records per page const fetchRequests = useCallback(async (page: number = 1) => { try { if (page === 1) { setLoading(true); setApiRequests([]); } const filterOptions = filtersRef.current.getFilters(); const result = await fetchUserParticipantRequestsData({ page, itemsPerPage, filters: filterOptions }); setApiRequests(result.data); // Paginated data (10 records) // Update pagination setCurrentPage(result.pagination.page); setTotalPages(result.pagination.totalPages); setTotalRecords(result.pagination.total); } catch (error) { setApiRequests([]); } finally { setLoading(false); } }, [itemsPerPage]); // Export to CSV const handleExportToCSV = useCallback(async () => { try { setExporting(true); const allData = await fetchAllRequestsForExport(filters.getFilters()); await exportRequestsToCSV(allData, filters.getFilters()); } catch (error: any) { console.error('Failed to export requests:', error); alert('Failed to export requests. Please try again.'); } finally { setExporting(false); } }, [filters]); // Initial fetch useEffect(() => { fetchDepartments(); fetchUsers(); }, [fetchDepartments, fetchUsers]); // Fetch backend stats when filters change (except status filter) // OPTIMIZED: Uses backend stats API instead of fetching 100 records useEffect(() => { const timeoutId = setTimeout(() => { const filtersWithoutStatus = { priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, department: filters.departmentFilter !== 'all' ? filters.departmentFilter : undefined, initiator: filters.initiatorFilter !== 'all' ? filters.initiatorFilter : undefined, approver: filters.approverFilter !== 'all' ? filters.approverFilter : undefined, approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined, search: filters.searchTerm || undefined, slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined }; const statsDateRange = filters.dateRange || 'month'; fetchBackendStatsRef.current( statsDateRange, filters.customStartDate, filters.customEndDate, filtersWithoutStatus ); }, filters.searchTerm ? 500 : 0); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ filters.searchTerm, filters.priorityFilter, filters.slaComplianceFilter, filters.departmentFilter, filters.initiatorFilter, filters.approverFilter, filters.approverFilterType, filters.dateRange, filters.customStartDate, filters.customEndDate // Note: statusFilter is NOT in dependencies - stats don't change when only status changes ]); // Fetch requests on mount and when filters change (for list display) useEffect(() => { const timeoutId = setTimeout(() => { setCurrentPage(1); fetchRequests(1); }, filters.searchTerm ? 500 : 0); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.slaComplianceFilter, filters.departmentFilter, filters.initiatorFilter, filters.approverFilter, filters.approverFilterType, filters.dateRange, filters.customStartDate, filters.customEndDate // fetchRequests excluded to prevent infinite loops ]); // Page change handler const handlePageChange = useCallback((newPage: number) => { if (newPage >= 1 && newPage <= totalPages) { setCurrentPage(newPage); fetchRequests(newPage); } }, [totalPages, fetchRequests]); // Transform requests const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]); // Calculate stats - Use backend stats API (OPTIMIZED) const stats = useMemo(() => { // Use backend stats if available if (backendStats) { return { total: backendStats.total || 0, pending: backendStats.pending || 0, paused: backendStats.paused || 0, approved: backendStats.approved || 0, rejected: backendStats.rejected || 0, draft: backendStats.draft || 0, closed: backendStats.closed || 0 }; } // Fallback: calculate from current page (less accurate, but works during initial load) const pending = convertedRequests.filter((r: any) => { const status = (r.status || '').toString().toLowerCase(); return status === 'pending' || status === 'in-progress'; }).length; const paused = convertedRequests.filter((r: any) => { const status = (r.status || '').toString().toLowerCase(); return status === 'paused'; }).length; const approved = convertedRequests.filter((r: any) => { const status = (r.status || '').toString().toLowerCase(); return status === 'approved'; }).length; const rejected = convertedRequests.filter((r: any) => { const status = (r.status || '').toString().toLowerCase(); return status === 'rejected'; }).length; const closed = convertedRequests.filter((r: any) => { const status = (r.status || '').toString().toLowerCase(); return status === 'closed'; }).length; return { total: totalRecords > 0 ? totalRecords : convertedRequests.length, pending, paused, approved, rejected, draft: 0, closed }; }, [backendStats, totalRecords, convertedRequests]); return (
{/* Header */} {/* Stats */} { filters.setStatusFilter(status); }} /> {/* Filters */}

Advanced Filters

{filters.hasActiveFilters && ( Active )}
{filters.hasActiveFilters && ( )}
{/* Primary Filters */}
filters.setSearchTerm(e.target.value)} className="pl-10 h-10" data-testid="search-input" />
{/* User Filters - Initiator and Approver */}
{/* Initiator Filter */}
{initiatorSearch.selectedUser ? (
{initiatorSearch.selectedUser.displayName || initiatorSearch.selectedUser.email}
) : ( <> initiatorSearch.handleSearch(e.target.value)} onFocus={() => { if (initiatorSearch.searchResults.length > 0) { initiatorSearch.setShowResults(true); } }} onBlur={() => setTimeout(() => initiatorSearch.setShowResults(false), 200)} className="h-10" data-testid="initiator-search-input" /> {initiatorSearch.showResults && initiatorSearch.searchResults.length > 0 && (
{initiatorSearch.searchResults.map((user) => ( ))}
)} )}
{/* Approver Filter */}
{filters.approverFilter !== 'all' && ( )}
{approverSearch.selectedUser ? (
{approverSearch.selectedUser.displayName || approverSearch.selectedUser.email}
) : ( <> approverSearch.handleSearch(e.target.value)} onFocus={() => { if (approverSearch.searchResults.length > 0) { approverSearch.setShowResults(true); } }} onBlur={() => setTimeout(() => approverSearch.setShowResults(false), 200)} className="h-10" data-testid="approver-search-input" /> {approverSearch.showResults && approverSearch.searchResults.length > 0 && (
{approverSearch.searchResults.map((user) => ( ))}
)} )}
{/* Date Range Filter */}
{filters.dateRange === 'custom' && (
{ const date = e.target.value ? new Date(e.target.value) : undefined; if (date) { filters.setCustomStartDate(date); if (filters.customEndDate && date > filters.customEndDate) { filters.setCustomEndDate(date); } } else { filters.setCustomStartDate(undefined); } }} max={format(new Date(), 'yyyy-MM-dd')} className="w-full" />
{ const date = e.target.value ? new Date(e.target.value) : undefined; if (date) { filters.setCustomEndDate(date); if (filters.customStartDate && date < filters.customStartDate) { filters.setCustomStartDate(date); } } else { filters.setCustomEndDate(undefined); } }} min={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : undefined} max={format(new Date(), 'yyyy-MM-dd')} className="w-full" />
)}
{/* Requests List */} {/* Pagination */}
); }