// ⚠️ 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'; import ConfirmModal from './ConfirmModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const VALID_GROUPS = ['Admin', 'Standard_User', 'Leadership', 'Read_Only']; const GROUP_LABELS = { Admin: 'Admin (full access)', Standard_User: 'Standard User (create, edit, limited delete)', Leadership: 'Leadership (read-only + exports)', Read_Only: 'Read Only (view only)' }; const GROUP_BADGE_STYLES = { 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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showAddUser, setShowAddUser] = useState(false); const [editingUser, setEditingUser] = useState(null); const [formData, setFormData] = useState({ username: '', email: '', password: '', group: 'Read_Only', bu_teams: '' }); const [formError, setFormError] = useState(''); const [formSuccess, setFormSuccess] = useState(''); const [pendingConfirm, setPendingConfirm] = useState(null); useEffect(() => { fetchUsers(); }, []); const fetchUsers = async () => { try { const response = await fetch(`${API_BASE}/users`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch users'); const data = await response.json(); setUsers(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const doSubmit = async () => { setFormError(''); setFormSuccess(''); try { const url = editingUser ? `${API_BASE}/users/${editingUser.id}` : `${API_BASE}/users`; const method = editingUser ? 'PATCH' : 'POST'; const body = { ...formData }; if (editingUser && !body.password) { delete body.password; } const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Operation failed'); } setFormSuccess(editingUser ? 'User updated successfully' : 'User created successfully'); fetchUsers(); setTimeout(() => { setShowAddUser(false); setEditingUser(null); setFormData({ username: '', email: '', password: '', group: 'Read_Only', bu_teams: '' }); setFormSuccess(''); }, 1500); } catch (err) { setFormError(err.message); } }; const handleSubmit = (e) => { e.preventDefault(); 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') { message += ` WARNING: You are removing Admin privileges from ${editingUser.username}. They will lose full system access.`; } setPendingConfirm({ title: 'Change User Group', message, confirmText: 'Change Group', variant: editingUser.group === 'Admin' ? 'danger' : 'warning', onConfirm: () => { setPendingConfirm(null); doSubmit(); }, }); return; } doSubmit(); }; const handleEdit = (user) => { setEditingUser(user); setFormData({ username: user.username, email: user.email, password: '', group: user.group, bu_teams: user.bu_teams || '' }); setShowAddUser(true); setFormError(''); setFormSuccess(''); }; const handleDelete = async (userId) => { setPendingConfirm({ title: 'Delete User', message: 'Are you sure you want to delete this user?', confirmText: 'Delete', onConfirm: async () => { setPendingConfirm(null); try { const response = await fetch(`${API_BASE}/users/${userId}`, { method: 'DELETE', credentials: 'include' }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Delete failed'); } fetchUsers(); } catch (err) { alert(err.message); } }, }); }; const handleToggleActive = async (user) => { try { const response = await fetch(`${API_BASE}/users/${user.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ is_active: !user.is_active }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Update failed'); } fetchUsers(); } catch (err) { alert(err.message); } }; const isGroupDropdownDisabled = (targetUser) => { if (!targetUser || !currentUser) return false; return targetUser.id === currentUser.id && currentUser.group === 'Admin'; }; return (
Manage user accounts and permissions
Loading users...
{error}
| User | Group | Teams | Status | Last Login | Actions |
|---|---|---|---|---|---|
|
{user.username} {user.email} |
{user.group ? user.group.replace('_', ' ') : 'Read Only'} |
{(user.teams && user.teams.length > 0) ? (
{user.teams.map(t => (
{t}
))}
) : (
⚠ No teams
)}
|
{user.last_login ? new Date(user.last_login).toLocaleString() : 'Never'} |
|