'use client'; import { useState, useEffect } from 'react'; import { cmsApi, User, Organization } from '@/lib/api'; import { useAuth } from '@/context/AuthContext'; import { UserCog, Mail, Search, Filter, ShieldCheck, Plus, X, UserPlus, Key, User as UserIcon, Building2, Gauge, Trash2, AlertTriangle } from 'lucide-react'; interface UserWithLimit extends User { monthly_token_limit?: number; token_usage_percentage?: number; } export default function UsersPage() { const [users, setUsers] = useState([]); const [organizations, setOrganizations] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [roleFilter, setRoleFilter] = useState(''); const { user: currentUser } = useAuth(); const [tokenLimits, setTokenLimits] = useState>({}); // Delete User States const [deleteConfirm, setDeleteConfirm] = useState(null); const [deleting, setDeleting] = useState(false); // Create User States const [isModalOpen, setIsModalOpen] = useState(false); const [newUser, setNewUser] = useState({ email: '', password: '', full_name: '', role: 'student', organization_id: '' }); useEffect(() => { loadData(); }, []); const loadData = async () => { try { const [usersData, orgData] = await Promise.all([ cmsApi.getAllUsers(), cmsApi.getOrganization() ]); setUsers(usersData); setOrganizations([orgData]); // Load token limits for each user const limits: Record = {}; for (const user of usersData) { try { const resp = await fetch( `${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/admin/users/${user.id}/token-limit/check`, { credentials: 'include' } ); if (resp.ok) { const data = await resp.json(); limits[user.id] = { limit: data.monthly_limit, percentage: data.monthly_limit > 0 ? Math.round((data.used_tokens / data.monthly_limit) * 100) : 0, }; } } catch (err) { console.error(`Failed to load limit for user ${user.id}:`, err); } } setTokenLimits(limits); } catch (error) { console.error('Failed to load data', error); } finally { setLoading(false); } }; const handleUpdateUser = async (userId: string, role: string, orgId: string) => { try { await cmsApi.updateUser(userId, { role, organization_id: orgId }); loadData(); } catch (error) { console.error('Failed to update user', error); } }; const handleDeleteUser = async () => { if (!deleteConfirm) return; setDeleting(true); try { await cmsApi.deleteUser(deleteConfirm.id); setDeleteConfirm(null); loadData(); } catch (error) { console.error('Failed to delete user', error); alert('No se pudo eliminar el usuario.'); } finally { setDeleting(false); } }; const handleCreateUser = async (e: React.FormEvent) => { e.preventDefault(); try { await cmsApi.createUser(newUser); setNewUser({ email: '', password: '', full_name: '', role: 'student', organization_id: '' }); setIsModalOpen(false); loadData(); } catch (error) { console.error('Failed to create user', error); alert('Failed to create user. Ensure email is unique.'); } }; const filteredUsers = users.filter(u => { const safeName = (u.full_name || '').toLowerCase(); const safeEmail = (u.email || '').toLowerCase(); const term = searchTerm.toLowerCase(); const matchesSearch = safeName.includes(term) || safeEmail.includes(term); const matchesRole = roleFilter === '' || u.role === roleFilter; return matchesSearch && matchesRole; }); const formatNumber = (num: number) => new Intl.NumberFormat('en-US').format(num); if (currentUser?.role !== 'admin') { return (

Access Denied

Only system administrators can access this page.

); } return (

User Management

Manage users, roles, and organization assignments.

setSearchTerm(e.target.value)} className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white shadow-sm dark:shadow-none" />
{loading ? ( [1, 2, 3, 4, 5].map(i => ( )) ) : filteredUsers.map((u) => ( ))}
User Role Organization Token Limit Actions
{((u.full_name || u.email || '?').trim().charAt(0) || '?').toUpperCase()}
{u.full_name || 'Sin nombre'}
{u.email || 'sin-email'}
{tokenLimits[u.id] ? (
= 100 ? 'text-red-600 bg-red-50 dark:bg-red-900/20' : tokenLimits[u.id].percentage >= 80 ? 'text-yellow-600 bg-yellow-50 dark:bg-yellow-900/20' : tokenLimits[u.id].percentage >= 50 ? 'text-blue-600 bg-blue-50 dark:bg-blue-900/20' : 'text-green-600 bg-green-50 dark:bg-green-900/20' }`}> {tokenLimits[u.id].limit === 0 ? '∞' : `${tokenLimits[u.id].percentage}%`} {tokenLimits[u.id].limit > 0 && (
= 100 ? 'bg-red-600' : tokenLimits[u.id].percentage >= 80 ? 'bg-yellow-600' : tokenLimits[u.id].percentage >= 50 ? 'bg-blue-600' : 'bg-green-600' }`} style={{ width: `${Math.min(tokenLimits[u.id].percentage, 100)}%` }} />
)} {tokenLimits[u.id].limit === 0 ? 'Unlimited' : formatNumber(tokenLimits[u.id].limit)}
) : ( - )}
{u.id !== currentUser?.id && ( )}
{!loading && filteredUsers.length === 0 && (
No users found matching your search.
)}
{/* Delete Confirmation Modal */} {deleteConfirm && (

Eliminar usuario

¿Confirmas que deseas eliminar a {deleteConfirm.full_name || deleteConfirm.email}? Esta acción no se puede deshacer.

)} {/* Create User Modal */} {isModalOpen && (

Add New User

setNewUser({ ...newUser, full_name: e.target.value })} className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" placeholder="e.g. John Doe" />
setNewUser({ ...newUser, email: e.target.value })} className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm text-slate-900 dark:text-white placeholder-slate-400" placeholder="user@example.com" />
setNewUser({ ...newUser, password: e.target.value })} className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" placeholder="••••••••" />
)}
); }