[CLAUDE] PE-Workflow: S21 t5 Chunk A — Mig 29 refactor Allow* sang per-NV (per-Level + per-Drafter)

Refactor 6 Allow* options từ workflow-level (Mig 28 S21 t4) sang per-NV scope:
- F1 (4 mode Trả lại) + F3 (Edit Section 2) → 5 flag MOVE xuống
  `ApprovalWorkflowLevels` (per slot Approver, cùng table với ApproverUserId).
- F2 (AllowDrafterSkipToFinal) → MOVE xuống `Users` (per-Drafter user, User Mgmt).

Mig 29 `RefactorAdvancedOptionsToPerLevelAndDrafterUser` — 4-stage migration
(EF auto-generated drop-then-add đã được REORDER manual):
1. ADD 5 column trên `ApprovalWorkflowLevels` (AllowReturnOneLevel/OneStep/
   ToAssignee/ToDrafter[default true]/AllowApproverEditDetails)
2. ADD 1 column trên `Users` (AllowDrafterSkipToFinal default false)
3. BACKFILL bulk SQL (preserve admin config Mig 28):
   - Levels: copy workflow.Allow* → all Levels của workflow (JOIN Steps)
   - Users: SET TRUE cho user nào từng Drafter PE link workflow Allow=true
4. DROP 6 column workflow-level (Mig 28 cleanup)
3-file rule complete. Apply LocalDB Dev + Design success.

Domain entity refactor:
- `ApprovalWorkflow.cs` — REMOVE 6 Allow* field (S21 t4 Mig 28 cũ)
- `ApprovalWorkflowLevel.cs` — ADD 5 Allow* field (F1 + F3)
- `User.cs` — ADD 1 Allow* field (F2 AllowDrafterSkipToFinal)

EF config update:
- `ApprovalWorkflowConfiguration.cs` — remove 6 HasDefaultValue workflow-level,
  add 5 HasDefaultValue per-Level (4 false + 1 AllowReturnToDrafter true S17)

Service refactor `ApplyReturnModeAsync` (`PurchaseEvaluationWorkflowService.cs`):
- Resolve currentLevel slot (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder)
- Read 5 Allow* từ `currentLevel.AllowXxx` thay vì workflow.Allow*
- Admin bypass per-Level flag check (unchanged behavior)
- Drafter mode đặc biệt: check AllowReturnToDrafter của currentLevel (vẫn validate)
- V1 legacy (no V2 schema) → fallback Drafter behavior tự động

DRAFTER trình refactor (`TransitionAsync` skipToFinal branch):
- Permission check moved from workflow-level → `drafterUser.AllowDrafterSkipToFinal`
- Use `userManager.FindByIdAsync(actorUserId)` để get current Drafter user entity
- Admin bypass user flag check (unchanged)

Helper `EnsureEditableForDetailsAsync` refactor:
- Read `level.AllowApproverEditDetails` thay vì workflow.AllowApproverEditDetails
- Error message rõ "Cấp Approver hiện tại (Bước X / Cấp Y)" thay vì "Workflow"

DTO refactor:
- `AwLevelDto` ADD 5 Allow* field (admin Designer GET per-Level)
- `AwDefinitionDto` REMOVE 6 Allow* (no longer workflow-level)
- `CreateAwLevelInput` ADD 5 Allow* param (admin Designer POST per-Level)
- `CreateAwDefinitionCommand` REMOVE 6 Allow* (Steps[].Levels[] now has them)
- `ApprovalWorkflowOptionsDto` chỉ còn 5 flag (F2 removed — separate field)
- `PurchaseEvaluationDetailBundleDto`:
  - rename `WorkflowOptions` → `CurrentLevelOptions` (clearer semantic per-slot)
  - ADD `DrafterAllowSkipToFinal bool` (resolve từ DrafterUserId → User entity)

GetPurchaseEvaluationQueryHandler populate:
- `currentLevelOptions` = 5 Allow* của Cấp hiện tại (null nếu V1 legacy / no pointer)
- `drafterAllowSkipToFinal` = User.AllowDrafterSkipToFinal lookup từ DrafterUserId

Backward compat verified:
- Mig 29 backfill preserve admin config S21 t4 — workflow cũ vẫn chạy đúng
  sau deploy. User chưa từng làm Drafter F2 phải opt-in lần đầu (no auto-set).
