devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
22.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 { CustomDropdown } from './shared/CustomDropdown';
import { Badge } from '@/components/ui/badge';
import {
Search,
Eye,
Edit,
Trash2,
Plus,
ArrowUpDown,
ArrowUp,
ArrowDown,
Grid3X3,
List,
UserPlus,
User
} from 'lucide-react';
import { Asset, AssetCondition, ASSET_CONDITIONS, ASSET_TYPES, AssetAssignment } from '@/types/asset-management';
import { LoadingSpinner } from './shared';
import Pagination from './shared/Pagination';
import AssetCard from './shared/AssetCard';
import { usePagination } from '@/hooks/asset-management/usePagination';
interface AssetListProps {
assets: Asset[];
assignments?: AssetAssignment[];
isLoading?: boolean;
onView?: (asset: Asset) => void;
onEdit?: (asset: Asset) => void;
onDelete?: (asset: Asset) => void;
onCreate?: () => void;
onAssign?: (asset: Asset) => void;
showActions?: boolean;
}
type SortField = keyof Asset;
type SortDirection = 'asc' | 'desc';
interface SortConfig {
field: SortField;
direction: SortDirection;
}
const AssetList: React.FC<AssetListProps> = ({
assets,
assignments = [],
isLoading = false,
onView,
onEdit,
onDelete,
onCreate,
onAssign,
showActions = true
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [conditionFilter, setConditionFilter] = useState<string>('all');
const [assignmentStatusFilter, setAssignmentStatusFilter] = useState<string>('all');
const [sortConfig, setSortConfig] = useState<SortConfig>({ field: 'assetCode', direction: 'asc' });
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
// Helper function to get badge variant based on asset condition
const getConditionBadgeVariant = (condition: AssetCondition) => {
switch (condition) {
case 'Excellent':
return 'default';
case 'Good':
return 'secondary';
case 'Fair':
return 'outline';
case 'Poor':
return 'destructive';
default:
return 'outline';
}
};
// Helper function to get active assignment for an asset
const getActiveAssignment = (assetId: number): AssetAssignment | undefined => {
return assignments.find(
assignment => assignment.assetID === assetId && !assignment.returnedDate
);
};
// Helper function to check if asset is available for assignment
const isAssetAvailable = (assetId: number): boolean => {
return !getActiveAssignment(assetId);
};
// Helper function to get assignment status for an asset
const getAssetAssignmentStatus = (assetId: number) => {
return getActiveAssignment(assetId) ? 'assigned' : 'unassigned';
};
// Filter and sort assets
const filteredAndSortedAssets = useMemo(() => {
let filtered = assets.filter(asset => {
const matchesSearch =
asset.assetType.toLowerCase().includes(searchTerm.toLowerCase()) ||
asset.brandModel.toLowerCase().includes(searchTerm.toLowerCase()) ||
asset.serialNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
asset.assetCode.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = typeFilter === 'all' || asset.assetType === typeFilter;
const matchesCondition = conditionFilter === 'all' || asset.assetCondition === conditionFilter;
const assetAssignmentStatus = getAssetAssignmentStatus(asset.assetID);
const matchesAssignmentStatus = assignmentStatusFilter === 'all' || assetAssignmentStatus === assignmentStatusFilter;
return matchesSearch && matchesType && matchesCondition && matchesAssignmentStatus;
});
// Sort assets
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;
}
return 0;
});
return filtered;
}, [assets, assignments, searchTerm, typeFilter, conditionFilter, assignmentStatusFilter, sortConfig]);
// Use pagination hook
const {
currentPage,
itemsPerPage,
totalPages,
paginatedData: paginatedAssets,
setCurrentPage,
setItemsPerPage,
} = usePagination(filteredAndSortedAssets);
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" />;
};
if (isLoading) {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Assets</h2>
</div>
<LoadingSpinner size="lg" text="Loading assets..." className="py-8" />
</div>
);
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Assets</h2>
<div className="flex items-center gap-2">
{/* View Mode Toggle */}
<div className="hidden md:flex border rounded-lg p-1">
<Button
variant={viewMode === 'table' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('table')}
className="h-8 px-3"
aria-label="Table view"
>
<List className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'cards' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('cards')}
className="h-8 px-3"
aria-label="Card view"
>
<Grid3X3 className="h-4 w-4" />
</Button>
</div>
{onCreate && (
<Button onClick={onCreate} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Add Asset
</Button>
)}
</div>
</div>
{/* Filters */}
<div className="flex flex-col gap-4" role="search" aria-label="Asset search and filters">
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4 pointer-events-none" aria-hidden="true" />
<Input
placeholder="Search by code, type, brand, model, or serial number..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
aria-label="Search assets by code, type, brand, model, or serial number"
type="search"
/>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4">
<div className="w-full sm:w-48">
<label htmlFor="type-filter" className="sr-only">Filter by asset type</label>
<CustomDropdown
options={[
{ value: 'all', label: 'All Types' },
...ASSET_TYPES.map(type => ({ value: type, label: type }))
]}
value={typeFilter}
onChange={setTypeFilter}
placeholder="Filter by type"
className="w-full"
aria-label="Filter by asset type"
/>
</div>
<div className="w-full sm:w-48">
<label htmlFor="condition-filter" className="sr-only">Filter by asset condition</label>
<CustomDropdown
options={[
{ value: 'all', label: 'All Conditions' },
...ASSET_CONDITIONS.map(condition => ({ value: condition, label: condition }))
]}
value={conditionFilter}
onChange={setConditionFilter}
placeholder="Filter by condition"
className="w-full"
aria-label="Filter by asset condition"
/>
</div>
<div className="w-full sm:w-48">
<label htmlFor="assignment-filter" className="sr-only">Filter by assignment status</label>
<CustomDropdown
options={[
{ value: 'all', label: 'All Assets' },
{ value: 'assigned', label: 'Assigned' },
{ value: 'unassigned', label: 'Unassigned' }
]}
value={assignmentStatusFilter}
onChange={setAssignmentStatusFilter}
placeholder="Filter by assignment"
className="w-full"
aria-label="Filter by assignment status"
/>
</div>
</div>
</div>
{/* Results count */}
<div className="text-sm text-muted-foreground" aria-live="polite" aria-atomic="true">
Showing {paginatedAssets.length} of {filteredAndSortedAssets.length} assets
{filteredAndSortedAssets.length !== assets.length && (
<span> (filtered from {assets.length} total)</span>
)}
</div>
{/* Table View - Desktop */}
{viewMode === 'table' && (
<div className="hidden md:block border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead
className="cursor-pointer hover:bg-muted/50 focus:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onClick={() => handleSort('assetCode')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort('assetCode');
}
}}
tabIndex={0}
role="button"
aria-label={`Sort by Code ${sortConfig.field === 'assetCode' ? (sortConfig.direction === 'asc' ? 'descending' : 'ascending') : 'ascending'}`}
>
<div className="flex items-center gap-2">
Code
{getSortIcon('assetCode')}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 focus:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onClick={() => handleSort('assetType')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort('assetType');
}
}}
tabIndex={0}
role="button"
aria-label={`Sort by Type ${sortConfig.field === 'assetType' ? (sortConfig.direction === 'asc' ? 'descending' : 'ascending') : 'ascending'}`}
>
<div className="flex items-center gap-2">
Type
{getSortIcon('assetType')}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 focus:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onClick={() => handleSort('brandModel')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort('brandModel');
}
}}
tabIndex={0}
role="button"
aria-label={`Sort by Brand/Model ${sortConfig.field === 'brandModel' ? (sortConfig.direction === 'asc' ? 'descending' : 'ascending') : 'ascending'}`}
>
<div className="flex items-center gap-2">
Brand/Model
{getSortIcon('brandModel')}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 focus:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onClick={() => handleSort('serialNumber')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort('serialNumber');
}
}}
tabIndex={0}
role="button"
aria-label={`Sort by Serial Number ${sortConfig.field === 'serialNumber' ? (sortConfig.direction === 'asc' ? 'descending' : 'ascending') : 'ascending'}`}
>
<div className="flex items-center gap-2">
Serial Number
{getSortIcon('serialNumber')}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 focus:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onClick={() => handleSort('assetCondition')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort('assetCondition');
}
}}
tabIndex={0}
role="button"
aria-label={`Sort by Condition ${sortConfig.field === 'assetCondition' ? (sortConfig.direction === 'asc' ? 'descending' : 'ascending') : 'ascending'}`}
>
<div className="flex items-center gap-2">
Condition
{getSortIcon('assetCondition')}
</div>
</TableHead>
<TableHead>Status</TableHead>
{showActions && <TableHead className="text-right">Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{paginatedAssets.length === 0 ? (
<TableRow>
<TableCell
colSpan={showActions ? 7 : 6}
className="text-center py-8 text-muted-foreground"
>
{assets.length === 0
? "No assets found. Create your first asset to get started."
: "No assets match your current filters."
}
</TableCell>
</TableRow>
) : (
paginatedAssets.map((asset) => {
const activeAssignment = getActiveAssignment(asset.assetID);
const isAvailable = isAssetAvailable(asset.assetID);
return (
<TableRow key={asset.assetID} className="hover:bg-muted/50">
<TableCell className="font-medium">{asset.assetCode}</TableCell>
<TableCell>{asset.assetType}</TableCell>
<TableCell>{asset.brandModel}</TableCell>
<TableCell className="font-mono text-sm">{asset.serialNumber}</TableCell>
<TableCell>
<Badge variant={getConditionBadgeVariant(asset.assetCondition as AssetCondition)}>
{asset.assetCondition}
</Badge>
</TableCell>
<TableCell>
{activeAssignment ? (
<Badge variant="secondary" className="flex items-center gap-1 w-fit">
<User className="h-3 w-3" />
<span className="text-xs">Assigned to {activeAssignment.employeeName}</span>
</Badge>
) : (
<Badge variant="outline" className="flex items-center gap-1 w-fit text-green-600 border-green-600">
<span className="text-xs">Available</span>
</Badge>
)}
</TableCell>
{showActions && (
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{onAssign && isAvailable && (
<Button
variant="ghost"
size="sm"
onClick={() => onAssign(asset)}
className="h-8 w-8 p-0"
aria-label={`Assign asset ${asset.assetCode} to employee`}
title="Assign to Employee"
>
<UserPlus className="h-4 w-4" />
<span className="sr-only">Assign asset {asset.assetCode}</span>
</Button>
)}
{onView && (
<Button
variant="ghost"
size="sm"
onClick={() => onView(asset)}
className="h-8 w-8 p-0"
aria-label={`View asset ${asset.brandModel} (${asset.serialNumber})`}
>
<Eye className="h-4 w-4" />
<span className="sr-only">View asset {asset.brandModel}</span>
</Button>
)}
{onEdit && (
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(asset)}
className="h-8 w-8 p-0"
aria-label={`Edit asset ${asset.brandModel} (${asset.serialNumber})`}
>
<Edit className="h-4 w-4" />
<span className="sr-only">Edit asset {asset.brandModel}</span>
</Button>
)}
{onDelete && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(asset)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
aria-label={`Delete asset ${asset.brandModel} (${asset.serialNumber})`}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete asset {asset.brandModel}</span>
</Button>
)}
</div>
</TableCell>
)}
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
)}
{/* Card View - Desktop */}
{viewMode === 'cards' && (
<div className="hidden md:block">
{paginatedAssets.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{assets.length === 0
? "No assets found. Create your first asset to get started."
: "No assets match your current filters."
}
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{paginatedAssets.map((asset) => (
<AssetCard
key={asset.assetID}
asset={asset}
onView={onView}
onEdit={onEdit}
onDelete={onDelete}
onAssign={onAssign}
activeAssignment={getActiveAssignment(asset.assetID)}
showActions={showActions}
compact={true}
/>
))}
</div>
)}
</div>
)}
{/* Mobile Card View */}
<div className="md:hidden space-y-6">
{paginatedAssets.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{assets.length === 0
? "No assets found. Create your first asset to get started."
: "No assets match your current filters."
}
</div>
) : (
paginatedAssets.map((asset) => (
<AssetCard
key={asset.assetID}
asset={asset}
onView={onView}
onEdit={onEdit}
onDelete={onDelete}
onAssign={onAssign}
activeAssignment={getActiveAssignment(asset.assetID)}
showActions={showActions}
compact={true}
/>
))
)}
</div>
{/* Pagination */}
{filteredAndSortedAssets.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={filteredAndSortedAssets.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={setItemsPerPage}
className="mt-4"
/>
)}
</div>
);
};
export default AssetList;