import { useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { DataTable, Pagination, type Column } from '@/components/DataTable' import { PermissionGuard } from '@/components/PermissionGuard' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' import { Select } from '@/components/ui/Select' import { Dialog } from '@/components/ui/Dialog' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { MenuKeys } from '@/lib/menuKeys' import type { Department, Paged } from '@/types/master' import { AVAILABLE_ROLES, RoleShortName, RoleLabel, type User } from '@/types/users' const fmtDate = (s: string) => new Date(s).toLocaleDateString('vi-VN') type CreateForm = { email: string fullName: string password: string roles: string[] departmentId: string position: string } type EditForm = { id: string fullName: string isActive: boolean departmentId: string position: string } const emptyCreate: CreateForm = { email: '', fullName: '', password: '', roles: [], departmentId: '', position: '' } export function UsersPage() { const qc = useQueryClient() const [page, setPage] = useState(1) const [search, setSearch] = useState('') const [createOpen, setCreateOpen] = useState(false) const [createForm, setCreateForm] = useState(emptyCreate) const [editForm, setEditForm] = useState(null) const [rolesModal, setRolesModal] = useState(null) const [roleSelection, setRoleSelection] = useState([]) const [resetModal, setResetModal] = useState(null) const [newPassword, setNewPassword] = useState('') const list = useQuery({ queryKey: ['users', { page, search }], queryFn: async () => (await api.get>('/users', { params: { page, pageSize: 20, search: search || undefined } })).data, }) const departments = useQuery({ queryKey: ['departments-all'], queryFn: async () => (await api.get>('/departments', { params: { page: 1, pageSize: 200 } })).data.items, }) const createMut = useMutation({ mutationFn: async () => { await api.post('/users', { email: createForm.email, fullName: createForm.fullName, password: createForm.password, roles: createForm.roles, departmentId: createForm.departmentId || null, position: createForm.position || null, }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['users'] }) toast.success('Đã tạo user') setCreateOpen(false) setCreateForm(emptyCreate) }, onError: err => toast.error(getErrorMessage(err)), }) const editMut = useMutation({ mutationFn: async () => { if (!editForm) return await api.put(`/users/${editForm.id}`, { id: editForm.id, fullName: editForm.fullName, isActive: editForm.isActive, departmentId: editForm.departmentId || null, position: editForm.position || null, }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['users'] }) toast.success('Đã lưu') setEditForm(null) }, onError: err => toast.error(getErrorMessage(err)), }) const rolesMut = useMutation({ mutationFn: async () => { if (!rolesModal) return await api.put(`/users/${rolesModal.id}/roles`, { roles: roleSelection }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['users'] }) toast.success('Đã cập nhật role') setRolesModal(null) }, onError: err => toast.error(getErrorMessage(err)), }) const resetMut = useMutation({ mutationFn: async () => { if (!resetModal) return await api.post(`/users/${resetModal.id}/reset-password`, { newPassword }) }, onSuccess: () => { toast.success(`Đã reset password. User phải login lại.`) setResetModal(null) setNewPassword('') }, onError: err => toast.error(getErrorMessage(err)), }) const unlockMut = useMutation({ mutationFn: (id: string) => api.post(`/users/${id}/unlock`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['users'] }) toast.success('Đã mở khóa') }, onError: err => toast.error(getErrorMessage(err)), }) const toggleActiveMut = useMutation({ mutationFn: (u: User) => api.put(`/users/${u.id}`, { id: u.id, fullName: u.fullName, isActive: !u.isActive, departmentId: u.departmentId, position: u.position, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['users'] }) toast.success('Đã cập nhật trạng thái') }, onError: err => toast.error(getErrorMessage(err)), }) function openRoles(u: User) { setRolesModal(u) setRoleSelection([...u.roles]) } function openEdit(u: User) { setEditForm({ id: u.id, fullName: u.fullName, isActive: u.isActive, departmentId: u.departmentId ?? '', position: u.position ?? '', }) } function toggleRole(r: string) { setRoleSelection(sel => (sel.includes(r) ? sel.filter(x => x !== r) : [...sel, r])) } function toggleCreateRole(r: string) { setCreateForm(f => ({ ...f, roles: f.roles.includes(r) ? f.roles.filter(x => x !== r) : [...f.roles, r], })) } const columns: Column[] = [ { key: 'email', header: 'Email', render: u => {u.email} }, { key: 'fullName', header: 'Họ tên', render: u => u.fullName }, { key: 'departmentName', header: 'Phòng ban', width: 'w-44', render: u => (
{u.departmentName ?? }
{u.position &&
{u.position}
}
), }, { key: 'roles', header: 'Vai trò', render: u => (
{u.roles.length === 0 && } {u.roles.map(r => ( {RoleShortName[r] ?? r} ))}
), }, { key: 'isActive', header: 'Active', width: 'w-20', align: 'center', render: u => u.isActive ? : , }, { key: 'isLocked', header: 'Locked', width: 'w-20', align: 'center', render: u => u.isLocked ? ( ) : ( ), }, { key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) }, { key: 'actions', header: '', align: 'right', width: 'w-56', render: u => (
{u.isLocked && ( )}
), }, ] return (
Người dùng } description="Tạo user + gán phòng ban + gán role để test quyền với non-admin." actions={ } />
{ setSearch(e.target.value); setPage(1) }} className="max-w-sm" />
u.id} isLoading={list.isLoading} /> {/* Create user */} setCreateOpen(false)} title="Thêm user mới" size="md" footer={ <> } >
{ e.preventDefault(); createMut.mutate() }}>
setCreateForm(f => ({ ...f, email: e.target.value }))} required />
setCreateForm(f => ({ ...f, fullName: e.target.value }))} required />
setCreateForm(f => ({ ...f, position: e.target.value }))} placeholder="vd: Trưởng phòng CCM" />
setCreateForm(f => ({ ...f, password: e.target.value }))} required minLength={8} />
Tối thiểu 8 ký tự + chữ hoa + thường + số + ký tự đặc biệt
{AVAILABLE_ROLES.map(r => ( ))}
{/* Edit user info */} setEditForm(null)} title="Sửa thông tin user" size="md" footer={ <> } > {editForm && (
setEditForm(f => f && { ...f, fullName: e.target.value })} />
setEditForm(f => f && { ...f, position: e.target.value })} placeholder="vd: Trưởng phòng CCM" />
setEditForm(f => f && { ...f, isActive: e.target.checked })} />
)}
{/* Assign roles */} setRolesModal(null)} title={ Gán role cho {rolesModal?.fullName} {rolesModal?.departmentName && ( {rolesModal.departmentName} )} } size="md" footer={ <> } >
{AVAILABLE_ROLES.map(r => ( ))}
{/* Reset password */} setResetModal(null)} title={`Reset password: ${resetModal?.email}`} size="sm" footer={ <> } >
User sẽ bị logout khỏi mọi session + phải login lại với password mới.
setNewPassword(e.target.value)} minLength={8} />
) }