Files
membership-fe/src/components/WordPressImportWizard.js

988 lines
42 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Progress } from './ui/progress';
import { Alert, AlertDescription } from './ui/alert';
import { toast } from 'sonner';
import api from '../utils/api';
import {
Upload,
FileCheck,
CheckCircle,
Eye,
Play,
CheckCircle2,
AlertCircle,
AlertTriangle,
Trash2,
FileDown,
Users,
ChevronLeft,
ChevronRight,
Loader2
} from 'lucide-react';
/**
* WordPress Import Wizard Component
*
* A comprehensive 6-step wizard for importing WordPress users to LOAF platform.
* Features:
* - CSV upload and analysis
* - Interactive status review and adjustment
* - Preview before import
* - Real-time import progress
* - Full rollback capability
* - Error reporting
*/
export default function WordPressImportWizard({ open, onOpenChange, onSuccess }) {
// Wizard state
const [currentStep, setCurrentStep] = useState(1);
const [importJobId, setImportJobId] = useState(null);
// Data state
const [uploadedFile, setUploadedFile] = useState(null);
const [analysisResult, setAnalysisResult] = useState(null);
const [previewData, setPreviewData] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
// Override state
const [statusOverrides, setStatusOverrides] = useState({});
const [selectedRows, setSelectedRows] = useState(new Set());
// Import execution state
const [importing, setImporting] = useState(false);
const [importProgress, setImportProgress] = useState(0);
const [importResults, setImportResults] = useState(null);
// UI state
const [uploading, setUploading] = useState(false);
const [loading, setLoading] = useState(false);
// Step definitions
const steps = [
{ number: 1, title: 'Upload CSV', icon: Upload },
{ number: 2, title: 'Field Mapping', icon: FileCheck },
{ number: 3, title: 'Review Status', icon: CheckCircle },
{ number: 4, title: 'Preview', icon: Eye },
{ number: 5, title: 'Execute', icon: Play },
{ number: 6, title: 'Results', icon: CheckCircle2 }
];
// Reset wizard state when dialog opens/closes
useEffect(() => {
if (!open) {
setTimeout(() => {
setCurrentStep(1);
setImportJobId(null);
setUploadedFile(null);
setAnalysisResult(null);
setPreviewData([]);
setStatusOverrides({});
setSelectedRows(new Set());
setImporting(false);
setImportProgress(0);
setImportResults(null);
}, 300); // Wait for dialog close animation
}
}, [open]);
// ============================================================================
// Step Navigation
// ============================================================================
const canProceed = () => {
switch (currentStep) {
case 1:
return uploadedFile && analysisResult;
case 2:
return true; // Field mapping auto-detected
case 3:
return true; // Status review is optional
case 4:
return true; // Preview is informational
case 5:
return !importing;
case 6:
return true;
default:
return false;
}
};
const handleNext = () => {
if (currentStep < 6 && canProceed()) {
if (currentStep === 3) {
// Load preview data when moving from step 3 to 4
loadPreviewData(1);
}
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
// ============================================================================
// Step 1: Upload CSV
// ============================================================================
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
if (!file.name.endsWith('.csv')) {
toast.error('Please upload a CSV file');
return;
}
setUploadedFile(file);
}
};
const handleUpload = async () => {
if (!uploadedFile) return;
setUploading(true);
const formData = new FormData();
formData.append('file', uploadedFile);
try {
const response = await api.post('/admin/import/upload-csv', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
setImportJobId(response.data.import_job_id);
setAnalysisResult(response.data);
toast.success(`CSV analyzed: ${response.data.valid_rows} valid rows, ${response.data.warnings} warnings`);
// Auto-advance to next step
setTimeout(() => setCurrentStep(2), 500);
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to upload CSV');
} finally {
setUploading(false);
}
};
// ============================================================================
// Step 3: Review & Adjust Status
// ============================================================================
const loadPreviewData = async (page = 1) => {
if (!importJobId) return;
setLoading(true);
try {
const response = await api.get(`/admin/import/${importJobId}/preview`, {
params: { page, page_size: 50 }
});
setPreviewData(response.data.rows);
setCurrentPage(response.data.page);
setTotalPages(response.data.total_pages);
} catch (error) {
toast.error('Failed to load preview data');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (currentStep === 3 && importJobId && previewData.length === 0) {
loadPreviewData(1);
}
}, [currentStep, importJobId]);
const handleStatusOverride = (rowNum, status) => {
setStatusOverrides(prev => ({
...prev,
[rowNum]: { status }
}));
};
const handleBulkStatusChange = (status) => {
const newOverrides = { ...statusOverrides };
selectedRows.forEach(rowNum => {
newOverrides[rowNum] = { status };
});
setStatusOverrides(newOverrides);
toast.success(`Updated ${selectedRows.size} users to ${status}`);
};
const toggleRowSelection = (rowNum) => {
setSelectedRows(prev => {
const newSet = new Set(prev);
if (newSet.has(rowNum)) {
newSet.delete(rowNum);
} else {
newSet.add(rowNum);
}
return newSet;
});
};
const toggleSelectAll = () => {
if (selectedRows.size === previewData.length) {
setSelectedRows(new Set());
} else {
setSelectedRows(new Set(previewData.map(row => row.row_number)));
}
};
// ============================================================================
// Step 5: Execute Import
// ============================================================================
const handleExecuteImport = async () => {
setImporting(true);
setCurrentStep(5);
try {
// Start import
const response = await api.post(`/admin/import/${importJobId}/execute`, {
overrides: statusOverrides,
options: {
send_password_emails: true,
skip_errors: true
}
});
setImportResults(response.data);
toast.success(`Import completed: ${response.data.successful_rows} users imported`);
// Move to results step
setCurrentStep(6);
} catch (error) {
toast.error(error.response?.data?.detail || 'Import failed');
} finally {
setImporting(false);
}
};
// Poll for import progress
useEffect(() => {
if (currentStep === 5 && importing && importJobId) {
const interval = setInterval(async () => {
try {
const response = await api.get(`/admin/import/${importJobId}/status`);
setImportProgress(response.data.progress_percent);
} catch (error) {
console.error('Failed to fetch import status:', error);
}
}, 1000);
return () => clearInterval(interval);
}
}, [currentStep, importing, importJobId]);
// ============================================================================
// Step 6: Rollback
// ============================================================================
const [rollbackConfirmOpen, setRollbackConfirmOpen] = useState(false);
const [confirmText, setConfirmText] = useState('');
const handleRollback = async () => {
try {
await api.post(`/admin/import/${importJobId}/rollback`, { confirm: true });
toast.success(`Rolled back ${importResults.successful_rows} users`);
onOpenChange(false);
if (onSuccess) onSuccess();
} catch (error) {
toast.error(error.response?.data?.detail || 'Rollback failed');
}
};
const handleDownloadErrors = async () => {
try {
const response = await api.get(`/admin/import/${importJobId}/errors/download`, {
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `import_errors_${importJobId}.csv`);
document.body.appendChild(link);
link.click();
link.remove();
toast.success('Error report downloaded');
} catch (error) {
toast.error('Failed to download error report');
}
};
// ============================================================================
// Status Badge Component
// ============================================================================
const StatusBadge = ({ status }) => {
const colors = {
active: 'bg-green-100 text-green-800 border-green-300',
pre_validated: 'bg-blue-100 text-blue-800 border-blue-300',
payment_pending: 'bg-yellow-100 text-yellow-800 border-yellow-300',
inactive: 'bg-gray-100 text-gray-800 border-gray-300'
};
return (
<Badge variant="outline" className={colors[status] || 'bg-gray-100 text-gray-800'}>
{status.replace('_', ' ')}
</Badge>
);
};
// ============================================================================
// Render Step Content
// ============================================================================
const renderStepContent = () => {
switch (currentStep) {
case 1:
return <Step1Upload />;
case 2:
return <Step2FieldMapping />;
case 3:
return <Step3ReviewStatus />;
case 4:
return <Step4Preview />;
case 5:
return <Step5Execute />;
case 6:
return <Step6Results />;
default:
return null;
}
};
// ============================================================================
// Step 1: Upload CSV
// ============================================================================
const Step1Upload = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-primary mb-2">Upload WordPress CSV Export</h3>
<p className="text-sm text-muted-foreground">
Select the WordPress user export CSV file. The file will be analyzed for data quality issues.
</p>
</div>
<Card className="p-6 border-2 border-dashed border-chart-6 bg-lavender-mist">
<div className="flex flex-col items-center gap-4">
<Upload className="h-12 w-12 text-muted-foreground" />
<div className="text-center">
<Input
type="file"
accept=".csv"
onChange={handleFileSelect}
className="max-w-xs"
/>
{uploadedFile && (
<p className="text-sm text-muted-foreground mt-2">
Selected: {uploadedFile.name}
</p>
)}
</div>
</div>
</Card>
{uploadedFile && !analysisResult && (
<Button
onClick={handleUpload}
disabled={uploading}
className="w-full bg-muted-foreground hover:bg-primary"
>
{uploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Analyzing CSV...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload and Analyze
</>
)}
</Button>
)}
{analysisResult && (
<Card className="p-6 bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-4">Analysis Complete</h4>
<div className="grid md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-green-700">Total Rows</p>
<p className="text-2xl font-semibold text-green-900">{analysisResult.total_rows}</p>
</div>
<div>
<p className="text-sm text-green-700">Valid Rows</p>
<p className="text-2xl font-semibold text-green-900">{analysisResult.valid_rows}</p>
</div>
<div>
<p className="text-sm text-yellow-700">Warnings</p>
<p className="text-2xl font-semibold text-yellow-900">{analysisResult.warnings}</p>
</div>
<div>
<p className="text-sm text-red-700">Errors</p>
<p className="text-2xl font-semibold text-red-900">{analysisResult.errors}</p>
</div>
</div>
{analysisResult.data_quality && (
<div className="mt-4 pt-4 border-t border-green-300">
<h5 className="text-sm font-semibold text-green-900 mb-2">Data Quality Issues:</h5>
<ul className="text-sm text-green-800 space-y-1">
{analysisResult.data_quality.invalid_dob > 0 && (
<li> {analysisResult.data_quality.invalid_dob} invalid dates of birth</li>
)}
{analysisResult.data_quality.missing_phone > 0 && (
<li> {analysisResult.data_quality.missing_phone} missing phone numbers</li>
)}
{analysisResult.data_quality.duplicate_email > 0 && (
<li> {analysisResult.data_quality.duplicate_email} duplicate emails</li>
)}
</ul>
</div>
)}
</Card>
)}
</div>
);
// ============================================================================
// Step 2: Field Mapping
// ============================================================================
const Step2FieldMapping = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-primary mb-2">Field Mapping</h3>
<p className="text-sm text-muted-foreground">
WordPress fields have been automatically mapped to LOAF platform fields.
</p>
</div>
<Card className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>WordPress Field</TableHead>
<TableHead></TableHead>
<TableHead>LOAF Field</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono text-sm">user_email</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">email</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">first_name</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">first_name</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">last_name</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">last_name</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">cell_phone</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">phone</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">date_of_birth</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">date_of_birth</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-sm">wp_capabilities</TableCell>
<TableCell></TableCell>
<TableCell className="font-mono text-sm">role + status</TableCell>
</TableRow>
</TableBody>
</Table>
</Card>
<Alert className="bg-blue-50 border-blue-200">
<AlertCircle className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-blue-800">
WordPress roles will be automatically converted:
<ul className="mt-2 ml-4 space-y-1">
<li> <code>loaf_admin</code> admin (active)</li>
<li> <code>loaf_treasure</code> finance (active)</li>
<li> <code>administrator</code> superadmin (active)</li>
<li> <code>pms_subscription_plan_63</code> member</li>
</ul>
</AlertDescription>
</Alert>
</div>
);
// ============================================================================
// Step 3: Review & Adjust Status (KEY FEATURE)
// ============================================================================
const Step3ReviewStatus = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-primary mb-2">Review & Adjust User Status</h3>
<p className="text-sm text-muted-foreground">
Review suggested status mappings and override as needed before import.
</p>
</div>
{/* Bulk edit toolbar */}
<Card className="p-4 bg-lavender-mist border-chart-6">
<div className="flex items-center gap-4">
<Checkbox
checked={selectedRows.size === previewData.length && previewData.length > 0}
onCheckedChange={toggleSelectAll}
/>
<span className="text-sm text-muted-foreground font-medium">
{selectedRows.size > 0 ? `${selectedRows.size} selected` : 'Select all'}
</span>
{selectedRows.size > 0 && (
<Select onValueChange={handleBulkStatusChange}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Change status to..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
<SelectItem value="payment_pending">Payment Pending</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
)}
</div>
</Card>
{/* Data table */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-lavender-mist">
<TableHead className="w-12">
<Checkbox checked={false} />
</TableHead>
<TableHead>Row</TableHead>
<TableHead>Email</TableHead>
<TableHead>Name</TableHead>
<TableHead>WP Role</TableHead>
<TableHead>Suggested Status</TableHead>
<TableHead>Override Status</TableHead>
<TableHead>Issues</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{previewData.map((row) => (
<TableRow key={row.row_number}>
<TableCell>
<Checkbox
checked={selectedRows.has(row.row_number)}
onCheckedChange={() => toggleRowSelection(row.row_number)}
/>
</TableCell>
<TableCell className="font-mono text-sm">{row.row_number}</TableCell>
<TableCell className="text-sm">{row.email}</TableCell>
<TableCell className="text-sm">
{row.first_name} {row.last_name}
</TableCell>
<TableCell>
<Badge className="bg-chart-6 text-primary">
{row.wordpress_roles?.join(', ') || 'N/A'}
</Badge>
</TableCell>
<TableCell>
<StatusBadge status={row.suggested_status} />
</TableCell>
<TableCell>
<Select
value={statusOverrides[row.row_number]?.status || row.suggested_status}
onValueChange={(value) => handleStatusOverride(row.row_number, value)}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="pre_validated">Pre-Validated</SelectItem>
<SelectItem value="payment_pending">Payment Pending</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
{row.warnings?.map((warning, idx) => (
<Badge
key={idx}
variant="outline"
className="text-orange-600 border-orange-300 mr-1"
>
<AlertCircle className="h-3 w-3 mr-1" />
{warning}
</Badge>
))}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => loadPreviewData(currentPage - 1)}
disabled={currentPage === 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => loadPreviewData(currentPage + 1)}
disabled={currentPage === totalPages || loading}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
);
// ============================================================================
// Step 4: Preview
// ============================================================================
const Step4Preview = () => {
const overrideCount = Object.keys(statusOverrides).length;
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-primary mb-2">Import Preview</h3>
<p className="text-sm text-muted-foreground">
Review the final import settings before execution.
</p>
</div>
<div className="grid md:grid-cols-3 gap-4">
<Card className="p-6">
<p className="text-sm text-muted-foreground">Total Users</p>
<p className="text-3xl font-semibold text-primary">{analysisResult?.total_rows}</p>
</Card>
<Card className="p-6">
<p className="text-sm text-muted-foreground">Status Overrides</p>
<p className="text-3xl font-semibold text-primary">{overrideCount}</p>
</Card>
<Card className="p-6">
<p className="text-sm text-muted-foreground">Expected Imports</p>
<p className="text-3xl font-semibold text-primary">{analysisResult?.valid_rows}</p>
</Card>
</div>
<Card className="p-6">
<h4 className="font-semibold text-primary mb-4">Import Options</h4>
<div className="space-y-3">
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-muted-foreground">Send password reset emails to all imported users</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-muted-foreground">Skip rows with errors and continue import</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-600" />
<span className="text-sm text-muted-foreground">Full rollback capability available after import</span>
</div>
</div>
</Card>
{overrideCount > 0 && (
<Alert className="bg-yellow-50 border-yellow-200">
<AlertCircle className="h-4 w-4 text-yellow-600" />
<AlertDescription className="text-yellow-800">
You have overridden {overrideCount} user status{overrideCount > 1 ? 'es' : ''}.
These will be applied during import.
</AlertDescription>
</Alert>
)}
</div>
);
};
// ============================================================================
// Step 5: Execute
// ============================================================================
const Step5Execute = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-primary mb-2">
{importing ? 'Import in Progress...' : 'Ready to Import'}
</h3>
<p className="text-sm text-muted-foreground">
{importing
? 'Please wait while users are imported. This may take a few minutes.'
: 'Click "Start Import" to begin importing users.'}
</p>
</div>
{importing && (
<div className="space-y-4">
<Progress value={importProgress} className="w-full" />
<p className="text-center text-sm text-muted-foreground">
{importProgress.toFixed(1)}% complete
</p>
</div>
)}
{!importing && !importResults && (
<Button
onClick={handleExecuteImport}
className="w-full bg-muted-foreground hover:bg-primary py-6 text-lg"
>
<Play className="mr-2 h-5 w-5" />
Start Import
</Button>
)}
</div>
);
// ============================================================================
// Step 6: Results & Rollback
// ============================================================================
const Step6Results = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-primary mb-2">Import Complete</h3>
<p className="text-sm text-muted-foreground">
Review the import results and download error reports if needed.
</p>
</div>
{/* Stats cards */}
<div className="grid md:grid-cols-3 gap-4">
<Card className="p-6 bg-green-50 border-green-200">
<p className="text-sm text-green-700">Successful Imports</p>
<p className="text-4xl font-semibold text-green-900">{importResults?.successful_rows || 0}</p>
</Card>
<Card className="p-6 bg-red-50 border-red-200">
<p className="text-sm text-red-700">Failed Imports</p>
<p className="text-4xl font-semibold text-red-900">{importResults?.failed_rows || 0}</p>
</Card>
<Card className="p-6 bg-blue-50 border-blue-200">
<p className="text-sm text-blue-700">Password Emails Sent</p>
<p className="text-4xl font-semibold text-blue-900">{importResults?.password_emails_queued || 0}</p>
</Card>
</div>
{/* Action buttons */}
<div className="flex gap-4 justify-between flex-wrap">
<div className="flex gap-4 flex-wrap">
{importResults?.failed_rows > 0 && (
<Button onClick={handleDownloadErrors} variant="outline">
<FileDown className="h-4 w-4 mr-2" />
Download Error Report
</Button>
)}
<Button
onClick={() => {
onOpenChange(false);
if (onSuccess) onSuccess();
}}
variant="outline"
>
<Users className="h-4 w-4 mr-2" />
View Imported Members
</Button>
</div>
{/* Rollback button (prominent, red) */}
{importResults?.successful_rows > 0 && (
<Button
onClick={() => setRollbackConfirmOpen(true)}
variant="destructive"
className="bg-red-600 hover:bg-red-700"
>
<Trash2 className="h-4 w-4 mr-2" />
Rollback Import ({importResults.successful_rows} users)
</Button>
)}
</div>
{/* Rollback confirmation dialog */}
<Dialog open={rollbackConfirmOpen} onOpenChange={setRollbackConfirmOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="p-3 bg-red-100 rounded-full">
<AlertTriangle className="h-6 w-6 text-red-600" />
</div>
<DialogTitle className="text-2xl font-semibold text-primary">
Confirm Rollback
</DialogTitle>
</div>
<DialogDescription className="text-muted-foreground">
This will permanently delete{' '}
<strong>{importResults?.successful_rows} users</strong> that were imported.
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 my-4">
<p className="text-sm text-red-800 font-medium mb-2">
Type "DELETE {importResults?.successful_rows} USERS" to confirm:
</p>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
className="mt-2"
placeholder={`DELETE ${importResults?.successful_rows} USERS`}
/>
</div>
<DialogFooter>
<Button onClick={() => setRollbackConfirmOpen(false)} variant="outline">
Cancel
</Button>
<Button
onClick={handleRollback}
disabled={confirmText !== `DELETE ${importResults?.successful_rows} USERS`}
className="bg-red-600 hover:bg-red-700"
>
Yes, Delete {importResults?.successful_rows} Users
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
// ============================================================================
// Main Render
// ============================================================================
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl font-semibold text-primary">
WordPress Import Wizard
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Import WordPress users with interactive status review and full rollback capability
</DialogDescription>
</DialogHeader>
{/* Step indicator */}
<div className="flex items-center justify-between mb-6 px-4">
{steps.map((step, index) => {
const StepIcon = step.icon;
const isCompleted = currentStep > step.number;
const isCurrent = currentStep === step.number;
return (
<React.Fragment key={step.number}>
<div className="flex flex-col items-center">
<div
className={`
w-10 h-10 rounded-full flex items-center justify-center
${isCurrent ? 'bg-muted-foreground text-white' : ''}
${isCompleted ? 'bg-green-600 text-white' : ''}
${!isCurrent && !isCompleted ? 'bg-gray-200 text-gray-600' : ''}
`}
>
{isCompleted ? (
<CheckCircle className="h-5 w-5" />
) : (
<StepIcon className="h-5 w-5" />
)}
</div>
<p className={`text-xs mt-1 ${isCurrent ? 'font-semibold text-primary' : 'text-gray-600'}`}>
{step.title}
</p>
</div>
{index < steps.length - 1 && (
<div className={`flex-1 h-0.5 mx-2 ${isCompleted ? 'bg-green-600' : 'bg-gray-300'}`} />
)}
</React.Fragment>
);
})}
</div>
{/* Step content */}
<div className="py-6">
{renderStepContent()}
</div>
{/* Navigation footer */}
<DialogFooter className="flex justify-between items-center">
<Button
variant="outline"
onClick={handleBack}
disabled={currentStep === 1 || importing}
>
<ChevronLeft className="h-4 w-4 mr-2" />
Back
</Button>
{currentStep < 5 && (
<Button
onClick={handleNext}
disabled={!canProceed()}
className="bg-muted-foreground hover:bg-primary"
>
Next
<ChevronRight className="h-4 w-4 ml-2" />
</Button>
)}
{currentStep === 6 && (
<Button
onClick={() => {
onOpenChange(false);
if (onSuccess) onSuccess();
}}
className="bg-muted-foreground hover:bg-primary"
>
Close
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}