| Request ID |
- Title |
+ Title |
Department |
Approver |
Level |
- Breach Time |
-
+ | Breach Time |
+
Reason
|
Priority |
@@ -95,8 +95,8 @@ export function TATBreachReport({
>
{req.requestNumber}
-
- {req.title}
+ |
+ {req.title}
|
â
)}
|
-
-
+ |
+
{formatBreachTime(breachTime)}
|
-
+ |
{req.breachReason || 'TAT Exceeded'}
diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx
index 3dac0fb..e0950cf 100644
--- a/src/pages/MyRequests/MyRequests.tsx
+++ b/src/pages/MyRequests/MyRequests.tsx
@@ -1,7 +1,9 @@
-import { useCallback, useRef } from 'react';
+import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
import { FileText } from 'lucide-react';
import { PageHeader } from '@/components/common/PageHeader';
import { Pagination } from '@/components/common/Pagination';
+import dashboardService from '@/services/dashboard.service';
+import { useAuth } from '@/contexts/AuthContext';
// Components
import { MyRequestsStatsSection } from './components/MyRequestsStats';
@@ -11,7 +13,6 @@ import { MyRequestsList } from './components/MyRequestsList';
// Hooks
import { useMyRequests } from './hooks/useMyRequests';
import { useMyRequestsFilters } from './hooks/useMyRequestsFilters';
-import { useMyRequestsStats } from './hooks/useMyRequestsStats';
// Utils
import { transformRequests } from './utils/requestTransformers';
@@ -25,6 +26,8 @@ interface MyRequestsProps {
}
export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsProps) {
+ const { user } = useAuth();
+
// Data fetching hook
const myRequests = useMyRequests({ itemsPerPage: 10 });
@@ -46,15 +49,95 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
),
});
+ // State for backend stats (calculated from entire dataset via SQL queries)
+ const [backendStats, setBackendStats] = useState<{
+ total: number;
+ pending: number;
+ approved: number;
+ rejected: number;
+ draft: number;
+ closed: number;
+ } | null>(null);
+ const [loadingStats, setLoadingStats] = useState(false);
+
+ // Fetch stats from backend API (calculates from entire dataset using SQL, not by fetching data)
+ // Backend automatically filters by userId for non-admin users (initiator_id = userId)
+ const fetchBackendStats = useCallback(async () => {
+ if (!user?.userId) return;
+
+ try {
+ setLoadingStats(true);
+
+ // Use backend stats API - it automatically filters by userId for non-admin users
+ // This calculates stats from entire dataset using SQL COUNT queries, not by fetching data
+ const stats = await dashboardService.getRequestStats(
+ 'month', // Default date range
+ undefined, // startDate
+ undefined, // endDate
+ filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
+ undefined, // department
+ undefined, // initiator (already filtered by userId in backend)
+ undefined, // approver
+ undefined, // approverType
+ filters.searchTerm || undefined,
+ undefined // slaCompliance
+ );
+
+ setBackendStats({
+ total: stats.totalRequests || 0,
+ pending: stats.openRequests || 0,
+ approved: stats.approvedRequests || 0,
+ rejected: stats.rejectedRequests || 0,
+ draft: stats.draftRequests || 0,
+ closed: stats.closedRequests || 0,
+ });
+ } catch (error) {
+ console.error('Failed to fetch backend stats:', error);
+ setBackendStats(null);
+ } finally {
+ setLoadingStats(false);
+ }
+ }, [user?.userId, filters.searchTerm, filters.priorityFilter]); // Exclude statusFilter
+
+ // Fetch stats when filters change (except status filter)
+ // Stats are calculated from entire dataset via backend SQL queries (no data fetching needed)
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ fetchBackendStats();
+ }, filters.searchTerm ? 500 : 0);
+
+ return () => clearTimeout(timeoutId);
+ }, [filters.searchTerm, filters.priorityFilter, fetchBackendStats]); // Exclude statusFilter
+
// Handle dynamic requests (fallback until API loads)
const convertedDynamicRequests = transformRequests(dynamicRequests);
const sourceRequests = myRequests.hasFetchedFromApi ? myRequests.requests : convertedDynamicRequests;
- // Stats calculation
- const stats = useMyRequestsStats({
- requests: sourceRequests,
- totalRecords: myRequests.pagination.totalRecords,
- });
+ // Calculate stats from backend stats API (calculated from entire dataset via SQL queries)
+ // This is much more efficient - backend uses COUNT queries, no data fetching needed
+ const stats = useMemo(() => {
+ if (backendStats) {
+ // Use backend stats (calculated from entire dataset via SQL COUNT queries)
+ return {
+ total: backendStats.total || 0,
+ pending: backendStats.pending || 0,
+ approved: backendStats.approved || 0,
+ rejected: backendStats.rejected || 0,
+ draft: backendStats.draft || 0,
+ closed: backendStats.closed || 0,
+ };
+ }
+
+ // Fallback: if stats haven't loaded yet, show zeros
+ return {
+ total: 0,
+ pending: 0,
+ approved: 0,
+ rejected: 0,
+ draft: 0,
+ closed: 0,
+ };
+ }, [backendStats]);
// Page change handler
const handlePageChange = useCallback(
@@ -78,15 +161,20 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
title="My Requests"
description="Track and manage all your submitted requests"
badge={{
- value: `${myRequests.pagination.totalRecords || sourceRequests.length} total`,
+ value: `${stats.total} total`,
label: 'requests',
- loading: myRequests.loading,
+ loading: myRequests.loading || loadingStats,
}}
testId="my-requests-header"
/>
{/* Stats Overview */}
-
+ {
+ filters.setStatusFilter(status);
+ }}
+ />
{/* Filters and Search */}
void;
}
-export function MyRequestsStatsSection({ stats }: MyRequestsStatsProps) {
+export function MyRequestsStatsSection({ stats, onStatusFilter }: MyRequestsStatsProps) {
+ const handleCardClick = (status: string) => {
+ if (onStatusFilter) {
+ onStatusFilter(status);
+ }
+ };
return (
-
+
handleCardClick('all') : undefined}
/>
handleCardClick('pending') : undefined}
/>
handleCardClick('approved') : undefined}
/>
handleCardClick('rejected') : undefined}
/>
handleCardClick('draft') : undefined}
+ />
+
+ handleCardClick('closed') : undefined}
/>
);
diff --git a/src/pages/MyRequests/hooks/useMyRequests.ts b/src/pages/MyRequests/hooks/useMyRequests.ts
index c331000..d529966 100644
--- a/src/pages/MyRequests/hooks/useMyRequests.ts
+++ b/src/pages/MyRequests/hooks/useMyRequests.ts
@@ -36,7 +36,7 @@ export function useMyRequests({ itemsPerPage = 10, initialFilters }: UseMyReques
setRequests([]);
}
- const result = await workflowApi.listMyWorkflows({
+ const result = await workflowApi.listMyInitiatedWorkflows({
page,
limit: itemsPerPage,
search: filters?.search,
diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx
index 798d754..b000e68 100644
--- a/src/pages/RequestDetail/RequestDetail.tsx
+++ b/src/pages/RequestDetail/RequestDetail.tsx
@@ -36,6 +36,9 @@ import { downloadDocument } from '@/services/workflowApi';
// Components
import { RequestDetailHeader } from './components/RequestDetailHeader';
+import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
+import { createSummary } from '@/services/summaryApi';
+import { toast } from 'sonner';
import { OverviewTab } from './components/tabs/OverviewTab';
import { WorkflowTab } from './components/tabs/WorkflowTab';
import { DocumentsTab } from './components/tabs/DocumentsTab';
@@ -95,6 +98,8 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
const initialTab = urlParams.get('tab') || 'overview';
const [activeTab, setActiveTab] = useState(initialTab);
+ const [showShareSummaryModal, setShowShareSummaryModal] = useState(false);
+ const [summaryId, setSummaryId] = useState (null);
const { user } = useAuth();
// Custom hooks
@@ -173,6 +178,32 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
refreshDetails();
};
+ const handleShareSummary = async () => {
+ if (!apiRequest?.requestId) {
+ toast.error('Request ID not found');
+ return;
+ }
+
+ try {
+ // Check if summary already exists, if not create it
+ let currentSummaryId = summaryId;
+ if (!currentSummaryId) {
+ const summary = await createSummary(apiRequest.requestId);
+ currentSummaryId = summary.summaryId;
+ setSummaryId(currentSummaryId);
+ }
+ setShowShareSummaryModal(true);
+ } catch (error: any) {
+ console.error('Failed to create/get summary:', error);
+ if (error?.response?.status === 400 && error?.response?.data?.message?.includes('already exists')) {
+ // Summary already exists, try to get it
+ toast.error('Summary already exists. Please refresh the page.');
+ } else {
+ toast.error(error?.response?.data?.message || 'Failed to prepare summary for sharing');
+ }
+ }
+ };
+
const needsClosure = request?.status === 'approved' && isInitiator;
// Get current levels for WorkNotesTab
@@ -222,6 +253,8 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
refreshing={refreshing}
onBack={onBack || (() => window.history.back())}
onRefresh={handleRefresh}
+ onShareSummary={handleShareSummary}
+ isInitiator={isInitiator}
/>
{/* Tabs */}
@@ -363,6 +396,19 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
+ {/* Share Summary Modal */}
+ {showShareSummaryModal && summaryId && (
+ setShowShareSummaryModal(false)}
+ summaryId={summaryId}
+ requestTitle={request?.title || 'N/A'}
+ onSuccess={() => {
+ refreshDetails();
+ }}
+ />
+ )}
+
{/* Modals */}
void;
onRefresh: () => void;
+ onShareSummary?: () => void;
+ isInitiator?: boolean;
}
-export function RequestDetailHeader({ request, refreshing, onBack, onRefresh }: RequestDetailHeaderProps) {
+export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, onShareSummary, isInitiator }: RequestDetailHeaderProps) {
const priorityConfig = getPriorityConfig(request?.priority || 'standard');
const statusConfig = getStatusConfig(request?.status || 'pending');
@@ -68,18 +70,34 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh }:
- {/* Refresh Button */}
-
+ {/* Action Buttons */}
+
+ {/* Share Summary Button - Only show for closed requests if user is initiator */}
+ {onShareSummary && isInitiator && request?.status?.toLowerCase() === 'closed' && (
+
+ )}
+ {/* Refresh Button */}
+
+
{/* Request Title */}
diff --git a/src/pages/Requests/Requests.tsx b/src/pages/Requests/Requests.tsx
index c087d2e..72434ad 100644
--- a/src/pages/Requests/Requests.tsx
+++ b/src/pages/Requests/Requests.tsx
@@ -28,6 +28,7 @@ import { exportRequestsToCSV } from './utils/csvExports';
// Services
import { fetchRequestsData, fetchAllRequestsForExport } from './services/requestsService';
+import workflowApi from '@/services/workflowApi';
// Types
import type { RequestsProps, BackendStats } from './types/requests.types';
@@ -55,9 +56,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
const [apiRequests, setApiRequests] = useState([]);
const [loading, setLoading] = useState(false);
const [exporting, setExporting] = useState(false);
- const [allFilteredRequests, setAllFilteredRequests] = useState([]);
const [backendStats, setBackendStats] = useState(null);
- const [loadingStats, setLoadingStats] = useState(false);
const [departments, setDepartments] = useState([]);
const [loadingDepartments, setLoadingDepartments] = useState(false);
const [allUsers, setAllUsers] = useState>([]);
@@ -82,29 +81,120 @@ export function Requests({ onViewRequest }: RequestsProps) {
});
// Fetch backend stats
- const fetchBackendStats = useCallback(async (statsDateRange?: DateRange, statsStartDate?: Date, statsEndDate?: Date) => {
+ // Stats should reflect filters (priority, department, initiator, approver, search, date range) but NOT status
+ // Status filter should not affect stats - stats should always show all status counts
+ const fetchBackendStats = useCallback(async (
+ statsDateRange?: DateRange,
+ statsStartDate?: Date,
+ statsEndDate?: Date,
+ filtersWithoutStatus?: {
+ priority?: string;
+ department?: string;
+ initiator?: string;
+ approver?: string;
+ approverType?: 'current' | 'any';
+ search?: string;
+ slaCompliance?: string;
+ }
+ ) => {
if (!isOrgLevel) return;
try {
- setLoadingStats(true);
- const stats = await dashboardService.getRequestStats(
- statsDateRange,
- statsStartDate ? statsStartDate.toISOString() : undefined,
- statsEndDate ? statsEndDate.toISOString() : undefined
- );
+ // For dynamic SLA statuses (on_track, approaching, critical), we need to fetch and enrich
+ // because these are calculated dynamically, not stored in DB
+ const slaCompliance = filtersWithoutStatus?.slaCompliance;
+ const isDynamicSlaStatus = slaCompliance &&
+ slaCompliance !== 'all' &&
+ slaCompliance !== 'breached' &&
+ slaCompliance !== 'compliant';
- setBackendStats({
- total: stats.totalRequests || 0,
- pending: stats.openRequests || 0,
- approved: stats.approvedRequests || 0,
- rejected: stats.rejectedRequests || 0,
- draft: stats.draftRequests || 0,
- closed: 0
- });
+ if (isDynamicSlaStatus) {
+ // Fetch a larger sample of requests, enrich them, filter by SLA, then calculate stats
+ const backendFilters: any = {};
+ if (filtersWithoutStatus?.search) backendFilters.search = filtersWithoutStatus.search;
+ if (filtersWithoutStatus?.priority && filtersWithoutStatus.priority !== 'all') {
+ backendFilters.priority = filtersWithoutStatus.priority;
+ }
+ if (filtersWithoutStatus?.department && filtersWithoutStatus.department !== 'all') {
+ backendFilters.department = filtersWithoutStatus.department;
+ }
+ if (filtersWithoutStatus?.initiator && filtersWithoutStatus.initiator !== 'all') {
+ backendFilters.initiator = filtersWithoutStatus.initiator;
+ }
+ if (filtersWithoutStatus?.approver && filtersWithoutStatus.approver !== 'all') {
+ backendFilters.approver = filtersWithoutStatus.approver;
+ backendFilters.approverType = filtersWithoutStatus.approverType || 'current';
+ }
+ backendFilters.slaCompliance = slaCompliance; // Include SLA filter - backend will enrich and filter
+ if (statsDateRange) backendFilters.dateRange = statsDateRange;
+ if (statsStartDate) backendFilters.startDate = statsStartDate.toISOString();
+ if (statsEndDate) backendFilters.endDate = statsEndDate.toISOString();
+
+ // Fetch up to 1000 requests (backend will enrich and filter by SLA)
+ const result = await workflowApi.listWorkflows({
+ page: 1,
+ limit: 1000,
+ ...backendFilters
+ });
+
+ const filteredData = Array.isArray(result?.data) ? result.data : [];
+
+ // Calculate stats from filtered data
+ const total = filteredData.length;
+ const pending = filteredData.filter((r: any) => {
+ const status = (r.status || '').toString().toUpperCase();
+ return status === 'PENDING' || status === 'IN_PROGRESS';
+ }).length;
+ const approved = filteredData.filter((r: any) => {
+ const status = (r.status || '').toString().toUpperCase();
+ return status === 'APPROVED';
+ }).length;
+ const rejected = filteredData.filter((r: any) => {
+ const status = (r.status || '').toString().toUpperCase();
+ return status === 'REJECTED';
+ }).length;
+ const closed = filteredData.filter((r: any) => {
+ const status = (r.status || '').toString().toUpperCase();
+ return status === 'CLOSED';
+ }).length;
+
+ setBackendStats({
+ total,
+ pending,
+ approved,
+ rejected,
+ draft: 0, // Drafts are excluded
+ closed
+ });
+ } else {
+ // For breached/compliant or no SLA filter, use dashboard stats API
+ const stats = await dashboardService.getRequestStats(
+ statsDateRange,
+ statsStartDate ? statsStartDate.toISOString() : undefined,
+ statsEndDate ? statsEndDate.toISOString() : undefined,
+ filtersWithoutStatus?.priority,
+ filtersWithoutStatus?.department,
+ filtersWithoutStatus?.initiator,
+ filtersWithoutStatus?.approver,
+ filtersWithoutStatus?.approverType,
+ filtersWithoutStatus?.search,
+ filtersWithoutStatus?.slaCompliance
+ );
+
+ setBackendStats({
+ total: stats.totalRequests || 0,
+ pending: stats.openRequests || 0,
+ approved: stats.approvedRequests || 0,
+ rejected: stats.rejectedRequests || 0,
+ draft: stats.draftRequests || 0,
+ closed: stats.closedRequests || 0
+ });
+ }
} catch (error) {
+ console.error('Failed to fetch backend stats:', error);
// Keep previous stats on error
} finally {
- setLoadingStats(false);
+ // Stats loading removed - no longer needed
}
}, [isOrgLevel]);
@@ -152,7 +242,6 @@ export function Requests({ onViewRequest }: RequestsProps) {
if (page === 1) {
setLoading(true);
setApiRequests([]);
- setAllFilteredRequests([]);
}
const filterOptions = filtersRef.current.getFilters();
@@ -163,25 +252,15 @@ export function Requests({ onViewRequest }: RequestsProps) {
isOrgLevel
});
- setApiRequests(result.data);
- setAllFilteredRequests(result.filteredData);
+ setApiRequests(result.data); // Paginated data WITH status filter (for list display)
+ // Note: Stats come from backend stats API (always unfiltered), not from allData
// Update pagination
setCurrentPage(result.pagination.page);
setTotalPages(result.pagination.totalPages);
setTotalRecords(result.pagination.total);
- // Fetch backend stats for org-level
- if (isOrgLevel) {
- const closedCount = result.allData.filter((req: any) => {
- const reqStatus = (req.status || '').toString().toUpperCase();
- return reqStatus === 'CLOSED';
- }).length;
-
- fetchBackendStatsRef.current(filterOptions.dateRange, filterOptions.startDate, filterOptions.endDate).then(() => {
- setBackendStats(prev => prev ? { ...prev, closed: closedCount } : null);
- });
- }
+ // Stats are fetched separately via useEffect when filters change
} catch (error) {
setApiRequests([]);
} finally {
@@ -209,13 +288,47 @@ export function Requests({ onViewRequest }: RequestsProps) {
fetchUsers();
}, [fetchDepartments, fetchUsers]);
- // Fetch backend stats when date range changes
+ // Fetch backend stats when filters change (excluding status)
+ // Stats should reflect priority, department, initiator, approver, search, and date range filters
+ // But NOT status filter - stats should always show all status counts
+ // Total changes when other filters are applied, but stays stable when only status changes
useEffect(() => {
if (isOrgLevel) {
- fetchBackendStatsRef.current(filters.dateRange, filters.customStartDate, filters.customEndDate);
+ const timeoutId = setTimeout(() => {
+ const filtersWithoutStatus = {
+ priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
+ department: filters.departmentFilter !== 'all' ? filters.departmentFilter : undefined,
+ initiator: filters.initiatorFilter !== 'all' ? filters.initiatorFilter : undefined,
+ approver: filters.approverFilter !== 'all' ? filters.approverFilter : undefined,
+ approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined,
+ search: filters.searchTerm || undefined,
+ slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined
+ };
+ fetchBackendStatsRef.current(
+ filters.dateRange,
+ filters.customStartDate,
+ filters.customEndDate,
+ filtersWithoutStatus
+ );
+ }, filters.searchTerm ? 500 : 0);
+
+ return () => clearTimeout(timeoutId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isOrgLevel, filters.dateRange, filters.customStartDate, filters.customEndDate]);
+ }, [
+ isOrgLevel,
+ filters.dateRange,
+ filters.customStartDate,
+ filters.customEndDate,
+ filters.priorityFilter,
+ filters.departmentFilter,
+ filters.initiatorFilter,
+ filters.approverFilter,
+ filters.approverFilterType,
+ filters.searchTerm,
+ filters.slaComplianceFilter
+ // Note: statusFilter is NOT in dependencies - stats don't change when only status changes
+ ]);
// Fetch requests on mount and when filters change
useEffect(() => {
@@ -252,34 +365,52 @@ export function Requests({ onViewRequest }: RequestsProps) {
// Transform requests
const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
- // Calculate stats
+ // Calculate stats - Always use backend stats API for overall counts (unfiltered)
+ // Stats should always show total counts regardless of any filters applied
const stats = useMemo(() => {
+ // For org-level: Use backend stats API (always unfiltered)
+ if (isOrgLevel && backendStats) {
+ return {
+ total: backendStats.total || 0,
+ pending: backendStats.pending || 0,
+ approved: backendStats.approved || 0,
+ rejected: backendStats.rejected || 0,
+ draft: backendStats.draft || 0,
+ closed: backendStats.closed || 0
+ };
+ }
+
+ // Fallback: Calculate from paginated data (less accurate, but better than nothing)
+ // This is for user-level where backend stats might not be available
return calculateStatsFromFilteredData(
- allFilteredRequests,
+ [], // Empty - we'll use backendStats or fallback
isOrgLevel,
backendStats,
- filters.hasActiveFilters,
+ false, // No filters for stats - always show overall
totalRecords,
convertedRequests
);
- }, [allFilteredRequests, isOrgLevel, backendStats, filters.hasActiveFilters, totalRecords, convertedRequests]);
+ }, [isOrgLevel, backendStats, totalRecords, convertedRequests]);
- const totalRequests = isOrgLevel && backendStats ? backendStats.total : (totalRecords || convertedRequests.length);
+ // Removed totalRequests - no longer displayed in header (shown in stat cards instead)
return (
{/* Header */}
{/* Stats */}
-
+ {
+ filters.setStatusFilter(status);
+ }}
+ />
{/* Filters - TODO: Extract to separate component */}
@@ -324,7 +455,6 @@ export function Requests({ onViewRequest }: RequestsProps) {
All Status
- Draft
Pending
Approved
Rejected
diff --git a/src/pages/Requests/UserAllRequests.tsx b/src/pages/Requests/UserAllRequests.tsx
new file mode 100644
index 0000000..015bb5e
--- /dev/null
+++ b/src/pages/Requests/UserAllRequests.tsx
@@ -0,0 +1,684 @@
+/**
+ * User All Requests Page - For Regular Users
+ *
+ * This is a SEPARATE screen for regular users' "All Requests" page.
+ * Shows only requests where the user is a participant (approver/spectator), NOT initiator.
+ * Completely separate from AdminAllRequests to avoid interference.
+ */
+
+import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
+import { Pagination } from '@/components/common/Pagination';
+import dashboardService from '@/services/dashboard.service';
+import userApi from '@/services/userApi';
+
+// Components
+import { RequestsHeader } from './components/RequestsHeader';
+import { RequestsStats } from './components/RequestsStats';
+import { RequestsList } from './components/RequestsList';
+
+// Hooks
+import { useRequestsFilters } from './hooks/useRequestsFilters';
+import { useUserSearch } from './hooks/useUserSearch';
+
+// Utils
+import { transformRequests } from './utils/requestTransformers';
+import { exportRequestsToCSV } from './utils/csvExports';
+
+// Services
+import { fetchUserParticipantRequestsData, fetchAllRequestsForExport } from './services/userRequestsService';
+
+// Types
+import type { RequestsProps } from './types/requests.types';
+
+// Filter UI components
+import { Card, CardContent } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
+import { format } from 'date-fns';
+
+export function UserAllRequests({ onViewRequest }: RequestsProps) {
+ // Filters hook
+ const filters = useRequestsFilters();
+
+ // State
+ const [apiRequests, setApiRequests] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [exporting, setExporting] = useState(false);
+ const [allRequestsForStats, setAllRequestsForStats] = useState([]); // All requests without status filter for stats
+ const [departments, setDepartments] = useState([]);
+ const [loadingDepartments, setLoadingDepartments] = useState(false);
+ const [allUsers, setAllUsers] = useState>([]);
+
+ // Pagination
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const [totalRecordsForStats, setTotalRecordsForStats] = useState(0); // For stats (unfiltered - stable)
+ const [itemsPerPage] = useState(10);
+
+ // User search hooks
+ const initiatorSearch = useUserSearch({
+ allUsers,
+ filterValue: filters.initiatorFilter,
+ onFilterChange: filters.setInitiatorFilter
+ });
+
+ const approverSearch = useUserSearch({
+ allUsers,
+ filterValue: filters.approverFilter,
+ onFilterChange: filters.setApproverFilter
+ });
+
+ // Fetch all requests for stats calculation
+ // Apply all filters EXCEPT status filter - this way:
+ // - Total changes when priority/department/SLA/etc. filters are applied
+ // - Total remains stable when only status filter is applied
+ const fetchAllRequestsForStats = useCallback(async () => {
+ try {
+ // Get current filters directly from the filters object (not ref) to ensure we have latest values
+ const filterOptions = filters.getFilters();
+
+ // Build filters WITHOUT status filter for stats
+ // This ensures total changes with other filters (including SLA) but stays stable with status filter
+ const statsFilters = { ...filterOptions };
+ delete statsFilters.status; // Remove status filter to get all statuses
+
+ // Fetch first page with a larger limit to get more data for stats
+ const result = await fetchUserParticipantRequestsData({
+ page: 1,
+ itemsPerPage: 100, // Fetch more data for accurate stats
+ filters: statsFilters // Apply all filters except status (includes SLA, priority, department, etc.)
+ });
+
+ setAllRequestsForStats(result.data || []);
+ // Update totalRecordsForStats from this fetch (with filters except status)
+ // This total will change when other filters (including SLA) are applied, but stay stable when only status changes
+ if (result.pagination?.total !== undefined) {
+ setTotalRecordsForStats(result.pagination.total);
+ }
+ } catch (error) {
+ console.error('Failed to fetch requests for stats:', error);
+ setAllRequestsForStats([]);
+ }
+ }, [filters]);
+
+ // Fetch departments
+ const fetchDepartments = useCallback(async () => {
+ try {
+ setLoadingDepartments(true);
+ const depts = await dashboardService.getDepartments();
+ setDepartments(depts);
+ } catch (error) {
+ // Leave departments empty on error
+ } finally {
+ setLoadingDepartments(false);
+ }
+ }, []);
+
+ // Fetch users
+ const fetchUsers = useCallback(async () => {
+ try {
+ const usersData = await userApi.getAllUsers();
+ const usersList = usersData.map((user: any) => ({
+ userId: user.userId,
+ email: user.email,
+ displayName: user.displayName || user.email
+ }));
+ setAllUsers(usersList);
+ } catch (error) {
+ console.error('Failed to fetch users:', error);
+ }
+ }, []);
+
+ // Use refs to store stable callbacks to prevent infinite loops
+ const filtersRef = useRef(filters);
+ const fetchAllRequestsForStatsRef = useRef(fetchAllRequestsForStats);
+
+ // Update refs on each render
+ useEffect(() => {
+ filtersRef.current = filters;
+ fetchAllRequestsForStatsRef.current = fetchAllRequestsForStats;
+ }, [filters, fetchAllRequestsForStats]);
+
+ // Fetch requests
+ const fetchRequests = useCallback(async (page: number = 1) => {
+ try {
+ if (page === 1) {
+ setLoading(true);
+ setApiRequests([]);
+ }
+
+ const filterOptions = filtersRef.current.getFilters();
+ const result = await fetchUserParticipantRequestsData({
+ page,
+ itemsPerPage,
+ filters: filterOptions
+ });
+
+ setApiRequests(result.data); // Paginated data WITH status filter (for list display)
+
+ // Update pagination (for list display - includes status filter)
+ setCurrentPage(result.pagination.page);
+ setTotalPages(result.pagination.totalPages);
+ // Don't update totalRecords here - it should come from stats fetch (without status filter)
+ // setTotalRecords(result.pagination.total); // Commented out - use totalRecordsForStats instead
+ } catch (error) {
+ setApiRequests([]);
+ } finally {
+ setLoading(false);
+ }
+ }, [itemsPerPage]);
+
+ // Export to CSV
+ const handleExportToCSV = useCallback(async () => {
+ try {
+ setExporting(true);
+ const allData = await fetchAllRequestsForExport(filters.getFilters());
+ await exportRequestsToCSV(allData, filters.getFilters());
+ } catch (error: any) {
+ console.error('Failed to export requests:', error);
+ alert('Failed to export requests. Please try again.');
+ } finally {
+ setExporting(false);
+ }
+ }, [filters]);
+
+ // Initial fetch
+ useEffect(() => {
+ fetchDepartments();
+ fetchUsers();
+ }, [fetchDepartments, fetchUsers]);
+
+ // Fetch stats when filters change (except status filter)
+ // This ensures total changes with other filters but stays stable with status filter
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ fetchAllRequestsForStats();
+ }, filters.searchTerm ? 500 : 0);
+
+ return () => clearTimeout(timeoutId);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ filters.searchTerm,
+ filters.priorityFilter,
+ filters.slaComplianceFilter,
+ filters.departmentFilter,
+ filters.initiatorFilter,
+ filters.approverFilter,
+ filters.approverFilterType,
+ filters.dateRange,
+ filters.customStartDate,
+ filters.customEndDate
+ // fetchAllRequestsForStats excluded to prevent infinite loops
+ // 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)
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ setCurrentPage(1);
+ fetchRequests(1);
+ }, filters.searchTerm ? 500 : 0);
+
+ return () => clearTimeout(timeoutId);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ filters.searchTerm,
+ filters.statusFilter,
+ filters.priorityFilter,
+ filters.slaComplianceFilter,
+ filters.departmentFilter,
+ filters.initiatorFilter,
+ filters.approverFilter,
+ filters.approverFilterType,
+ filters.dateRange,
+ filters.customStartDate,
+ filters.customEndDate
+ // fetchRequests excluded to prevent infinite loops
+ ]);
+
+ // Page change handler
+ const handlePageChange = useCallback((newPage: number) => {
+ if (newPage >= 1 && newPage <= totalPages) {
+ setCurrentPage(newPage);
+ fetchRequests(newPage);
+ }
+ }, [totalPages, fetchRequests]);
+
+ // Transform requests
+ const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
+
+ // Transform all requests for stats (without status filter)
+ const allConvertedRequestsForStats = useMemo(() => transformRequests(allRequestsForStats), [allRequestsForStats]);
+
+ // Calculate stats from all fetched data (without status filter)
+ const stats = useMemo(() => {
+ // For regular users, calculate stats from allRequestsForStats (fetched without status filter)
+ // Use totalRecords for total (from backend), and calculate individual status counts from fetched data
+ if (allConvertedRequestsForStats.length > 0) {
+ // Calculate individual status counts from all fetched requests
+ const pending = allConvertedRequestsForStats.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'pending' || status === 'in-progress';
+ }).length;
+ const approved = allConvertedRequestsForStats.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'approved';
+ }).length;
+ const rejected = allConvertedRequestsForStats.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'rejected';
+ }).length;
+ const closed = allConvertedRequestsForStats.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'closed';
+ }).length;
+
+ // Use totalRecordsForStats for total - this changes when other filters (priority, department, etc.) are applied
+ // but stays stable when only status filter changes
+ return {
+ total: totalRecordsForStats > 0 ? totalRecordsForStats : allConvertedRequestsForStats.length, // Use total from stats fetch (with other filters, without status)
+ pending,
+ approved,
+ rejected,
+ draft: 0, // Drafts are excluded
+ closed
+ };
+ } else {
+ // Fallback: calculate from convertedRequests (current page only) - less accurate
+ const pending = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'pending' || status === 'in-progress';
+ }).length;
+ const approved = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'approved';
+ }).length;
+ const rejected = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'rejected';
+ }).length;
+ const closed = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'closed';
+ }).length;
+
+ return {
+ total: totalRecordsForStats > 0 ? totalRecordsForStats : convertedRequests.length, // Use total from stats fetch if available
+ pending,
+ approved,
+ rejected,
+ draft: 0,
+ closed
+ };
+ }
+ }, [totalRecordsForStats, allConvertedRequestsForStats, convertedRequests]);
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Stats */}
+ {
+ filters.setStatusFilter(status);
+ }}
+ />
+
+ {/* Filters */}
+
+
+
+
+
+
+ Advanced Filters
+ {filters.hasActiveFilters && (
+
+ Active
+
+ )}
+
+ {filters.hasActiveFilters && (
+
+ )}
+
+
+
+
+ {/* Primary Filters */}
+
+
+
+ filters.setSearchTerm(e.target.value)}
+ className="pl-10 h-10"
+ data-testid="search-input"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {/* User Filters - Initiator and Approver */}
+
+ {/* Initiator Filter */}
+
+
+
+ {initiatorSearch.selectedUser ? (
+
+
+ {initiatorSearch.selectedUser.displayName || initiatorSearch.selectedUser.email}
+
+
+
+ ) : (
+ <>
+ initiatorSearch.handleSearch(e.target.value)}
+ onFocus={() => {
+ if (initiatorSearch.searchResults.length > 0) {
+ initiatorSearch.setShowResults(true);
+ }
+ }}
+ onBlur={() => setTimeout(() => initiatorSearch.setShowResults(false), 200)}
+ className="h-10"
+ data-testid="initiator-search-input"
+ />
+ {initiatorSearch.showResults && initiatorSearch.searchResults.length > 0 && (
+
+ {initiatorSearch.searchResults.map((user) => (
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+
+ {/* Approver Filter */}
+
+
+
+ {filters.approverFilter !== 'all' && (
+
+ )}
+
+
+ {approverSearch.selectedUser ? (
+
+
+ {approverSearch.selectedUser.displayName || approverSearch.selectedUser.email}
+
+
+
+ ) : (
+ <>
+ approverSearch.handleSearch(e.target.value)}
+ onFocus={() => {
+ if (approverSearch.searchResults.length > 0) {
+ approverSearch.setShowResults(true);
+ }
+ }}
+ onBlur={() => setTimeout(() => approverSearch.setShowResults(false), 200)}
+ className="h-10"
+ data-testid="approver-search-input"
+ />
+ {approverSearch.showResults && approverSearch.searchResults.length > 0 && (
+
+ {approverSearch.searchResults.map((user) => (
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+
+
+ {/* Date Range Filter */}
+
+
+
+
+ {filters.dateRange === 'custom' && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Requests List */}
+
+
+ {/* Pagination */}
+
+
+ );
+}
+
diff --git a/src/pages/Requests/components/RequestsHeader.tsx b/src/pages/Requests/components/RequestsHeader.tsx
index 1ff0648..b51f3be 100644
--- a/src/pages/Requests/components/RequestsHeader.tsx
+++ b/src/pages/Requests/components/RequestsHeader.tsx
@@ -8,18 +8,14 @@ import { PageHeader } from '@/components/common/PageHeader';
interface RequestsHeaderProps {
isOrgLevel: boolean;
- totalRequests: number;
loading: boolean;
- loadingStats: boolean;
exporting: boolean;
onExport: () => void;
}
export function RequestsHeader({
isOrgLevel,
- totalRequests,
loading,
- loadingStats,
exporting,
onExport
}: RequestsHeaderProps) {
@@ -27,15 +23,10 @@ export function RequestsHeader({
|