From 43b13ef52de4dc7c8203fd991910785761aa83c0 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Mon, 10 Nov 2025 15:42:09 +0000 Subject: [PATCH] Adding more shit --- backend/app/api/v1/memberships.py | 34 +- backend/app/api/v1/payments.py | 49 ++- backend/app/api/v1/tiers.py | 14 +- backend/app/services/email_service.py | 78 ++++ frontend/src/App.css | 76 ++++ frontend/src/components/MembershipSetup.tsx | 2 +- frontend/src/components/ProfileMenu.tsx | 32 +- frontend/src/components/SuperAdminMenu.tsx | 376 ++++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 4 +- frontend/src/services/membershipService.ts | 38 +- 10 files changed, 682 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/SuperAdminMenu.tsx diff --git a/backend/app/api/v1/memberships.py b/backend/app/api/v1/memberships.py index e432694..3c98100 100644 --- a/backend/app/api/v1/memberships.py +++ b/backend/app/api/v1/memberships.py @@ -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 diff --git a/backend/app/api/v1/payments.py b/backend/app/api/v1/payments.py index 4f58ace..14e899f 100644 --- a/backend/app/api/v1/payments.py +++ b/backend/app/api/v1/payments.py @@ -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 diff --git a/backend/app/api/v1/tiers.py b/backend/app/api/v1/tiers.py index eb5241a..24469ea 100644 --- a/backend/app/api/v1/tiers.py +++ b/backend/app/api/v1/tiers.py @@ -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( diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py index 488aa6e..32deb4e 100644 --- a/backend/app/services/email_service.py +++ b/backend/app/services/email_service.py @@ -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""" + + +

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

+ + + """ + + 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, diff --git a/frontend/src/App.css b/frontend/src/App.css index e35f5b1..704507d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; +} diff --git a/frontend/src/components/MembershipSetup.tsx b/frontend/src/components/MembershipSetup.tsx index b49262b..9ea7a84 100644 --- a/frontend/src/components/MembershipSetup.tsx +++ b/frontend/src/components/MembershipSetup.tsx @@ -147,7 +147,7 @@ const MembershipSetup: React.FC = ({ onMembershipCreated,
Demo Payment

- 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

diff --git a/frontend/src/components/ProfileMenu.tsx b/frontend/src/components/ProfileMenu.tsx index 77f0e14..9e189c0 100644 --- a/frontend/src/components/ProfileMenu.tsx +++ b/frontend/src/components/ProfileMenu.tsx @@ -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 = ({ userName }) => { +const ProfileMenu: React.FC = ({ userName, userRole }) => { const [isOpen, setIsOpen] = useState(false); const [showChangePassword, setShowChangePassword] = useState(false); + const [showSuperAdmin, setShowSuperAdmin] = useState(false); const menuRef = useRef(null); const navigate = useNavigate(); @@ -35,8 +38,13 @@ const ProfileMenu: React.FC = ({ 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 = ({ userName }) => { {isOpen && (
+ {userRole === 'super_admin' && ( + + )} +
+ +
+
+ + + +
+
+ + {activeTab === 'tiers' && ( + setShowCreateForm(true)} + onHideCreateForm={() => setShowCreateForm(false)} + onEditTier={setEditingTier} + onCancelEdit={() => setEditingTier(null)} + /> + )} + + {activeTab === 'users' && ( +
+ User management features coming soon... +
+ )} + + {activeTab === 'system' && ( +
+ System settings coming soon... +
+ )} + + + ); +}; + +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 = ({ + tiers, + loading, + showCreateForm, + editingTier, + onCreateTier, + onUpdateTier, + onDeleteTier, + onShowCreateForm, + onHideCreateForm, + onEditTier, + onCancelEdit +}) => { + if (loading) { + return
Loading tiers...
; + } + + return ( +
+
+

Membership Tiers Management

+ +
+ + {showCreateForm && ( + + )} + + {editingTier && ( + onUpdateTier(editingTier.id, data)} + onCancel={onCancelEdit} + title="Edit Membership Tier" + /> + )} + +
+ + + + + + + + + + + + + {tiers.map(tier => ( + + + + + + + + + ))} + +
NameDescriptionAnnual FeeBenefitsStatusActions
{tier.name} + {tier.description || 'No description'} + £{tier.annual_fee.toFixed(2)} + {tier.benefits || 'No benefits listed'} + + + {tier.is_active ? 'ACTIVE' : 'INACTIVE'} + + + + +
+
+
+ ); +}; + +interface TierFormProps { + initialData?: MembershipTier; + onSubmit: (data: MembershipTierCreateData | MembershipTierUpdateData) => void; + onCancel: () => void; + title: string; +} + +const TierForm: React.FC = ({ 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 ( +
+

{title}

+ +
+
+
+ + handleChange('name', e.target.value)} + required + /> +
+ +
+ + handleChange('annual_fee', parseFloat(e.target.value) || 0)} + required + /> +
+
+ +
+ + handleChange('description', e.target.value)} + placeholder="Optional description" + /> +
+ +
+ +