import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import { useOpenRequestsFilters } from './hooks/useOpenRequestsFilters'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Calendar, Clock, FileText, AlertCircle, ArrowRight, RefreshCw, CheckCircle, XCircle, Lock, Flame, Target } from 'lucide-react'; import workflowApi from '@/services/workflowApi'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { getUserFilterType } from '@/utils/userFilterUtils'; import { getRequestsFilters } from '@/flows'; import { TokenManager } from '@/utils/tokenManager'; interface Request { id: string; title: string; description: string; status: 'pending' | 'approved' | 'rejected' | 'closed' | 'paused'; 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 isPaused?: boolean; // Pause status pauseInfo?: any; // Pause details templateType?: string; // Template type for badge display } 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 [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); // Pagination states (currentPage now in Redux) const [totalPages, setTotalPages] = useState(1); const [totalRecords, setTotalRecords] = useState(0); const [itemsPerPage] = useState(10); const fetchRequestsRef = useRef(null); // Use Redux for filters with callback (persists during navigation) const filters = useOpenRequestsFilters(); // Get user filter type and corresponding filter component (plug-and-play pattern) const userFilterType = useMemo(() => { try { const userData = TokenManager.getUserData(); return getUserFilterType(userData); } catch (error) { console.error('[OpenRequests] Error getting user filter type:', error); return 'STANDARD' as const; } }, []); // Get the appropriate filter component based on user type const RequestsFiltersComponent = useMemo(() => { return getRequestsFilters(userFilterType); }, [userFilterType]); // Determine once - use this throughout instead of checking repeatedly const isDealer = userFilterType === 'DEALER'; // Helper to build filter params for API - excludes dealer-restricted filters // Since we know user type initially, this helper uses that knowledge // Note: This doesn't need useCallback since we'll use it inline in effects to avoid dependency issues const getFilterParams = (includeStatus?: boolean) => { return { search: filters.searchTerm || undefined, // Only include status, priority, and templateType filters if user is not a dealer status: includeStatus && !isDealer && filters.statusFilter !== 'all' ? filters.statusFilter : undefined, priority: !isDealer && filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, templateType: !isDealer && filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, sortBy: filters.sortBy, sortOrder: filters.sortOrder }; }; // 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, filterParams?: { search?: string; status?: string; priority?: string; templateType?: 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: filterParams?.search, status: filterParams?.status, priority: filterParams?.priority, templateType: filterParams?.templateType, sortBy: filterParams?.sortBy, sortOrder: filterParams?.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) { filters.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 templateType: r.templateType || r.template_type, // ← Template type for badge display }; }); setItems(mapped); } finally { setLoading(false); setRefreshing(false); } }, [itemsPerPage, filters]); fetchRequestsRef.current = fetchRequests; const handleRefresh = useCallback(() => { setRefreshing(true); fetchRequests(filters.currentPage, getFilterParams(true)); }, [filters.currentPage, fetchRequests]); const handlePageChange = useCallback((newPage: number) => { if (newPage >= 1 && newPage <= totalPages) { filters.setCurrentPage(newPage); fetchRequests(newPage, getFilterParams(true)); } }, [totalPages, filters, fetchRequests]); const getPageNumbers = () => { const pages = []; const maxPagesToShow = 5; let startPage = Math.max(1, filters.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 initial mount const hasInitialFetchRun = useRef(false); // Initial fetch on mount - use stored page from Redux useEffect(() => { if (!hasInitialFetchRun.current) { hasInitialFetchRun.current = true; const storedPage = filters.currentPage || 1; fetchRequests(storedPage, getFilterParams(true)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only on mount // Track filter changes and refetch useEffect(() => { // Skip until initial fetch has completed if (!hasInitialFetchRun.current) return; // Debounce search const timeoutId = setTimeout(() => { filters.setCurrentPage(1); // Reset to page 1 when filters change fetchRequests(1, getFilterParams(true)); }, filters.searchTerm ? 500 : 0); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.sortBy, filters.sortOrder, isDealer]); // Backend handles both filtering and sorting - use items directly // No client-side sorting needed anymore const filteredAndSortedRequests = items; return (
{/* Enhanced Header */}

My Open Requests

Manage and track your active approval requests

{loading ? 'Loading…' : `${totalRecords || items.length} open`} requests
{/* Enhanced Filters Section - Plug-and-play pattern */} {/* 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} )} {/* Template Type Badge */} {(() => { const templateType = (request as any)?.templateType || (request as any)?.template_type || ''; const templateTypeUpper = templateType?.toUpperCase() || ''; // Direct mapping from templateType let templateLabel = 'Custom'; let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200'; if (templateTypeUpper === 'DEALER CLAIM') { templateLabel = 'Dealer Claim'; templateColor = 'bg-blue-100 !text-blue-700 border-blue-200'; } else if (templateTypeUpper === 'TEMPLATE') { templateLabel = 'Template'; } return ( ); })()}
{/* Title */}

{request.title}

{/* SLA Display - Compact Version */} {request.currentLevelSLA && (() => { // Check pause status from isPaused field, pauseInfo, OR status field const isPaused = Boolean( request.isPaused || request.pauseInfo?.isPaused || request.status === 'paused' ); // Use percentage-based colors to match approver SLA tracker // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) // Grey: When paused (frozen state) const percentUsed = request.currentLevelSLA.percentageUsed || 0; const getSLAColors = () => { // If paused, always use grey colors (frozen state) if (isPaused) { return { bg: 'bg-gray-100 border border-gray-300', progress: 'bg-gray-500', text: 'text-gray-600', icon: 'text-gray-600' }; } if (percentUsed >= 100) { return { bg: 'bg-red-50 border border-red-200', progress: 'bg-red-600', text: 'text-red-600', icon: 'text-blue-600' }; } else if (percentUsed >= 75) { return { bg: 'bg-orange-50 border border-orange-200', progress: 'bg-orange-500', text: 'text-orange-600', icon: 'text-blue-600' }; } else if (percentUsed >= 50) { return { bg: 'bg-amber-50 border border-amber-200', progress: 'bg-amber-500', text: 'text-amber-600', icon: 'text-blue-600' }; } else { return { bg: 'bg-green-50 border border-green-200', progress: 'bg-green-600', text: 'text-gray-700', icon: 'text-blue-600' }; } }; const colors = getSLAColors(); return (
{isPaused ? ( ) : ( )} TAT: {percentUsed}% {isPaused && '(paused)'}
{request.currentLevelSLA.elapsedText} {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

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

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