pmtool / src /components /asset-management /shared /SystemDetailsExcelImport.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
19.3 kB
import React, { useState, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
Upload,
Download,
FileSpreadsheet,
CheckCircle,
XCircle,
AlertTriangle,
Loader2,
Settings
} from 'lucide-react';
import * as XLSX from 'xlsx';
import { SystemDetailsCreateRequest } from '@/types/asset-management';
import ImportResults from './ImportResults';
interface SystemDetailsImportData extends Omit<SystemDetailsCreateRequest, 'assetID'> {
assetID?: number;
// Asset creation fields if asset doesn't exist
assetType?: string;
brandModel?: string;
serialNumber?: string;
assetCondition?: string;
purchaseDate?: string;
warrantyExpiry?: string;
purchaseCost?: number;
currentValue?: number;
location?: string;
notes?: string;
}
interface ImportResult {
success: boolean;
data?: SystemDetailsImportData[];
errors?: string[];
warnings?: string[];
totalRows?: number;
validRows?: number;
}
interface SystemDetailsBulkImportResponse {
success: boolean;
message: string;
results: {
successful: number;
failed: number;
details: Array<{
row: number;
success: boolean;
message: string;
data?: any;
}>;
};
}
interface SystemDetailsExcelImportProps {
onImport: (systemDetails: SystemDetailsImportData[]) => Promise<SystemDetailsBulkImportResponse>;
isImporting?: boolean;
}
const SystemDetailsExcelImport: React.FC<SystemDetailsExcelImportProps> = ({
onImport,
isImporting = false
}) => {
const [file, setFile] = useState<File | null>(null);
const [importResult, setImportResult] = useState<ImportResult | null>(null);
const [apiResult, setApiResult] = useState<SystemDetailsBulkImportResponse | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Template data for Excel generation
const generateTemplate = () => {
const templateData = [
{
'Asset ID': 1,
'System Model': 'Dell Latitude 5430',
'Processor': 'Intel Core i7-1265U',
'Installed RAM': '16GB DDR4',
'Graphics': 'Intel Iris Xe Graphics',
'Screen Resolution': '1920x1080',
'Disk Model': 'Samsung SSD 980 PRO',
'Disk Size': '512GB',
'Manufacturer': 'Dell',
// Asset creation fields (used if asset ID doesn't exist)
'Asset Type': 'Laptop',
'Brand/Model': 'Dell Latitude 5430',
'Serial Number': 'DL5430-001',
'Asset Condition': 'Excellent',
'Purchase Date': '2024-01-15',
'Warranty Expiry': '2027-01-15',
'Purchase Cost': '1200.00',
'Current Value': '1000.00',
'Location': 'Office Floor 1',
'Notes': 'Primary development laptop'
},
{
'Asset ID': 999,
'System Model': 'Samsung 27" 4K Monitor',
'Processor': 'N/A',
'Installed RAM': 'N/A',
'Graphics': 'Built-in Display Controller',
'Screen Resolution': '3840x2160',
'Disk Model': 'N/A',
'Disk Size': 'N/A',
'Manufacturer': 'Samsung',
// Asset creation fields (will be used since Asset ID 999 likely doesn't exist)
'Asset Type': 'Extended Screen',
'Brand/Model': 'Samsung 27" 4K',
'Serial Number': 'SM27-4K-002',
'Asset Condition': 'Good',
'Purchase Date': '2024-02-01',
'Warranty Expiry': '2027-02-01',
'Purchase Cost': '400.00',
'Current Value': '350.00',
'Location': 'Office Floor 2',
'Notes': 'External monitor for workstation'
},
{
'Asset ID': '',
'System Model': 'MacBook Pro 16"',
'Processor': 'Apple M2 Pro',
'Installed RAM': '32GB',
'Graphics': 'Apple M2 Pro GPU',
'Screen Resolution': '3456x2234',
'Disk Model': 'Apple SSD',
'Disk Size': '1TB',
'Manufacturer': 'Apple',
// Asset creation fields (required since no Asset ID provided)
'Asset Type': 'Laptop',
'Brand/Model': 'MacBook Pro 16"',
'Serial Number': 'MBP16-003',
'Asset Condition': 'Excellent',
'Purchase Date': '2024-03-01',
'Warranty Expiry': '2027-03-01',
'Purchase Cost': '2500.00',
'Current Value': '2200.00',
'Location': 'Office Floor 3',
'Notes': 'Design team laptop'
}
];
const ws = XLSX.utils.json_to_sheet(templateData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'System Details Template');
// Set column widths
const colWidths = [
{ wch: 10 }, // Asset ID
{ wch: 25 }, // System Model
{ wch: 20 }, // Processor
{ wch: 15 }, // Installed RAM
{ wch: 25 }, // Graphics
{ wch: 15 }, // Screen Resolution
{ wch: 20 }, // Disk Model
{ wch: 12 }, // Disk Size
{ wch: 15 }, // Manufacturer
{ wch: 15 }, // Asset Type
{ wch: 25 }, // Brand/Model
{ wch: 20 }, // Serial Number
{ wch: 15 }, // Asset Condition
{ wch: 15 }, // Purchase Date
{ wch: 15 }, // Warranty Expiry
{ wch: 12 }, // Purchase Cost
{ wch: 12 }, // Current Value
{ wch: 20 }, // Location
{ wch: 30 } // Notes
];
ws['!cols'] = colWidths;
XLSX.writeFile(wb, 'system_details_import_template.xlsx');
};
const validateSystemDetailsData = (data: any[]): ImportResult => {
const errors: string[] = [];
const warnings: string[] = [];
const validSystemDetails: SystemDetailsImportData[] = [];
data.forEach((row, index) => {
const rowNum = index + 2; // +2 because Excel is 1-indexed and we skip header
const systemDetail: SystemDetailsImportData = {};
// Asset ID validation (optional - if not provided, we'll create a new asset)
if (row['Asset ID']) {
const assetId = parseInt(row['Asset ID']);
if (isNaN(assetId) || assetId <= 0) {
warnings.push(`Row ${rowNum}: Invalid Asset ID "${row['Asset ID']}". Will attempt to create new asset.`);
} else {
systemDetail.assetID = assetId;
}
}
// Required system details fields
if (!row['System Model']) {
errors.push(`Row ${rowNum}: System Model is required`);
return;
}
systemDetail.systemModel = String(row['System Model']).trim();
if (!row['Manufacturer']) {
errors.push(`Row ${rowNum}: Manufacturer is required`);
return;
}
systemDetail.manufacturer = String(row['Manufacturer']).trim();
// Optional system details fields
systemDetail.processor = row['Processor'] ? String(row['Processor']).trim() : '';
systemDetail.installedRAM = row['Installed RAM'] ? String(row['Installed RAM']).trim() : '';
systemDetail.graphics = row['Graphics'] ? String(row['Graphics']).trim() : '';
systemDetail.screenResolution = row['Screen Resolution'] ? String(row['Screen Resolution']).trim() : '';
systemDetail.diskModel = row['Disk Model'] ? String(row['Disk Model']).trim() : '';
systemDetail.diskSize = row['Disk Size'] ? String(row['Disk Size']).trim() : '';
// Asset creation fields (always include for validation, will be used if asset doesn't exist)
if (row['Asset Type']) {
systemDetail.assetType = String(row['Asset Type']).trim();
}
if (row['Brand/Model']) {
systemDetail.brandModel = String(row['Brand/Model']).trim();
}
if (row['Serial Number']) {
systemDetail.serialNumber = String(row['Serial Number']).trim();
}
if (row['Asset Condition']) {
systemDetail.assetCondition = String(row['Asset Condition']).trim();
}
// If no asset ID provided, require asset creation fields
if (!systemDetail.assetID) {
if (!systemDetail.assetType) {
errors.push(`Row ${rowNum}: Asset Type is required when Asset ID is not provided`);
return;
}
if (!systemDetail.brandModel) {
errors.push(`Row ${rowNum}: Brand/Model is required when Asset ID is not provided`);
return;
}
if (!systemDetail.serialNumber) {
errors.push(`Row ${rowNum}: Serial Number is required when Asset ID is not provided`);
return;
}
if (!systemDetail.assetCondition) {
errors.push(`Row ${rowNum}: Asset Condition is required when Asset ID is not provided`);
return;
}
}
// Optional asset fields (always include for potential asset creation)
if (row['Purchase Date']) {
const purchaseDate = new Date(row['Purchase Date']);
if (isNaN(purchaseDate.getTime())) {
warnings.push(`Row ${rowNum}: Invalid Purchase Date format`);
} else {
systemDetail.purchaseDate = purchaseDate.toISOString().split('T')[0];
}
}
if (row['Warranty Expiry']) {
const warrantyExpiry = new Date(row['Warranty Expiry']);
if (isNaN(warrantyExpiry.getTime())) {
warnings.push(`Row ${rowNum}: Invalid Warranty Expiry format`);
} else {
systemDetail.warrantyExpiry = warrantyExpiry.toISOString().split('T')[0];
}
}
if (row['Purchase Cost']) {
const cost = parseFloat(row['Purchase Cost']);
if (isNaN(cost) || cost < 0) {
warnings.push(`Row ${rowNum}: Invalid Purchase Cost`);
} else {
systemDetail.purchaseCost = cost;
}
}
if (row['Current Value']) {
const value = parseFloat(row['Current Value']);
if (isNaN(value) || value < 0) {
warnings.push(`Row ${rowNum}: Invalid Current Value`);
} else {
systemDetail.currentValue = value;
}
}
if (row['Location']) {
systemDetail.location = String(row['Location']).trim();
}
if (row['Notes']) {
systemDetail.notes = String(row['Notes']).trim();
}
validSystemDetails.push(systemDetail);
});
return {
success: errors.length === 0,
data: validSystemDetails,
errors,
warnings,
totalRows: data.length,
validRows: validSystemDetails.length
};
};
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
setImportResult(null);
}
};
const processFile = async () => {
if (!file) return;
setIsProcessing(true);
try {
const data = await file.arrayBuffer();
const workbook = XLSX.read(data);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
if (jsonData.length === 0) {
setImportResult({
success: false,
errors: ['The Excel file appears to be empty or has no data rows.']
});
return;
}
const result = validateSystemDetailsData(jsonData);
setImportResult(result);
} catch (error) {
setImportResult({
success: false,
errors: ['Failed to process the Excel file. Please ensure it\'s a valid Excel file.']
});
} finally {
setIsProcessing(false);
}
};
const handleImport = async () => {
if (!importResult?.data) return;
try {
const result = await onImport(importResult.data);
setApiResult(result);
// Only clear the form if the import was completely successful
if (result.success && result.results.failed === 0) {
setFile(null);
setImportResult(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
} catch (error) {
console.error('Import failed:', error);
}
};
const resetImport = () => {
setFile(null);
setImportResult(null);
setApiResult(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="space-y-6">
{/* Template Download */}
<Card>
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2">
<FileSpreadsheet className="h-5 w-5" />
Excel Template
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Download the Excel template to ensure your data is formatted correctly for import.
If you provide an Asset ID, the system will check if it exists - if not found, a new asset will be created using the asset fields.
If no Asset ID is provided, a new asset will be created automatically.
</p>
<Button
onClick={generateTemplate}
variant="outline"
className="flex items-center gap-2"
>
<Download className="h-4 w-4" />
Download Template
</Button>
</CardContent>
</Card>
{/* File Upload */}
<Card>
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
Upload Excel File
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="excel-file">Select Excel File</Label>
<Input
id="excel-file"
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileSelect}
className="mt-1"
/>
</div>
{file && (
<div className="flex items-center justify-between p-3 bg-muted rounded-lg">
<div className="flex items-center gap-2">
<FileSpreadsheet className="h-4 w-4" />
<span className="text-sm font-medium">{file.name}</span>
<Badge variant="secondary">{(file.size / 1024).toFixed(1)} KB</Badge>
</div>
<div className="flex gap-2">
<Button
onClick={processFile}
disabled={isProcessing}
size="sm"
>
{isProcessing ? (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
Processing...
</>
) : (
'Process File'
)}
</Button>
<Button
onClick={resetImport}
variant="outline"
size="sm"
>
Remove
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{/* Import Results */}
{importResult && (
<Card>
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2">
{importResult.success ? (
<CheckCircle className="h-5 w-5 text-green-600" />
) : (
<XCircle className="h-5 w-5 text-red-600" />
)}
Import Results
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Summary */}
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-3 bg-muted rounded-lg">
<div className="text-2xl font-bold">{importResult.totalRows || 0}</div>
<div className="text-sm text-muted-foreground">Total Rows</div>
</div>
<div className="text-center p-3 bg-muted rounded-lg">
<div className="text-2xl font-bold text-green-600">{importResult.validRows || 0}</div>
<div className="text-sm text-muted-foreground">Valid Rows</div>
</div>
</div>
{/* Errors */}
{importResult.errors && importResult.errors.length > 0 && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription>
<div className="font-medium mb-2">Errors found:</div>
<ul className="list-disc list-inside space-y-1 text-sm">
{importResult.errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
{/* Warnings */}
{importResult.warnings && importResult.warnings.length > 0 && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<div className="font-medium mb-2">Warnings:</div>
<ul className="list-disc list-inside space-y-1 text-sm">
{importResult.warnings.map((warning, index) => (
<li key={index}>{warning}</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
{/* Import Button */}
{importResult.success && importResult.data && importResult.data.length > 0 && (
<div className="flex justify-end gap-2 pt-4">
<Button
onClick={resetImport}
variant="outline"
>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={isImporting}
className="flex items-center gap-2"
>
{isImporting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Importing...
</>
) : (
<>
<Settings className="h-4 w-4" />
Import {importResult.data.length} System Details
</>
)}
</Button>
</div>
)}
</CardContent>
</Card>
)}
{/* API Import Results */}
{apiResult && (
<ImportResults
result={apiResult}
onClose={resetImport}
/>
)}
</div>
);
};
export default SystemDetailsExcelImport;