Adding more shit

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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