diff --git a/fe-admin/src/pages/system/UsersPage.tsx b/fe-admin/src/pages/system/UsersPage.tsx index b391b2b..29ca3ee 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 { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle } from 'lucide-react' +import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { DataTable, Pagination, type Column } from '@/components/DataTable' @@ -150,6 +150,19 @@ export function UsersPage() { onError: err => toast.error(getErrorMessage(err)), }) + // 2-stage dept approval (Migration 16): bật bypass cho NV → cho phép họ + // Confirm trực tiếp thay vì chỉ Review. Dùng cho phòng ban không có TPB + // hoặc TPB ủy quyền cho 1 NV cụ thể. + const bypassMut = useMutation({ + mutationFn: (u: User) => + api.patch(`/users/${u.id}/bypass-review`, { canBypassReview: !u.canBypassReview }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['users'] }) + toast.success('Đã cập nhật quyền bypass review') + }, + onError: err => toast.error(getErrorMessage(err)), + }) + function openRoles(u: User) { setRolesModal(u) setRoleSelection([...u.roles]) @@ -225,6 +238,21 @@ export function UsersPage() { ), }, + { + key: 'canBypassReview', + header: 'Bypass', + width: 'w-20', + align: 'center', + render: u => + u.canBypassReview ? ( + + + bypass + + ) : ( + + ), + }, { key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) }, { key: 'actions', @@ -248,6 +276,14 @@ export function UsersPage() { )} + diff --git a/fe-admin/src/types/users.ts b/fe-admin/src/types/users.ts index fda5131..274678f 100644 --- a/fe-admin/src/types/users.ts +++ b/fe-admin/src/types/users.ts @@ -9,6 +9,7 @@ export type User = { departmentId: string | null departmentName: string | null position: string | null + canBypassReview: boolean } export type CreateUserInput = { diff --git a/src/Backend/SolutionErp.Application/Users/UserFeatures.cs b/src/Backend/SolutionErp.Application/Users/UserFeatures.cs index 61112da..7f3fc87 100644 --- a/src/Backend/SolutionErp.Application/Users/UserFeatures.cs +++ b/src/Backend/SolutionErp.Application/Users/UserFeatures.cs @@ -20,7 +20,8 @@ public record UserDto( List Roles, Guid? DepartmentId, string? DepartmentName, - string? Position); + string? Position, + bool CanBypassReview); // ========== LIST ========== public record ListUsersQuery : PagedRequest, IRequest>; @@ -59,7 +60,7 @@ public class ListUsersQueryHandler(UserManager userManager, IApplicationDb var roles = await userManager.GetRolesAsync(u); var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now; string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null; - items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position)); + items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview)); } return new PagedResult(items, total, request.Page, request.PageSize); @@ -81,7 +82,7 @@ public class GetUserQueryHandler(UserManager userManager, IApplicationDbCo string? deptName = null; if (u.DepartmentId is { } did) deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct); - return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position); + return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview); } }