Files
sasa-membership/frontend/src/pages/Dashboard.tsx
James Pattinson dac8b43915 Event editing
2025-11-12 21:04:00 +00:00

1241 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;