Spaces:
Sleeping
Sleeping
| 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 { Package, Loader2, AlertCircle, Home } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { | |
| Breadcrumb, | |
| BreadcrumbList, | |
| BreadcrumbItem, | |
| BreadcrumbLink, | |
| BreadcrumbPage, | |
| BreadcrumbSeparator, | |
| } from '@/components/ui/breadcrumb'; | |
| import AssetRequestForm from '@/components/asset-request/AssetRequestForm'; | |
| import MyAssetRequests from '@/components/asset-request/MyAssetRequests'; | |
| import AcknowledgeDialog from '@/components/asset-request/AcknowledgeDialog'; | |
| import ErrorBoundary from '@/components/asset-request/ErrorBoundary'; | |
| import { ErrorDisplay } from '@/components/asset-request/ErrorDisplay'; | |
| import { AssetRequestFormSkeleton, MyAssetRequestsSkeleton } from '@/components/asset-request/LoadingSkeletons'; | |
| import { assetRequestApi } from '@/services/assetRequestApi'; | |
| import { AssetRequestCreateRequest } from '@/types/asset-request'; | |
| import { notifyRequestSubmitted, notifyError } from '@/utils/assetRequestNotifications'; | |
| import { showToast } from '@/components/toast-utils'; | |
| const AssetRequestsPage: React.FC = () => { | |
| const { userData } = useAuth(); | |
| const queryClient = useQueryClient(); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const [showAcknowledgeDialog, setShowAcknowledgeDialog] = useState(false); | |
| const [selectedRequestId, setSelectedRequestId] = useState<number | null>(null); | |
| // Get employee ID from user data | |
| const employeeID = userData?.employeeId || 0; | |
| // Fetch employee's asset requests | |
| const { | |
| data: requests = [], | |
| isLoading, | |
| error, | |
| refetch, | |
| } = useQuery({ | |
| queryKey: ['assetRequests', 'employee', employeeID], | |
| queryFn: () => assetRequestApi.getByEmployeeId(employeeID), | |
| enabled: employeeID > 0, | |
| staleTime: 30000, // 30 seconds | |
| refetchInterval: 60000, // Refresh every minute | |
| }); | |
| // Create asset request mutation with optimistic updates | |
| const createRequestMutation = useMutation({ | |
| mutationFn: (data: AssetRequestCreateRequest) => assetRequestApi.create(data), | |
| onMutate: async (newRequestData) => { | |
| // Cancel any outgoing refetches | |
| await queryClient.cancelQueries({ queryKey: ['assetRequests', 'employee', employeeID] }); | |
| // Snapshot the previous value | |
| const previousRequests = queryClient.getQueryData(['assetRequests', 'employee', employeeID]); | |
| // Optimistically update with a temporary request | |
| const optimisticRequest = { | |
| requestID: Date.now(), // Temporary ID | |
| employeeID: newRequestData.employeeID, | |
| employeeName: userData ? `${userData.firstName} ${userData.lastName}` : '', | |
| requestedAssets: newRequestData.requestedAssets, | |
| justification: newRequestData.justification, | |
| status: 'pending' as const, | |
| submittedDate: new Date().toISOString(), | |
| reviewedDate: null, | |
| reviewedBy: null, | |
| reviewerName: null, | |
| adminComments: null, | |
| assignedDate: null, | |
| assignedBy: null, | |
| assignerName: null, | |
| }; | |
| queryClient.setQueryData( | |
| ['assetRequests', 'employee', employeeID], | |
| (old: any) => [optimisticRequest, ...(old || [])] | |
| ); | |
| return { previousRequests }; | |
| }, | |
| onSuccess: (newRequest) => { | |
| // Invalidate and refetch to get the real data | |
| queryClient.invalidateQueries({ queryKey: ['assetRequests', 'employee', employeeID] }); | |
| queryClient.invalidateQueries({ queryKey: ['assetRequests', 'pending'] }); | |
| // Show success notification | |
| notifyRequestSubmitted(newRequest); | |
| }, | |
| onError: (error: Error, _newRequest, context) => { | |
| // Rollback optimistic update | |
| if (context?.previousRequests) { | |
| queryClient.setQueryData( | |
| ['assetRequests', 'employee', employeeID], | |
| context.previousRequests | |
| ); | |
| } | |
| // Show error notification | |
| notifyError('Submit Request', error.message || 'Failed to submit asset request. Please try again.'); | |
| }, | |
| }); | |
| // Acknowledge mutation | |
| const acknowledgeMutation = useMutation({ | |
| mutationFn: ({ requestId, comments }: { requestId: number; comments: string }) => | |
| assetRequestApi.acknowledge(requestId, { | |
| acknowledgementComments: comments || undefined, | |
| acknowledgedBy: employeeID, | |
| }), | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: ['assetRequests', 'employee', employeeID] }); | |
| showToast('success', 'Asset receipt acknowledged successfully'); | |
| }, | |
| onError: (error: Error) => { | |
| notifyError('Acknowledge Receipt', error.message || 'Failed to acknowledge asset receipt. Please try again.'); | |
| }, | |
| }); | |
| const handleSubmitRequest = async (data: AssetRequestCreateRequest) => { | |
| setIsSubmitting(true); | |
| try { | |
| await createRequestMutation.mutateAsync(data); | |
| } finally { | |
| setIsSubmitting(false); | |
| } | |
| }; | |
| const handleAcknowledge = (requestId: number) => { | |
| setSelectedRequestId(requestId); | |
| setShowAcknowledgeDialog(true); | |
| }; | |
| const handleAcknowledgeConfirm = async (comments: string) => { | |
| if (!selectedRequestId) return; | |
| await acknowledgeMutation.mutateAsync({ requestId: selectedRequestId, comments }); | |
| setSelectedRequestId(null); | |
| }; | |
| const handleRetry = () => { | |
| refetch(); | |
| }; | |
| // 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> | |
| ); | |
| } | |
| // Error state for missing employee ID | |
| if (employeeID === 0) { | |
| 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"> | |
| <AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" /> | |
| <h2 className="text-xl font-semibold mb-2">Employee Profile Not Found</h2> | |
| <p className="text-muted-foreground mb-4"> | |
| Your user account is not linked to an employee profile. Please contact your | |
| administrator to set up your employee profile. | |
| </p> | |
| </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>My Asset Requests</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 Requests</span> | |
| </h1> | |
| <p className="text-muted-foreground mt-1 text-sm sm:text-base"> | |
| Request assets you need and track the status of your requests | |
| </p> | |
| </div> | |
| {/* Error State */} | |
| {error && ( | |
| <div className="mb-6"> | |
| <ErrorDisplay | |
| error={error} | |
| title="Failed to Load Requests" | |
| onRetry={handleRetry} | |
| variant="card" | |
| /> | |
| </div> | |
| )} | |
| {/* Two-Column Layout */} | |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
| {/* Left Column: Request Form */} | |
| <div className="order-2 lg:order-1"> | |
| <ErrorBoundary onReset={() => window.location.reload()}> | |
| {isLoading ? ( | |
| <AssetRequestFormSkeleton /> | |
| ) : ( | |
| <AssetRequestForm | |
| employeeID={employeeID} | |
| onSubmit={handleSubmitRequest} | |
| isSubmitting={isSubmitting} | |
| /> | |
| )} | |
| </ErrorBoundary> | |
| </div> | |
| {/* Right Column: My Requests */} | |
| <div className="order-1 lg:order-2"> | |
| <ErrorBoundary onReset={handleRetry}> | |
| {isLoading ? ( | |
| <MyAssetRequestsSkeleton /> | |
| ) : ( | |
| <MyAssetRequests | |
| requests={requests} | |
| isLoading={isLoading} | |
| onAcknowledge={handleAcknowledge} | |
| /> | |
| )} | |
| </ErrorBoundary> | |
| </div> | |
| </div> | |
| {/* Acknowledge Dialog */} | |
| {selectedRequestId && ( | |
| <AcknowledgeDialog | |
| isOpen={showAcknowledgeDialog} | |
| onClose={() => { | |
| setShowAcknowledgeDialog(false); | |
| setSelectedRequestId(null); | |
| }} | |
| onConfirm={handleAcknowledgeConfirm} | |
| requestId={selectedRequestId} | |
| employeeName={userData?.firstName + ' ' + userData?.lastName || ''} | |
| /> | |
| )} | |
| </Layout> | |
| ); | |
| }; | |
| export default AssetRequestsPage; | |