diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js
index 7f80a5c..f22f44d 100644
--- a/src/components/AdminSidebar.js
+++ b/src/components/AdminSidebar.js
@@ -40,7 +40,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
try {
const response = await api.get('/admin/users');
const pending = response.data.filter(u =>
- ['pending_validation', 'pre_validated'].includes(u.status)
+ ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(u.status)
);
setPendingCount(pending.length);
} catch (error) {
diff --git a/src/components/Navbar.js b/src/components/Navbar.js
index 95e50c4..dfb05cc 100644
--- a/src/components/Navbar.js
+++ b/src/components/Navbar.js
@@ -141,13 +141,35 @@ const Navbar = () => {
>
Gallery
-
- Documents
-
+
+
+
+
+
+
+
+ Newsletters
+
+
+
+
+ Financials
+
+
+
+
+ Bylaws
+
+
+
+
{
Gallery
- setIsMobileMenuOpen(false)}
- className="block px-4 py-3 text-white text-base font-medium hover:bg-white/10 rounded-lg transition-colors"
- style={{ fontFamily: "'Poppins', sans-serif" }}
- >
- Documents
-
+ {/* Documents Section */}
+
+
+ Documents
+
+
setIsMobileMenuOpen(false)}
+ className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors"
+ style={{ fontFamily: "'Poppins', sans-serif" }}
+ >
+ Newsletters
+
+
setIsMobileMenuOpen(false)}
+ className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors"
+ style={{ fontFamily: "'Poppins', sans-serif" }}
+ >
+ Financials
+
+
setIsMobileMenuOpen(false)}
+ className="block px-6 py-2 text-white text-base hover:bg-white/10 rounded-lg transition-colors"
+ style={{ fontFamily: "'Poppins', sans-serif" }}
+ >
+ Bylaws
+
+
{
+ 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 (
+
+ {status.replace('_', ' ')}
+
+ );
+ };
+
+ // ============================================================================
+ // Render Step Content
+ // ============================================================================
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ case 5:
+ return ;
+ case 6:
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ // ============================================================================
+ // Step 1: Upload CSV
+ // ============================================================================
+
+ const Step1Upload = () => (
+
+
+
Upload WordPress CSV Export
+
+ Select the WordPress user export CSV file. The file will be analyzed for data quality issues.
+
+
+
+
+
+
+
+
+ {uploadedFile && (
+
+ Selected: {uploadedFile.name}
+
+ )}
+
+
+
+
+ {uploadedFile && !analysisResult && (
+
+ )}
+
+ {analysisResult && (
+
+ Analysis Complete
+
+
+
Total Rows
+
{analysisResult.total_rows}
+
+
+
Valid Rows
+
{analysisResult.valid_rows}
+
+
+
Warnings
+
{analysisResult.warnings}
+
+
+
Errors
+
{analysisResult.errors}
+
+
+
+ {analysisResult.data_quality && (
+
+
Data Quality Issues:
+
+ {analysisResult.data_quality.invalid_dob > 0 && (
+ - • {analysisResult.data_quality.invalid_dob} invalid dates of birth
+ )}
+ {analysisResult.data_quality.missing_phone > 0 && (
+ - • {analysisResult.data_quality.missing_phone} missing phone numbers
+ )}
+ {analysisResult.data_quality.duplicate_email > 0 && (
+ - • {analysisResult.data_quality.duplicate_email} duplicate emails
+ )}
+
+
+ )}
+
+ )}
+
+ );
+
+ // ============================================================================
+ // Step 2: Field Mapping
+ // ============================================================================
+
+ const Step2FieldMapping = () => (
+
+
+
Field Mapping
+
+ WordPress fields have been automatically mapped to LOAF platform fields.
+
+
+
+
+
+
+
+ WordPress Field
+ →
+ LOAF Field
+
+
+
+
+ user_email
+ →
+ email
+
+
+ first_name
+ →
+ first_name
+
+
+ last_name
+ →
+ last_name
+
+
+ cell_phone
+ →
+ phone
+
+
+ date_of_birth
+ →
+ date_of_birth
+
+
+ wp_capabilities
+ →
+ role + status
+
+
+
+
+
+
+
+
+ WordPress roles will be automatically converted:
+
+ - •
loaf_admin → admin (active)
+ - •
loaf_treasure → finance (active)
+ - •
administrator → superadmin (active)
+ - •
pms_subscription_plan_63 → member
+
+
+
+
+ );
+
+ // ============================================================================
+ // Step 3: Review & Adjust Status (KEY FEATURE)
+ // ============================================================================
+
+ const Step3ReviewStatus = () => (
+
+
+
Review & Adjust User Status
+
+ Review suggested status mappings and override as needed before import.
+
+
+
+ {/* Bulk edit toolbar */}
+
+
+ 0}
+ onCheckedChange={toggleSelectAll}
+ />
+
+ {selectedRows.size > 0 ? `${selectedRows.size} selected` : 'Select all'}
+
+ {selectedRows.size > 0 && (
+
+ )}
+
+
+
+ {/* Data table */}
+ {loading ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+
+ Row
+ Email
+ Name
+ WP Role
+ Suggested Status
+ Override Status
+ Issues
+
+
+
+ {previewData.map((row) => (
+
+
+ toggleRowSelection(row.row_number)}
+ />
+
+ {row.row_number}
+ {row.email}
+
+ {row.first_name} {row.last_name}
+
+
+
+ {row.wordpress_roles?.join(', ') || 'N/A'}
+
+
+
+
+
+
+
+
+
+ {row.warnings?.map((warning, idx) => (
+
+
+ {warning}
+
+ ))}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+
+
+
+ )}
+
+ );
+
+ // ============================================================================
+ // Step 4: Preview
+ // ============================================================================
+
+ const Step4Preview = () => {
+ const overrideCount = Object.keys(statusOverrides).length;
+
+ return (
+
+
+
Import Preview
+
+ Review the final import settings before execution.
+
+
+
+
+
+ Total Users
+ {analysisResult?.total_rows}
+
+
+ Status Overrides
+ {overrideCount}
+
+
+ Expected Imports
+ {analysisResult?.valid_rows}
+
+
+
+
+ Import Options
+
+
+
+ Send password reset emails to all imported users
+
+
+
+ Skip rows with errors and continue import
+
+
+
+ Full rollback capability available after import
+
+
+
+
+ {overrideCount > 0 && (
+
+
+
+ You have overridden {overrideCount} user status{overrideCount > 1 ? 'es' : ''}.
+ These will be applied during import.
+
+
+ )}
+
+ );
+ };
+
+ // ============================================================================
+ // Step 5: Execute
+ // ============================================================================
+
+ const Step5Execute = () => (
+
+
+
+ {importing ? 'Import in Progress...' : 'Ready to Import'}
+
+
+ {importing
+ ? 'Please wait while users are imported. This may take a few minutes.'
+ : 'Click "Start Import" to begin importing users.'}
+
+
+
+ {importing && (
+
+
+
+ {importProgress.toFixed(1)}% complete
+
+
+ )}
+
+ {!importing && !importResults && (
+
+ )}
+
+ );
+
+ // ============================================================================
+ // Step 6: Results & Rollback
+ // ============================================================================
+
+ const Step6Results = () => (
+
+
+
Import Complete
+
+ Review the import results and download error reports if needed.
+
+
+
+ {/* Stats cards */}
+
+
+ Successful Imports
+ {importResults?.successful_rows || 0}
+
+
+ Failed Imports
+ {importResults?.failed_rows || 0}
+
+
+ Password Emails Sent
+ {importResults?.password_emails_queued || 0}
+
+
+
+ {/* Action buttons */}
+
+
+ {importResults?.failed_rows > 0 && (
+
+ )}
+
+
+
+ {/* Rollback button (prominent, red) */}
+ {importResults?.successful_rows > 0 && (
+
+ )}
+
+
+ {/* Rollback confirmation dialog */}
+
+
+ );
+
+ // ============================================================================
+ // Main Render
+ // ============================================================================
+
+ return (
+
+ );
+}
diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js
index 792f25f..ca70795 100644
--- a/src/context/AuthContext.js
+++ b/src/context/AuthContext.js
@@ -60,8 +60,13 @@ export const AuthProvider = ({ children }) => {
setToken(access_token);
setUser(userData);
- // Fetch user permissions
- await fetchPermissions(access_token);
+ // Fetch user permissions (don't let this fail the login)
+ try {
+ await fetchPermissions(access_token);
+ } catch (error) {
+ console.error('Failed to fetch permissions during login, will retry later:', error);
+ // Don't throw - permissions can be fetched later if needed
+ }
return userData;
};
diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js
index 55d4054..e2d798b 100644
--- a/src/pages/Dashboard.js
+++ b/src/pages/Dashboard.js
@@ -15,9 +15,12 @@ const Dashboard = () => {
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [resendLoading, setResendLoading] = useState(false);
+ const [eventActivity, setEventActivity] = useState(null);
+ const [activityLoading, setActivityLoading] = useState(true);
useEffect(() => {
fetchUpcomingEvents();
+ fetchEventActivity();
}, []);
const fetchUpcomingEvents = async () => {
@@ -32,6 +35,17 @@ const Dashboard = () => {
}
};
+ const fetchEventActivity = async () => {
+ try {
+ const response = await api.get('/members/event-activity');
+ setEventActivity(response.data);
+ } catch (error) {
+ console.error('Failed to fetch event activity:', error);
+ } finally {
+ setActivityLoading(false);
+ }
+ };
+
const handleResendVerification = async () => {
setResendLoading(true);
try {
@@ -298,6 +312,156 @@ const Dashboard = () => {
)}
+
+ {/* Event Activity Section */}
+
+
+
+ My Event Activity
+
+
+
+ {activityLoading ? (
+
Loading event activity...
+ ) : eventActivity ? (
+
+ {/* Stats Cards */}
+
+
+
+
+
+
+
+
Total RSVPs
+
+ {eventActivity.total_rsvps}
+
+
+
+
+
+
+
+
+
+
+
Events Attended
+
+ {eventActivity.total_attended}
+
+
+
+
+
+
+ {/* Upcoming RSVP'd Events */}
+ {eventActivity.upcoming_events && eventActivity.upcoming_events.length > 0 && (
+
+
+ Upcoming Events (RSVP'd)
+
+
+ {eventActivity.upcoming_events.map((event) => (
+
+
+
+
+
{event.title}
+
+ {new Date(event.start_at).toLocaleDateString()} at{' '}
+ {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
{event.location}
+
+
+ {event.rsvp_status === 'yes' ? 'Going' :
+ event.rsvp_status === 'maybe' ? 'Maybe' : 'Not Going'}
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Past Events & Attendance */}
+ {eventActivity.past_events && eventActivity.past_events.length > 0 && (
+
+
+ Past Events
+
+
+ {eventActivity.past_events.slice(0, 5).map((event) => (
+
+
+
+
{event.title}
+
+ {new Date(event.start_at).toLocaleDateString()} at{' '}
+ {new Date(event.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
+
+
+ {event.attended ? 'Attended' : 'Did not attend'}
+
+ {event.attended && event.attended_at && (
+
+ Checked in: {new Date(event.attended_at).toLocaleDateString()}
+
+ )}
+
+
+
+ ))}
+
+ {eventActivity.past_events.length > 5 && (
+
+ Showing 5 of {eventActivity.past_events.length} past events
+
+ )}
+
+ )}
+
+ {/* No Events Message */}
+ {(!eventActivity.upcoming_events || eventActivity.upcoming_events.length === 0) &&
+ (!eventActivity.past_events || eventActivity.past_events.length === 0) && (
+
+
+
+
+ No Event Activity Yet
+
+
+ Browse upcoming events and RSVP to start building your event history!
+
+
+
+
+
+
+ )}
+
+ ) : (
+
+
+
+
+ Failed to load event activity. Please try refreshing the page.
+
+
+
+ )}
+
diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js
index b581e6b..be79d52 100644
--- a/src/pages/admin/AdminMembers.js
+++ b/src/pages/admin/AdminMembers.js
@@ -19,7 +19,7 @@ import PaymentActivationDialog from '../../components/PaymentActivationDialog';
import ConfirmationDialog from '../../components/ConfirmationDialog';
import CreateMemberDialog from '../../components/CreateMemberDialog';
import InviteStaffDialog from '../../components/InviteStaffDialog';
-import ImportMembersDialog from '../../components/ImportMembersDialog';
+import WordPressImportWizard from '../../components/WordPressImportWizard';
const AdminMembers = () => {
const navigate = useNavigate();
@@ -569,7 +569,7 @@ const AdminMembers = () => {
onSuccess={fetchMembers}
/>
- {
try {
const response = await api.get('/admin/users');
const pending = response.data.filter(user =>
- ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending'].includes(user.status)
+ ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending', 'rejected'].includes(user.status)
);
setPendingUsers(pending);
} catch (error) {
@@ -218,12 +218,28 @@ const AdminValidations = () => {
}
};
+ const handleReactivateUser = async (user) => {
+ setActionLoading(user.id);
+ try {
+ await api.put(`/admin/users/${user.id}/status`, {
+ status: 'pending_validation'
+ });
+ toast.success(`${user.first_name} ${user.last_name} has been reactivated and moved to pending validation`);
+ fetchPendingUsers();
+ } catch (error) {
+ toast.error(error.response?.data?.detail || 'Failed to reactivate user');
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
const getStatusBadge = (status) => {
const config = {
pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' },
pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' },
pre_validated: { label: 'Pre-Validated', className: 'bg-[#81B29A] text-white' },
- payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' }
+ payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' },
+ rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' }
};
const statusConfig = config[status];
@@ -302,6 +318,12 @@ const AdminValidations = () => {
{pendingUsers.filter(u => u.status === 'payment_pending').length}
+
+
Rejected
+
+ {pendingUsers.filter(u => u.status === 'rejected').length}
+
+
@@ -327,6 +349,8 @@ const AdminValidations = () => {
Awaiting Email
Pending Validation
Pre-Validated
+ Payment Pending
+ Rejected
@@ -384,7 +408,16 @@ const AdminValidations = () => {
- {user.status === 'pending_email' ? (
+ {user.status === 'rejected' ? (
+
+ ) : user.status === 'pending_email' ? (
<>