1241 lines
50 KiB
TypeScript
1241 lines
50 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { authService, userService, membershipService, paymentService, eventService, User, Membership, Payment, Event, EventRSVP } from '../services/membershipService';
|
||
import MembershipSetup from '../components/MembershipSetup';
|
||
import ProfileMenu from '../components/ProfileMenu';
|
||
import ProfileEdit from '../components/ProfileEdit';
|
||
|
||
const Dashboard: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const [user, setUser] = useState<User | null>(null);
|
||
const [memberships, setMemberships] = useState<Membership[]>([]);
|
||
const [payments, setPayments] = useState<Payment[]>([]);
|
||
const [allPayments, setAllPayments] = useState<Payment[]>([]);
|
||
const [allMemberships, setAllMemberships] = useState<Membership[]>([]);
|
||
const [allUsers, setAllUsers] = useState<User[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [showMembershipSetup, setShowMembershipSetup] = useState(false);
|
||
const [showProfileEdit, setShowProfileEdit] = useState(false);
|
||
const [searchTerm, setSearchTerm] = useState('');
|
||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||
const [showUserDetails, setShowUserDetails] = useState(false);
|
||
const [isEditingUser, setIsEditingUser] = useState(false);
|
||
const [editFormData, setEditFormData] = useState<Partial<User>>({});
|
||
const [upcomingEvents, setUpcomingEvents] = useState<Event[]>([]);
|
||
const [allEvents, setAllEvents] = useState<Event[]>([]);
|
||
const [eventRSVPs, setEventRSVPs] = useState<EventRSVP[]>([]);
|
||
const [eventRSVPCounts, setEventRSVPCounts] = useState<{[eventId: number]: {attending: number, maybe: number, not_attending: number}}>({});
|
||
const [rsvpLoading, setRsvpLoading] = useState<{[eventId: number]: boolean}>({});
|
||
const [showEventModal, setShowEventModal] = useState(false);
|
||
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
||
const [eventFormData, setEventFormData] = useState({
|
||
title: '',
|
||
description: '',
|
||
event_date: '',
|
||
event_time: '',
|
||
location: '',
|
||
max_attendees: ''
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (!authService.isAuthenticated()) {
|
||
navigate('/login');
|
||
return;
|
||
}
|
||
|
||
loadData();
|
||
}, []);
|
||
|
||
const mergeRSVPStatus = (events: Event[], rsvps: EventRSVP[]): Event[] => {
|
||
return events.map(event => {
|
||
const rsvp = rsvps.find(r => r.event_id === event.id);
|
||
return {
|
||
...event,
|
||
rsvp_status: rsvp ? rsvp.status : undefined
|
||
};
|
||
});
|
||
};
|
||
|
||
const loadData = async () => {
|
||
try {
|
||
const [userData, membershipData, paymentData] = await Promise.all([
|
||
userService.getCurrentUser(),
|
||
membershipService.getMyMemberships(),
|
||
paymentService.getMyPayments()
|
||
]);
|
||
|
||
setUser(userData);
|
||
setMemberships(membershipData);
|
||
setPayments(paymentData);
|
||
|
||
// Load upcoming events and user's RSVPs
|
||
const [eventsData, rsvpsData] = await Promise.all([
|
||
eventService.getUpcomingEvents(),
|
||
eventService.getMyRSVPs()
|
||
]);
|
||
|
||
// Merge RSVP status with events
|
||
const eventsWithRSVP = mergeRSVPStatus(eventsData, rsvpsData);
|
||
setUpcomingEvents(eventsWithRSVP);
|
||
|
||
// Load admin data if user is admin
|
||
if (userData.role === 'admin' || userData.role === 'super_admin') {
|
||
const [allPaymentsData, allMembershipsData, allUsersData, allEventsData] = await Promise.all([
|
||
paymentService.getAllPayments(),
|
||
membershipService.getAllMemberships(),
|
||
userService.getAllUsers(),
|
||
eventService.getAllEvents()
|
||
]);
|
||
setAllPayments(allPaymentsData);
|
||
setAllMemberships(allMembershipsData);
|
||
setAllUsers(allUsersData);
|
||
setAllEvents(allEventsData);
|
||
|
||
// Load RSVP counts for all events
|
||
await loadEventRSVPCounts(allEventsData);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load data:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const loadEventRSVPCounts = async (events: Event[]) => {
|
||
const counts: {[eventId: number]: {attending: number, maybe: number, not_attending: number}} = {};
|
||
|
||
for (const event of events) {
|
||
try {
|
||
const rsvps = await eventService.getEventRSVPs(event.id);
|
||
counts[event.id] = {
|
||
attending: rsvps.filter(r => r.status === 'attending').length,
|
||
maybe: rsvps.filter(r => r.status === 'maybe').length,
|
||
not_attending: rsvps.filter(r => r.status === 'not_attending').length
|
||
};
|
||
} catch (error) {
|
||
console.error(`Failed to load RSVPs for event ${event.id}:`, error);
|
||
counts[event.id] = { attending: 0, maybe: 0, not_attending: 0 };
|
||
}
|
||
}
|
||
|
||
setEventRSVPCounts(counts);
|
||
};
|
||
|
||
const handleMembershipSetup = () => {
|
||
setShowMembershipSetup(true);
|
||
};
|
||
|
||
const handleMembershipCreated = () => {
|
||
setShowMembershipSetup(false);
|
||
loadData(); // Reload data to show the new membership
|
||
};
|
||
|
||
const handleCancelMembershipSetup = () => {
|
||
setShowMembershipSetup(false);
|
||
};
|
||
|
||
const handleProfileEdit = () => {
|
||
setShowProfileEdit(true);
|
||
};
|
||
|
||
const handleProfileSave = (updatedUser: User) => {
|
||
setUser(updatedUser);
|
||
setShowProfileEdit(false);
|
||
};
|
||
|
||
const handleProfileCancel = () => {
|
||
setShowProfileEdit(false);
|
||
};
|
||
|
||
const getUserName = (userId: number): string => {
|
||
const user = allUsers.find(u => u.id === userId);
|
||
return user ? `${user.first_name} ${user.last_name}` : `User #${userId}`;
|
||
};
|
||
|
||
const handleApprovePayment = async (paymentId: number, membershipId?: number) => {
|
||
try {
|
||
// Approve the payment
|
||
await paymentService.updatePayment(paymentId, { status: 'completed' });
|
||
|
||
// If there's an associated membership, activate it
|
||
if (membershipId) {
|
||
await membershipService.updateMembership(membershipId, { status: 'active' });
|
||
}
|
||
|
||
// Reload data
|
||
await loadData();
|
||
} catch (error) {
|
||
console.error('Failed to approve payment:', error);
|
||
alert('Failed to approve payment. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleUpdateUserRole = async (userId: number, newRole: string) => {
|
||
try {
|
||
await userService.updateUser(userId, { role: newRole });
|
||
// Reload data to reflect changes
|
||
await loadData();
|
||
} catch (error) {
|
||
console.error('Failed to update user role:', error);
|
||
alert('Failed to update user role. Please try again.');
|
||
}
|
||
};
|
||
|
||
const getStatusClass = (status: string) => {
|
||
switch (status.toLowerCase()) {
|
||
case 'active':
|
||
return 'status-active';
|
||
case 'pending':
|
||
return 'status-pending';
|
||
case 'expired':
|
||
case 'cancelled':
|
||
return 'status-expired';
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
const filteredUsers = allUsers.filter(user => {
|
||
const fullName = `${user.first_name} ${user.last_name}`.toLowerCase();
|
||
const email = user.email.toLowerCase();
|
||
const search = searchTerm.toLowerCase();
|
||
return fullName.includes(search) || email.includes(search);
|
||
});
|
||
|
||
const handleUserClick = (user: User) => {
|
||
setSelectedUser(user);
|
||
setEditFormData({
|
||
first_name: user.first_name,
|
||
last_name: user.last_name,
|
||
email: user.email,
|
||
phone: user.phone || '',
|
||
address: user.address || ''
|
||
});
|
||
setShowUserDetails(true);
|
||
setIsEditingUser(false);
|
||
};
|
||
|
||
const handleCloseUserDetails = () => {
|
||
setSelectedUser(null);
|
||
setShowUserDetails(false);
|
||
setIsEditingUser(false);
|
||
setEditFormData({});
|
||
};
|
||
|
||
const handleEditUser = () => {
|
||
setIsEditingUser(true);
|
||
};
|
||
|
||
const handleCancelEdit = () => {
|
||
setIsEditingUser(false);
|
||
if (selectedUser) {
|
||
setEditFormData({
|
||
first_name: selectedUser.first_name,
|
||
last_name: selectedUser.last_name,
|
||
email: selectedUser.email,
|
||
phone: selectedUser.phone || '',
|
||
address: selectedUser.address || ''
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleFormChange = (field: string, value: string) => {
|
||
setEditFormData(prev => ({
|
||
...prev,
|
||
[field]: value
|
||
}));
|
||
};
|
||
|
||
const handleSaveUser = async () => {
|
||
if (!selectedUser) return;
|
||
|
||
try {
|
||
await userService.updateUser(selectedUser.id, editFormData);
|
||
// Refresh data
|
||
await loadData();
|
||
setIsEditingUser(false);
|
||
// Update selected user with new data
|
||
const updatedUser = allUsers.find(u => u.id === selectedUser.id);
|
||
if (updatedUser) {
|
||
setSelectedUser(updatedUser);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to update user:', error);
|
||
alert('Failed to update user. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleRSVP = async (eventId: number, status: 'attending' | 'not_attending' | 'maybe') => {
|
||
// Set loading state for this event
|
||
setRsvpLoading(prev => ({ ...prev, [eventId]: true }));
|
||
|
||
// Optimistically update the UI
|
||
setUpcomingEvents(prevEvents =>
|
||
prevEvents.map(event =>
|
||
event.id === eventId
|
||
? { ...event, rsvp_status: status }
|
||
: event
|
||
)
|
||
);
|
||
|
||
try {
|
||
await eventService.createOrUpdateRSVP(eventId, { status });
|
||
// Reload RSVPs and merge with events to get the latest data
|
||
const [eventsData, rsvpsData] = await Promise.all([
|
||
eventService.getUpcomingEvents(),
|
||
eventService.getMyRSVPs()
|
||
]);
|
||
const eventsWithRSVP = mergeRSVPStatus(eventsData, rsvpsData);
|
||
setUpcomingEvents(eventsWithRSVP);
|
||
} catch (error) {
|
||
console.error('Failed to update RSVP:', error);
|
||
alert('Failed to update RSVP. Please try again.');
|
||
// Revert optimistic update on error
|
||
const [eventsData, rsvpsData] = await Promise.all([
|
||
eventService.getUpcomingEvents(),
|
||
eventService.getMyRSVPs()
|
||
]);
|
||
const eventsWithRSVP = mergeRSVPStatus(eventsData, rsvpsData);
|
||
setUpcomingEvents(eventsWithRSVP);
|
||
} finally {
|
||
// Clear loading state
|
||
setRsvpLoading(prev => ({ ...prev, [eventId]: false }));
|
||
}
|
||
};
|
||
|
||
const handlePublishEvent = async (eventId: number) => {
|
||
try {
|
||
await eventService.updateEvent(eventId, { status: 'published' });
|
||
// Reload events to reflect the change
|
||
const eventsData = await eventService.getAllEvents();
|
||
setAllEvents(eventsData);
|
||
// Reload RSVP counts
|
||
await loadEventRSVPCounts(eventsData);
|
||
} catch (error) {
|
||
console.error('Failed to publish event:', error);
|
||
alert('Failed to publish event. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleCancelEvent = async (eventId: number) => {
|
||
if (!confirm('Are you sure you want to cancel this event?')) {
|
||
return;
|
||
}
|
||
try {
|
||
await eventService.updateEvent(eventId, { status: 'cancelled' });
|
||
// Reload events to reflect the change
|
||
const eventsData = await eventService.getAllEvents();
|
||
setAllEvents(eventsData);
|
||
// Reload RSVP counts
|
||
await loadEventRSVPCounts(eventsData);
|
||
} catch (error) {
|
||
console.error('Failed to cancel event:', error);
|
||
alert('Failed to cancel event. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleCreateEvent = () => {
|
||
setEditingEvent(null);
|
||
setEventFormData({
|
||
title: '',
|
||
description: '',
|
||
event_date: '',
|
||
event_time: '',
|
||
location: '',
|
||
max_attendees: ''
|
||
});
|
||
setShowEventModal(true);
|
||
};
|
||
|
||
const handleEditEvent = (event: Event) => {
|
||
setEditingEvent(event);
|
||
|
||
// Convert event_date to YYYY-MM-DD format for date input
|
||
const dateObj = new Date(event.event_date);
|
||
const formattedDate = dateObj.toISOString().split('T')[0];
|
||
|
||
setEventFormData({
|
||
title: event.title,
|
||
description: event.description || '',
|
||
event_date: formattedDate,
|
||
event_time: event.event_time || '',
|
||
location: event.location || '',
|
||
max_attendees: event.max_attendees?.toString() || ''
|
||
});
|
||
setShowEventModal(true);
|
||
};
|
||
|
||
const handleEventFormChange = (field: string, value: string) => {
|
||
setEventFormData(prev => ({
|
||
...prev,
|
||
[field]: value
|
||
}));
|
||
};
|
||
|
||
const handleSaveEvent = async () => {
|
||
try {
|
||
const eventData = {
|
||
title: eventFormData.title,
|
||
description: eventFormData.description || undefined,
|
||
event_date: eventFormData.event_date,
|
||
event_time: eventFormData.event_time || undefined,
|
||
location: eventFormData.location || undefined,
|
||
max_attendees: eventFormData.max_attendees ? parseInt(eventFormData.max_attendees) : undefined
|
||
};
|
||
|
||
if (editingEvent) {
|
||
// Update existing event
|
||
await eventService.updateEvent(editingEvent.id, eventData);
|
||
} else {
|
||
// Create new event
|
||
await eventService.createEvent(eventData);
|
||
}
|
||
|
||
// Reload events
|
||
const eventsData = await eventService.getAllEvents();
|
||
setAllEvents(eventsData);
|
||
await loadEventRSVPCounts(eventsData);
|
||
|
||
// Close modal
|
||
setShowEventModal(false);
|
||
setEditingEvent(null);
|
||
} catch (error) {
|
||
console.error('Failed to save event:', error);
|
||
alert('Failed to save event. Please try again.');
|
||
}
|
||
};
|
||
|
||
const handleCloseEventModal = () => {
|
||
setShowEventModal(false);
|
||
setEditingEvent(null);
|
||
};
|
||
|
||
const formatDate = (dateString: string) => {
|
||
return new Date(dateString).toLocaleDateString('en-GB', {
|
||
day: 'numeric',
|
||
month: 'long',
|
||
year: 'numeric'
|
||
});
|
||
};
|
||
|
||
if (loading) {
|
||
return <div className="container">Loading...</div>;
|
||
}
|
||
|
||
if (showMembershipSetup) {
|
||
return (
|
||
<>
|
||
<nav className="navbar">
|
||
<h1>SASA Membership Portal</h1>
|
||
<ProfileMenu userName={`${user?.first_name} ${user?.last_name}`} userRole={user?.role || ''} />
|
||
</nav>
|
||
<div className="container">
|
||
<MembershipSetup
|
||
onMembershipCreated={handleMembershipCreated}
|
||
onCancel={handleCancelMembershipSetup}
|
||
/>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
const activeMembership = memberships.find(m => m.status === 'active') || memberships[0];
|
||
|
||
return (
|
||
<>
|
||
<nav className="navbar">
|
||
<h1>SASA Membership Portal</h1>
|
||
<ProfileMenu
|
||
userName={`${user?.first_name} ${user?.last_name}`}
|
||
userRole={user?.role || ''}
|
||
user={user}
|
||
onEditProfile={handleProfileEdit}
|
||
/>
|
||
</nav>
|
||
|
||
<div className="container">
|
||
<h2 style={{ marginTop: '20px', marginBottom: '20px' }}>Welcome, {user?.first_name}!</h2>
|
||
|
||
<div className="dashboard-grid">
|
||
{/* Membership Card */}
|
||
{activeMembership ? (
|
||
<div className="card">
|
||
<h3 style={{ marginBottom: '16px' }}>Your Membership</h3>
|
||
<h4 style={{ color: '#0066cc', marginBottom: '8px' }}>{activeMembership.tier.name}</h4>
|
||
<p><strong>Membership Number:</strong> {activeMembership.id}</p>
|
||
<p><strong>Status:</strong> <span className={`status-badge ${getStatusClass(activeMembership.status)}`}>{activeMembership.status.toUpperCase()}</span></p>
|
||
<p><strong>Annual Fee:</strong> £{activeMembership.tier.annual_fee.toFixed(2)}</p>
|
||
<p><strong>Valid From:</strong> {formatDate(activeMembership.start_date)}</p>
|
||
<p><strong>Valid Until:</strong> {formatDate(activeMembership.end_date)}</p>
|
||
<p><strong>Auto Renew:</strong> {activeMembership.auto_renew ? 'Yes' : 'No'}</p>
|
||
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
|
||
<strong>Benefits:</strong>
|
||
<p style={{ marginTop: '4px', fontSize: '14px' }}>{activeMembership.tier.benefits}</p>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="card">
|
||
<h3 style={{ marginBottom: '16px' }}>Set Up Your Membership</h3>
|
||
<p>Choose from our membership tiers to get started with SASA benefits.</p>
|
||
<p style={{ marginTop: '12px', color: '#666' }}>Available tiers include Personal, Aircraft Owners, and Corporate memberships.</p>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={handleMembershipSetup}
|
||
style={{ marginTop: '16px' }}
|
||
>
|
||
Set Up Membership
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Upcoming Events */}
|
||
<div className="card">
|
||
<h3 style={{ marginBottom: '16px' }}>Upcoming Events</h3>
|
||
{upcomingEvents.length > 0 ? (
|
||
<div className="events-container">
|
||
{upcomingEvents.map(event => (
|
||
<div key={event.id} className="event-card">
|
||
<div className="event-header">
|
||
<div className="event-info">
|
||
<h4 className="event-title">{event.title}</h4>
|
||
<p className="event-datetime">
|
||
{formatDate(event.event_date)} at {event.event_time}
|
||
</p>
|
||
{event.location && (
|
||
<p className="event-location">
|
||
📍 {event.location}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="event-rsvp-buttons">
|
||
<button
|
||
className={`rsvp-btn rsvp-btn-attending ${event.rsvp_status === 'attending' ? 'active' : ''}`}
|
||
onClick={() => handleRSVP(event.id, 'attending')}
|
||
disabled={rsvpLoading[event.id]}
|
||
>
|
||
{rsvpLoading[event.id] ? '...' : 'Attending'}
|
||
</button>
|
||
<button
|
||
className={`rsvp-btn rsvp-btn-maybe ${event.rsvp_status === 'maybe' ? 'active' : ''}`}
|
||
onClick={() => handleRSVP(event.id, 'maybe')}
|
||
disabled={rsvpLoading[event.id]}
|
||
>
|
||
{rsvpLoading[event.id] ? '...' : 'Maybe'}
|
||
</button>
|
||
<button
|
||
className={`rsvp-btn rsvp-btn-not-attending ${event.rsvp_status === 'not_attending' ? 'active' : ''}`}
|
||
onClick={() => handleRSVP(event.id, 'not_attending')}
|
||
disabled={rsvpLoading[event.id]}
|
||
>
|
||
{rsvpLoading[event.id] ? '...' : 'Not Attending'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{event.description && (
|
||
<p className="event-description">
|
||
{event.description}
|
||
</p>
|
||
)}
|
||
{event.rsvp_status && (
|
||
<div className={`event-rsvp-status ${event.rsvp_status}`}>
|
||
<strong>Your RSVP:</strong> <span style={{ textTransform: 'capitalize' }}>{event.rsvp_status.replace('_', ' ')}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p style={{ color: '#666' }}>No upcoming events at this time.</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Payment History */}
|
||
<div className="card" style={{ marginTop: '20px' }}>
|
||
<h3 style={{ marginBottom: '16px' }}>Payment History</h3>
|
||
{payments.length > 0 ? (
|
||
<div className="table-container">
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Date</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Amount</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Method</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{payments.map(payment => (
|
||
<tr key={payment.id} style={{ borderBottom: '1px solid #eee' }}>
|
||
<td style={{ padding: '12px' }}>{payment.payment_date ? formatDate(payment.payment_date) : 'Pending'}</td>
|
||
<td style={{ padding: '12px' }}>£{payment.amount.toFixed(2)}</td>
|
||
<td style={{ padding: '12px' }}>{payment.payment_method}</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<span className={`status-badge ${getStatusClass(payment.status)}`}>
|
||
{payment.status.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<p style={{ color: '#666' }}>No payment history available.</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Admin Section */}
|
||
{(user?.role === 'admin' || user?.role === 'super_admin') && (
|
||
<div className="card" style={{ marginTop: '20px' }}>
|
||
<h3 style={{ marginBottom: '16px' }}>Admin Panel - Pending Approvals</h3>
|
||
|
||
{/* Pending Payments */}
|
||
{allPayments.filter(p => p.status === 'pending').length > 0 && (
|
||
<div style={{ marginBottom: '20px' }}>
|
||
<h4 style={{ marginBottom: '12px' }}>Pending Payments</h4>
|
||
<div className="table-container">
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>User</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Amount</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Method</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Membership</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{allPayments.filter(p => p.status === 'pending').map(payment => {
|
||
const membership = allMemberships.find(m => m.id === payment.membership_id);
|
||
return (
|
||
<tr key={payment.id} style={{ borderBottom: '1px solid #eee' }}>
|
||
<td style={{ padding: '12px' }}>{getUserName(payment.user_id)}</td>
|
||
<td style={{ padding: '12px' }}>£{payment.amount.toFixed(2)}</td>
|
||
<td style={{ padding: '12px' }}>{payment.payment_method}</td>
|
||
<td style={{ padding: '12px' }}>
|
||
{membership ? `${membership.tier.name} (${membership.status})` : 'N/A'}
|
||
</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => handleApprovePayment(payment.id, payment.membership_id || undefined)}
|
||
style={{ fontSize: '12px', padding: '6px 12px' }}
|
||
>
|
||
Approve
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Pending Memberships */}
|
||
{allMemberships.filter(m => m.status === 'pending').length > 0 && (
|
||
<div>
|
||
<h4 style={{ marginBottom: '12px' }}>Pending Memberships</h4>
|
||
<div className="table-container">
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>User</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Tier</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Start Date</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{allMemberships.filter(m => m.status === 'pending').map(membership => (
|
||
<tr key={membership.id} style={{ borderBottom: '1px solid #eee' }}>
|
||
<td style={{ padding: '12px' }}>{getUserName(membership.user_id)}</td>
|
||
<td style={{ padding: '12px' }}>{membership.tier.name}</td>
|
||
<td style={{ padding: '12px' }}>{formatDate(membership.start_date)}</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<span className={`status-badge ${getStatusClass(membership.status)}`}>
|
||
{membership.status.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{allPayments.filter(p => p.status === 'pending').length === 0 &&
|
||
allMemberships.filter(m => m.status === 'pending').length === 0 && (
|
||
<p style={{ color: '#666' }}>No pending approvals at this time.</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* User Management Section */}
|
||
{(user?.role === 'admin' || user?.role === 'super_admin') && (
|
||
<div className="card" style={{ marginTop: '20px' }}>
|
||
<h3 style={{ marginBottom: '16px' }}>User Management</h3>
|
||
|
||
{/* Search Input */}
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<input
|
||
type="text"
|
||
placeholder="Search users by name or email..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px 12px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div className="table-container">
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Name</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Email</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Membership #</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Role</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Joined</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filteredUsers.map(u => {
|
||
const userMembership = allMemberships.find(m => m.user_id === u.id && m.status === 'active');
|
||
return (
|
||
<tr
|
||
key={u.id}
|
||
style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }}
|
||
onClick={() => handleUserClick(u)}
|
||
>
|
||
<td style={{ padding: '12px' }}>{u.first_name} {u.last_name}</td>
|
||
<td style={{ padding: '12px' }}>{u.email}</td>
|
||
<td style={{ padding: '12px' }}>{userMembership ? userMembership.id : 'N/A'}</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<span style={{
|
||
backgroundColor: u.role === 'super_admin' ? '#dc3545' :
|
||
u.role === 'admin' ? '#ffc107' : '#28a745',
|
||
color: u.role === 'member' ? 'white' : 'black',
|
||
padding: '4px 8px',
|
||
borderRadius: '4px',
|
||
fontSize: '12px',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{u.role.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<span className={`status-badge ${u.is_active ? 'status-active' : 'status-expired'}`}>
|
||
{u.is_active ? 'ACTIVE' : 'INACTIVE'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '12px' }}>{formatDate(u.created_at)}</td>
|
||
<td style={{ padding: '12px' }}>
|
||
{u.role === 'member' && (
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={(e) => {
|
||
e.stopPropagation(); // Prevent row click
|
||
handleUpdateUserRole(u.id, 'admin');
|
||
}}
|
||
style={{ fontSize: '12px', padding: '4px 8px', marginRight: '4px' }}
|
||
>
|
||
Make Admin
|
||
</button>
|
||
)}
|
||
{u.role === 'admin' && u.id !== user?.id && (
|
||
<button
|
||
className="btn btn-secondary"
|
||
onClick={(e) => {
|
||
e.stopPropagation(); // Prevent row click
|
||
handleUpdateUserRole(u.id, 'member');
|
||
}}
|
||
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||
>
|
||
Remove Admin
|
||
</button>
|
||
)}
|
||
{u.role === 'super_admin' && (
|
||
<span style={{ fontSize: '12px', color: '#666' }}>Super Admin</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{showProfileEdit && user && (
|
||
<ProfileEdit
|
||
user={user}
|
||
onSave={handleProfileSave}
|
||
onCancel={handleProfileCancel}
|
||
/>
|
||
)}
|
||
|
||
{/* Event Management Section for Admins */}
|
||
{(user?.role === 'admin' || user?.role === 'super_admin') && (
|
||
<div className="card" style={{ marginTop: '20px' }}>
|
||
<h3 style={{ marginBottom: '16px' }}>Event Management</h3>
|
||
|
||
{/* Create New Event Button */}
|
||
<div style={{ marginBottom: '16px' }}>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={handleCreateEvent}
|
||
style={{ fontSize: '14px', padding: '8px 16px' }}
|
||
>
|
||
Create New Event
|
||
</button>
|
||
</div>
|
||
|
||
{/* Events List */}
|
||
<div className="table-container">
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Event</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Date & Time</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Location</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>RSVPs</th>
|
||
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{allEvents.map(event => (
|
||
<tr key={event.id} style={{ borderBottom: '1px solid #eee' }}>
|
||
<td style={{ padding: '12px' }}>
|
||
<div>
|
||
<strong>{event.title}</strong>
|
||
{event.description && (
|
||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px', maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{event.description}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<div>{formatDate(event.event_date)}</div>
|
||
<div style={{ fontSize: '12px', color: '#666' }}>{event.event_time}</div>
|
||
</td>
|
||
<td style={{ padding: '12px' }}>{event.location || 'TBD'}</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<span className={`status-badge ${event.status === 'published' ? 'status-active' : event.status === 'cancelled' ? 'status-expired' : 'status-pending'}`}>
|
||
{event.status.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '12px' }}>
|
||
{eventRSVPCounts[event.id] ? (
|
||
<div style={{ fontSize: '12px' }}>
|
||
<div>Attending: {eventRSVPCounts[event.id].attending}</div>
|
||
<div>Maybe: {eventRSVPCounts[event.id].maybe}</div>
|
||
<div>Not: {eventRSVPCounts[event.id].not_attending}</div>
|
||
</div>
|
||
) : (
|
||
<span style={{ fontSize: '12px', color: '#666' }}>Loading...</span>
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '12px' }}>
|
||
<div style={{ display: 'flex', gap: '4px' }}>
|
||
<button
|
||
className="btn btn-secondary"
|
||
onClick={() => handleEditEvent(event)}
|
||
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||
>
|
||
Edit
|
||
</button>
|
||
{event.status === 'draft' && (
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => handlePublishEvent(event.id)}
|
||
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||
>
|
||
Publish
|
||
</button>
|
||
)}
|
||
{event.status === 'published' && (
|
||
<button
|
||
className="btn btn-secondary"
|
||
onClick={() => handleCancelEvent(event.id)}
|
||
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||
>
|
||
Cancel
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{allEvents.length === 0 && (
|
||
<p style={{ color: '#666', textAlign: 'center', padding: '20px' }}>No events created yet.</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* User Details Modal */}
|
||
{showUserDetails && selectedUser && (
|
||
<div style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
zIndex: 1000
|
||
}}>
|
||
<div style={{
|
||
backgroundColor: 'white',
|
||
borderRadius: '8px',
|
||
padding: '24px',
|
||
maxWidth: '600px',
|
||
maxHeight: '80vh',
|
||
overflow: 'auto',
|
||
width: '90%'
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||
<h3 style={{ margin: 0 }}>User Details</h3>
|
||
<div>
|
||
{!isEditingUser && (user?.role === 'admin' || user?.role === 'super_admin') && (
|
||
<button
|
||
onClick={handleEditUser}
|
||
style={{
|
||
backgroundColor: '#007bff',
|
||
color: 'white',
|
||
border: 'none',
|
||
padding: '6px 12px',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
marginRight: '8px'
|
||
}}
|
||
>
|
||
Edit
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={handleCloseUserDetails}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
fontSize: '24px',
|
||
cursor: 'pointer',
|
||
color: '#666'
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* User Profile */}
|
||
<div style={{ marginBottom: '20px' }}>
|
||
<h4>Profile Information</h4>
|
||
{isEditingUser ? (
|
||
<div style={{ display: 'grid', gap: '12px' }}>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||
<label style={{ fontWeight: 'bold' }}>First Name:</label>
|
||
<input
|
||
type="text"
|
||
value={editFormData.first_name || ''}
|
||
onChange={(e) => handleFormChange('first_name', e.target.value)}
|
||
style={{
|
||
padding: '6px 8px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||
<label style={{ fontWeight: 'bold' }}>Last Name:</label>
|
||
<input
|
||
type="text"
|
||
value={editFormData.last_name || ''}
|
||
onChange={(e) => handleFormChange('last_name', e.target.value)}
|
||
style={{
|
||
padding: '6px 8px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||
<label style={{ fontWeight: 'bold' }}>Email:</label>
|
||
<input
|
||
type="email"
|
||
value={editFormData.email || ''}
|
||
onChange={(e) => handleFormChange('email', e.target.value)}
|
||
style={{
|
||
padding: '6px 8px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||
<label style={{ fontWeight: 'bold' }}>Phone:</label>
|
||
<input
|
||
type="tel"
|
||
value={editFormData.phone || ''}
|
||
onChange={(e) => handleFormChange('phone', e.target.value)}
|
||
style={{
|
||
padding: '6px 8px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||
<label style={{ fontWeight: 'bold' }}>Address:</label>
|
||
<textarea
|
||
value={editFormData.address || ''}
|
||
onChange={(e) => handleFormChange('address', e.target.value)}
|
||
rows={3}
|
||
style={{
|
||
padding: '6px 8px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '14px',
|
||
resize: 'vertical'
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
||
<button
|
||
onClick={handleSaveUser}
|
||
style={{
|
||
backgroundColor: '#28a745',
|
||
color: 'white',
|
||
border: 'none',
|
||
padding: '8px 16px',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Save
|
||
</button>
|
||
<button
|
||
onClick={handleCancelEdit}
|
||
style={{
|
||
backgroundColor: '#6c757d',
|
||
color: 'white',
|
||
border: 'none',
|
||
padding: '8px 16px',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<p><strong>Name:</strong> {selectedUser.first_name} {selectedUser.last_name}</p>
|
||
<p><strong>Email:</strong> {selectedUser.email}</p>
|
||
{selectedUser.phone && <p><strong>Phone:</strong> {selectedUser.phone}</p>}
|
||
{selectedUser.address && <p><strong>Address:</strong> {selectedUser.address}</p>}
|
||
<p><strong>Role:</strong> {selectedUser.role.toUpperCase()}</p>
|
||
<p><strong>Status:</strong> {selectedUser.is_active ? 'Active' : 'Inactive'}</p>
|
||
<p><strong>Joined:</strong> {formatDate(selectedUser.created_at)}</p>
|
||
{selectedUser.last_login && <p><strong>Last Login:</strong> {formatDate(selectedUser.last_login)}</p>}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* User Memberships */}
|
||
<div style={{ marginBottom: '20px' }}>
|
||
<h4>Memberships</h4>
|
||
{allMemberships.filter(m => m.user_id === selectedUser.id).length > 0 ? (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '1px solid #ddd' }}>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Tier</th>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Status</th>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Start Date</th>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>End Date</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{allMemberships.filter(m => m.user_id === selectedUser.id).map(membership => (
|
||
<tr key={membership.id} style={{ borderBottom: '1px solid #eee' }}>
|
||
<td style={{ padding: '8px' }}>{membership.tier.name}</td>
|
||
<td style={{ padding: '8px' }}>
|
||
<span className={`status-badge ${getStatusClass(membership.status)}`}>
|
||
{membership.status.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '8px' }}>{formatDate(membership.start_date)}</td>
|
||
<td style={{ padding: '8px' }}>{formatDate(membership.end_date)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
) : (
|
||
<p style={{ color: '#666' }}>No memberships found.</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* User Payments */}
|
||
<div>
|
||
<h4>Payment History</h4>
|
||
{allPayments.filter(p => p.user_id === selectedUser.id).length > 0 ? (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '1px solid #ddd' }}>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Date</th>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Amount</th>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Method</th>
|
||
<th style={{ padding: '8px', textAlign: 'left' }}>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{allPayments.filter(p => p.user_id === selectedUser.id).map(payment => (
|
||
<tr key={payment.id} style={{ borderBottom: '1px solid #eee' }}>
|
||
<td style={{ padding: '8px' }}>{payment.payment_date ? formatDate(payment.payment_date) : 'Pending'}</td>
|
||
<td style={{ padding: '8px' }}>£{payment.amount.toFixed(2)}</td>
|
||
<td style={{ padding: '8px' }}>{payment.payment_method}</td>
|
||
<td style={{ padding: '8px' }}>
|
||
<span className={`status-badge ${getStatusClass(payment.status)}`}>
|
||
{payment.status.toUpperCase()}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
) : (
|
||
<p style={{ color: '#666' }}>No payment history found.</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Event Create/Edit Modal */}
|
||
{showEventModal && (
|
||
<div className="modal-overlay">
|
||
<div className="modal-content" style={{ maxWidth: '600px' }}>
|
||
<h3>{editingEvent ? 'Edit Event' : 'Create New Event'}</h3>
|
||
|
||
<div className="modal-form-group">
|
||
<label>Event Title *</label>
|
||
<input
|
||
type="text"
|
||
value={eventFormData.title}
|
||
onChange={(e) => handleEventFormChange('title', e.target.value)}
|
||
required
|
||
placeholder="Annual General Meeting"
|
||
/>
|
||
</div>
|
||
|
||
<div className="modal-form-group">
|
||
<label>Description</label>
|
||
<textarea
|
||
value={eventFormData.description}
|
||
onChange={(e) => handleEventFormChange('description', e.target.value)}
|
||
rows={4}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
border: '1px solid #ddd',
|
||
borderRadius: '4px',
|
||
fontSize: '16px',
|
||
resize: 'vertical'
|
||
}}
|
||
placeholder="Event details and agenda..."
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||
<div className="modal-form-group">
|
||
<label>Event Date *</label>
|
||
<input
|
||
type="date"
|
||
value={eventFormData.event_date}
|
||
onChange={(e) => handleEventFormChange('event_date', e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="modal-form-group">
|
||
<label>Event Time</label>
|
||
<input
|
||
type="time"
|
||
value={eventFormData.event_time}
|
||
onChange={(e) => handleEventFormChange('event_time', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="modal-form-group">
|
||
<label>Location</label>
|
||
<input
|
||
type="text"
|
||
value={eventFormData.location}
|
||
onChange={(e) => handleEventFormChange('location', e.target.value)}
|
||
placeholder="Swansea Airport Conference Room"
|
||
/>
|
||
</div>
|
||
|
||
<div className="modal-form-group">
|
||
<label>Max Attendees (optional)</label>
|
||
<input
|
||
type="number"
|
||
value={eventFormData.max_attendees}
|
||
onChange={(e) => handleEventFormChange('max_attendees', e.target.value)}
|
||
min="1"
|
||
placeholder="Leave blank for unlimited"
|
||
/>
|
||
</div>
|
||
|
||
<div className="modal-buttons">
|
||
<button
|
||
type="button"
|
||
onClick={handleCloseEventModal}
|
||
className="modal-btn-cancel"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleSaveEvent}
|
||
className="modal-btn-primary"
|
||
disabled={!eventFormData.title || !eventFormData.event_date}
|
||
>
|
||
{editingEvent ? 'Update Event' : 'Create Event'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default Dashboard;
|