pmtool / src /components /asset-request /AssetRequestReview.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
15.9 kB
import React, { useState, useMemo } from 'react';
import { format } from 'date-fns';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
CheckCircle,
XCircle,
Search,
User,
Package,
Loader2,
ChevronDown,
ChevronUp,
AlertTriangle,
} from 'lucide-react';
import {
AssetRequestWithAvailability,
AssetRequestApprovalRequest,
AssetRequestRejectionRequest,
} from '@/types/asset-request';
import ActionDialog from './ActionDialog';
import ViewDetailsDialog from './ViewDetailsDialog';
import CommentDialog from './CommentDialog';
interface AssetRequestReviewProps {
requests: AssetRequestWithAvailability[];
isLoading?: boolean;
onApprove: (id: number, data: AssetRequestApprovalRequest) => Promise<void>;
onReject: (id: number, data: AssetRequestRejectionRequest) => Promise<void>;
onAddComment?: (id: number, comments: string) => Promise<void>;
currentUserId: number;
}
type SortField = 'date' | 'employee' | 'assetType';
type SortOrder = 'asc' | 'desc';
const AssetRequestReview: React.FC<AssetRequestReviewProps> = ({
requests,
isLoading = false,
onApprove,
onReject,
onAddComment,
currentUserId,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState<SortField>('date');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [selectedRequest, setSelectedRequest] = useState<AssetRequestWithAvailability | null>(null);
const [showDetailDialog, setShowDetailDialog] = useState(false);
const [showApproveDialog, setShowApproveDialog] = useState(false);
const [showRejectDialog, setShowRejectDialog] = useState(false);
const [showCommentDialog, setShowCommentDialog] = useState(false);
// Filter and sort requests
const filteredAndSortedRequests = useMemo(() => {
let filtered = [...requests];
// Apply search filter
if (searchTerm) {
const term = searchTerm.toLowerCase();
filtered = filtered.filter(
(req) =>
req.requestID.toString().includes(term) ||
req.employeeName.toLowerCase().includes(term) ||
req.justification.toLowerCase().includes(term) ||
req.requestedAssets.some((asset) =>
asset.assetType.toLowerCase().includes(term)
)
);
}
// Apply sorting
filtered.sort((a, b) => {
let comparison = 0;
switch (sortField) {
case 'date':
comparison =
new Date(a.submittedDate).getTime() - new Date(b.submittedDate).getTime();
break;
case 'employee':
comparison = a.employeeName.localeCompare(b.employeeName);
break;
case 'assetType':
const aTypes = a.requestedAssets.map((asset) => asset.assetType).join(', ');
const bTypes = b.requestedAssets.map((asset) => asset.assetType).join(', ');
comparison = aTypes.localeCompare(bTypes);
break;
}
return sortOrder === 'asc' ? comparison : -comparison;
});
return filtered;
}, [requests, searchTerm, sortField, sortOrder]);
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('asc');
}
};
const handleViewDetails = (request: AssetRequestWithAvailability) => {
setSelectedRequest(request);
setShowDetailDialog(true);
};
const handleApproveClick = (request: AssetRequestWithAvailability) => {
setSelectedRequest(request);
// Use setTimeout to ensure state is set before opening dialog
setTimeout(() => setShowApproveDialog(true), 0);
};
const handleRejectClick = (request: AssetRequestWithAvailability) => {
setSelectedRequest(request);
// Use setTimeout to ensure state is set before opening dialog
setTimeout(() => setShowRejectDialog(true), 0);
};
const handleAddCommentClick = (request: AssetRequestWithAvailability) => {
setSelectedRequest(request);
setTimeout(() => setShowCommentDialog(true), 0);
};
const handleApproveConfirm = async (comments: string) => {
if (!selectedRequest) return;
await onApprove(selectedRequest.requestID, {
adminComments: comments,
reviewedBy: currentUserId,
});
};
const handleRejectConfirm = async (comments: string) => {
if (!selectedRequest) return;
await onReject(selectedRequest.requestID, {
adminComments: comments,
reviewedBy: currentUserId,
});
};
const handleCommentConfirm = async (comments: string) => {
if (!selectedRequest || !onAddComment) return;
await onAddComment(selectedRequest.requestID, comments);
};
const handleApproveDialogClose = () => {
setShowApproveDialog(false);
setSelectedRequest(null);
};
const handleRejectDialogClose = () => {
setShowRejectDialog(false);
setSelectedRequest(null);
};
const handleCommentDialogClose = () => {
setShowCommentDialog(false);
setSelectedRequest(null);
};
const getAvailabilityBadge = (available: number, requested: number, showWarning: boolean = false) => {
if (available >= requested) {
return (
<div className="flex items-center gap-1">
<Badge className="bg-green-100 text-green-800 border-green-200">
Available ({available})
</Badge>
{showWarning && available < requested * 2 && (
<div className="relative group">
<AlertTriangle className="h-4 w-4 text-yellow-600" />
<span className="absolute hidden group-hover:block bg-gray-900 text-white text-xs rounded px-2 py-1 -top-8 left-1/2 transform -translate-x-1/2 whitespace-nowrap">
Low stock warning
</span>
</div>
)}
</div>
);
} else if (available > 0) {
return (
<Badge className="bg-yellow-100 text-yellow-800 border-yellow-200">
<AlertTriangle className="h-3 w-3 mr-1 inline" />
Partial ({available}/{requested})
</Badge>
);
} else {
return (
<Badge className="bg-red-100 text-red-800 border-red-200">
<XCircle className="h-3 w-3 mr-1 inline" />
Unavailable
</Badge>
);
}
};
const SortIcon: React.FC<{ field: SortField }> = ({ field }) => {
if (sortField !== field) return null;
return sortOrder === 'asc' ? (
<ChevronUp className="h-4 w-4 inline ml-1" />
) : (
<ChevronDown className="h-4 w-4 inline ml-1" />
);
};
if (isLoading) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
{/* Search and Sort Controls */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Pending Asset Requests</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by ID, employee, asset type, or justification..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select
value={sortField}
onValueChange={(value) => setSortField(value as SortField)}
>
<SelectTrigger className="w-full sm:w-48">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="date">Date</SelectItem>
<SelectItem value="employee">Employee</SelectItem>
<SelectItem value="assetType">Asset Type</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Requests Table */}
{filteredAndSortedRequests.length === 0 ? (
<Card>
<CardContent 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">
{searchTerm
? 'No requests match your search criteria'
: 'No pending asset requests'}
</p>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('date')}
>
Date <SortIcon field="date" />
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('employee')}
>
Employee <SortIcon field="employee" />
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('assetType')}
>
Assets <SortIcon field="assetType" />
</TableHead>
<TableHead>Availability</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAndSortedRequests.map((request) => (
<TableRow key={request.requestID}>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">#{request.requestID}</span>
<span className="text-sm text-muted-foreground">
{format(new Date(request.submittedDate), 'MMM dd, yyyy')}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{request.employeeName}</span>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
{request.requestedAssets.map((asset, idx) => (
<div key={idx} className="text-sm">
{asset.assetType} (x{asset.quantity})
</div>
))}
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
{request.availabilityStatus.map((status, idx) => (
<div key={idx}>
{getAvailabilityBadge(status.available, status.requested, true)}
</div>
))}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleViewDetails(request)}
>
View
</Button>
<Button
variant="default"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleApproveClick(request);
}}
className="bg-green-600 hover:bg-green-700"
>
<CheckCircle className="h-4 w-4 mr-1" />
Approve
</Button>
<Button
variant="destructive"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleRejectClick(request);
}}
>
<XCircle className="h-4 w-4 mr-1" />
Reject
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)}
{/* View Details Dialog */}
<ViewDetailsDialog
isOpen={showDetailDialog}
onClose={() => setShowDetailDialog(false)}
request={selectedRequest}
onApprove={handleApproveClick}
onReject={handleRejectClick}
onAddComment={onAddComment ? handleAddCommentClick : undefined}
/>
{/* Approve Dialog */}
{selectedRequest && (
<ActionDialog
isOpen={showApproveDialog}
onClose={handleApproveDialogClose}
onConfirm={handleApproveConfirm}
title="Approve Asset Request"
description={`Approve request #${selectedRequest.requestID} from ${selectedRequest.employeeName}`}
actionType="approve"
requestId={selectedRequest.requestID}
employeeName={selectedRequest.employeeName}
/>
)}
{/* Reject Dialog */}
{selectedRequest && (
<ActionDialog
isOpen={showRejectDialog}
onClose={handleRejectDialogClose}
onConfirm={handleRejectConfirm}
title="Reject Asset Request"
description={`Reject request #${selectedRequest.requestID} from ${selectedRequest.employeeName}`}
actionType="reject"
requestId={selectedRequest.requestID}
employeeName={selectedRequest.employeeName}
/>
)}
{/* Comment Dialog */}
{selectedRequest && onAddComment && (
<CommentDialog
isOpen={showCommentDialog}
onClose={handleCommentDialogClose}
onConfirm={handleCommentConfirm}
requestId={selectedRequest.requestID}
employeeName={selectedRequest.employeeName}
/>
)}
</div>
);
};
export default AssetRequestReview;