From 33a4d8f4c46f2cf733df17db68fd040ab3163720 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:35:30 +0700 Subject: [PATCH 1/9] Document Upload Dialogue update --- src/pages/admin/AdminBylaws.js | 25 ++++++++++++++++--------- src/pages/admin/AdminFinancials.js | 23 +++++++++++++++-------- src/pages/admin/AdminNewsletters.js | 25 ++++++++++++++++--------- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/pages/admin/AdminBylaws.js b/src/pages/admin/AdminBylaws.js index 38c0d56..e66ece4 100644 --- a/src/pages/admin/AdminBylaws.js +++ b/src/pages/admin/AdminBylaws.js @@ -44,7 +44,7 @@ const AdminBylaws = () => { version: '', effective_date: '', document_url: '', - document_type: 'google_drive', + document_type: 'link', is_current: false }); const [submitting, setSubmitting] = useState(false); @@ -71,9 +71,10 @@ const AdminBylaws = () => { version: '', effective_date: new Date().toISOString().split('T')[0], document_url: '', - document_type: 'google_drive', + document_type: 'link', is_current: bylaws.length === 0 // Auto-check if this is the first bylaws }); + setUploadedFile(null); setDialogOpen(true); }; @@ -246,7 +247,7 @@ const AdminBylaws = () => {
Effective Date: {formatDate(currentBylaws.effective_date)} - Document Type: {currentBylaws.document_type === 'google_drive' ? 'Google Drive' : currentBylaws.document_type.toUpperCase()} + Document Type: {currentBylaws.document_type === 'upload' ? 'PDF Upload' : 'Link'}
) : ( @@ -363,14 +364,16 @@ const AdminBylaws = () => { @@ -391,6 +394,11 @@ const AdminBylaws = () => { Selected: {uploadedFile.name}

)} + {selectedBylaws && !uploadedFile && ( +

+ Current file will be kept if no new file is selected +

+ )} ) : (
@@ -399,12 +407,11 @@ const AdminBylaws = () => { id="document_url" value={formData.document_url} onChange={(e) => setFormData({ ...formData, document_url: e.target.value })} - placeholder="https://drive.google.com/file/d/..." + placeholder="https://docs.google.com/... or https://example.com/file.pdf" required />

- {formData.document_type === 'google_drive' && 'Paste the shareable link to your Google Drive file'} - {formData.document_type === 'pdf' && 'Paste the URL to your PDF file'} + Paste the shareable link to your document (Google Drive, Dropbox, PDF URL, etc.)

)} diff --git a/src/pages/admin/AdminFinancials.js b/src/pages/admin/AdminFinancials.js index 5787f7c..e79a2b7 100644 --- a/src/pages/admin/AdminFinancials.js +++ b/src/pages/admin/AdminFinancials.js @@ -42,7 +42,7 @@ const AdminFinancials = () => { year: new Date().getFullYear(), title: '', document_url: '', - document_type: 'google_drive' + document_type: 'link' }); const [submitting, setSubmitting] = useState(false); @@ -67,8 +67,9 @@ const AdminFinancials = () => { year: new Date().getFullYear(), title: '', document_url: '', - document_type: 'google_drive' + document_type: 'link' }); + setUploadedFile(null); setDialogOpen(true); }; @@ -274,14 +275,16 @@ const AdminFinancials = () => { @@ -302,6 +305,11 @@ const AdminFinancials = () => { Selected: {uploadedFile.name}

)} + {selectedReport && !uploadedFile && ( +

+ Current file will be kept if no new file is selected +

+ )} ) : (
@@ -310,12 +318,11 @@ const AdminFinancials = () => { id="document_url" value={formData.document_url} onChange={(e) => setFormData({ ...formData, document_url: e.target.value })} - placeholder="https://drive.google.com/file/d/..." + placeholder="https://docs.google.com/... or https://example.com/file.pdf" required />

- {formData.document_type === 'google_drive' && 'Paste the shareable link to your Google Drive file'} - {formData.document_type === 'pdf' && 'Paste the URL to your PDF file'} + Paste the shareable link to your document (Google Drive, Dropbox, PDF URL, etc.)

)} diff --git a/src/pages/admin/AdminNewsletters.js b/src/pages/admin/AdminNewsletters.js index 1e63c22..e6d807b 100644 --- a/src/pages/admin/AdminNewsletters.js +++ b/src/pages/admin/AdminNewsletters.js @@ -44,7 +44,7 @@ const AdminNewsletters = () => { description: '', published_date: '', document_url: '', - document_type: 'google_docs' + document_type: 'link' }); const [submitting, setSubmitting] = useState(false); @@ -70,8 +70,9 @@ const AdminNewsletters = () => { description: '', published_date: new Date().toISOString().split('T')[0], document_url: '', - document_type: 'google_docs' + document_type: 'link' }); + setUploadedFile(null); setDialogOpen(true); }; @@ -232,7 +233,7 @@ const AdminNewsletters = () => { {formatDate(newsletter.published_date)} - {newsletter.document_type === 'google_docs' ? 'Google Docs' : newsletter.document_type.toUpperCase()} + {newsletter.document_type === 'upload' ? 'PDF Upload' : 'Link'} + + {hasPermission('users.status') && ( + + )} + + {hasPermission('users.delete') && ( + + )} From 48802fe0c6167e2549a70d4b43f12701dfd575f9 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:15:22 +0700 Subject: [PATCH 4/9] Fix invitation redirect: admin users now go to /admin instead of /admin/dashboard --- src/pages/AcceptInvitation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/AcceptInvitation.js b/src/pages/AcceptInvitation.js index 2be4be0..1825b32 100644 --- a/src/pages/AcceptInvitation.js +++ b/src/pages/AcceptInvitation.js @@ -143,7 +143,7 @@ const AcceptInvitation = () => { // Redirect based on role if (user.role === 'admin' || user.role === 'superadmin') { - navigate('/admin/dashboard'); + navigate('/admin'); } else { navigate('/dashboard'); } From fa9a1d1d1dbcf3e9d4970538725e5b90bc4fef3a Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:51:39 +0700 Subject: [PATCH 5/9] Add 404 page and invitation success screen - Created NotFound component with proper error messaging and navigation - Added catch-all route (*) in App.js for undefined routes - Added success state in AcceptInvitation with user info display - Auto-redirect after 3 seconds with manual continue button option - Improved UX with animated success indicator --- src/App.js | 4 ++ src/pages/AcceptInvitation.js | 99 ++++++++++++++++++++++++++++++++--- src/pages/NotFound.js | 81 ++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 src/pages/NotFound.js diff --git a/src/App.js b/src/App.js index e348d88..9aa68b8 100644 --- a/src/App.js +++ b/src/App.js @@ -51,6 +51,7 @@ import ContactUs from './pages/ContactUs'; import TermsOfService from './pages/TermsOfService'; import PrivacyPolicy from './pages/PrivacyPolicy'; import AcceptInvitation from './pages/AcceptInvitation'; +import NotFound from './pages/NotFound'; const PrivateRoute = ({ children, adminOnly = false }) => { const { user, loading } = useAuth(); @@ -280,6 +281,9 @@ function App() { } /> + + {/* 404 - Catch all undefined routes */} + } /> diff --git a/src/pages/AcceptInvitation.js b/src/pages/AcceptInvitation.js index 1825b32..5410c0f 100644 --- a/src/pages/AcceptInvitation.js +++ b/src/pages/AcceptInvitation.js @@ -19,6 +19,8 @@ const AcceptInvitation = () => { const [invitation, setInvitation] = useState(null); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); + const [success, setSuccess] = useState(false); + const [successUser, setSuccessUser] = useState(null); const [error, setError] = useState(null); const [formData, setFormData] = useState({ password: '', @@ -134,19 +136,23 @@ const AcceptInvitation = () => { const { access_token, user } = response.data; localStorage.setItem('token', access_token); - toast.success('Welcome to LOAF! Your account has been created successfully.'); - // Call login to update auth context if (login) { await login(invitation.email, formData.password); } - // Redirect based on role - if (user.role === 'admin' || user.role === 'superadmin') { - navigate('/admin'); - } else { - navigate('/dashboard'); - } + // Show success state + setSuccessUser(user); + setSuccess(true); + + // Auto-redirect after 3 seconds + setTimeout(() => { + if (user.role === 'admin' || user.role === 'superadmin') { + navigate('/admin'); + } else { + navigate('/dashboard'); + } + }, 3000); } catch (error) { const errorMessage = error.response?.data?.detail || 'Failed to accept invitation'; toast.error(errorMessage); @@ -206,6 +212,83 @@ const AcceptInvitation = () => { ); } + if (success) { + const redirectPath = successUser?.role === 'admin' || successUser?.role === 'superadmin' ? '/admin' : '/dashboard'; + + return ( +
+ + {/* Success Animation */} +
+
+ +
+
+ + {/* Success Message */} +

+ Welcome to LOAF! 🎉 +

+

+ Your account has been created successfully. +

+ + {/* User Info Card */} +
+
+
+

+ Name +

+

+ {successUser?.first_name} {successUser?.last_name} +

+
+
+

+ Email +

+

+ {successUser?.email} +

+
+
+

+ Role +

+
{getRoleBadge(successUser?.role)}
+
+
+

+ Status +

+ + {successUser?.status} + +
+
+
+ + {/* Redirect Info */} +
+

+ + Redirecting you to your dashboard in 3 seconds... +

+
+ + {/* Manual Continue Button */} + +
+
+ ); + } + return (
diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js new file mode 100644 index 0000000..21d68b0 --- /dev/null +++ b/src/pages/NotFound.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '../components/ui/button'; +import { Card } from '../components/ui/card'; +import { Home, ArrowLeft, Search } from 'lucide-react'; + +const NotFound = () => { + const navigate = useNavigate(); + + return ( +
+ + {/* 404 Illustration */} +
+
+

+ 404 +

+
+ +
+
+
+ + {/* Message */} +

+ Page Not Found +

+

+ Oops! The page you're looking for doesn't exist. It might have been moved or deleted. +

+ + {/* Action Buttons */} +
+ + +
+ + {/* Help Text */} +
+

+ Need help? Contact us at{' '} + + support@loaftx.org + +

+
+
+
+ ); +}; + +export default NotFound; From 1acb13ba7934ec9eb275918666a4e6d86a03e0ee Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:00:41 +0700 Subject: [PATCH 6/9] Add comprehensive login diagnostics and retry logic - Add detailed console logging throughout login flow - Add 30s timeout to prevent hanging requests - Defer permission fetching with setTimeout to avoid race conditions - Add automatic retry for 5xx errors and network failures - Enhanced error logging with full context for debugging This addresses intermittent login failures reported by users --- src/context/AuthContext.js | 80 +++++++++++++++++++++++++++++++------- src/utils/api.js | 58 ++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 19 deletions(-) diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js index ca70795..b029b7a 100644 --- a/src/context/AuthContext.js +++ b/src/context/AuthContext.js @@ -54,21 +54,75 @@ export const AuthProvider = ({ children }) => { }; const login = async (email, password) => { - const response = await axios.post(`${API_URL}/api/auth/login`, { email, password }); - const { access_token, user: userData } = response.data; - localStorage.setItem('token', access_token); - setToken(access_token); - setUser(userData); - - // 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 - } + console.log('[AuthContext] Starting login request...'); - return userData; + const response = await axios.post( + `${API_URL}/api/auth/login`, + { email, password }, + { + timeout: 30000, // 30 second timeout + headers: { + 'Content-Type': 'application/json' + } + } + ); + + console.log('[AuthContext] Login response received:', { + status: response.status, + hasToken: !!response.data?.access_token, + hasUser: !!response.data?.user + }); + + const { access_token, user: userData } = response.data; + + // Store token first + localStorage.setItem('token', access_token); + console.log('[AuthContext] Token stored in localStorage'); + + // Update state + setToken(access_token); + setUser(userData); + console.log('[AuthContext] User state updated:', { + email: userData.email, + role: userData.role + }); + + // Fetch user permissions (don't let this fail the login) + // Use setTimeout to defer permission fetching slightly + setTimeout(async () => { + try { + console.log('[AuthContext] Fetching permissions...'); + await fetchPermissions(access_token); + console.log('[AuthContext] Permissions fetched successfully'); + } catch (error) { + console.error('[AuthContext] Failed to fetch permissions (non-critical):', { + message: error.message, + response: error.response?.data, + status: error.response?.status + }); + // Don't throw - permissions can be fetched later if needed + } + }, 100); // Small delay to ensure state is settled + + return userData; + } catch (error) { + // Enhanced error logging + console.error('[AuthContext] Login failed:', { + message: error.message, + response: error.response?.data, + status: error.response?.status, + code: error.code, + config: { + url: error.config?.url, + method: error.config?.method, + timeout: error.config?.timeout + } + }); + + // Re-throw to let Login component handle the error + throw error; + } }; const logout = () => { diff --git a/src/utils/api.js b/src/utils/api.js index 7a1a797..b79e35b 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -4,14 +4,60 @@ const API_URL = process.env.REACT_APP_BACKEND_URL; export const api = axios.create({ baseURL: `${API_URL}/api`, + timeout: 30000, // 30 second timeout for all requests }); -api.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; +// Request interceptor - add auth token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + console.error('[API] Request error:', error); + return Promise.reject(error); } - return config; -}); +); + +// Response interceptor - handle errors and retries +api.interceptors.response.use( + (response) => { + return response; + }, + async (error) => { + const config = error.config; + + // Don't retry if we've already retried or if it's a client error (4xx) + if (!config || config.__isRetry || (error.response && error.response.status < 500)) { + console.error('[API] Request failed:', { + url: config?.url, + method: config?.method, + status: error.response?.status, + message: error.message, + data: error.response?.data + }); + return Promise.reject(error); + } + + // Mark as retry to prevent infinite loops + config.__isRetry = true; + + // Retry after 1 second for server errors or network issues + console.warn('[API] Retrying request after 1s:', { + url: config.url, + method: config.method, + error: error.message + }); + + return new Promise((resolve) => { + setTimeout(() => { + resolve(api.request(config)); + }, 1000); + }); + } +); export default api; From 03b76a8e582c92ded7ddd75e4318ce6ea32d2f6c Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:42:11 +0700 Subject: [PATCH 7/9] Add defensive backend URL validation and auto-cleanup - Add getApiUrl() function to validate and clean backend URL - Automatically strip /membership or /api suffix if present - Log all environment variables on module load for debugging - Add detailed URL logging in login function - Provide fallback if REACT_APP_BACKEND_URL is undefined This fixes the intermittent CORS error caused by incorrect backend URL --- src/context/AuthContext.js | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js index b029b7a..b2aa1b7 100644 --- a/src/context/AuthContext.js +++ b/src/context/AuthContext.js @@ -3,7 +3,34 @@ import axios from 'axios'; const AuthContext = createContext(); -const API_URL = process.env.REACT_APP_BACKEND_URL; +// Ensure API_URL is correctly set, with fallback and validation +const getApiUrl = () => { + const url = process.env.REACT_APP_BACKEND_URL; + + // Log the environment variable for debugging + console.log('[AuthContext] Environment check:', { + REACT_APP_BACKEND_URL: url, + REACT_APP_BASENAME: process.env.REACT_APP_BASENAME, + PUBLIC_URL: process.env.PUBLIC_URL + }); + + if (!url) { + console.error('[AuthContext] REACT_APP_BACKEND_URL is not defined!'); + // Fallback to current origin API (same domain) + return window.location.origin.replace('/membership', ''); + } + + // Remove any trailing /membership or /api from the backend URL + const cleanUrl = url.replace(/\/(membership|api)\/?$/, ''); + + if (cleanUrl !== url) { + console.warn('[AuthContext] Cleaned backend URL:', { original: url, cleaned: cleanUrl }); + } + + return cleanUrl; +}; + +const API_URL = getApiUrl(); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); @@ -55,7 +82,11 @@ export const AuthProvider = ({ children }) => { const login = async (email, password) => { try { - console.log('[AuthContext] Starting login request...'); + console.log('[AuthContext] Starting login request...', { + API_URL: API_URL, + envBackendUrl: process.env.REACT_APP_BACKEND_URL, + fullUrl: `${API_URL}/api/auth/login` + }); const response = await axios.post( `${API_URL}/api/auth/login`, From 56711e9136b6ea1f2a189aa9a3c1cbbf90d14ea6 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:47:30 +0700 Subject: [PATCH 8/9] Revert URL cleanup - backend path is correct The /membership path in backend URL is correct for development. Issue is CORS configuration on backend, not URL format. --- src/context/AuthContext.js | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js index b2aa1b7..24df9bf 100644 --- a/src/context/AuthContext.js +++ b/src/context/AuthContext.js @@ -3,34 +3,14 @@ import axios from 'axios'; const AuthContext = createContext(); -// Ensure API_URL is correctly set, with fallback and validation -const getApiUrl = () => { - const url = process.env.REACT_APP_BACKEND_URL; +const API_URL = process.env.REACT_APP_BACKEND_URL || window.location.origin; - // Log the environment variable for debugging - console.log('[AuthContext] Environment check:', { - REACT_APP_BACKEND_URL: url, - REACT_APP_BASENAME: process.env.REACT_APP_BASENAME, - PUBLIC_URL: process.env.PUBLIC_URL - }); - - if (!url) { - console.error('[AuthContext] REACT_APP_BACKEND_URL is not defined!'); - // Fallback to current origin API (same domain) - return window.location.origin.replace('/membership', ''); - } - - // Remove any trailing /membership or /api from the backend URL - const cleanUrl = url.replace(/\/(membership|api)\/?$/, ''); - - if (cleanUrl !== url) { - console.warn('[AuthContext] Cleaned backend URL:', { original: url, cleaned: cleanUrl }); - } - - return cleanUrl; -}; - -const API_URL = getApiUrl(); +// Log environment on module load for debugging +console.log('[AuthContext] Module initialized with:', { + REACT_APP_BACKEND_URL: process.env.REACT_APP_BACKEND_URL, + REACT_APP_BASENAME: process.env.REACT_APP_BASENAME, + API_URL: API_URL +}); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); From 0249cad261530214545b1ecaaa173f9f544a63a7 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Tue, 6 Jan 2026 01:02:16 +0700 Subject: [PATCH 9/9] Improve UX with navigation, attendance management, and calendar fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Quick Wins - **AdminSidebar**: Move "View Public Site" to clickable logo area - **Plans**: Fix layout to center single plan, dynamic grid for multiple - **AdminGallery**: Add empty state message with "Create Event" button ## Event Attendance Enhancement - **NEW: AdminEventAttendance page** with full-featured table view: - Tab filters (All/Yes/No/Maybe RSVPs) - Search by name/email - Bulk selection with Select All - Individual attendance toggle buttons (merged column) - CSV export functionality (client requirement) - Summary statistics cards - **AdminEvents**: Navigate to new attendance page instead of dialog - **App.js**: Add /admin/events/:eventId/attendance route ## Calendar Fixes - **MemberCalendar**: Add state management for navigation (date/view) - Fix non-functional buttons (Today/Back/Next/Month/Week/Day/Agenda) - Add onNavigate and onView handlers - **NEW: MemberCalendar.css**: Extract styles from broken jsx syntax - Fix toolbar button styling and interactivity 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/App.js | 8 + src/components/AdminSidebar.js | 15 +- src/pages/Plans.js | 8 +- src/pages/admin/AdminEventAttendance.js | 548 ++++++++++++++++++++++++ src/pages/admin/AdminEvents.js | 22 +- src/pages/admin/AdminGallery.js | 29 +- src/pages/members/MemberCalendar.css | 93 ++++ src/pages/members/MemberCalendar.js | 74 +--- 8 files changed, 714 insertions(+), 83 deletions(-) create mode 100644 src/pages/admin/AdminEventAttendance.js create mode 100644 src/pages/members/MemberCalendar.css diff --git a/src/App.js b/src/App.js index 9aa68b8..d706f13 100644 --- a/src/App.js +++ b/src/App.js @@ -23,6 +23,7 @@ import AdminMembers from './pages/admin/AdminMembers'; import AdminPermissions from './pages/admin/AdminPermissions'; import AdminRoles from './pages/admin/AdminRoles'; import AdminEvents from './pages/admin/AdminEvents'; +import AdminEventAttendance from './pages/admin/AdminEventAttendance'; import AdminValidations from './pages/admin/AdminValidations'; import AdminPlans from './pages/admin/AdminPlans'; import AdminSubscriptions from './pages/admin/AdminSubscriptions'; @@ -218,6 +219,13 @@ function App() { } /> + + + + + + } /> diff --git a/src/components/AdminSidebar.js b/src/components/AdminSidebar.js index f22f44d..07757aa 100644 --- a/src/components/AdminSidebar.js +++ b/src/components/AdminSidebar.js @@ -265,7 +265,7 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => { > {/* Header */}
-
+ LOAF Logo { }`} /> {isOpen && ( -

- Admin -

+
+

+ Admin +

+

+ View Public Site +

+
)} -
+
) : plans.length > 0 ? ( -
+
{plans.map((plan) => { const minimumPrice = plan.minimum_price_cents || plan.price_cents || 3000; const suggestedPrice = plan.suggested_price_cents || minimumPrice; diff --git a/src/pages/admin/AdminEventAttendance.js b/src/pages/admin/AdminEventAttendance.js new file mode 100644 index 0000000..5fa5c09 --- /dev/null +++ b/src/pages/admin/AdminEventAttendance.js @@ -0,0 +1,548 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import api from '../../utils/api'; +import { Card } from '../../components/ui/card'; +import { Button } from '../../components/ui/button'; +import { Input } from '../../components/ui/input'; +import { Badge } from '../../components/ui/badge'; +import { Checkbox } from '../../components/ui/checkbox'; +import { + ArrowLeft, + Calendar, + MapPin, + Download, + Check, + X, + Search, + Users, + UserCheck, + UserX, + HelpCircle +} from 'lucide-react'; +import { toast } from 'sonner'; +import moment from 'moment'; + +const AdminEventAttendance = () => { + const { eventId } = useParams(); + const navigate = useNavigate(); + + const [event, setEvent] = useState(null); + const [rsvps, setRsvps] = useState([]); + const [filteredRsvps, setFilteredRsvps] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // Filters and search + const [activeTab, setActiveTab] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + // Bulk selection + const [selectedRsvps, setSelectedRsvps] = useState(new Set()); + const [selectAll, setSelectAll] = useState(false); + + useEffect(() => { + fetchEventAndRsvps(); + }, [eventId]); + + useEffect(() => { + filterRsvps(); + }, [rsvps, activeTab, searchQuery]); + + const fetchEventAndRsvps = async () => { + try { + setLoading(true); + const [eventRes, rsvpsRes] = await Promise.all([ + api.get(`/admin/events/${eventId}`), + api.get(`/admin/events/${eventId}/rsvps`) + ]); + setEvent(eventRes.data); + setRsvps(rsvpsRes.data); + } catch (error) { + console.error('Failed to fetch event data:', error); + toast.error('Failed to load event data'); + } finally { + setLoading(false); + } + }; + + const filterRsvps = () => { + let filtered = [...rsvps]; + + // Filter by RSVP status tab + if (activeTab !== 'all') { + filtered = filtered.filter(rsvp => rsvp.rsvp_status === activeTab); + } + + // Filter by search query + if (searchQuery) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter(rsvp => + rsvp.user_name?.toLowerCase().includes(query) || + rsvp.user_email?.toLowerCase().includes(query) + ); + } + + setFilteredRsvps(filtered); + }; + + const handleSelectAll = () => { + if (selectAll) { + setSelectedRsvps(new Set()); + } else { + setSelectedRsvps(new Set(filteredRsvps.map(rsvp => rsvp.user_id))); + } + setSelectAll(!selectAll); + }; + + const handleSelectRsvp = (userId) => { + const newSelected = new Set(selectedRsvps); + if (newSelected.has(userId)) { + newSelected.delete(userId); + } else { + newSelected.add(userId); + } + setSelectedRsvps(newSelected); + setSelectAll(newSelected.size === filteredRsvps.length); + }; + + const handleBulkAttendance = async (attended) => { + if (selectedRsvps.size === 0) { + toast.error('Please select at least one RSVP'); + return; + } + + try { + setSaving(true); + const updates = Array.from(selectedRsvps).map(userId => ({ + user_id: userId, + attended + })); + + await api.put(`/admin/events/${eventId}/attendance`, { updates }); + + toast.success(`Marked ${selectedRsvps.size} ${selectedRsvps.size === 1 ? 'person' : 'people'} as ${attended ? 'attended' : 'not attended'}`); + + // Refresh data + await fetchEventAndRsvps(); + + // Clear selection + setSelectedRsvps(new Set()); + setSelectAll(false); + } catch (error) { + console.error('Failed to update attendance:', error); + toast.error('Failed to update attendance'); + } finally { + setSaving(false); + } + }; + + const handleIndividualAttendance = async (userId, attended) => { + try { + setSaving(true); + const updates = [{ + user_id: userId, + attended + }]; + + await api.put(`/admin/events/${eventId}/attendance`, { updates }); + + toast.success(`Attendance ${attended ? 'confirmed' : 'removed'}`); + + // Refresh data + await fetchEventAndRsvps(); + } catch (error) { + console.error('Failed to update attendance:', error); + toast.error('Failed to update attendance'); + } finally { + setSaving(false); + } + }; + + const exportToCSV = () => { + if (filteredRsvps.length === 0) { + toast.error('No RSVPs to export'); + return; + } + + // CSV header + const headers = ['Name', 'Email', 'RSVP Status', 'Attended', 'Attended At']; + + // CSV rows + const rows = filteredRsvps.map(rsvp => [ + `"${rsvp.user_name}"`, + `"${rsvp.user_email}"`, + `"${rsvp.rsvp_status.toUpperCase()}"`, + rsvp.attended ? 'Yes' : 'No', + rsvp.attended_at ? `"${moment(rsvp.attended_at).format('YYYY-MM-DD HH:mm A')}"` : '' + ]); + + // Combine headers and rows + const csvContent = [ + headers.join(','), + ...rows.map(row => row.join(',')) + ].join('\n'); + + // Create blob and download + 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', `${event?.title.replace(/\s+/g, '_')}_RSVPs_${moment().format('YYYY-MM-DD')}.csv`); + link.style.visibility = 'hidden'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + toast.success('CSV exported successfully'); + }; + + const getStats = () => { + const total = rsvps.length; + const yesCount = rsvps.filter(r => r.rsvp_status === 'yes').length; + const noCount = rsvps.filter(r => r.rsvp_status === 'no').length; + const maybeCount = rsvps.filter(r => r.rsvp_status === 'maybe').length; + const attendedCount = rsvps.filter(r => r.attended).length; + + return { total, yesCount, noCount, maybeCount, attendedCount }; + }; + + const stats = getStats(); + + if (loading) { + return ( +
+
Loading event data...
+
+ ); + } + + if (!event) { + return ( +
+

Event not found

+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

+ Event Attendance +

+

+ Manage RSVPs and track attendance for this event +

+
+
+ +
+ + {/* Event Details Card */} + +
+
+

+ {event.title} +

+
+
+ + {moment(event.start_at).format('MMMM D, YYYY [at] h:mm A')} +
+ {event.location && ( +
+ + {event.location} +
+ )} +
+
+ + {event.published ? 'Published' : 'Draft'} + +
+
+ + {/* Statistics Cards */} +
+ +
+ +
+

Total RSVPs

+

{stats.total}

+
+
+
+ + +
+ +
+

Yes

+

{stats.yesCount}

+
+
+
+ + +
+ +
+

No

+

{stats.noCount}

+
+
+
+ + +
+ +
+

Maybe

+

{stats.maybeCount}

+
+
+
+ + +
+ +
+

Attended

+

{stats.attendedCount}

+
+
+
+
+ + {/* Filters and Actions */} + +
+ {/* Tab Filters */} +
+ + + + +
+ + {/* Search and Bulk Actions */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 border-[#ddd8eb] rounded-xl" + style={{ fontFamily: "'Nunito Sans', sans-serif" }} + /> +
+ + {selectedRsvps.size > 0 && ( +
+ + {selectedRsvps.size} selected + + + +
+ )} +
+
+
+ + {/* RSVP Table */} + +
+ + + + + + + + + + + + + {filteredRsvps.length > 0 ? ( + filteredRsvps.map((rsvp) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+ + + Name + + Email + + RSVP Status + + Attendance + + Attended At +
+ handleSelectRsvp(rsvp.user_id)} + /> + + {rsvp.user_name} + + {rsvp.user_email} + + + {rsvp.rsvp_status.toUpperCase()} + + + {rsvp.attended ? ( + + ) : ( + + )} + + {rsvp.attended_at ? moment(rsvp.attended_at).format('MMM D, YYYY h:mm A') : '-'} +
+

+ {searchQuery ? 'No RSVPs match your search' : 'No RSVPs for this filter'} +

+
+
+
+
+ ); +}; + +export default AdminEventAttendance; diff --git a/src/pages/admin/AdminEvents.js b/src/pages/admin/AdminEvents.js index 9c8b89c..8d25af7 100644 --- a/src/pages/admin/AdminEvents.js +++ b/src/pages/admin/AdminEvents.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import api from '../../utils/api'; import { Card } from '../../components/ui/card'; @@ -8,16 +9,14 @@ import { Input } from '../../components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../../components/ui/dialog'; import { toast } from 'sonner'; import { Calendar, MapPin, Users, Plus, Edit, Trash2, Eye, EyeOff } from 'lucide-react'; -import { AttendanceDialog } from '../../components/AttendanceDialog'; const AdminEvents = () => { + const navigate = useNavigate(); const { hasPermission } = useAuth(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [editingEvent, setEditingEvent] = useState(null); - const [attendanceDialogOpen, setAttendanceDialogOpen] = useState(false); - const [selectedEvent, setSelectedEvent] = useState(null); const [formData, setFormData] = useState({ title: '', @@ -342,19 +341,16 @@ const AdminEvents = () => { {/* Actions */}
- {/* Mark Attendance Button */} + {/* Manage Attendance Button */} {/* Other Actions */} @@ -419,14 +415,6 @@ const AdminEvents = () => {
)} - - {/* Attendance Dialog */} - ); }; diff --git a/src/pages/admin/AdminGallery.js b/src/pages/admin/AdminGallery.js index 723518b..e7aac7d 100644 --- a/src/pages/admin/AdminGallery.js +++ b/src/pages/admin/AdminGallery.js @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; +import { Link } from 'react-router-dom'; import api from '../../utils/api'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; @@ -14,7 +15,7 @@ import { SelectValue, } from '../../components/ui/select'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../components/ui/dialog'; -import { Upload, Trash2, Edit, X, ImageIcon, Calendar, MapPin } from 'lucide-react'; +import { Upload, Trash2, Edit, X, ImageIcon, Calendar, MapPin, AlertCircle } from 'lucide-react'; import { toast } from 'sonner'; import moment from 'moment'; @@ -179,6 +180,32 @@ const AdminGallery = () => {
+ {/* Empty State Message */} + {events.length === 0 && ( +
+
+ +
+

+ No Events Available +

+

+ You need to create an event before uploading gallery images. Events help organize photos by occasion. +

+ + + +
+
+
+ )} + {selectedEvent && (
{ fetchEvents(); @@ -58,6 +61,14 @@ export default function MemberCalendar() { setIsDialogOpen(true); }; + const handleNavigate = (newDate) => { + setCurrentDate(newDate); + }; + + const handleViewChange = (newView) => { + setCurrentView(newView); + }; + const handleRSVP = async (status) => { if (!selectedEvent) return; @@ -171,10 +182,13 @@ export default function MemberCalendar() { startAccessor="start" endAccessor="end" style={{ height: 700 }} + date={currentDate} + view={currentView} + onNavigate={handleNavigate} + onView={handleViewChange} onSelectEvent={handleSelectEvent} eventPropGetter={eventStyleGetter} views={['month', 'week', 'day', 'agenda']} - defaultView="month" popup className="member-calendar" /> @@ -320,64 +334,6 @@ export default function MemberCalendar() {
-
);