devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
15.6 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
} from 'lucide-react';
import * as XLSX from 'xlsx';
import { AssetCreateRequest, ASSET_TYPES, ASSET_CONDITIONS, BulkImportResponse } from '@/types/asset-management';
import ImportResults from './ImportResults';
interface ImportResult {
success: boolean;
data?: AssetCreateRequest[];
errors?: string[];
warnings?: string[];
totalRows?: number;
validRows?: number;
}
interface ExcelImportProps {
onImport: (assets: AssetCreateRequest[]) => Promise<BulkImportResponse>;
isImporting?: boolean;
}
const ExcelImport: React.FC<ExcelImportProps> = ({
onImport,
isImporting = false
}) => {
const [file, setFile] = useState<File | null>(null);
const [importResult, setImportResult] = useState<ImportResult | null>(null);
const [apiResult, setApiResult] = useState<BulkImportResponse | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Template data for Excel generation
const generateTemplate = () => {
const templateData = [
{
'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 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 Type': 'Wireless Mouse',
'Brand/Model': 'Logitech MX Master 3',
'Serial Number': 'LG-MX3-003',
'Asset Condition': 'Excellent',
'Purchase Date': '2024-03-01',
'Warranty Expiry': '2026-03-01',
'Purchase Cost': '99.99',
'Current Value': '80.00',
'Location': 'Office Floor 1',
'Notes': 'Ergonomic wireless mouse'
},
{
'Asset Type': 'Laptop Stand',
'Brand/Model': 'Rain Design mStand',
'Serial Number': 'RD-MS-004',
'Asset Condition': 'Good',
'Purchase Date': '2024-03-15',
'Warranty Expiry': '2026-03-15',
'Purchase Cost': '59.99',
'Current Value': '50.00',
'Location': 'Office Floor 1',
'Notes': 'Aluminum laptop stand for ergonomics'
}
];
const ws = XLSX.utils.json_to_sheet(templateData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Assets Template');
// Set column widths
const colWidths = [
{ 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, 'assets_import_template.xlsx');
};
const validateAssetData = (data: any[]): ImportResult => {
const errors: string[] = [];
const warnings: string[] = [];
const validAssets: AssetCreateRequest[] = [];
data.forEach((row, index) => {
const rowNum = index + 2; // +2 because Excel is 1-indexed and we skip header
const asset: Partial<AssetCreateRequest> = {};
// Required fields validation
if (!row['Asset Type']) {
errors.push(`Row ${rowNum}: Asset Type is required`);
return;
}
if (!ASSET_TYPES.includes(row['Asset Type'])) {
errors.push(`Row ${rowNum}: Invalid Asset Type "${row['Asset Type']}". Must be one of: ${ASSET_TYPES.join(', ')}`);
return;
}
asset.assetType = String(row['Asset Type']).trim();
if (!row['Brand/Model']) {
errors.push(`Row ${rowNum}: Brand/Model is required`);
return;
}
asset.brandModel = String(row['Brand/Model']).trim();
if (!row['Serial Number']) {
errors.push(`Row ${rowNum}: Serial Number is required`);
return;
}
// Convert serialNumber to string (handles numbers, strings, etc.)
asset.serialNumber = String(row['Serial Number']).trim();
if (!row['Asset Condition']) {
errors.push(`Row ${rowNum}: Asset Condition is required`);
return;
}
if (!ASSET_CONDITIONS.includes(row['Asset Condition'])) {
errors.push(`Row ${rowNum}: Invalid Asset Condition "${row['Asset Condition']}". Must be one of: ${ASSET_CONDITIONS.join(', ')}`);
return;
}
asset.assetCondition = String(row['Asset Condition']).trim();
// Optional fields with validation
if (row['Purchase Date']) {
const purchaseDate = new Date(row['Purchase Date']);
if (isNaN(purchaseDate.getTime())) {
warnings.push(`Row ${rowNum}: Invalid Purchase Date format`);
} else {
asset.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 {
asset.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 {
asset.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 {
asset.currentValue = value;
}
}
if (row['Location']) {
asset.location = String(row['Location']).trim();
}
if (row['Notes']) {
asset.notes = String(row['Notes']).trim();
}
validAssets.push(asset as AssetCreateRequest);
});
return {
success: errors.length === 0,
data: validAssets,
errors,
warnings,
totalRows: data.length,
validRows: validAssets.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 = validateAssetData(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.
</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...
</>
) : (
<>
<Upload className="h-4 w-4" />
Import {importResult.data.length} Assets
</>
)}
</Button>
</div>
)}
</CardContent>
</Card>
)}
{/* API Import Results */}
{apiResult && (
<ImportResults
result={apiResult}
onClose={resetImport}
/>
)}
</div>
);
};
export default ExcelImport;