i have made filter date consistent even after navigation

This commit is contained in:
laxmanhalaki 2025-12-04 21:17:01 +05:30
parent 7358c3ff30
commit a4abc2ab58
27 changed files with 988 additions and 321 deletions

View File

@ -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() {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="Type name or email address..."
placeholder="Type @ to search users..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-10 border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20"
@ -407,7 +410,7 @@ export function UserManagement() {
)}
</div>
</div>
<p className="text-xs text-muted-foreground">Start typing to search across all Okta users</p>
<p className="text-xs text-muted-foreground">Start with @ to search users (e.g., @john)</p>
{/* Search Results Dropdown */}
{searchResults.length > 0 && (

View File

@ -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() {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
<Input
type="text"
placeholder="Type name or email address..."
placeholder="Type @ to search users..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-10 pr-10 border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20"
@ -452,7 +455,7 @@ export function UserRoleManager() {
<Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-re-green animate-spin" />
)}
</div>
<p className="text-xs text-gray-500">Start typing to search across all Okta users</p>
<p className="text-xs text-gray-500">Start with @ to search users (e.g., @john)</p>
{/* Search Results Dropdown */}
{searchResults.length > 0 && (

View File

@ -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({
<div data-testid={testId}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-blue-600" />
<span className="text-sm font-semibold text-gray-900">SLA Progress</span>
{isPaused ? (
<Lock className={`h-4 w-4 ${colors.icon}`} />
) : (
<Clock className={`h-4 w-4 ${colors.icon}`} />
)}
<span className="text-sm font-semibold text-gray-900">
{isPaused ? 'SLA Progress (Paused)' : 'SLA Progress'}
</span>
</div>
<Badge
className={`text-xs ${colors.badge}`}
data-testid={`${testId}-badge`}
>
{sla.percentageUsed || 0}% elapsed
{sla.percentageUsed || 0}% elapsed {isPaused && '(frozen)'}
</Badge>
</div>

View File

@ -369,6 +369,7 @@ export function ApprovalStepCard({
{/* Current Approver - Time Tracking */}
<div className={`border rounded-lg p-3 ${
isPaused ? 'bg-gray-100 border-gray-300' :
(approval.sla.percentageUsed || 0) >= 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({
}`}>
<p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
Current Approver - Time Tracking
Current Approver - Time Tracking {isPaused && '(Paused)'}
</p>
<div className="space-y-2 text-xs mb-3">
@ -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';

View File

@ -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

View File

@ -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);

View File

@ -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,

View File

@ -106,20 +106,20 @@ export function ClosedRequestsFilters({
<Select value={statusFilter} onValueChange={onStatusChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-status-filter">
<SelectValue placeholder="All Statuses" />
<SelectValue placeholder="Closure Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="closed">
<SelectItem value="all">All Closures</SelectItem>
<SelectItem value="approved">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-gray-600" />
<span>Closed</span>
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Closed After Approval</span>
</div>
</SelectItem>
<SelectItem value="rejected">
<div className="flex items-center gap-2">
<XCircle className="w-4 h-4 text-red-600" />
<span>Rejected</span>
<span>Closed After Rejection</span>
</div>
</SelectItem>
</SelectContent>

View File

@ -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<ClosedRequest[]>([]);
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);

View File

@ -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,14 +21,21 @@ 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<NodeJS.Timeout | null>(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 {
search: searchTerm,
@ -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,

View File

@ -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<string>) => {
state.searchTerm = action.payload;
},
setStatusFilter: (state, action: PayloadAction<string>) => {
state.statusFilter = action.payload;
},
setPriorityFilter: (state, action: PayloadAction<string>) => {
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<number>) => {
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;

View File

@ -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,20 +32,59 @@ 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
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.search || undefined,
status: filters.status !== 'all' ? filters.status : undefined,
priority: filters.priority !== 'all' ? filters.priority : undefined,
});
},
[]
),
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<{
total: number;
@ -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 */}
<Pagination
currentPage={myRequests.pagination.currentPage}
currentPage={filters.currentPage || myRequests.pagination.currentPage}
totalPages={myRequests.pagination.totalPages}
totalRecords={myRequests.pagination.totalRecords}
itemsPerPage={myRequests.pagination.itemsPerPage}

View File

@ -2,7 +2,7 @@
* Hook for fetching and managing My Requests data
*/
import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback } from 'react';
import workflowApi from '@/services/workflowApi';
import { MyRequest, PaginationState } from '../types/myRequests.types';
import { transformRequests } from '../utils/requestTransformers';
@ -16,7 +16,7 @@ interface UseMyRequestsOptions {
};
}
export function useMyRequests({ itemsPerPage = 10, initialFilters }: UseMyRequestsOptions = {}) {
export function useMyRequests({ itemsPerPage = 10 }: UseMyRequestsOptions = {}) {
const [requests, setRequests] = useState<MyRequest[]>([]);
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,

View File

@ -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<NodeJS.Timeout | null>(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,
};

View File

@ -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<string>) => {
state.searchTerm = action.payload;
},
setStatusFilter: (state, action: PayloadAction<string>) => {
state.statusFilter = action.payload;
},
setPriorityFilter: (state, action: PayloadAction<string>) => {
state.priorityFilter = action.payload;
state.currentPage = 1; // Reset to page 1 when filter changes
},
setCurrentPage: (state, action: PayloadAction<number>) => {
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;

View File

@ -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<Request[]>([]);
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<any>(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 (
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto">
@ -334,19 +320,19 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<div>
<CardTitle className="text-base sm:text-lg">Filters & Search</CardTitle>
<CardDescription className="text-xs sm:text-sm">
{activeFiltersCount > 0 && (
{filters.activeFiltersCount > 0 && (
<span className="text-blue-600 font-medium">
{activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active
{filters.activeFiltersCount} filter{filters.activeFiltersCount > 1 ? 's' : ''} active
</span>
)}
</CardDescription>
</div>
</div>
{activeFiltersCount > 0 && (
{filters.activeFiltersCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
onClick={filters.clearFilters}
className="text-red-600 hover:bg-red-50 gap-1 h-8 sm:h-9 px-2 sm:px-3"
>
<X className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
@ -362,13 +348,13 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" />
<Input
placeholder="Search requests, IDs..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<Select value={filters.priorityFilter} onValueChange={filters.setPriorityFilter}>
<SelectTrigger className="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">
<SelectValue placeholder="All Priorities" />
</SelectTrigger>
@ -389,7 +375,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<Select value={filters.statusFilter} onValueChange={filters.setStatusFilter}>
<SelectTrigger className="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">
<SelectValue placeholder="All Statuses" />
</SelectTrigger>
@ -401,7 +387,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</Select>
<div className="flex gap-2">
<Select value={sortBy} onValueChange={(value: any) => setSortBy(value)}>
<Select value={filters.sortBy} onValueChange={(value: any) => filters.setSortBy(value)}>
<SelectTrigger className="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">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
@ -416,10 +402,10 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<Button
variant="outline"
size="sm"
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
onClick={() => filters.setSortOrder(filters.sortOrder === 'asc' ? 'desc' : 'asc')}
className="px-2 sm:px-3 h-9 sm:h-10 md:h-11"
>
{sortOrder === 'asc' ? <SortAsc className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> : <SortDesc className="w-3.5 h-3.5 sm:w-4 sm:h-4" />}
{filters.sortOrder === 'asc' ? <SortAsc className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> : <SortDesc className="w-3.5 h-3.5 sm:w-4 sm:h-4" />}
</Button>
</div>
</div>
@ -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) {
<div className={`p-2 rounded-md ${colors.bg}`}>
<div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-gray-600" />
<span className="text-xs font-medium text-gray-900">TAT: {percentUsed}%</span>
{isPaused ? (
<Lock className={`w-3.5 h-3.5 ${colors.icon}`} />
) : (
<Clock className={`w-3.5 h-3.5 ${colors.icon}`} />
)}
<span className="text-xs font-medium text-gray-900">
TAT: {percentUsed}% {isPaused && '(paused)'}
</span>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span>
<span className={`font-semibold ${
percentUsed >= 100 ? 'text-red-600' :
percentUsed >= 75 ? 'text-orange-600' :
percentUsed >= 50 ? 'text-amber-600' :
'text-gray-700'
}`}>
<span className={`font-semibold ${colors.text}`}>
{request.currentLevelSLA.remainingText} left
</span>
</div>
@ -599,16 +607,16 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No requests found</h3>
<p className="text-gray-600 text-center max-w-md">
{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.'
}
</p>
{activeFiltersCount > 0 && (
{filters.activeFiltersCount > 0 && (
<Button
variant="outline"
className="mt-4"
onClick={clearFilters}
onClick={filters.clearFilters}
>
Clear all filters
</Button>
@ -623,21 +631,21 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-xs sm:text-sm text-muted-foreground">
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
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
onClick={() => handlePageChange(filters.currentPage - 1)}
disabled={filters.currentPage === 1}
className="h-8 w-8 p-0"
>
<ArrowRight className="h-4 w-4 rotate-180" />
</Button>
{currentPage > 3 && totalPages > 5 && (
{filters.currentPage > 3 && totalPages > 5 && (
<>
<Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button>
<span className="text-muted-foreground">...</span>
@ -647,16 +655,16 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{getPageNumbers().map((pageNum) => (
<Button
key={pageNum}
variant={pageNum === currentPage ? "default" : "outline"}
variant={pageNum === filters.currentPage ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(pageNum)}
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
className={`h-8 w-8 p-0 ${pageNum === filters.currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
>
{pageNum}
</Button>
))}
{currentPage < totalPages - 2 && totalPages > 5 && (
{filters.currentPage < totalPages - 2 && totalPages > 5 && (
<>
<span className="text-muted-foreground">...</span>
<Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button>
@ -666,8 +674,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
onClick={() => handlePageChange(filters.currentPage + 1)}
disabled={filters.currentPage === totalPages}
className="h-8 w-8 p-0"
>
<ArrowRight className="h-4 w-4" />

View File

@ -0,0 +1,55 @@
/**
* Hook for managing Open Requests filters and sorting using Redux
*/
import { useCallback } from 'react';
import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import {
setSearchTerm as setSearchTermAction,
setStatusFilter as setStatusFilterAction,
setPriorityFilter as setPriorityFilterAction,
setSortBy as setSortByAction,
setSortOrder as setSortOrderAction,
setCurrentPage as setCurrentPageAction,
clearFilters as clearFiltersAction,
} from '../redux/openRequestsSlice';
export function useOpenRequestsFilters() {
const dispatch = useAppDispatch();
// Get filters from Redux
const { searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, currentPage } = useAppSelector((state) => state.openRequests);
// 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' | 'sla') => dispatch(setSortByAction(value)), [dispatch]);
const setSortOrder = useCallback((value: 'asc' | 'desc') => dispatch(setSortOrderAction(value)), [dispatch]);
const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
const clearFilters = useCallback(() => dispatch(clearFiltersAction()), [dispatch]);
const activeFiltersCount = [
searchTerm,
priorityFilter !== 'all' ? priorityFilter : null,
statusFilter !== 'all' ? statusFilter : null
].filter(Boolean).length;
return {
searchTerm,
statusFilter,
priorityFilter,
sortBy,
sortOrder,
currentPage,
setSearchTerm,
setStatusFilter,
setPriorityFilter,
setSortBy,
setSortOrder,
setCurrentPage,
clearFilters,
activeFiltersCount,
};
}

View File

@ -0,0 +1,63 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface OpenRequestsFiltersState {
searchTerm: string;
statusFilter: string;
priorityFilter: string;
sortBy: 'created' | 'due' | 'priority' | 'sla';
sortOrder: 'asc' | 'desc';
currentPage: number;
}
const initialState: OpenRequestsFiltersState = {
searchTerm: '',
statusFilter: 'all',
priorityFilter: 'all',
sortBy: 'created',
sortOrder: 'desc',
currentPage: 1,
};
const openRequestsSlice = createSlice({
name: 'openRequests',
initialState,
reducers: {
setSearchTerm: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
setStatusFilter: (state, action: PayloadAction<string>) => {
state.statusFilter = action.payload;
},
setPriorityFilter: (state, action: PayloadAction<string>) => {
state.priorityFilter = action.payload;
},
setSortBy: (state, action: PayloadAction<'created' | 'due' | 'priority' | 'sla'>) => {
state.sortBy = action.payload;
},
setSortOrder: (state, action: PayloadAction<'asc' | 'desc'>) => {
state.sortOrder = action.payload;
},
setCurrentPage: (state, action: PayloadAction<number>) => {
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,
} = openRequestsSlice.actions;
export default openRequestsSlice;

View File

@ -41,7 +41,7 @@ import { downloadDocument } from '@/services/workflowApi';
// Components
import { RequestDetailHeader } from './components/RequestDetailHeader';
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
import { getSummaryDetails, type SummaryDetails } from '@/services/summaryApi';
import { getSummaryDetails, getSummaryByRequestId, type SummaryDetails } from '@/services/summaryApi';
import { toast } from 'sonner';
import { OverviewTab } from './components/tabs/OverviewTab';
import { WorkflowTab } from './components/tabs/WorkflowTab';
@ -257,7 +257,6 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
setLoadingSummary(true);
// Just fetch the summary by requestId - don't try to create it
// Summary is auto-created by backend on final approval/rejection
const { getSummaryByRequestId } = await import('@/services/summaryApi');
const summary = await getSummaryByRequestId(apiRequest.requestId);
if (summary?.summaryId) {

View File

@ -9,6 +9,7 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle } from 'lucide-react';
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
import { useAuth } from '@/contexts/AuthContext';
import notificationApi, { type Notification } from '@/services/notificationApi';
interface QuickActionsSidebarProps {
request: any;
@ -46,6 +47,7 @@ export function QuickActionsSidebar({
const { user } = useAuth();
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
const [loadingRecipients, setLoadingRecipients] = useState(false);
const [hasRetriggerNotification, setHasRetriggerNotification] = useState(false);
const isClosed = request?.status === 'closed';
const isPaused = request?.pauseInfo?.isPaused || false;
const pausedByUserId = request?.pauseInfo?.pausedBy?.userId;
@ -60,6 +62,38 @@ export function QuickActionsSidebar({
// Retrigger: Only for initiator when approver paused (initiator asks approver to resume)
const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger;
// Check for retrigger notification (initiator requested resume)
// ONLY check when: 1) Request is paused, 2) Current user is an approver
// This avoids unnecessary API calls for non-paused requests or initiators
useEffect(() => {
// Skip check if request is not paused or user is not an approver
if (!isPaused || !currentApprovalLevel || !request?.requestId) {
setHasRetriggerNotification(false);
return;
}
const checkRetriggerNotification = async () => {
try {
const response = await notificationApi.list({ page: 1, limit: 50, unreadOnly: true }); // Only unread
const notifications: Notification[] = response.data?.notifications || [];
// Check if there's an UNREAD pause_retrigger_request notification for this request
const hasRetrigger = notifications.some(
(notif: Notification) =>
notif.requestId === request.requestId &&
notif.notificationType === 'pause_retrigger_request'
);
setHasRetriggerNotification(hasRetrigger);
} catch (error) {
console.error('Failed to check retrigger notifications:', error);
setHasRetriggerNotification(false);
}
};
checkRetriggerNotification();
}, [isPaused, currentApprovalLevel, request?.requestId, refreshTrigger]); // Only when paused state or approver status changes
// Fetch shared recipients when request is closed and summaryId is available
useEffect(() => {
const fetchSharedRecipients = async () => {
@ -180,8 +214,48 @@ export function QuickActionsSidebar({
)}
{isPaused && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 text-center">
{/* Different messages based on who paused, who is viewing, and if retrigger was sent */}
{pausedByUserId === currentUserId ? (
// User viewing is the one who paused
<>
<p className="text-xs text-orange-800 font-medium flex items-center justify-center gap-1.5">
{hasRetriggerNotification && <AlertCircle className="w-3.5 h-3.5" />}
{hasRetriggerNotification ? 'Initiator has requested you to resume' : 'You paused this workflow'}
</p>
<p className="text-xs text-orange-600 mt-1">
{hasRetriggerNotification ? 'Please review and resume if appropriate' : 'Click "Resume Workflow" to continue'}
</p>
</>
) : currentApprovalLevel && pausedByUserId !== currentUserId && hasRetriggerNotification ? (
// Approver viewing, and initiator sent retrigger
<>
<p className="text-xs text-orange-800 font-medium flex items-center justify-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5" />
Initiator has requested resume
</p>
<p className="text-xs text-orange-600 mt-1">Please review and resume if appropriate</p>
</>
) : currentApprovalLevel && pausedByUserId !== currentUserId ? (
// Approver viewing but someone else paused (initiator or another approver)
<>
<p className="text-xs text-orange-800 font-medium">Workflow is paused</p>
<p className="text-xs text-orange-600 mt-1">You can resume to continue approval</p>
</>
) : isInitiator && pausedByUserId && pausedByUserId !== currentUserId ? (
// Initiator viewing, approver paused
<>
<p className="text-xs text-orange-800 font-medium">Approver has paused this workflow</p>
<p className="text-xs text-orange-600 mt-1">
{canRetrigger ? 'Click "Request Resume" to notify approver' : 'Resume request sent - Waiting for approver'}
</p>
</>
) : (
// Default message
<>
<p className="text-xs text-orange-800 font-medium">Workflow is paused</p>
<p className="text-xs text-orange-600 mt-1">Actions are disabled until resumed</p>
</>
)}
</div>
)}
</div>

View File

@ -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 (
<div className="bg-white rounded-lg shadow-sm border border-gray-300 mb-4 sm:mb-6" data-testid="request-detail-header">
@ -109,8 +110,15 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, on
</div>
{/* SLA Progress Section */}
<div className="px-3 sm:px-4 md:px-6 py-3 sm:py-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200" data-testid="sla-section">
<SLAProgressBar sla={request.summary?.sla || request.sla} requestStatus={request.status} testId="request-sla" />
<div className={`px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-gray-200 ${
isPaused ? 'bg-gradient-to-r from-gray-100 to-gray-200' : 'bg-gradient-to-r from-blue-50 to-indigo-50'
}`} data-testid="sla-section">
<SLAProgressBar
sla={request.summary?.sla || request.sla}
requestStatus={request.status}
isPaused={isPaused}
testId="request-sla"
/>
</div>
</div>
);

View File

@ -71,8 +71,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
const [loadingDepartments, setLoadingDepartments] = useState(false);
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
// 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
currentPage={currentPage}
currentPage={filters.currentPage || 1}
totalPages={totalPages}
totalRecords={totalRecords}
itemsPerPage={itemsPerPage}

View File

@ -57,8 +57,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
const [loadingDepartments, setLoadingDepartments] = useState(false);
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
// 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 */}
<Pagination
currentPage={currentPage}
currentPage={filters.currentPage || 1}
totalPages={totalPages}
totalRecords={totalRecords}
itemsPerPage={itemsPerPage}

View File

@ -1,55 +1,34 @@
/**
* Hook for managing Requests filters
* Hook for managing Requests filters using Redux
* Filters persist in Redux (in-memory) during navigation, but reset on page refresh
*/
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useCallback } from 'react';
import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import type { DateRange } from '@/services/dashboard.service';
import type { RequestFilters } from '../types/requests.types';
import {
setSearchTerm as setSearchTermAction,
setStatusFilter as setStatusFilterAction,
setPriorityFilter as setPriorityFilterAction,
setSlaComplianceFilter as setSlaComplianceFilterAction,
setDepartmentFilter as setDepartmentFilterAction,
setInitiatorFilter as setInitiatorFilterAction,
setApproverFilter as setApproverFilterAction,
setApproverFilterType as setApproverFilterTypeAction,
setDateRange as setDateRangeAction,
setCustomStartDate as setCustomStartDateAction,
setCustomEndDate as setCustomEndDateAction,
setShowCustomDatePicker as setShowCustomDatePickerAction,
setCurrentPage as setCurrentPageAction,
clearFilters as clearFiltersAction,
} from '../redux/requestsSlice';
export function useRequestsFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const dispatch = useAppDispatch();
const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || '');
const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all');
const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all');
const [slaComplianceFilter, setSlaComplianceFilter] = useState(searchParams.get('slaCompliance') || 'all');
const [departmentFilter, setDepartmentFilter] = useState(searchParams.get('department') || 'all');
const [initiatorFilter, setInitiatorFilter] = useState(searchParams.get('initiator') || 'all');
const [approverFilter, setApproverFilter] = useState(searchParams.get('approver') || 'all');
const [approverFilterType, setApproverFilterType] = useState<'current' | 'any'>(
searchParams.get('approverType') === 'any' ? 'any' : 'current'
);
const [dateRange, setDateRange] = useState<DateRange>((searchParams.get('dateRange') as DateRange) || 'all');
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined
);
const [customEndDate, setCustomEndDate] = useState<Date | undefined>(
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,

View File

@ -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<string>) => {
state.searchTerm = action.payload;
},
setStatusFilter: (state, action: PayloadAction<string>) => {
state.statusFilter = action.payload;
},
setPriorityFilter: (state, action: PayloadAction<string>) => {
state.priorityFilter = action.payload;
},
setSlaComplianceFilter: (state, action: PayloadAction<string>) => {
state.slaComplianceFilter = action.payload;
},
setDepartmentFilter: (state, action: PayloadAction<string>) => {
state.departmentFilter = action.payload;
},
setInitiatorFilter: (state, action: PayloadAction<string>) => {
state.initiatorFilter = action.payload;
},
setApproverFilter: (state, action: PayloadAction<string>) => {
state.approverFilter = action.payload;
},
setApproverFilterType: (state, action: PayloadAction<'current' | 'any'>) => {
state.approverFilterType = action.payload;
},
setDateRange: (state, action: PayloadAction<DateRange>) => {
state.dateRange = action.payload;
},
setCustomStartDate: (state, action: PayloadAction<Date | undefined>) => {
state.customStartDate = action.payload;
},
setCustomEndDate: (state, action: PayloadAction<Date | undefined>) => {
state.customEndDate = action.payload;
},
setShowCustomDatePicker: (state, action: PayloadAction<boolean>) => {
state.showCustomDatePicker = action.payload;
},
setCurrentPage: (state, action: PayloadAction<number>) => {
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;

View File

@ -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({