[CLAUDE] Workflow: Max 3 cấp/bước + N NV/cấp + sequential gating (V2 UAT iter 2)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m16s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m16s
User feedback: "tối đa 3 cấp (không có cấp 4)" — không phải bắt buộc 3.
Mỗi cấp = N NV (add bao nhiêu cũng được). Quy trình chạy theo số cấp
thật sự cấu hình (1/2/3). C2 chưa thao tác được khi C1 chưa có NV.
Convention DB: nhiều `ApprovalWorkflowLevel` row cùng Order = same Cấp,
mỗi row = 1 NV. Service iterate group by Order; trong cùng cấp =
OR-of-N (1 NV duyệt → cấp pass).
BE — Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs:
- Validator strict:
- Order ∈ {1, 2, 3} (`MaxLevelsPerStep`)
- Sequential gating: HaveSequentialOrders → 1 / 1+2 / 1+2+3, KHÔNG
cho 2 (thiếu 1) hoặc 1+3 (thiếu 2)
- HaveNoDuplicateApproverInSameLevel: 1 NV không thêm 2 lần cùng cấp
- Schema KHÔNG đổi (giữ ApprovalWorkflowLevel.ApproverUserId 1-1).
- Handler không đổi — auto handle multiple rows cùng Order.
FE — ApprovalWorkflowsV2Page.tsx rewrite Levels section:
- Type EditStep.levels → levelEntries: { order: 1|2|3; approverUserId }[]
flat list (group by order trong render).
- 3 SECTION CỐ ĐỊNH C1/C2/C3 trong Designer:
- Mỗi section: header "Cấp N" + count NV + nút "+ Thêm NV"
- List rows mỗi NV với Select dropdown filtered theo Phòng + Trash
- C2 disabled (opacity-60) khi C1 empty. C3 disabled khi C2 empty.
- Tooltip "+ Thêm NV": "Cấp k-1 phải có ≥1 NV trước"
- Add NV: dropdown chỉ NV thuộc Phòng + chưa được thêm cùng cấp
(no duplicate same level).
- Xóa NV: chặn xóa NV cuối Cấp k nếu Cấp k+1 còn entries (toast error
"Hãy xóa hết NV ở Cấp k+1 trước khi rỗng Cấp k").
- Đổi Phòng → clear toàn bộ levelEntries (NV cũ không thuộc Phòng mới).
- DefinitionCard read-only: group s.levels by Order → render mỗi cấp
là 1 row với badge "Cấp N" + list NV bên dưới.
- Save validate: Phòng required + Cấp 1 ≥1 NV + sequential + NV thuộc
đúng Phòng (defensive double-check).
Verify: dotnet build BE OK · 77 test pass · npm build fe-admin OK.
Logic Service PE/Contract chưa wire schema mới — vẫn pin Mig 21 legacy.
This commit is contained in:
@ -180,6 +180,13 @@ public record CreateAwDefinitionCommand(
|
||||
|
||||
public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefinitionCommand>
|
||||
{
|
||||
// Convention Mig 22 + UAT iter 2 (2026-05-08):
|
||||
// Tối đa 3 Cấp/Bước (Order ∈ {1,2,3}). Mỗi Cấp có N approver (multiple
|
||||
// Level rows cùng Order = same Cấp, mỗi row = 1 NV). Sequential gating:
|
||||
// có Cấp k yêu cầu Cấp k-1 đã có ≥1 NV. Không duplicate (Order, UserId)
|
||||
// cùng 1 Bước.
|
||||
public const int MaxLevelsPerStep = 3;
|
||||
|
||||
public CreateAwDefinitionCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ApplicableType).Must(t => Enum.IsDefined(typeof(ApprovalWorkflowApplicableType), t))
|
||||
@ -199,13 +206,39 @@ public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefi
|
||||
.WithMessage("Mỗi bước phải có ít nhất 1 cấp duyệt.");
|
||||
step.RuleForEach(s => s.Levels).ChildRules(level =>
|
||||
{
|
||||
level.RuleFor(l => l.Order).GreaterThanOrEqualTo(1);
|
||||
level.RuleFor(l => l.Order).InclusiveBetween(1, MaxLevelsPerStep)
|
||||
.WithMessage($"Cấp duyệt chỉ trong khoảng 1..{MaxLevelsPerStep}.");
|
||||
level.RuleFor(l => l.Name).MaximumLength(200);
|
||||
level.RuleFor(l => l.ApproverUserId).NotEmpty()
|
||||
.WithMessage("Cấp duyệt phải chỉ định 1 nhân viên cụ thể.");
|
||||
.WithMessage("Mỗi dòng cấp phải chỉ định 1 NV duyệt.");
|
||||
});
|
||||
// Sequential gating + no-duplicate (Order, UserId).
|
||||
step.RuleFor(s => s.Levels).Must(HaveSequentialOrders)
|
||||
.WithMessage($"Cấp duyệt phải tuần tự từ 1 (có Cấp k cần Cấp k-1). Tối đa {MaxLevelsPerStep} cấp.");
|
||||
step.RuleFor(s => s.Levels).Must(HaveNoDuplicateApproverInSameLevel)
|
||||
.WithMessage("Một NV không được duyệt hai lần trong cùng một Cấp.");
|
||||
});
|
||||
}
|
||||
|
||||
// Cho phép 1 / 1+2 / 1+2+3 — KHÔNG cho 2 (thiếu 1) hoặc 1+3 (thiếu 2).
|
||||
private static bool HaveSequentialOrders(List<CreateAwLevelInput> levels)
|
||||
{
|
||||
if (levels.Count == 0) return false;
|
||||
var orders = levels.Select(l => l.Order).Distinct().OrderBy(o => o).ToList();
|
||||
if (orders.Count == 0 || orders.Count > MaxLevelsPerStep) return false;
|
||||
for (int i = 0; i < orders.Count; i++)
|
||||
{
|
||||
if (orders[i] != i + 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HaveNoDuplicateApproverInSameLevel(List<CreateAwLevelInput> levels)
|
||||
{
|
||||
return levels
|
||||
.GroupBy(l => new { l.Order, l.ApproverUserId })
|
||||
.All(g => g.Count() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
|
||||
Reference in New Issue
Block a user