= 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({
}`}>
@@ -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({
-
+
- All Statuses
-
+ All Closures
+
-
- Closed
+
+ Closed After Approval
- Rejected
+ Closed After Rejection
diff --git a/src/pages/ClosedRequests/hooks/useClosedRequests.ts b/src/pages/ClosedRequests/hooks/useClosedRequests.ts
index 44e3b60..2e0b76b 100644
--- a/src/pages/ClosedRequests/hooks/useClosedRequests.ts
+++ b/src/pages/ClosedRequests/hooks/useClosedRequests.ts
@@ -2,7 +2,7 @@
* Hook for fetching and managing Closed Requests data
*/
-import { useState, useCallback, useEffect, useRef } from 'react';
+import { useState, useCallback } from 'react';
import workflowApi from '@/services/workflowApi';
import { ClosedRequest, PaginationState } from '../types/closedRequests.types';
import { transformClosedRequests } from '../utils/requestTransformers';
@@ -18,7 +18,7 @@ interface UseClosedRequestsOptions {
};
}
-export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClosedRequestsOptions = {}) {
+export function useClosedRequests({ itemsPerPage = 10 }: UseClosedRequestsOptions = {}) {
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
@@ -28,7 +28,6 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
totalRecords: 0,
itemsPerPage,
});
- const isInitialMount = useRef(true);
const fetchRequests = useCallback(
async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => {
@@ -74,17 +73,9 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
: [];
const mapped = transformClosedRequests(data);
- // Filter out approved requests - only show rejected and closed
- const filtered = mapped.filter(request =>
- request.status === 'rejected' || request.status === 'closed'
- );
- setRequests(filtered);
+ setRequests(mapped); // No client-side filtering - backend returns only CLOSED requests
- // Set pagination data
- // Note: Since we're filtering out approved requests client-side,
- // the pagination count from backend may include approved requests.
- // We'll use the filtered count for this page, but total records
- // from backend is approximate.
+ // Set pagination data from backend
const paginationData = (result as any)?.pagination;
if (paginationData) {
// If we filtered out some requests on this page, we need to adjust pagination
@@ -108,14 +99,8 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
[itemsPerPage]
);
- // Initial fetch on mount
- useEffect(() => {
- if (isInitialMount.current) {
- fetchRequests(1, initialFilters);
- isInitialMount.current = false;
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []); // Only run on mount
+ // Initial fetch removed - component handles initial fetch using Redux stored page
+ // This prevents duplicate fetches and allows page persistence
const handleRefresh = useCallback((filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => {
setRefreshing(true);
diff --git a/src/pages/ClosedRequests/hooks/useClosedRequestsFilters.ts b/src/pages/ClosedRequests/hooks/useClosedRequestsFilters.ts
index c37e517..3d3885a 100644
--- a/src/pages/ClosedRequests/hooks/useClosedRequestsFilters.ts
+++ b/src/pages/ClosedRequests/hooks/useClosedRequestsFilters.ts
@@ -1,9 +1,19 @@
/**
- * Hook for managing Closed Requests filters and sorting
+ * Hook for managing Closed Requests filters and sorting using Redux
*/
-import { useState, useCallback, useEffect, useRef } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
+import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import { ClosedRequestsFilters } from '../types/closedRequests.types';
+import {
+ setSearchTerm as setSearchTermAction,
+ setStatusFilter as setStatusFilterAction,
+ setPriorityFilter as setPriorityFilterAction,
+ setSortBy as setSortByAction,
+ setSortOrder as setSortOrderAction,
+ setCurrentPage as setCurrentPageAction,
+ clearFilters as clearFiltersAction,
+} from '../redux/closedRequestsSlice';
interface UseClosedRequestsFiltersOptions {
onFiltersChange?: (filters: ClosedRequestsFilters) => void;
@@ -11,13 +21,20 @@ interface UseClosedRequestsFiltersOptions {
}
export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseClosedRequestsFiltersOptions = {}) {
- const [searchTerm, setSearchTerm] = useState('');
- const [priorityFilter, setPriorityFilter] = useState('all');
- const [statusFilter, setStatusFilter] = useState('all');
- const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority'>('due');
- const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
+ const dispatch = useAppDispatch();
const debounceTimeoutRef = useRef(null);
const isInitialMount = useRef(true);
+
+ // Get filters from Redux
+ const { searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, currentPage } = useAppSelector((state) => state.closedRequests);
+
+ // 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 setSortBy = useCallback((value: 'created' | 'due' | 'priority') => dispatch(setSortByAction(value)), [dispatch]);
+ const setSortOrder = useCallback((value: 'asc' | 'desc') => dispatch(setSortOrderAction(value)), [dispatch]);
+ const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
const getFilters = useCallback((): ClosedRequestsFilters => {
return {
@@ -57,10 +74,8 @@ export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }:
}, [searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, onFiltersChange, getFilters, debounceMs]);
const clearFilters = useCallback(() => {
- setSearchTerm('');
- setPriorityFilter('all');
- setStatusFilter('all');
- }, []);
+ dispatch(clearFiltersAction());
+ }, [dispatch]);
const activeFiltersCount = [
searchTerm,
@@ -74,11 +89,13 @@ export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }:
statusFilter,
sortBy,
sortOrder,
+ currentPage,
setSearchTerm,
setPriorityFilter,
setStatusFilter,
setSortBy,
setSortOrder,
+ setCurrentPage,
clearFilters,
activeFiltersCount,
getFilters,
diff --git a/src/pages/ClosedRequests/redux/closedRequestsSlice.ts b/src/pages/ClosedRequests/redux/closedRequestsSlice.ts
new file mode 100644
index 0000000..ac6be67
--- /dev/null
+++ b/src/pages/ClosedRequests/redux/closedRequestsSlice.ts
@@ -0,0 +1,63 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export interface ClosedRequestsFiltersState {
+ searchTerm: string;
+ statusFilter: string;
+ priorityFilter: string;
+ sortBy: 'created' | 'due' | 'priority';
+ sortOrder: 'asc' | 'desc';
+ currentPage: number;
+}
+
+const initialState: ClosedRequestsFiltersState = {
+ searchTerm: '',
+ statusFilter: 'all',
+ priorityFilter: 'all',
+ sortBy: 'created',
+ sortOrder: 'desc',
+ currentPage: 1,
+};
+
+const closedRequestsSlice = createSlice({
+ name: 'closedRequests',
+ 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;
+ },
+ setSortBy: (state, action: PayloadAction<'created' | 'due' | 'priority'>) => {
+ state.sortBy = action.payload;
+ },
+ setSortOrder: (state, action: PayloadAction<'asc' | 'desc'>) => {
+ state.sortOrder = action.payload;
+ },
+ setCurrentPage: (state, action: PayloadAction) => {
+ state.currentPage = action.payload;
+ },
+ clearFilters: (state) => {
+ state.searchTerm = '';
+ state.statusFilter = 'all';
+ state.priorityFilter = 'all';
+ state.currentPage = 1;
+ },
+ },
+});
+
+export const {
+ setSearchTerm,
+ setStatusFilter,
+ setPriorityFilter,
+ setSortBy,
+ setSortOrder,
+ setCurrentPage,
+ clearFilters,
+} = closedRequestsSlice.actions;
+
+export default closedRequestsSlice;
+
diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx
index ea469f2..45fdb00 100644
--- a/src/pages/MyRequests/MyRequests.tsx
+++ b/src/pages/MyRequests/MyRequests.tsx
@@ -17,9 +17,6 @@ import { useMyRequestsFilters } from './hooks/useMyRequestsFilters';
// Utils
import { transformRequests } from './utils/requestTransformers';
-// Types
-import type { MyRequestsFilters } from './types/myRequests.types';
-
interface MyRequestsProps {
onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void;
dynamicRequests?: any[];
@@ -35,19 +32,58 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
const fetchRef = useRef(myRequests.fetchMyRequests);
fetchRef.current = myRequests.fetchMyRequests;
- const filters = useMyRequestsFilters({
- onFiltersChange: useCallback(
- (filters: MyRequestsFilters) => {
- // 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 = useMyRequestsFilters();
+ const prevFiltersRef = useRef({
+ searchTerm: filters.searchTerm,
+ statusFilter: filters.statusFilter,
+ priorityFilter: filters.priorityFilter,
});
+ 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,
+ });
+ 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;
+
+ 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,
+ });
+
+ // Update previous values
+ prevFiltersRef.current = {
+ searchTerm: filters.searchTerm,
+ statusFilter: filters.statusFilter,
+ priorityFilter: filters.priorityFilter,
+ };
+ }, filters.searchTerm !== prev.searchTerm ? 500 : 0);
+
+ return () => clearTimeout(timeoutId);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter]);
// State for backend stats (calculated from entire dataset via SQL queries)
const [backendStats, setBackendStats] = useState<{
@@ -155,6 +191,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
const handlePageChange = useCallback(
(newPage: number) => {
if (newPage >= 1 && newPage <= myRequests.pagination.totalPages) {
+ filters.setCurrentPage(newPage); // Update page in Redux
myRequests.fetchMyRequests(newPage, {
search: filters.searchTerm || undefined,
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
@@ -210,7 +247,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
{/* Pagination */}
([]);
const [loading, setLoading] = useState(false);
const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false);
@@ -26,7 +26,6 @@ export function useMyRequests({ itemsPerPage = 10, initialFilters }: UseMyReques
totalRecords: 0,
itemsPerPage,
});
- const isInitialMount = useRef(true);
const fetchMyRequests = useCallback(
async (page: number = 1, filters?: { search?: string; status?: string; priority?: string }) => {
@@ -72,14 +71,8 @@ export function useMyRequests({ itemsPerPage = 10, initialFilters }: UseMyReques
[itemsPerPage]
);
- // Initial fetch on mount
- useEffect(() => {
- if (isInitialMount.current) {
- fetchMyRequests(1, initialFilters);
- isInitialMount.current = false;
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []); // Only run on mount
+ // Initial fetch removed - component handles initial fetch using Redux stored page
+ // This prevents duplicate fetches and allows page persistence
return {
requests,
diff --git a/src/pages/MyRequests/hooks/useMyRequestsFilters.ts b/src/pages/MyRequests/hooks/useMyRequestsFilters.ts
index 009bdea..e66d09b 100644
--- a/src/pages/MyRequests/hooks/useMyRequestsFilters.ts
+++ b/src/pages/MyRequests/hooks/useMyRequestsFilters.ts
@@ -1,9 +1,17 @@
/**
- * Hook for managing My Requests filters and search
+ * Hook for managing My Requests filters and search using Redux
*/
-import { useState, useCallback, useEffect, useRef } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
+import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import { MyRequestsFilters } from '../types/myRequests.types';
+import {
+ setSearchTerm as setSearchTermAction,
+ setStatusFilter as setStatusFilterAction,
+ setPriorityFilter as setPriorityFilterAction,
+ setCurrentPage as setCurrentPageAction,
+ clearFilters as clearFiltersAction,
+} from '../redux/myRequestsSlice';
interface UseMyRequestsFiltersOptions {
onFiltersChange?: (filters: MyRequestsFilters) => void;
@@ -11,10 +19,18 @@ interface UseMyRequestsFiltersOptions {
}
export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseMyRequestsFiltersOptions = {}) {
- const [searchTerm, setSearchTerm] = useState('');
- const [statusFilter, setStatusFilter] = useState('all');
- const [priorityFilter, setPriorityFilter] = useState('all');
+ const dispatch = useAppDispatch();
const debounceTimeoutRef = useRef(null);
+ const isInitialMount = useRef(true);
+
+ // Get filters from Redux
+ const { searchTerm, statusFilter, priorityFilter, currentPage } = useAppSelector((state) => state.myRequests);
+
+ // 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 setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
const getFilters = useCallback((): MyRequestsFilters => {
return {
@@ -26,6 +42,12 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
// Debounced filter change handler
useEffect(() => {
+ // Skip initial mount - let component handle initial fetch
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ return;
+ }
+
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
@@ -46,18 +68,18 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
}, [searchTerm, statusFilter, priorityFilter, onFiltersChange, getFilters, debounceMs]);
const resetFilters = useCallback(() => {
- setSearchTerm('');
- setStatusFilter('all');
- setPriorityFilter('all');
- }, []);
+ dispatch(clearFiltersAction());
+ }, [dispatch]);
return {
searchTerm,
statusFilter,
priorityFilter,
+ currentPage,
setSearchTerm,
setStatusFilter,
setPriorityFilter,
+ setCurrentPage,
getFilters,
resetFilters,
};
diff --git a/src/pages/MyRequests/redux/myRequestsSlice.ts b/src/pages/MyRequests/redux/myRequestsSlice.ts
new file mode 100644
index 0000000..a88dffe
--- /dev/null
+++ b/src/pages/MyRequests/redux/myRequestsSlice.ts
@@ -0,0 +1,52 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export interface MyRequestsFiltersState {
+ searchTerm: string;
+ statusFilter: string;
+ priorityFilter: string;
+ currentPage: number;
+}
+
+const initialState: MyRequestsFiltersState = {
+ searchTerm: '',
+ statusFilter: 'all',
+ priorityFilter: 'all',
+ currentPage: 1,
+};
+
+const myRequestsSlice = createSlice({
+ name: 'myRequests',
+ 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;
+ state.currentPage = 1; // Reset to page 1 when filter changes
+ },
+ setCurrentPage: (state, action: PayloadAction) => {
+ state.currentPage = action.payload;
+ },
+ clearFilters: (state) => {
+ state.searchTerm = '';
+ state.statusFilter = 'all';
+ state.priorityFilter = 'all';
+ state.currentPage = 1;
+ },
+ },
+});
+
+export const {
+ setSearchTerm,
+ setStatusFilter,
+ setPriorityFilter,
+ setCurrentPage,
+ clearFilters,
+} = myRequestsSlice.actions;
+
+export default myRequestsSlice;
+
diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx
index ce577ac..aee3655 100644
--- a/src/pages/OpenRequests/OpenRequests.tsx
+++ b/src/pages/OpenRequests/OpenRequests.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback, useRef } from 'react';
-import { useSearchParams } from 'react-router-dom';
+import { useOpenRequestsFilters } from './hooks/useOpenRequestsFilters';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -7,14 +7,14 @@ import { Input } from '@/components/ui/input';
import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
-import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, X, CheckCircle, XCircle } from 'lucide-react';
+import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, X, CheckCircle, XCircle, Lock } from 'lucide-react';
import workflowApi from '@/services/workflowApi';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface Request {
id: string;
title: string;
description: string;
- status: 'pending' | 'approved' | 'rejected' | 'closed';
+ status: 'pending' | 'approved' | 'rejected' | 'closed' | 'paused';
priority: 'express' | 'standard';
initiator: { name: string; avatar: string };
currentApprover?: {
@@ -26,6 +26,8 @@ interface Request {
approvalStep?: string;
department?: string;
currentLevelSLA?: any; // Backend-provided SLA for current level
+ isPaused?: boolean; // Pause status
+ pauseInfo?: any; // Pause details
}
interface OpenRequestsProps {
@@ -99,27 +101,18 @@ const getStatusConfig = (status: string) => {
// getSLAUrgency removed - now using SLATracker component for real-time SLA display
export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- const [searchParams] = useSearchParams();
-
- // Initialize filters from URL params
- const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || '');
- const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all');
- const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all');
- const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>(
- (searchParams.get('sortBy') as 'created' | 'due' | 'priority' | 'sla') || 'created'
- );
- const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(
- (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc'
- );
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
- // Pagination states
- const [currentPage, setCurrentPage] = useState(1);
+ // Pagination states (currentPage now in Redux)
const [totalPages, setTotalPages] = useState(1);
const [totalRecords, setTotalRecords] = useState(0);
const [itemsPerPage] = useState(10);
+ const fetchRequestsRef = useRef(null);
+
+ // Use Redux for filters with callback (persists during navigation)
+ const filters = useOpenRequestsFilters();
// Fetch open requests for the current user only (user-scoped, not organization-wide)
// Note: This endpoint returns only requests where the user is:
@@ -128,7 +121,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
// - An initiator (for approved requests awaiting closure)
// This applies to ALL users including regular users, MANAGEMENT, and ADMIN roles
// For organization-wide view, users should use the "All Requests" screen (/requests)
- const fetchRequests = useCallback(async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => {
+ const fetchRequests = useCallback(async (page: number = 1, filterParams?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => {
try {
if (page === 1) {
setLoading(true);
@@ -141,11 +134,11 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const result = await workflowApi.listOpenForMe({
page,
limit: itemsPerPage,
- search: filters?.search,
- status: filters?.status,
- priority: filters?.priority,
- sortBy: filters?.sortBy,
- sortOrder: filters?.sortOrder
+ search: filterParams?.search,
+ status: filterParams?.status,
+ priority: filterParams?.priority,
+ sortBy: filterParams?.sortBy,
+ sortOrder: filterParams?.sortOrder
});
// Extract data - workflowApi now returns { data: [], pagination: {} }
@@ -156,7 +149,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
// Set pagination data
const pagination = (result as any)?.pagination;
if (pagination) {
- setCurrentPage(pagination.page || 1);
+ filters.setCurrentPage(pagination.page || 1);
setTotalPages(pagination.totalPages || 1);
setTotalRecords(pagination.total || 0);
}
@@ -192,28 +185,30 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
setLoading(false);
setRefreshing(false);
}
- }, [itemsPerPage]);
+ }, [itemsPerPage, filters]);
+
+ fetchRequestsRef.current = fetchRequests;
const handleRefresh = () => {
setRefreshing(true);
- fetchRequests(currentPage, {
- search: searchTerm || undefined,
- status: statusFilter !== 'all' ? statusFilter : undefined,
- priority: priorityFilter !== 'all' ? priorityFilter : undefined,
- sortBy,
- sortOrder
+ fetchRequests(filters.currentPage, {
+ search: filters.searchTerm || undefined,
+ status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
+ priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
+ sortBy: filters.sortBy,
+ sortOrder: filters.sortOrder
});
};
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
- setCurrentPage(newPage);
+ filters.setCurrentPage(newPage);
fetchRequests(newPage, {
- search: searchTerm || undefined,
- status: statusFilter !== 'all' ? statusFilter : undefined,
- priority: priorityFilter !== 'all' ? priorityFilter : undefined,
- sortBy,
- sortOrder
+ search: filters.searchTerm || undefined,
+ status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
+ priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
+ sortBy: filters.sortBy,
+ sortOrder: filters.sortOrder
});
}
};
@@ -221,7 +216,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const getPageNumbers = () => {
const pages = [];
const maxPagesToShow = 5;
- let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
+ let startPage = Math.max(1, filters.currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
if (endPage - startPage < maxPagesToShow - 1) {
@@ -235,59 +230,50 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
return pages;
};
- // Track if this is the initial mount
- const isInitialMount = useRef(true);
+ // Track if this is initial mount
+ const hasInitialFetchRun = useRef(false);
- // Initial fetch on mount with URL params
+ // Initial fetch on mount - use stored page from Redux
useEffect(() => {
- if (isInitialMount.current) {
- isInitialMount.current = false;
- fetchRequests(1, {
- search: searchTerm || undefined,
- status: statusFilter !== 'all' ? statusFilter : undefined,
- priority: priorityFilter !== 'all' ? priorityFilter : undefined,
- sortBy,
- sortOrder
+ if (!hasInitialFetchRun.current) {
+ hasInitialFetchRun.current = true;
+ const storedPage = filters.currentPage || 1;
+ fetchRequests(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,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []); // Only run on mount to use URL params
+ }, []); // Only on mount
- // Fetch when filters or sorting change (with debouncing for search)
+ // Track filter changes and refetch
useEffect(() => {
- // Skip initial mount to avoid double fetch
- if (isInitialMount.current) return;
+ // Skip until initial fetch has completed
+ if (!hasInitialFetchRun.current) return;
- // Debounce search: wait 500ms after user stops typing
+ // Debounce search
const timeoutId = setTimeout(() => {
- setCurrentPage(1); // Reset to page 1 when filters change
+ filters.setCurrentPage(1); // Reset to page 1 when filters change
fetchRequests(1, {
- search: searchTerm || undefined,
- status: statusFilter !== 'all' ? statusFilter : undefined,
- priority: priorityFilter !== 'all' ? priorityFilter : undefined,
- sortBy,
- sortOrder
+ search: filters.searchTerm || undefined,
+ status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
+ priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
+ sortBy: filters.sortBy,
+ sortOrder: filters.sortOrder,
});
- }, searchTerm ? 500 : 0); // Debounce only for search, instant for dropdowns
+ }, filters.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId);
- }, [searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, fetchRequests]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.sortBy, filters.sortOrder]);
// Backend handles both filtering and sorting - use items directly
// No client-side sorting needed anymore
const filteredAndSortedRequests = items;
- const clearFilters = () => {
- setSearchTerm('');
- setPriorityFilter('all');
- setStatusFilter('all');
- };
-
- const activeFiltersCount = [
- searchTerm,
- priorityFilter !== 'all' ? priorityFilter : null,
- statusFilter !== 'all' ? statusFilter : null
- ].filter(Boolean).length;
return (
@@ -334,19 +320,19 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
Filters & Search
- {activeFiltersCount > 0 && (
+ {filters.activeFiltersCount > 0 && (
- {activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active
+ {filters.activeFiltersCount} filter{filters.activeFiltersCount > 1 ? 's' : ''} active
)}
- {activeFiltersCount > 0 && (
+ {filters.activeFiltersCount > 0 && (
@@ -362,13 +348,13 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
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"
/>