Adding more shit

This commit is contained in:
James Pattinson
2025-11-10 15:42:09 +00:00
parent f1c4ff19d6
commit 43b13ef52d
10 changed files with 682 additions and 21 deletions

View File

@@ -4,11 +4,12 @@ from typing import List
from datetime import date, timedelta
from ...core.database import get_db
from ...models.models import Membership, MembershipStatus, User, MembershipTier
from ...models.models import Membership, MembershipStatus, User, MembershipTier, Payment, PaymentStatus
from ...schemas import (
MembershipCreate, MembershipUpdate, MembershipResponse, MessageResponse
)
from ...api.dependencies import get_current_active_user, get_admin_user
from ...services.email_service import email_service
router = APIRouter()
@@ -112,6 +113,11 @@ async def update_membership(
)
update_data = membership_update.model_dump(exclude_unset=True)
was_activated = False
# Check if status is being changed to ACTIVE
if update_data.get("status") == MembershipStatus.ACTIVE and membership.status != MembershipStatus.ACTIVE:
was_activated = True
for field, value in update_data.items():
setattr(membership, field, value)
@@ -119,6 +125,32 @@ async def update_membership(
db.commit()
db.refresh(membership)
# Send activation email if membership was just activated
if was_activated:
try:
# Get the most recent payment for this membership
recent_payment = db.query(Payment).filter(
Payment.membership_id == membership.id,
Payment.status == PaymentStatus.COMPLETED
).order_by(Payment.payment_date.desc()).first()
payment_amount = recent_payment.amount if recent_payment else membership.tier.annual_fee
payment_method = recent_payment.payment_method.value if recent_payment else "N/A"
# Send activation email (non-blocking)
await email_service.send_membership_activation_email(
to_email=membership.user.email,
first_name=membership.user.first_name,
membership_tier=membership.tier.name,
annual_fee=membership.tier.annual_fee,
payment_amount=payment_amount,
payment_method=payment_method,
renewal_date=membership.end_date.strftime("%d %B %Y")
)
except Exception as e:
# Log error but don't fail the membership update
print(f"Failed to send membership activation email: {e}")
return membership

View File

@@ -4,11 +4,12 @@ from typing import List
from datetime import datetime
from ...core.database import get_db
from ...models.models import Payment, PaymentStatus, User, Membership
from ...models.models import Payment, PaymentStatus, User, Membership, MembershipStatus
from ...schemas import (
PaymentCreate, PaymentUpdate, PaymentResponse, MessageResponse
)
from ...api.dependencies import get_current_active_user, get_admin_user
from ...services.email_service import email_service
router = APIRouter()
@@ -114,6 +115,31 @@ async def update_payment(
db.commit()
db.refresh(payment)
# If payment was just marked as completed and has an associated membership,
# activate the membership and send activation email
if (update_data.get("status") == PaymentStatus.COMPLETED and
payment.membership_id and
payment.membership.status == MembershipStatus.PENDING):
# Activate the membership
payment.membership.status = MembershipStatus.ACTIVE
db.commit()
# Send activation email (non-blocking)
try:
await email_service.send_membership_activation_email(
to_email=payment.membership.user.email,
first_name=payment.membership.user.first_name,
membership_tier=payment.membership.tier.name,
annual_fee=payment.membership.tier.annual_fee,
payment_amount=payment.amount,
payment_method=payment.payment_method.value,
renewal_date=payment.membership.end_date.strftime("%d %B %Y")
)
except Exception as e:
# Log error but don't fail the payment update
print(f"Failed to send membership activation email: {e}")
return payment
@@ -152,6 +178,7 @@ async def record_manual_payment(
)
# Verify membership if provided
membership = None
if payment_data.membership_id:
membership = db.query(Membership).filter(
Membership.id == payment_data.membership_id,
@@ -178,4 +205,24 @@ async def record_manual_payment(
db.commit()
db.refresh(payment)
# If payment has an associated membership that's pending, activate it and send email
if membership and membership.status == MembershipStatus.PENDING:
membership.status = MembershipStatus.ACTIVE
db.commit()
# Send activation email (non-blocking)
try:
await email_service.send_membership_activation_email(
to_email=membership.user.email,
first_name=membership.user.first_name,
membership_tier=membership.tier.name,
annual_fee=membership.tier.annual_fee,
payment_amount=payment.amount,
payment_method=payment.payment_method.value,
renewal_date=membership.end_date.strftime("%d %B %Y")
)
except Exception as e:
# Log error but don't fail the payment creation
print(f"Failed to send membership activation email: {e}")
return payment

View File

@@ -7,7 +7,7 @@ from ...models.models import MembershipTier
from ...schemas import (
MembershipTierCreate, MembershipTierUpdate, MembershipTierResponse, MessageResponse
)
from ...api.dependencies import get_current_active_user, get_admin_user
from ...api.dependencies import get_current_active_user, get_admin_user, get_super_admin_user
router = APIRouter()
@@ -47,10 +47,10 @@ async def get_membership_tier(
@router.post("/", response_model=MembershipTierResponse, status_code=status.HTTP_201_CREATED)
async def create_membership_tier(
tier_data: MembershipTierCreate,
current_user = Depends(get_admin_user),
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""Create a new membership tier (admin only)"""
"""Create a new membership tier (super admin only)"""
# Check if tier with same name exists
existing_tier = db.query(MembershipTier).filter(
MembershipTier.name == tier_data.name
@@ -74,10 +74,10 @@ async def create_membership_tier(
async def update_membership_tier(
tier_id: int,
tier_update: MembershipTierUpdate,
current_user = Depends(get_admin_user),
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""Update membership tier (admin only)"""
"""Update membership tier (super admin only)"""
tier = db.query(MembershipTier).filter(MembershipTier.id == tier_id).first()
if not tier:
raise HTTPException(
@@ -99,10 +99,10 @@ async def update_membership_tier(
@router.delete("/{tier_id}", response_model=MessageResponse)
async def delete_membership_tier(
tier_id: int,
current_user = Depends(get_admin_user),
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""Delete membership tier (admin only)"""
"""Delete membership tier (super admin only)"""
tier = db.query(MembershipTier).filter(MembershipTier.id == tier_id).first()
if not tier:
raise HTTPException(

View File

@@ -1,5 +1,6 @@
import httpx
from typing import List, Optional
from datetime import datetime
from ..core.config import settings
@@ -141,6 +142,83 @@ class EmailService:
return await self.send_email(to_email, subject, html_body, text_body)
async def send_membership_activation_email(
self,
to_email: str,
first_name: str,
membership_tier: str,
annual_fee: float,
payment_amount: float,
payment_method: str,
renewal_date: str
) -> dict:
"""Send membership activation email with payment details and renewal date"""
subject = f"Your {settings.APP_NAME} Membership is Now Active!"
html_body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #28a745;">Welcome to {settings.APP_NAME}!</h2>
<p>Hello {first_name},</p>
<p>Great news! Your membership has been successfully activated. You now have full access to all the benefits of your membership tier.</p>
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #28a745;">
<h3 style="margin-top: 0; color: #28a745;">Membership Details</h3>
<p style="margin: 8px 0;"><strong>Membership Tier:</strong> {membership_tier}</p>
<p style="margin: 8px 0;"><strong>Annual Fee:</strong> £{annual_fee:.2f}</p>
<p style="margin: 8px 0;"><strong>Next Renewal Date:</strong> {renewal_date}</p>
</div>
<div style="background-color: #e9ecef; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0; color: #495057;">Payment Information</h3>
<p style="margin: 8px 0;"><strong>Amount Paid:</strong> £{payment_amount:.2f}</p>
<p style="margin: 8px 0;"><strong>Payment Method:</strong> {payment_method}</p>
<p style="margin: 8px 0;"><strong>Payment Date:</strong> {datetime.now().strftime('%d %B %Y')}</p>
</div>
<p>Your membership will automatically renew on <strong>{renewal_date}</strong> unless you choose to cancel it. You can manage your membership settings in your account dashboard.</p>
<p>If you have any questions about your membership or need assistance, please don't hesitate to contact us.</p>
<p>Welcome to the {settings.APP_NAME} community!</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME} Team</strong></p>
</body>
</html>
"""
text_body = f"""
Welcome to {settings.APP_NAME}!
Hello {first_name},
Great news! Your membership has been successfully activated. You now have full access to all the benefits of your membership tier.
MEMBERSHIP DETAILS
------------------
Membership Tier: {membership_tier}
Annual Fee: £{annual_fee:.2f}
Next Renewal Date: {renewal_date}
PAYMENT INFORMATION
-------------------
Amount Paid: £{payment_amount:.2f}
Payment Method: {payment_method}
Payment Date: {datetime.now().strftime('%d %B %Y')}
Your membership will automatically renew on {renewal_date} unless you choose to cancel it. You can manage your membership settings in your account dashboard.
If you have any questions about your membership or need assistance, please don't hesitate to contact us.
Welcome to the {settings.APP_NAME} community!
Best regards,
{settings.APP_NAME} Team
"""
return await self.send_email(to_email, subject, html_body, text_body)
async def send_membership_renewal_reminder(
self,
to_email: str,

View File

@@ -309,3 +309,79 @@ body {
background: #6c757d;
cursor: not-allowed;
}
/* Tab Styles */
.tab-active {
padding: 10px 20px;
border: none;
background: #007bff;
color: white;
cursor: pointer;
border-bottom: 2px solid #007bff;
font-weight: bold;
}
.tab-inactive {
padding: 10px 20px;
border: none;
background: none;
color: #666;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-inactive:hover {
color: #007bff;
border-bottom-color: #007bff;
}
/* Super Admin Panel Styles */
.super-admin-table th {
color: #333;
font-weight: 600;
}
.super-admin-table td {
color: #555;
}
.super-admin-loading {
color: #666;
}
.super-admin-placeholder {
color: #666;
}
/* Action buttons in tables */
.action-btn {
padding: 6px 12px;
border: 1px solid #007bff;
border-radius: 4px;
background: #007bff;
color: white !important;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s;
display: inline-block;
text-decoration: none;
}
.action-btn:hover {
background: #0056b3;
border-color: #0056b3;
color: white !important;
}
.action-btn-danger {
border-color: #dc3545;
background: #dc3545;
color: white !important;
}
.action-btn-danger:hover {
background: #c82333;
border-color: #c82333;
color: white !important;
}

View File

@@ -147,7 +147,7 @@ const MembershipSetup: React.FC<MembershipSetupProps> = ({ onMembershipCreated,
<div style={{ backgroundColor: '#fff3cd', border: '1px solid #ffeaa7', borderRadius: '4px', padding: '16px', marginBottom: '20px' }}>
<strong>Demo Payment</strong>
<p style={{ marginTop: '8px', marginBottom: 0 }}>
This is a fake payment flow for demonstration purposes. In a real application, you would integrate with a payment processor like Stripe or Square.
This is a fake payment flow for demo purposes. Square will come soon
</p>
</div>

View File

@@ -1,14 +1,17 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { authService } from '../services/membershipService';
import SuperAdminMenu from './SuperAdminMenu';
interface ProfileMenuProps {
userName: string;
userRole: string;
}
const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName }) => {
const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
const [isOpen, setIsOpen] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
const [showSuperAdmin, setShowSuperAdmin] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
@@ -35,8 +38,13 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName }) => {
setIsOpen(false);
};
const handleCloseChangePassword = () => {
setShowChangePassword(false);
const handleSuperAdmin = () => {
setShowSuperAdmin(true);
setIsOpen(false);
};
const handleCloseSuperAdmin = () => {
setShowSuperAdmin(false);
};
const dropdownStyle: React.CSSProperties = {
@@ -85,8 +93,20 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName }) => {
{isOpen && (
<div style={dropdownStyle}>
{userRole === 'super_admin' && (
<button
style={{ ...menuItemStyle, borderRadius: '4px 4px 0 0' }}
onClick={handleSuperAdmin}
>
Super Admin Panel
</button>
)}
<button
style={{
...menuItemStyle,
borderRadius: userRole === 'super_admin' ? '0' : '4px 4px 0 0',
borderTop: userRole === 'super_admin' ? '1px solid #eee' : 'none'
}}
onClick={handleChangePassword}
>
Change Password
@@ -104,6 +124,10 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName }) => {
{showChangePassword && (
<ChangePasswordModal onClose={handleCloseChangePassword} />
)}
{showSuperAdmin && (
<SuperAdminMenu onClose={handleCloseSuperAdmin} />
)}
</>
);
};

View File

@@ -0,0 +1,376 @@
import React, { useState, useEffect } from 'react';
import { membershipService, MembershipTier, MembershipTierCreateData, MembershipTierUpdateData } from '../services/membershipService';
interface SuperAdminMenuProps {
onClose: () => void;
}
const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
const [activeTab, setActiveTab] = useState<'tiers' | 'users' | 'system'>('tiers');
const [tiers, setTiers] = useState<MembershipTier[]>([]);
const [loading, setLoading] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingTier, setEditingTier] = useState<MembershipTier | null>(null);
useEffect(() => {
if (activeTab === 'tiers') {
loadTiers();
}
}, [activeTab]);
const loadTiers = async () => {
try {
setLoading(true);
const tierData = await membershipService.getAllTiers(true);
setTiers(tierData);
} catch (error) {
console.error('Failed to load tiers:', error);
alert('Failed to load membership tiers');
} finally {
setLoading(false);
}
};
const handleCreateTier = async (data: MembershipTierCreateData) => {
try {
await membershipService.createTier(data);
setShowCreateForm(false);
loadTiers();
} catch (error: any) {
alert(error.response?.data?.detail || 'Failed to create tier');
}
};
const handleUpdateTier = async (tierId: number, data: MembershipTierUpdateData) => {
try {
await membershipService.updateTier(tierId, data);
setEditingTier(null);
loadTiers();
} catch (error: any) {
alert(error.response?.data?.detail || 'Failed to update tier');
}
};
const handleDeleteTier = async (tierId: number) => {
if (!confirm('Are you sure you want to delete this membership tier? This action cannot be undone.')) {
return;
}
try {
await membershipService.deleteTier(tierId);
loadTiers();
} catch (error: any) {
alert(error.response?.data?.detail || 'Failed to delete tier');
}
};
return (
<div className="modal-overlay">
<div className="modal-content" style={{ maxWidth: '900px', width: '90%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h3 style={{ margin: 0, color: '#333' }}>Super Admin Panel</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
color: '#666'
}}
>
×
</button>
</div>
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', gap: '10px', borderBottom: '1px solid #ddd' }}>
<button
onClick={() => setActiveTab('tiers')}
className={activeTab === 'tiers' ? 'tab-active' : 'tab-inactive'}
>
Membership Tiers
</button>
<button
onClick={() => setActiveTab('users')}
className={activeTab === 'users' ? 'tab-active' : 'tab-inactive'}
>
User Management
</button>
<button
onClick={() => setActiveTab('system')}
className={activeTab === 'system' ? 'tab-active' : 'tab-inactive'}
>
System Settings
</button>
</div>
</div>
{activeTab === 'tiers' && (
<TierManagement
tiers={tiers}
loading={loading}
showCreateForm={showCreateForm}
editingTier={editingTier}
onCreateTier={handleCreateTier}
onUpdateTier={handleUpdateTier}
onDeleteTier={handleDeleteTier}
onShowCreateForm={() => setShowCreateForm(true)}
onHideCreateForm={() => setShowCreateForm(false)}
onEditTier={setEditingTier}
onCancelEdit={() => setEditingTier(null)}
/>
)}
{activeTab === 'users' && (
<div style={{ padding: '20px', textAlign: 'center' }} className="super-admin-placeholder">
User management features coming soon...
</div>
)}
{activeTab === 'system' && (
<div style={{ padding: '20px', textAlign: 'center' }} className="super-admin-placeholder">
System settings coming soon...
</div>
)}
</div>
</div>
);
};
interface TierManagementProps {
tiers: MembershipTier[];
loading: boolean;
showCreateForm: boolean;
editingTier: MembershipTier | null;
onCreateTier: (data: MembershipTierCreateData) => void;
onUpdateTier: (tierId: number, data: MembershipTierUpdateData) => void;
onDeleteTier: (tierId: number) => void;
onShowCreateForm: () => void;
onHideCreateForm: () => void;
onEditTier: (tier: MembershipTier) => void;
onCancelEdit: () => void;
}
const TierManagement: React.FC<TierManagementProps> = ({
tiers,
loading,
showCreateForm,
editingTier,
onCreateTier,
onUpdateTier,
onDeleteTier,
onShowCreateForm,
onHideCreateForm,
onEditTier,
onCancelEdit
}) => {
if (loading) {
return <div style={{ padding: '20px', textAlign: 'center' }} className="super-admin-loading">Loading tiers...</div>;
}
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h4 style={{ margin: 0, color: '#333' }}>Membership Tiers Management</h4>
<button
onClick={onShowCreateForm}
className="btn btn-primary"
style={{ fontSize: '14px', padding: '8px 16px' }}
>
Create New Tier
</button>
</div>
{showCreateForm && (
<TierForm
onSubmit={onCreateTier}
onCancel={onHideCreateForm}
title="Create New Membership Tier"
/>
)}
{editingTier && (
<TierForm
initialData={editingTier}
onSubmit={(data) => onUpdateTier(editingTier.id, data)}
onCancel={onCancelEdit}
title="Edit Membership Tier"
/>
)}
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '20px' }} className="super-admin-table">
<thead>
<tr style={{ borderBottom: '2px solid #ddd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Name</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Description</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Annual Fee</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Benefits</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Status</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Actions</th>
</tr>
</thead>
<tbody>
{tiers.map(tier => (
<tr key={tier.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px', fontWeight: 'bold' }}>{tier.name}</td>
<td style={{ padding: '12px', maxWidth: '200px' }}>
{tier.description || 'No description'}
</td>
<td style={{ padding: '12px' }}>£{tier.annual_fee.toFixed(2)}</td>
<td style={{ padding: '12px', maxWidth: '250px' }}>
{tier.benefits || 'No benefits listed'}
</td>
<td style={{ padding: '12px' }}>
<span className={`status-badge ${tier.is_active ? 'status-active' : 'status-expired'}`}>
{tier.is_active ? 'ACTIVE' : 'INACTIVE'}
</span>
</td>
<td style={{ padding: '12px' }}>
<button
onClick={() => onEditTier(tier)}
className="action-btn"
style={{ marginRight: '8px', color: 'white', backgroundColor: '#007bff', border: '1px solid #007bff' }}
>
Edit
</button>
<button
onClick={() => onDeleteTier(tier.id)}
className="action-btn action-btn-danger"
style={{ color: 'white', backgroundColor: '#dc3545', border: '1px solid #dc3545' }}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
interface TierFormProps {
initialData?: MembershipTier;
onSubmit: (data: MembershipTierCreateData | MembershipTierUpdateData) => void;
onCancel: () => void;
title: string;
}
const TierForm: React.FC<TierFormProps> = ({ initialData, onSubmit, onCancel, title }) => {
const [formData, setFormData] = useState({
name: initialData?.name || '',
description: initialData?.description || '',
annual_fee: initialData?.annual_fee || 0,
benefits: initialData?.benefits || '',
is_active: initialData?.is_active ?? true
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
const handleChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<div style={{
background: '#f8f9fa',
padding: '20px',
borderRadius: '8px',
marginBottom: '20px',
border: '1px solid #dee2e6'
}}>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>{title}</h4>
<form onSubmit={handleSubmit}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div className="modal-form-group">
<label>Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
</div>
<div className="modal-form-group">
<label>Annual Fee (£) *</label>
<input
type="number"
step="0.01"
min="0"
value={formData.annual_fee}
onChange={(e) => handleChange('annual_fee', parseFloat(e.target.value) || 0)}
required
/>
</div>
</div>
<div className="modal-form-group" style={{ marginBottom: '16px' }}>
<label>Description</label>
<input
type="text"
value={formData.description}
onChange={(e) => handleChange('description', e.target.value)}
placeholder="Optional description"
/>
</div>
<div className="modal-form-group" style={{ marginBottom: '16px' }}>
<label>Benefits</label>
<textarea
value={formData.benefits}
onChange={(e) => handleChange('benefits', e.target.value)}
placeholder="List the benefits of this membership tier"
rows={3}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '14px',
color: '#333',
backgroundColor: '#fff',
resize: 'vertical'
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => handleChange('is_active', e.target.checked)}
/>
Active (visible to users)
</label>
</div>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button
type="button"
onClick={onCancel}
className="modal-btn-cancel"
>
Cancel
</button>
<button
type="submit"
className="modal-btn-primary"
>
{initialData ? 'Update Tier' : 'Create Tier'}
</button>
</div>
</form>
</div>
);
};
export default SuperAdminMenu;

View File

@@ -132,7 +132,7 @@ const Dashboard: React.FC = () => {
<>
<nav className="navbar">
<h1>SASA Membership Portal</h1>
<ProfileMenu userName={`${user?.first_name} ${user?.last_name}`} />
<ProfileMenu userName={`${user?.first_name} ${user?.last_name}`} userRole={user?.role || ''} />
</nav>
<div className="container">
<MembershipSetup
@@ -150,7 +150,7 @@ const Dashboard: React.FC = () => {
<>
<nav className="navbar">
<h1>SASA Membership Portal</h1>
<ProfileMenu userName={`${user?.first_name} ${user?.last_name}`} />
<ProfileMenu userName={`${user?.first_name} ${user?.last_name}`} userRole={user?.role || ''} />
</nav>
<div className="container">

View File

@@ -97,11 +97,19 @@ export interface PaymentUpdateData {
notes?: string;
}
export interface MembershipUpdateData {
tier_id?: number;
status?: string;
end_date?: string;
auto_renew?: boolean;
export interface MembershipTierCreateData {
name: string;
description?: string;
annual_fee: number;
benefits?: string;
}
export interface MembershipTierUpdateData {
name?: string;
description?: string;
annual_fee?: number;
benefits?: string;
is_active?: boolean;
}
export const authService = {
@@ -186,6 +194,26 @@ export const membershipService = {
async getTiers(): Promise<MembershipTier[]> {
const response = await api.get('/tiers/');
return response.data;
},
async createTier(data: MembershipTierCreateData): Promise<MembershipTier> {
const response = await api.post('/tiers/', data);
return response.data;
},
async updateTier(tierId: number, data: MembershipTierUpdateData): Promise<MembershipTier> {
const response = await api.put(`/tiers/${tierId}`, data);
return response.data;
},
async deleteTier(tierId: number): Promise<{ message: string }> {
const response = await api.delete(`/tiers/${tierId}`);
return response.data;
},
async getAllTiers(showInactive: boolean = true): Promise<MembershipTier[]> {
const response = await api.get(`/tiers/?show_inactive=${showInactive}`);
return response.data;
}
};