- 84 test PASS (58 Domain + 26 Infra unchanged, 3 gotcha #45 guard test backward
  compat signature).

Pending Chunk B/C: FE Admin Designer move 5 checkbox xuống per-Level slot + FE
eOffice read currentLevelOptions + drafterAllowSkipToFinal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 20:03:28 +07:00
parent eea86fdfe7
commit 036694638e
11 changed files with 4361 additions and 177 deletions

View File

@ -26,7 +26,14 @@ public record AwLevelDto(
string? Name,
Guid ApproverUserId,
string? ApproverUserName,
string? ApproverEmail);
string? ApproverEmail,
// Mig 29 (S21 t5) — 5 advanced options per slot Approver (F1 mode Trả lại
// + F3 Edit Section 2). Mỗi NV trong workflow có quyền riêng.
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowApproverEditDetails);
public record AwStepDto(
Guid Id,
@ -46,15 +53,9 @@ public record AwDefinitionDto(
string? Description,
bool IsActive,
bool IsUserSelectable,
// Mig 28 (S21 t4) — 6 advanced options của workflow per version. Admin
// Designer tick stick → checkbox. FE eOffice render dropdown / Skip / Edit
// conditional theo flag tương ứng.
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowDrafterSkipToFinal,
bool AllowApproverEditDetails,
// Mig 29 (S21 t5) — 6 advanced options đã MOVE per-NV: 5 flag (F1+F3) xuống
// AwLevelDto (per slot Approver), F2 AllowDrafterSkipToFinal xuống User table
// (per-Drafter user). Workflow-level Mig 28 dropped.
DateTime? ActivatedAt,
DateTime CreatedAt,
List<AwStepDto> Steps);
@ -137,13 +138,6 @@ public class GetAwAdminOverviewQueryHandler(
d.Description,
d.IsActive,
d.IsUserSelectable,
// Mig 28 — 6 Allow* flag
d.AllowReturnOneLevel,
d.AllowReturnOneStep,
d.AllowReturnToAssignee,
d.AllowReturnToDrafter,
d.AllowDrafterSkipToFinal,
d.AllowApproverEditDetails,
d.ActivatedAt,
d.CreatedAt,
d.Steps.OrderBy(s => s.Order).Select(s => new AwStepDto(
@ -155,7 +149,10 @@ public class GetAwAdminOverviewQueryHandler(
s.Levels.OrderBy(l => l.Order).Select(l =>
{
users.TryGetValue(l.ApproverUserId, out var info);
return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email);
// Mig 29 (S21 t5) — 5 Allow* flag per slot Level
return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email,
l.AllowReturnOneLevel, l.AllowReturnOneStep, l.AllowReturnToAssignee,
l.AllowReturnToDrafter, l.AllowApproverEditDetails);
}).ToList()
)).ToList());
@ -181,7 +178,18 @@ public class GetAwAdminOverviewQueryHandler(
// ========== POST new version ==========
public record CreateAwLevelInput(int Order, string? Name, Guid ApproverUserId);
public record CreateAwLevelInput(
int Order,
string? Name,
Guid ApproverUserId,
// Mig 29 (S21 t5) — 5 Allow* options per slot. Admin Designer tick per
// Level row. Default backward compat: AllowReturnToDrafter=true, 4 còn lại
// false (admin opt-in từng slot).
bool AllowReturnOneLevel = false,
bool AllowReturnOneStep = false,
bool AllowReturnToAssignee = false,
bool AllowReturnToDrafter = true,
bool AllowApproverEditDetails = false);
public record CreateAwStepInput(
int Order,
@ -194,15 +202,7 @@ public record CreateAwDefinitionCommand(
string Code,
string Name,
string? Description,
List<CreateAwStepInput> Steps,
// Mig 28 (S21 t4) — 6 Allow* options. Default = backward compat S17
// (chỉ Trả về Drafter enabled). Admin tick stick để mở mode khác.
bool AllowReturnOneLevel = false,
bool AllowReturnOneStep = false,
bool AllowReturnToAssignee = false,
bool AllowReturnToDrafter = true,
bool AllowDrafterSkipToFinal = false,
bool AllowApproverEditDetails = false) : IRequest<Guid>;
List<CreateAwStepInput> Steps) : IRequest<Guid>;
public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefinitionCommand>
{
@ -295,13 +295,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
Description = request.Description,
IsActive = true,
IsUserSelectable = true, // Mig 25 — version mới mặc định cho user pick
// Mig 28 (S21 t4) — 6 Allow* options
AllowReturnOneLevel = request.AllowReturnOneLevel,
AllowReturnOneStep = request.AllowReturnOneStep,
AllowReturnToAssignee = request.AllowReturnToAssignee,
AllowReturnToDrafter = request.AllowReturnToDrafter,
AllowDrafterSkipToFinal = request.AllowDrafterSkipToFinal,
AllowApproverEditDetails = request.AllowApproverEditDetails,
// Mig 29 (S21 t5) — Allow* options đã move xuống Level slot (per-NV)
ActivatedAt = DateTime.UtcNow,
Steps = request.Steps.OrderBy(s => s.Order)
.Select(s => new ApprovalWorkflowStep
@ -315,6 +309,12 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
Order = l.Order,
Name = l.Name,
ApproverUserId = l.ApproverUserId,
// Mig 29 (S21 t5) — 5 Allow* per slot
AllowReturnOneLevel = l.AllowReturnOneLevel,
AllowReturnOneStep = l.AllowReturnOneStep,
AllowReturnToAssignee = l.AllowReturnToAssignee,
AllowReturnToDrafter = l.AllowReturnToDrafter,
AllowApproverEditDetails = l.AllowApproverEditDetails,
}).ToList(),
})
.ToList(),

