[CLAUDE] Users: Plan D — F2 toggle AllowDrafterSkipToFinal per-user (Mig 29 wire UI)

BE: UserDto +AllowDrafterSkipToFinal + SetUserAllowDrafterSkipToFinalCommand
+ Handler + UsersController PATCH /api/users/{id}/allow-skip-final body
{allowDrafterSkipToFinal:bool} Policy=Users.Update.

FE Admin: User type +allowDrafterSkipToFinal. UsersPage column "Skip cuối"
violet FastForward badge + action button toggle mirror bypass-review pattern.

fe-user KHÔNG mirror (UsersPage admin-only).

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warning DocxRenderer pre-existing
- npm run build fe-admin — pass 638ms

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 21:03:27 +07:00
parent 3d725c42f7
commit 60efeeda63
4 changed files with 76 additions and 4 deletions

View File

@ -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<PagedResult<UserDto>>;
@ -61,7 +62,7 @@ public class ListUsersQueryHandler(UserManager<User> 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<UserDto>(items, total, request.Page, request.PageSize);
@ -83,7 +84,7 @@ public class GetUserQueryHandler(UserManager<User> 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<User> 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<User> userManager)
: IRequestHandler<SetUserAllowDrafterSkipToFinalCommand>
{
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)));
}
}