import { useEffect, useState, useCallback, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, X, CheckCircle, XCircle } from 'lucide-react'; import workflowApi from '@/services/workflowApi'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; interface Request { id: string; title: string; description: string; status: 'pending' | 'approved' | 'rejected' | 'closed'; priority: 'express' | 'standard'; initiator: { name: string; avatar: string }; currentApprover?: { name: string; avatar: string; sla?: any; // Backend-calculated SLA data }; createdAt: string; approvalStep?: string; department?: string; currentLevelSLA?: any; // Backend-provided SLA for current level } interface OpenRequestsProps { onViewRequest?: (requestId: string, requestTitle?: string) => void; } // Utility functions const getPriorityConfig = (priority: string) => { switch (priority) { case 'express': return { color: 'bg-red-100 text-red-800 border-red-200', icon: Flame, iconColor: 'text-red-600' }; case 'standard': return { color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Target, iconColor: 'text-blue-600' }; default: return { color: 'bg-gray-100 text-gray-800 border-gray-200', icon: Target, iconColor: 'text-gray-600' }; } }; const getStatusConfig = (status: string) => { switch (status) { case 'pending': return { color: 'bg-yellow-100 text-yellow-800 border-yellow-200', icon: Clock, iconColor: 'text-yellow-600', label: 'Pending' }; case 'approved': return { color: 'bg-green-100 text-green-800 border-green-200', icon: AlertCircle, iconColor: 'text-green-600', label: 'Needs Closure' }; case 'rejected': return { color: 'bg-red-100 text-red-800 border-red-200', icon: XCircle, iconColor: 'text-red-600', label: 'Rejected' }; case 'closed': return { color: 'bg-gray-100 text-gray-800 border-gray-200', icon: CheckCircle, iconColor: 'text-gray-600', label: 'Closed' }; default: return { color: 'bg-gray-100 text-gray-800 border-gray-200', icon: AlertCircle, iconColor: 'text-gray-600', label: status }; } }; // getSLAUrgency removed - now using SLATracker component for real-time SLA display export function OpenRequests({ onViewRequest }: OpenRequestsProps) { const [searchParams] = useSearchParams(); // Initialize filters from URL params const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || ''); const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all'); const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all'); const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>( (searchParams.get('sortBy') as 'created' | 'due' | 'priority' | 'sla') || 'created' ); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>( (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc' ); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); // Pagination states const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalRecords, setTotalRecords] = useState(0); const [itemsPerPage] = useState(10); // Fetch open requests for the current user only (user-scoped, not organization-wide) // Note: This endpoint returns only requests where the user is: // - An approver (with pending/in-progress status) // - A spectator // - An initiator (for approved requests awaiting closure) // This applies to ALL users including regular users, MANAGEMENT, and ADMIN roles // For organization-wide view, users should use the "All Requests" screen (/requests) const fetchRequests = useCallback(async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; sortBy?: string; sortOrder?: string }) => { try { if (page === 1) { setLoading(true); setItems([]); } // Always use user-scoped endpoint (not organization-wide) // Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user) // For organization-wide requests, use the "All Requests" screen (/requests) const result = await workflowApi.listOpenForMe({ page, limit: itemsPerPage, search: filters?.search, status: filters?.status, priority: filters?.priority, sortBy: filters?.sortBy, sortOrder: filters?.sortOrder }); // Extract data - workflowApi now returns { data: [], pagination: {} } const data = Array.isArray((result as any)?.data) ? (result as any).data : []; // Set pagination data const pagination = (result as any)?.pagination; if (pagination) { setCurrentPage(pagination.page || 1); setTotalPages(pagination.totalPages || 1); setTotalRecords(pagination.total || 0); } const mapped: Request[] = data.map((r: any) => { const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at; return { id: r.requestNumber || r.request_number || r.requestId, requestId: r.requestId, displayId: r.requestNumber || r.request_number || r.requestId, title: r.title, description: r.description, status: (r.status || '').toString().toLowerCase().replace('_', '-'), priority: (r.priority || '').toString().toLowerCase(), initiator: { name: (r.initiator?.displayName || r.initiator?.email || '—'), avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()) }, currentApprover: r.currentApprover ? { name: (r.currentApprover.name || r.currentApprover.email || '—'), avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()), sla: r.currentApprover.sla // ← Backend-calculated SLA } : undefined, createdAt: createdAt || '—', approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined, department: r.department, currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA }; }); setItems(mapped); } finally { setLoading(false); setRefreshing(false); } }, [itemsPerPage]); const handleRefresh = () => { setRefreshing(true); fetchRequests(currentPage, { search: searchTerm || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, priority: priorityFilter !== 'all' ? priorityFilter : undefined, sortBy, sortOrder }); }; const handlePageChange = (newPage: number) => { if (newPage >= 1 && newPage <= totalPages) { setCurrentPage(newPage); fetchRequests(newPage, { search: searchTerm || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, priority: priorityFilter !== 'all' ? priorityFilter : undefined, sortBy, sortOrder }); } }; const getPageNumbers = () => { const pages = []; const maxPagesToShow = 5; let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); if (endPage - startPage < maxPagesToShow - 1) { startPage = Math.max(1, endPage - maxPagesToShow + 1); } for (let i = startPage; i <= endPage; i++) { pages.push(i); } return pages; }; // Track if this is the initial mount const isInitialMount = useRef(true); // Initial fetch on mount with URL params useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; fetchRequests(1, { search: searchTerm || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, priority: priorityFilter !== 'all' ? priorityFilter : undefined, sortBy, sortOrder }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only run on mount to use URL params // Fetch when filters or sorting change (with debouncing for search) useEffect(() => { // Skip initial mount to avoid double fetch if (isInitialMount.current) return; // Debounce search: wait 500ms after user stops typing const timeoutId = setTimeout(() => { setCurrentPage(1); // Reset to page 1 when filters change fetchRequests(1, { search: searchTerm || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, priority: priorityFilter !== 'all' ? priorityFilter : undefined, sortBy, sortOrder }); }, searchTerm ? 500 : 0); // Debounce only for search, instant for dropdowns return () => clearTimeout(timeoutId); }, [searchTerm, statusFilter, priorityFilter, sortBy, sortOrder, fetchRequests]); // Backend handles both filtering and sorting - use items directly // No client-side sorting needed anymore const filteredAndSortedRequests = items; const clearFilters = () => { setSearchTerm(''); setPriorityFilter('all'); setStatusFilter('all'); }; const activeFiltersCount = [ searchTerm, priorityFilter !== 'all' ? priorityFilter : null, statusFilter !== 'all' ? statusFilter : null ].filter(Boolean).length; return (
{/* Enhanced Header */}

My Open Requests

Manage and track your active approval requests

{loading ? 'Loading…' : `${totalRecords || items.length} open`} requests
{/* Enhanced Filters Section */}
Filters & Search {activeFiltersCount > 0 && ( {activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active )}
{activeFiltersCount > 0 && ( )}
{/* Primary 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" />
{/* Requests List */}
{filteredAndSortedRequests.map((request) => { const priorityConfig = getPriorityConfig(request.priority); const statusConfig = getStatusConfig(request.status); return ( onViewRequest?.(request.id, request.title)} >
{/* Left: Priority Icon */}
{/* Center: Main Content */}
{/* Header Row */}

{(request as any).displayId || request.id}

{(statusConfig as any).label || request.status} {request.department && ( {request.department} )}
{/* Title */}

{request.title}

{/* SLA Display - Compact Version */} {request.currentLevelSLA && (() => { // Use percentage-based colors to match approver SLA tracker // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) const percentUsed = request.currentLevelSLA.percentageUsed || 0; const getSLAColors = () => { if (percentUsed >= 100) { return { bg: 'bg-red-50 border border-red-200', progress: 'bg-red-600', text: 'text-red-600' }; } else if (percentUsed >= 75) { return { bg: 'bg-orange-50 border border-orange-200', progress: 'bg-orange-500', text: 'text-orange-600' }; } else if (percentUsed >= 50) { return { bg: 'bg-amber-50 border border-amber-200', progress: 'bg-amber-500', text: 'text-amber-600' }; } else { return { bg: 'bg-green-50 border border-green-200', progress: 'bg-green-600', text: 'text-gray-700' }; } }; const colors = getSLAColors(); return (
TAT: {percentUsed}%
{request.currentLevelSLA.elapsedText} = 100 ? 'text-red-600' : percentUsed >= 75 ? 'text-orange-600' : percentUsed >= 50 ? 'text-amber-600' : 'text-gray-700' }`}> {request.currentLevelSLA.remainingText} left
); })()} {/* Metadata Row */}
{request.initiator.avatar} {request.initiator.name}
{request.currentApprover && (
{request.currentApprover.avatar} {request.currentApprover.name}
)} {request.approvalStep && (
{request.approvalStep}
)}
Created: {request.createdAt !== '—' ? formatDateDDMMYYYY(request.createdAt) : '—'}
{/* Right: Arrow */}
); })}
{/* Empty State */} {filteredAndSortedRequests.length === 0 && (

No requests found

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

{activeFiltersCount > 0 && ( )}
)} {/* Pagination Controls */} {totalPages > 1 && !loading && (
Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests
{currentPage > 3 && totalPages > 5 && ( <> ... )} {getPageNumbers().map((pageNum) => ( ))} {currentPage < totalPages - 2 && totalPages > 5 && ( <> ... )}
)}
); }