Email template management

This commit is contained in:
James Pattinson
2025-11-10 16:07:22 +00:00
parent 43b13ef52d
commit 7fd237c28b
17 changed files with 1421 additions and 259 deletions

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter
from . import auth, users, tiers, memberships, payments, email
from . import auth, users, tiers, memberships, payments, email, email_templates
api_router = APIRouter()
@@ -9,3 +9,4 @@ api_router.include_router(tiers.router, prefix="/tiers", tags=["membership-tiers
api_router.include_router(memberships.router, prefix="/memberships", tags=["memberships"])
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_templates.router, prefix="/email-templates", tags=["email-templates"])

View File

@@ -52,7 +52,8 @@ async def register(
try:
await email_service.send_welcome_email(
to_email=db_user.email,
first_name=db_user.first_name
first_name=db_user.first_name,
db=db
)
except Exception as e:
# Log error but don't fail registration
@@ -171,7 +172,8 @@ async def forgot_password(
await email_service.send_password_reset_email(
to_email=user.email,
first_name=user.first_name,
reset_token=reset_token
reset_token=reset_token,
db=db
)
except Exception as e:
# Log error but don't fail the request

View File

@@ -0,0 +1,98 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from ...core.database import get_db
from ...models.models import EmailTemplate
from ...schemas import (
EmailTemplateCreate, EmailTemplateUpdate, EmailTemplateResponse, MessageResponse
)
from ...api.dependencies import get_super_admin_user
router = APIRouter()
@router.get("/", response_model=List[EmailTemplateResponse])
async def list_email_templates(
skip: int = 0,
limit: int = 100,
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""List all email templates (super admin only)"""
templates = db.query(EmailTemplate).offset(skip).limit(limit).all()
return templates
@router.get("/{template_key}", response_model=EmailTemplateResponse)
async def get_email_template(
template_key: str,
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""Get email template by key (super admin only)"""
template = db.query(EmailTemplate).filter(EmailTemplate.template_key == template_key).first()
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Email template not found"
)
return template
@router.put("/{template_key}", response_model=EmailTemplateResponse)
async def update_email_template(
template_key: str,
template_update: EmailTemplateUpdate,
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""Update email template (super admin only)"""
template = db.query(EmailTemplate).filter(EmailTemplate.template_key == template_key).first()
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Email template not found"
)
update_data = template_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(template, field, value)
db.commit()
db.refresh(template)
return template
@router.post("/seed-defaults", response_model=MessageResponse)
async def seed_default_templates(
current_user = Depends(get_super_admin_user),
db: Session = Depends(get_db)
):
"""Seed database with default email templates (super admin only)"""
from ...services.email_service import get_default_templates
default_templates = get_default_templates()
created_count = 0
for template_data in default_templates:
# Check if template already exists
existing = db.query(EmailTemplate).filter(
EmailTemplate.template_key == template_data["template_key"]
).first()
if not existing:
template = EmailTemplate(**template_data)
db.add(template)
created_count += 1
if created_count > 0:
db.commit()
return {"message": f"Created {created_count} default email templates"}
else:
return {"message": "All default templates already exist"}

View File

@@ -145,7 +145,8 @@ async def update_membership(
annual_fee=membership.tier.annual_fee,
payment_amount=payment_amount,
payment_method=payment_method,
renewal_date=membership.end_date.strftime("%d %B %Y")
renewal_date=membership.end_date.strftime("%d %B %Y"),
db=db
)
except Exception as e:
# Log error but don't fail the membership update

View File

@@ -134,7 +134,8 @@ async def update_payment(
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")
renewal_date=payment.membership.end_date.strftime("%d %B %Y"),
db=db
)
except Exception as e:
# Log error but don't fail the payment update
@@ -219,7 +220,8 @@ async def record_manual_payment(
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")
renewal_date=membership.end_date.strftime("%d %B %Y"),
db=db
)
except Exception as e:
# Log error but don't fail the payment creation

View File

@@ -1,5 +1,5 @@
from sqlalchemy.orm import Session
from ..models.models import MembershipTier, User, UserRole
from ..models.models import MembershipTier, User, UserRole, EmailTemplate
from .security import get_password_hash
from datetime import datetime
@@ -54,3 +54,19 @@ def init_default_data(db: Session):
db.commit()
print("✓ Created default admin user (admin@swanseaairport.org / admin123)")
print(" ⚠️ Remember to change the admin password!")
# Check if email templates exist
existing_templates = db.query(EmailTemplate).count()
if existing_templates == 0:
print("Creating default email templates...")
from ..services.email_service import get_default_templates
default_templates_data = get_default_templates()
default_templates = []
for template_data in default_templates_data:
template = EmailTemplate(**template_data)
default_templates.append(template)
db.add_all(default_templates)
db.commit()
print(f"✓ Created {len(default_templates)} default email templates")

View File

@@ -273,3 +273,18 @@ class PasswordResetToken(Base):
# Relationships
user = relationship("User", backref="password_reset_tokens")
class EmailTemplate(Base):
__tablename__ = "email_templates"
id = Column(Integer, primary_key=True, index=True)
template_key = Column(String(100), unique=True, nullable=False, index=True)
name = Column(String(255), nullable=False)
subject = Column(String(255), nullable=False)
html_body = Column(Text, nullable=False)
text_body = Column(Text, nullable=True)
variables = Column(Text, nullable=True) # JSON string of available variables
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

View File

@@ -23,6 +23,10 @@ from .schemas import (
PaymentUpdate,
PaymentResponse,
MessageResponse,
EmailTemplateBase,
EmailTemplateCreate,
EmailTemplateUpdate,
EmailTemplateResponse,
)
__all__ = [
@@ -50,4 +54,8 @@ __all__ = [
"PaymentUpdate",
"PaymentResponse",
"MessageResponse",
"EmailTemplateBase",
"EmailTemplateCreate",
"EmailTemplateUpdate",
"EmailTemplateResponse",
]

View File

@@ -166,3 +166,35 @@ class PaymentResponse(BaseModel):
class MessageResponse(BaseModel):
message: str
detail: Optional[str] = None
# Email Template Schemas
class EmailTemplateBase(BaseModel):
template_key: str
name: str
subject: str
html_body: str
text_body: Optional[str] = None
variables: Optional[str] = None
class EmailTemplateCreate(EmailTemplateBase):
pass
class EmailTemplateUpdate(BaseModel):
name: Optional[str] = None
subject: Optional[str] = None
html_body: Optional[str] = None
text_body: Optional[str] = None
variables: Optional[str] = None
is_active: Optional[bool] = None
class EmailTemplateResponse(EmailTemplateBase):
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool
created_at: datetime
updated_at: datetime

View File

@@ -1,11 +1,14 @@
import httpx
from typing import List, Optional
from typing import List, Optional, Dict, Any
from datetime import datetime
from ..core.database import get_db
from ..models.models import EmailTemplate
from sqlalchemy.orm import Session
from ..core.config import settings
class EmailService:
"""Email service using SMTP2GO API"""
"""Email service using SMTP2GO API with database-stored templates"""
def __init__(self):
self.api_key = settings.SMTP2GO_API_KEY
@@ -51,14 +54,141 @@ class EmailService:
response = await client.post(self.api_url, json=payload, headers=headers)
return response.json()
async def send_welcome_email(self, to_email: str, first_name: str) -> dict:
"""Send welcome email to new user"""
subject = f"Welcome to {settings.APP_NAME}!"
async def get_template(self, template_key: str, db: Session) -> Optional[EmailTemplate]:
"""Get email template from database"""
return db.query(EmailTemplate).filter(
EmailTemplate.template_key == template_key,
EmailTemplate.is_active == True
).first()
html_body = f"""
def render_template(self, template_body: str, variables: Dict[str, Any]) -> str:
"""Render template with variables"""
result = template_body
for key, value in variables.items():
result = result.replace(f"{{{key}}}", str(value))
return result
async def send_templated_email(
self,
template_key: str,
to_email: str,
variables: Dict[str, Any],
db: Session
) -> dict:
"""Send email using database template"""
template = await self.get_template(template_key, db)
if not template:
raise ValueError(f"Email template '{template_key}' not found or inactive")
# Render subject and bodies with variables
subject = self.render_template(template.subject, variables)
html_body = self.render_template(template.html_body, variables)
text_body = None
if template.text_body:
text_body = self.render_template(template.text_body, variables)
return await self.send_email(to_email, subject, html_body, text_body)
async def send_welcome_email(self, to_email: str, first_name: str, db: Session) -> dict:
"""Send welcome email to new user"""
variables = {
"first_name": first_name,
"app_name": settings.APP_NAME
}
return await self.send_templated_email("welcome", to_email, variables, db)
async def send_payment_confirmation(
self,
to_email: str,
first_name: str,
amount: float,
payment_method: str,
membership_tier: str,
db: Session
) -> dict:
"""Send payment confirmation email"""
variables = {
"first_name": first_name,
"amount": f"£{amount:.2f}",
"payment_method": payment_method,
"membership_tier": membership_tier,
"app_name": settings.APP_NAME
}
return await self.send_templated_email("payment_confirmation", to_email, variables, db)
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,
db: Session
) -> dict:
"""Send membership activation email with payment details and renewal date"""
variables = {
"first_name": first_name,
"membership_tier": membership_tier,
"annual_fee": f"£{annual_fee:.2f}",
"payment_amount": f"£{payment_amount:.2f}",
"payment_method": payment_method,
"renewal_date": renewal_date,
"payment_date": datetime.now().strftime("%d %B %Y"),
"app_name": settings.APP_NAME
}
return await self.send_templated_email("membership_activation", to_email, variables, db)
async def send_membership_renewal_reminder(
self,
to_email: str,
first_name: str,
expiry_date: str,
membership_tier: str,
annual_fee: float,
db: Session
) -> dict:
"""Send membership renewal reminder"""
variables = {
"first_name": first_name,
"expiry_date": expiry_date,
"membership_tier": membership_tier,
"annual_fee": f"£{annual_fee:.2f}",
"app_name": settings.APP_NAME
}
return await self.send_templated_email("renewal_reminder", to_email, variables, db)
async def send_password_reset_email(
self,
to_email: str,
first_name: str,
reset_token: str,
db: Session
) -> dict:
"""Send password reset email with reset link"""
reset_url = f"{settings.FRONTEND_URL}/reset-password?token={reset_token}"
variables = {
"first_name": first_name,
"reset_url": reset_url,
"app_name": settings.APP_NAME
}
return await self.send_templated_email("password_reset", to_email, variables, db)
def get_default_templates() -> List[Dict[str, Any]]:
"""Get default email templates for seeding the database"""
return [
{
"template_key": "welcome",
"name": "Welcome Email",
"subject": "Welcome to {app_name}!",
"html_body": """
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #0066cc;">Welcome to {settings.APP_NAME}!</h2>
<h2 style="color: #0066cc;">Welcome to {app_name}!</h2>
<p>Hello {first_name},</p>
<p>Thank you for registering with us. Your account has been successfully created.</p>
<p>You can now:</p>
@@ -69,13 +199,12 @@ class EmailService:
</ul>
<p>If you have any questions, please don't hesitate to contact us.</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME}</strong></p>
<strong>{app_name}</strong></p>
</body>
</html>
"""
text_body = f"""
Welcome to {settings.APP_NAME}!
""".strip(),
"text_body": """
Welcome to {app_name}!
Hello {first_name},
@@ -89,107 +218,89 @@ class EmailService:
If you have any questions, please don't hesitate to contact us.
Best regards,
{settings.APP_NAME}
"""
return await self.send_email(to_email, subject, html_body, text_body)
async def send_payment_confirmation(
self,
to_email: str,
first_name: str,
amount: float,
payment_method: str,
membership_tier: str
) -> dict:
"""Send payment confirmation email"""
subject = "Payment Confirmation"
html_body = f"""
{app_name}
""".strip(),
"variables": '["first_name", "app_name"]',
"is_active": True
},
{
"template_key": "payment_confirmation",
"name": "Payment Confirmation",
"subject": "Payment Confirmation",
"html_body": """
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #0066cc;">Payment Confirmed!</h2>
<p>Hello {first_name},</p>
<p>We have received your payment. Thank you!</p>
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="margin: 5px 0;"><strong>Amount:</strong> £{amount:.2f}</p>
<p style="margin: 5px 0;"><strong>Amount:</strong> {amount}</p>
<p style="margin: 5px 0;"><strong>Payment Method:</strong> {payment_method}</p>
<p style="margin: 5px 0;"><strong>Membership Tier:</strong> {membership_tier}</p>
</div>
<p>Your membership is now active. You can access all the benefits associated with your tier.</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME}</strong></p>
<strong>{app_name}</strong></p>
</body>
</html>
"""
text_body = f"""
""".strip(),
"text_body": """
Payment Confirmed!
Hello {first_name},
We have received your payment. Thank you!
Amount: £{amount:.2f}
Amount: {amount}
Payment Method: {payment_method}
Membership Tier: {membership_tier}
Your membership is now active. You can access all the benefits associated with your tier.
Best regards,
{settings.APP_NAME}
"""
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"""
{app_name}
""".strip(),
"variables": '["first_name", "amount", "payment_method", "membership_tier", "app_name"]',
"is_active": True
},
{
"template_key": "membership_activation",
"name": "Membership Activation",
"subject": "Your {app_name} Membership is Now Active!",
"html_body": """
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #28a745;">Welcome to {settings.APP_NAME}!</h2>
<h2 style="color: #28a745;">Welcome to {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>Annual Fee:</strong> {annual_fee}</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>Amount Paid:</strong> {payment_amount}</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>
<p style="margin: 8px 0;"><strong>Payment Date:</strong> {payment_date}</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>Welcome to the {app_name} community!</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME} Team</strong></p>
<strong>{app_name} Team</strong></p>
</body>
</html>
"""
text_body = f"""
Welcome to {settings.APP_NAME}!
""".strip(),
"text_body": """
Welcome to {app_name}!
Hello {first_name},
@@ -198,39 +309,32 @@ class EmailService:
MEMBERSHIP DETAILS
------------------
Membership Tier: {membership_tier}
Annual Fee: £{annual_fee:.2f}
Annual Fee: {annual_fee}
Next Renewal Date: {renewal_date}
PAYMENT INFORMATION
-------------------
Amount Paid: £{payment_amount:.2f}
Amount Paid: {payment_amount}
Payment Method: {payment_method}
Payment Date: {datetime.now().strftime('%d %B %Y')}
Payment Date: {payment_date}
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!
Welcome to the {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,
first_name: str,
expiry_date: str,
membership_tier: str,
annual_fee: float
) -> dict:
"""Send membership renewal reminder"""
subject = "Membership Renewal Reminder"
html_body = f"""
{app_name} Team
""".strip(),
"variables": '["first_name", "membership_tier", "annual_fee", "payment_amount", "payment_method", "renewal_date", "payment_date", "app_name"]',
"is_active": True
},
{
"template_key": "renewal_reminder",
"name": "Membership Renewal Reminder",
"subject": "Membership Renewal Reminder",
"html_body": """
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #0066cc;">Membership Renewal Reminder</h2>
@@ -239,17 +343,16 @@ class EmailService:
<p>To continue enjoying your membership benefits, please renew your membership.</p>
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p style="margin: 5px 0;"><strong>Membership Tier:</strong> {membership_tier}</p>
<p style="margin: 5px 0;"><strong>Annual Fee:</strong> £{annual_fee:.2f}</p>
<p style="margin: 5px 0;"><strong>Annual Fee:</strong> {annual_fee}</p>
<p style="margin: 5px 0;"><strong>Expires:</strong> {expiry_date}</p>
</div>
<p>Please log in to your account to renew your membership.</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME}</strong></p>
<strong>{app_name}</strong></p>
</body>
</html>
"""
text_body = f"""
""".strip(),
"text_body": """
Membership Renewal Reminder
Hello {first_name},
@@ -259,34 +362,27 @@ class EmailService:
To continue enjoying your membership benefits, please renew your membership.
Membership Tier: {membership_tier}
Annual Fee: £{annual_fee:.2f}
Annual Fee: {annual_fee}
Expires: {expiry_date}
Please log in to your account to renew your membership.
Best regards,
{settings.APP_NAME}
"""
return await self.send_email(to_email, subject, html_body, text_body)
async def send_password_reset_email(
self,
to_email: str,
first_name: str,
reset_token: str
) -> dict:
"""Send password reset email with reset link"""
subject = f"Password Reset - {settings.APP_NAME}"
reset_url = f"{settings.FRONTEND_URL}/reset-password?token={reset_token}"
html_body = f"""
{app_name}
""".strip(),
"variables": '["first_name", "expiry_date", "membership_tier", "annual_fee", "app_name"]',
"is_active": True
},
{
"template_key": "password_reset",
"name": "Password Reset",
"subject": "Password Reset - {app_name}",
"html_body": """
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2 style="color: #0066cc;">Password Reset Request</h2>
<p>Hello {first_name},</p>
<p>You have requested to reset your password for your {settings.APP_NAME} account.</p>
<p>You have requested to reset your password for your {app_name} account.</p>
<p>Please click the button below to reset your password:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{reset_url}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
@@ -297,17 +393,16 @@ class EmailService:
<p>If you didn't request this password reset, please ignore this email. Your password will remain unchanged.</p>
<p>For security reasons, please don't share this email with anyone.</p>
<p>Best regards,<br>
<strong>{settings.APP_NAME}</strong></p>
<strong>{app_name}</strong></p>
</body>
</html>
"""
text_body = f"""
""".strip(),
"text_body": """
Password Reset Request
Hello {first_name},
You have requested to reset your password for your {settings.APP_NAME} account.
You have requested to reset your password for your {app_name} account.
Please use this link to reset your password: {reset_url}
@@ -318,10 +413,12 @@ class EmailService:
For security reasons, please don't share this email with anyone.
Best regards,
{settings.APP_NAME}
"""
return await self.send_email(to_email, subject, html_body, text_body)
{app_name}
""".strip(),
"variables": '["first_name", "reset_url", "app_name"]',
"is_active": True
}
]
# Create a singleton instance

View File

@@ -5,6 +5,8 @@ import Login from './pages/Login';
import ForgotPassword from './pages/ForgotPassword';
import ResetPassword from './pages/ResetPassword';
import Dashboard from './pages/Dashboard';
import EmailTemplates from './pages/EmailTemplates';
import MembershipTiers from './pages/MembershipTiers';
import './App.css';
const App: React.FC = () => {
@@ -17,6 +19,8 @@ const App: React.FC = () => {
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/email-templates" element={<EmailTemplates />} />
<Route path="/membership-tiers" element={<MembershipTiers />} />
</Routes>
</BrowserRouter>
);

View File

@@ -0,0 +1,401 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface EmailTemplate {
template_key: string;
name: string;
subject: string;
html_body: string;
text_body: string;
variables: string; // This comes as JSON string from backend
is_active: boolean;
}
const EmailTemplateManagement: React.FC = () => {
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
const [showEditForm, setShowEditForm] = useState(false);
useEffect(() => {
fetchTemplates();
}, []);
const fetchTemplates = async () => {
try {
const token = localStorage.getItem('token');
const response = await axios.get('/api/v1/email-templates/', {
headers: { Authorization: `Bearer ${token}` }
});
setTemplates(response.data);
} catch (error) {
console.error('Error fetching email templates:', error);
} finally {
setLoading(false);
}
};
const handleEditTemplate = (template: EmailTemplate) => {
setEditingTemplate(template);
setShowEditForm(true);
};
const handleSaveTemplate = async (updatedTemplate: EmailTemplate) => {
try {
const token = localStorage.getItem('token');
await axios.put(`/api/v1/email-templates/${updatedTemplate.template_key}`, updatedTemplate, {
headers: { Authorization: `Bearer ${token}` }
});
setShowEditForm(false);
setEditingTemplate(null);
fetchTemplates(); // Refresh the list
} catch (error) {
console.error('Error updating email template:', error);
}
};
const handleCancelEdit = () => {
setShowEditForm(false);
setEditingTemplate(null);
};
if (loading) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading email templates...</div>;
}
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<button
onClick={fetchTemplates}
style={{
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Refresh Templates
</button>
</div>
<div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))' }}>
{templates.map((template) => (
<div
key={template.template_key}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, color: '#333', fontSize: '18px' }}>{template.name}</h3>
<div>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold',
backgroundColor: template.is_active ? '#d4edda' : '#f8d7da',
color: template.is_active ? '#155724' : '#721c24'
}}>
{template.is_active ? 'Active' : 'Inactive'}
</span>
<button
onClick={() => handleEditTemplate(template)}
style={{
marginLeft: '10px',
padding: '6px 12px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Edit
</button>
</div>
</div>
<div style={{ marginBottom: '10px' }}>
<strong style={{ color: '#666' }}>Key:</strong> <code style={{ backgroundColor: '#f8f9fa', padding: '2px 4px', borderRadius: '3px' }}>{template.template_key}</code>
</div>
<div style={{ marginBottom: '10px' }}>
<strong style={{ color: '#666' }}>Subject:</strong> <span style={{ color: '#333' }}>{template.subject}</span>
</div>
<div style={{ marginBottom: '15px' }}>
<strong style={{ color: '#666' }}>Variables:</strong>
<div style={{ marginTop: '5px', fontFamily: 'monospace', fontSize: '14px', color: '#333' }}>
{(() => {
try {
const vars = JSON.parse(template.variables);
return Array.isArray(vars) ? vars.join(', ') : template.variables;
} catch {
return template.variables;
}
})()}
</div>
</div>
<div>
<strong style={{ color: '#666' }}>HTML Body Preview:</strong>
<div
style={{
marginTop: '8px',
padding: '12px',
backgroundColor: '#f8f9fa',
border: '1px solid #e9ecef',
borderRadius: '4px',
maxHeight: '200px',
overflow: 'auto',
fontSize: '13px',
lineHeight: '1.4',
color: '#333'
}}
dangerouslySetInnerHTML={{ __html: template.html_body.substring(0, 300) + '...' }}
/>
</div>
</div>
))}
</div>
{showEditForm && editingTemplate && (
<EmailTemplateEditForm
template={editingTemplate}
onSave={handleSaveTemplate}
onCancel={handleCancelEdit}
/>
)}
</div>
);
};
interface EmailTemplateEditFormProps {
template: EmailTemplate;
onSave: (template: EmailTemplate) => void;
onCancel: () => void;
}
const EmailTemplateEditForm: React.FC<EmailTemplateEditFormProps> = ({ template, onSave, onCancel }) => {
const [formData, setFormData] = useState({
name: template.name,
subject: template.subject,
html_body: template.html_body,
text_body: template.text_body,
variables: (() => {
try {
const vars = JSON.parse(template.variables);
return Array.isArray(vars) ? vars : [];
} catch {
return [];
}
})(),
is_active: template.is_active
});
const handleChange = (field: keyof EmailTemplate, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const dataToSave = {
template_key: template.template_key,
...formData,
variables: JSON.stringify(formData.variables)
};
onSave(dataToSave);
}; return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '90%',
maxWidth: '800px',
maxHeight: '90vh',
overflow: 'auto'
}}>
<h3 style={{ marginTop: 0, marginBottom: '20px' }}>Edit Email Template: {template.name}</h3>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Template Key:
</label>
<input
type="text"
value={template.template_key}
disabled
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f5f5f5'
}}
/>
<small style={{ color: '#666' }}>Template key cannot be changed</small>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Name:
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Subject:
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => handleChange('subject', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Variables (comma-separated):
</label>
<input
type="text"
value={formData.variables.join(', ')}
onChange={(e) => handleChange('variables', e.target.value.split(',').map(v => v.trim()))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
HTML Body:
</label>
<textarea
value={formData.html_body}
onChange={(e) => handleChange('html_body', e.target.value)}
rows={15}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '14px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Text Body:
</label>
<textarea
value={formData.text_body}
onChange={(e) => handleChange('text_body', e.target.value)}
rows={10}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '14px'
}}
required
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => handleChange('is_active', e.target.checked)}
style={{ marginRight: '8px' }}
/>
Active
</label>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
<button
type="button"
onClick={onCancel}
style={{
padding: '8px 16px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
type="submit"
style={{
padding: '8px 16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Save Changes
</button>
</div>
</form>
</div>
</div>
);
};
export default EmailTemplateManagement;

View File

@@ -1,7 +1,6 @@
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;
@@ -11,7 +10,6 @@ interface ProfileMenuProps {
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();
@@ -38,15 +36,6 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
setIsOpen(false);
};
const handleSuperAdmin = () => {
setShowSuperAdmin(true);
setIsOpen(false);
};
const handleCloseSuperAdmin = () => {
setShowSuperAdmin(false);
};
const dropdownStyle: React.CSSProperties = {
position: 'absolute',
top: '100%',
@@ -94,17 +83,31 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
{isOpen && (
<div style={dropdownStyle}>
{userRole === 'super_admin' && (
<>
<button
style={{ ...menuItemStyle, borderRadius: '4px 4px 0 0' }}
onClick={handleSuperAdmin}
onClick={() => {
navigate('/membership-tiers');
setIsOpen(false);
}}
>
Super Admin Panel
Membership Tiers
</button>
<button
style={{ ...menuItemStyle, borderTop: '1px solid #eee', borderRadius: '0' }}
onClick={() => {
navigate('/email-templates');
setIsOpen(false);
}}
>
Email Templates
</button>
</>
)}
<button
style={{
...menuItemStyle,
borderRadius: userRole === 'super_admin' ? '0' : '4px 4px 0 0',
borderRadius: userRole === 'super_admin' ? '0 0 4px 4px' : '4px 4px 0 0',
borderTop: userRole === 'super_admin' ? '1px solid #eee' : 'none'
}}
onClick={handleChangePassword}
@@ -122,11 +125,7 @@ const ProfileMenu: React.FC<ProfileMenuProps> = ({ userName, userRole }) => {
</div>
{showChangePassword && (
<ChangePasswordModal onClose={handleCloseChangePassword} />
)}
{showSuperAdmin && (
<SuperAdminMenu onClose={handleCloseSuperAdmin} />
<ChangePasswordModal onClose={() => setShowChangePassword(false)} />
)}
</>
);

View File

@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react';
import { membershipService, MembershipTier, MembershipTierCreateData, MembershipTierUpdateData } from '../services/membershipService';
import EmailTemplateManagement from './EmailTemplateManagement';
interface SuperAdminMenuProps {
onClose: () => void;
}
const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
const [activeTab, setActiveTab] = useState<'tiers' | 'users' | 'system'>('tiers');
const [activeTab, setActiveTab] = useState<'tiers' | 'users' | 'email' | 'system'>('tiers');
const [tiers, setTiers] = useState<MembershipTier[]>([]);
const [loading, setLoading] = useState(false);
const [showCreateForm, setShowCreateForm] = useState(false);
@@ -31,9 +32,9 @@ const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
}
};
const handleCreateTier = async (data: MembershipTierCreateData) => {
const handleCreateTier = async (data: MembershipTierCreateData | MembershipTierUpdateData) => {
try {
await membershipService.createTier(data);
await membershipService.createTier(data as MembershipTierCreateData);
setShowCreateForm(false);
loadTiers();
} catch (error: any) {
@@ -97,6 +98,12 @@ const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
>
User Management
</button>
<button
onClick={() => setActiveTab('email')}
className={activeTab === 'email' ? 'tab-active' : 'tab-inactive'}
>
Email Templates
</button>
<button
onClick={() => setActiveTab('system')}
className={activeTab === 'system' ? 'tab-active' : 'tab-inactive'}
@@ -128,6 +135,10 @@ const SuperAdminMenu: React.FC<SuperAdminMenuProps> = ({ onClose }) => {
</div>
)}
{activeTab === 'email' && (
<EmailTemplateManagement />
)}
{activeTab === 'system' && (
<div style={{ padding: '20px', textAlign: 'center' }} className="super-admin-placeholder">
System settings coming soon...
@@ -143,7 +154,7 @@ interface TierManagementProps {
loading: boolean;
showCreateForm: boolean;
editingTier: MembershipTier | null;
onCreateTier: (data: MembershipTierCreateData) => void;
onCreateTier: (data: MembershipTierCreateData | MembershipTierUpdateData) => void;
onUpdateTier: (tierId: number, data: MembershipTierUpdateData) => void;
onDeleteTier: (tierId: number) => void;
onShowCreateForm: () => void;

View File

@@ -0,0 +1,110 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import EmailTemplateManagement from '../components/EmailTemplateManagement';
const EmailTemplates: React.FC = () => {
const navigate = useNavigate();
const [isSuperAdmin, setIsSuperAdmin] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkSuperAdminAccess();
}, []);
const checkSuperAdminAccess = async () => {
try {
const token = localStorage.getItem('token');
if (!token) {
navigate('/login');
return;
}
const response = await axios.get('/api/v1/users/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.data.role !== 'super_admin') {
navigate('/dashboard');
return;
}
setIsSuperAdmin(true);
} catch (error) {
console.error('Error checking user role:', error);
navigate('/login');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
backgroundColor: '#f8f9fa'
}}>
<div>Loading...</div>
</div>
);
}
if (!isSuperAdmin) {
return null; // Will redirect
}
return (
<div style={{
minHeight: '100vh',
backgroundColor: '#f8f9fa',
padding: '20px'
}}>
<div style={{
maxWidth: '1200px',
margin: '0 auto',
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
overflow: 'hidden'
}}>
<div style={{
backgroundColor: '#007bff',
color: 'white',
padding: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h1 style={{ margin: 0, fontSize: '24px' }}>Email Template Management</h1>
<p style={{ margin: '5px 0 0 0', opacity: 0.9 }}>
Manage email templates for the membership system
</p>
</div>
<button
onClick={() => navigate('/dashboard')}
style={{
padding: '8px 16px',
backgroundColor: 'rgba(255,255,255,0.2)',
color: 'white',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Back to Dashboard
</button>
</div>
<div style={{ padding: '20px' }}>
<EmailTemplateManagement />
</div>
</div>
</div>
);
};
export default EmailTemplates;

View File

@@ -0,0 +1,358 @@
import React, { useState, useEffect } from 'react';
import { membershipService, MembershipTier, MembershipTierCreateData, MembershipTierUpdateData } from '../services/membershipService';
const MembershipTiers: React.FC = () => {
const [tiers, setTiers] = useState<MembershipTier[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingTier, setEditingTier] = useState<MembershipTier | null>(null);
useEffect(() => {
loadTiers();
}, []);
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 | MembershipTierUpdateData) => {
try {
await membershipService.createTier(data as MembershipTierCreateData);
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');
}
};
const handleEditTier = (tier: MembershipTier) => {
setEditingTier(tier);
};
const handleCancelEdit = () => {
setEditingTier(null);
setShowCreateForm(false);
};
if (loading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '200px',
fontSize: '16px',
color: '#666'
}}>
Loading membership tiers...
</div>
);
}
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px'
}}>
<h1 style={{ margin: 0, color: '#333' }}>Membership Tiers Management</h1>
<button
onClick={() => setShowCreateForm(true)}
style={{
padding: '10px 20px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Create New Tier
</button>
</div>
<div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
{tiers.map((tier) => (
<div
key={tier.id}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '20px',
backgroundColor: 'white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '15px' }}>
<h3 style={{ margin: 0, color: '#333', fontSize: '18px' }}>{tier.name}</h3>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => handleEditTier(tier)}
style={{
padding: '6px 12px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
Edit
</button>
<button
onClick={() => handleDeleteTier(tier.id)}
style={{
padding: '6px 12px',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
Delete
</button>
</div>
</div>
<div style={{ marginBottom: '10px' }}>
<strong style={{ color: '#666' }}>Annual Fee:</strong>
<span style={{ color: '#28a745', fontWeight: 'bold', marginLeft: '8px' }}>
£{tier.annual_fee.toFixed(2)}
</span>
</div>
<div style={{ marginBottom: '10px' }}>
<strong style={{ color: '#666' }}>Status:</strong>
<span style={{
marginLeft: '8px',
padding: '2px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold',
backgroundColor: tier.is_active ? '#d4edda' : '#f8d7da',
color: tier.is_active ? '#155724' : '#721c24'
}}>
{tier.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div>
<strong style={{ color: '#666' }}>Benefits:</strong>
<p style={{
marginTop: '8px',
color: '#555',
fontSize: '14px',
lineHeight: '1.4'
}}>
{tier.benefits}
</p>
</div>
</div>
))}
</div>
{(showCreateForm || editingTier) && (
<MembershipTierForm
tier={editingTier}
onSave={editingTier ? (data) => handleUpdateTier(editingTier.id, data) : handleCreateTier}
onCancel={handleCancelEdit}
/>
)}
</div>
);
};
interface MembershipTierFormProps {
tier: MembershipTier | null;
onSave: (data: MembershipTierCreateData | MembershipTierUpdateData) => void;
onCancel: () => void;
}
const MembershipTierForm: React.FC<MembershipTierFormProps> = ({ tier, onSave, onCancel }) => {
const [formData, setFormData] = useState({
name: tier?.name || '',
annual_fee: tier?.annual_fee || 0,
benefits: tier?.benefits || '',
is_active: tier?.is_active ?? true
});
const handleChange = (field: string, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
};
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
width: '90%',
maxWidth: '500px',
maxHeight: '90vh',
overflow: 'auto'
}}>
<h3 style={{ marginTop: 0, marginBottom: '20px' }}>
{tier ? 'Edit Membership Tier' : 'Create New Membership Tier'}
</h3>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Name:
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
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)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
required
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Benefits:
</label>
<textarea
value={formData.benefits}
onChange={(e) => handleChange('benefits', e.target.value)}
rows={4}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'inherit',
resize: 'vertical'
}}
required
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => handleChange('is_active', e.target.checked)}
style={{ marginRight: '8px' }}
/>
Active
</label>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
<button
type="button"
onClick={onCancel}
style={{
padding: '8px 16px',
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
type="submit"
style={{
padding: '8px 16px',
backgroundColor: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{tier ? 'Update Tier' : 'Create Tier'}
</button>
</div>
</form>
</div>
</div>
);
};
export default MembershipTiers;

View File

@@ -83,6 +83,13 @@ export interface MembershipCreateData {
auto_renew: boolean;
}
export interface MembershipUpdateData {
status?: string;
start_date?: string;
end_date?: string;
auto_renew?: boolean;
}
export interface PaymentCreateData {
amount: number;
payment_method: string;