[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 AssignmentValue,
|
||||||
string? DisplayName);
|
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(
|
public record PeWorkflowStepDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
int Order,
|
int Order,
|
||||||
@ -25,7 +37,8 @@ public record PeWorkflowStepDto(
|
|||||||
string PhaseLabel,
|
string PhaseLabel,
|
||||||
string Name,
|
string Name,
|
||||||
int? SlaDays,
|
int? SlaDays,
|
||||||
List<PeWorkflowStepApproverDto> Approvers);
|
List<PeWorkflowStepApproverDto> Approvers,
|
||||||
|
List<PeWorkflowStepInnerStepDto> InnerSteps);
|
||||||
|
|
||||||
public record PeWorkflowDefinitionDto(
|
public record PeWorkflowDefinitionDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
@ -79,9 +92,21 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
|||||||
var definitions = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
|
var definitions = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
|
||||||
.Include(d => d.Steps.OrderBy(s => s.Order))
|
.Include(d => d.Steps.OrderBy(s => s.Order))
|
||||||
.ThenInclude(s => s.Approvers)
|
.ThenInclude(s => s.Approvers)
|
||||||
|
.Include(d => d.Steps)
|
||||||
|
.ThenInclude(s => s.InnerSteps.OrderBy(i => i.Order))
|
||||||
.OrderByDescending(d => d.Version)
|
.OrderByDescending(d => d.Version)
|
||||||
.ToListAsync(ct);
|
.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
|
// Resolve user names cho User-kind approvers
|
||||||
var userIds = definitions
|
var userIds = definitions
|
||||||
.SelectMany(d => d.Steps)
|
.SelectMany(d => d.Steps)
|
||||||
@ -125,7 +150,16 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
|||||||
s.Approvers.Select(a => new PeWorkflowStepApproverDto(
|
s.Approvers.Select(a => new PeWorkflowStepApproverDto(
|
||||||
(int)a.Kind,
|
(int)a.Kind,
|
||||||
a.AssignmentValue,
|
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());
|
)).ToList());
|
||||||
|
|
||||||
var types = Enum.GetValues<PurchaseEvaluationType>()
|
var types = Enum.GetValues<PurchaseEvaluationType>()
|
||||||
@ -155,12 +189,23 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
|||||||
|
|
||||||
public record CreatePeWorkflowStepApproverInput(int Kind, string AssignmentValue);
|
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(
|
public record CreatePeWorkflowStepInput(
|
||||||
int Order,
|
int Order,
|
||||||
int Phase,
|
int Phase,
|
||||||
string Name,
|
string Name,
|
||||||
int? SlaDays,
|
int? SlaDays,
|
||||||
List<CreatePeWorkflowStepApproverInput> Approvers);
|
List<CreatePeWorkflowStepApproverInput> Approvers,
|
||||||
|
List<CreatePeWorkflowStepInnerStepInput>? InnerSteps = null);
|
||||||
|
|
||||||
public record CreatePeWorkflowDefinitionCommand(
|
public record CreatePeWorkflowDefinitionCommand(
|
||||||
PurchaseEvaluationType EvaluationType,
|
PurchaseEvaluationType EvaluationType,
|
||||||
@ -195,6 +240,16 @@ public class CreatePeWorkflowDefinitionCommandValidator : AbstractValidator<Crea
|
|||||||
app.RuleFor(a => a.Kind).InclusiveBetween(1, 2);
|
app.RuleFor(a => a.Kind).InclusiveBetween(1, 2);
|
||||||
app.RuleFor(a => a.AssignmentValue).NotEmpty().MaximumLength(100);
|
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,
|
Kind = (WorkflowApproverKind)a.Kind,
|
||||||
AssignmentValue = a.AssignmentValue,
|
AssignmentValue = a.AssignmentValue,
|
||||||
}).ToList(),
|
}).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(),
|
.ToList(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,7 +21,8 @@ public record UserDto(
|
|||||||
Guid? DepartmentId,
|
Guid? DepartmentId,
|
||||||
string? DepartmentName,
|
string? DepartmentName,
|
||||||
string? Position,
|
string? Position,
|
||||||
bool CanBypassReview);
|
bool CanBypassReview,
|
||||||
|
int? PositionLevel); // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
|
||||||
|
|
||||||
// ========== LIST ==========
|
// ========== LIST ==========
|
||||||
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
|
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 roles = await userManager.GetRolesAsync(u);
|
||||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
||||||
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
|
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);
|
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
||||||
@ -82,7 +83,7 @@ public class GetUserQueryHandler(UserManager<User> userManager, IApplicationDbCo
|
|||||||
string? deptName = null;
|
string? deptName = null;
|
||||||
if (u.DepartmentId is { } did)
|
if (u.DepartmentId is { } did)
|
||||||
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
|
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)));
|
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