Re_Figma_Code/src/pages/Requests/UserAllRequests.tsx

537 lines
19 KiB
TypeScript

/**
* 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<any[]>([]);
const [loading, setLoading] = useState(false);
const [exporting, setExporting] = useState(false);
const [backendStats, setBackendStats] = useState<BackendStats | null>(null); // Stats from backend API
const [departments, setDepartments] = useState<string[]>([]);
const [loadingDepartments, setLoadingDepartments] = useState(false);
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
// 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 (
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto" data-testid="user-all-requests-page">
{/* Header */}
<RequestsHeader
isOrgLevel={false}
isAdmin={false}
loading={loading}
exporting={exporting}
onExport={handleExportToCSV}
/>
{/* Stats */}
<RequestsStats
stats={stats}
onStatusFilter={(status) => {
filters.setStatusFilter(status);
}}
/>
{/* Filters - Plug-and-play pattern */}
<UserAllRequestsFiltersComponent
searchTerm={filters.searchTerm}
statusFilter={filters.statusFilter}
priorityFilter={filters.priorityFilter}
templateTypeFilter={filters.templateTypeFilter}
departmentFilter={filters.departmentFilter}
slaComplianceFilter={filters.slaComplianceFilter}
initiatorFilter={filters.initiatorFilter}
approverFilter={filters.approverFilter}
approverFilterType={filters.approverFilterType}
dateRange={filters.dateRange}
customStartDate={filters.customStartDate}
customEndDate={filters.customEndDate}
showCustomDatePicker={filters.showCustomDatePicker}
departments={departments}
loadingDepartments={loadingDepartments}
initiatorSearch={initiatorSearch}
approverSearch={approverSearch}
onSearchChange={filters.setSearchTerm}
onStatusChange={filters.setStatusFilter}
onPriorityChange={filters.setPriorityFilter}
onTemplateTypeChange={filters.setTemplateTypeFilter}
onDepartmentChange={filters.setDepartmentFilter}
onSlaComplianceChange={filters.setSlaComplianceFilter}
onInitiatorChange={filters.setInitiatorFilter}
onApproverChange={filters.setApproverFilter}
onApproverTypeChange={filters.setApproverFilterType}
onDateRangeChange={filters.handleDateRangeChange}
onCustomStartDateChange={filters.setCustomStartDate}
onCustomEndDateChange={filters.setCustomEndDate}
onShowCustomDatePickerChange={filters.setShowCustomDatePicker}
onApplyCustomDate={filters.handleApplyCustomDate}
onClearFilters={filters.clearFilters}
hasActiveFilters={calculateActiveFiltersCount()}
/>
{/* Requests List */}
<RequestsList
requests={convertedRequests}
loading={loading}
hasActiveFilters={filters.hasActiveFilters}
onViewRequest={onViewRequest}
/>
{/* Pagination */}
<Pagination
currentPage={filters.currentPage || 1}
totalPages={totalPages}
totalRecords={totalRecords}
itemsPerPage={itemsPerPage}
onPageChange={handlePageChange}
loading={loading}
itemLabel="requests"
testIdPrefix="requests-pagination"
/>
</div>
);
}