Compare commits

..

2 Commits

Author SHA1 Message Date
James Pattinson
107c208746 Basic event management 2025-11-12 18:08:11 +00:00
James Pattinson
e5fdd0ecb8 Mobile improvements 2025-11-12 16:47:21 +00:00
11 changed files with 960 additions and 176 deletions

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import auth, users, tiers, memberships, payments, email, email_templates from . import auth, users, tiers, memberships, payments, email, email_templates, events
api_router = APIRouter() api_router = APIRouter()
@@ -10,3 +10,4 @@ api_router.include_router(memberships.router, prefix="/memberships", tags=["memb
api_router.include_router(payments.router, prefix="/payments", tags=["payments"]) api_router.include_router(payments.router, prefix="/payments", tags=["payments"])
api_router.include_router(email.router, prefix="/email", tags=["email"]) api_router.include_router(email.router, prefix="/email", tags=["email"])
api_router.include_router(email_templates.router, prefix="/email-templates", tags=["email-templates"]) api_router.include_router(email_templates.router, prefix="/email-templates", tags=["email-templates"])
api_router.include_router(events.router, prefix="/events", tags=["events"])

View File

@@ -0,0 +1,207 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime
from ...core.database import get_db
from ...models.models import Event, EventRSVP, User, EventStatus
from ...schemas import (
EventCreate, EventUpdate, EventResponse, EventRSVPResponse, EventRSVPUpdate, MessageResponse
)
from ...api.dependencies import get_current_active_user, get_admin_user
router = APIRouter()
@router.get("/", response_model=List[EventResponse])
async def get_events(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get all events (admin) or published events (members)"""
if current_user.role in ['admin', 'super_admin']:
events = db.query(Event).order_by(Event.event_date).all()
else:
events = db.query(Event).filter(
Event.status == EventStatus.PUBLISHED
).order_by(Event.event_date).all()
return events
@router.get("/upcoming", response_model=List[EventResponse])
async def get_upcoming_events(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get upcoming events"""
now = datetime.now()
events = db.query(Event).filter(
Event.event_date >= now.date(),
Event.status == EventStatus.PUBLISHED
).order_by(Event.event_date).all()
return events
@router.post("/", response_model=EventResponse, status_code=status.HTTP_201_CREATED)
async def create_event(
event_data: EventCreate,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""Create a new event (admin only)"""
# Validate event date is in the future
if event_data.event_date < datetime.now():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Event date must be in the future"
)
event = Event(
title=event_data.title,
description=event_data.description,
event_date=event_data.event_date,
event_time=event_data.event_time,
location=event_data.location,
max_attendees=event_data.max_attendees,
status=EventStatus.DRAFT,
created_by=current_user.id
)
db.add(event)
db.commit()
db.refresh(event)
return event
@router.put("/{event_id}", response_model=EventResponse)
async def update_event(
event_id: int,
event_data: EventUpdate,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""Update an event (admin only)"""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
# Update fields
for field, value in event_data.dict(exclude_unset=True).items():
setattr(event, field, value)
event.updated_at = datetime.now()
db.commit()
db.refresh(event)
return event
@router.delete("/{event_id}", response_model=MessageResponse)
async def delete_event(
event_id: int,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""Delete an event (admin only)"""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
db.delete(event)
db.commit()
return {"message": "Event deleted successfully"}
@router.get("/{event_id}/rsvps", response_model=List[EventRSVPResponse])
async def get_event_rsvps(
event_id: int,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""Get RSVPs for an event (admin only)"""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
rsvps = db.query(EventRSVP).filter(EventRSVP.event_id == event_id).all()
return rsvps
@router.post("/{event_id}/rsvp", response_model=EventRSVPResponse)
async def create_or_update_rsvp(
event_id: int,
rsvp_data: EventRSVPUpdate,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Create or update RSVP for an event"""
event = db.query(Event).filter(Event.id == event_id).first()
if not event:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Event not found"
)
if event.status != EventStatus.PUBLISHED:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Event is not available for RSVP"
)
# Check if RSVP already exists
existing_rsvp = db.query(EventRSVP).filter(
EventRSVP.event_id == event_id,
EventRSVP.user_id == current_user.id
).first()
if existing_rsvp:
# Update existing RSVP
existing_rsvp.status = rsvp_data.status
if rsvp_data.notes is not None:
existing_rsvp.notes = rsvp_data.notes
existing_rsvp.updated_at = datetime.now()
db.commit()
db.refresh(existing_rsvp)
return existing_rsvp
else:
# Check attendee limit
if event.max_attendees:
current_rsvp_count = db.query(EventRSVP).filter(
EventRSVP.event_id == event_id,
EventRSVP.status == 'attending'
).count()
if current_rsvp_count >= event.max_attendees:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Event is at maximum capacity"
)
# Create new RSVP
rsvp = EventRSVP(
event_id=event_id,
user_id=current_user.id,
status=rsvp_data.status,
notes=rsvp_data.notes
)
db.add(rsvp)
db.commit()
db.refresh(rsvp)
return rsvp
@router.get("/my-rsvps", response_model=List[EventRSVPResponse])
async def get_my_rsvps(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
"""Get current user's RSVPs"""
rsvps = db.query(EventRSVP).filter(EventRSVP.user_id == current_user.id).all()
return rsvps

View File

@@ -41,7 +41,7 @@ class Settings(BaseSettings):
FRONTEND_URL: str = "http://localhost:3500" FRONTEND_URL: str = "http://localhost:3500"
# CORS # CORS
BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8080"] BACKEND_CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8080", "https://members.sasalliance.org"]
# File Storage # File Storage
UPLOAD_DIR: str = "/app/uploads" UPLOAD_DIR: str = "/app/uploads"

View File

@@ -135,6 +135,7 @@ class Event(Base):
title = Column(String(255), nullable=False) title = Column(String(255), nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
event_date = Column(DateTime, nullable=False) event_date = Column(DateTime, nullable=False)
event_time = Column(String(10), nullable=True) # HH:MM format
location = Column(String(255), nullable=True) location = Column(String(255), nullable=True)
max_attendees = Column(Integer, nullable=True) max_attendees = Column(Integer, nullable=True)
status = Column(SQLEnum(EventStatus, values_callable=lambda x: [e.value for e in x]), default=EventStatus.DRAFT, nullable=False) status = Column(SQLEnum(EventStatus, values_callable=lambda x: [e.value for e in x]), default=EventStatus.DRAFT, nullable=False)

View File

@@ -30,6 +30,13 @@ from .schemas import (
EmailTemplateCreate, EmailTemplateCreate,
EmailTemplateUpdate, EmailTemplateUpdate,
EmailTemplateResponse, EmailTemplateResponse,
EventBase,
EventCreate,
EventUpdate,
EventResponse,
EventRSVPBase,
EventRSVPUpdate,
EventRSVPResponse,
) )
__all__ = [ __all__ = [
@@ -64,4 +71,11 @@ __all__ = [
"EmailTemplateCreate", "EmailTemplateCreate",
"EmailTemplateUpdate", "EmailTemplateUpdate",
"EmailTemplateResponse", "EmailTemplateResponse",
"EventBase",
"EventCreate",
"EventUpdate",
"EventResponse",
"EventRSVPBase",
"EventRSVPUpdate",
"EventRSVPResponse",
] ]

View File

@@ -229,3 +229,58 @@ class EmailTemplateResponse(EmailTemplateBase):
is_active: bool is_active: bool
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
# Event Schemas
class EventBase(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
event_date: datetime
event_time: Optional[str] = Field(None, pattern=r'^([01]?[0-9]|2[0-3]):[0-5][0-9]$')
location: Optional[str] = None
max_attendees: Optional[int] = Field(None, gt=0)
class EventCreate(EventBase):
pass
class EventUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
event_date: Optional[datetime] = None
event_time: Optional[str] = Field(None, pattern=r'^([01]?[0-9]|2[0-3]):[0-5][0-9]$')
location: Optional[str] = None
max_attendees: Optional[int] = Field(None, gt=0)
status: Optional[str] = None
class EventResponse(EventBase):
model_config = ConfigDict(from_attributes=True)
id: int
status: str
created_by: int
created_at: datetime
updated_at: datetime
# Event RSVP Schemas
class EventRSVPBase(BaseModel):
status: str = Field(..., pattern="^(pending|attending|not_attending|maybe)$")
notes: Optional[str] = None
class EventRSVPUpdate(EventRSVPBase):
pass
class EventRSVPResponse(EventRSVPBase):
model_config = ConfigDict(from_attributes=True)
id: int
event_id: int
user_id: int
attended: bool
created_at: datetime
updated_at: datetime

View File

@@ -71,6 +71,7 @@ CREATE TABLE events (
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
description TEXT, description TEXT,
event_date TIMESTAMP NOT NULL, event_date TIMESTAMP NOT NULL,
event_time VARCHAR(10), -- HH:MM format
location VARCHAR(255), location VARCHAR(255),
max_attendees INT, max_attendees INT,
status ENUM('draft', 'published', 'cancelled', 'completed') NOT NULL DEFAULT 'draft', status ENUM('draft', 'published', 'cancelled', 'completed') NOT NULL DEFAULT 'draft',

View File

@@ -79,7 +79,7 @@ body {
padding: 10px; padding: 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 16px;
} }
.form-group input:focus, .form-group input:focus,
@@ -185,6 +185,70 @@ body {
margin-top: 20px; margin-top: 20px;
} }
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.container {
padding: 10px;
}
.card {
padding: 16px;
}
.navbar {
padding: 12px 16px;
}
.navbar h1 {
font-size: 18px;
}
/* Make tables responsive */
table {
width: 100%;
min-width: 600px; /* Ensure minimum width for readability */
}
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Auth pages mobile adjustments */
.auth-container {
flex-direction: column;
padding: 20px;
gap: 20px;
}
.auth-card {
max-width: 100%;
padding: 24px;
}
/* Welcome section mobile adjustments */
.welcome-section {
max-width: 100% !important;
padding: 20px !important;
}
.welcome-section h1 {
font-size: 1.8rem !important;
}
/* Form grid mobile adjustments */
@media (max-width: 768px) {
form[style*="grid-template-columns"] {
grid-template-columns: 1fr !important;
gap: 16px !important;
}
}
}
.status-badge { .status-badge {
display: inline-block; display: inline-block;
padding: 4px 12px; padding: 4px 12px;
@@ -255,7 +319,7 @@ body {
padding: 8px; padding: 8px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 16px;
color: #333; color: #333;
background-color: #fff; background-color: #fff;
} }

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; 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 MembershipSetup from '../components/MembershipSetup';
import ProfileMenu from '../components/ProfileMenu'; import ProfileMenu from '../components/ProfileMenu';
import ProfileEdit from '../components/ProfileEdit'; import ProfileEdit from '../components/ProfileEdit';
@@ -21,6 +21,11 @@ const Dashboard: React.FC = () => {
const [showUserDetails, setShowUserDetails] = useState(false); const [showUserDetails, setShowUserDetails] = useState(false);
const [isEditingUser, setIsEditingUser] = useState(false); const [isEditingUser, setIsEditingUser] = useState(false);
const [editFormData, setEditFormData] = useState<Partial<User>>({}); 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(() => { useEffect(() => {
if (!authService.isAuthenticated()) { if (!authService.isAuthenticated()) {
@@ -31,6 +36,16 @@ const Dashboard: React.FC = () => {
loadData(); 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 () => { const loadData = async () => {
try { try {
const [userData, membershipData, paymentData] = await Promise.all([ const [userData, membershipData, paymentData] = await Promise.all([
@@ -43,16 +58,31 @@ const Dashboard: React.FC = () => {
setMemberships(membershipData); setMemberships(membershipData);
setPayments(paymentData); 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 // Load admin data if user is admin
if (userData.role === 'admin' || userData.role === 'super_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(), paymentService.getAllPayments(),
membershipService.getAllMemberships(), membershipService.getAllMemberships(),
userService.getAllUsers() userService.getAllUsers(),
eventService.getAllEvents()
]); ]);
setAllPayments(allPaymentsData); setAllPayments(allPaymentsData);
setAllMemberships(allMembershipsData); setAllMemberships(allMembershipsData);
setAllUsers(allUsersData); setAllUsers(allUsersData);
setAllEvents(allEventsData);
// Load RSVP counts for all events
await loadEventRSVPCounts(allEventsData);
} }
} catch (error) { } catch (error) {
console.error('Failed to load data:', 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 = () => { const handleMembershipSetup = () => {
setShowMembershipSetup(true); setShowMembershipSetup(true);
}; };
@@ -198,11 +248,73 @@ const Dashboard: React.FC = () => {
} }
}; };
const handleFormChange = (field: keyof User, value: string) => { const handleRSVP = async (eventId: number, status: 'attending' | 'not_attending' | 'maybe') => {
setEditFormData(prev => ({ // Set loading state for this event
...prev, setRsvpLoading(prev => ({ ...prev, [eventId]: true }));
[field]: value
})); // 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) => { const formatDate = (dateString: string) => {
@@ -271,10 +383,11 @@ const Dashboard: React.FC = () => {
<div className="card"> <div className="card">
<h3 style={{ marginBottom: '16px' }}>Your Membership</h3> <h3 style={{ marginBottom: '16px' }}>Your Membership</h3>
<h4 style={{ color: '#0066cc', marginBottom: '8px' }}>{activeMembership.tier.name}</h4> <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>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>Annual Fee:</strong> £{activeMembership.tier.annual_fee.toFixed(2)}</p>
<p><strong>Member since:</strong> {formatDate(activeMembership.start_date)}</p> <p><strong>Valid From:</strong> {formatDate(activeMembership.start_date)}</p>
<p><strong>Renewal Date:</strong> {formatDate(activeMembership.end_date)}</p> <p><strong>Valid Until:</strong> {formatDate(activeMembership.end_date)}</p>
<p><strong>Auto Renew:</strong> {activeMembership.auto_renew ? 'Yes' : 'No'}</p> <p><strong>Auto Renew:</strong> {activeMembership.auto_renew ? 'Yes' : 'No'}</p>
<div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}> <div style={{ marginTop: '12px', padding: '12px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
<strong>Benefits:</strong> <strong>Benefits:</strong>
@@ -301,35 +414,156 @@ const Dashboard: React.FC = () => {
<div className="card" style={{ marginTop: '20px' }}> <div className="card" style={{ marginTop: '20px' }}>
<h3 style={{ marginBottom: '16px' }}>Payment History</h3> <h3 style={{ marginBottom: '16px' }}>Payment History</h3>
{payments.length > 0 ? ( {payments.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <div className="table-container">
<thead> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tr style={{ borderBottom: '2px solid #ddd' }}> <thead>
<th style={{ padding: '12px', textAlign: 'left' }}>Date</th> <tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Amount</th> <th style={{ padding: '12px', textAlign: 'left' }}>Date</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Method</th> <th style={{ padding: '12px', textAlign: 'left' }}>Amount</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th> <th style={{ padding: '12px', textAlign: 'left' }}>Method</th>
</tr> <th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
</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> </tr>
))} </thead>
</tbody> <tbody>
</table> {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> <p style={{ color: '#666' }}>No payment history available.</p>
)} )}
</div> </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 */} {/* Admin Section */}
{(user?.role === 'admin' || user?.role === 'super_admin') && ( {(user?.role === 'admin' || user?.role === 'super_admin') && (
<div className="card" style={{ marginTop: '20px' }}> <div className="card" style={{ marginTop: '20px' }}>
@@ -339,41 +573,43 @@ const Dashboard: React.FC = () => {
{allPayments.filter(p => p.status === 'pending').length > 0 && ( {allPayments.filter(p => p.status === 'pending').length > 0 && (
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<h4 style={{ marginBottom: '12px' }}>Pending Payments</h4> <h4 style={{ marginBottom: '12px' }}>Pending Payments</h4>
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <div className="table-container">
<thead> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tr style={{ borderBottom: '2px solid #ddd' }}> <thead>
<th style={{ padding: '12px', textAlign: 'left' }}>User</th> <tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Amount</th> <th style={{ padding: '12px', textAlign: 'left' }}>User</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Method</th> <th style={{ padding: '12px', textAlign: 'left' }}>Amount</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Membership</th> <th style={{ padding: '12px', textAlign: 'left' }}>Method</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th> <th style={{ padding: '12px', textAlign: 'left' }}>Membership</th>
</tr> <th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
</thead> </tr>
<tbody> </thead>
{allPayments.filter(p => p.status === 'pending').map(payment => { <tbody>
const membership = allMemberships.find(m => m.id === payment.membership_id); {allPayments.filter(p => p.status === 'pending').map(payment => {
return ( const membership = allMemberships.find(m => m.id === payment.membership_id);
<tr key={payment.id} style={{ borderBottom: '1px solid #eee' }}> return (
<td style={{ padding: '12px' }}>{getUserName(payment.user_id)}</td> <tr key={payment.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}>£{payment.amount.toFixed(2)}</td> <td style={{ padding: '12px' }}>{getUserName(payment.user_id)}</td>
<td style={{ padding: '12px' }}>{payment.payment_method}</td> <td style={{ padding: '12px' }}>£{payment.amount.toFixed(2)}</td>
<td style={{ padding: '12px' }}> <td style={{ padding: '12px' }}>{payment.payment_method}</td>
{membership ? `${membership.tier.name} (${membership.status})` : 'N/A'} <td style={{ padding: '12px' }}>
</td> {membership ? `${membership.tier.name} (${membership.status})` : 'N/A'}
<td style={{ padding: '12px' }}> </td>
<button <td style={{ padding: '12px' }}>
className="btn btn-primary" <button
onClick={() => handleApprovePayment(payment.id, payment.membership_id || undefined)} className="btn btn-primary"
style={{ fontSize: '12px', padding: '6px 12px' }} onClick={() => handleApprovePayment(payment.id, payment.membership_id || undefined)}
> style={{ fontSize: '12px', padding: '6px 12px' }}
Approve >
</button> Approve
</td> </button>
</tr> </td>
); </tr>
})} );
</tbody> })}
</table> </tbody>
</table>
</div>
</div> </div>
)} )}
@@ -381,30 +617,32 @@ const Dashboard: React.FC = () => {
{allMemberships.filter(m => m.status === 'pending').length > 0 && ( {allMemberships.filter(m => m.status === 'pending').length > 0 && (
<div> <div>
<h4 style={{ marginBottom: '12px' }}>Pending Memberships</h4> <h4 style={{ marginBottom: '12px' }}>Pending Memberships</h4>
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <div className="table-container">
<thead> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tr style={{ borderBottom: '2px solid #ddd' }}> <thead>
<th style={{ padding: '12px', textAlign: 'left' }}>User</th> <tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Tier</th> <th style={{ padding: '12px', textAlign: 'left' }}>User</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Start Date</th> <th style={{ padding: '12px', textAlign: 'left' }}>Tier</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th> <th style={{ padding: '12px', textAlign: 'left' }}>Start Date</th>
</tr> <th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
</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> </tr>
))} </thead>
</tbody> <tbody>
</table> {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> </div>
)} )}
@@ -437,78 +675,85 @@ const Dashboard: React.FC = () => {
/> />
</div> </div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <div className="table-container">
<thead> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<tr style={{ borderBottom: '2px solid #ddd' }}> <thead>
<th style={{ padding: '12px', textAlign: 'left' }}>Name</th> <tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Email</th> <th style={{ padding: '12px', textAlign: 'left' }}>Name</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Role</th> <th style={{ padding: '12px', textAlign: 'left' }}>Email</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th> <th style={{ padding: '12px', textAlign: 'left' }}>Membership #</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Joined</th> <th style={{ padding: '12px', textAlign: 'left' }}>Role</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th> <th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
</tr> <th style={{ padding: '12px', textAlign: 'left' }}>Joined</th>
</thead> <th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
<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> </tr>
))} </thead>
</tbody> <tbody>
</table> {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>
)} )}
</div> </div>
@@ -521,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 */} {/* User Details Modal */}
{showUserDetails && selectedUser && ( {showUserDetails && selectedUser && (
<div style={{ <div style={{

View File

@@ -43,7 +43,7 @@ const Login: React.FC = () => {
}; };
return ( return (
<div className="auth-container" style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: '40px', padding: '20px' }}> <div className="auth-container" style={{ gap: '40px', padding: '20px' }}>
<div className="welcome-section" style={{ <div className="welcome-section" style={{
flex: '1', flex: '1',
maxWidth: '400px', maxWidth: '400px',

View File

@@ -14,6 +14,10 @@ export interface LoginData {
password: string; password: string;
} }
export interface ForgotPasswordData {
email: string;
}
export interface User { export interface User {
id: number; id: number;
email: string; email: string;
@@ -62,10 +66,6 @@ export interface Payment {
created_at: string; created_at: string;
} }
export interface ForgotPasswordData {
email: string;
}
export interface ResetPasswordData { export interface ResetPasswordData {
token: string; token: string;
new_password: string; new_password: string;
@@ -83,6 +83,14 @@ export interface MembershipCreateData {
auto_renew: boolean; auto_renew: boolean;
} }
export interface MembershipTierUpdateData {
name?: string;
description?: string;
annual_fee?: number;
benefits?: string;
is_active?: boolean;
}
export interface MembershipUpdateData { export interface MembershipUpdateData {
status?: string; status?: string;
start_date?: string; start_date?: string;
@@ -111,12 +119,54 @@ export interface MembershipTierCreateData {
benefits?: string; benefits?: string;
} }
export interface MembershipTierUpdateData { export interface Event {
name?: string; 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; description?: string;
annual_fee?: number; event_date: string;
benefits?: string; event_time?: string;
is_active?: boolean; 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 = { export const authService = {
@@ -245,3 +295,45 @@ export const paymentService = {
return response.data; 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;
}
};