[CLAUDE] App: PE workflow inner steps DTO + UpdateUserPositionLevel CQRS (Chunk B)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m42s
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:
@ -18,6 +18,18 @@ public record PeWorkflowStepApproverDto(
|
||||
string AssignmentValue,
|
||||
string? DisplayName);
|
||||
|
||||
// Mig 18 — N-stage approval inner step level con. Cấu hình động trong cùng
|
||||
// 1 phase: NV.A → PP.A → TP.A → NV.B → ... theo Order asc.
|
||||
public record PeWorkflowStepInnerStepDto(
|
||||
Guid Id,
|
||||
int Order,
|
||||
Guid DepartmentId,
|
||||
string? DepartmentName,
|
||||
int PositionLevel, // 1=NV, 2=PP, 3=TP
|
||||
string? Name,
|
||||
int? SlaDays,
|
||||
bool IsRequired);
|
||||
|
||||
public record PeWorkflowStepDto(
|
||||
Guid Id,
|
||||
int Order,
|
||||
@ -25,7 +37,8 @@ public record PeWorkflowStepDto(
|
||||
string PhaseLabel,
|
||||
string Name,
|
||||
int? SlaDays,
|
||||
List<PeWorkflowStepApproverDto> Approvers);
|
||||
List<PeWorkflowStepApproverDto> Approvers,
|
||||
List<PeWorkflowStepInnerStepDto> InnerSteps);
|
||||
|
||||
public record PeWorkflowDefinitionDto(
|
||||
Guid Id,
|
||||
@ -79,9 +92,21 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
||||
var definitions = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
|
||||
.Include(d => d.Steps.OrderBy(s => s.Order))
|
||||
.ThenInclude(s => s.Approvers)
|
||||
.Include(d => d.Steps)
|
||||
.ThenInclude(s => s.InnerSteps.OrderBy(i => i.Order))
|
||||
.OrderByDescending(d => d.Version)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Resolve dept names cho InnerStep.DepartmentName display
|
||||
var deptIds = definitions
|
||||
.SelectMany(d => d.Steps).SelectMany(s => s.InnerSteps)
|
||||
.Select(i => i.DepartmentId).Distinct().ToList();
|
||||
var deptNames = deptIds.Count == 0
|
||||
? new Dictionary<Guid, string>()
|
||||
: await db.Departments.AsNoTracking()
|
||||
.Where(d => deptIds.Contains(d.Id))
|
||||
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
|
||||
|
||||
// Resolve user names cho User-kind approvers
|
||||
var userIds = definitions
|
||||
.SelectMany(d => d.Steps)
|
||||
@ -125,7 +150,16 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
||||
s.Approvers.Select(a => new PeWorkflowStepApproverDto(
|
||||
(int)a.Kind,
|
||||
a.AssignmentValue,
|
||||
ResolveDisplay(a, userNames))).ToList()
|
||||
ResolveDisplay(a, userNames))).ToList(),
|
||||
s.InnerSteps.OrderBy(i => i.Order).Select(i => new PeWorkflowStepInnerStepDto(
|
||||
i.Id,
|
||||
i.Order,
|
||||
i.DepartmentId,
|
||||
deptNames.GetValueOrDefault(i.DepartmentId),
|
||||
(int)i.PositionLevel,
|
||||
i.Name,
|
||||
i.SlaDays,
|
||||
i.IsRequired)).ToList()
|
||||
)).ToList());
|
||||
|
||||
var types = Enum.GetValues<PurchaseEvaluationType>()
|
||||
@ -155,12 +189,23 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
||||
|
||||
public record CreatePeWorkflowStepApproverInput(int Kind, string AssignmentValue);
|
||||
|
||||
// Mig 18 — Inner step input cho designer N-stage. InnerSteps optional empty
|
||||
// list → service fallback 2-stage Review/Confirm logic legacy Mig 16.
|
||||
public record CreatePeWorkflowStepInnerStepInput(
|
||||
int Order,
|
||||
Guid DepartmentId,
|
||||
int PositionLevel, // 1=NV, 2=PP, 3=TP
|
||||
string? Name,
|
||||
int? SlaDays,
|
||||
bool IsRequired);
|
||||
|
||||
public record CreatePeWorkflowStepInput(
|
||||
int Order,
|
||||
int Phase,
|
||||
string Name,
|
||||
int? SlaDays,
|
||||
List<CreatePeWorkflowStepApproverInput> Approvers);
|
||||
List<CreatePeWorkflowStepApproverInput> Approvers,
|
||||
List<CreatePeWorkflowStepInnerStepInput>? InnerSteps = null);
|
||||
|
||||
public record CreatePeWorkflowDefinitionCommand(
|
||||
PurchaseEvaluationType EvaluationType,
|
||||
@ -195,6 +240,16 @@ public class CreatePeWorkflowDefinitionCommandValidator : AbstractValidator<Crea
|
||||
app.RuleFor(a => a.Kind).InclusiveBetween(1, 2);
|
||||
app.RuleFor(a => a.AssignmentValue).NotEmpty().MaximumLength(100);
|
||||
});
|
||||
step.RuleForEach(s => s.InnerSteps!).ChildRules(inner =>
|
||||
{
|
||||
inner.RuleFor(i => i.Order).GreaterThanOrEqualTo(1);
|
||||
inner.RuleFor(i => i.DepartmentId).NotEmpty();
|
||||
inner.RuleFor(i => i.PositionLevel).InclusiveBetween(1, 3)
|
||||
.WithMessage("PositionLevel: 1=NV, 2=PP, 3=TP.");
|
||||
inner.RuleFor(i => i.Name).MaximumLength(200);
|
||||
inner.RuleFor(i => i.SlaDays).GreaterThanOrEqualTo(0)
|
||||
.When(i => i.SlaDays != null);
|
||||
}).When(s => s.InnerSteps != null);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -237,6 +292,15 @@ public class CreatePeWorkflowDefinitionCommandHandler(IApplicationDbContext db)
|
||||
Kind = (WorkflowApproverKind)a.Kind,
|
||||
AssignmentValue = a.AssignmentValue,
|
||||
}).ToList(),
|
||||
InnerSteps = (s.InnerSteps ?? new()).OrderBy(i => i.Order).Select(i => new PurchaseEvaluationWorkflowStepInnerStep
|
||||
{
|
||||
Order = i.Order,
|
||||
DepartmentId = i.DepartmentId,
|
||||
PositionLevel = (PositionLevel)i.PositionLevel,
|
||||
Name = i.Name,
|
||||
SlaDays = i.SlaDays,
|
||||
IsRequired = i.IsRequired,
|
||||
}).ToList(),
|
||||
})
|
||||
.ToList(),
|
||||
};
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user