+
{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/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 = () => {
Document Type *
setFormData({ ...formData, document_type: value })}
+ onValueChange={(value) => {
+ setFormData({ ...formData, document_type: value, document_url: '' });
+ setUploadedFile(null);
+ }}
>
- Google Drive
- PDF
+ Link
Upload
@@ -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/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
+
navigate('/admin/events')} className="bg-[#664fa3] hover:bg-[#422268] text-white rounded-xl">
+ Back to Events
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
navigate('/admin/events')}
+ variant="outline"
+ className="border-[#ddd8eb] text-[#664fa3] rounded-xl"
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+
+ Back to Events
+
+
+
+ Event Attendance
+
+
+ Manage RSVPs and track attendance for this event
+
+
+
+
+
+ Export to CSV
+
+
+
+ {/* 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}
+
+
+
+
+
+
+
+
+
+
+
+
+
Maybe
+
{stats.maybeCount}
+
+
+
+
+
+
+
+
+
Attended
+
{stats.attendedCount}
+
+
+
+
+
+ {/* Filters and Actions */}
+
+
+ {/* Tab Filters */}
+
+ setActiveTab('all')}
+ variant={activeTab === 'all' ? 'default' : 'outline'}
+ className={`rounded-xl ${
+ activeTab === 'all'
+ ? 'bg-[#664fa3] hover:bg-[#422268] text-white'
+ : 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
+ }`}
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+ All ({stats.total})
+
+ setActiveTab('yes')}
+ variant={activeTab === 'yes' ? 'default' : 'outline'}
+ className={`rounded-xl ${
+ activeTab === 'yes'
+ ? 'bg-[#81B29A] hover:bg-[#6a9a83] text-white'
+ : 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
+ }`}
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+ Yes ({stats.yesCount})
+
+ setActiveTab('no')}
+ variant={activeTab === 'no' ? 'default' : 'outline'}
+ className={`rounded-xl ${
+ activeTab === 'no'
+ ? 'bg-[#E07A5F] hover:bg-[#d16b54] text-white'
+ : 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
+ }`}
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+ No ({stats.noCount})
+
+ setActiveTab('maybe')}
+ variant={activeTab === 'maybe' ? 'default' : 'outline'}
+ className={`rounded-xl ${
+ activeTab === 'maybe'
+ ? 'bg-[#F2CC8F] hover:bg-[#e8bf7a] text-[#422268]'
+ : 'border-[#ddd8eb] text-[#664fa3] hover:bg-[#F8F7FB]'
+ }`}
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+ Maybe ({stats.maybeCount})
+
+
+
+ {/* 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
+
+ handleBulkAttendance(true)}
+ disabled={saving}
+ className="bg-[#81B29A] hover:bg-[#6a9a83] text-white rounded-xl"
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+
+ Mark Attended
+
+ handleBulkAttendance(false)}
+ disabled={saving}
+ className="bg-[#E07A5F] hover:bg-[#d16b54] text-white rounded-xl"
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+
+ Mark Not Attended
+
+
+ )}
+
+
+
+
+ {/* RSVP Table */}
+
+
+
+
+
+
+
+
+
+ Name
+
+
+ Email
+
+
+ RSVP Status
+
+
+ Attendance
+
+
+ Attended At
+
+
+
+
+ {filteredRsvps.length > 0 ? (
+ filteredRsvps.map((rsvp) => (
+
+
+ handleSelectRsvp(rsvp.user_id)}
+ />
+
+
+ {rsvp.user_name}
+
+
+ {rsvp.user_email}
+
+
+
+ {rsvp.rsvp_status.toUpperCase()}
+
+
+
+ {rsvp.attended ? (
+ handleIndividualAttendance(rsvp.user_id, false)}
+ disabled={saving}
+ size="sm"
+ className="bg-[#81B29A] hover:bg-[#6a9a83] text-white rounded-lg min-w-[120px]"
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+
+ Attended
+
+ ) : (
+ handleIndividualAttendance(rsvp.user_id, true)}
+ disabled={saving}
+ size="sm"
+ variant="outline"
+ className="border-[#ddd8eb] text-[#664fa3] hover:bg-[#81B29A] hover:text-white hover:border-[#81B29A] rounded-lg min-w-[120px]"
+ style={{ fontFamily: "'Inter', sans-serif" }}
+ >
+
+ Not 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 */}
{
- setSelectedEvent(event);
- setAttendanceDialogOpen(true);
- }}
+ onClick={() => navigate(`/admin/events/${event.id}/attendance`)}
variant="outline"
size="sm"
className="w-full border-[#81B29A] text-[#81B29A] hover:bg-[#81B29A] hover:text-white"
data-testid={`mark-attendance-${event.id}`}
>
- Mark Attendance ({event.rsvp_count || 0} RSVPs)
+ Manage Attendance ({event.rsvp_count || 0} RSVPs)
{/* Other Actions */}
@@ -419,14 +415,6 @@ const AdminEvents = () => {
)}
-
- {/* Attendance Dialog */}
-
>
);
};
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 = () => {
Document Type *
setFormData({ ...formData, document_type: value })}
+ onValueChange={(value) => {
+ setFormData({ ...formData, document_type: value, document_url: '' });
+ setUploadedFile(null);
+ }}
>
- Google Drive
- PDF
+ Link
Upload
@@ -302,6 +305,11 @@ const AdminFinancials = () => {
Selected: {uploadedFile.name}
)}
+ {selectedReport && !uploadedFile && (
+
+ Current file will be kept if no new file is selected
+
+ )}
) : (