From ae59cfeb5d55e64b444a13278e17241682ba99d3 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 23 Apr 2026 14:28:32 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin:=20UsersPage=20dept/positio?= =?UTF-8?q?n=20field=20+=20RoleShortName=20ti=E1=BA=BFng=20Vi=E1=BB=87t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## types/users.ts - User type + departmentId/departmentName/position - CreateUserInput + departmentId/position - RoleShortName map (Mã viết tắt VN per role): QTV/NV.PB/TPB/PM/PRO/CCM/ FIN/ACT/EQU/BOD/NĐUQ/HRA - RoleLabel map (Tên đầy đủ VN per role) - roleDisplayName(role) → "BOD — Ban Giám đốc" combined helper ## types/menu.ts Role type + shortName field (mirror BE RoleDto). ## UsersPage redesign - Column "Phòng ban" (departmentName + position 2 dòng) - Column "Vai trò" hiển thị badge ShortName ("BOD", "CCM", "PM"), tooltip hover full label - Column actions thêm "Sửa thông tin" (Pencil icon) — dialog edit dept/ position/active state - Create dialog 2-col grid: Email | Họ tên / Phòng ban (dropdown) | Chức vụ / Password (col-span-2). Roles checkboxes hiển thị "ShortName — full label" - Edit dialog mới — sửa fullName + dept + position + isActive - Roles dialog title kèm dept name (context cho user reviewer) - toggleActive mutation include departmentId/position để không reset ## PermissionsPage Panel 1 role list: - 2-line per row: ShortName (semibold) + Description (truncate small) - Tooltip = description đầy đủ - Active row vẫn ring-brand-200 Panel 2 header badge: ShortName thay name code English. ## Build fe-admin: tsc + vite pass (12.17s lần 1, 671ms lần 2) Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/pages/system/PermissionsPage.tsx | 14 +- fe-admin/src/pages/system/UsersPage.tsx | 231 +++++++++++++++--- fe-admin/src/types/menu.ts | 1 + fe-admin/src/types/users.ts | 51 +++- 4 files changed, 246 insertions(+), 51 deletions(-) diff --git a/fe-admin/src/pages/system/PermissionsPage.tsx b/fe-admin/src/pages/system/PermissionsPage.tsx index 2f66d8b..1c159f4 100644 --- a/fe-admin/src/pages/system/PermissionsPage.tsx +++ b/fe-admin/src/pages/system/PermissionsPage.tsx @@ -130,13 +130,19 @@ export function PermissionsPage() { key={r.id} onClick={() => setRoleId(r.id)} className={cn( - 'flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm transition', + 'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left transition', roleId === r.id ? 'bg-brand-50 text-brand-700 font-medium ring-1 ring-brand-200' : 'text-slate-600 hover:bg-slate-50', )} + title={r.description ?? r.name} > - {r.name} + + {r.shortName ?? r.name} + + {r.description ?? r.name} + + {roleId === r.id && } ))} @@ -149,8 +155,8 @@ export function PermissionsPage() {

2. Quyền theo menu

