Adding more shit
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
376
frontend/src/components/SuperAdminMenu.tsx
Normal file
376
frontend/src/components/SuperAdminMenu.tsx
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user