View File

@ -78,14 +78,15 @@ public record PurchaseEvaluationChangelogDto(
string? ContextNote,
DateTime CreatedAt);
// Mig 28 (S21 t4) — 6 advanced options của workflow pin. FE filter Trả lại
// dropdown / Skip submit / Edit Section 2 enabled theo flag tương ứng.
// Mig 29 (S21 t5) — Approver options của slot Level hiện tại (per-NV).
// FE eOffice filter Trả lại dropdown + Edit Section 2 enabled theo flag của
// Cấp hiện tại NV đang duyệt. Null nếu phiếu V1 legacy hoặc không ChoDuyet.
// F2 (Drafter skip) đã move sang `PeDetailBundleDto.DrafterAllowSkipToFinal`.
public record ApprovalWorkflowOptionsDto(
bool AllowReturnOneLevel,
bool AllowReturnOneStep,
bool AllowReturnToAssignee,
bool AllowReturnToDrafter,
bool AllowDrafterSkipToFinal,
bool AllowApproverEditDetails);
public record PurchaseEvaluationWorkflowSummaryDto(
@ -204,9 +205,14 @@ public record PurchaseEvaluationDetailBundleDto(
string? ApprovalWorkflowCode,
string? ApprovalWorkflowName,
int? ApprovalWorkflowVersion,
// Mig 28 (S21 t4) — 6 Allow* options của workflow pin. Null nếu phiếu V1
// legacy. FE render Trả lại dropdown + Skip + Edit Section 2 conditional.
ApprovalWorkflowOptionsDto? WorkflowOptions,
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null
// nếu V1 legacy hoặc không ChoDuyet. FE render Trả lại dropdown + Edit
// Section 2 conditional. Field rename "WorkflowOptions" → "CurrentLevelOptions"
// để rõ semantic per-slot không phải workflow-wide.
ApprovalWorkflowOptionsDto? CurrentLevelOptions,
// Mig 29 — F2 per-Drafter: cờ AllowDrafterSkipToFinal của Drafter user pin
// phiếu. Workspace conditional render checkbox "Gửi thẳng Cấp cuối".
bool DrafterAllowSkipToFinal,
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
PurchaseEvaluationApprovalFlowDto? ApprovalFlow,
List<PurchaseEvaluationSupplierDto> Suppliers,

View File

@ -68,16 +68,19 @@ internal static class PurchaseEvaluationDraftGuard
.FirstOrDefaultAsync(w => w.Id == awId, ct)
?? throw new ConflictException("Workflow không tồn tại.");
if (!workflow.AllowApproverEditDetails)
throw new ConflictException(
"Workflow không bật mode 'Approver chỉnh sửa Section 2'. " +
"Phải Trả lại Drafter sửa hoặc liên hệ Admin Designer.");
var step = workflow.Steps.OrderBy(s => s.Order).Skip(stepIdx).FirstOrDefault();
var level = step?.Levels.FirstOrDefault(lv => lv.Order == levelOrder);
if (level is null)
throw new ConflictException("Workflow Bước/Cấp không tìm thấy — schema lỗi.");
// Mig 29 (S21 t5) — F3 flag move xuống Level slot (per-NV). Đọc
// từ level.AllowApproverEditDetails thay vì workflow-level cũ.
if (!level.AllowApproverEditDetails)
throw new ConflictException(
$"Cấp Approver hiện tại (Bước {step!.Order} / Cấp {levelOrder}) " +
"không được cấp quyền chỉnh sửa Section 2. " +
"Phải Trả lại Drafter sửa hoặc liên hệ Admin Designer cấp quyền slot.");
if (level.ApproverUserId != actorUserId)
throw new ForbiddenException(
$"Chỉ NV phụ trách Bước {step!.Order} / Cấp {levelOrder} " +

View File

@ -560,9 +560,19 @@ public class GetPurchaseEvaluationQueryHandler(
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
// Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full
// Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards.
// Mig 29 (S21 t5) — F2 drafter flag từ User entity (per-Drafter user
// AllowDrafterSkipToFinal). Default false nếu DrafterUserId null.
var drafterAllowSkipToFinal = false;
if (e.DrafterUserId is Guid drafterId)
{
var drafterUser = await userManager.Users.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == drafterId, ct);
drafterAllowSkipToFinal = drafterUser?.AllowDrafterSkipToFinal ?? false;
}
string? awCode = null, awName = null;
int? awVersion = null;
ApprovalWorkflowOptionsDto? awOptions = null;
ApprovalWorkflowOptionsDto? currentLevelOptions = null;
PurchaseEvaluationCurrentApprovalDto? currentApproval = null;
PurchaseEvaluationApprovalFlowDto? approvalFlow = null;
if (e.ApprovalWorkflowId is Guid awId)
@ -576,14 +586,25 @@ public class GetPurchaseEvaluationQueryHandler(
awCode = aw.Code;
awName = aw.Name;
awVersion = aw.Version;
// Mig 28 — 6 Allow* options pin lúc PE create
awOptions = new ApprovalWorkflowOptionsDto(
aw.AllowReturnOneLevel,
aw.AllowReturnOneStep,
aw.AllowReturnToAssignee,
aw.AllowReturnToDrafter,
aw.AllowDrafterSkipToFinal,
aw.AllowApproverEditDetails);
// Mig 29 (S21 t5) — Resolve Cấp hiện tại + populate 5 Allow* flag
// của slot Approver đang duyệt. Null nếu pointer chưa init.
if (e.CurrentWorkflowStepIndex is int curStepIdx
&& curStepIdx >= 0 && curStepIdx < aw.Steps.Count
&& e.CurrentApprovalLevelOrder is int curLevelOrder)
{
var curStep = aw.Steps.OrderBy(s => s.Order).Skip(curStepIdx).FirstOrDefault();
var curLevel = curStep?.Levels.FirstOrDefault(l => l.Order == curLevelOrder);
if (curLevel is not null)
{
currentLevelOptions = new ApprovalWorkflowOptionsDto(
curLevel.AllowReturnOneLevel,
curLevel.AllowReturnOneStep,
curLevel.AllowReturnToAssignee,
curLevel.AllowReturnToDrafter,
curLevel.AllowApproverEditDetails);
}
}
var steps = aw.Steps.OrderBy(s => s.Order).ToList();
// Resolve dept names cho Steps
@ -703,7 +724,9 @@ public class GetPurchaseEvaluationQueryHandler(
e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
e.BudgetId, budgetSummary,
e.BudgetManualName, e.BudgetManualAmount,
e.ApprovalWorkflowId, awCode, awName, awVersion, awOptions,
e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions,
// Mig 29 (S21 t5) — F2 drafter flag từ User entity
drafterAllowSkipToFinal,
currentApproval, approvalFlow,
e.Suppliers
.OrderBy(s => s.Order)