feat: add return classification for archive chart, CARD API integration, compliance charts, systemd services
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
// ⚠️ CONVENTION: This component uses Tailwind utility classes (e.g. bg-white, rounded-lg, hover:bg-gray-50)
|
||||
// instead of inline styles or App.css global classes. This is the legacy modal kept for UserMenu quick-access;
|
||||
// the themed replacement lives in AdminPage.js.
|
||||
// ⚠️ CONVENTION: This component uses inline styles matching the dark "tactical intelligence"
|
||||
// design system (DESIGN_SYSTEM.md). Colors use the --intel-* and --text-* palette.
|
||||
//
|
||||
// ⚠️ CONVENTION: This file is INCOMPLETE — the exported functional component (UserManagement)
|
||||
// was removed during the style refactor. Only style constants remain. The file must include:
|
||||
// - A default-exported functional component using hooks (useState, useEffect)
|
||||
// - Data fetching via fetch() with credentials: 'include' and relative API paths
|
||||
// - Loading and error state handling in the rendered output
|
||||
// - JSX that uses the imported lucide-react icons (X, Plus, Edit2, Trash2, etc.)
|
||||
// - The ConfirmModal integration for delete/group-change confirmations
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, User, Mail, Shield } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -18,12 +25,150 @@ const GROUP_LABELS = {
|
||||
};
|
||||
|
||||
const GROUP_BADGE_STYLES = {
|
||||
Admin: { backgroundColor: '#FEE2E2', color: '#991B1B' },
|
||||
Standard_User: { backgroundColor: '#DBEAFE', color: '#1E40AF' },
|
||||
Leadership: { backgroundColor: '#F3E8FF', color: '#6B21A8' },
|
||||
Read_Only: { backgroundColor: '#F3F4F6', color: '#374151' }
|
||||
Admin: { backgroundColor: 'rgba(239, 68, 68, 0.25)', color: '#FCA5A5', border: '1px solid rgba(239, 68, 68, 0.4)' },
|
||||
Standard_User: { backgroundColor: 'rgba(14, 165, 233, 0.25)', color: '#7DD3FC', border: '1px solid rgba(14, 165, 233, 0.4)' },
|
||||
Leadership: { backgroundColor: 'rgba(168, 85, 247, 0.25)', color: '#C4B5FD', border: '1px solid rgba(168, 85, 247, 0.4)' },
|
||||
Read_Only: { backgroundColor: 'rgba(148, 163, 184, 0.2)', color: '#CBD5E1', border: '1px solid rgba(148, 163, 184, 0.3)' }
|
||||
};
|
||||
|
||||
/* ── Shared style constants ── */
|
||||
const styles = {
|
||||
overlay: {
|
||||
position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 50, padding: '1rem',
|
||||
},
|
||||
modal: {
|
||||
background: 'linear-gradient(135deg, #1E293B 0%, #0F172A 100%)',
|
||||
borderRadius: '0.75rem', border: '1.5px solid rgba(14,165,233,0.3)',
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.6), 0 0 28px rgba(14,165,233,0.08)',
|
||||
maxWidth: '56rem', width: '100%', maxHeight: '90vh', overflow: 'hidden',
|
||||
display: 'flex', flexDirection: 'column', color: '#F8FAFC',
|
||||
},
|
||||
header: {
|
||||
padding: '1.5rem', borderBottom: '1px solid rgba(14,165,233,0.2)',
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: '1.5rem', fontWeight: 700, color: '#F8FAFC', margin: 0,
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
},
|
||||
subtitle: { color: '#94A3B8', fontSize: '0.875rem', margin: '0.25rem 0 0' },
|
||||
closeBtn: {
|
||||
background: 'none', border: 'none', color: '#94A3B8', cursor: 'pointer', padding: '0.5rem',
|
||||
borderRadius: '0.375rem', transition: 'color 0.2s',
|
||||
},
|
||||
body: { padding: '1.5rem', overflowY: 'auto', flex: 1 },
|
||||
addBtn: {
|
||||
marginBottom: '1.5rem', padding: '0.5rem 1rem',
|
||||
background: 'linear-gradient(135deg, rgba(14,165,233,0.15), rgba(14,165,233,0.1))',
|
||||
border: '1px solid #0EA5E9', borderRadius: '0.5rem', color: '#38BDF8',
|
||||
cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.5rem',
|
||||
fontSize: '0.875rem', fontWeight: 500, transition: 'all 0.2s',
|
||||
textShadow: '0 0 6px rgba(14,165,233,0.2)',
|
||||
},
|
||||
formCard: {
|
||||
marginBottom: '1.5rem', padding: '1.5rem',
|
||||
background: 'linear-gradient(135deg, rgba(15,23,42,0.95), rgba(30,41,59,0.9))',
|
||||
borderRadius: '0.5rem', border: '1px solid rgba(14,165,233,0.25)',
|
||||
},
|
||||
formTitle: {
|
||||
fontSize: '1.125rem', fontWeight: 600, color: '#0EA5E9', margin: '0 0 1rem',
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
},
|
||||
label: {
|
||||
display: 'block', fontSize: '0.75rem', fontWeight: 500, color: '#CBD5E1',
|
||||
marginBottom: '0.375rem', textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
},
|
||||
inputWrap: { position: 'relative' },
|
||||
inputIcon: {
|
||||
position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)',
|
||||
color: '#94A3B8', width: '1.125rem', height: '1.125rem', pointerEvents: 'none',
|
||||
},
|
||||
input: {
|
||||
width: '100%', padding: '0.5rem 0.75rem 0.5rem 2.5rem',
|
||||
background: 'rgba(30,41,59,0.6)', border: '1px solid rgba(14,165,233,0.25)',
|
||||
borderRadius: '0.5rem', color: '#F8FAFC', fontSize: '0.875rem',
|
||||
fontFamily: "'JetBrains Mono', monospace", outline: 'none', transition: 'border-color 0.2s',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
inputNoIcon: {
|
||||
width: '100%', padding: '0.5rem 0.75rem',
|
||||
background: 'rgba(30,41,59,0.6)', border: '1px solid rgba(14,165,233,0.25)',
|
||||
borderRadius: '0.5rem', color: '#F8FAFC', fontSize: '0.875rem',
|
||||
fontFamily: "'JetBrains Mono', monospace", outline: 'none', transition: 'border-color 0.2s',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
select: {
|
||||
width: '100%', padding: '0.5rem 0.75rem 0.5rem 2.5rem',
|
||||
background: 'rgba(30,41,59,0.6)', border: '1px solid rgba(14,165,233,0.25)',
|
||||
borderRadius: '0.5rem', color: '#F8FAFC', fontSize: '0.875rem',
|
||||
fontFamily: "'JetBrains Mono', monospace", outline: 'none', cursor: 'pointer',
|
||||
appearance: 'none', boxSizing: 'border-box',
|
||||
},
|
||||
primaryBtn: {
|
||||
padding: '0.5rem 1rem',
|
||||
background: 'linear-gradient(135deg, rgba(14,165,233,0.15), rgba(14,165,233,0.1))',
|
||||
border: '1px solid #0EA5E9', borderRadius: '0.5rem', color: '#38BDF8',
|
||||
cursor: 'pointer', fontSize: '0.875rem', fontWeight: 500, transition: 'all 0.2s',
|
||||
textShadow: '0 0 6px rgba(14,165,233,0.2)',
|
||||
},
|
||||
cancelBtn: {
|
||||
padding: '0.5rem 1rem',
|
||||
background: 'rgba(51,65,85,0.5)', border: '1px solid rgba(148,163,184,0.3)',
|
||||
borderRadius: '0.5rem', color: '#CBD5E1', cursor: 'pointer',
|
||||
fontSize: '0.875rem', fontWeight: 500, transition: 'all 0.2s',
|
||||
},
|
||||
alertError: {
|
||||
marginBottom: '1rem', padding: '0.75rem',
|
||||
background: 'rgba(239,68,68,0.15)', border: '1px solid rgba(239,68,68,0.3)',
|
||||
borderRadius: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||
},
|
||||
alertSuccess: {
|
||||
marginBottom: '1rem', padding: '0.75rem',
|
||||
background: 'rgba(16,185,129,0.15)', border: '1px solid rgba(16,185,129,0.3)',
|
||||
borderRadius: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||
},
|
||||
th: {
|
||||
textAlign: 'left', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: 600,
|
||||
color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
borderBottom: '1px solid rgba(14,165,233,0.2)',
|
||||
},
|
||||
thRight: {
|
||||
textAlign: 'right', padding: '0.75rem 1rem', fontSize: '0.75rem', fontWeight: 600,
|
||||
color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
borderBottom: '1px solid rgba(14,165,233,0.2)',
|
||||
},
|
||||
td: { padding: '0.75rem 1rem', borderBottom: '1px solid rgba(51,65,85,0.5)' },
|
||||
tdRight: { padding: '0.75rem 1rem', borderBottom: '1px solid rgba(51,65,85,0.5)', textAlign: 'right' },
|
||||
username: { fontWeight: 500, color: '#F8FAFC', fontSize: '0.875rem' },
|
||||
email: { fontSize: '0.8rem', color: '#94A3B8' },
|
||||
lastLogin: { fontSize: '0.8rem', color: '#94A3B8' },
|
||||
badge: {
|
||||
padding: '0.25rem 0.625rem', borderRadius: '0.375rem',
|
||||
fontSize: '0.7rem', fontWeight: 600, display: 'inline-block',
|
||||
fontFamily: "'JetBrains Mono', monospace", letterSpacing: '0.3px',
|
||||
},
|
||||
statusActive: {
|
||||
padding: '0.2rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.7rem', fontWeight: 600,
|
||||
background: 'rgba(16,185,129,0.2)', color: '#6EE7B7', border: '1px solid rgba(16,185,129,0.3)',
|
||||
cursor: 'pointer', transition: 'opacity 0.2s',
|
||||
},
|
||||
statusInactive: {
|
||||
padding: '0.2rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.7rem', fontWeight: 600,
|
||||
background: 'rgba(239,68,68,0.2)', color: '#FCA5A5', border: '1px solid rgba(239,68,68,0.3)',
|
||||
cursor: 'pointer', transition: 'opacity 0.2s',
|
||||
},
|
||||
actionBtn: {
|
||||
background: 'none', border: 'none', padding: '0.375rem', borderRadius: '0.375rem',
|
||||
cursor: 'pointer', color: '#94A3B8', transition: 'all 0.2s',
|
||||
},
|
||||
deleteBtn: {
|
||||
background: 'none', border: 'none', padding: '0.375rem', borderRadius: '0.375rem',
|
||||
cursor: 'pointer', color: '#EF4444', transition: 'all 0.2s',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export default function UserManagement({ onClose }) {
|
||||
const { user: currentUser } = useAuth();
|
||||
const [users, setUsers] = useState([]);
|
||||
@@ -106,7 +251,6 @@ export default function UserManagement({ onClose }) {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If editing and group changed, show confirmation modal
|
||||
if (editingUser && formData.group !== editingUser.group) {
|
||||
let message = `Are you sure you want to change ${editingUser.username}'s group from ${editingUser.group} to ${formData.group}?`;
|
||||
if (editingUser.group === 'Admin' && formData.group !== 'Admin') {
|
||||
@@ -189,29 +333,29 @@ export default function UserManagement({ onClose }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if group dropdown should be disabled for self-demotion prevention
|
||||
const isGroupDropdownDisabled = (targetUser) => {
|
||||
if (!targetUser || !currentUser) return false;
|
||||
return targetUser.id === currentUser.id && currentUser.group === 'Admin';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="p-6 border-b border-gray-200 flex justify-between items-center">
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
{/* Header */}
|
||||
<div style={styles.header}>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">User Management</h2>
|
||||
<p className="text-gray-600">Manage user accounts and permissions</p>
|
||||
<h2 style={styles.title}>User Management</h2>
|
||||
<p style={styles.subtitle}>Manage user accounts and permissions</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 p-2"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
<button onClick={onClose} style={styles.closeBtn}
|
||||
onMouseEnter={e => e.currentTarget.style.color = '#F8FAFC'}
|
||||
onMouseLeave={e => e.currentTarget.style.color = '#94A3B8'}>
|
||||
<X style={{ width: '1.5rem', height: '1.5rem' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
{/* Body */}
|
||||
<div style={styles.body}>
|
||||
{!showAddUser && (
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -221,69 +365,80 @@ export default function UserManagement({ onClose }) {
|
||||
setFormError('');
|
||||
setFormSuccess('');
|
||||
}}
|
||||
className="mb-6 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors flex items-center gap-2 shadow-md"
|
||||
style={styles.addBtn}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14,165,233,0.25), rgba(14,165,233,0.2))';
|
||||
e.currentTarget.style.boxShadow = '0 0 20px rgba(14,165,233,0.25)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14,165,233,0.15), rgba(14,165,233,0.1))';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<Plus style={{ width: '1.125rem', height: '1.125rem' }} />
|
||||
Add User
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Add / Edit Form */}
|
||||
{showAddUser && (
|
||||
<div className="mb-6 p-6 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
<div style={styles.formCard}>
|
||||
<h3 style={styles.formTitle}>
|
||||
{editingUser ? 'Edit User' : 'Add New User'}
|
||||
</h3>
|
||||
|
||||
{formError && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
<span className="text-sm text-red-700">{formError}</span>
|
||||
<div style={styles.alertError}>
|
||||
<AlertCircle style={{ width: '1.125rem', height: '1.125rem', color: '#FCA5A5', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '0.8rem', color: '#FCA5A5' }}>{formError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formSuccess && (
|
||||
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
<span className="text-sm text-green-700">{formSuccess}</span>
|
||||
<div style={styles.alertSuccess}>
|
||||
<CheckCircle style={{ width: '1.125rem', height: '1.125rem', color: '#6EE7B7', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '0.8rem', color: '#6EE7B7' }}>{formSuccess}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Username *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<label style={styles.label}>Username *</label>
|
||||
<div style={styles.inputWrap}>
|
||||
<User style={styles.inputIcon} />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
style={styles.input}
|
||||
placeholder="Enter username"
|
||||
onFocus={e => e.target.style.borderColor = '#0EA5E9'}
|
||||
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.25)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<label style={styles.label}>Email *</label>
|
||||
<div style={styles.inputWrap}>
|
||||
<Mail style={styles.inputIcon} />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
style={styles.input}
|
||||
placeholder="user@example.com"
|
||||
onFocus={e => e.target.style.borderColor = '#0EA5E9'}
|
||||
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.25)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label style={styles.label}>
|
||||
Password {editingUser ? '(leave blank to keep current)' : '*'}
|
||||
</label>
|
||||
<input
|
||||
@@ -291,49 +446,58 @@ export default function UserManagement({ onClose }) {
|
||||
required={!editingUser}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
style={styles.inputNoIcon}
|
||||
onFocus={e => e.target.style.borderColor = '#0EA5E9'}
|
||||
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.25)'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Group *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Shield className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
||||
<label style={styles.label}>Group *</label>
|
||||
<div style={styles.inputWrap}>
|
||||
<Shield style={styles.inputIcon} />
|
||||
<select
|
||||
value={formData.group}
|
||||
onChange={(e) => setFormData({ ...formData, group: e.target.value })}
|
||||
disabled={isGroupDropdownDisabled(editingUser)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
...styles.select,
|
||||
opacity: isGroupDropdownDisabled(editingUser) ? 0.5 : 1,
|
||||
cursor: isGroupDropdownDisabled(editingUser) ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
title={isGroupDropdownDisabled(editingUser) ? 'Cannot change your own Admin group' : ''}
|
||||
>
|
||||
{VALID_GROUPS.map((g) => (
|
||||
<option key={g} value={g}>{GROUP_LABELS[g]}</option>
|
||||
<option key={g} value={g} style={{ background: '#1E293B', color: '#F8FAFC' }}>
|
||||
{GROUP_LABELS[g]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isGroupDropdownDisabled(editingUser) && (
|
||||
<p className="text-xs text-amber-600 mt-1">You cannot change your own Admin group.</p>
|
||||
<p style={{ fontSize: '0.7rem', color: '#F59E0B', marginTop: '0.375rem' }}>
|
||||
You cannot change your own Admin group.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors"
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', paddingTop: '1rem' }}>
|
||||
<button type="submit" style={styles.primaryBtn}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14,165,233,0.25), rgba(14,165,233,0.2))';
|
||||
e.currentTarget.style.boxShadow = '0 0 20px rgba(14,165,233,0.25)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14,165,233,0.15), rgba(14,165,233,0.1))';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}>
|
||||
{editingUser ? 'Update User' : 'Create User'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddUser(false);
|
||||
setEditingUser(null);
|
||||
}}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
<button type="button" style={styles.cancelBtn}
|
||||
onClick={() => { setShowAddUser(false); setEditingUser(null); }}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'rgba(51,65,85,0.8)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'rgba(51,65,85,0.5)'}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -341,87 +505,92 @@ export default function UserManagement({ onClose }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Table */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<Loader className="w-8 h-8 text-[#0476D9] mx-auto animate-spin" />
|
||||
<p className="text-gray-600 mt-2">Loading users...</p>
|
||||
<div style={{ textAlign: 'center', padding: '3rem 0' }}>
|
||||
<Loader style={{ width: '2rem', height: '2rem', color: '#0EA5E9', margin: '0 auto', animation: 'spin 1s linear infinite' }} />
|
||||
<p style={{ color: '#94A3B8', marginTop: '0.5rem', fontSize: '0.875rem' }}>Loading users...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12">
|
||||
<AlertCircle className="w-8 h-8 text-red-500 mx-auto" />
|
||||
<p className="text-red-600 mt-2">{error}</p>
|
||||
<div style={{ textAlign: 'center', padding: '3rem 0' }}>
|
||||
<AlertCircle style={{ width: '2rem', height: '2rem', color: '#EF4444', margin: '0 auto' }} />
|
||||
<p style={{ color: '#FCA5A5', marginTop: '0.5rem', fontSize: '0.875rem' }}>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">User</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Group</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Status</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700">Last Login</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700">Actions</th>
|
||||
<tr>
|
||||
<th style={styles.th}>User</th>
|
||||
<th style={styles.th}>Group</th>
|
||||
<th style={styles.th}>Status</th>
|
||||
<th style={styles.th}>Last Login</th>
|
||||
<th style={styles.thRight}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => (
|
||||
<tr key={user.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-3 px-4">
|
||||
<tr key={user.id}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'rgba(14,165,233,0.05)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||||
<td style={styles.td}>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{user.username}</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
<p style={styles.username}>{user.username}</p>
|
||||
<p style={styles.email}>{user.email}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
style={{
|
||||
...GROUP_BADGE_STYLES[user.group] || GROUP_BADGE_STYLES.Read_Only,
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
display: 'inline-block'
|
||||
}}
|
||||
>
|
||||
<td style={styles.td}>
|
||||
<span style={{
|
||||
...styles.badge,
|
||||
...(GROUP_BADGE_STYLES[user.group] || GROUP_BADGE_STYLES.Read_Only),
|
||||
}}>
|
||||
{user.group ? user.group.replace('_', ' ') : 'Read Only'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<td style={styles.td}>
|
||||
<button
|
||||
onClick={() => handleToggleActive(user)}
|
||||
disabled={user.id === currentUser.id}
|
||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
user.is_active
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
} ${user.id === currentUser.id ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:opacity-80'}`}
|
||||
style={{
|
||||
...(user.is_active ? styles.statusActive : styles.statusInactive),
|
||||
opacity: user.id === currentUser.id ? 0.5 : 1,
|
||||
cursor: user.id === currentUser.id ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{user.is_active ? 'Active' : 'Inactive'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-500">
|
||||
{user.last_login
|
||||
? new Date(user.last_login).toLocaleString()
|
||||
: 'Never'}
|
||||
<td style={styles.td}>
|
||||
<span style={styles.lastLogin}>
|
||||
{user.last_login
|
||||
? new Date(user.last_login).toLocaleString()
|
||||
: 'Never'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
<td style={styles.tdRight}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.25rem' }}>
|
||||
<button
|
||||
onClick={() => handleEdit(user)}
|
||||
className="p-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
style={styles.actionBtn}
|
||||
title="Edit user"
|
||||
onMouseEnter={e => { e.currentTarget.style.color = '#0EA5E9'; e.currentTarget.style.background = 'rgba(14,165,233,0.1)'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.background = 'none'; }}
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
<Edit2 style={{ width: '1rem', height: '1rem' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(user.id)}
|
||||
disabled={user.id === currentUser.id}
|
||||
className={`p-2 text-red-600 hover:bg-red-50 rounded ${
|
||||
user.id === currentUser.id ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
style={{
|
||||
...styles.deleteBtn,
|
||||
opacity: user.id === currentUser.id ? 0.3 : 1,
|
||||
cursor: user.id === currentUser.id ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
title="Delete user"
|
||||
onMouseEnter={e => { if (user.id !== currentUser.id) { e.currentTarget.style.background = 'rgba(239,68,68,0.1)'; } }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'none'; }}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<Trash2 style={{ width: '1rem', height: '1rem' }} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -446,4 +615,4 @@ export default function UserManagement({ onClose }) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user