import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, Save, UserX, UserCheck, Check, X as XIcon, Mail, LayoutGrid, Monitor } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAuth } from '../../context/auth-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; import { listUsers, listRoles, createUser, updateUser, setUserActive, approveUser, rejectUser, resendUserEmail, listUserSessions, revokeUserSession, revokeAllUserSessions } from '../../services/users.js'; import { listDepartments } from '../../services/departments.js'; import { listEmployees } from '../../services/integrations.js'; import { ListView } from '../../components/ListView.jsx'; import { SearchInput } from '../../components/SearchInput.jsx'; import { Button } from '../../components/Button.jsx'; import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { Badge } from '../../components/Badge.jsx'; import { DetailPopup } from '../../components/DetailPopup.jsx'; import { UserModulesDialog } from './UserModulesDialog.jsx'; import { DraftNotice } from '../../components/DraftNotice.jsx'; import { DraftMark, useGuardedClose } from '../../components/NavigationGuard.jsx'; import { useDraftForm, useUserDrafts, hasDraftFor, useReopenDraft } from '../../hooks/useDraftForm.js'; import { PasswordInput } from '../../components/PasswordInput.jsx'; import { SessionsList } from '../../components/SessionsList.jsx'; import { EmailChangeRequests } from './EmailChangeRequests.jsx'; function flatten(nodes, acc = []) { for (const n of nodes) { acc.push(n); if (n.children?.length) flatten(n.children, acc); } return acc; } const STATUS_COLOR = { pending_verification: 'muted', pending_approval: 'warning', active: 'success', invited: 'muted' }; const EMPTY_FORM = { name: '', email: '', roleId: '', departmentId: '', position: '', password: '', employeeId: '' }; // Nómina viene en MAYÚSCULAS: "RIERA CAJAMARCA" → "Riera Cajamarca". const titleCase = (s) => String(s || '').toLowerCase().replace(/(^|\s)\p{L}/gu, (m) => m.toUpperCase()).trim(); const fold = (s) => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim().toUpperCase(); export default function UsersPanel() { const { t, pushToast, openDialog } = useUi(); const { user: currentUser, logout } = useAuth(); const navigate = useNavigate(); const runAction = useAction(); const { data: users, reload } = useAsync(listUsers, []); const { data: roles } = useAsync(listRoles, []); const { data: deptTree } = useAsync(listDepartments, []); const canManage = ['admin', 'superadmin'].includes(currentUser?.roleCode); const isSuperadmin = currentUser?.roleCode === 'superadmin'; const [modulesUser, setModulesUser] = useState(null); const [sessionsUser, setSessionsUser] = useState(null); const loadSessions = useCallback(() => listUserSessions(sessionsUser.id), [sessionsUser]); const revokeSession = useCallback((id) => revokeUserSession(sessionsUser.id, id), [sessionsUser]); const revokeAllSessions = useCallback(() => revokeAllUserSessions(sessionsUser.id), [sessionsUser]); const [search, setSearch] = useState(''); const [employees, setEmployees] = useState([]); const [editing, setEditing] = useState(false); const [selected, setSelected] = useState(null); const [approving, setApproving] = useState(null); const guardedClose = useGuardedClose(); // Borrador del usuario (nuevo o edicion). La contraseña nunca se guarda como borrador. const draft = useDraftForm({ formId: selected ? 'user-edit' : 'user-new', entityId: selected?.id ?? null, initial: selected ? { name: selected.name, email: selected.email, roleId: selected.role_id, departmentId: selected.department_id || '', position: selected.position || '', password: '', editVersion: selected.edit_version } : EMPTY_FORM, label: selected ? `${t('common_edit')}: ${selected.name}` : t('user_new'), enabled: editing, exclude: ['password'], }); const form = draft.values; const setForm = draft.setValues; const collaboratorRole = (roles || []).find((r) => r.code === 'collaborator'); const approveDraft = useDraftForm({ formId: 'user-approve', entityId: approving?.id, initial: { roleId: approving?.role_id || collaboratorRole?.id || '', departmentId: '', editVersion: approving?.edit_version }, label: `${t('user_approve_title')}: ${approving?.name || ''}`, enabled: !!approving, }); const approveForm = approveDraft.values; const setApproveForm = approveDraft.setValues; const pendingNew = hasDraftFor(useUserDrafts(), 'user-new', null); useReopenDraft({ newFormId: canManage ? 'user-new' : null, editFormId: canManage ? 'user-edit' : null, items: users, openNew: () => startNew(), openEdit: (u) => startEdit(u) }); useReopenDraft({ editFormId: canManage ? 'user-approve' : null, items: users, openEdit: (u) => setApproving(u) }); const deptOptions = deptTree ? flatten(deptTree).map((d) => ({ value: d.id, label: d.name })) : []; const roleOptions = (roles || []).map((r) => ({ value: r.id, label: r.name })); function startNew() { setSelected(null); setEditing(true); // Empleados de nómina que aún no tienen usuario (si hay fuente de nómina configurada). listEmployees(true).then(setEmployees).catch(() => setEmployees([])); } function pickEmployee(id) { const e = employees.find((x) => x.id === Number(id)); if (!e) return setForm((f) => ({ ...f, employeeId: '' })); const dept = deptOptions.find((d) => fold(d.label) === fold(e.department_name)); setForm((f) => ({ ...f, employeeId: e.id, name: titleCase(`${e.first_names} ${e.last_names}`), email: e.email || f.email, position: e.position ? titleCase(e.position) : f.position, departmentId: dept ? dept.value : f.departmentId })); } function startEdit(user) { if (!canManage) return; setSelected(user); setEditing(true); } async function save() { if (!form.name.trim() || !form.email.trim() || !form.roleId) return; if (!selected && !form.password) return; const run = () => runAction(async () => { if (selected) { await updateUser(selected.id, { name: form.name.trim(), roleId: form.roleId, departmentId: form.departmentId || null, position: form.position || null, editVersion: form.editVersion }); } else { await createUser({ name: form.name.trim(), email: form.email.trim(), roleId: form.roleId, departmentId: form.departmentId || null, position: form.position || null, password: form.password, employeeId: form.employeeId || undefined }); } draft.clear(); await reload(); setEditing(false); pushToast({ tone: 'success', message: t('common_saved') }); }); if (selected) await draft.guardSave(run); else await run(); } async function toggleActive(user) { await runAction(async () => { await setUserActive(user.id, !user.is_active); await reload(); }); } function startApprove(user) { setApproving(user); } async function confirmApprove() { await approveDraft.guardSave(() => runAction(async () => { await approveUser(approving.id, { roleId: approveForm.roleId, departmentId: approveForm.departmentId || null, editVersion: approveForm.editVersion }); approveDraft.clear(); setApproving(null); await reload(); pushToast({ tone: 'success', message: t('common_saved') }); })); } function resendEmail(user) { runAction(async () => { await resendUserEmail(user.id); pushToast({ tone: 'success', message: t('user_resend_sent') }); }); } function confirmReject(user) { openDialog({ tone: 'warning', title: t('user_reject'), message: t('user_reject_confirm_msg'), actions: [ { label: t('common_cancel'), variant: 'standard' }, { label: t('user_reject'), variant: 'danger', onClick: () => runAction(async () => { await rejectUser(user.id); await reload(); }) }, ], }); } const columns = [ { key: 'name', label: t('common_name') }, { key: 'email', label: t('user_email') }, { key: 'role_name', label: t('user_role') }, { key: 'department_name', label: t('common_department'), render: (r) => r.department_name || '—' }, { key: 'status', label: t('common_status'), sortable: false, render: (r) => ( {t(`user_status_${r.status}`)} ) }, ]; if (!users) return
{t('common_loading')}
; return (
{canManage && }
{canManage && } `${u.name} ${u.email} ${u.role_name} ${u.department_name || ''}`} getRowKey={(u) => u.id} onRowClick={canManage ? startEdit : undefined} rowActions={canManage ? (u) => { if (u.status === 'pending_approval') { return [ { label: t('user_approve'), icon: Check, onClick: () => startApprove(u) }, { label: t('user_reject'), icon: XIcon, danger: true, onClick: () => confirmReject(u) }, ]; } if (u.status === 'pending_verification') { return [ { label: t('user_resend'), icon: Mail, onClick: () => resendEmail(u) }, { label: t('user_reject'), icon: XIcon, danger: true, onClick: () => confirmReject(u) }, ]; } if (u.status === 'invited') { return [{ label: t('user_resend'), icon: Mail, onClick: () => resendEmail(u) }]; } return [ { label: t('common_edit'), onClick: () => startEdit(u) }, ...(u.role_code !== 'superadmin' || isSuperadmin ? [{ label: t('sessions_title'), icon: Monitor, onClick: () => setSessionsUser(u) }] : []), ...(isSuperadmin && u.role_code !== 'superadmin' ? [{ label: t('user_modules'), icon: LayoutGrid, onClick: () => setModulesUser(u) }] : []), u.is_active ? { label: t('common_inactive'), icon: UserX, danger: true, onClick: () => toggleActive(u) } : { label: t('common_active'), icon: UserCheck, onClick: () => toggleActive(u) }, ]; } : undefined} /> {modulesUser && setModulesUser(null)} />} setSessionsUser(null)} width={850} height={540}> {sessionsUser && { logout(); navigate('/login'); }} />} guardedClose(draft, () => setEditing(false))} width={560} height={500}> {!selected && employees.length > 0 && ( ({ value: e.id, label: `${e.last_names} ${e.first_names} · ${e.identification}${e.position ? ` · ${e.position}` : ''}` }))]} value={form.employeeId} onChange={pickEmployee} /> )} setForm((f) => ({ ...f, name: e.target.value }))} autoFocus /> setForm((f) => ({ ...f, email: e.target.value }))} /> setForm((f) => ({ ...f, roleId: v }))} /> setForm((f) => ({ ...f, departmentId: v }))} /> setForm((f) => ({ ...f, position: e.target.value }))} /> {!selected && ( setForm((f) => ({ ...f, password: e.target.value }))} /> )}
guardedClose(approveDraft, () => setApproving(null))} width={420} height={340}> setApproveForm((f) => ({ ...f, roleId: v }))} /> setApproveForm((f) => ({ ...f, departmentId: v }))} />
); }