pmtool / src /components /asset-management /AssetDetail.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
23.7 kB
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;