import React, { useState } from 'react'; import { useAuth } from '@/lib/auth-context'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import Layout from '@/components/layout/layout'; import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Package, Loader2, AlertCircle, Shield, Home } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import AssetRequestReview from '@/components/asset-request/AssetRequestReview'; import AssetRequestAssignment from '@/components/asset-request/AssetRequestAssignment'; import AssetRequestDashboard from '@/components/asset-request/AssetRequestDashboard'; import ErrorBoundary from '@/components/asset-request/ErrorBoundary'; import { ErrorDisplay } from '@/components/asset-request/ErrorDisplay'; import { AssetRequestReviewSkeleton, AssetRequestDashboardSkeleton, AssetRequestAssignmentSkeleton, } from '@/components/asset-request/LoadingSkeletons'; import { assetRequestApi } from '@/services/assetRequestApi'; import { assetApi } from '@/services/assetApi'; import { assetAssignmentApi } from '@/services/assetAssignmentApi'; import { AssetRequestApprovalRequest, AssetRequestRejectionRequest, AssetRequestAssignmentRequest, } from '@/types/asset-request'; import { notifyAdminApprovalSuccess, notifyAdminRejectionSuccess, notifyAdminAssignmentSuccess, notifyError, } from '@/utils/assetRequestNotifications'; const AssetRequestManagementPage: React.FC = () => { const { userData } = useAuth(); const queryClient = useQueryClient(); const [activeTab, setActiveTab] = useState('pending'); // Check if user is admin const isAdmin = userData?.roleId === 1 || userData?.roleName?.toLowerCase() === 'admin'; // Get current user ID const currentUserId = userData?.employeeId || 0; // Fetch pending requests with availability const { data: pendingRequests = [], isLoading: isPendingLoading, error: pendingError, refetch: refetchPending, } = useQuery({ queryKey: ['assetRequests', 'pending'], queryFn: () => assetRequestApi.getPending(), enabled: isAdmin, staleTime: 30000, // 30 seconds refetchInterval: 60000, // Refresh every minute }); // Fetch all requests for dashboard const { data: allRequests = [], isLoading: isAllLoading, error: allError, refetch: refetchAll, } = useQuery({ queryKey: ['assetRequests', 'all'], queryFn: () => assetRequestApi.getAll(), enabled: isAdmin, staleTime: 30000, refetchInterval: 60000, }); // Fetch approved requests (for assignment queue) const approvedRequests = allRequests.filter((req) => req.status === 'approved'); // Fetch available assets const { data: allAssets = [], isLoading: isAssetsLoading, } = useQuery({ queryKey: ['assets', 'all'], queryFn: () => assetApi.getAll(), enabled: isAdmin, staleTime: 30000, }); // Fetch all assignments to determine available assets const { data: allAssignments = [], isLoading: isAssignmentsLoading, } = useQuery({ queryKey: ['assetAssignments', 'all'], queryFn: () => assetAssignmentApi.getAll(), enabled: isAdmin, staleTime: 30000, }); // Filter available assets (not currently assigned) const availableAssets = allAssets.filter((asset) => { const activeAssignment = allAssignments.find( (assignment) => assignment.assetID === asset.assetID && !assignment.returnedDate ); return !activeAssignment; }); // Approve request mutation with optimistic updates const approveMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: AssetRequestApprovalRequest }) => assetRequestApi.approve(id, data), onMutate: async ({ id, data }) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['assetRequests'] }); // Snapshot previous values const previousPending = queryClient.getQueryData(['assetRequests', 'pending']); const previousAll = queryClient.getQueryData(['assetRequests', 'all']); // Optimistically update const updateRequest = (old: any) => { if (!old) return old; return old.map((req: any) => req.requestID === id ? { ...req, status: 'approved', reviewedDate: new Date().toISOString(), reviewedBy: data.reviewedBy, reviewerName: userData ? `${userData.firstName} ${userData.lastName}` : '', adminComments: data.adminComments, } : req ); }; queryClient.setQueryData(['assetRequests', 'pending'], (old: any) => old ? old.filter((req: any) => req.requestID !== id) : old ); queryClient.setQueryData(['assetRequests', 'all'], updateRequest); return { previousPending, previousAll }; }, onSuccess: (updatedRequest) => { queryClient.invalidateQueries({ queryKey: ['assetRequests'] }); notifyAdminApprovalSuccess(updatedRequest); }, onError: (error: Error, _variables, context) => { // Rollback on error if (context?.previousPending) { queryClient.setQueryData(['assetRequests', 'pending'], context.previousPending); } if (context?.previousAll) { queryClient.setQueryData(['assetRequests', 'all'], context.previousAll); } notifyError('Approve Request', error.message || 'Failed to approve the request. Please try again.'); }, }); // Reject request mutation with optimistic updates const rejectMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: AssetRequestRejectionRequest }) => assetRequestApi.reject(id, data), onMutate: async ({ id, data }) => { await queryClient.cancelQueries({ queryKey: ['assetRequests'] }); const previousPending = queryClient.getQueryData(['assetRequests', 'pending']); const previousAll = queryClient.getQueryData(['assetRequests', 'all']); const updateRequest = (old: any) => { if (!old) return old; return old.map((req: any) => req.requestID === id ? { ...req, status: 'rejected', reviewedDate: new Date().toISOString(), reviewedBy: data.reviewedBy, reviewerName: userData ? `${userData.firstName} ${userData.lastName}` : '', adminComments: data.adminComments, } : req ); }; queryClient.setQueryData(['assetRequests', 'pending'], (old: any) => old ? old.filter((req: any) => req.requestID !== id) : old ); queryClient.setQueryData(['assetRequests', 'all'], updateRequest); return { previousPending, previousAll }; }, onSuccess: (updatedRequest) => { queryClient.invalidateQueries({ queryKey: ['assetRequests'] }); notifyAdminRejectionSuccess(updatedRequest); }, onError: (error: Error, _variables, context) => { if (context?.previousPending) { queryClient.setQueryData(['assetRequests', 'pending'], context.previousPending); } if (context?.previousAll) { queryClient.setQueryData(['assetRequests', 'all'], context.previousAll); } notifyError('Reject Request', error.message || 'Failed to reject the request. Please try again.'); }, }); // Add comment mutation const addCommentMutation = useMutation({ mutationFn: ({ id, comments }: { id: number; comments: string }) => assetRequestApi.addComment(id, { adminComments: comments, commentedBy: currentUserId, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['assetRequests'] }); notifyAdminApprovalSuccess({ requestID: 0 } as any); // Use generic success toast }, onError: (error: Error) => { notifyError('Add Comment', error.message || 'Failed to add comment. Please try again.'); }, }); // Assign assets mutation with optimistic updates const assignMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: AssetRequestAssignmentRequest }) => assetRequestApi.assign(id, data), onMutate: async ({ id, data }) => { await queryClient.cancelQueries({ queryKey: ['assetRequests'] }); await queryClient.cancelQueries({ queryKey: ['assets'] }); await queryClient.cancelQueries({ queryKey: ['assetAssignments'] }); const previousAll = queryClient.getQueryData(['assetRequests', 'all']); const updateRequest = (old: any) => { if (!old) return old; return old.map((req: any) => req.requestID === id ? { ...req, status: 'assigned', assignedDate: new Date().toISOString(), assignedBy: data.assignedBy, assignerName: userData ? `${userData.firstName} ${userData.lastName}` : '', } : req ); }; queryClient.setQueryData(['assetRequests', 'all'], updateRequest); return { previousAll }; }, onSuccess: (updatedRequest) => { queryClient.invalidateQueries({ queryKey: ['assetRequests'] }); queryClient.invalidateQueries({ queryKey: ['assets'] }); queryClient.invalidateQueries({ queryKey: ['assetAssignments'] }); notifyAdminAssignmentSuccess(updatedRequest); }, onError: (error: Error, _variables, context) => { if (context?.previousAll) { queryClient.setQueryData(['assetRequests', 'all'], context.previousAll); } notifyError('Assign Assets', error.message || 'Failed to assign assets. Please try again.'); }, }); const handleApprove = async (id: number, data: AssetRequestApprovalRequest) => { await approveMutation.mutateAsync({ id, data }); }; const handleReject = async (id: number, data: AssetRequestRejectionRequest) => { await rejectMutation.mutateAsync({ id, data }); }; const handleAddComment = async (id: number, comments: string) => { await addCommentMutation.mutateAsync({ id, comments }); }; const handleAssign = async (id: number, data: AssetRequestAssignmentRequest) => { await assignMutation.mutateAsync({ id, data }); }; const handleRetry = () => { refetchPending(); refetchAll(); }; // Loading state if (!userData) { return (

Loading user information...

); } // Access denied for non-admin users if (!isAdmin) { return (

Access Denied

You do not have permission to access this page. Only administrators can manage asset requests.

); } return ( {/* Breadcrumb Navigation */} Home Asset Management Asset Request Management {/* Page Header */}

Review, approve, and assign assets to employee requests

{/* Error State */} {(pendingError || allError) && (
)} {/* Tabbed Interface */} Pending Requests {pendingRequests.length > 0 && ( {pendingRequests.length} )} All Requests Assignment Queue {approvedRequests.length > 0 && ( {approvedRequests.length} )} {/* Pending Requests Tab */} {isPendingLoading ? ( ) : ( )} {/* All Requests Tab */} {isAllLoading ? ( ) : ( )} {/* Assignment Queue Tab */} {isAssetsLoading || isAssignmentsLoading ? ( ) : ( !a.returnedDate)} isLoading={isAssetsLoading || isAssignmentsLoading} onAssign={handleAssign} currentUserId={currentUserId} /> )}
); }; export default AssetRequestManagementPage;