pmtool / src /components /asset-management /AssignmentList.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
24.9 kB
import React, { useState, useMemo } from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
import { CustomDropdown } from './shared/CustomDropdown';
import { Badge } from '@/components/ui/badge';
import {
Search,
Eye,
Edit,
Trash2,
Plus,
ArrowUpDown,
ArrowUp,
ArrowDown,
User,
Calendar,
CheckCircle,
Clock
} from 'lucide-react';
import { AssetAssignment, AssetAssignmentWithDetails } from '@/types/asset-management';
import { format, isValid, subDays, subMonths, subYears } from 'date-fns';
import { LoadingSpinner } from './shared';
import Pagination from './shared/Pagination';
import { usePagination } from '@/hooks/asset-management/usePagination';
import { useIsMobile } from '@/hooks/use-mobile';
interface AssignmentListProps {
assignments: AssetAssignment[] | AssetAssignmentWithDetails[];
isLoading?: boolean;
onView?: (assignment: AssetAssignment) => void;
onEdit?: (assignment: AssetAssignment) => void;
onDelete?: (assignment: AssetAssignment) => void;
onCreate?: () => void;
onNavigateToAsset?: (assetId: number) => void;
onNavigateToEmployee?: (employeeId: number) => void;
showActions?: boolean;
showAssetInfo?: boolean;
}
type SortField = keyof AssetAssignment;
type SortDirection = 'asc' | 'desc';
interface SortConfig {
field: SortField;
direction: SortDirection;
}
const AssignmentList: React.FC<AssignmentListProps> = ({
assignments,
isLoading = false,
onView,
onEdit,
onDelete,
onCreate,
onNavigateToAsset,
onNavigateToEmployee,
showActions = true,
showAssetInfo = true
}) => {
const isMobile = useIsMobile();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [dateRangeFilter, setDateRangeFilter] = useState<string>('all');
const [sortConfig, setSortConfig] = useState<SortConfig>({ field: 'assignmentID', direction: 'desc' });
// Helper function to determine assignment status
const getAssignmentStatus = (assignment: AssetAssignment) => {
return assignment.returnedDate ? 'returned' : 'active';
};
// Helper function to get status badge variant
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'active':
return 'default';
case 'returned':
return 'secondary';
default:
return 'outline';
}
};
// Helper function to format date safely
const formatDate = (dateString: string | null) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return isValid(date) ? format(date, 'PP') : 'Invalid Date';
};
// Helper function to check if assignment matches date range filter
const matchesDateRange = (assignment: AssetAssignment) => {
if (dateRangeFilter === 'all') return true;
const assignedDate = new Date(assignment.assignedDate);
const now = new Date();
switch (dateRangeFilter) {
case 'last7days':
return assignedDate >= subDays(now, 7);
case 'last30days':
return assignedDate >= subDays(now, 30);
case 'last3months':
return assignedDate >= subMonths(now, 3);
case 'lastyear':
return assignedDate >= subYears(now, 1);
default:
return true;
}
};
// Filter and sort assignments
const filteredAndSortedAssignments = useMemo(() => {
let filtered = assignments.filter(assignment => {
const assignmentWithDetails = assignment as AssetAssignmentWithDetails;
const status = getAssignmentStatus(assignment);
const matchesSearch =
assignment.employeeID.toString().includes(searchTerm) ||
assignment.assetID.toString().includes(searchTerm) ||
(assignment.assetCode && assignment.assetCode.toLowerCase().includes(searchTerm.toLowerCase())) ||
assignment.remarks.toLowerCase().includes(searchTerm.toLowerCase()) ||
(assignment.employeeName && assignment.employeeName.toLowerCase().includes(searchTerm.toLowerCase())) ||
(assignmentWithDetails.asset &&
(assignmentWithDetails.asset.assetCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
assignmentWithDetails.asset.assetType.toLowerCase().includes(searchTerm.toLowerCase()) ||
assignmentWithDetails.asset.brandModel.toLowerCase().includes(searchTerm.toLowerCase()) ||
assignmentWithDetails.asset.serialNumber.toLowerCase().includes(searchTerm.toLowerCase())));
const matchesStatus = statusFilter === 'all' || status === statusFilter;
const matchesDateRangeFilter = matchesDateRange(assignment);
return matchesSearch && matchesStatus && matchesDateRangeFilter;
});
// Sort assignments
filtered.sort((a, b) => {
const aValue = a[sortConfig.field];
const bValue = b[sortConfig.field];
if (typeof aValue === 'string' && typeof bValue === 'string') {
const comparison = aValue.localeCompare(bValue);
return sortConfig.direction === 'asc' ? comparison : -comparison;
}
if (typeof aValue === 'number' && typeof bValue === 'number') {
const comparison = aValue - bValue;
return sortConfig.direction === 'asc' ? comparison : -comparison;
}
// Handle date sorting
if (sortConfig.field === 'assignedDate' || sortConfig.field === 'returnedDate') {
const aDate = new Date(aValue as string);
const bDate = new Date(bValue as string);
const comparison = aDate.getTime() - bDate.getTime();
return sortConfig.direction === 'asc' ? comparison : -comparison;
}
return 0;
});
return filtered;
}, [assignments, searchTerm, statusFilter, dateRangeFilter, sortConfig]);
// Use pagination hook
const {
currentPage,
itemsPerPage,
totalPages,
paginatedData: paginatedAssignments,
setCurrentPage,
setItemsPerPage,
} = usePagination(filteredAndSortedAssignments);
const handleSort = (field: SortField) => {
setSortConfig(prev => ({
field,
direction: prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc'
}));
};
const getSortIcon = (field: SortField) => {
if (sortConfig.field !== field) {
return <ArrowUpDown className="h-4 w-4" />;
}
return sortConfig.direction === 'asc'
? <ArrowUp className="h-4 w-4" />
: <ArrowDown className="h-4 w-4" />;
};
// Mobile Card Component
const AssignmentCard: React.FC<{ assignment: AssetAssignment }> = ({ assignment }) => {
const assignmentWithDetails = assignment as AssetAssignmentWithDetails;
const status = getAssignmentStatus(assignment);
return (
<Card className="mb-3 hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
ID #{assignment.assignmentID}
</Badge>
<Badge variant={getStatusBadgeVariant(status)} className="text-xs">
{status === 'active' ? (
<>
<Clock className="h-3 w-3 mr-1" />
Active
</>
) : (
<>
<CheckCircle className="h-3 w-3 mr-1" />
Returned
</>
)}
</Badge>
</div>
{showActions && (
<div className="flex gap-1">
{onView && (
<Button
variant="ghost"
size="sm"
onClick={() => onView(assignment)}
className="h-8 w-8 p-0"
>
<Eye className="h-4 w-4" />
</Button>
)}
{onEdit && (
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(assignment)}
className="h-8 w-8 p-0"
>
<Edit className="h-4 w-4" />
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(assignment)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
)}
</div>
{/* Employee Info */}
<div className="flex items-center gap-2 mb-2">
<User className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="min-w-0 flex-1">
<div className="font-medium text-sm truncate">
{assignment.employeeName || `Employee #${assignment.employeeID}`}
</div>
{assignmentWithDetails.employeeEmail && (
<div className="text-xs text-muted-foreground truncate">
{assignmentWithDetails.employeeEmail}
</div>
)}
</div>
</div>
{/* Asset Info */}
{showAssetInfo && (
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="text-xs flex-shrink-0">
{assignment.assetCode || `Asset #${assignment.assetID}`}
</Badge>
<div className="min-w-0 flex-1">
{assignmentWithDetails.asset ? (
<>
<div className="font-medium text-sm truncate">
{assignmentWithDetails.asset.assetType}
</div>
<div className="text-xs text-muted-foreground truncate">
{assignmentWithDetails.asset.brandModel}
</div>
</>
) : (
<div className="font-medium text-sm truncate">
{assignment.assetCode}
</div>
)}
</div>
</div>
)}
{/* Dates */}
<div className="space-y-1 mb-2">
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">Assigned:</span>
<span className="font-medium">{formatDate(assignment.assignedDate)}</span>
</div>
{assignment.returnedDate && (
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">Returned:</span>
<span className="font-medium">{formatDate(assignment.returnedDate)}</span>
</div>
)}
</div>
{/* Remarks */}
{assignment.remarks && (
<div className="text-sm text-muted-foreground">
<span className="font-medium">Remarks:</span> {assignment.remarks}
</div>
)}
</CardContent>
</Card>
);
};
if (isLoading) {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Asset Assignments</h2>
</div>
<LoadingSpinner size="lg" text="Loading assignments..." className="py-8" />
</div>
);
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Asset Assignments</h2>
{onCreate && (
<Button onClick={onCreate} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
New Assignment
</Button>
)}
</div>
{/* Filters */}
<div className="flex flex-col gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search by employee, asset code, asset type, or remarks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<CustomDropdown
options={[
{ value: 'all', label: 'All Assignments' },
{ value: 'active', label: 'Active' },
{ value: 'returned', label: 'Returned' }
]}
value={statusFilter}
onChange={setStatusFilter}
placeholder="Filter by status"
className="w-full sm:w-48"
aria-label="Filter by assignment status"
/>
<CustomDropdown
options={[
{ value: 'all', label: 'All Time' },
{ value: 'last7days', label: 'Last 7 Days' },
{ value: 'last30days', label: 'Last 30 Days' },
{ value: 'last3months', label: 'Last 3 Months' },
{ value: 'lastyear', label: 'Last Year' }
]}
value={dateRangeFilter}
onChange={setDateRangeFilter}
placeholder="Filter by date range"
className="w-full sm:w-48"
aria-label="Filter by date range"
/>
</div>
</div>
{/* Results count */}
<div className="text-sm text-muted-foreground">
Showing {paginatedAssignments.length} of {filteredAndSortedAssignments.length} assignments
{filteredAndSortedAssignments.length !== assignments.length && (
<span> (filtered from {assignments.length} total)</span>
)}
</div>
{/* Mobile Card View / Desktop Table View */}
{isMobile ? (
// Mobile Card Layout
<div className="space-y-3">
{paginatedAssignments.length === 0 ? (
<Card>
<CardContent className="p-6 text-center text-muted-foreground">
{assignments.length === 0
? "No assignments found. Create your first assignment to get started."
: "No assignments match your current filters."
}
</CardContent>
</Card>
) : (
paginatedAssignments.map((assignment) => (
<AssignmentCard key={assignment.assignmentID} assignment={assignment} />
))
)}
</div>
) : (
// Desktop Table Layout
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('assignmentID')}
>
<div className="flex items-center gap-2">
ID
{getSortIcon('assignmentID')}
</div>
</TableHead>
{showAssetInfo && (
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('assetCode')}
>
<div className="flex items-center gap-2">
Asset
{getSortIcon('assetCode')}
</div>
</TableHead>
)}
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('employeeID')}
>
<div className="flex items-center gap-2">
Employee
{getSortIcon('employeeID')}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('assignedDate')}
>
<div className="flex items-center gap-2">
Assigned Date
{getSortIcon('assignedDate')}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleSort('returnedDate')}
>
<div className="flex items-center gap-2">
Returned Date
{getSortIcon('returnedDate')}
</div>
</TableHead>
<TableHead>Status</TableHead>
<TableHead>Remarks</TableHead>
{showActions && <TableHead className="text-right">Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{paginatedAssignments.length === 0 ? (
<TableRow>
<TableCell
colSpan={showActions ? (showAssetInfo ? 8 : 7) : (showAssetInfo ? 7 : 6)}
className="text-center py-8 text-muted-foreground"
>
{assignments.length === 0
? "No assignments found. Create your first assignment to get started."
: "No assignments match your current filters."
}
</TableCell>
</TableRow>
) : (
paginatedAssignments.map((assignment) => {
const assignmentWithDetails = assignment as AssetAssignmentWithDetails;
const status = getAssignmentStatus(assignment);
return (
<TableRow key={assignment.assignmentID} className="hover:bg-muted/50">
<TableCell className="font-medium">{assignment.assignmentID}</TableCell>
{showAssetInfo && (
<TableCell>
<div className="flex items-center gap-2">
{/* <Badge variant="outline">{assignment.assetCode || `#${assignment.assetID}`}</Badge> */}
{assignmentWithDetails.asset ? (
<div className="text-sm">
<div className="font-medium">{assignmentWithDetails.asset.assetType}</div>
<div className="text-muted-foreground text-xs">
{assignmentWithDetails.asset.brandModel}
</div>
</div>
) : (
<div className="text-sm">
<div className="font-medium">{assignment.assetCode}</div>
</div>
)}
</div>
</TableCell>
)}
<TableCell>
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">
{assignment.employeeName || `Employee #${assignment.employeeID}`}
</div>
{assignmentWithDetails.employeeEmail && (
<div className="text-xs text-muted-foreground">
{assignmentWithDetails.employeeEmail}
</div>
)}
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
{formatDate(assignment.assignedDate)}
</div>
</TableCell>
<TableCell>
{assignment.returnedDate ? (
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
{formatDate(assignment.returnedDate)}
</div>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
<Badge variant={getStatusBadgeVariant(status)}>
{status === 'active' ? (
<>
<Clock className="h-3 w-3 mr-1" />
Active
</>
) : (
<>
<CheckCircle className="h-3 w-3 mr-1" />
Returned
</>
)}
</Badge>
</TableCell>
<TableCell className="max-w-48 truncate" title={assignment.remarks}>
{assignment.remarks}
</TableCell>
{showActions && (
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{onView && (
<Button
variant="ghost"
size="sm"
onClick={() => onView(assignment)}
className="h-8 w-8 p-0"
>
<Eye className="h-4 w-4" />
<span className="sr-only">View assignment</span>
</Button>
)}
{onEdit && (
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(assignment)}
className="h-8 w-8 p-0"
>
<Edit className="h-4 w-4" />
<span className="sr-only">Edit assignment</span>
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(assignment)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete assignment</span>
</Button>
)}
</div>
</TableCell>
)}
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
)}
{/* Pagination */}
{filteredAndSortedAssignments.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={filteredAndSortedAssignments.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={setItemsPerPage}
className="mt-4"
/>
)}
</div>
);
};
export default AssignmentList;