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);
}
}