[CLAUDE] App: PE workflow inner steps DTO + UpdateUserPositionLevel CQRS (Chunk B)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m42s

DTO + Validator + Handler mở rộng cho N-stage admin designer (Mig 18):

PeWorkflowAdminFeatures:
- PeWorkflowStepInnerStepDto record (Order/Dept/PositionLevel/Name/SlaDays/IsRequired)
- PeWorkflowStepDto extend +InnerSteps List
- GetPeWorkflowAdminOverviewQuery: Include InnerSteps OrderBy Order +
  resolve DeptNames cho display
- CreatePeWorkflowStepInnerStepInput record
- CreatePeWorkflowStepInput extend +InnerSteps (nullable, default null —
  backward compat existing test PeWorkflowAdminTests positional new())
- Validator child rules cho InnerSteps (Order >=1, DeptId not empty,
  PositionLevel 1-3, SlaDays >=0)
- Handler convert InnerSteps khi build entity (atomic batch insert)

UserFeatures:
- UserDto +PositionLevel int? field
- ListUsers + GetUser handlers map (int?)u.PositionLevel
- SetUserPositionLevelCommand + Validator + Handler mirror
  SetUserBypassReviewCommand pattern (admin set qua UserManager UI)

Verify:
- dotnet build SolutionErp.slnx 0 error
- dotnet test 83 pass (54+29) — no regression
- Backward compat: PeWorkflowAdminTests existing 6 test pass (named-arg
  positional record vẫn work với InnerSteps default null)

Pending Chunk C: PurchaseEvaluationWorkflowService.TransitionAsync N-stage
logic + legacy 2-stage fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 18:14:39 +07:00
parent 13ab533fe7
commit 0e56bd0a67
2 changed files with 104 additions and 6 deletions

View File

@ -21,7 +21,8 @@ public record UserDto(
Guid? DepartmentId,
string? DepartmentName,
string? Position,
bool CanBypassReview);
bool CanBypassReview,
int? PositionLevel); // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
// ========== LIST ==========
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
@ -60,7 +61,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));
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));
}
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
@ -82,7 +83,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);
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);
}
}
@ -288,3 +289,36 @@ public class SetUserBypassReviewCommandHandler(UserManager<User> userManager)
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
}
}
// ========== SET POSITION LEVEL (Mig 18) ==========
// Admin set cấp chức danh trong phòng cho user phục vụ N-stage workflow inner
// step matching. Nullable — null cho admin/system/external user. Chỉ accept
// 1 (NV) / 2 (PP) / 3 (TP) hoặc null. Không kiểm role tương ứng — admin
// tự chịu trách nhiệm map đúng.
public record SetUserPositionLevelCommand(Guid Id, int? PositionLevel) : IRequest;
public class SetUserPositionLevelCommandValidator : AbstractValidator<SetUserPositionLevelCommand>
{
public SetUserPositionLevelCommandValidator()
{
RuleFor(x => x.PositionLevel)
.Must(v => v == null || (v >= 1 && v <= 3))
.WithMessage("PositionLevel chỉ chấp nhận null hoặc 1=NV, 2=PP, 3=TP.");
}
}
public class SetUserPositionLevelCommandHandler(UserManager<User> userManager)
: IRequestHandler<SetUserPositionLevelCommand>
{
public async Task Handle(SetUserPositionLevelCommand request, CancellationToken ct)
{
var user = await userManager.FindByIdAsync(request.Id.ToString())
?? throw new NotFoundException("User", request.Id);
user.PositionLevel = request.PositionLevel == null
? null
: (PositionLevel?)request.PositionLevel.Value;
var result = await userManager.UpdateAsync(user);
if (!result.Succeeded)
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
}
}