Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | |
| import { Separator } from '@/components/ui/separator'; | |
| import { | |
| Monitor, | |
| Laptop, | |
| Smartphone, | |
| Printer, | |
| HardDrive, | |
| Edit, | |
| Trash2, | |
| User, | |
| Calendar, | |
| Settings, | |
| History, | |
| Info, | |
| ArrowLeft, | |
| Package | |
| } from 'lucide-react'; | |
| import { | |
| Asset, | |
| AssetWithDetails, | |
| SystemDetails, | |
| AssetAssignment, | |
| AssetItem, | |
| AssetCondition | |
| } from '@/types/asset-management'; | |
| import { format } from 'date-fns'; | |
| import { LoadingSpinner, ConfirmDialog } from './shared'; | |
| import AssetItemList from './AssetItemList'; | |
| import AssignmentTimeline from './shared/AssignmentTimeline'; | |
| import RelatedEntitiesNav from './shared/RelatedEntitiesNav'; | |
| // 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 asset type icon | |
| const getAssetTypeIcon = (assetType: string) => { | |
| const type = assetType.toLowerCase(); | |
| if (type.includes('laptop')) return <Laptop className="h-5 w-5" />; | |
| if (type.includes('monitor')) return <Monitor className="h-5 w-5" />; | |
| if (type.includes('phone') || type.includes('tablet')) return <Smartphone className="h-5 w-5" />; | |
| if (type.includes('printer') || type.includes('scanner')) return <Printer className="h-5 w-5" />; | |
| return <HardDrive className="h-5 w-5" />; | |
| }; | |
| interface AssetDetailProps { | |
| asset: AssetWithDetails; | |
| systemDetails?: SystemDetails; | |
| assignments?: AssetAssignment[]; | |
| items?: AssetItem[]; | |
| isLoading?: boolean; | |
| onEdit?: (asset: Asset) => void; | |
| onDelete?: (asset: Asset) => void; | |
| onBack?: () => void; | |
| onEditSystemDetails?: (systemDetails: SystemDetails) => void; | |
| onCreateSystemDetails?: (assetId: number) => void; | |
| onDeleteSystemDetails?: (systemDetails: SystemDetails) => void; | |
| onEditAssignment?: (assignment: AssetAssignment) => void; | |
| onCreateAssignment?: (assetId: number) => void; | |
| onDeleteAssignment?: (assignment: AssetAssignment) => void; | |
| onEditItem?: (item: AssetItem) => void; | |
| onCreateItem?: () => void; | |
| onDeleteItem?: (item: AssetItem) => void; | |
| onNavigateToEmployee?: (employeeId: number) => void; | |
| onNavigateToSystemDetails?: (systemDetailsId: number) => void; | |
| showRelatedEntities?: boolean; | |
| } | |
| const AssetDetail: React.FC<AssetDetailProps> = ({ | |
| asset, | |
| systemDetails, | |
| assignments = [], | |
| items = [], | |
| isLoading = false, | |
| onEdit, | |
| onDelete, | |
| onBack, | |
| onEditSystemDetails, | |
| onCreateSystemDetails, | |
| onDeleteSystemDetails, | |
| onEditAssignment, | |
| onCreateAssignment, | |
| onDeleteAssignment, | |
| onEditItem, | |
| onCreateItem, | |
| onDeleteItem, | |
| onNavigateToEmployee, | |
| onNavigateToSystemDetails, | |
| showRelatedEntities = true | |
| }) => { | |
| const [showDeleteDialog, setShowDeleteDialog] = useState(false); | |
| const [showDeleteSystemDetailsDialog, setShowDeleteSystemDetailsDialog] = useState(false); | |
| const [showDeleteAssignmentDialog, setShowDeleteAssignmentDialog] = useState(false); | |
| const [selectedAssignment, setSelectedAssignment] = useState<AssetAssignment | null>(null); | |
| const currentAssignment = assignments.find(a => !a.returnedDate); | |
| const pastAssignments = assignments.filter(a => a.returnedDate); | |
| const handleDeleteAsset = () => { | |
| // Check for active assignments before allowing deletion | |
| const activeAssignments = assignments.filter(a => !a.returnedDate); | |
| if (activeAssignments.length > 0) { | |
| // This should be handled by the API, but we add a client-side check for better UX | |
| console.warn('Cannot delete asset with active assignments'); | |
| return; | |
| } | |
| if (onDelete) { | |
| onDelete(asset); | |
| setShowDeleteDialog(false); | |
| } | |
| }; | |
| const handleDeleteSystemDetails = () => { | |
| if (onDeleteSystemDetails && systemDetails) { | |
| onDeleteSystemDetails(systemDetails); | |
| setShowDeleteSystemDetailsDialog(false); | |
| } | |
| }; | |
| const handleDeleteAssignment = () => { | |
| if (onDeleteAssignment && selectedAssignment) { | |
| onDeleteAssignment(selectedAssignment); | |
| setShowDeleteAssignmentDialog(false); | |
| setSelectedAssignment(null); | |
| } | |
| }; | |
| if (isLoading) { | |
| return ( | |
| <div className="space-y-4"> | |
| <LoadingSpinner size="lg" text="Loading asset details..." className="py-8" /> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Header */} | |
| <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> | |
| <div className="flex items-center gap-3 sm:gap-4 min-w-0 flex-1"> | |
| {onBack && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onBack} | |
| aria-label="Go back to assets list" | |
| className="shrink-0" | |
| > | |
| <ArrowLeft className="h-4 w-4" /> | |
| <span className="sr-only">Back</span> | |
| </Button> | |
| )} | |
| <div className="flex items-center gap-3 min-w-0"> | |
| <div className="shrink-0" aria-hidden="true"> | |
| {getAssetTypeIcon(asset.assetType)} | |
| </div> | |
| <div className="min-w-0"> | |
| <h1 className="text-xl sm:text-2xl font-bold truncate">{asset.assetType}</h1> | |
| <p className="text-muted-foreground text-sm sm:text-base truncate">{asset.brandModel}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 w-full sm:w-auto"> | |
| {/* Quick Assign Button - Only show if asset is available */} | |
| {onCreateAssignment && !currentAssignment && ( | |
| <Button | |
| variant="default" | |
| onClick={() => onCreateAssignment(asset.assetID)} | |
| className="flex-1 sm:flex-none min-h-10" | |
| aria-label={`Quick assign ${asset.brandModel}`} | |
| > | |
| <User className="h-4 w-4 sm:mr-2" aria-hidden="true" /> | |
| <span>Assign</span> | |
| </Button> | |
| )} | |
| {onEdit && ( | |
| <Button | |
| variant="outline" | |
| onClick={() => onEdit(asset)} | |
| className="flex-1 sm:flex-none min-h-10" | |
| aria-label={`Edit asset ${asset.brandModel}`} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| onEdit(asset); | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| <Edit className="h-4 w-4 sm:mr-2" aria-hidden="true" /> | |
| <span className="hidden sm:inline">Edit</span> | |
| </Button> | |
| )} | |
| {onDelete && ( | |
| <Button | |
| variant="destructive" | |
| onClick={() => setShowDeleteDialog(true)} | |
| disabled={assignments.filter(a => !a.returnedDate).length > 0} | |
| title={assignments.filter(a => !a.returnedDate).length > 0 ? 'Cannot delete asset with active assignments' : undefined} | |
| className="flex-1 sm:flex-none min-h-10" | |
| aria-label={`Delete asset ${asset.brandModel}`} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| if (assignments.filter(a => !a.returnedDate).length === 0) { | |
| setShowDeleteDialog(true); | |
| } | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| > | |
| <Trash2 className="h-4 w-4 sm:mr-2" aria-hidden="true" /> | |
| <span className="hidden sm:inline">Delete</span> | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Asset Overview Card */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Info className="h-5 w-5" aria-hidden="true" /> | |
| Asset Information | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="asset-code-label">Asset Code</label> | |
| <p className="text-base sm:text-lg font-mono" aria-labelledby="asset-code-label">{asset.assetCode}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="condition-label">Condition</label> | |
| <div className="mt-1" aria-labelledby="condition-label"> | |
| <Badge variant={getConditionBadgeVariant(asset.assetCondition as AssetCondition)}> | |
| {asset.assetCondition} | |
| </Badge> | |
| </div> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="serial-label">Serial Number</label> | |
| <p className="text-base sm:text-lg font-mono break-all" aria-labelledby="serial-label">{asset.serialNumber}</p> | |
| </div> | |
| <div className="sm:col-span-2"> | |
| <label className="text-sm font-medium text-muted-foreground" id="status-label">Assignment Status</label> | |
| <div className="mt-2 flex items-center justify-between gap-3" aria-labelledby="status-label" aria-live="polite"> | |
| <div className="flex items-center gap-2"> | |
| {currentAssignment ? ( | |
| <> | |
| <Badge variant="default" className="bg-green-600"> | |
| <User className="h-3 w-3 mr-1" aria-hidden="true" /> | |
| Assigned | |
| </Badge> | |
| <span className="text-sm text-muted-foreground"> | |
| to <span className="font-medium text-foreground">{currentAssignment.employeeName}</span> | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| (since {format(new Date(currentAssignment.assignedDate), 'MMM d, yyyy')}) | |
| </span> | |
| </> | |
| ) : ( | |
| <> | |
| <Badge variant="outline"> | |
| <Calendar className="h-3 w-3 mr-1" aria-hidden="true" /> | |
| Available | |
| </Badge> | |
| <span className="text-sm text-muted-foreground">Ready to be assigned</span> | |
| </> | |
| )} | |
| </div> | |
| {onCreateAssignment && !currentAssignment && ( | |
| <Button | |
| variant="default" | |
| size="sm" | |
| onClick={() => onCreateAssignment(asset.assetID)} | |
| className="shrink-0" | |
| aria-label="Quick assign this asset" | |
| > | |
| <User className="h-4 w-4 mr-2" aria-hidden="true" /> | |
| Assign Now | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Main Content Layout */} | |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> | |
| {/* Main Content */} | |
| <div className="lg:col-span-3"> | |
| {/* Tabbed Content */} | |
| <Tabs defaultValue="system-details" className="w-full"> | |
| <TabsList className="grid w-full grid-cols-3" role="tablist"> | |
| <TabsTrigger | |
| value="system-details" | |
| className="flex items-center gap-2 text-xs sm:text-sm" | |
| role="tab" | |
| aria-controls="system-details-panel" | |
| > | |
| <Settings className="h-4 w-4" aria-hidden="true" /> | |
| <span className="hidden sm:inline">System Details</span> | |
| <span className="sm:hidden">Details</span> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="items" | |
| className="flex items-center gap-2 text-xs sm:text-sm" | |
| role="tab" | |
| aria-controls="items-panel" | |
| > | |
| <Package className="h-4 w-4" aria-hidden="true" /> | |
| <span className="hidden sm:inline">Items ({items.length})</span> | |
| <span className="sm:hidden">Items</span> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="assignments" | |
| className="flex items-center gap-2 text-xs sm:text-sm" | |
| role="tab" | |
| aria-controls="assignments-panel" | |
| > | |
| <History className="h-4 w-4" aria-hidden="true" /> | |
| <span className="hidden sm:inline">Assignment History</span> | |
| <span className="sm:hidden">History</span> | |
| </TabsTrigger> | |
| </TabsList> | |
| {/* System Details Tab */} | |
| <TabsContent value="system-details" id="system-details-panel" role="tabpanel"> | |
| <Card> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle>Technical Specifications</CardTitle> | |
| <div className="flex gap-2"> | |
| {systemDetails ? ( | |
| <> | |
| {onEditSystemDetails && ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => onEditSystemDetails(systemDetails)} | |
| > | |
| <Edit className="h-4 w-4 mr-2" /> | |
| Edit | |
| </Button> | |
| )} | |
| {onDeleteSystemDetails && ( | |
| <Button | |
| variant="destructive" | |
| size="sm" | |
| onClick={() => setShowDeleteSystemDetailsDialog(true)} | |
| > | |
| <Trash2 className="h-4 w-4 mr-2" /> | |
| Delete | |
| </Button> | |
| )} | |
| </> | |
| ) : ( | |
| onCreateSystemDetails && ( | |
| <Button | |
| variant="default" | |
| size="sm" | |
| onClick={() => onCreateSystemDetails(asset.assetID)} | |
| > | |
| Add System Details | |
| </Button> | |
| ) | |
| )} | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {systemDetails ? ( | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="system-model-label">System Model</label> | |
| <p className="break-words" aria-labelledby="system-model-label">{systemDetails.systemModel}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="manufacturer-label">Manufacturer</label> | |
| <p className="break-words" aria-labelledby="manufacturer-label">{systemDetails.manufacturer}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="processor-label">Processor</label> | |
| <p className="break-words" aria-labelledby="processor-label">{systemDetails.processor}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="ram-label">Installed RAM</label> | |
| <p className="break-words" aria-labelledby="ram-label">{systemDetails.installedRAM}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="graphics-label">Graphics</label> | |
| <p className="break-words" aria-labelledby="graphics-label">{systemDetails.graphics}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="resolution-label">Screen Resolution</label> | |
| <p className="break-words" aria-labelledby="resolution-label">{systemDetails.screenResolution}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="disk-model-label">Disk Model</label> | |
| <p className="break-words" aria-labelledby="disk-model-label">{systemDetails.diskModel}</p> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-muted-foreground" id="disk-size-label">Disk Size</label> | |
| <p className="break-words" aria-labelledby="disk-size-label">{systemDetails.diskSize}</p> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="text-center py-8 text-muted-foreground"> | |
| <Settings className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
| <p>No system details available for this asset.</p> | |
| {onCreateSystemDetails && ( | |
| <Button | |
| variant="outline" | |
| className="mt-4" | |
| onClick={() => onCreateSystemDetails(asset.assetID)} | |
| > | |
| Add System Details | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| {/* Items Tab */} | |
| <TabsContent value="items" id="items-panel" role="tabpanel"> | |
| <Card> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle>Asset Items & Components</CardTitle> | |
| {onCreateItem && ( | |
| <Button | |
| variant="default" | |
| size="sm" | |
| onClick={onCreateItem} | |
| > | |
| Add Item | |
| </Button> | |
| )} | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <AssetItemList | |
| items={items} | |
| onEdit={onEditItem} | |
| onDelete={onDeleteItem} | |
| showActions={true} | |
| /> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| {/* Assignments Tab */} | |
| <TabsContent value="assignments" id="assignments-panel" role="tabpanel"> | |
| <Card> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle>Assignment History</CardTitle> | |
| {onCreateAssignment && ( | |
| <Button | |
| variant="default" | |
| size="sm" | |
| onClick={() => onCreateAssignment(asset.assetID)} | |
| > | |
| New Assignment | |
| </Button> | |
| )} | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <AssignmentTimeline | |
| assignments={assignments} | |
| onEditAssignment={onEditAssignment} | |
| onDeleteAssignment={(assignment) => { | |
| setSelectedAssignment(assignment); | |
| setShowDeleteAssignmentDialog(true); | |
| }} | |
| onNavigateToEmployee={onNavigateToEmployee} | |
| showActions={true} | |
| /> | |
| </CardContent> | |
| </Card> | |
| </TabsContent> | |
| </Tabs> | |
| </div> | |
| {/* Related Entities Sidebar */} | |
| {showRelatedEntities && ( | |
| <div className="lg:col-span-1"> | |
| <RelatedEntitiesNav | |
| asset={asset} | |
| systemDetails={systemDetails} | |
| assignments={assignments} | |
| onNavigateToSystemDetails={onNavigateToSystemDetails} | |
| onNavigateToEmployee={onNavigateToEmployee} | |
| currentView="asset" | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| {/* Delete Dialogs */} | |
| <ConfirmDialog | |
| isOpen={showDeleteDialog} | |
| onClose={() => setShowDeleteDialog(false)} | |
| onConfirm={handleDeleteAsset} | |
| title="Delete Asset" | |
| description={ | |
| <div> | |
| <p>Are you sure you want to delete this asset?</p> | |
| <p className="mt-2 text-sm text-muted-foreground"> | |
| <strong>{asset.assetType}</strong> - {asset.brandModel} ({asset.serialNumber}) | |
| </p> | |
| {assignments.filter(a => !a.returnedDate).length > 0 ? ( | |
| <div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-md"> | |
| <p className="text-sm text-destructive font-medium">Cannot Delete Asset</p> | |
| <p className="text-sm text-destructive mt-1"> | |
| This asset has {assignments.filter(a => !a.returnedDate).length} active assignment(s). | |
| Please return all active assignments before deleting this asset. | |
| </p> | |
| </div> | |
| ) : assignments.length > 0 ? ( | |
| <div className="mt-3 p-3 bg-orange-50 border border-orange-200 rounded-md"> | |
| <p className="text-sm text-orange-800 font-medium">Warning</p> | |
| <p className="text-sm text-orange-700 mt-1"> | |
| This will also delete {assignments.length} assignment record(s) and any associated system details. | |
| </p> | |
| </div> | |
| ) : null} | |
| <p className="mt-2 text-sm text-destructive">This action cannot be undone.</p> | |
| </div> | |
| } | |
| confirmText="Delete Asset" | |
| type="delete" | |
| /> | |
| <ConfirmDialog | |
| isOpen={showDeleteSystemDetailsDialog} | |
| onClose={() => setShowDeleteSystemDetailsDialog(false)} | |
| onConfirm={handleDeleteSystemDetails} | |
| title="Delete System Details" | |
| description="Are you sure you want to delete the system details for this asset? This action cannot be undone." | |
| confirmText="Delete System Details" | |
| type="delete" | |
| /> | |
| <ConfirmDialog | |
| isOpen={showDeleteAssignmentDialog} | |
| onClose={() => setShowDeleteAssignmentDialog(false)} | |
| onConfirm={handleDeleteAssignment} | |
| title="Delete Assignment" | |
| description="Are you sure you want to delete this assignment record? This action cannot be undone." | |
| confirmText="Delete Assignment" | |
| type="delete" | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default AssetDetail; |