pmtool / src /components /asset-request /MyAssetRequests.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
17.3 kB
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;