import React, { useState, useCallback, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog'; import { Button } from './ui/button'; import { Card } from './ui/card'; import { Label } from './ui/label'; import { Checkbox } from './ui/checkbox'; import { Badge } from './ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from './ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from './ui/table'; import { Tabs, TabsContent, TabsList, TabsTrigger, } from './ui/tabs'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from './ui/accordion'; import { Upload, Download, FileSpreadsheet, Users, CreditCard, Heart, Receipt, ClipboardList, CheckCircle2, XCircle, AlertTriangle, Loader2, ChevronLeft, ChevronRight, Info, FileDown, ExternalLink, } from 'lucide-react'; import { toast } from 'sonner'; import api from '../utils/api'; const STEPS = [ { id: 'templates', title: 'Download Templates', icon: Download }, { id: 'upload', title: 'Upload Files', icon: Upload }, { id: 'preview', title: 'Preview Data', icon: FileSpreadsheet }, { id: 'options', title: 'Import Options', icon: Users }, { id: 'execute', title: 'Execute Import', icon: CheckCircle2 }, ]; const FILE_TYPES = { users: { name: 'Users', icon: Users, color: 'text-blue-600', required: true }, subscriptions: { name: 'Subscriptions', icon: CreditCard, color: 'text-green-600', required: false }, donations: { name: 'Donations', icon: Heart, color: 'text-pink-600', required: false }, payments: { name: 'Payments', icon: Receipt, color: 'text-purple-600', required: false }, registration_data: { name: 'Custom Fields', icon: ClipboardList, color: 'text-orange-600', required: false }, }; const STATUS_COLORS = { active: 'bg-green-100 text-green-800', inactive: 'bg-gray-100 text-gray-800', pending_email: 'bg-yellow-100 text-yellow-800', pending_validation: 'bg-purple-100 text-purple-800', pre_validated: 'bg-blue-100 text-blue-800', payment_pending: 'bg-orange-100 text-orange-800', canceled: 'bg-red-100 text-red-800', expired: 'bg-red-100 text-red-800', }; /** * TemplateImportWizard - Template-based CSV import wizard * * Provides standardized CSV templates for importing: * - Users (required) * - Subscriptions (optional) * - Donations (optional) * - Payments (optional) * - Custom Registration Fields (optional) */ const TemplateImportWizard = ({ open, onOpenChange, onSuccess }) => { const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); const [importJobId, setImportJobId] = useState(null); // Templates state const [templates, setTemplates] = useState([]); const [templatesLoading, setTemplatesLoading] = useState(false); // File upload state const [files, setFiles] = useState({ users: null, subscriptions: null, donations: null, payments: null, registration_data: null, }); // Upload/validation results const [uploadSummary, setUploadSummary] = useState(null); // Preview state const [previewTab, setPreviewTab] = useState('users'); const [previewData, setPreviewData] = useState({}); const [previewPage, setPreviewPage] = useState(1); const [previewTotalPages, setPreviewTotalPages] = useState(1); // Import options const [options, setOptions] = useState({ skip_notifications: true, update_existing: false, import_subscriptions: true, import_donations: true, import_payments: true, import_registration_data: true, skip_errors: true, }); // Results state const [importResults, setImportResults] = useState(null); // Fetch templates on mount useEffect(() => { if (open && templates.length === 0) { fetchTemplates(); } }, [open]); const fetchTemplates = async () => { setTemplatesLoading(true); try { const response = await api.get('/admin/import/templates'); setTemplates(response.data); } catch (error) { toast.error('Failed to load templates'); } finally { setTemplatesLoading(false); } }; // Reset wizard state const resetWizard = useCallback(() => { setCurrentStep(0); setLoading(false); setImportJobId(null); setFiles({ users: null, subscriptions: null, donations: null, payments: null, registration_data: null, }); setUploadSummary(null); setPreviewData({}); setPreviewTab('users'); setPreviewPage(1); setPreviewTotalPages(1); setOptions({ skip_notifications: true, update_existing: false, import_subscriptions: true, import_donations: true, import_payments: true, import_registration_data: true, skip_errors: true, }); setImportResults(null); }, []); // Handle dialog close const handleClose = () => { if (loading) return; resetWizard(); onOpenChange(false); }; // Download template const downloadTemplate = async (templateType) => { try { const response = await api.get(`/admin/import/templates/${templateType}/download`, { responseType: 'blob', }); const blob = new Blob([response.data], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${templateType}_template.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); toast.success(`Downloaded ${templateType} template`); } catch (error) { toast.error(`Failed to download ${templateType} template`); } }; // Handle file selection const handleFileChange = (fileType, file) => { setFiles((prev) => ({ ...prev, [fileType]: file, })); }; // Step 2: Upload files const handleUpload = async () => { if (!files.users) { toast.error('Please select a Users CSV file (required)'); return; } setLoading(true); try { const formData = new FormData(); formData.append('users_file', files.users); if (files.subscriptions) { formData.append('subscriptions_file', files.subscriptions); } if (files.donations) { formData.append('donations_file', files.donations); } if (files.payments) { formData.append('payments_file', files.payments); } if (files.registration_data) { formData.append('registration_data_file', files.registration_data); } const response = await api.post('/admin/import/template/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); setImportJobId(response.data.import_job_id); setUploadSummary(response.data); // Fetch preview for users await fetchPreview(response.data.import_job_id, 'users', 1); toast.success('Files uploaded and validated successfully'); setCurrentStep(2); } catch (error) { const message = error.response?.data?.detail?.message || error.response?.data?.detail || 'Failed to upload files'; toast.error(message); } finally { setLoading(false); } }; // Fetch preview data const fetchPreview = async (jobId, fileType, page) => { try { const response = await api.get(`/admin/import/template/${jobId}/preview`, { params: { file_type: fileType, page, page_size: 10, }, }); setPreviewData((prev) => ({ ...prev, [fileType]: response.data, })); setPreviewTotalPages(response.data.total_pages); setPreviewPage(page); } catch (error) { toast.error('Failed to fetch preview'); } }; // Handle preview tab change const handlePreviewTabChange = async (tab) => { setPreviewTab(tab); setPreviewPage(1); if (!previewData[tab] && importJobId) { await fetchPreview(importJobId, tab, 1); } }; // Execute import const handleExecute = async () => { if (!importJobId) return; setLoading(true); try { const response = await api.post(`/admin/import/template/${importJobId}/execute`, { options, }); setImportResults(response.data); setCurrentStep(4); toast.success('Import completed successfully'); if (onSuccess) { onSuccess(); } } catch (error) { const message = error.response?.data?.detail || 'Failed to execute import'; toast.error(message); } finally { setLoading(false); } }; // Render step indicator const renderStepIndicator = () => (
1. Download the CSV templates below
2. Fill in your data following the template format
3. Upload your completed files in the next step
4. Review and import your data
{template.description}
{template.field_count} fields | Required: {template.required_fields.join(', ')}
The Users CSV is required. Other files are optional and will be linked by email address.
{file.name}
) : (No file selected
)}Don't send welcome or password emails during import
If email already exists, update the user instead of skipping
Continue importing even if some rows have errors
Create subscription records from subscriptions.csv
Create donation records from donations.csv
Create payment records from payments.csv
Save custom registration data from registration_data.csv
All imported users will have force_password_change enabled.
You can use the Bulk Password Reset feature to send login instructions after import.