[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:
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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} " +
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -34,45 +34,10 @@ public class ApprovalWorkflow : BaseEntity
|
||||
// khi tạo version mới (mirror IsActive default), admin có thể unstick.
|
||||
public bool IsUserSelectable { get; set; }
|
||||
|
||||
// ===== Mig 28 (Session 21 turn 4) — 6 advanced options per workflow =====
|
||||
// Cấu hình "Cấu hình nâng cao" trong Admin Designer. User eOffice render
|
||||
// dropdown/checkbox theo flag enabled. 4 flag Return* = mode Trả lại (F1).
|
||||
// 1 flag Skip = Drafter trình thẳng Cấp cuối (F2). 1 flag EditDetails =
|
||||
// Approver chỉnh Section 2 (F3).
|
||||
//
|
||||
// Default backward compat S17: AllowReturnToDrafter=true (mọi workflow cũ
|
||||
// chạy đúng — fallback "Trả về Drafter" như Session 17 spec). 5 flag còn
|
||||
// lại default false — admin opt-in per workflow để audit nghiêm.
|
||||
|
||||
/// F1 mode 1 — Cho phép Approver Trả lại 1 Cấp trước (lùi pointer trong
|
||||
/// cùng Step). Phiếu GIỮ Phase=ChoDuyet (peer review chain).
|
||||
public bool AllowReturnOneLevel { get; set; }
|
||||
|
||||
/// F1 mode 2 — Cho phép Approver Trả lại 1 Bước trước (lùi sang Step trước,
|
||||
/// set level = max của step đó). Phiếu GIỮ Phase=ChoDuyet.
|
||||
public bool AllowReturnOneStep { get; set; }
|
||||
|
||||
/// F1 mode 3 — Cho phép Approver Trả lại Người chỉ định (pick runtime từ
|
||||
/// list NV ĐÃ DUYỆT trong PeLevelOpinions). Phiếu GIỮ Phase=ChoDuyet, set
|
||||
/// Step/Level = vị trí của user pick trong workflow.
|
||||
public bool AllowReturnToAssignee { get; set; }
|
||||
|
||||
/// F1 mode 4 — Cho phép Approver Trả lại Người soạn thảo (Drafter). Phiếu
|
||||
/// đi vào Phase=TraLai, clear pointer (như Session 17 spec). Default TRUE
|
||||
/// để backward compat — admin có thể unstick force peer review only.
|
||||
public bool AllowReturnToDrafter { get; set; } = true;
|
||||
|
||||
/// F2 — Cho phép Drafter gửi thẳng Cấp cuối (skip mọi Bước/Cấp trung gian).
|
||||
/// UI eOffice trình duyệt thêm dropdown 2 option ("Gửi tuần tự" default vs
|
||||
/// "Gửi thẳng Cấp cuối"). BE set CurrentWorkflowStepIndex=maxStep,
|
||||
/// CurrentApprovalLevelOrder=maxLevel. Audit changelog "Drafter skip C1..N".
|
||||
public bool AllowDrafterSkipToFinal { get; set; }
|
||||
|
||||
/// F3 — Cho phép Approver chỉnh sửa Section 2 (Hạng mục + NCC + Báo giá)
|
||||
/// khi phase=ChoDuyet + actor match CurrentLevel.ApproverUserId. KHÔNG đụng
|
||||
/// PE Header (TenGoiThau/Project/Budget). KHÔNG reset workflow. Audit ghi
|
||||
/// PurchaseEvaluationChangelog cho mỗi field/row thay đổi.
|
||||
public bool AllowApproverEditDetails { get; set; }
|
||||
// Mig 28 cũ 6 column workflow-level Allow* đã DROP trong Mig 29 (S21 t5).
|
||||
// Refactor sang per-NV (per ApprovalWorkflowLevel slot + Users F2). Backfill
|
||||
// bulk SQL copy workflow → all Levels của workflow trước khi DROP — preserve
|
||||
// admin config từ S21 t4.
|
||||
|
||||
public List<ApprovalWorkflowStep> Steps { get; set; } = new();
|
||||
}
|
||||
@ -105,5 +70,33 @@ public class ApprovalWorkflowLevel : BaseEntity
|
||||
public string? Name { get; set; } // "Cấp 1" — display optional
|
||||
public Guid ApproverUserId { get; set; } // 1 NV cụ thể duyệt cấp này
|
||||
|
||||
// ===== Mig 29 (Session 21 turn 5) — 5 advanced options per slot =====
|
||||
// Cấu hình quyền duyệt riêng cho TỪNG NV trong slot này. Admin Designer
|
||||
// tick stick per Level row (KHÔNG còn workflow-level cũ Mig 28).
|
||||
//
|
||||
// F1 (4 mode Trả lại) + F3 (Edit Section 2) = quyền của Approver Level.
|
||||
// F2 (Drafter skip) đã move sang Users.AllowDrafterSkipToFinal (per-Drafter
|
||||
// user — không liên quan slot Approver).
|
||||
//
|
||||
// Backfill Mig 29: copy từ workflow-level Allow* cũ → all Levels của workflow.
|
||||
// Default backward compat: AllowReturnToDrafter=true (S17 fallback). 4 flag
|
||||
// còn lại default false (admin opt-in per Level).
|
||||
|
||||
/// F1 mode 1 — Lùi 1 Cấp trong cùng Bước. Phase giữ ChoDuyet (peer review).
|
||||
public bool AllowReturnOneLevel { get; set; }
|
||||
|
||||
/// F1 mode 2 — Lùi sang Bước trước Cấp cuối. Phase giữ ChoDuyet.
|
||||
public bool AllowReturnOneStep { get; set; }
|
||||
|
||||
/// F1 mode 3 — Pick runtime từ NV đã ký (PeLevelOpinions). Phase giữ ChoDuyet.
|
||||
public bool AllowReturnToAssignee { get; set; }
|
||||
|
||||
/// F1 mode 4 — Phase=TraLai, clear pointer (S17 fallback). Default TRUE.
|
||||
public bool AllowReturnToDrafter { get; set; } = true;
|
||||
|
||||
/// F3 — Cho phép NV này edit Section 2 (Hạng mục + NCC + Báo giá) lúc đang
|
||||
/// duyệt. KHÔNG đụng PE Header, KHÔNG reset workflow.
|
||||
public bool AllowApproverEditDetails { get; set; }
|
||||
|
||||
public ApprovalWorkflowStep? Step { get; set; }
|
||||
}
|
||||
|
||||
@ -27,4 +27,13 @@ public class User : IdentityUser<Guid>
|
||||
// Mỗi inner step yêu cầu user khớp DepartmentId + PositionLevel mới duyệt
|
||||
// được sub-step đó. Null cho admin/system/external user.
|
||||
public PositionLevel? PositionLevel { get; set; }
|
||||
|
||||
// Mig 29 (Session 21 turn 5) — F2 per-Drafter: cho phép user này (khi đóng
|
||||
// vai Drafter) gửi PE thẳng Cấp cuối, skip mọi Bước/Cấp trung gian. Workspace
|
||||
// hiện checkbox "Gửi thẳng Cấp cuối" conditional theo flag này.
|
||||
//
|
||||
// Mặc định false (an toàn). Admin set ở User Management page. Backfill
|
||||
// Mig 29: bulk set TRUE cho user nào từng Drafter PE link workflow có
|
||||
// workflow.AllowDrafterSkipToFinal=true (preserve admin config S21 t4).
|
||||
public bool AllowDrafterSkipToFinal { get; set; }
|
||||
}
|
||||
|
||||
@ -19,14 +19,9 @@ public class ApprovalWorkflowConfiguration : IEntityTypeConfiguration<ApprovalWo
|
||||
e.HasIndex(x => new { x.Code, x.Version }).IsUnique();
|
||||
e.HasIndex(x => new { x.ApplicableType, x.IsActive });
|
||||
|
||||
// Mig 28 — 6 advanced options. 5 default false (admin opt-in). 1
|
||||
// AllowReturnToDrafter default true (backward compat S17 fallback).
|
||||
e.Property(x => x.AllowReturnOneLevel).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowReturnOneStep).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowReturnToAssignee).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowReturnToDrafter).HasDefaultValue(true);
|
||||
e.Property(x => x.AllowDrafterSkipToFinal).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowApproverEditDetails).HasDefaultValue(false);
|
||||
// Mig 28 cũ 6 column Allow* đã DROP trong Mig 29 (S21 t5) — refactor sang
|
||||
// per-NV (Level table cho F1+F3, Users table cho F2). Backfill bulk SQL
|
||||
// preserve config admin từ S21 t4 trước khi drop.
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,5 +69,13 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
|
||||
|
||||
e.HasIndex(x => new { x.ApprovalWorkflowStepId, x.Order });
|
||||
e.HasIndex(x => x.ApproverUserId);
|
||||
|
||||
// Mig 29 (S21 t5) — 5 per-NV advanced options. 4 default false (admin
|
||||
// opt-in). 1 AllowReturnToDrafter default true (backward compat S17).
|
||||
e.Property(x => x.AllowReturnOneLevel).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowReturnOneStep).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowReturnToAssignee).HasDefaultValue(false);
|
||||
e.Property(x => x.AllowReturnToDrafter).HasDefaultValue(true);
|
||||
e.Property(x => x.AllowApproverEditDetails).HasDefaultValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,196 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RefactorAdvancedOptionsToPerLevelAndDrafterUser : Migration
|
||||
{
|
||||
// Mig 29 (S21 t5) — Refactor Allow* options từ workflow-level (Mig 28
|
||||
// S21 t4) sang per-NV:
|
||||
// - F1 (4 mode Trả lại) + F3 (Edit Section 2) = 5 flag move xuống
|
||||
// ApprovalWorkflowLevels (per slot Approver).
|
||||
// - F2 (AllowDrafterSkipToFinal) move xuống Users (per-Drafter user).
|
||||
//
|
||||
// Migration order: ADD columns mới TRƯỚC → BACKFILL bulk SQL từ workflow
|
||||
// → DROP columns workflow-level. Preserve admin config S21 t4 (Mig 28).
|
||||
//
|
||||
// EF auto-generated order (drop-then-add) đã được reorder manual.
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// ===== Stage 1: ADD 5 column trên ApprovalWorkflowLevels (per slot) =====
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnOneLevel",
|
||||
table: "ApprovalWorkflowLevels",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnOneStep",
|
||||
table: "ApprovalWorkflowLevels",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnToAssignee",
|
||||
table: "ApprovalWorkflowLevels",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnToDrafter",
|
||||
table: "ApprovalWorkflowLevels",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowApproverEditDetails",
|
||||
table: "ApprovalWorkflowLevels",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
// ===== Stage 2: ADD 1 column trên Users (per-Drafter F2) =====
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowDrafterSkipToFinal",
|
||||
table: "Users",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
// ===== Stage 3: BACKFILL bulk từ workflow-level (Mig 28) =====
|
||||
// Copy 5 F1+F3 flag từ workflow → all Levels của workflow đó.
|
||||
// SQL Server compatible (UPDATE ... FROM ... JOIN ...).
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE l SET
|
||||
l.AllowReturnOneLevel = w.AllowReturnOneLevel,
|
||||
l.AllowReturnOneStep = w.AllowReturnOneStep,
|
||||
l.AllowReturnToAssignee = w.AllowReturnToAssignee,
|
||||
l.AllowReturnToDrafter = w.AllowReturnToDrafter,
|
||||
l.AllowApproverEditDetails = w.AllowApproverEditDetails
|
||||
FROM ApprovalWorkflowLevels l
|
||||
INNER JOIN ApprovalWorkflowSteps s ON s.Id = l.ApprovalWorkflowStepId
|
||||
INNER JOIN ApprovalWorkflows w ON w.Id = s.ApprovalWorkflowId;
|
||||
");
|
||||
|
||||
// Backfill Users.AllowDrafterSkipToFinal: set TRUE cho user nào
|
||||
// từng làm Drafter PE pin workflow có AllowDrafterSkipToFinal=true.
|
||||
// Conservative: preserve admin config Mig 28 cho user thực tế dùng,
|
||||
// các user khác giữ false (admin opt-in lần đầu).
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE u SET u.AllowDrafterSkipToFinal = 1
|
||||
FROM Users u
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM PurchaseEvaluations pe
|
||||
INNER JOIN ApprovalWorkflows w ON w.Id = pe.ApprovalWorkflowId
|
||||
WHERE pe.DrafterUserId = u.Id
|
||||
AND w.AllowDrafterSkipToFinal = 1
|
||||
);
|
||||
");
|
||||
|
||||
// ===== Stage 4: DROP 6 column workflow-level (Mig 28 cleanup) =====
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowApproverEditDetails",
|
||||
table: "ApprovalWorkflows");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowDrafterSkipToFinal",
|
||||
table: "ApprovalWorkflows");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnOneLevel",
|
||||
table: "ApprovalWorkflows");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnOneStep",
|
||||
table: "ApprovalWorkflows");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnToAssignee",
|
||||
table: "ApprovalWorkflows");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnToDrafter",
|
||||
table: "ApprovalWorkflows");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Rollback: re-add 6 column workflow-level + drop 5 Level + 1 User.
|
||||
// No reverse backfill (data loss accepted khi rollback).
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowApproverEditDetails",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowDrafterSkipToFinal",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnOneLevel",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnOneStep",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnToAssignee",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowReturnToDrafter",
|
||||
table: "ApprovalWorkflows",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowDrafterSkipToFinal",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowApproverEditDetails",
|
||||
table: "ApprovalWorkflowLevels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnOneLevel",
|
||||
table: "ApprovalWorkflowLevels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnOneStep",
|
||||
table: "ApprovalWorkflowLevels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnToAssignee",
|
||||
table: "ApprovalWorkflowLevels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowReturnToDrafter",
|
||||
table: "ApprovalWorkflowLevels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,36 +134,6 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Property<DateTime?>("ActivatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("AllowApproverEditDetails")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowDrafterSkipToFinal")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnOneLevel")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnOneStep")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnToAssignee")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnToDrafter")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<int>("ApplicableType")
|
||||
.HasColumnType("int");
|
||||
|
||||
@ -218,6 +188,31 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("AllowApproverEditDetails")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnOneLevel")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnOneStep")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnToAssignee")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowReturnToDrafter")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<Guid>("ApprovalWorkflowStepId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
@ -1945,6 +1940,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("AllowDrafterSkipToFinal")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("CanBypassReview")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Notifications;
|
||||
using SolutionErp.Application.PurchaseEvaluations.Services;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Identity;
|
||||
@ -111,19 +112,27 @@ public class PurchaseEvaluationWorkflowService(
|
||||
}
|
||||
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
|
||||
|
||||
// F2 (Mig 28 — S21 t4) — Drafter skip thẳng Cấp cuối. Workflow phải
|
||||
// AllowDrafterSkipToFinal=true. Set pointer = max Step + max Level.
|
||||
// Audit changelog ghi rõ "Drafter skip" để approver Cấp cuối biết.
|
||||
// F2 (Mig 29 — S21 t5) — Drafter skip thẳng Cấp cuối. Permission
|
||||
// check moved sang `User.AllowDrafterSkipToFinal` (per-Drafter user,
|
||||
// không còn workflow-level Mig 28).
|
||||
// Admin bypass user flag check.
|
||||
if (skipToFinal && evaluation.ApprovalWorkflowId is Guid skipAwId)
|
||||
{
|
||||
if (!isAdmin)
|
||||
{
|
||||
if (actorUserId is null)
|
||||
throw new ConflictException("skipToFinal yêu cầu authenticated user.");
|
||||
var drafterUser = await userManager.FindByIdAsync(actorUserId.Value.ToString())
|
||||
?? throw new ConflictException("User không tồn tại.");
|
||||
if (!drafterUser.AllowDrafterSkipToFinal)
|
||||
throw new ConflictException(
|
||||
$"User '{drafterUser.FullName}' không được phép gửi thẳng Cấp cuối. " +
|
||||
"Liên hệ Admin để cấp quyền ở User Management.");
|
||||
}
|
||||
var wfSkip = await db.ApprovalWorkflows
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == skipAwId, ct)
|
||||
?? throw new ConflictException("Workflow không tồn tại.");
|
||||
if (!wfSkip.AllowDrafterSkipToFinal)
|
||||
throw new ConflictException(
|
||||
"Workflow không bật mode 'Gửi thẳng Cấp cuối'. " +
|
||||
"Liên hệ Admin để config Designer.");
|
||||
var finalStep = wfSkip.Steps.OrderBy(s => s.Order).LastOrDefault()
|
||||
?? throw new ConflictException("Workflow chưa có Bước nào.");
|
||||
var finalLevelOrder = finalStep.Levels.OrderBy(l => l.Order).LastOrDefault()?.Order
|
||||
@ -189,18 +198,57 @@ public class PurchaseEvaluationWorkflowService(
|
||||
bool isAdmin,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Mode Drafter — Session 17 default (always allowed for backward compat,
|
||||
// workflow.AllowReturnToDrafter default true).
|
||||
// Mig 29 (S21 t5) refactor: Allow* flag đã move xuống ApprovalWorkflowLevel
|
||||
// (per-slot Approver). Cần load workflow Steps+Levels để lấy Level hiện
|
||||
// tại (curStepIdx + curLevel).
|
||||
if (evaluation.ApprovalWorkflowId is not Guid awId)
|
||||
{
|
||||
// Phiếu V1 legacy không có Allow* → fallback Drafter (S17 behavior).
|
||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
evaluation.SlaDeadline = null;
|
||||
return mode == WorkflowReturnMode.Drafter
|
||||
? "Trả về Người soạn thảo"
|
||||
: $"Trả về Người soạn thảo (fallback — phiếu V1 không hỗ trợ mode '{mode}')";
|
||||
}
|
||||
|
||||
var workflow = await db.ApprovalWorkflows
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == awId, ct)
|
||||
?? throw new ConflictException("Workflow không tồn tại.");
|
||||
|
||||
var stepsOrdered = workflow.Steps.OrderBy(s => s.Order).ToList();
|
||||
|
||||
// Resolve Level hiện tại (slot Approver đang duyệt) — đọc Allow* từ slot
|
||||
// này. Required cho mọi mode (kể cả Drafter — Approver hiện tại quyết
|
||||
// định mode Trả lại theo flag riêng của slot).
|
||||
ApprovalWorkflowLevel? currentLevel = null;
|
||||
if (evaluation.CurrentWorkflowStepIndex is int csi && csi >= 0 && csi < stepsOrdered.Count)
|
||||
{
|
||||
var step = stepsOrdered[csi];
|
||||
currentLevel = step.Levels.FirstOrDefault(l => l.Order == evaluation.CurrentApprovalLevelOrder);
|
||||
}
|
||||
|
||||
// Validate Allow* flag từ Level slot hiện tại (Admin bypass)
|
||||
if (!isAdmin && currentLevel is not null)
|
||||
{
|
||||
var allowed = mode switch
|
||||
{
|
||||
WorkflowReturnMode.OneLevel => currentLevel.AllowReturnOneLevel,
|
||||
WorkflowReturnMode.OneStep => currentLevel.AllowReturnOneStep,
|
||||
WorkflowReturnMode.Assignee => currentLevel.AllowReturnToAssignee,
|
||||
WorkflowReturnMode.Drafter => currentLevel.AllowReturnToDrafter,
|
||||
_ => false,
|
||||
};
|
||||
if (!allowed)
|
||||
throw new ConflictException(
|
||||
$"Cấp Approver hiện tại không bật mode '{mode}'. Liên hệ Admin Designer để config Level slot.");
|
||||
}
|
||||
|
||||
// Mode Drafter — Session 17 default (Phase=TraLai clear pointer)
|
||||
if (mode == WorkflowReturnMode.Drafter)
|
||||
{
|
||||
// Validate workflow flag (admin có thể disable mode này force peer review)
|
||||
if (evaluation.ApprovalWorkflowId is Guid awId0 && !isAdmin)
|
||||
{
|
||||
var wf0 = await db.ApprovalWorkflows.FirstOrDefaultAsync(w => w.Id == awId0, ct);
|
||||
if (wf0 is not null && !wf0.AllowReturnToDrafter)
|
||||
throw new ConflictException(
|
||||
"Workflow không bật mode 'Trả về Drafter'. Phải dùng mode khác.");
|
||||
}
|
||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
@ -208,38 +256,12 @@ public class PurchaseEvaluationWorkflowService(
|
||||
return "Trả về Người soạn thảo";
|
||||
}
|
||||
|
||||
// 3 mode còn lại (OneLevel / OneStep / Assignee) — yêu cầu V2 schema +
|
||||
// pointer hợp lệ.
|
||||
if (evaluation.ApprovalWorkflowId is not Guid awId)
|
||||
throw new ConflictException(
|
||||
$"Mode '{mode}' yêu cầu phiếu pin V2 workflow (ApprovalWorkflowId).");
|
||||
// 3 mode còn lại — yêu cầu pointer hợp lệ
|
||||
if (evaluation.CurrentWorkflowStepIndex is not int curStepIdx
|
||||
|| evaluation.CurrentApprovalLevelOrder is not int curLevel)
|
||||
throw new ConflictException(
|
||||
$"Mode '{mode}' yêu cầu phiếu đang ChoDuyet + pointer init. " +
|
||||
$"State hiện tại: Step={evaluation.CurrentWorkflowStepIndex}, Level={evaluation.CurrentApprovalLevelOrder}.");
|
||||
|
||||
var workflow = await db.ApprovalWorkflows
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == awId, ct)
|
||||
?? throw new ConflictException("Workflow không tồn tại.");
|
||||
|
||||
// Validate Allow* flag (Admin bypass — admin có thể trả lại bất chấp config)
|
||||
if (!isAdmin)
|
||||
{
|
||||
var allowed = mode switch
|
||||
{
|
||||
WorkflowReturnMode.OneLevel => workflow.AllowReturnOneLevel,
|
||||
WorkflowReturnMode.OneStep => workflow.AllowReturnOneStep,
|
||||
WorkflowReturnMode.Assignee => workflow.AllowReturnToAssignee,
|
||||
_ => false,
|
||||
};
|
||||
if (!allowed)
|
||||
throw new ConflictException(
|
||||
$"Workflow không bật mode '{mode}'. Liên hệ Admin Designer để config.");
|
||||
}
|
||||
|
||||
var stepsOrdered = workflow.Steps.OrderBy(s => s.Order).ToList();
|
||||
var summary = string.Empty;
|
||||
|
||||
switch (mode)
|
||||
|
||||
Reference in New Issue
Block a user