Basic event management
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { authService, userService, membershipService, paymentService, User, Membership, Payment } from '../services/membershipService';
|
||||
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';
|
||||
@@ -21,6 +21,11 @@ const Dashboard: React.FC = () => {
|
||||
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}>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!authService.isAuthenticated()) {
|
||||
@@ -31,6 +36,16 @@ const Dashboard: React.FC = () => {
|
||||
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([
|
||||
@@ -43,16 +58,31 @@ const Dashboard: React.FC = () => {
|
||||
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] = await Promise.all([
|
||||
const [allPaymentsData, allMembershipsData, allUsersData, allEventsData] = await Promise.all([
|
||||
paymentService.getAllPayments(),
|
||||
membershipService.getAllMemberships(),
|
||||
userService.getAllUsers()
|
||||
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);
|
||||
@@ -61,6 +91,26 @@ const Dashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -198,11 +248,73 @@ const Dashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormChange = (field: keyof User, value: string) => {
|
||||
setEditFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
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 formatDate = (dateString: string) => {
|
||||
@@ -271,10 +383,11 @@ const Dashboard: React.FC = () => {
|
||||
<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>Member since:</strong> {formatDate(activeMembership.start_date)}</p>
|
||||
<p><strong>Renewal Date:</strong> {formatDate(activeMembership.end_date)}</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>
|
||||
@@ -332,6 +445,125 @@ const Dashboard: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Upcoming Events */}
|
||||
<div className="card" style={{ marginTop: '20px' }}>
|
||||
<h3 style={{ marginBottom: '16px' }}>Upcoming Events</h3>
|
||||
{upcomingEvents.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{upcomingEvents.map(event => (
|
||||
<div key={event.id} style={{
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
backgroundColor: '#f9f9f9'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
|
||||
<div>
|
||||
<h4 style={{ margin: '0 0 4px 0', color: '#0066cc' }}>{event.title}</h4>
|
||||
<p style={{ margin: '0', fontSize: '14px', color: '#666' }}>
|
||||
{formatDate(event.event_date)} at {event.event_time}
|
||||
</p>
|
||||
{event.location && (
|
||||
<p style={{ margin: '4px 0 0 0', fontSize: '14px', color: '#666' }}>
|
||||
📍 {event.location}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
className={`btn ${event.rsvp_status === 'attending' ? 'btn-success' : 'btn-outline-secondary'}`}
|
||||
onClick={() => handleRSVP(event.id, 'attending')}
|
||||
disabled={rsvpLoading[event.id]}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
padding: '8px 16px',
|
||||
opacity: rsvpLoading[event.id] ? 0.6 : event.rsvp_status ? (event.rsvp_status === 'attending' ? 1 : 0.4) : 1,
|
||||
cursor: rsvpLoading[event.id] ? 'not-allowed' : 'pointer',
|
||||
fontWeight: event.rsvp_status === 'attending' ? 'bold' : 'normal',
|
||||
border: event.rsvp_status === 'attending' ? '3px solid #28a745' : '2px solid #adb5bd',
|
||||
backgroundColor: event.rsvp_status === 'attending' ? '#28a745' : 'transparent',
|
||||
color: event.rsvp_status === 'attending' ? 'white' : '#6c757d',
|
||||
transform: event.rsvp_status === 'attending' ? 'scale(1.1)' : event.rsvp_status ? 'scale(0.95)' : 'scale(1)',
|
||||
boxShadow: event.rsvp_status === 'attending' ? '0 4px 8px rgba(40, 167, 69, 0.3)' : 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
filter: event.rsvp_status && event.rsvp_status !== 'attending' ? 'grayscale(50%)' : 'none'
|
||||
}}
|
||||
>
|
||||
{rsvpLoading[event.id] ? '...' : 'Attending'}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${event.rsvp_status === 'maybe' ? 'btn-warning' : 'btn-outline-secondary'}`}
|
||||
onClick={() => handleRSVP(event.id, 'maybe')}
|
||||
disabled={rsvpLoading[event.id]}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
padding: '8px 16px',
|
||||
opacity: rsvpLoading[event.id] ? 0.6 : event.rsvp_status ? (event.rsvp_status === 'maybe' ? 1 : 0.4) : 1,
|
||||
cursor: rsvpLoading[event.id] ? 'not-allowed' : 'pointer',
|
||||
fontWeight: event.rsvp_status === 'maybe' ? 'bold' : 'normal',
|
||||
border: event.rsvp_status === 'maybe' ? '3px solid #ffc107' : '2px solid #adb5bd',
|
||||
backgroundColor: event.rsvp_status === 'maybe' ? '#ffc107' : 'transparent',
|
||||
color: event.rsvp_status === 'maybe' ? '#212529' : '#6c757d',
|
||||
transform: event.rsvp_status === 'maybe' ? 'scale(1.1)' : event.rsvp_status ? 'scale(0.95)' : 'scale(1)',
|
||||
boxShadow: event.rsvp_status === 'maybe' ? '0 4px 8px rgba(255, 193, 7, 0.3)' : 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
filter: event.rsvp_status && event.rsvp_status !== 'maybe' ? 'grayscale(50%)' : 'none'
|
||||
}}
|
||||
>
|
||||
{rsvpLoading[event.id] ? '...' : 'Maybe'}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${event.rsvp_status === 'not_attending' ? 'btn-danger' : 'btn-outline-secondary'}`}
|
||||
onClick={() => handleRSVP(event.id, 'not_attending')}
|
||||
disabled={rsvpLoading[event.id]}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
padding: '8px 16px',
|
||||
opacity: rsvpLoading[event.id] ? 0.6 : event.rsvp_status ? (event.rsvp_status === 'not_attending' ? 1 : 0.4) : 1,
|
||||
cursor: rsvpLoading[event.id] ? 'not-allowed' : 'pointer',
|
||||
fontWeight: event.rsvp_status === 'not_attending' ? 'bold' : 'normal',
|
||||
border: event.rsvp_status === 'not_attending' ? '3px solid #dc3545' : '2px solid #adb5bd',
|
||||
backgroundColor: event.rsvp_status === 'not_attending' ? '#dc3545' : 'transparent',
|
||||
color: event.rsvp_status === 'not_attending' ? 'white' : '#6c757d',
|
||||
transform: event.rsvp_status === 'not_attending' ? 'scale(1.1)' : event.rsvp_status ? 'scale(0.95)' : 'scale(1)',
|
||||
boxShadow: event.rsvp_status === 'not_attending' ? '0 4px 8px rgba(220, 53, 69, 0.3)' : 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
filter: event.rsvp_status && event.rsvp_status !== 'not_attending' ? 'grayscale(50%)' : 'none'
|
||||
}}
|
||||
>
|
||||
{rsvpLoading[event.id] ? '...' : 'Not Attending'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{event.description && (
|
||||
<p style={{ margin: '0', fontSize: '14px', lineHeight: '1.4' }}>
|
||||
{event.description}
|
||||
</p>
|
||||
)}
|
||||
{event.rsvp_status && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: event.rsvp_status === 'attending' ? '#d4edda' :
|
||||
event.rsvp_status === 'maybe' ? '#fff3cd' : '#f8d7da',
|
||||
border: `1px solid ${event.rsvp_status === 'attending' ? '#c3e6cb' :
|
||||
event.rsvp_status === 'maybe' ? '#ffeaa7' : '#f5c6cb'}`,
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: event.rsvp_status === 'attending' ? '#155724' :
|
||||
event.rsvp_status === 'maybe' ? '#856404' : '#721c24'
|
||||
}}>
|
||||
<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>
|
||||
|
||||
{/* Admin Section */}
|
||||
{(user?.role === 'admin' || user?.role === 'super_admin') && (
|
||||
<div className="card" style={{ marginTop: '20px' }}>
|
||||
@@ -449,6 +681,7 @@ const Dashboard: React.FC = () => {
|
||||
<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>
|
||||
@@ -456,64 +689,68 @@ const Dashboard: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map(u => (
|
||||
<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' }}>
|
||||
<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>
|
||||
))}
|
||||
{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>
|
||||
@@ -529,6 +766,110 @@ const Dashboard: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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={() => {/* TODO: Implement create event modal */}}
|
||||
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={() => {/* TODO: Implement edit 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={{
|
||||
|
||||
@@ -14,6 +14,10 @@ export interface LoginData {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordData {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
@@ -62,10 +66,6 @@ export interface Payment {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordData {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordData {
|
||||
token: string;
|
||||
new_password: string;
|
||||
@@ -83,6 +83,14 @@ export interface MembershipCreateData {
|
||||
auto_renew: boolean;
|
||||
}
|
||||
|
||||
export interface MembershipTierUpdateData {
|
||||
name?: string;
|
||||
description?: string;
|
||||
annual_fee?: number;
|
||||
benefits?: string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface MembershipUpdateData {
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
@@ -111,12 +119,54 @@ export interface MembershipTierCreateData {
|
||||
benefits?: string;
|
||||
}
|
||||
|
||||
export interface MembershipTierUpdateData {
|
||||
name?: string;
|
||||
export interface Event {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
event_date: string;
|
||||
event_time: string | null;
|
||||
location: string | null;
|
||||
max_attendees: number | null;
|
||||
status: string;
|
||||
created_by: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
rsvp_status?: string; // Current user's RSVP status
|
||||
}
|
||||
|
||||
export interface EventCreateData {
|
||||
title: string;
|
||||
description?: string;
|
||||
annual_fee?: number;
|
||||
benefits?: string;
|
||||
is_active?: boolean;
|
||||
event_date: string;
|
||||
event_time?: string;
|
||||
location?: string;
|
||||
max_attendees?: number;
|
||||
}
|
||||
|
||||
export interface EventUpdateData {
|
||||
title?: string;
|
||||
description?: string;
|
||||
event_date?: string;
|
||||
event_time?: string;
|
||||
location?: string;
|
||||
max_attendees?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface EventRSVP {
|
||||
id: number;
|
||||
event_id: number;
|
||||
user_id: number;
|
||||
status: string;
|
||||
attended: boolean;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface EventRSVPData {
|
||||
status: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export const authService = {
|
||||
@@ -245,3 +295,45 @@ export const paymentService = {
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const eventService = {
|
||||
async getAllEvents(): Promise<Event[]> {
|
||||
const response = await api.get('/events/');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getUpcomingEvents(): Promise<Event[]> {
|
||||
const response = await api.get('/events/upcoming');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createEvent(data: EventCreateData): Promise<Event> {
|
||||
const response = await api.post('/events/', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateEvent(eventId: number, data: EventUpdateData): Promise<Event> {
|
||||
const response = await api.put(`/events/${eventId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteEvent(eventId: number): Promise<{ message: string }> {
|
||||
const response = await api.delete(`/events/${eventId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getEventRSVPs(eventId: number): Promise<EventRSVP[]> {
|
||||
const response = await api.get(`/events/${eventId}/rsvps`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createOrUpdateRSVP(eventId: number, data: EventRSVPData): Promise<EventRSVP> {
|
||||
const response = await api.post(`/events/${eventId}/rsvp`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getMyRSVPs(): Promise<EventRSVP[]> {
|
||||
const response = await api.get('/events/my-rsvps');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user