'use client' 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 { Alert, AlertDescription } from '@/components/ui/alert' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Progress } from '@/components/ui/progress' import { Upload, Download, FileText, AlertTriangle, CheckCircle, X } from 'lucide-react' import Papa from 'papaparse' interface ColumnDefinition { key: string label: string required?: boolean } interface CsvImportProps { title: string description: string templateColumns: Array onImport: (data: any[], onProgress?: (progress: number) => void) => Promise<{ success: boolean message: string successCount?: number errorCount?: number errors?: string[] }> sampleData?: Record[] validationRules?: { required?: string[] formats?: Record boolean> transforms?: Record any> } maxFileSize?: number // in MB children?: React.ReactNode } export default function CsvImport({ title, description, templateColumns, onImport, sampleData = [], validationRules = {}, maxFileSize = 10, children }: CsvImportProps) { const [isOpen, setIsOpen] = useState(false) const [file, setFile] = useState(null) const [importing, setImporting] = useState(false) const [progress, setProgress] = useState(0) const [result, setResult] = useState<{ success: boolean message: string successCount?: number errorCount?: number errors?: string[] } | null>(null) const [preview, setPreview] = useState([]) const [validationErrors, setValidationErrors] = useState([]) const fileInputRef = useRef(null) // Normalize templateColumns to always be ColumnDefinition objects const normalizedColumns: ColumnDefinition[] = templateColumns.map(col => typeof col === 'string' ? { key: col, label: col, required: false } : col ) const downloadTemplate = () => { const headers = normalizedColumns.map(col => col.label) const csvContent = Papa.unparse([ headers, ...sampleData.map(row => normalizedColumns.map(col => row[col.key] || '')) ]) const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) const link = document.createElement('a') const url = URL.createObjectURL(blob) link.setAttribute('href', url) link.setAttribute('download', `${title.toLowerCase().replace(/\s+/g, '_')}_template.csv`) link.style.visibility = 'hidden' document.body.appendChild(link) link.click() document.body.removeChild(link) } const validateData = (data: any[]): string[] => { const errors: string[] = [] const { required = [], formats = {}, transforms = {} } = validationRules // Get required fields from template columns and validation rules const requiredFields = [ ...required, ...normalizedColumns.filter(col => col.required).map(col => col.key) ] data.forEach((row, index) => { // Check required fields requiredFields.forEach(field => { if (!row[field] || row[field].toString().trim() === '') { const column = normalizedColumns.find(col => col.key === field) const fieldLabel = column ? column.label : field errors.push(`Row ${index + 1}: ${fieldLabel} is required`) } }) // Check format validations Object.entries(formats).forEach(([field, validator]) => { if (row[field] && !validator(row[field])) { const column = normalizedColumns.find(col => col.key === field) const fieldLabel = column ? column.label : field errors.push(`Row ${index + 1}: ${fieldLabel} has invalid format`) } }) // Apply transformations Object.entries(transforms).forEach(([field, transformer]) => { if (row[field]) { try { row[field] = transformer(row[field]) } catch (error) { const column = normalizedColumns.find(col => col.key === field) const fieldLabel = column ? column.label : field errors.push(`Row ${index + 1}: ${fieldLabel} transformation failed`) } } }) }) return errors } const handleFileChange = (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0] if (!selectedFile) return // Check file size if (selectedFile.size > maxFileSize * 1024 * 1024) { setValidationErrors([`File size must be less than ${maxFileSize}MB`]) return } // Check file type if (!selectedFile.name.toLowerCase().endsWith('.csv')) { setValidationErrors(['Please select a CSV file']) return } setFile(selectedFile) setValidationErrors([]) setResult(null) // Parse and preview the file Papa.parse(selectedFile, { header: true, skipEmptyLines: true, complete: (results) => { if (results.errors.length > 0) { setValidationErrors(results.errors.map(err => err.message)) return } const data = results.data as any[] const errors = validateData(data) if (errors.length > 0) { setValidationErrors(errors.slice(0, 10)) // Show first 10 errors return } setPreview(data.slice(0, 5)) // Show first 5 rows setValidationErrors([]) }, error: (error) => { setValidationErrors([error.message]) } }) } const handleImport = async () => { if (!file) return setImporting(true) setProgress(0) setResult(null) try { Papa.parse(file, { header: true, skipEmptyLines: true, complete: async (results) => { try { const data = results.data as any[] const errors = validateData(data) if (errors.length > 0) { setResult({ success: false, message: 'Validation failed', errors: errors }) return } const result = await onImport(data, setProgress) setResult(result) if (result.success) { setFile(null) setPreview([]) if (fileInputRef.current) { fileInputRef.current.value = '' } } } catch (error) { setResult({ success: false, message: error instanceof Error ? error.message : 'Import failed' }) } finally { setImporting(false) setProgress(0) } } }) } catch (error) { setResult({ success: false, message: error instanceof Error ? error.message : 'Import failed' }) setImporting(false) setProgress(0) } } const resetDialog = () => { setFile(null) setPreview([]) setValidationErrors([]) setResult(null) setProgress(0) if (fileInputRef.current) { fileInputRef.current.value = '' } } return ( { setIsOpen(open) if (!open) resetDialog() }}> {children || ( )} {title} {description}
{/* Download Template */}

Download Template

Download CSV template with required columns

{/* File Upload */}

Maximum file size: {maxFileSize}MB. Only CSV files are supported.

{/* Validation Errors */} {validationErrors.length > 0 && (

Validation Errors:

    {validationErrors.map((error, index) => (
  • {error}
  • ))}
)} {/* Preview */} {preview.length > 0 && (

Preview (First 5 rows)

{normalizedColumns.map(col => ( ))} {preview.map((row, index) => ( {normalizedColumns.map(col => ( ))} ))}
{col.label} {col.required && *}
{row[col.key]?.toString() || ''}
)} {/* Import Progress */} {importing && (
Importing... {Math.round(progress)}%
)} {/* Import Result */} {result && ( {result.success ? ( ) : ( )}

{result.message}

{result.successCount !== undefined && (

Successfully imported: {result.successCount} records

)} {result.errorCount !== undefined && result.errorCount > 0 && (

Failed to import: {result.errorCount} records

)} {result.errors && result.errors.length > 0 && (

Errors:

    {result.errors.slice(0, 10).map((error, index) => (
  • {error}
  • ))} {result.errors.length > 10 && (
  • ... and {result.errors.length - 10} more errors
  • )}
)}
)} {/* Action Buttons */}
) }