/** * 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'; import { getUserFilterType } from '@/utils/userFilterUtils'; import { getUserAllRequestsFilters } from '@/flows'; import { TokenManager } from '@/utils/tokenManager'; // Services import { fetchUserParticipantRequestsData, fetchAllRequestsForExport } from './services/userRequestsService'; // Types import type { RequestsProps, BackendStats } from './types/requests.types'; export function UserAllRequests({ onViewRequest }: RequestsProps) { // Filters hook const filters = useRequestsFilters(); // Get user filter type and corresponding filter component (plug-and-play pattern) // Determine once at the beginning - no need to check repeatedly const userFilterType = useMemo(() => { try { const userData = TokenManager.getUserData(); return getUserFilterType(userData); } catch (error) { console.error('[UserAllRequests] Error getting user filter type:', error); return 'STANDARD' as const; } }, []); // Get the appropriate filter component based on user type const UserAllRequestsFiltersComponent = useMemo(() => { return getUserAllRequestsFilters(userFilterType); }, [userFilterType]); // Determine once - use this throughout instead of checking repeatedly const isDealer = userFilterType === 'DEALER'; // Helper to get filters for API - excludes dealer-restricted filters // Since we know user type initially, this helper uses that knowledge const getFiltersForApi = useCallback(() => { const filterOptions = filters.getFilters(); if (isDealer) { // For dealers, exclude priority, templateType, department, and slaCompliance const { priority, templateType, department, slaCompliance, ...dealerFilters } = filterOptions; return dealerFilters; } return filterOptions; }, [filters, isDealer]); // Helper to calculate active filters count based on user type const calculateActiveFiltersCount = useCallback(() => { if (isDealer) { // For dealers: only count search, status, initiator, approver, and date filters return !!( filters.searchTerm || filters.statusFilter !== 'all' || filters.initiatorFilter !== 'all' || filters.approverFilter !== 'all' || filters.dateRange !== 'all' || filters.customStartDate || filters.customEndDate ); } // For standard users: count all filters (use existing hasActiveFilters) return filters.hasActiveFilters; }, [isDealer, filters]); // 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 (currentPage now in Redux) 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; templateType?: 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?.templateType, 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); const getFiltersForApiRef = useRef(getFiltersForApi); // Update refs on each render useEffect(() => { filtersRef.current = filters; fetchBackendStatsRef.current = fetchBackendStats; getFiltersForApiRef.current = getFiltersForApi; }, [filters, fetchBackendStats, getFiltersForApi]); // 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 = getFiltersForApiRef.current(); const result = await fetchUserParticipantRequestsData({ page, itemsPerPage, filters: filterOptions }); setApiRequests(result.data); // Paginated data (10 records) // Update pagination filters.setCurrentPage(result.pagination.page); setTotalPages(result.pagination.totalPages); setTotalRecords(result.pagination.total); } catch (error) { setApiRequests([]); } finally { setLoading(false); } }, [itemsPerPage, filters]); // Export to CSV const handleExportToCSV = useCallback(async () => { try { setExporting(true); const exportFilters = getFiltersForApi(); const allData = await fetchAllRequestsForExport(exportFilters); await exportRequestsToCSV(allData, exportFilters); } catch (error: any) { console.error('Failed to export requests:', error); alert('Failed to export requests. Please try again.'); } finally { setExporting(false); } }, [getFiltersForApi]); // 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?: string; templateType?: string; department?: string; initiator?: string; approver?: string; approverType?: 'current' | 'any'; search?: string; slaCompliance?: string; } = { 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, }; // Only include priority, templateType, department, and slaCompliance if user is not a dealer if (!isDealer) { if (filters.priorityFilter !== 'all') filtersWithoutStatus.priority = filters.priorityFilter; if (filters.templateTypeFilter !== 'all') filtersWithoutStatus.templateType = filters.templateTypeFilter; if (filters.departmentFilter !== 'all') filtersWithoutStatus.department = filters.departmentFilter; if (filters.slaComplianceFilter !== 'all') filtersWithoutStatus.slaCompliance = filters.slaComplianceFilter; } // Use 'all' if dateRange is 'all', otherwise use the selected dateRange or default to 'month' const statsDateRange = filters.dateRange === 'all' ? 'all' : (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, filters.templateTypeFilter, isDealer // Note: statusFilter is NOT in dependencies - stats don't change when only status changes ]); // Track previous filter values to detect changes const prevFiltersRef = useRef({ searchTerm: filters.searchTerm, statusFilter: filters.statusFilter, priorityFilter: filters.priorityFilter, templateTypeFilter: filters.templateTypeFilter, slaComplianceFilter: filters.slaComplianceFilter, departmentFilter: filters.departmentFilter, initiatorFilter: filters.initiatorFilter, approverFilter: filters.approverFilter, approverFilterType: filters.approverFilterType, dateRange: filters.dateRange, customStartDate: filters.customStartDate, customEndDate: filters.customEndDate, }); const hasInitialFetchRun = useRef(false); // Initial fetch on mount - use stored page from Redux useEffect(() => { const storedPage = filters.currentPage || 1; fetchRequests(storedPage); hasInitialFetchRun.current = true; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only on mount // Fetch when filters change useEffect(() => { if (!hasInitialFetchRun.current) return; const prev = prevFiltersRef.current; const hasChanged = prev.searchTerm !== filters.searchTerm || prev.statusFilter !== filters.statusFilter || prev.priorityFilter !== filters.priorityFilter || prev.templateTypeFilter !== filters.templateTypeFilter || prev.slaComplianceFilter !== filters.slaComplianceFilter || prev.departmentFilter !== filters.departmentFilter || prev.initiatorFilter !== filters.initiatorFilter || prev.approverFilter !== filters.approverFilter || prev.approverFilterType !== filters.approverFilterType || prev.dateRange !== filters.dateRange || prev.customStartDate !== filters.customStartDate || prev.customEndDate !== filters.customEndDate; if (!hasChanged) return; const timeoutId = setTimeout(() => { filters.setCurrentPage(1); fetchRequests(1); prevFiltersRef.current = { searchTerm: filters.searchTerm, statusFilter: filters.statusFilter, priorityFilter: filters.priorityFilter, templateTypeFilter: filters.templateTypeFilter, slaComplianceFilter: filters.slaComplianceFilter, departmentFilter: filters.departmentFilter, initiatorFilter: filters.initiatorFilter, approverFilter: filters.approverFilter, approverFilterType: filters.approverFilterType, dateRange: filters.dateRange, customStartDate: filters.customStartDate, customEndDate: filters.customEndDate, }; }, filters.searchTerm !== prev.searchTerm ? 500 : 0); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.slaComplianceFilter, filters.departmentFilter, filters.initiatorFilter, filters.approverFilter, filters.approverFilterType, filters.dateRange, filters.customStartDate, filters.customEndDate ]); // Page change handler const handlePageChange = useCallback((newPage: number) => { if (newPage >= 1 && newPage <= totalPages) { filters.setCurrentPage(newPage); fetchRequests(newPage); } }, [totalPages, fetchRequests, filters]); // 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 - Plug-and-play pattern */} {/* Requests List */} {/* Pagination */}
); }