pmtool / src /components /asset-request /AssetRequestDashboard.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
15.4 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 {
Clock,
CheckCircle,
XCircle,
Package,
Search,
Download,
User,
Calendar,
TrendingUp,
FileText,
Loader2,
} from 'lucide-react';
import {
AssetRequest,
RequestStatus,
REQUEST_STATUS_LABELS,
} from '@/types/asset-request';
interface AssetRequestDashboardProps {
requests: AssetRequest[];
isLoading?: boolean;
}
interface DashboardStats {
total: number;
pending: number;
approved: number;
rejected: number;
assigned: number;
}
const AssetRequestDashboard: React.FC<AssetRequestDashboardProps> = ({
requests,
isLoading = false,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<RequestStatus | 'all'>('all');
const [dateRangeFilter, setDateRangeFilter] = useState<'all' | '7days' | '30days' | '90days'>(
'all'
);
const [employeeFilter, setEmployeeFilter] = useState<string>('all');
// Calculate statistics
const stats: DashboardStats = useMemo(() => {
return {
total: requests.length,
pending: requests.filter((r) => r.status === 'pending').length,
approved: requests.filter((r) => r.status === 'approved').length,
rejected: requests.filter((r) => r.status === 'rejected').length,
assigned: requests.filter((r) => r.status === 'assigned').length,
};
}, [requests]);
// Get unique employees
const employees = useMemo(() => {
const uniqueEmployees = new Map<number, string>();
requests.forEach((req) => {
uniqueEmployees.set(req.employeeID, req.employeeName);
});
return Array.from(uniqueEmployees.entries()).map(([id, name]) => ({ id, name }));
}, [requests]);
// Filter requests
const filteredRequests = useMemo(() => {
let filtered = [...requests];
// Status filter
if (statusFilter !== 'all') {
filtered = filtered.filter((req) => req.status === statusFilter);
}
// Date range filter
if (dateRangeFilter !== 'all') {
const now = new Date();
const daysAgo = {
'7days': 7,
'30days': 30,
'90days': 90,
}[dateRangeFilter];
const cutoffDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
filtered = filtered.filter(
(req) => new Date(req.submittedDate) >= cutoffDate
);
}
// Employee filter
if (employeeFilter !== 'all') {
filtered = filtered.filter((req) => req.employeeID.toString() === employeeFilter);
}
// 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)
) ||
(req.adminComments && req.adminComments.toLowerCase().includes(term))
);
}
// Sort by date (newest first)
return filtered.sort(
(a, b) =>
new Date(b.submittedDate).getTime() - new Date(a.submittedDate).getTime()
);
}, [requests, searchTerm, statusFilter, dateRangeFilter, employeeFilter]);
const handleExport = () => {
// Create CSV content
const headers = [
'Request ID',
'Employee',
'Status',
'Submitted Date',
'Assets',
'Justification',
'Reviewer',
'Admin Comments',
];
const rows = filteredRequests.map((req) => [
req.requestID,
req.employeeName,
req.status,
format(new Date(req.submittedDate), 'yyyy-MM-dd'),
req.requestedAssets.map((a) => `${a.assetType} (x${a.quantity})`).join('; '),
req.justification.replace(/,/g, ';'),
req.reviewerName || '',
req.adminComments?.replace(/,/g, ';') || '',
]);
const csvContent = [
headers.join(','),
...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
].join('\n');
// Create and download file
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute(
'download',
`asset-requests-${format(new Date(), 'yyyy-MM-dd')}.csv`
);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
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" />;
}
};
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';
}
};
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">
{/* Statistics Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Requests</p>
<p className="text-2xl font-bold">{stats.total}</p>
</div>
<TrendingUp className="h-8 w-8 text-muted-foreground" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Pending</p>
<p className="text-2xl font-bold text-yellow-600">{stats.pending}</p>
</div>
<Clock className="h-8 w-8 text-yellow-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Approved</p>
<p className="text-2xl font-bold text-green-600">{stats.approved}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Rejected</p>
<p className="text-2xl font-bold text-red-600">{stats.rejected}</p>
</div>
<XCircle className="h-8 w-8 text-red-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Assigned</p>
<p className="text-2xl font-bold text-blue-600">{stats.assigned}</p>
</div>
<Package className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
</div>
{/* Filters and Search */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">All Asset Requests</CardTitle>
<Button variant="outline" size="sm" onClick={handleExport}>
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<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..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as RequestStatus | 'all')}>
<SelectTrigger>
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="approved">Approved</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem>
<SelectItem value="assigned">Assigned</SelectItem>
</SelectContent>
</Select>
<Select value={dateRangeFilter} onValueChange={(value) => setDateRangeFilter(value as any)}>
<SelectTrigger>
<SelectValue placeholder="Filter by date" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Time</SelectItem>
<SelectItem value="7days">Last 7 Days</SelectItem>
<SelectItem value="30days">Last 30 Days</SelectItem>
<SelectItem value="90days">Last 90 Days</SelectItem>
</SelectContent>
</Select>
<Select value={employeeFilter} onValueChange={setEmployeeFilter}>
<SelectTrigger>
<SelectValue placeholder="Filter by employee" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Employees</SelectItem>
{employees.map((emp) => (
<SelectItem key={emp.id} value={emp.id.toString()}>
{emp.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Requests Table */}
{filteredRequests.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">
{searchTerm || statusFilter !== 'all' || dateRangeFilter !== 'all' || employeeFilter !== 'all'
? 'No requests match your filters'
: 'No asset requests found'}
</p>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Employee</TableHead>
<TableHead>Status</TableHead>
<TableHead>Assets</TableHead>
<TableHead>Submitted</TableHead>
<TableHead>Reviewer</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRequests.map((request) => (
<TableRow key={request.requestID}>
<TableCell className="font-medium">#{request.requestID}</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>
<Badge
variant="outline"
className={`flex items-center gap-1 w-fit ${getStatusColor(
request.status
)}`}
>
{getStatusIcon(request.status)}
{REQUEST_STATUS_LABELS[request.status]}
</Badge>
</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="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span>{format(new Date(request.submittedDate), 'MMM dd, yyyy')}</span>
</div>
</TableCell>
<TableCell>
{request.reviewerName ? (
<div className="text-sm">{request.reviewerName}</div>
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)}
{/* Results Count */}
{filteredRequests.length > 0 && (
<div className="text-sm text-muted-foreground text-center">
Showing {filteredRequests.length} of {requests.length} requests
</div>
)}
</div>
);
};
export default AssetRequestDashboard;