Adding more shit
This commit is contained in:
@@ -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: '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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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