User search and edit
This commit is contained in:
@@ -16,6 +16,11 @@ const Dashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showMembershipSetup, setShowMembershipSetup] = useState(false);
|
||||
const [showProfileEdit, setShowProfileEdit] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [showUserDetails, setShowUserDetails] = useState(false);
|
||||
const [isEditingUser, setIsEditingUser] = useState(false);
|
||||
const [editFormData, setEditFormData] = useState<Partial<User>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!authService.isAuthenticated()) {
|
||||
@@ -130,6 +135,76 @@ const Dashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = allUsers.filter(user => {
|
||||
const fullName = `${user.first_name} ${user.last_name}`.toLowerCase();
|
||||
const email = user.email.toLowerCase();
|
||||
const search = searchTerm.toLowerCase();
|
||||
return fullName.includes(search) || email.includes(search);
|
||||
});
|
||||
|
||||
const handleUserClick = (user: User) => {
|
||||
setSelectedUser(user);
|
||||
setEditFormData({
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
email: user.email,
|
||||
phone: user.phone || '',
|
||||
address: user.address || ''
|
||||
});
|
||||
setShowUserDetails(true);
|
||||
setIsEditingUser(false);
|
||||
};
|
||||
|
||||
const handleCloseUserDetails = () => {
|
||||
setSelectedUser(null);
|
||||
setShowUserDetails(false);
|
||||
setIsEditingUser(false);
|
||||
setEditFormData({});
|
||||
};
|
||||
|
||||
const handleEditUser = () => {
|
||||
setIsEditingUser(true);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditingUser(false);
|
||||
if (selectedUser) {
|
||||
setEditFormData({
|
||||
first_name: selectedUser.first_name,
|
||||
last_name: selectedUser.last_name,
|
||||
email: selectedUser.email,
|
||||
phone: selectedUser.phone || '',
|
||||
address: selectedUser.address || ''
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveUser = async () => {
|
||||
if (!selectedUser) return;
|
||||
|
||||
try {
|
||||
await userService.updateUser(selectedUser.id, editFormData);
|
||||
// Refresh data
|
||||
await loadData();
|
||||
setIsEditingUser(false);
|
||||
// Update selected user with new data
|
||||
const updatedUser = allUsers.find(u => u.id === selectedUser.id);
|
||||
if (updatedUser) {
|
||||
setSelectedUser(updatedUser);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update user:', error);
|
||||
alert('Failed to update user. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormChange = (field: keyof User, value: string) => {
|
||||
setEditFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
@@ -345,6 +420,23 @@ const Dashboard: React.FC = () => {
|
||||
<div className="card" style={{ marginTop: '20px' }}>
|
||||
<h3 style={{ marginBottom: '16px' }}>User Management</h3>
|
||||
|
||||
{/* Search Input */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search users by name or email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||||
@@ -357,8 +449,12 @@ const Dashboard: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allUsers.map(u => (
|
||||
<tr key={u.id} style={{ borderBottom: '1px solid #eee' }}>
|
||||
{filteredUsers.map(u => (
|
||||
<tr
|
||||
key={u.id}
|
||||
style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }}
|
||||
onClick={() => handleUserClick(u)}
|
||||
>
|
||||
<td style={{ padding: '12px' }}>{u.first_name} {u.last_name}</td>
|
||||
<td style={{ padding: '12px' }}>{u.email}</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
@@ -384,7 +480,10 @@ const Dashboard: React.FC = () => {
|
||||
{u.role === 'member' && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => handleUpdateUserRole(u.id, 'admin')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent row click
|
||||
handleUpdateUserRole(u.id, 'admin');
|
||||
}}
|
||||
style={{ fontSize: '12px', padding: '4px 8px', marginRight: '4px' }}
|
||||
>
|
||||
Make Admin
|
||||
@@ -393,7 +492,10 @@ const Dashboard: React.FC = () => {
|
||||
{u.role === 'admin' && u.id !== user?.id && (
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => handleUpdateUserRole(u.id, 'member')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent row click
|
||||
handleUpdateUserRole(u.id, 'member');
|
||||
}}
|
||||
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||
>
|
||||
Remove Admin
|
||||
@@ -418,6 +520,251 @@ const Dashboard: React.FC = () => {
|
||||
onCancel={handleProfileCancel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User Details Modal */}
|
||||
{showUserDetails && selectedUser && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
width: '90%'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: 0 }}>User Details</h3>
|
||||
<div>
|
||||
{!isEditingUser && (user?.role === 'admin' || user?.role === 'super_admin') && (
|
||||
<button
|
||||
onClick={handleEditUser}
|
||||
style={{
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '8px'
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCloseUserDetails}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Profile */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4>Profile Information</h4>
|
||||
{isEditingUser ? (
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||||
<label style={{ fontWeight: 'bold' }}>First Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editFormData.first_name || ''}
|
||||
onChange={(e) => handleFormChange('first_name', e.target.value)}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||||
<label style={{ fontWeight: 'bold' }}>Last Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editFormData.last_name || ''}
|
||||
onChange={(e) => handleFormChange('last_name', e.target.value)}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||||
<label style={{ fontWeight: 'bold' }}>Email:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={editFormData.email || ''}
|
||||
onChange={(e) => handleFormChange('email', e.target.value)}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||||
<label style={{ fontWeight: 'bold' }}>Phone:</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={editFormData.phone || ''}
|
||||
onChange={(e) => handleFormChange('phone', e.target.value)}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px', alignItems: 'center' }}>
|
||||
<label style={{ fontWeight: 'bold' }}>Address:</label>
|
||||
<textarea
|
||||
value={editFormData.address || ''}
|
||||
onChange={(e) => handleFormChange('address', e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
||||
<button
|
||||
onClick={handleSaveUser}
|
||||
style={{
|
||||
backgroundColor: '#28a745',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
style={{
|
||||
backgroundColor: '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p><strong>Name:</strong> {selectedUser.first_name} {selectedUser.last_name}</p>
|
||||
<p><strong>Email:</strong> {selectedUser.email}</p>
|
||||
{selectedUser.phone && <p><strong>Phone:</strong> {selectedUser.phone}</p>}
|
||||
{selectedUser.address && <p><strong>Address:</strong> {selectedUser.address}</p>}
|
||||
<p><strong>Role:</strong> {selectedUser.role.toUpperCase()}</p>
|
||||
<p><strong>Status:</strong> {selectedUser.is_active ? 'Active' : 'Inactive'}</p>
|
||||
<p><strong>Joined:</strong> {formatDate(selectedUser.created_at)}</p>
|
||||
{selectedUser.last_login && <p><strong>Last Login:</strong> {formatDate(selectedUser.last_login)}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Memberships */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4>Memberships</h4>
|
||||
{allMemberships.filter(m => m.user_id === selectedUser.id).length > 0 ? (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #ddd' }}>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Tier</th>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Status</th>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Start Date</th>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>End Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allMemberships.filter(m => m.user_id === selectedUser.id).map(membership => (
|
||||
<tr key={membership.id} style={{ borderBottom: '1px solid #eee' }}>
|
||||
<td style={{ padding: '8px' }}>{membership.tier.name}</td>
|
||||
<td style={{ padding: '8px' }}>
|
||||
<span className={`status-badge ${getStatusClass(membership.status)}`}>
|
||||
{membership.status.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '8px' }}>{formatDate(membership.start_date)}</td>
|
||||
<td style={{ padding: '8px' }}>{formatDate(membership.end_date)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p style={{ color: '#666' }}>No memberships found.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Payments */}
|
||||
<div>
|
||||
<h4>Payment History</h4>
|
||||
{allPayments.filter(p => p.user_id === selectedUser.id).length > 0 ? (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #ddd' }}>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Date</th>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Amount</th>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Method</th>
|
||||
<th style={{ padding: '8px', textAlign: 'left' }}>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allPayments.filter(p => p.user_id === selectedUser.id).map(payment => (
|
||||
<tr key={payment.id} style={{ borderBottom: '1px solid #eee' }}>
|
||||
<td style={{ padding: '8px' }}>{payment.payment_date ? formatDate(payment.payment_date) : 'Pending'}</td>
|
||||
<td style={{ padding: '8px' }}>£{payment.amount.toFixed(2)}</td>
|
||||
<td style={{ padding: '8px' }}>{payment.payment_method}</td>
|
||||
<td style={{ padding: '8px' }}>
|
||||
<span className={`status-badge ${getStatusClass(payment.status)}`}>
|
||||
{payment.status.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p style={{ color: '#666' }}>No payment history found.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user