396 lines
12 KiB
TypeScript
396 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { membershipService, MembershipTier, MembershipTierCreateData, MembershipTierUpdateData } from '../services/membershipService';
|
|
|
|
const MembershipTiers: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
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={{
|
|
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' }}>Membership Tiers Management</h1>
|
|
<p style={{ margin: '5px 0 0 0', opacity: 0.9 }}>
|
|
Manage membership tiers and pricing
|
|
</p>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '10px' }}>
|
|
<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>
|
|
<button
|
|
onClick={() => setShowCreateForm(true)}
|
|
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'
|
|
}}
|
|
>
|
|
Create New Tier
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ padding: '20px' }}>
|
|
<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>
|
|
</div>
|
|
</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; |