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 // Search users from Okta
const searchUsers = useCallback( const searchUsers = useCallback(
debounce(async (query: string) => { debounce(async (query: string) => {
if (!query || query.length < 2) { // Only trigger search when using @ sign
if (!query || !query.startsWith('@') || query.length < 2) {
setSearchResults([]); setSearchResults([]);
setSearching(false);
return; return;
} }
setSearching(true); setSearching(true);
try { 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 || []; const users = response.data?.data || [];
setSearchResults(users); setSearchResults(users);
} catch (error: any) { } 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" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder="Type name or email address..." placeholder="Type @ to search users..."
value={searchQuery} value={searchQuery}
onChange={handleSearchChange} onChange={handleSearchChange}
className="pl-10 border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" 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>
</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 */} {/* Search Results Dropdown */}
{searchResults.length > 0 && ( {searchResults.length > 0 && (

View File

@ -100,14 +100,17 @@ export function UserRoleManager() {
// Search users from Okta // Search users from Okta
const searchUsers = useCallback( const searchUsers = useCallback(
debounce(async (query: string) => { debounce(async (query: string) => {
if (!query || query.length < 2) { // Only trigger search when using @ sign
if (!query || !query.startsWith('@') || query.length < 2) {
setSearchResults([]); setSearchResults([]);
setSearching(false);
return; return;
} }
setSearching(true); setSearching(true);
try { 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 } // Backend returns { success: true, data: [...users], message, timestamp }
// Axios response is in response.data, actual user array is in response.data.data // 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" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
<Input <Input
type="text" type="text"
placeholder="Type name or email address..." placeholder="Type @ to search users..."
value={searchQuery} value={searchQuery}
onChange={handleSearchChange} onChange={handleSearchChange}
className="pl-10 pr-10 border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" 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" /> <Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-re-green animate-spin" />
)} )}
</div> </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 */} {/* Search Results Dropdown */}
{searchResults.length > 0 && ( {searchResults.length > 0 && (

View File

@ -17,12 +17,14 @@ export interface SLAData {
interface SLAProgressBarProps { interface SLAProgressBarProps {
sla: SLAData | null; sla: SLAData | null;
requestStatus: string; requestStatus: string;
isPaused?: boolean;
testId?: string; testId?: string;
} }
export function SLAProgressBar({ export function SLAProgressBar({
sla, sla,
requestStatus, requestStatus,
isPaused = false,
testId = 'sla-progress' testId = 'sla-progress'
}: SLAProgressBarProps) { }: SLAProgressBarProps) {
// If request is closed/approved/rejected or no SLA data, show status message // 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 // Use percentage-based colors to match approver SLA tracker
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
// Grey: When paused (frozen state)
const percentageUsed = sla.percentageUsed || 0; const percentageUsed = sla.percentageUsed || 0;
const rawStatus = sla.status || 'on_track'; const rawStatus = sla.status || 'on_track';
// Determine colors based on percentage (matching ApprovalStepCard logic) // Determine colors based on percentage (matching ApprovalStepCard logic)
const getStatusColors = () => { 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) { if (percentageUsed >= 100) {
return { return {
badge: 'bg-red-600 text-white animate-pulse', badge: 'bg-red-600 text-white animate-pulse',
progress: 'bg-red-600', progress: 'bg-red-600',
text: 'text-red-600' text: 'text-red-600',
icon: 'text-blue-600'
}; };
} else if (percentageUsed >= 75) { } else if (percentageUsed >= 75) {
return { return {
badge: 'bg-orange-500 text-white', badge: 'bg-orange-500 text-white',
progress: 'bg-orange-500', progress: 'bg-orange-500',
text: 'text-orange-600' text: 'text-orange-600',
icon: 'text-blue-600'
}; };
} else if (percentageUsed >= 50) { } else if (percentageUsed >= 50) {
return { return {
badge: 'bg-amber-500 text-white', badge: 'bg-amber-500 text-white',
progress: 'bg-amber-500', progress: 'bg-amber-500',
text: 'text-amber-600' text: 'text-amber-600',
icon: 'text-blue-600'
}; };
} else { } else {
return { return {
badge: 'bg-green-600 text-white', badge: 'bg-green-600 text-white',
progress: 'bg-green-600', 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 data-testid={testId}>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-blue-600" /> {isPaused ? (
<span className="text-sm font-semibold text-gray-900">SLA Progress</span> <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> </div>
<Badge <Badge
className={`text-xs ${colors.badge}`} className={`text-xs ${colors.badge}`}
data-testid={`${testId}-badge`} data-testid={`${testId}-badge`}
> >
{sla.percentageUsed || 0}% elapsed {sla.percentageUsed || 0}% elapsed {isPaused && '(frozen)'}
</Badge> </Badge>
</div> </div>

View File

@ -369,6 +369,7 @@ export function ApprovalStepCard({
{/* Current Approver - Time Tracking */} {/* Current Approver - Time Tracking */}
<div className={`border rounded-lg p-3 ${ <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) >= 100 ? 'bg-red-50 border-red-200' :
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' : (approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-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"> <p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
Current Approver - Time Tracking Current Approver - Time Tracking {isPaused && '(Paused)'}
</p> </p>
<div className="space-y-2 text-xs mb-3"> <div className="space-y-2 text-xs mb-3">
@ -395,14 +396,17 @@ export function ApprovalStepCard({
{(() => { {(() => {
// Determine color based on percentage used // Determine color based on percentage used
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
// Grey: When paused (frozen state)
const percentUsed = approval.sla.percentageUsed || 0; const percentUsed = approval.sla.percentageUsed || 0;
const getActiveIndicatorColor = () => { const getActiveIndicatorColor = () => {
if (isPaused) return 'bg-gray-500'; // Grey when paused
if (percentUsed >= 100) return 'bg-red-600'; if (percentUsed >= 100) return 'bg-red-600';
if (percentUsed >= 75) return 'bg-orange-500'; if (percentUsed >= 75) return 'bg-orange-500';
if (percentUsed >= 50) return 'bg-amber-500'; if (percentUsed >= 50) return 'bg-amber-500';
return 'bg-green-600'; return 'bg-green-600';
}; };
const getActiveTextColor = () => { const getActiveTextColor = () => {
if (isPaused) return 'text-gray-600'; // Grey when paused
if (percentUsed >= 100) return 'text-red-600'; if (percentUsed >= 100) return 'text-red-600';
if (percentUsed >= 75) return 'text-orange-600'; if (percentUsed >= 75) return 'text-orange-600';
if (percentUsed >= 50) return 'text-amber-600'; if (percentUsed >= 50) return 'text-amber-600';

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; 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 { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
import { getSocket } from '@/utils/socket'; import { getSocket } from '@/utils/socket';
@ -223,7 +223,6 @@ export function useRequestDetails(
*/ */
let pauseInfo = null; let pauseInfo = null;
try { try {
const { getPauseDetails } = await import('@/services/workflowApi');
pauseInfo = await getPauseDetails(wf.requestId); pauseInfo = await getPauseDetails(wf.requestId);
} catch (error) { } catch (error) {
// Pause info not available or request not paused - ignore // Pause info not available or request not paused - ignore
@ -436,7 +435,6 @@ export function useRequestDetails(
// Fetch pause details // Fetch pause details
let pauseInfo = null; let pauseInfo = null;
try { try {
const { getPauseDetails } = await import('@/services/workflowApi');
pauseInfo = await getPauseDetails(wf.requestId); pauseInfo = await getPauseDetails(wf.requestId);
} catch (error) { } catch (error) {
// Pause info not available or request not paused - ignore // Pause info not available or request not paused - ignore

View File

@ -50,13 +50,6 @@ export function useApproverPerformanceData({
try { try {
const dateRangeToSend = dateRange === 'all' ? undefined : dateRange; 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( const stats = await dashboardService.getSingleApproverStats(
approverId, approverId,
@ -67,7 +60,6 @@ export function useApproverPerformanceData({
slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined
); );
console.log('[Stats] Received stats:', stats);
setApproverStats(stats); setApproverStats(stats);
} catch (error) { } catch (error) {
console.error('[ApproverPerformance] Failed to fetch approver stats:', 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 // Components
import { ClosedRequestsHeader } from './components/ClosedRequestsHeader'; import { ClosedRequestsHeader } from './components/ClosedRequestsHeader';
@ -12,7 +12,7 @@ import { useClosedRequests } from './hooks/useClosedRequests';
import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters'; import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters';
// Types // Types
import type { ClosedRequestsProps, ClosedRequestsFilters } from './types/closedRequests.types'; import type { ClosedRequestsProps } from './types/closedRequests.types';
export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
// Data fetching hook // Data fetching hook
@ -22,26 +22,74 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
const fetchRef = useRef(closedRequests.fetchRequests); const fetchRef = useRef(closedRequests.fetchRequests);
fetchRef.current = closedRequests.fetchRequests; fetchRef.current = closedRequests.fetchRequests;
const filters = useClosedRequestsFilters({ const filters = useClosedRequestsFilters();
onFiltersChange: useCallback( const prevFiltersRef = useRef({
(filters: ClosedRequestsFilters) => { searchTerm: filters.searchTerm,
// Reset to page 1 when filters change statusFilter: filters.statusFilter,
fetchRef.current(1, { priorityFilter: filters.priorityFilter,
search: filters.search || undefined, sortBy: filters.sortBy,
status: filters.status !== 'all' ? filters.status : undefined, sortOrder: filters.sortOrder,
priority: filters.priority !== 'all' ? filters.priority : undefined, });
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, sortBy: filters.sortBy,
sortOrder: filters.sortOrder, 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 // Page change handler
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
if (newPage >= 1 && newPage <= closedRequests.pagination.totalPages) { if (newPage >= 1 && newPage <= closedRequests.pagination.totalPages) {
filters.setCurrentPage(newPage); // Update page in Redux
closedRequests.fetchRequests(newPage, { closedRequests.fetchRequests(newPage, {
search: filters.searchTerm || undefined, search: filters.searchTerm || undefined,
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,

View File

@ -106,20 +106,20 @@ export function ClosedRequestsFilters({
<Select value={statusFilter} onValueChange={onStatusChange}> <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"> <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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Statuses</SelectItem> <SelectItem value="all">All Closures</SelectItem>
<SelectItem value="closed"> <SelectItem value="approved">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-gray-600" /> <CheckCircle className="w-4 h-4 text-green-600" />
<span>Closed</span> <span>Closed After Approval</span>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value="rejected"> <SelectItem value="rejected">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<XCircle className="w-4 h-4 text-red-600" /> <XCircle className="w-4 h-4 text-red-600" />
<span>Rejected</span> <span>Closed After Rejection</span>
</div> </div>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>

View File

@ -2,7 +2,7 @@
* Hook for fetching and managing Closed Requests data * 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 workflowApi from '@/services/workflowApi';
import { ClosedRequest, PaginationState } from '../types/closedRequests.types'; import { ClosedRequest, PaginationState } from '../types/closedRequests.types';
import { transformClosedRequests } from '../utils/requestTransformers'; 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 [requests, setRequests] = useState<ClosedRequest[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
@ -28,7 +28,6 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
totalRecords: 0, totalRecords: 0,
itemsPerPage, itemsPerPage,
}); });
const isInitialMount = useRef(true);
const fetchRequests = useCallback( const fetchRequests = useCallback(
async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => { 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); const mapped = transformClosedRequests(data);
// Filter out approved requests - only show rejected and closed setRequests(mapped); // No client-side filtering - backend returns only CLOSED requests
const filtered = mapped.filter(request =>
request.status === 'rejected' || request.status === 'closed'
);
setRequests(filtered);
// Set pagination data // Set pagination data from backend
// 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.
const paginationData = (result as any)?.pagination; const paginationData = (result as any)?.pagination;
if (paginationData) { if (paginationData) {
// If we filtered out some requests on this page, we need to adjust pagination // 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] [itemsPerPage]
); );
// Initial fetch on mount // Initial fetch removed - component handles initial fetch using Redux stored page
useEffect(() => { // This prevents duplicate fetches and allows page persistence
if (isInitialMount.current) {
fetchRequests(1, initialFilters);
isInitialMount.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run on mount
const handleRefresh = useCallback((filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => { const handleRefresh = useCallback((filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => {
setRefreshing(true); 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 { 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 { interface UseClosedRequestsFiltersOptions {
onFiltersChange?: (filters: ClosedRequestsFilters) => void; onFiltersChange?: (filters: ClosedRequestsFilters) => void;
@ -11,13 +21,20 @@ interface UseClosedRequestsFiltersOptions {
} }
export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseClosedRequestsFiltersOptions = {}) { export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseClosedRequestsFiltersOptions = {}) {
const [searchTerm, setSearchTerm] = useState(''); const dispatch = useAppDispatch();
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 debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null); const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isInitialMount = useRef(true); 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 => { const getFilters = useCallback((): ClosedRequestsFilters => {
return { return {
@ -57,10 +74,8 @@ export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }:
}, [searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, onFiltersChange, getFilters, debounceMs]); }, [searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, onFiltersChange, getFilters, debounceMs]);
const clearFilters = useCallback(() => { const clearFilters = useCallback(() => {
setSearchTerm(''); dispatch(clearFiltersAction());
setPriorityFilter('all'); }, [dispatch]);
setStatusFilter('all');
}, []);
const activeFiltersCount = [ const activeFiltersCount = [
searchTerm, searchTerm,
@ -74,11 +89,13 @@ export function useClosedRequestsFilters({ onFiltersChange, debounceMs = 500 }:
statusFilter, statusFilter,
sortBy, sortBy,
sortOrder, sortOrder,
currentPage,
setSearchTerm, setSearchTerm,
setPriorityFilter, setPriorityFilter,
setStatusFilter, setStatusFilter,
setSortBy, setSortBy,
setSortOrder, setSortOrder,
setCurrentPage,
clearFilters, clearFilters,
activeFiltersCount, activeFiltersCount,
getFilters, 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 // Utils
import { transformRequests } from './utils/requestTransformers'; import { transformRequests } from './utils/requestTransformers';
// Types
import type { MyRequestsFilters } from './types/myRequests.types';
interface MyRequestsProps { interface MyRequestsProps {
onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void; onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void;
dynamicRequests?: any[]; dynamicRequests?: any[];
@ -35,19 +32,58 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
const fetchRef = useRef(myRequests.fetchMyRequests); const fetchRef = useRef(myRequests.fetchMyRequests);
fetchRef.current = myRequests.fetchMyRequests; fetchRef.current = myRequests.fetchMyRequests;
const filters = useMyRequestsFilters({ const filters = useMyRequestsFilters();
onFiltersChange: useCallback( const prevFiltersRef = useRef({
(filters: MyRequestsFilters) => { searchTerm: filters.searchTerm,
// Reset to page 1 when filters change statusFilter: filters.statusFilter,
fetchRef.current(1, { priorityFilter: filters.priorityFilter,
search: filters.search || undefined,
status: filters.status !== 'all' ? filters.status : undefined,
priority: filters.priority !== 'all' ? filters.priority : undefined,
});
},
[]
),
}); });
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) // State for backend stats (calculated from entire dataset via SQL queries)
const [backendStats, setBackendStats] = useState<{ const [backendStats, setBackendStats] = useState<{
@ -155,6 +191,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
const handlePageChange = useCallback( const handlePageChange = useCallback(
(newPage: number) => { (newPage: number) => {
if (newPage >= 1 && newPage <= myRequests.pagination.totalPages) { if (newPage >= 1 && newPage <= myRequests.pagination.totalPages) {
filters.setCurrentPage(newPage); // Update page in Redux
myRequests.fetchMyRequests(newPage, { myRequests.fetchMyRequests(newPage, {
search: filters.searchTerm || undefined, search: filters.searchTerm || undefined,
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
@ -210,7 +247,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
{/* Pagination */} {/* Pagination */}
<Pagination <Pagination
currentPage={myRequests.pagination.currentPage} currentPage={filters.currentPage || myRequests.pagination.currentPage}
totalPages={myRequests.pagination.totalPages} totalPages={myRequests.pagination.totalPages}
totalRecords={myRequests.pagination.totalRecords} totalRecords={myRequests.pagination.totalRecords}
itemsPerPage={myRequests.pagination.itemsPerPage} itemsPerPage={myRequests.pagination.itemsPerPage}

View File

@ -2,7 +2,7 @@
* Hook for fetching and managing My Requests data * 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 workflowApi from '@/services/workflowApi';
import { MyRequest, PaginationState } from '../types/myRequests.types'; import { MyRequest, PaginationState } from '../types/myRequests.types';
import { transformRequests } from '../utils/requestTransformers'; 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 [requests, setRequests] = useState<MyRequest[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false); const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false);
@ -26,7 +26,6 @@ export function useMyRequests({ itemsPerPage = 10, initialFilters }: UseMyReques
totalRecords: 0, totalRecords: 0,
itemsPerPage, itemsPerPage,
}); });
const isInitialMount = useRef(true);
const fetchMyRequests = useCallback( const fetchMyRequests = useCallback(
async (page: number = 1, filters?: { search?: string; status?: string; priority?: string }) => { async (page: number = 1, filters?: { search?: string; status?: string; priority?: string }) => {
@ -72,14 +71,8 @@ export function useMyRequests({ itemsPerPage = 10, initialFilters }: UseMyReques
[itemsPerPage] [itemsPerPage]
); );
// Initial fetch on mount // Initial fetch removed - component handles initial fetch using Redux stored page
useEffect(() => { // This prevents duplicate fetches and allows page persistence
if (isInitialMount.current) {
fetchMyRequests(1, initialFilters);
isInitialMount.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run on mount
return { return {
requests, 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 { 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 { interface UseMyRequestsFiltersOptions {
onFiltersChange?: (filters: MyRequestsFilters) => void; onFiltersChange?: (filters: MyRequestsFilters) => void;
@ -11,10 +19,18 @@ interface UseMyRequestsFiltersOptions {
} }
export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseMyRequestsFiltersOptions = {}) { export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseMyRequestsFiltersOptions = {}) {
const [searchTerm, setSearchTerm] = useState(''); const dispatch = useAppDispatch();
const [statusFilter, setStatusFilter] = useState('all');
const [priorityFilter, setPriorityFilter] = useState('all');
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null); 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 => { const getFilters = useCallback((): MyRequestsFilters => {
return { return {
@ -26,6 +42,12 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
// Debounced filter change handler // Debounced filter change handler
useEffect(() => { useEffect(() => {
// Skip initial mount - let component handle initial fetch
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
if (debounceTimeoutRef.current) { if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current); clearTimeout(debounceTimeoutRef.current);
} }
@ -46,18 +68,18 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
}, [searchTerm, statusFilter, priorityFilter, onFiltersChange, getFilters, debounceMs]); }, [searchTerm, statusFilter, priorityFilter, onFiltersChange, getFilters, debounceMs]);
const resetFilters = useCallback(() => { const resetFilters = useCallback(() => {
setSearchTerm(''); dispatch(clearFiltersAction());
setStatusFilter('all'); }, [dispatch]);
setPriorityFilter('all');
}, []);
return { return {
searchTerm, searchTerm,
statusFilter, statusFilter,
priorityFilter, priorityFilter,
currentPage,
setSearchTerm, setSearchTerm,
setStatusFilter, setStatusFilter,
setPriorityFilter, setPriorityFilter,
setCurrentPage,
getFilters, getFilters,
resetFilters, 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 { 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -7,14 +7,14 @@ import { Input } from '@/components/ui/input';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; 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 workflowApi from '@/services/workflowApi';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface Request { interface Request {
id: string; id: string;
title: string; title: string;
description: string; description: string;
status: 'pending' | 'approved' | 'rejected' | 'closed'; status: 'pending' | 'approved' | 'rejected' | 'closed' | 'paused';
priority: 'express' | 'standard'; priority: 'express' | 'standard';
initiator: { name: string; avatar: string }; initiator: { name: string; avatar: string };
currentApprover?: { currentApprover?: {
@ -26,6 +26,8 @@ interface Request {
approvalStep?: string; approvalStep?: string;
department?: string; department?: string;
currentLevelSLA?: any; // Backend-provided SLA for current level currentLevelSLA?: any; // Backend-provided SLA for current level
isPaused?: boolean; // Pause status
pauseInfo?: any; // Pause details
} }
interface OpenRequestsProps { interface OpenRequestsProps {
@ -99,27 +101,18 @@ const getStatusConfig = (status: string) => {
// getSLAUrgency removed - now using SLATracker component for real-time SLA display // getSLAUrgency removed - now using SLATracker component for real-time SLA display
export function OpenRequests({ onViewRequest }: OpenRequestsProps) { 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 [items, setItems] = useState<Request[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// Pagination states // Pagination states (currentPage now in Redux)
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalRecords, setTotalRecords] = useState(0); const [totalRecords, setTotalRecords] = useState(0);
const [itemsPerPage] = useState(10); 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) // Fetch open requests for the current user only (user-scoped, not organization-wide)
// Note: This endpoint returns only requests where the user is: // 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) // - An initiator (for approved requests awaiting closure)
// This applies to ALL users including regular users, MANAGEMENT, and ADMIN roles // This applies to ALL users including regular users, MANAGEMENT, and ADMIN roles
// For organization-wide view, users should use the "All Requests" screen (/requests) // 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 { try {
if (page === 1) { if (page === 1) {
setLoading(true); setLoading(true);
@ -141,11 +134,11 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const result = await workflowApi.listOpenForMe({ const result = await workflowApi.listOpenForMe({
page, page,
limit: itemsPerPage, limit: itemsPerPage,
search: filters?.search, search: filterParams?.search,
status: filters?.status, status: filterParams?.status,
priority: filters?.priority, priority: filterParams?.priority,
sortBy: filters?.sortBy, sortBy: filterParams?.sortBy,
sortOrder: filters?.sortOrder sortOrder: filterParams?.sortOrder
}); });
// Extract data - workflowApi now returns { data: [], pagination: {} } // Extract data - workflowApi now returns { data: [], pagination: {} }
@ -156,7 +149,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
// Set pagination data // Set pagination data
const pagination = (result as any)?.pagination; const pagination = (result as any)?.pagination;
if (pagination) { if (pagination) {
setCurrentPage(pagination.page || 1); filters.setCurrentPage(pagination.page || 1);
setTotalPages(pagination.totalPages || 1); setTotalPages(pagination.totalPages || 1);
setTotalRecords(pagination.total || 0); setTotalRecords(pagination.total || 0);
} }
@ -192,28 +185,30 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
} }
}, [itemsPerPage]); }, [itemsPerPage, filters]);
fetchRequestsRef.current = fetchRequests;
const handleRefresh = () => { const handleRefresh = () => {
setRefreshing(true); setRefreshing(true);
fetchRequests(currentPage, { fetchRequests(filters.currentPage, {
search: searchTerm || undefined, search: filters.searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined, priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
sortBy, sortBy: filters.sortBy,
sortOrder sortOrder: filters.sortOrder
}); });
}; };
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) { if (newPage >= 1 && newPage <= totalPages) {
setCurrentPage(newPage); filters.setCurrentPage(newPage);
fetchRequests(newPage, { fetchRequests(newPage, {
search: searchTerm || undefined, search: filters.searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined, priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
sortBy, sortBy: filters.sortBy,
sortOrder sortOrder: filters.sortOrder
}); });
} }
}; };
@ -221,7 +216,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const getPageNumbers = () => { const getPageNumbers = () => {
const pages = []; const pages = [];
const maxPagesToShow = 5; 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); let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
if (endPage - startPage < maxPagesToShow - 1) { if (endPage - startPage < maxPagesToShow - 1) {
@ -235,59 +230,50 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
return pages; return pages;
}; };
// Track if this is the initial mount // Track if this is initial mount
const isInitialMount = useRef(true); const hasInitialFetchRun = useRef(false);
// Initial fetch on mount with URL params // Initial fetch on mount - use stored page from Redux
useEffect(() => { useEffect(() => {
if (isInitialMount.current) { if (!hasInitialFetchRun.current) {
isInitialMount.current = false; hasInitialFetchRun.current = true;
fetchRequests(1, { const storedPage = filters.currentPage || 1;
search: searchTerm || undefined, fetchRequests(storedPage, {
status: statusFilter !== 'all' ? statusFilter : undefined, search: filters.searchTerm || undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
sortBy, priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
sortOrder sortBy: filters.sortBy,
sortOrder: filters.sortOrder,
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // 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(() => { useEffect(() => {
// Skip initial mount to avoid double fetch // Skip until initial fetch has completed
if (isInitialMount.current) return; if (!hasInitialFetchRun.current) return;
// Debounce search: wait 500ms after user stops typing // Debounce search
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
setCurrentPage(1); // Reset to page 1 when filters change filters.setCurrentPage(1); // Reset to page 1 when filters change
fetchRequests(1, { fetchRequests(1, {
search: searchTerm || undefined, search: filters.searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined, priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
sortBy, sortBy: filters.sortBy,
sortOrder sortOrder: filters.sortOrder,
}); });
}, searchTerm ? 500 : 0); // Debounce only for search, instant for dropdowns }, filters.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId); 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 // Backend handles both filtering and sorting - use items directly
// No client-side sorting needed anymore // No client-side sorting needed anymore
const filteredAndSortedRequests = items; 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 ( return (
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto"> <div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto">
@ -334,19 +320,19 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<div> <div>
<CardTitle className="text-base sm:text-lg">Filters & Search</CardTitle> <CardTitle className="text-base sm:text-lg">Filters & Search</CardTitle>
<CardDescription className="text-xs sm:text-sm"> <CardDescription className="text-xs sm:text-sm">
{activeFiltersCount > 0 && ( {filters.activeFiltersCount > 0 && (
<span className="text-blue-600 font-medium"> <span className="text-blue-600 font-medium">
{activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active {filters.activeFiltersCount} filter{filters.activeFiltersCount > 1 ? 's' : ''} active
</span> </span>
)} )}
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
{activeFiltersCount > 0 && ( {filters.activeFiltersCount > 0 && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" 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" 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" /> <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" /> <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 <Input
placeholder="Search requests, IDs..." placeholder="Search requests, IDs..."
value={searchTerm} value={filters.searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} 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" 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> </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"> <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" /> <SelectValue placeholder="All Priorities" />
</SelectTrigger> </SelectTrigger>
@ -389,7 +375,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</SelectContent> </SelectContent>
</Select> </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"> <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" /> <SelectValue placeholder="All Statuses" />
</SelectTrigger> </SelectTrigger>
@ -401,7 +387,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</Select> </Select>
<div className="flex gap-2"> <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"> <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" /> <SelectValue placeholder="Sort by" />
</SelectTrigger> </SelectTrigger>
@ -416,10 +402,10 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<Button <Button
variant="outline" variant="outline"
size="sm" 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" 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> </Button>
</div> </div>
</div> </div>
@ -481,34 +467,55 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{/* SLA Display - Compact Version */} {/* SLA Display - Compact Version */}
{request.currentLevelSLA && (() => { {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 // Use percentage-based colors to match approver SLA tracker
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
// Grey: When paused (frozen state)
const percentUsed = request.currentLevelSLA.percentageUsed || 0; const percentUsed = request.currentLevelSLA.percentageUsed || 0;
const getSLAColors = () => { 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) { if (percentUsed >= 100) {
return { return {
bg: 'bg-red-50 border border-red-200', bg: 'bg-red-50 border border-red-200',
progress: 'bg-red-600', progress: 'bg-red-600',
text: 'text-red-600' text: 'text-red-600',
icon: 'text-blue-600'
}; };
} else if (percentUsed >= 75) { } else if (percentUsed >= 75) {
return { return {
bg: 'bg-orange-50 border border-orange-200', bg: 'bg-orange-50 border border-orange-200',
progress: 'bg-orange-500', progress: 'bg-orange-500',
text: 'text-orange-600' text: 'text-orange-600',
icon: 'text-blue-600'
}; };
} else if (percentUsed >= 50) { } else if (percentUsed >= 50) {
return { return {
bg: 'bg-amber-50 border border-amber-200', bg: 'bg-amber-50 border border-amber-200',
progress: 'bg-amber-500', progress: 'bg-amber-500',
text: 'text-amber-600' text: 'text-amber-600',
icon: 'text-blue-600'
}; };
} else { } else {
return { return {
bg: 'bg-green-50 border border-green-200', bg: 'bg-green-50 border border-green-200',
progress: 'bg-green-600', 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={`p-2 rounded-md ${colors.bg}`}>
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-gray-600" /> {isPaused ? (
<span className="text-xs font-medium text-gray-900">TAT: {percentUsed}%</span> <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>
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span> <span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span>
<span className={`font-semibold ${ <span className={`font-semibold ${colors.text}`}>
percentUsed >= 100 ? 'text-red-600' :
percentUsed >= 75 ? 'text-orange-600' :
percentUsed >= 50 ? 'text-amber-600' :
'text-gray-700'
}`}>
{request.currentLevelSLA.remainingText} left {request.currentLevelSLA.remainingText} left
</span> </span>
</div> </div>
@ -599,16 +607,16 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</div> </div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No requests found</h3> <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"> <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.' ? 'Try adjusting your filters or search terms to see more results.'
: 'No open requests available at the moment.' : 'No open requests available at the moment.'
} }
</p> </p>
{activeFiltersCount > 0 && ( {filters.activeFiltersCount > 0 && (
<Button <Button
variant="outline" variant="outline"
className="mt-4" className="mt-4"
onClick={clearFilters} onClick={filters.clearFilters}
> >
Clear all filters Clear all filters
</Button> </Button>
@ -623,21 +631,21 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-xs sm:text-sm text-muted-foreground"> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => handlePageChange(currentPage - 1)} onClick={() => handlePageChange(filters.currentPage - 1)}
disabled={currentPage === 1} disabled={filters.currentPage === 1}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
> >
<ArrowRight className="h-4 w-4 rotate-180" /> <ArrowRight className="h-4 w-4 rotate-180" />
</Button> </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> <Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button>
<span className="text-muted-foreground">...</span> <span className="text-muted-foreground">...</span>
@ -647,16 +655,16 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{getPageNumbers().map((pageNum) => ( {getPageNumbers().map((pageNum) => (
<Button <Button
key={pageNum} key={pageNum}
variant={pageNum === currentPage ? "default" : "outline"} variant={pageNum === filters.currentPage ? "default" : "outline"}
size="sm" size="sm"
onClick={() => handlePageChange(pageNum)} 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} {pageNum}
</Button> </Button>
))} ))}
{currentPage < totalPages - 2 && totalPages > 5 && ( {filters.currentPage < totalPages - 2 && totalPages > 5 && (
<> <>
<span className="text-muted-foreground">...</span> <span className="text-muted-foreground">...</span>
<Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button> <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 <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => handlePageChange(currentPage + 1)} onClick={() => handlePageChange(filters.currentPage + 1)}
disabled={currentPage === totalPages} disabled={filters.currentPage === totalPages}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
> >
<ArrowRight className="h-4 w-4" /> <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 // Components
import { RequestDetailHeader } from './components/RequestDetailHeader'; import { RequestDetailHeader } from './components/RequestDetailHeader';
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal'; 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 { toast } from 'sonner';
import { OverviewTab } from './components/tabs/OverviewTab'; import { OverviewTab } from './components/tabs/OverviewTab';
import { WorkflowTab } from './components/tabs/WorkflowTab'; import { WorkflowTab } from './components/tabs/WorkflowTab';
@ -257,7 +257,6 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
setLoadingSummary(true); setLoadingSummary(true);
// Just fetch the summary by requestId - don't try to create it // Just fetch the summary by requestId - don't try to create it
// Summary is auto-created by backend on final approval/rejection // Summary is auto-created by backend on final approval/rejection
const { getSummaryByRequestId } = await import('@/services/summaryApi');
const summary = await getSummaryByRequestId(apiRequest.requestId); const summary = await getSummaryByRequestId(apiRequest.requestId);
if (summary?.summaryId) { 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 { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle } from 'lucide-react';
import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi'; import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import notificationApi, { type Notification } from '@/services/notificationApi';
interface QuickActionsSidebarProps { interface QuickActionsSidebarProps {
request: any; request: any;
@ -46,6 +47,7 @@ export function QuickActionsSidebar({
const { user } = useAuth(); const { user } = useAuth();
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]); const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
const [loadingRecipients, setLoadingRecipients] = useState(false); const [loadingRecipients, setLoadingRecipients] = useState(false);
const [hasRetriggerNotification, setHasRetriggerNotification] = useState(false);
const isClosed = request?.status === 'closed'; const isClosed = request?.status === 'closed';
const isPaused = request?.pauseInfo?.isPaused || false; const isPaused = request?.pauseInfo?.isPaused || false;
const pausedByUserId = request?.pauseInfo?.pausedBy?.userId; const pausedByUserId = request?.pauseInfo?.pausedBy?.userId;
@ -59,6 +61,38 @@ export function QuickActionsSidebar({
// Retrigger: Only for initiator when approver paused (initiator asks approver to resume) // Retrigger: Only for initiator when approver paused (initiator asks approver to resume)
const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger; 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 // Fetch shared recipients when request is closed and summaryId is available
useEffect(() => { useEffect(() => {
@ -180,8 +214,48 @@ export function QuickActionsSidebar({
)} )}
{isPaused && ( {isPaused && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 text-center"> <div className="bg-orange-50 border border-orange-200 rounded-lg p-3 text-center">
<p className="text-xs text-orange-800 font-medium">Workflow is paused</p> {/* Different messages based on who paused, who is viewing, and if retrigger was sent */}
<p className="text-xs text-orange-600 mt-1">Actions are disabled until resumed</p> {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>
)} )}
</div> </div>

View File

@ -20,6 +20,7 @@ interface RequestDetailHeaderProps {
export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, onShareSummary, isInitiator }: RequestDetailHeaderProps) { export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, onShareSummary, isInitiator }: RequestDetailHeaderProps) {
const priorityConfig = getPriorityConfig(request?.priority || 'standard'); const priorityConfig = getPriorityConfig(request?.priority || 'standard');
const statusConfig = getStatusConfig(request?.status || 'pending'); const statusConfig = getStatusConfig(request?.status || 'pending');
const isPaused = request?.pauseInfo?.isPaused || false;
return ( return (
<div className="bg-white rounded-lg shadow-sm border border-gray-300 mb-4 sm:mb-6" data-testid="request-detail-header"> <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> </div>
{/* SLA Progress Section */} {/* 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"> <div className={`px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-gray-200 ${
<SLAProgressBar sla={request.summary?.sla || request.sla} requestStatus={request.status} testId="request-sla" /> 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>
</div> </div>
); );

View File

@ -71,8 +71,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
const [loadingDepartments, setLoadingDepartments] = useState(false); const [loadingDepartments, setLoadingDepartments] = useState(false);
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]); const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
// Pagination // Pagination (currentPage now in Redux)
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalRecords, setTotalRecords] = useState(0); const [totalRecords, setTotalRecords] = useState(0);
const [itemsPerPage] = useState(10); 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 // Note: Stats come from backend stats API (always unfiltered), not from allData
// Update pagination // Update pagination
setCurrentPage(result.pagination.page); filters.setCurrentPage(result.pagination.page);
setTotalPages(result.pagination.totalPages); setTotalPages(result.pagination.totalPages);
setTotalRecords(result.pagination.total); 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 // Note: statusFilter is NOT in dependencies - stats don't change when only status changes
]); ]);
// Fetch requests on mount and when filters change // Track previous filter values to detect changes
// Also refetch when isOrgLevel changes (when admin toggles between Org/Personal in Dashboard) 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(() => { 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(() => { const timeoutId = setTimeout(() => {
setCurrentPage(1); filters.setCurrentPage(1);
fetchRequests(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); return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
isOrgLevel, // Re-fetch when org/personal toggle changes isOrgLevel,
filters.searchTerm, filters.searchTerm,
filters.statusFilter, filters.statusFilter,
filters.priorityFilter, filters.priorityFilter,
@ -370,16 +427,15 @@ export function Requests({ onViewRequest }: RequestsProps) {
filters.dateRange, filters.dateRange,
filters.customStartDate, filters.customStartDate,
filters.customEndDate filters.customEndDate
// fetchRequests excluded to prevent infinite loops
]); ]);
// Page change handler // Page change handler
const handlePageChange = useCallback((newPage: number) => { const handlePageChange = useCallback((newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) { if (newPage >= 1 && newPage <= totalPages) {
setCurrentPage(newPage); filters.setCurrentPage(newPage);
fetchRequests(newPage); fetchRequests(newPage);
} }
}, [totalPages, fetchRequests]); }, [totalPages, fetchRequests, filters]);
// Transform requests // Transform requests
const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]); const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
@ -768,7 +824,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
{/* Pagination */} {/* Pagination */}
<Pagination <Pagination
currentPage={currentPage} currentPage={filters.currentPage || 1}
totalPages={totalPages} totalPages={totalPages}
totalRecords={totalRecords} totalRecords={totalRecords}
itemsPerPage={itemsPerPage} itemsPerPage={itemsPerPage}

View File

@ -57,8 +57,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
const [loadingDepartments, setLoadingDepartments] = useState(false); const [loadingDepartments, setLoadingDepartments] = useState(false);
const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]); const [allUsers, setAllUsers] = useState<Array<{ userId: string; email: string; displayName?: string }>>([]);
// Pagination // Pagination (currentPage now in Redux)
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalRecords, setTotalRecords] = useState(0); const [totalRecords, setTotalRecords] = useState(0);
const [itemsPerPage] = useState(10); const [itemsPerPage] = useState(10);
@ -181,7 +180,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
setApiRequests(result.data); // Paginated data (10 records) setApiRequests(result.data); // Paginated data (10 records)
// Update pagination // Update pagination
setCurrentPage(result.pagination.page); filters.setCurrentPage(result.pagination.page);
setTotalPages(result.pagination.totalPages); setTotalPages(result.pagination.totalPages);
setTotalRecords(result.pagination.total); setTotalRecords(result.pagination.total);
} catch (error) { } 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 // 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(() => { 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(() => { const timeoutId = setTimeout(() => {
setCurrentPage(1); filters.setCurrentPage(1);
fetchRequests(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); return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -271,16 +326,15 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
filters.dateRange, filters.dateRange,
filters.customStartDate, filters.customStartDate,
filters.customEndDate filters.customEndDate
// fetchRequests excluded to prevent infinite loops
]); ]);
// Page change handler // Page change handler
const handlePageChange = useCallback((newPage: number) => { const handlePageChange = useCallback((newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) { if (newPage >= 1 && newPage <= totalPages) {
setCurrentPage(newPage); filters.setCurrentPage(newPage);
fetchRequests(newPage); fetchRequests(newPage);
} }
}, [totalPages, fetchRequests]); }, [totalPages, fetchRequests, filters]);
// Transform requests // Transform requests
const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]); const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
@ -688,7 +742,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
{/* Pagination */} {/* Pagination */}
<Pagination <Pagination
currentPage={currentPage} currentPage={filters.currentPage || 1}
totalPages={totalPages} totalPages={totalPages}
totalRecords={totalRecords} totalRecords={totalRecords}
itemsPerPage={itemsPerPage} 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 { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import type { DateRange } from '@/services/dashboard.service'; import type { DateRange } from '@/services/dashboard.service';
import type { RequestFilters } from '../types/requests.types'; 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() { export function useRequestsFilters() {
const [searchParams, setSearchParams] = useSearchParams(); const dispatch = useAppDispatch();
const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || ''); // Get all filter state from Redux
const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all'); const {
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 });
}, [
searchTerm, searchTerm,
statusFilter, statusFilter,
priorityFilter, priorityFilter,
@ -61,8 +40,24 @@ export function useRequestsFilters() {
dateRange, dateRange,
customStartDate, customStartDate,
customEndDate, 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 => { const getFilters = useCallback((): RequestFilters => {
return { return {
@ -93,42 +88,31 @@ export function useRequestsFilters() {
]); ]);
const clearFilters = useCallback(() => { const clearFilters = useCallback(() => {
setSearchTerm(''); dispatch(clearFiltersAction());
setStatusFilter('all'); }, [dispatch]);
setPriorityFilter('all');
setSlaComplianceFilter('all');
setDepartmentFilter('all');
setInitiatorFilter('all');
setApproverFilter('all');
setApproverFilterType('current');
setDateRange('all');
setCustomStartDate(undefined);
setCustomEndDate(undefined);
setShowCustomDatePicker(false);
}, []);
const handleDateRangeChange = useCallback((value: string) => { const handleDateRangeChange = useCallback((value: string) => {
const newRange = value as DateRange; const newRange = value as DateRange;
setDateRange(newRange); dispatch(setDateRangeAction(newRange));
if (newRange !== 'custom') { if (newRange !== 'custom') {
setCustomStartDate(undefined); dispatch(setCustomStartDateAction(undefined));
setCustomEndDate(undefined); dispatch(setCustomEndDateAction(undefined));
setShowCustomDatePicker(false); dispatch(setShowCustomDatePickerAction(false));
} else { } else {
setShowCustomDatePicker(true); dispatch(setShowCustomDatePickerAction(true));
} }
}, []); }, [dispatch]);
const handleApplyCustomDate = useCallback(() => { const handleApplyCustomDate = useCallback(() => {
if (customStartDate && customEndDate) { if (customStartDate && customEndDate) {
if (customStartDate > customEndDate) { if (customStartDate > customEndDate) {
const temp = customStartDate; // Swap dates if start is after end
setCustomStartDate(customEndDate); dispatch(setCustomStartDateAction(customEndDate));
setCustomEndDate(temp); dispatch(setCustomEndDateAction(customStartDate));
} }
setShowCustomDatePicker(false); dispatch(setShowCustomDatePickerAction(false));
} }
}, [customStartDate, customEndDate]); }, [customStartDate, customEndDate, dispatch]);
const hasActiveFilters: boolean = !!( const hasActiveFilters: boolean = !!(
searchTerm || searchTerm ||
@ -157,6 +141,7 @@ export function useRequestsFilters() {
customStartDate, customStartDate,
customEndDate, customEndDate,
showCustomDatePicker, showCustomDatePicker,
currentPage,
hasActiveFilters, hasActiveFilters,
// Setters // Setters
setSearchTerm, setSearchTerm,
@ -171,6 +156,7 @@ export function useRequestsFilters() {
setCustomStartDate, setCustomStartDate,
setCustomEndDate, setCustomEndDate,
setShowCustomDatePicker, setShowCustomDatePicker,
setCurrentPage,
// Helpers // Helpers
getFilters, getFilters,
clearFilters, 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 { configureStore } from '@reduxjs/toolkit';
import authSlice from './slices/authSlice'; import authSlice from './slices/authSlice';
import dashboardSlice from '../pages/Dashboard/redux/dashboardSlice'; 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({ export const store = configureStore({
reducer: { reducer: {
auth: authSlice.reducer, auth: authSlice.reducer,
dashboard: dashboardSlice.reducer, dashboard: dashboardSlice.reducer,
requests: requestsSlice.reducer,
myRequests: myRequestsSlice.reducer,
openRequests: openRequestsSlice.reducer,
closedRequests: closedRequestsSlice.reducer,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({

View File

@ -667,7 +667,7 @@ class DashboardService {
slaCompliance?: string, slaCompliance?: string,
search?: string search?: string
): Promise<{ ): Promise<{
requests: any[], requests: any[],
pagination: { pagination: {
currentPage: number, currentPage: number,
totalPages: number, totalPages: number,