diff --git a/fe-admin/src/pages/system/UsersPage.tsx b/fe-admin/src/pages/system/UsersPage.tsx index 00398ea..3801b05 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, ShieldCheck } from 'lucide-react' +import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck, FastForward } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { DataTable, Pagination, type Column } from '@/components/DataTable' @@ -175,6 +175,19 @@ export function UsersPage() { onError: err => toast.error(getErrorMessage(err)), }) + // F2 per-Drafter (Mig 29): toggle AllowDrafterSkipToFinal. Khi true, Drafter + // được tick "Gửi thẳng Cấp cuối" trong PE Workspace để skip Bước/Cấp trung + // gian và bay thẳng tới Cấp cuối workflow. + const allowSkipMut = useMutation({ + mutationFn: (u: User) => + api.patch(`/users/${u.id}/allow-skip-final`, { allowDrafterSkipToFinal: !u.allowDrafterSkipToFinal }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['users'] }) + toast.success('Đã cập nhật quyền gửi thẳng Cấp cuối') + }, + onError: err => toast.error(getErrorMessage(err)), + }) + function nextPositionLevel(current: number | null): number | null { // Cycle null → 1 (NV) → 2 (PP) → 3 (TP) → null if (current == null) return 1 @@ -289,6 +302,21 @@ export function UsersPage() { ), }, + { + key: 'allowDrafterSkipToFinal', + header: 'Skip cuối', + width: 'w-20', + align: 'center', + render: u => + u.allowDrafterSkipToFinal ? ( + + + skip + + ) : ( + + ), + }, { key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) }, { key: 'actions', @@ -334,6 +362,14 @@ export function UsersPage() { {u.positionLevel != null ? PositionLevelShort[u.positionLevel] : '—'} + diff --git a/fe-admin/src/types/users.ts b/fe-admin/src/types/users.ts index f41f4e5..749835f 100644 --- a/fe-admin/src/types/users.ts +++ b/fe-admin/src/types/users.ts @@ -11,6 +11,7 @@ export type User = { position: string | null canBypassReview: boolean positionLevel: number | null // Mig 18 — 1=NV, 2=PP, 3=TP, null=admin/external + allowDrafterSkipToFinal: boolean // Mig 29 — F2: Drafter được gửi thẳng Cấp cuối workflow PE } // Cấp chức danh trong phòng (Mig 18) — phục vụ N-stage workflow inner step. diff --git a/src/Backend/SolutionErp.Api/Controllers/UsersController.cs b/src/Backend/SolutionErp.Api/Controllers/UsersController.cs index a2e9910..07ba7d7 100644 --- a/src/Backend/SolutionErp.Api/Controllers/UsersController.cs +++ b/src/Backend/SolutionErp.Api/Controllers/UsersController.cs @@ -84,9 +84,22 @@ public class UsersController(IMediator mediator) : ControllerBase await mediator.Send(new SetUserPositionLevelCommand(id, body.PositionLevel), ct); return NoContent(); } + + // Mig 29 F2 per-Drafter: admin toggle AllowDrafterSkipToFinal cho user. Khi + // true, Drafter có thể tick "Gửi thẳng Cấp cuối" trong PE Workspace để bay + // thẳng tới Cấp cuối workflow. Mặc định false. + [HttpPatch("{id:guid}/allow-skip-final")] + [Authorize(Policy = "Users.Update")] + public async Task SetAllowDrafterSkipToFinal( + Guid id, [FromBody] SetAllowDrafterSkipToFinalBody body, CancellationToken ct) + { + await mediator.Send(new SetUserAllowDrafterSkipToFinalCommand(id, body.AllowDrafterSkipToFinal), ct); + return NoContent(); + } } public record AssignRolesBody(List Roles); public record ResetPasswordBody(string NewPassword); public record SetBypassReviewBody(bool CanBypassReview); public record SetPositionLevelBody(int? PositionLevel); +public record SetAllowDrafterSkipToFinalBody(bool AllowDrafterSkipToFinal); diff --git a/src/Backend/SolutionErp.Application/Users/UserFeatures.cs b/src/Backend/SolutionErp.Application/Users/UserFeatures.cs index 056abe1..40e281c 100644 --- a/src/Backend/SolutionErp.Application/Users/UserFeatures.cs +++ b/src/Backend/SolutionErp.Application/Users/UserFeatures.cs @@ -22,7 +22,8 @@ public record UserDto( string? DepartmentName, string? Position, bool CanBypassReview, - int? PositionLevel); // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user. + int? PositionLevel, // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user. + bool AllowDrafterSkipToFinal); // Mig 29 — F2 per-Drafter: cho phép Drafter gửi thẳng Cấp cuối khi tạo PE. // ========== LIST ========== public record ListUsersQuery : PagedRequest, IRequest>; @@ -61,7 +62,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, u.CanBypassReview, (int?)u.PositionLevel)); + items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal)); } return new PagedResult(items, total, request.Page, request.PageSize); @@ -83,7 +84,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, u.CanBypassReview, (int?)u.PositionLevel); + return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal); } } @@ -322,3 +323,24 @@ public class SetUserPositionLevelCommandHandler(UserManager userManager) throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); } } + +// ========== SET ALLOW DRAFTER SKIP TO FINAL (Mig 29 — F2 per-Drafter) ========== +// Admin toggle AllowDrafterSkipToFinal cho 1 user. Khi true, user (Drafter) được +// dùng checkbox "Gửi thẳng Cấp cuối" trong PE Workspace để skip toàn bộ Bước/Cấp +// trung gian và bay thẳng tới Cấp cuối. Mặc định false (an toàn — Drafter phải +// tuần tự qua mọi Bước/Cấp theo workflow). +public record SetUserAllowDrafterSkipToFinalCommand(Guid Id, bool AllowDrafterSkipToFinal) : IRequest; + +public class SetUserAllowDrafterSkipToFinalCommandHandler(UserManager userManager) + : IRequestHandler +{ + public async Task Handle(SetUserAllowDrafterSkipToFinalCommand request, CancellationToken ct) + { + var user = await userManager.FindByIdAsync(request.Id.ToString()) + ?? throw new NotFoundException("User", request.Id); + user.AllowDrafterSkipToFinal = request.AllowDrafterSkipToFinal; + var result = await userManager.UpdateAsync(user); + if (!result.Succeeded) + throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); + } +}