Spaces:
Sleeping
Sleeping
| 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, | |
| Monitor, | |
| Laptop, | |
| HardDrive | |
| } from 'lucide-react'; | |
| import { SystemDetails, SystemDetailsWithAsset } from '@/types/asset-management'; | |
| import { LoadingSpinner } from './shared'; | |
| import Pagination from './shared/Pagination'; | |
| import { usePagination } from '@/hooks/asset-management/usePagination'; | |
| interface SystemDetailsListProps { | |
| systemDetails: SystemDetails[] | SystemDetailsWithAsset[]; | |
| isLoading?: boolean; | |
| onView?: (systemDetails: SystemDetails) => void; | |
| onEdit?: (systemDetails: SystemDetails) => void; | |
| onDelete?: (systemDetails: SystemDetails) => void; | |
| onCreate?: () => void; | |
| onNavigateToAsset?: (assetId: number) => void; | |
| showActions?: boolean; | |
| showAssetInfo?: boolean; | |
| } | |
| type SortField = keyof SystemDetails; | |
| type SortDirection = 'asc' | 'desc'; | |
| interface SortConfig { | |
| field: SortField; | |
| direction: SortDirection; | |
| } | |
| const SystemDetailsList: React.FC<SystemDetailsListProps> = ({ | |
| systemDetails, | |
| isLoading = false, | |
| onView, | |
| onEdit, | |
| onDelete, | |
| onCreate, | |
| onNavigateToAsset, | |
| showActions = true, | |
| showAssetInfo = true | |
| }) => { | |
| const [searchTerm, setSearchTerm] = useState(''); | |
| const [manufacturerFilter, setManufacturerFilter] = useState<string>('all'); | |
| const [sortConfig, setSortConfig] = useState<SortConfig>({ field: 'systemDetailsID', direction: 'asc' }); | |
| // Helper function to get system type icon | |
| const getSystemTypeIcon = (systemModel: string) => { | |
| const model = systemModel.toLowerCase(); | |
| if (model.includes('laptop') || model.includes('notebook')) return <Laptop className="h-4 w-4" />; | |
| if (model.includes('monitor') || model.includes('display')) return <Monitor className="h-4 w-4" />; | |
| return <HardDrive className="h-4 w-4" />; | |
| }; | |
| // Get unique manufacturers for filter | |
| const uniqueManufacturers = useMemo(() => { | |
| const manufacturers = systemDetails.map(details => details.manufacturer); | |
| return Array.from(new Set(manufacturers)).sort(); | |
| }, [systemDetails]); | |
| // Filter and sort system details | |
| const filteredAndSortedSystemDetails = useMemo(() => { | |
| let filtered = systemDetails.filter(details => { | |
| const matchesSearch = | |
| details.systemModel.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| details.manufacturer.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| details.processor.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| details.installedRAM.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| details.graphics.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| details.diskModel.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| details.assetID.toString().includes(searchTerm); | |
| const matchesManufacturer = manufacturerFilter === 'all' || details.manufacturer === manufacturerFilter; | |
| return matchesSearch && matchesManufacturer; | |
| }); | |
| // Sort system details | |
| 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; | |
| }, [systemDetails, searchTerm, manufacturerFilter, sortConfig]); | |
| // Use pagination hook | |
| const { | |
| currentPage, | |
| itemsPerPage, | |
| totalPages, | |
| paginatedData: paginatedSystemDetails, | |
| setCurrentPage, | |
| setItemsPerPage, | |
| } = usePagination(filteredAndSortedSystemDetails); | |
| 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">System Details</h2> | |
| </div> | |
| <LoadingSpinner size="lg" text="Loading system details..." className="py-8" /> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-4"> | |
| {/* Header */} | |
| <div className="flex justify-between items-center"> | |
| <h2 className="text-2xl font-bold">System Details</h2> | |
| {onCreate && ( | |
| <Button onClick={onCreate} className="flex items-center gap-2"> | |
| <Plus className="h-4 w-4" /> | |
| Add System Details | |
| </Button> | |
| )} | |
| </div> | |
| {/* 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" /> | |
| <Input | |
| placeholder="Search by model, manufacturer, processor, RAM, graphics, or disk..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| className="pl-10" | |
| /> | |
| </div> | |
| <CustomDropdown | |
| options={[ | |
| { value: 'all', label: 'All Manufacturers' }, | |
| ...uniqueManufacturers.map(manufacturer => ({ value: manufacturer, label: manufacturer })) | |
| ]} | |
| value={manufacturerFilter} | |
| onChange={setManufacturerFilter} | |
| placeholder="Filter by manufacturer" | |
| className="w-full sm:w-48" | |
| aria-label="Filter by manufacturer" | |
| /> | |
| </div> | |
| {/* Results count */} | |
| <div className="text-sm text-muted-foreground"> | |
| Showing {paginatedSystemDetails.length} of {filteredAndSortedSystemDetails.length} system details | |
| {filteredAndSortedSystemDetails.length !== systemDetails.length && ( | |
| <span> (filtered from {systemDetails.length} total)</span> | |
| )} | |
| </div> | |
| {/* Table */} | |
| <div className="border rounded-lg"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => handleSort('systemDetailsID')} | |
| > | |
| <div className="flex items-center gap-2"> | |
| ID | |
| {getSortIcon('systemDetailsID')} | |
| </div> | |
| </TableHead> | |
| {showAssetInfo && ( | |
| <TableHead | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => handleSort('assetID')} | |
| > | |
| <div className="flex items-center gap-2"> | |
| Asset ID | |
| {getSortIcon('assetID')} | |
| </div> | |
| </TableHead> | |
| )} | |
| <TableHead | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => handleSort('systemModel')} | |
| > | |
| <div className="flex items-center gap-2"> | |
| System Model | |
| {getSortIcon('systemModel')} | |
| </div> | |
| </TableHead> | |
| <TableHead | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => handleSort('manufacturer')} | |
| > | |
| <div className="flex items-center gap-2"> | |
| Manufacturer | |
| {getSortIcon('manufacturer')} | |
| </div> | |
| </TableHead> | |
| <TableHead | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => handleSort('processor')} | |
| > | |
| <div className="flex items-center gap-2"> | |
| Processor | |
| {getSortIcon('processor')} | |
| </div> | |
| </TableHead> | |
| <TableHead | |
| className="cursor-pointer hover:bg-muted/50" | |
| onClick={() => handleSort('installedRAM')} | |
| > | |
| <div className="flex items-center gap-2"> | |
| RAM | |
| {getSortIcon('installedRAM')} | |
| </div> | |
| </TableHead> | |
| <TableHead>Storage</TableHead> | |
| {showActions && <TableHead className="text-right">Actions</TableHead>} | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {paginatedSystemDetails.length === 0 ? ( | |
| <TableRow> | |
| <TableCell | |
| colSpan={showActions ? (showAssetInfo ? 8 : 7) : (showAssetInfo ? 7 : 6)} | |
| className="text-center py-8 text-muted-foreground" | |
| > | |
| {systemDetails.length === 0 | |
| ? "No system details found. Create your first system details record to get started." | |
| : "No system details match your current filters." | |
| } | |
| </TableCell> | |
| </TableRow> | |
| ) : ( | |
| paginatedSystemDetails.map((details) => { | |
| const detailsWithAsset = details as SystemDetailsWithAsset; | |
| return ( | |
| <TableRow key={details.systemDetailsID} className="hover:bg-muted/50"> | |
| <TableCell className="font-medium">{details.systemDetailsID}</TableCell> | |
| {showAssetInfo && ( | |
| <TableCell> | |
| <div className="flex items-center gap-2"> | |
| <Badge variant="outline">{details.assetID}</Badge> | |
| {detailsWithAsset.asset && ( | |
| <span className="text-sm text-muted-foreground"> | |
| {detailsWithAsset.asset.assetType} | |
| </span> | |
| )} | |
| {onNavigateToAsset && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-6 px-2 text-xs" | |
| onClick={() => onNavigateToAsset(details.assetID)} | |
| > | |
| View Asset | |
| </Button> | |
| )} | |
| </div> | |
| </TableCell> | |
| )} | |
| <TableCell> | |
| <div className="flex items-center gap-2"> | |
| {getSystemTypeIcon(details.systemModel)} | |
| <span>{details.systemModel}</span> | |
| </div> | |
| </TableCell> | |
| <TableCell>{details.manufacturer}</TableCell> | |
| <TableCell className="max-w-48 truncate" title={details.processor}> | |
| {details.processor} | |
| </TableCell> | |
| <TableCell> | |
| <Badge variant="secondary">{details.installedRAM}</Badge> | |
| </TableCell> | |
| <TableCell> | |
| <div className="text-sm"> | |
| <div className="font-medium">{details.diskSize}</div> | |
| <div className="text-muted-foreground text-xs truncate max-w-32" title={details.diskModel}> | |
| {details.diskModel} | |
| </div> | |
| </div> | |
| </TableCell> | |
| {showActions && ( | |
| <TableCell className="text-right"> | |
| <div className="flex justify-end gap-2"> | |
| {onView && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => onView(details)} | |
| className="h-8 w-8 p-0" | |
| > | |
| <Eye className="h-4 w-4" /> | |
| <span className="sr-only">View system details</span> | |
| </Button> | |
| )} | |
| {onEdit && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => onEdit(details)} | |
| className="h-8 w-8 p-0" | |
| > | |
| <Edit className="h-4 w-4" /> | |
| <span className="sr-only">Edit system details</span> | |
| </Button> | |
| )} | |
| {onDelete && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => onDelete(details)} | |
| className="h-8 w-8 p-0 text-destructive hover:text-destructive" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| <span className="sr-only">Delete system details</span> | |
| </Button> | |
| )} | |
| </div> | |
| </TableCell> | |
| )} | |
| </TableRow> | |
| ); | |
| }) | |
| )} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| {/* Pagination */} | |
| {filteredAndSortedSystemDetails.length > 0 && ( | |
| <Pagination | |
| currentPage={currentPage} | |
| totalPages={totalPages} | |
| totalItems={filteredAndSortedSystemDetails.length} | |
| itemsPerPage={itemsPerPage} | |
| onPageChange={setCurrentPage} | |
| onItemsPerPageChange={setItemsPerPage} | |
| className="mt-4" | |
| /> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default SystemDetailsList; |