{selectedRole && ( - - {selectedRole.name} + + {selectedRole.shortName ?? selectedRole.name} )}
diff --git a/fe-admin/src/pages/system/UsersPage.tsx b/fe-admin/src/pages/system/UsersPage.tsx index 0d7f7f3..b391b2b 100644 --- a/fe-admin/src/pages/system/UsersPage.tsx +++ b/fe-admin/src/pages/system/UsersPage.tsx @@ -1,6 +1,6 @@ import { useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { KeyRound, Plus, Shield, Unlock, Users, CheckCircle2, XCircle } from 'lucide-react' +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' @@ -8,22 +8,43 @@ 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 { Paged } from '@/types/master' -import { AVAILABLE_ROLES, RoleLabel, type User } from '@/types/users' +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({ email: '', fullName: '', password: '', roles: [] as string[] }) + const [createForm, setCreateForm] = useState(emptyCreate) + + const [editForm, setEditForm] = useState(null) const [rolesModal, setRolesModal] = useState(null) const [roleSelection, setRoleSelection] = useState([]) @@ -37,15 +58,46 @@ export function UsersPage() { (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', createForm) + 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({ email: '', fullName: '', password: '', roles: [] }) + 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)), }) @@ -86,7 +138,11 @@ export function UsersPage() { }) const toggleActiveMut = useMutation({ - mutationFn: (u: User) => api.put(`/users/${u.id}`, { id: u.id, fullName: u.fullName, isActive: !u.isActive }), + 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') @@ -98,11 +154,16 @@ export function UsersPage() { 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, @@ -113,6 +174,17 @@ export function UsersPage() { 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ò', @@ -120,8 +192,12 @@ export function UsersPage() {
{u.roles.length === 0 && } {u.roles.map(r => ( - - {RoleLabel[r] ?? r} + + {RoleShortName[r] ?? r} ))}
@@ -130,39 +206,37 @@ export function UsersPage() { { key: 'isActive', header: 'Active', - width: 'w-24', + width: 'w-20', align: 'center', render: u => - u.isActive ? ( - - ) : ( - - ), + u.isActive ? : , }, { key: 'isLocked', header: 'Locked', - width: 'w-24', + width: 'w-20', align: 'center', render: u => u.isLocked ? ( - Locked ) : ( ), }, - { key: 'createdAt', header: 'Ngày tạo', width: 'w-28', render: u => fmtDate(u.createdAt) }, + { key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) }, { key: 'actions', header: '', align: 'right', - width: 'w-52', + width: 'w-56', render: u => (
+ @@ -192,7 +266,7 @@ export function UsersPage() { Người dùng } - description="Tạo user + gán role để test quyền với non-admin." + description="Tạo user + gán phòng ban + gán role để test quyền với non-admin." actions={ + + + } + > + {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}`} + title={ + + + Gán role cho {rolesModal?.fullName} + {rolesModal?.departmentName && ( + + {rolesModal.departmentName} + + )} + + } size="md" footer={ <> @@ -287,7 +443,8 @@ export function UsersPage() { checked={roleSelection.includes(r)} onChange={() => toggleRole(r)} /> - {RoleLabel[r]} + {RoleShortName[r]} + — {RoleLabel[r]} ))}
diff --git a/fe-admin/src/types/menu.ts b/fe-admin/src/types/menu.ts index d5bed7a..2808753 100644 --- a/fe-admin/src/types/menu.ts +++ b/fe-admin/src/types/menu.ts @@ -22,6 +22,7 @@ export type MenuItem = { export type Role = { id: string name: string + shortName: string | null description: string | null createdAt: string } diff --git a/fe-admin/src/types/users.ts b/fe-admin/src/types/users.ts index 7973c88..fda5131 100644 --- a/fe-admin/src/types/users.ts +++ b/fe-admin/src/types/users.ts @@ -6,6 +6,9 @@ export type User = { isLocked: boolean createdAt: string roles: string[] + departmentId: string | null + departmentName: string | null + position: string | null } export type CreateUserInput = { @@ -13,9 +16,12 @@ export type CreateUserInput = { fullName: string password: string roles: string[] + departmentId: string | null + position: string | null } -// 12 role seed trong BE (AppRoles.cs) +// 12 role seed trong BE (AppRoles.cs). Mã + Tên đầy đủ tiếng Việt khớp +// với BE SeedRolesAsync (Role.ShortName + Role.Description). export const AVAILABLE_ROLES = [ 'Admin', 'Drafter', @@ -31,17 +37,42 @@ export const AVAILABLE_ROLES = [ 'HrAdmin', ] as const +// Mã viết tắt (Vietnamese abbreviation) — dùng khi không gian hẹp (badge, chip) +export const RoleShortName: Record = { + Admin: 'QTV', + Drafter: 'NV.PB', + DeptManager: 'TPB', + ProjectManager: 'PM', + Procurement: 'PRO', + CostControl: 'CCM', + Finance: 'FIN', + Accounting: 'ACT', + Equipment: 'EQU', + Director: 'BOD', + AuthorizedSigner: 'NĐUQ', + HrAdmin: 'HRA', +} + +// Tên đầy đủ tiếng Việt — dùng khi rộng rãi (form, list) export const RoleLabel: Record = { - Admin: 'Quản trị', - Drafter: 'Người soạn', + Admin: 'Quản trị viên hệ thống', + Drafter: 'Nhân viên phòng ban (soạn HĐ)', DeptManager: 'Trưởng phòng ban', ProjectManager: 'Giám đốc dự án', - Procurement: 'Cung ứng', - CostControl: 'Kiểm soát chi phí', - Finance: 'Tài chính', - Accounting: 'Kế toán', - Equipment: 'Thiết bị', + Procurement: 'Phòng Cung ứng', + CostControl: 'Phòng Kiểm soát chi phí', + Finance: 'Phòng Tài chính', + Accounting: 'Phòng Kế toán', + Equipment: 'Phòng Thiết bị', Director: 'Ban Giám đốc', - AuthorizedSigner: 'Người ủy quyền ký', - HrAdmin: 'Nhân sự / Đóng dấu', + AuthorizedSigner: 'Người được Ủy quyền ký HĐ', + HrAdmin: 'Phòng Nhân sự - Hành chính', +} + +// Combined display: "BOD — Ban Giám đốc" +export function roleDisplayName(roleName: string): string { + const short = RoleShortName[roleName] + const full = RoleLabel[roleName] + if (short && full) return `${short} — ${full}` + return full ?? roleName }