From a4abc2ab581b1c5d7d345dc1b0ed3cc9caed84ad Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Thu, 4 Dec 2025 21:17:01 +0530 Subject: [PATCH] i have made filter date consistent even after navigation --- .../admin/UserManagement/UserManagement.tsx | 11 +- .../admin/UserRoleManager/UserRoleManager.tsx | 11 +- .../sla/SLAProgressBar/SLAProgressBar.tsx | 37 ++- .../ApprovalWorkflow/ApprovalStepCard.tsx | 6 +- src/hooks/useRequestDetails.ts | 4 +- .../hooks/useApproverPerformanceData.ts | 8 - src/pages/ClosedRequests/ClosedRequests.tsx | 74 +++++- .../components/ClosedRequestsFilters.tsx | 12 +- .../ClosedRequests/hooks/useClosedRequests.ts | 27 +-- .../hooks/useClosedRequestsFilters.ts | 39 ++- .../redux/closedRequestsSlice.ts | 63 +++++ src/pages/MyRequests/MyRequests.tsx | 69 ++++-- src/pages/MyRequests/hooks/useMyRequests.ts | 15 +- .../MyRequests/hooks/useMyRequestsFilters.ts | 40 +++- src/pages/MyRequests/redux/myRequestsSlice.ts | 52 ++++ src/pages/OpenRequests/OpenRequests.tsx | 224 +++++++++--------- .../hooks/useOpenRequestsFilters.ts | 55 +++++ .../OpenRequests/redux/openRequestsSlice.ts | 63 +++++ src/pages/RequestDetail/RequestDetail.tsx | 3 +- .../components/QuickActionsSidebar.tsx | 78 +++++- .../components/RequestDetailHeader.tsx | 12 +- src/pages/Requests/Requests.tsx | 80 ++++++- src/pages/Requests/UserAllRequests.tsx | 74 +++++- .../Requests/hooks/useRequestsFilters.ts | 126 +++++----- src/pages/Requests/redux/requestsSlice.ts | 116 +++++++++ src/redux/store.ts | 8 + src/services/dashboard.service.ts | 2 +- 27 files changed, 988 insertions(+), 321 deletions(-) create mode 100644 src/pages/ClosedRequests/redux/closedRequestsSlice.ts create mode 100644 src/pages/MyRequests/redux/myRequestsSlice.ts create mode 100644 src/pages/OpenRequests/hooks/useOpenRequestsFilters.ts create mode 100644 src/pages/OpenRequests/redux/openRequestsSlice.ts create mode 100644 src/pages/Requests/redux/requestsSlice.ts diff --git a/src/components/admin/UserManagement/UserManagement.tsx b/src/components/admin/UserManagement/UserManagement.tsx index a24192a..6c116c3 100644 --- a/src/components/admin/UserManagement/UserManagement.tsx +++ b/src/components/admin/UserManagement/UserManagement.tsx @@ -89,14 +89,17 @@ export function UserManagement() { // Search users from Okta const searchUsers = useCallback( debounce(async (query: string) => { - if (!query || query.length < 2) { + // Only trigger search when using @ sign + if (!query || !query.startsWith('@') || query.length < 2) { setSearchResults([]); + setSearching(false); return; } setSearching(true); try { - const response = await userApi.searchUsers(query, 20); + const term = query.slice(1); // Remove @ prefix + const response = await userApi.searchUsers(term, 20); const users = response.data?.data || []; setSearchResults(users); } catch (error: any) { @@ -397,7 +400,7 @@ export function UserManagement() { -

Start typing to search across all Okta users

+

Start with @ to search users (e.g., @john)

{/* Search Results Dropdown */} {searchResults.length > 0 && ( diff --git a/src/components/admin/UserRoleManager/UserRoleManager.tsx b/src/components/admin/UserRoleManager/UserRoleManager.tsx index 582da5c..197eb15 100644 --- a/src/components/admin/UserRoleManager/UserRoleManager.tsx +++ b/src/components/admin/UserRoleManager/UserRoleManager.tsx @@ -100,14 +100,17 @@ export function UserRoleManager() { // Search users from Okta const searchUsers = useCallback( debounce(async (query: string) => { - if (!query || query.length < 2) { + // Only trigger search when using @ sign + if (!query || !query.startsWith('@') || query.length < 2) { setSearchResults([]); + setSearching(false); return; } setSearching(true); try { - const response = await userApi.searchUsers(query, 20); + const term = query.slice(1); // Remove @ prefix + const response = await userApi.searchUsers(term, 20); // Backend returns { success: true, data: [...users], message, timestamp } // Axios response is in response.data, actual user array is in response.data.data @@ -442,7 +445,7 @@ export function UserRoleManager() { )} -

Start typing to search across all Okta users

+

Start with @ to search users (e.g., @john)

{/* Search Results Dropdown */} {searchResults.length > 0 && ( diff --git a/src/components/sla/SLAProgressBar/SLAProgressBar.tsx b/src/components/sla/SLAProgressBar/SLAProgressBar.tsx index e4c2930..6bc67bd 100644 --- a/src/components/sla/SLAProgressBar/SLAProgressBar.tsx +++ b/src/components/sla/SLAProgressBar/SLAProgressBar.tsx @@ -17,12 +17,14 @@ export interface SLAData { interface SLAProgressBarProps { sla: SLAData | null; requestStatus: string; + isPaused?: boolean; testId?: string; } export function SLAProgressBar({ sla, requestStatus, + isPaused = false, testId = 'sla-progress' }: SLAProgressBarProps) { // If request is closed/approved/rejected or no SLA data, show status message @@ -44,34 +46,49 @@ export function SLAProgressBar({ // Use percentage-based colors to match approver SLA tracker // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) + // Grey: When paused (frozen state) const percentageUsed = sla.percentageUsed || 0; const rawStatus = sla.status || 'on_track'; // Determine colors based on percentage (matching ApprovalStepCard logic) const getStatusColors = () => { + // If paused, use grey colors regardless of percentage + if (isPaused) { + return { + badge: 'bg-gray-500 text-white', + progress: 'bg-gray-500', + text: 'text-gray-600', + icon: 'text-gray-500' + }; + } + if (percentageUsed >= 100) { return { badge: 'bg-red-600 text-white animate-pulse', progress: 'bg-red-600', - text: 'text-red-600' + text: 'text-red-600', + icon: 'text-blue-600' }; } else if (percentageUsed >= 75) { return { badge: 'bg-orange-500 text-white', progress: 'bg-orange-500', - text: 'text-orange-600' + text: 'text-orange-600', + icon: 'text-blue-600' }; } else if (percentageUsed >= 50) { return { badge: 'bg-amber-500 text-white', progress: 'bg-amber-500', - text: 'text-amber-600' + text: 'text-amber-600', + icon: 'text-blue-600' }; } else { return { badge: 'bg-green-600 text-white', progress: 'bg-green-600', - text: 'text-gray-700' + text: 'text-gray-700', + icon: 'text-blue-600' }; } }; @@ -87,14 +104,20 @@ export function SLAProgressBar({
- - SLA Progress + {isPaused ? ( + + ) : ( + + )} + + {isPaused ? 'SLA Progress (Paused)' : 'SLA Progress'} +
- {sla.percentageUsed || 0}% elapsed + {sla.percentageUsed || 0}% elapsed {isPaused && '(frozen)'}
diff --git a/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx b/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx index 66bcc4c..afb6561 100644 --- a/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx +++ b/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx @@ -369,6 +369,7 @@ export function ApprovalStepCard({ {/* Current Approver - Time Tracking */}
= 100 ? 'bg-red-50 border-red-200' : (approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' : (approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' : @@ -376,7 +377,7 @@ export function ApprovalStepCard({ }`}>

- Current Approver - Time Tracking + Current Approver - Time Tracking {isPaused && '(Paused)'}

@@ -395,14 +396,17 @@ export function ApprovalStepCard({ {(() => { // Determine color based on percentage used // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) + // Grey: When paused (frozen state) const percentUsed = approval.sla.percentageUsed || 0; const getActiveIndicatorColor = () => { + if (isPaused) return 'bg-gray-500'; // Grey when paused if (percentUsed >= 100) return 'bg-red-600'; if (percentUsed >= 75) return 'bg-orange-500'; if (percentUsed >= 50) return 'bg-amber-500'; return 'bg-green-600'; }; const getActiveTextColor = () => { + if (isPaused) return 'text-gray-600'; // Grey when paused if (percentUsed >= 100) return 'text-red-600'; if (percentUsed >= 75) return 'text-orange-600'; if (percentUsed >= 50) return 'text-amber-600'; diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts index 9eaf6a1..391945d 100644 --- a/src/hooks/useRequestDetails.ts +++ b/src/hooks/useRequestDetails.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; -import workflowApi from '@/services/workflowApi'; +import workflowApi, { getPauseDetails } from '@/services/workflowApi'; import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; import { getSocket } from '@/utils/socket'; @@ -223,7 +223,6 @@ export function useRequestDetails( */ let pauseInfo = null; try { - const { getPauseDetails } = await import('@/services/workflowApi'); pauseInfo = await getPauseDetails(wf.requestId); } catch (error) { // Pause info not available or request not paused - ignore @@ -436,7 +435,6 @@ export function useRequestDetails( // Fetch pause details let pauseInfo = null; try { - const { getPauseDetails } = await import('@/services/workflowApi'); pauseInfo = await getPauseDetails(wf.requestId); } catch (error) { // Pause info not available or request not paused - ignore diff --git a/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts b/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts index fe5d0b5..403f1f4 100644 --- a/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts +++ b/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts @@ -50,13 +50,6 @@ export function useApproverPerformanceData({ try { const dateRangeToSend = dateRange === 'all' ? undefined : dateRange; - console.log('[Stats] Fetching with filters:', { - dateRange: dateRangeToSend, - customStartDate, - customEndDate, - priority: priorityFilter, - sla: slaComplianceFilter - }); const stats = await dashboardService.getSingleApproverStats( approverId, @@ -67,7 +60,6 @@ export function useApproverPerformanceData({ slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined ); - console.log('[Stats] Received stats:', stats); setApproverStats(stats); } catch (error) { console.error('[ApproverPerformance] Failed to fetch approver stats:', error); diff --git a/src/pages/ClosedRequests/ClosedRequests.tsx b/src/pages/ClosedRequests/ClosedRequests.tsx index efc9f51..cbf29a2 100644 --- a/src/pages/ClosedRequests/ClosedRequests.tsx +++ b/src/pages/ClosedRequests/ClosedRequests.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useRef, useEffect } from 'react'; // Components import { ClosedRequestsHeader } from './components/ClosedRequestsHeader'; @@ -12,7 +12,7 @@ import { useClosedRequests } from './hooks/useClosedRequests'; import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters'; // Types -import type { ClosedRequestsProps, ClosedRequestsFilters } from './types/closedRequests.types'; +import type { ClosedRequestsProps } from './types/closedRequests.types'; export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { // Data fetching hook @@ -22,26 +22,74 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { const fetchRef = useRef(closedRequests.fetchRequests); fetchRef.current = closedRequests.fetchRequests; - const filters = useClosedRequestsFilters({ - onFiltersChange: useCallback( - (filters: ClosedRequestsFilters) => { - // Reset to page 1 when filters change - fetchRef.current(1, { - search: filters.search || undefined, - status: filters.status !== 'all' ? filters.status : undefined, - priority: filters.priority !== 'all' ? filters.priority : undefined, + const filters = useClosedRequestsFilters(); + const prevFiltersRef = useRef({ + searchTerm: filters.searchTerm, + statusFilter: filters.statusFilter, + priorityFilter: filters.priorityFilter, + sortBy: filters.sortBy, + sortOrder: filters.sortOrder, + }); + const hasInitialFetchRun = useRef(false); + + // Initial fetch on mount - use stored page from Redux + useEffect(() => { + const storedPage = filters.currentPage || 1; + fetchRef.current(storedPage, { + search: filters.searchTerm || undefined, + status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, + priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, sortBy: filters.sortBy, sortOrder: filters.sortOrder, }); - }, - [] - ), + hasInitialFetchRun.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Only on mount + + // Track filter changes and refetch + useEffect(() => { + if (!hasInitialFetchRun.current) return; + + const prev = prevFiltersRef.current; + const hasChanged = + prev.searchTerm !== filters.searchTerm || + prev.statusFilter !== filters.statusFilter || + prev.priorityFilter !== filters.priorityFilter || + prev.sortBy !== filters.sortBy || + prev.sortOrder !== filters.sortOrder; + + if (!hasChanged) return; // No actual change, skip + + // Debounce search + const timeoutId = setTimeout(() => { + filters.setCurrentPage(1); // Reset to page 1 when filters change + fetchRef.current(1, { + search: filters.searchTerm || undefined, + status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, + priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, + sortBy: filters.sortBy, + sortOrder: filters.sortOrder, }); + + // Update previous values + prevFiltersRef.current = { + searchTerm: filters.searchTerm, + statusFilter: filters.statusFilter, + priorityFilter: filters.priorityFilter, + sortBy: filters.sortBy, + sortOrder: filters.sortOrder, + }; + }, filters.searchTerm !== prev.searchTerm ? 500 : 0); + + return () => clearTimeout(timeoutId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.sortBy, filters.sortOrder]); // Page change handler const handlePageChange = useCallback( (newPage: number) => { if (newPage >= 1 && newPage <= closedRequests.pagination.totalPages) { + filters.setCurrentPage(newPage); // Update page in Redux closedRequests.fetchRequests(newPage, { search: filters.searchTerm || undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, diff --git a/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx b/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx index 7a118dc..9fe3198 100644 --- a/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx +++ b/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx @@ -106,20 +106,20 @@ export function ClosedRequestsFilters({ setSearchTerm(e.target.value)} + value={filters.searchTerm} + onChange={(e) => filters.setSearchTerm(e.target.value)} className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-1 focus:ring-blue-200 transition-colors" />
- @@ -389,7 +375,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { - @@ -401,7 +387,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- filters.setSortBy(value)}> @@ -416,10 +402,10 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
@@ -481,34 +467,55 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { {/* SLA Display - Compact Version */} {request.currentLevelSLA && (() => { + // Check pause status from isPaused field, pauseInfo, OR status field + const isPaused = Boolean( + request.isPaused || + request.pauseInfo?.isPaused || + request.status === 'paused' + ); // Use percentage-based colors to match approver SLA tracker // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) + // Grey: When paused (frozen state) const percentUsed = request.currentLevelSLA.percentageUsed || 0; const getSLAColors = () => { + // If paused, always use grey colors (frozen state) + if (isPaused) { + return { + bg: 'bg-gray-100 border border-gray-300', + progress: 'bg-gray-500', + text: 'text-gray-600', + icon: 'text-gray-600' + }; + } + if (percentUsed >= 100) { return { bg: 'bg-red-50 border border-red-200', progress: 'bg-red-600', - text: 'text-red-600' + text: 'text-red-600', + icon: 'text-blue-600' }; } else if (percentUsed >= 75) { return { bg: 'bg-orange-50 border border-orange-200', progress: 'bg-orange-500', - text: 'text-orange-600' + text: 'text-orange-600', + icon: 'text-blue-600' }; } else if (percentUsed >= 50) { return { bg: 'bg-amber-50 border border-amber-200', progress: 'bg-amber-500', - text: 'text-amber-600' + text: 'text-amber-600', + icon: 'text-blue-600' }; } else { return { bg: 'bg-green-50 border border-green-200', progress: 'bg-green-600', - text: 'text-gray-700' + text: 'text-gray-700', + icon: 'text-blue-600' }; } }; @@ -519,17 +526,18 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- - TAT: {percentUsed}% + {isPaused ? ( + + ) : ( + + )} + + TAT: {percentUsed}% {isPaused && '(paused)'} +
{request.currentLevelSLA.elapsedText} - = 100 ? 'text-red-600' : - percentUsed >= 75 ? 'text-orange-600' : - percentUsed >= 50 ? 'text-amber-600' : - 'text-gray-700' - }`}> + {request.currentLevelSLA.remainingText} left
@@ -599,16 +607,16 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {

No requests found

- {searchTerm || activeFiltersCount > 0 + {filters.searchTerm || filters.activeFiltersCount > 0 ? 'Try adjusting your filters or search terms to see more results.' : 'No open requests available at the moment.' }

- {activeFiltersCount > 0 && ( + {filters.activeFiltersCount > 0 && ( @@ -623,21 +631,21 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests + Showing {((filters.currentPage - 1) * itemsPerPage) + 1} to {Math.min(filters.currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests
- {currentPage > 3 && totalPages > 5 && ( + {filters.currentPage > 3 && totalPages > 5 && ( <> ... @@ -647,16 +655,16 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { {getPageNumbers().map((pageNum) => ( ))} - {currentPage < totalPages - 2 && totalPages > 5 && ( + {filters.currentPage < totalPages - 2 && totalPages > 5 && ( <> ... @@ -666,8 +674,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
diff --git a/src/pages/RequestDetail/components/RequestDetailHeader.tsx b/src/pages/RequestDetail/components/RequestDetailHeader.tsx index 8aad598..021cc2b 100644 --- a/src/pages/RequestDetail/components/RequestDetailHeader.tsx +++ b/src/pages/RequestDetail/components/RequestDetailHeader.tsx @@ -20,6 +20,7 @@ interface RequestDetailHeaderProps { export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, onShareSummary, isInitiator }: RequestDetailHeaderProps) { const priorityConfig = getPriorityConfig(request?.priority || 'standard'); const statusConfig = getStatusConfig(request?.status || 'pending'); + const isPaused = request?.pauseInfo?.isPaused || false; return (
@@ -109,8 +110,15 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, on
{/* SLA Progress Section */} -
- +
+
); diff --git a/src/pages/Requests/Requests.tsx b/src/pages/Requests/Requests.tsx index 82e10e9..6eb48fd 100644 --- a/src/pages/Requests/Requests.tsx +++ b/src/pages/Requests/Requests.tsx @@ -71,8 +71,7 @@ export function Requests({ onViewRequest }: RequestsProps) { const [loadingDepartments, setLoadingDepartments] = useState(false); const [allUsers, setAllUsers] = useState>([]); - // Pagination - const [currentPage, setCurrentPage] = useState(1); + // Pagination (currentPage now in Redux) const [totalPages, setTotalPages] = useState(1); const [totalRecords, setTotalRecords] = useState(0); const [itemsPerPage] = useState(10); @@ -270,7 +269,7 @@ export function Requests({ onViewRequest }: RequestsProps) { // Note: Stats come from backend stats API (always unfiltered), not from allData // Update pagination - setCurrentPage(result.pagination.page); + filters.setCurrentPage(result.pagination.page); setTotalPages(result.pagination.totalPages); setTotalRecords(result.pagination.total); @@ -347,18 +346,76 @@ export function Requests({ onViewRequest }: RequestsProps) { // Note: statusFilter is NOT in dependencies - stats don't change when only status changes ]); - // Fetch requests on mount and when filters change - // Also refetch when isOrgLevel changes (when admin toggles between Org/Personal in Dashboard) + // Track previous filter values to detect changes + const prevFiltersRef = useRef({ + searchTerm: filters.searchTerm, + statusFilter: filters.statusFilter, + priorityFilter: filters.priorityFilter, + slaComplianceFilter: filters.slaComplianceFilter, + departmentFilter: filters.departmentFilter, + initiatorFilter: filters.initiatorFilter, + approverFilter: filters.approverFilter, + approverFilterType: filters.approverFilterType, + dateRange: filters.dateRange, + customStartDate: filters.customStartDate, + customEndDate: filters.customEndDate, + isOrgLevel, + }); + 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 or isOrgLevel changes + useEffect(() => { + if (!hasInitialFetchRun.current) return; + + const prev = prevFiltersRef.current; + const hasChanged = + prev.searchTerm !== filters.searchTerm || + prev.statusFilter !== filters.statusFilter || + prev.priorityFilter !== filters.priorityFilter || + 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 || + prev.isOrgLevel !== isOrgLevel; + + if (!hasChanged) return; + const timeoutId = setTimeout(() => { - setCurrentPage(1); + filters.setCurrentPage(1); fetchRequests(1); - }, filters.searchTerm ? 500 : 0); + + prevFiltersRef.current = { + searchTerm: filters.searchTerm, + statusFilter: filters.statusFilter, + priorityFilter: filters.priorityFilter, + slaComplianceFilter: filters.slaComplianceFilter, + departmentFilter: filters.departmentFilter, + initiatorFilter: filters.initiatorFilter, + approverFilter: filters.approverFilter, + approverFilterType: filters.approverFilterType, + dateRange: filters.dateRange, + customStartDate: filters.customStartDate, + customEndDate: filters.customEndDate, + isOrgLevel, + }; + }, filters.searchTerm !== prev.searchTerm ? 500 : 0); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - isOrgLevel, // Re-fetch when org/personal toggle changes + isOrgLevel, filters.searchTerm, filters.statusFilter, filters.priorityFilter, @@ -370,16 +427,15 @@ export function Requests({ onViewRequest }: RequestsProps) { 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); + filters.setCurrentPage(newPage); fetchRequests(newPage); } - }, [totalPages, fetchRequests]); + }, [totalPages, fetchRequests, filters]); // Transform requests const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]); @@ -768,7 +824,7 @@ export function Requests({ onViewRequest }: RequestsProps) { {/* Pagination */} >([]); - // Pagination - const [currentPage, setCurrentPage] = useState(1); + // Pagination (currentPage now in Redux) const [totalPages, setTotalPages] = useState(1); const [totalRecords, setTotalRecords] = useState(0); const [itemsPerPage] = useState(10); @@ -181,7 +180,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { setApiRequests(result.data); // Paginated data (10 records) // Update pagination - setCurrentPage(result.pagination.page); + filters.setCurrentPage(result.pagination.page); setTotalPages(result.pagination.totalPages); setTotalRecords(result.pagination.total); } catch (error) { @@ -250,12 +249,68 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // 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) + // Track previous filter values to detect changes + const prevFiltersRef = useRef({ + searchTerm: filters.searchTerm, + statusFilter: filters.statusFilter, + priorityFilter: filters.priorityFilter, + 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.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(() => { - setCurrentPage(1); + filters.setCurrentPage(1); fetchRequests(1); - }, filters.searchTerm ? 500 : 0); + + prevFiltersRef.current = { + searchTerm: filters.searchTerm, + statusFilter: filters.statusFilter, + priorityFilter: filters.priorityFilter, + 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 @@ -271,16 +326,15 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { 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); + filters.setCurrentPage(newPage); fetchRequests(newPage); } - }, [totalPages, fetchRequests]); + }, [totalPages, fetchRequests, filters]); // Transform requests const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]); @@ -688,7 +742,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { {/* Pagination */} ( - searchParams.get('approverType') === 'any' ? 'any' : 'current' - ); - const [dateRange, setDateRange] = useState((searchParams.get('dateRange') as DateRange) || 'all'); - const [customStartDate, setCustomStartDate] = useState( - searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined - ); - const [customEndDate, setCustomEndDate] = useState( - searchParams.get('endDate') ? new Date(searchParams.get('endDate')!) : undefined - ); - const [showCustomDatePicker, setShowCustomDatePicker] = useState(false); - - // Update URL params when filters change - useEffect(() => { - const params = new URLSearchParams(); - if (searchTerm) params.set('search', searchTerm); - if (statusFilter !== 'all') params.set('status', statusFilter); - if (priorityFilter !== 'all') params.set('priority', priorityFilter); - if (slaComplianceFilter !== 'all') params.set('slaCompliance', slaComplianceFilter); - if (departmentFilter !== 'all') params.set('department', departmentFilter); - if (initiatorFilter !== 'all') { - params.set('initiator', initiatorFilter); - } - if (approverFilter !== 'all') { - params.set('approver', approverFilter); - params.set('approverType', approverFilterType); - } - if (dateRange) params.set('dateRange', dateRange); - if (customStartDate) params.set('startDate', customStartDate.toISOString()); - if (customEndDate) params.set('endDate', customEndDate.toISOString()); - - setSearchParams(params, { replace: true }); - }, [ + // Get all filter state from Redux + const { searchTerm, statusFilter, priorityFilter, @@ -61,8 +40,24 @@ export function useRequestsFilters() { dateRange, customStartDate, customEndDate, - setSearchParams - ]); + showCustomDatePicker, + currentPage, + } = useAppSelector((state) => state.requests); + + // Create setters that dispatch Redux actions + const setSearchTerm = useCallback((value: string) => dispatch(setSearchTermAction(value)), [dispatch]); + const setStatusFilter = useCallback((value: string) => dispatch(setStatusFilterAction(value)), [dispatch]); + const setPriorityFilter = useCallback((value: string) => dispatch(setPriorityFilterAction(value)), [dispatch]); + const setSlaComplianceFilter = useCallback((value: string) => dispatch(setSlaComplianceFilterAction(value)), [dispatch]); + const setDepartmentFilter = useCallback((value: string) => dispatch(setDepartmentFilterAction(value)), [dispatch]); + const setInitiatorFilter = useCallback((value: string) => dispatch(setInitiatorFilterAction(value)), [dispatch]); + const setApproverFilter = useCallback((value: string) => dispatch(setApproverFilterAction(value)), [dispatch]); + const setApproverFilterType = useCallback((value: 'current' | 'any') => dispatch(setApproverFilterTypeAction(value)), [dispatch]); + const setDateRange = useCallback((value: DateRange) => dispatch(setDateRangeAction(value)), [dispatch]); + const setCustomStartDate = useCallback((value: Date | undefined) => dispatch(setCustomStartDateAction(value)), [dispatch]); + const setCustomEndDate = useCallback((value: Date | undefined) => dispatch(setCustomEndDateAction(value)), [dispatch]); + const setShowCustomDatePicker = useCallback((value: boolean) => dispatch(setShowCustomDatePickerAction(value)), [dispatch]); + const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]); const getFilters = useCallback((): RequestFilters => { return { @@ -93,42 +88,31 @@ export function useRequestsFilters() { ]); const clearFilters = useCallback(() => { - setSearchTerm(''); - setStatusFilter('all'); - setPriorityFilter('all'); - setSlaComplianceFilter('all'); - setDepartmentFilter('all'); - setInitiatorFilter('all'); - setApproverFilter('all'); - setApproverFilterType('current'); - setDateRange('all'); - setCustomStartDate(undefined); - setCustomEndDate(undefined); - setShowCustomDatePicker(false); - }, []); + dispatch(clearFiltersAction()); + }, [dispatch]); const handleDateRangeChange = useCallback((value: string) => { const newRange = value as DateRange; - setDateRange(newRange); + dispatch(setDateRangeAction(newRange)); if (newRange !== 'custom') { - setCustomStartDate(undefined); - setCustomEndDate(undefined); - setShowCustomDatePicker(false); + dispatch(setCustomStartDateAction(undefined)); + dispatch(setCustomEndDateAction(undefined)); + dispatch(setShowCustomDatePickerAction(false)); } else { - setShowCustomDatePicker(true); + dispatch(setShowCustomDatePickerAction(true)); } - }, []); + }, [dispatch]); const handleApplyCustomDate = useCallback(() => { if (customStartDate && customEndDate) { if (customStartDate > customEndDate) { - const temp = customStartDate; - setCustomStartDate(customEndDate); - setCustomEndDate(temp); + // Swap dates if start is after end + dispatch(setCustomStartDateAction(customEndDate)); + dispatch(setCustomEndDateAction(customStartDate)); } - setShowCustomDatePicker(false); + dispatch(setShowCustomDatePickerAction(false)); } - }, [customStartDate, customEndDate]); + }, [customStartDate, customEndDate, dispatch]); const hasActiveFilters: boolean = !!( searchTerm || @@ -157,6 +141,7 @@ export function useRequestsFilters() { customStartDate, customEndDate, showCustomDatePicker, + currentPage, hasActiveFilters, // Setters setSearchTerm, @@ -171,6 +156,7 @@ export function useRequestsFilters() { setCustomStartDate, setCustomEndDate, setShowCustomDatePicker, + setCurrentPage, // Helpers getFilters, clearFilters, diff --git a/src/pages/Requests/redux/requestsSlice.ts b/src/pages/Requests/redux/requestsSlice.ts new file mode 100644 index 0000000..e3d2c44 --- /dev/null +++ b/src/pages/Requests/redux/requestsSlice.ts @@ -0,0 +1,116 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { DateRange } from '@/services/dashboard.service'; + +export interface RequestsFiltersState { + searchTerm: string; + statusFilter: string; + priorityFilter: string; + slaComplianceFilter: string; + departmentFilter: string; + initiatorFilter: string; + approverFilter: string; + approverFilterType: 'current' | 'any'; + dateRange: DateRange; + customStartDate?: Date; + customEndDate?: Date; + showCustomDatePicker: boolean; + currentPage: number; +} + +const initialState: RequestsFiltersState = { + searchTerm: '', + statusFilter: 'all', + priorityFilter: 'all', + slaComplianceFilter: 'all', + departmentFilter: 'all', + initiatorFilter: 'all', + approverFilter: 'all', + approverFilterType: 'current', + dateRange: 'all', + customStartDate: undefined, + customEndDate: undefined, + showCustomDatePicker: false, + currentPage: 1, +}; + +const requestsSlice = createSlice({ + name: 'requests', + initialState, + reducers: { + setSearchTerm: (state, action: PayloadAction) => { + state.searchTerm = action.payload; + }, + setStatusFilter: (state, action: PayloadAction) => { + state.statusFilter = action.payload; + }, + setPriorityFilter: (state, action: PayloadAction) => { + state.priorityFilter = action.payload; + }, + setSlaComplianceFilter: (state, action: PayloadAction) => { + state.slaComplianceFilter = action.payload; + }, + setDepartmentFilter: (state, action: PayloadAction) => { + state.departmentFilter = action.payload; + }, + setInitiatorFilter: (state, action: PayloadAction) => { + state.initiatorFilter = action.payload; + }, + setApproverFilter: (state, action: PayloadAction) => { + state.approverFilter = action.payload; + }, + setApproverFilterType: (state, action: PayloadAction<'current' | 'any'>) => { + state.approverFilterType = action.payload; + }, + setDateRange: (state, action: PayloadAction) => { + state.dateRange = action.payload; + }, + setCustomStartDate: (state, action: PayloadAction) => { + state.customStartDate = action.payload; + }, + setCustomEndDate: (state, action: PayloadAction) => { + state.customEndDate = action.payload; + }, + setShowCustomDatePicker: (state, action: PayloadAction) => { + state.showCustomDatePicker = action.payload; + }, + setCurrentPage: (state, action: PayloadAction) => { + state.currentPage = action.payload; + }, + clearFilters: (state) => { + state.searchTerm = ''; + state.statusFilter = 'all'; + state.priorityFilter = 'all'; + state.slaComplianceFilter = 'all'; + state.departmentFilter = 'all'; + state.initiatorFilter = 'all'; + state.approverFilter = 'all'; + state.approverFilterType = 'current'; + state.dateRange = 'all'; + state.customStartDate = undefined; + state.customEndDate = undefined; + state.showCustomDatePicker = false; + state.currentPage = 1; + }, + }, +}); + +export const { + setSearchTerm, + setStatusFilter, + setPriorityFilter, + setSlaComplianceFilter, + setDepartmentFilter, + setInitiatorFilter, + setApproverFilter, + setApproverFilterType, + setDateRange, + setCustomStartDate, + setCustomEndDate, + setShowCustomDatePicker, + setCurrentPage, + clearFilters, +} = requestsSlice.actions; + +export default requestsSlice; + + diff --git a/src/redux/store.ts b/src/redux/store.ts index 92d8f5c..824c968 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,11 +1,19 @@ import { configureStore } from '@reduxjs/toolkit'; import authSlice from './slices/authSlice'; import dashboardSlice from '../pages/Dashboard/redux/dashboardSlice'; +import requestsSlice from '../pages/Requests/redux/requestsSlice'; +import myRequestsSlice from '../pages/MyRequests/redux/myRequestsSlice'; +import openRequestsSlice from '../pages/OpenRequests/redux/openRequestsSlice'; +import closedRequestsSlice from '../pages/ClosedRequests/redux/closedRequestsSlice'; export const store = configureStore({ reducer: { auth: authSlice.reducer, dashboard: dashboardSlice.reducer, + requests: requestsSlice.reducer, + myRequests: myRequestsSlice.reducer, + openRequests: openRequestsSlice.reducer, + closedRequests: closedRequestsSlice.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts index 78ba9a3..239fb15 100644 --- a/src/services/dashboard.service.ts +++ b/src/services/dashboard.service.ts @@ -667,7 +667,7 @@ class DashboardService { slaCompliance?: string, search?: string ): Promise<{ - requests: any[], + requests: any[], pagination: { currentPage: number, totalPages: number,