pmtool / src /components /asset-request /AssetRequestAssignment.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
22.6 kB
import React, { useState, useMemo, useEffect } from 'react';
import { format } from 'date-fns';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Package,
User,
Calendar,
CheckCircle,
Loader2,
AlertCircle,
Search,
Plus,
Minus,
} from 'lucide-react';
import {
AssetRequest,
AssetRequestAssignmentRequest,
AssignedAssetDetail,
} from '@/types/asset-request';
import { Asset, AssetAssignment } from '@/types/asset-management';
import {
validateAssignmentRequest,
calculateInventoryImpact,
logInventoryChange,
createInventoryAuditLog,
} from '@/utils/inventoryManagement';
interface AssetRequestAssignmentProps {
approvedRequests: AssetRequest[];
availableAssets: Asset[];
allAssets: Asset[];
activeAssignments: AssetAssignment[];
isLoading?: boolean;
onAssign: (id: number, data: AssetRequestAssignmentRequest) => Promise<void>;
currentUserId: number;
}
interface AssetAllocation {
assetType: string;
requestedQuantity: number;
allocatedAssets: Asset[];
}
const AssetRequestAssignment: React.FC<AssetRequestAssignmentProps> = ({
approvedRequests,
availableAssets,
allAssets,
activeAssignments,
isLoading = false,
onAssign,
currentUserId,
}) => {
const [selectedRequest, setSelectedRequest] = useState<AssetRequest | null>(null);
const [showAssignmentDialog, setShowAssignmentDialog] = useState(false);
const [assetAllocations, setAssetAllocations] = useState<AssetAllocation[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [filterAssetType, setFilterAssetType] = useState<string>('all');
// Get unique asset types from available assets
const assetTypes = useMemo(() => {
const types = new Set(availableAssets.map((asset) => asset.assetType));
return Array.from(types).sort();
}, [availableAssets]);
// Filter available assets based on search and type
const filteredAvailableAssets = useMemo(() => {
let filtered = [...availableAssets];
if (filterAssetType !== 'all') {
filtered = filtered.filter((asset) => asset.assetType === filterAssetType);
}
if (searchTerm) {
const term = searchTerm.toLowerCase();
filtered = filtered.filter(
(asset) =>
asset.assetCode.toLowerCase().includes(term) ||
asset.assetType.toLowerCase().includes(term) ||
asset.brandModel.toLowerCase().includes(term) ||
asset.serialNumber.toLowerCase().includes(term)
);
}
return filtered;
}, [availableAssets, searchTerm, filterAssetType]);
// Initialize allocations when a request is selected
useEffect(() => {
if (selectedRequest) {
const allocations: AssetAllocation[] = selectedRequest.requestedAssets.map((item) => ({
assetType: item.assetType,
requestedQuantity: item.quantity,
allocatedAssets: [],
}));
setAssetAllocations(allocations);
}
}, [selectedRequest]);
const handleSelectRequest = (request: AssetRequest) => {
setSelectedRequest(request);
setShowAssignmentDialog(true);
setSearchTerm('');
setFilterAssetType('all');
};
const handleAddAsset = (asset: Asset, assetType: string) => {
setAssetAllocations((prev) =>
prev.map((allocation) => {
if (allocation.assetType === assetType) {
// Check if asset is already allocated
if (allocation.allocatedAssets.some((a) => a.assetID === asset.assetID)) {
return allocation;
}
// Check if we've reached the requested quantity
if (allocation.allocatedAssets.length >= allocation.requestedQuantity) {
return allocation;
}
return {
...allocation,
allocatedAssets: [...allocation.allocatedAssets, asset],
};
}
return allocation;
})
);
};
const handleRemoveAsset = (assetId: number, assetType: string) => {
setAssetAllocations((prev) =>
prev.map((allocation) => {
if (allocation.assetType === assetType) {
return {
...allocation,
allocatedAssets: allocation.allocatedAssets.filter(
(a) => a.assetID !== assetId
),
};
}
return allocation;
})
);
};
const handleAssignSubmit = async () => {
if (!selectedRequest) return;
// Validate that all requested assets have been allocated
const allAllocated = assetAllocations.every(
(allocation) => allocation.allocatedAssets.length === allocation.requestedQuantity
);
if (!allAllocated) {
return;
}
// Build assigned assets list
const assignedAssets: AssignedAssetDetail[] = [];
assetAllocations.forEach((allocation) => {
allocation.allocatedAssets.forEach((asset) => {
assignedAssets.push({
assetID: asset.assetID,
assetCode: asset.assetCode,
assetType: asset.assetType,
quantity: 1, // Each asset is assigned individually
});
});
});
// Validate assignment request
const validation = validateAssignmentRequest(assignedAssets, allAssets, activeAssignments);
if (!validation.valid) {
console.error('Assignment validation failed:', validation.errors);
alert(`Cannot assign assets:\n${validation.errors.join('\n')}`);
return;
}
// Show warnings if any
if (validation.warnings.length > 0) {
const proceed = window.confirm(
`Warning:\n${validation.warnings.join('\n')}\n\nDo you want to proceed with the assignment?`
);
if (!proceed) {
return;
}
}
// Calculate inventory impact
const impact = calculateInventoryImpact(assignedAssets, allAssets, activeAssignments);
console.log('Inventory impact:', impact);
// Log each assignment for audit trail
assignedAssets.forEach((asset) => {
const auditLog = createInventoryAuditLog({
action: 'assignment',
assetType: asset.assetType,
assetId: asset.assetID,
assetCode: asset.assetCode,
employeeId: selectedRequest.employeeID,
employeeName: selectedRequest.employeeName,
performedBy: currentUserId,
previousState: 'available',
newState: 'assigned',
requestId: selectedRequest.requestID,
remarks: `Assigned via asset request #${selectedRequest.requestID}`
});
logInventoryChange(auditLog);
});
setIsSubmitting(true);
try {
await onAssign(selectedRequest.requestID, {
assignedAssets,
assignedBy: currentUserId,
});
console.log('Assignment completed successfully');
console.log('Updated inventory status:', impact);
setShowAssignmentDialog(false);
setSelectedRequest(null);
setAssetAllocations([]);
} catch (error) {
console.error('Error assigning assets:', error);
alert(`Failed to assign assets: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSubmitting(false);
}
};
const isAssetAllocated = (assetId: number): boolean => {
return assetAllocations.some((allocation) =>
allocation.allocatedAssets.some((a) => a.assetID === assetId)
);
};
const canAddMoreAssets = (assetType: string): boolean => {
const allocation = assetAllocations.find((a) => a.assetType === assetType);
if (!allocation) return false;
return allocation.allocatedAssets.length < allocation.requestedQuantity;
};
const getAllocationProgress = (assetType: string): { current: number; total: number } => {
const allocation = assetAllocations.find((a) => a.assetType === assetType);
if (!allocation) return { current: 0, total: 0 };
return {
current: allocation.allocatedAssets.length,
total: allocation.requestedQuantity,
};
};
const isAllocationComplete = (): boolean => {
return assetAllocations.every(
(allocation) => allocation.allocatedAssets.length === allocation.requestedQuantity
);
};
if (isLoading) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">Approved Requests - Assignment Queue</CardTitle>
</CardHeader>
<CardContent>
{approvedRequests.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Package className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">
No approved requests waiting for asset assignment
</p>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Request ID</TableHead>
<TableHead>Employee</TableHead>
<TableHead>Requested Assets</TableHead>
<TableHead>Approved Date</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{approvedRequests.map((request) => (
<TableRow key={request.requestID}>
<TableCell className="font-medium">#{request.requestID}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{request.employeeName}</span>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
{request.requestedAssets.map((asset, idx) => (
<div key={idx} className="text-sm">
{asset.assetType} (x{asset.quantity})
</div>
))}
</div>
</TableCell>
<TableCell>
{request.reviewedDate && (
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span>{format(new Date(request.reviewedDate), 'MMM dd, yyyy')}</span>
</div>
)}
</TableCell>
<TableCell className="text-right">
<Button
variant="default"
size="sm"
onClick={() => handleSelectRequest(request)}
>
<Package className="h-4 w-4 mr-2" />
Assign Assets
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
{/* Assignment Dialog */}
<Dialog open={showAssignmentDialog} onOpenChange={setShowAssignmentDialog}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Assign Assets to Request #{selectedRequest?.requestID}</DialogTitle>
<DialogDescription>
Select specific assets to assign to {selectedRequest?.employeeName}
</DialogDescription>
</DialogHeader>
{selectedRequest && (
<div className="space-y-6">
{/* Request Summary */}
<Card className="border-2">
<CardHeader className="pb-3">
<CardTitle className="text-base">Request Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Employee:</span>{' '}
<span className="font-medium">{selectedRequest.employeeName}</span>
</div>
<div>
<span className="text-muted-foreground">Submitted:</span>{' '}
<span className="font-medium">
{format(new Date(selectedRequest.submittedDate), 'MMM dd, yyyy')}
</span>
</div>
</div>
<div className="text-sm">
<span className="text-muted-foreground">Justification:</span>{' '}
<span>{selectedRequest.justification}</span>
</div>
</CardContent>
</Card>
{/* Allocation Progress */}
<div className="space-y-3">
<h3 className="text-sm font-semibold">Allocation Progress</h3>
{assetAllocations.map((allocation, idx) => {
const progress = getAllocationProgress(allocation.assetType);
const isComplete = progress.current === progress.total;
return (
<Card key={idx} className={isComplete ? 'border-green-500' : 'border-2'}>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Package className="h-4 w-4" />
<span className="font-medium">{allocation.assetType}</span>
</div>
<Badge
className={
isComplete
? 'bg-green-100 text-green-800 border-green-200'
: 'bg-yellow-100 text-yellow-800 border-yellow-200'
}
>
{progress.current}/{progress.total} Assigned
</Badge>
</div>
{/* Allocated Assets */}
{allocation.allocatedAssets.length > 0 && (
<div className="space-y-2">
{allocation.allocatedAssets.map((asset) => (
<div
key={asset.assetID}
className="flex items-center justify-between bg-muted/50 rounded p-2 text-sm"
>
<div>
<span className="font-medium">{asset.assetCode}</span>
<span className="text-muted-foreground ml-2">
{asset.brandModel}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() =>
handleRemoveAsset(asset.assetID, allocation.assetType)
}
className="h-7 w-7 p-0"
>
<Minus className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
})}
</div>
{/* Asset Selection */}
<div className="space-y-3">
<h3 className="text-sm font-semibold">Available Assets</h3>
{/* Filters */}
<div className="flex gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by code, type, brand, or serial number..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={filterAssetType} onValueChange={setFilterAssetType}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
{assetTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Assets List */}
<div className="border rounded-md max-h-64 overflow-y-auto">
{filteredAvailableAssets.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<AlertCircle className="h-8 w-8 text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">
No available assets match your search
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Asset Code</TableHead>
<TableHead>Type</TableHead>
<TableHead>Brand/Model</TableHead>
<TableHead>Condition</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAvailableAssets.map((asset) => {
const allocated = isAssetAllocated(asset.assetID);
const canAdd = canAddMoreAssets(asset.assetType);
return (
<TableRow key={asset.assetID}>
<TableCell className="font-medium">{asset.assetCode}</TableCell>
<TableCell>{asset.assetType}</TableCell>
<TableCell>{asset.brandModel}</TableCell>
<TableCell>
<Badge variant="outline">{asset.assetCondition}</Badge>
</TableCell>
<TableCell className="text-right">
{allocated ? (
<Badge className="bg-green-100 text-green-800 border-green-200">
<CheckCircle className="h-3 w-3 mr-1" />
Allocated
</Badge>
) : (
<Button
variant="outline"
size="sm"
onClick={() => handleAddAsset(asset, asset.assetType)}
disabled={!canAdd}
>
<Plus className="h-4 w-4 mr-1" />
Add
</Button>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</div>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowAssignmentDialog(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
onClick={handleAssignSubmit}
disabled={isSubmitting || !isAllocationComplete()}
className="bg-green-600 hover:bg-green-700"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Assigning...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
Confirm Assignment
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default AssetRequestAssignment;