pmtool / src /pages /admin /asset-request-management.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
16.6 kB
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 (
<Layout>
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-4" />
<p className="text-muted-foreground">Loading user information...</p>
</div>
</div>
</Layout>
);
}
// Access denied for non-admin users
if (!isAdmin) {
return (
<Layout>
<div className="flex items-center justify-center min-h-[400px]">
<Card className="max-w-md">
<CardContent className="pt-6">
<div className="text-center">
<Shield className="h-12 w-12 text-destructive mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Access Denied</h2>
<p className="text-muted-foreground mb-4">
You do not have permission to access this page. Only administrators can
manage asset requests.
</p>
<Button onClick={() => window.history.back()}>Go Back</Button>
</div>
</CardContent>
</Card>
</div>
</Layout>
);
}
return (
<Layout>
{/* Breadcrumb Navigation */}
<Breadcrumb className="mb-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/dashboard" className="flex items-center gap-1">
<Home className="h-4 w-4" />
<span>Home</span>
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/assets">Asset Management</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Asset Request Management</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 flex items-center gap-2">
<Package className="h-6 w-6 sm:h-8 sm:w-8 shrink-0" aria-hidden="true" />
<span>Asset Request Management</span>
</h1>
<p className="text-muted-foreground mt-1 text-sm sm:text-base">
Review, approve, and assign assets to employee requests
</p>
</div>
{/* Error State */}
{(pendingError || allError) && (
<div className="mb-6">
<ErrorDisplay
error={pendingError || allError}
title="Failed to Load Requests"
onRetry={handleRetry}
variant="card"
/>
</div>
)}
{/* Tabbed Interface */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="pending" className="relative">
Pending Requests
{pendingRequests.length > 0 && (
<span className="ml-2 bg-yellow-500 text-white text-xs rounded-full px-2 py-0.5">
{pendingRequests.length}
</span>
)}
</TabsTrigger>
<TabsTrigger value="all">All Requests</TabsTrigger>
<TabsTrigger value="assignment" className="relative">
Assignment Queue
{approvedRequests.length > 0 && (
<span className="ml-2 bg-green-500 text-white text-xs rounded-full px-2 py-0.5">
{approvedRequests.length}
</span>
)}
</TabsTrigger>
</TabsList>
{/* Pending Requests Tab */}
<TabsContent value="pending">
<ErrorBoundary onReset={refetchPending}>
{isPendingLoading ? (
<AssetRequestReviewSkeleton />
) : (
<AssetRequestReview
requests={pendingRequests}
isLoading={isPendingLoading}
onApprove={handleApprove}
onReject={handleReject}
onAddComment={handleAddComment}
currentUserId={currentUserId}
/>
)}
</ErrorBoundary>
</TabsContent>
{/* All Requests Tab */}
<TabsContent value="all">
<ErrorBoundary onReset={refetchAll}>
{isAllLoading ? (
<AssetRequestDashboardSkeleton />
) : (
<AssetRequestDashboard requests={allRequests} isLoading={isAllLoading} />
)}
</ErrorBoundary>
</TabsContent>
{/* Assignment Queue Tab */}
<TabsContent value="assignment">
<ErrorBoundary onReset={handleRetry}>
{isAssetsLoading || isAssignmentsLoading ? (
<AssetRequestAssignmentSkeleton />
) : (
<AssetRequestAssignment
approvedRequests={approvedRequests}
availableAssets={availableAssets}
allAssets={allAssets}
activeAssignments={allAssignments.filter(a => !a.returnedDate)}
isLoading={isAssetsLoading || isAssignmentsLoading}
onAssign={handleAssign}
currentUserId={currentUserId}
/>
)}
</ErrorBoundary>
</TabsContent>
</Tabs>
</Layout>
);
};
export default AssetRequestManagementPage;