Spaces:
Sleeping
Sleeping
| import React, { useState, useMemo } from 'react'; | |
| import { format } from 'date-fns'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Input } from '@/components/ui/input'; | |
| import { Button } from '@/components/ui/button'; | |
| import { | |
| Collapsible, | |
| CollapsibleContent, | |
| CollapsibleTrigger, | |
| } from '@/components/ui/collapsible'; | |
| import { | |
| Clock, | |
| CheckCircle, | |
| XCircle, | |
| Package, | |
| ChevronDown, | |
| ChevronUp, | |
| Search, | |
| FileText, | |
| Calendar, | |
| User, | |
| MessageSquare, | |
| } from 'lucide-react'; | |
| import { | |
| AssetRequest, | |
| RequestStatus, | |
| REQUEST_STATUS_LABELS, | |
| } from '@/types/asset-request'; | |
| interface MyAssetRequestsProps { | |
| requests: AssetRequest[]; | |
| isLoading?: boolean; | |
| onAcknowledge?: (requestId: number) => void; | |
| } | |
| const MyAssetRequests: React.FC<MyAssetRequestsProps> = ({ | |
| requests, | |
| isLoading = false, | |
| onAcknowledge, | |
| }) => { | |
| const [searchTerm, setSearchTerm] = useState(''); | |
| const [expandedRequests, setExpandedRequests] = useState<Set<number>>(new Set()); | |
| const toggleExpanded = (requestID: number) => { | |
| setExpandedRequests((prev) => { | |
| const newSet = new Set(prev); | |
| if (newSet.has(requestID)) { | |
| newSet.delete(requestID); | |
| } else { | |
| newSet.add(requestID); | |
| } | |
| return newSet; | |
| }); | |
| }; | |
| // Filter requests by status | |
| const filterByStatus = (status?: RequestStatus) => { | |
| let filtered = requests; | |
| if (status) { | |
| filtered = filtered.filter((req) => req.status === status); | |
| } | |
| // Apply search filter | |
| if (searchTerm) { | |
| const term = searchTerm.toLowerCase(); | |
| filtered = filtered.filter( | |
| (req) => | |
| req.requestID.toString().includes(term) || | |
| req.justification.toLowerCase().includes(term) || | |
| req.requestedAssets.some((asset) => | |
| asset.assetType.toLowerCase().includes(term) | |
| ) || | |
| (req.adminComments && req.adminComments.toLowerCase().includes(term)) | |
| ); | |
| } | |
| return filtered.sort( | |
| (a, b) => | |
| new Date(b.submittedDate).getTime() - new Date(a.submittedDate).getTime() | |
| ); | |
| }; | |
| const allRequests = useMemo(() => filterByStatus(), [requests, searchTerm]); | |
| const pendingRequests = useMemo(() => filterByStatus('pending'), [requests, searchTerm]); | |
| const approvedRequests = useMemo(() => filterByStatus('approved'), [requests, searchTerm]); | |
| const rejectedRequests = useMemo(() => filterByStatus('rejected'), [requests, searchTerm]); | |
| const assignedRequests = useMemo(() => filterByStatus('assigned'), [requests, searchTerm]); | |
| const acknowledgedRequests = useMemo(() => filterByStatus('acknowledged'), [requests, searchTerm]); | |
| const getStatusIcon = (status: RequestStatus) => { | |
| switch (status) { | |
| case 'pending': | |
| return <Clock className="h-4 w-4" />; | |
| case 'approved': | |
| return <CheckCircle className="h-4 w-4" />; | |
| case 'rejected': | |
| return <XCircle className="h-4 w-4" />; | |
| case 'assigned': | |
| return <Package className="h-4 w-4" />; | |
| case 'acknowledged': | |
| return <CheckCircle className="h-4 w-4" />; | |
| } | |
| }; | |
| const getStatusColor = (status: RequestStatus) => { | |
| switch (status) { | |
| case 'pending': | |
| return 'bg-yellow-100 text-yellow-800 border-yellow-200'; | |
| case 'approved': | |
| return 'bg-green-100 text-green-800 border-green-200'; | |
| case 'rejected': | |
| return 'bg-red-100 text-red-800 border-red-200'; | |
| case 'assigned': | |
| return 'bg-blue-100 text-blue-800 border-blue-200'; | |
| case 'acknowledged': | |
| return 'bg-purple-100 text-purple-800 border-purple-200'; | |
| } | |
| }; | |
| const RequestCard: React.FC<{ request: AssetRequest }> = ({ request }) => { | |
| const isExpanded = expandedRequests.has(request.requestID); | |
| return ( | |
| <Card className="mb-3"> | |
| <Collapsible open={isExpanded} onOpenChange={() => toggleExpanded(request.requestID)}> | |
| <CardHeader className="pb-3"> | |
| <div className="flex items-start justify-between gap-3"> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <CardTitle className="text-base">Request #{request.requestID}</CardTitle> | |
| <Badge | |
| variant="outline" | |
| className={`flex items-center gap-1 ${getStatusColor(request.status)}`} | |
| > | |
| {getStatusIcon(request.status)} | |
| {REQUEST_STATUS_LABELS[request.status]} | |
| </Badge> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground"> | |
| <span className="flex items-center gap-1"> | |
| <Calendar className="h-3 w-3" /> | |
| {format(new Date(request.submittedDate), 'MMM dd, yyyy')} | |
| </span> | |
| <span className="flex items-center gap-1"> | |
| <Package className="h-3 w-3" /> | |
| {request.requestedAssets.length} item(s) | |
| </span> | |
| </div> | |
| </div> | |
| <CollapsibleTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-8 w-8 p-0" | |
| aria-label={isExpanded ? 'Collapse details' : 'Expand details'} | |
| > | |
| {isExpanded ? ( | |
| <ChevronUp className="h-4 w-4" /> | |
| ) : ( | |
| <ChevronDown className="h-4 w-4" /> | |
| )} | |
| </Button> | |
| </CollapsibleTrigger> | |
| </div> | |
| </CardHeader> | |
| <CollapsibleContent> | |
| <CardContent className="pt-0 space-y-4"> | |
| {/* Requested Assets */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-2 flex items-center gap-2"> | |
| <Package className="h-4 w-4" /> | |
| Requested Assets | |
| </h4> | |
| <div className="space-y-2"> | |
| {request.requestedAssets.map((asset, index) => ( | |
| <div | |
| key={index} | |
| className="bg-muted/50 rounded-md p-3 text-sm" | |
| > | |
| <div className="flex items-start justify-between gap-2"> | |
| <div className="flex-1"> | |
| <p className="font-medium">{asset.assetType}</p> | |
| <p className="text-muted-foreground">Quantity: {asset.quantity}</p> | |
| {asset.specifications && ( | |
| <p className="text-muted-foreground mt-1"> | |
| Specs: {asset.specifications} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Justification */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-2 flex items-center gap-2"> | |
| <FileText className="h-4 w-4" /> | |
| Justification | |
| </h4> | |
| <p className="text-sm text-muted-foreground bg-muted/50 rounded-md p-3"> | |
| {request.justification} | |
| </p> | |
| </div> | |
| {/* Review Information */} | |
| {(request.reviewedDate || request.adminComments) && ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-2 flex items-center gap-2"> | |
| <User className="h-4 w-4" /> | |
| Review Information | |
| </h4> | |
| <div className="bg-muted/50 rounded-md p-3 space-y-2 text-sm"> | |
| {request.reviewedDate && ( | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-3 w-3 text-muted-foreground" /> | |
| <span className="text-muted-foreground"> | |
| Reviewed on {format(new Date(request.reviewedDate), 'MMM dd, yyyy')} | |
| </span> | |
| </div> | |
| )} | |
| {request.reviewerName && ( | |
| <div className="flex items-center gap-2"> | |
| <User className="h-3 w-3 text-muted-foreground" /> | |
| <span className="text-muted-foreground"> | |
| Reviewed by {request.reviewerName} | |
| </span> | |
| </div> | |
| )} | |
| {request.adminComments && ( | |
| <div className="mt-2"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <MessageSquare className="h-3 w-3 text-muted-foreground" /> | |
| <span className="font-medium">Admin Comments:</span> | |
| </div> | |
| <p className="text-muted-foreground pl-5">{request.adminComments}</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Assignment Information */} | |
| {request.assignedDate && ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-2 flex items-center gap-2"> | |
| <Package className="h-4 w-4" /> | |
| Assignment Information | |
| </h4> | |
| <div className="bg-muted/50 rounded-md p-3 space-y-2 text-sm"> | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-3 w-3 text-muted-foreground" /> | |
| <span className="text-muted-foreground"> | |
| Assigned on {format(new Date(request.assignedDate), 'MMM dd, yyyy')} | |
| </span> | |
| </div> | |
| {request.assignerName && ( | |
| <div className="flex items-center gap-2"> | |
| <User className="h-3 w-3 text-muted-foreground" /> | |
| <span className="text-muted-foreground"> | |
| Assigned by {request.assignerName} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| {/* Acknowledge Button - Only show if status is 'assigned' and not yet acknowledged */} | |
| {request.status === 'assigned' && !request.acknowledgedDate && onAcknowledge && ( | |
| <div className="mt-3"> | |
| <Button | |
| onClick={() => onAcknowledge(request.requestID)} | |
| className="w-full bg-purple-600 hover:bg-purple-700 text-white" | |
| size="sm" | |
| > | |
| <CheckCircle className="h-4 w-4 mr-2" /> | |
| Acknowledge Receipt | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Acknowledgement Information */} | |
| {request.acknowledgedDate && ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-2 flex items-center gap-2"> | |
| <CheckCircle className="h-4 w-4" /> | |
| Acknowledgement Information | |
| </h4> | |
| <div className="bg-muted/50 rounded-md p-3 space-y-2 text-sm"> | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-3 w-3 text-muted-foreground" /> | |
| <span className="text-muted-foreground"> | |
| Acknowledged on {format(new Date(request.acknowledgedDate), 'MMM dd, yyyy')} | |
| </span> | |
| </div> | |
| {request.acknowledgementComments && ( | |
| <div className="mt-2"> | |
| <div className="flex items-center gap-2 mb-1"> | |
| <MessageSquare className="h-3 w-3 text-muted-foreground" /> | |
| <span className="font-medium">Comments:</span> | |
| </div> | |
| <p className="text-muted-foreground pl-5">{request.acknowledgementComments}</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| </CollapsibleContent> | |
| </Collapsible> | |
| </Card> | |
| ); | |
| }; | |
| const EmptyState: React.FC<{ message: string }> = ({ message }) => ( | |
| <div className="flex flex-col items-center justify-center py-12 text-center"> | |
| <Package className="h-12 w-12 text-muted-foreground mb-4" /> | |
| <p className="text-muted-foreground">{message}</p> | |
| </div> | |
| ); | |
| const RequestList: React.FC<{ requests: AssetRequest[]; emptyMessage: string }> = ({ | |
| requests, | |
| emptyMessage, | |
| }) => { | |
| if (isLoading) { | |
| return ( | |
| <div className="flex items-center justify-center py-12"> | |
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> | |
| </div> | |
| ); | |
| } | |
| if (requests.length === 0) { | |
| return <EmptyState message={emptyMessage} />; | |
| } | |
| return ( | |
| <div> | |
| {requests.map((request) => ( | |
| <RequestCard key={request.requestID} request={request} /> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <Card className="w-full"> | |
| <CardHeader> | |
| <CardTitle className="text-lg sm:text-xl">My Asset Requests</CardTitle> | |
| <div className="mt-4"> | |
| <div className="relative"> | |
| <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search requests by ID, asset type, or comments..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| className="pl-10" | |
| aria-label="Search asset requests" | |
| /> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <Tabs defaultValue="all" className="w-full"> | |
| <TabsList className="grid w-full grid-cols-6 mb-4"> | |
| <TabsTrigger value="all" className="text-xs sm:text-sm"> | |
| All ({allRequests.length}) | |
| </TabsTrigger> | |
| <TabsTrigger value="pending" className="text-xs sm:text-sm"> | |
| Pending ({pendingRequests.length}) | |
| </TabsTrigger> | |
| <TabsTrigger value="approved" className="text-xs sm:text-sm"> | |
| Approved ({approvedRequests.length}) | |
| </TabsTrigger> | |
| <TabsTrigger value="rejected" className="text-xs sm:text-sm"> | |
| Rejected ({rejectedRequests.length}) | |
| </TabsTrigger> | |
| <TabsTrigger value="assigned" className="text-xs sm:text-sm"> | |
| Assigned ({assignedRequests.length}) | |
| </TabsTrigger> | |
| <TabsTrigger value="acknowledged" className="text-xs sm:text-sm"> | |
| Acknowledged ({acknowledgedRequests.length}) | |
| </TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="all"> | |
| <RequestList | |
| requests={allRequests} | |
| emptyMessage="No asset requests found. Submit your first request above!" | |
| /> | |
| </TabsContent> | |
| <TabsContent value="pending"> | |
| <RequestList | |
| requests={pendingRequests} | |
| emptyMessage="No pending requests. Your submitted requests will appear here." | |
| /> | |
| </TabsContent> | |
| <TabsContent value="approved"> | |
| <RequestList | |
| requests={approvedRequests} | |
| emptyMessage="No approved requests yet." | |
| /> | |
| </TabsContent> | |
| <TabsContent value="rejected"> | |
| <RequestList | |
| requests={rejectedRequests} | |
| emptyMessage="No rejected requests." | |
| /> | |
| </TabsContent> | |
| <TabsContent value="assigned"> | |
| <RequestList | |
| requests={assignedRequests} | |
| emptyMessage="No assigned requests yet. Approved requests will be assigned by administrators." | |
| /> | |
| </TabsContent> | |
| <TabsContent value="acknowledged"> | |
| <RequestList | |
| requests={acknowledgedRequests} | |
| emptyMessage="No acknowledged requests yet. Acknowledge assigned requests to confirm receipt." | |
| /> | |
| </TabsContent> | |
| </Tabs> | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |
| export default MyAssetRequests; | |