Files
solution-erp/docs/changelog/sessions/2026-05-13-1200-s21-turn4-pe-workflow-advanced-options.md
pqhuy1987 eea86fdfe7
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m31s
[CLAUDE] Docs: Chunk E — chốt Session 21 turn 4 F1+F2+F3 PE Workflow advanced options (Mig 28)
Update docs theo rule §6.5 KEEP narrative:

- `docs/database/schema-diagram.md §14` title "Mig 22-28, S17-21" + thêm 6
  column Allow* inline trong Core ApprovalWorkflows block (inline comment
  F1/F2/F3 mapping). Bonus: 3 bảng (Steps + Levels) unchanged.

- `docs/STATUS.md` Last updated S21 t4 + count 27→28 mig (Mig 28 advanced
  options 6 column). UAT defer test count unchanged 84 (test-after-uat candidate
  bundle Plan C carry).

- `docs/HANDOFF.md` Insert TL;DR S21 t4 đầy đủ (trước S21 t3):
  - Q&A clarify 2 lượt chốt scope (F1 cả 2 mode admin, F1 Assignee runtime,
    F2 chỉ Cấp cuối, F3 Section 2 only, F2+F3 admin tick, F3 mọi approver
    active, test-after UAT)
  - 5 chunk narrative đầy đủ A schema → B BE → C FE Admin → D FE eOffice → E Docs
  - Pattern reusable: backward-compat option flags, boundary helper extension
  - State table cumulative + pending Plan C test-after catch-up

- NEW session log `docs/changelog/sessions/2026-05-13-1200-s21-turn4-pe-workflow-advanced-options.md`:
  - Trigger + Q&A 2 lượt
  - 5 chunk narrative chi tiết với code snippets
  - Pattern reusable 5 lessons learned
  - References file paths + spec context

Stats cumulative S21 t4:
- 28 mig (+1) · 59 tables · ~143 endpoints (+1) · 34 FE pages · 84 test pass
  (UAT defer test-after §7) · 45 gotcha unchanged · 17 memory · 6 skills
- 5 commits S21 t4 cumulative ready push remote

Pending: bro confirm push `0a3b747..HEAD` 8 commits ahead (S21 t3 + S21 t4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:11:14 +07:00

14 KiB
Raw Blame History

Session 21 turn 4 — 2026-05-13 12:00 — PE Workflow advanced options (F1+F2+F3, Mig 28)

Dev: Claude Opus 4.7 1M Max (em main solo — 3 feature multi-layer Implementer REFUSE per cross-stack reasoning chain rule) Duration: ~3h (clarify Q&A 2 lượt + 5 chunk implement + verify build cả 2 app mỗi chunk) Base commit: 6d30ba4 (S21 t3 fix gotcha #45 Chunk C Docs) Commits này turn: 0294693 (A schema) → c56024b (B BE) → a508564 (C FE Admin) → d27caaf (D FE eOffice) → this (E Docs)

Trigger

User chốt 3 tính năng mới trong "Quy trình duyệt NCC":

  1. F1 — 4 mode Trả lại trong workflow (admin stick per workflow):

    • Cho trả về 1 bậc trước đó
    • Cho trả về người chỉ định
    • Trả về người soạn thảo
    • Workflow tick stick mode nào enabled → user eOffice dropdown chỉ hiện mode đó
  2. F2 — Drafter chọn "Gửi duyệt thẳng cấp" (vd skip → CEO):

    • "Các bước này đều ghi nhận vào quy trình duyệt phiếu"
  3. F3 — Approver chỉnh sửa phiếu (Section 2 chi tiết):

    • "lưu vào lịch sử chỉnh sửa luôn"

Clarify Q&A (2 lượt AskUserQuestion)

Lượt 1 — Schema + UX scope:

Câu User chốt
F1 "Trả về 1 bậc trước đó" nghĩa là gì? Cả 2 mode (admin chọn) — 1 Cấp + 1 Bước stick độc lập
F1 "Người chỉ định" nguồn từ đâu? Approver pick runtime — dropdown từ list NV đã ký (PeLevelOpinions)
F2 skip scope Chỉ skip tới Level cuối (CEO) — Dropdown 2 option
F3 edit scope Section 2 (Hạng mục + NCC + Báo giá) — KHÔNG đụng Header, KHÔNG reset workflow

Lượt 2 — Behavior + admin enable:

Câu User chốt
3 mode Trả lại behavior Giữ ChoDuyet, lùi pointer (peer review chain). Mode Drafter giữ Phase=TraLai S17
F2+F3 admin enable Cả 2 cần admin tick per workflow (audit nghiêm)
F3 approver perm Mọi approver Cấp đang active (currentLevel match)
Test timing Test-after UAT default Phase 9 (skip dotnet test mỗi chunk, npm build × 2 app)

Chunk A — Schema + Migration 28 (0294693)

Domain ApprovalWorkflow.cs +6 bool

public bool AllowReturnOneLevel { get; set; }        // F1 mode 1
public bool AllowReturnOneStep { get; set; }         // F1 mode 2
public bool AllowReturnToAssignee { get; set; }      // F1 mode 3
public bool AllowReturnToDrafter { get; set; } = true;  // F1 mode 4 (backward compat S17)
public bool AllowDrafterSkipToFinal { get; set; }    // F2
public bool AllowApproverEditDetails { get; set; }   // F3

EF config ApprovalWorkflowConfiguration

e.Property(x => x.AllowReturnOneLevel).HasDefaultValue(false);
// ... 4 more false ...
e.Property(x => x.AllowReturnToDrafter).HasDefaultValue(true);  // backfill rows cũ

Migration 28 AddAdvancedOptionsToApprovalWorkflows

  • 6 AddColumn bit NOT NULL DEFAULT 0/1
  • 3-file rule complete (mig.cs + Designer.cs + Snapshot.cs)
  • Apply LocalDB Dev + Design success

Chunk B — BE Service + handlers + DTOs (c56024b)

Service signature extend (backward compat)

public async Task TransitionAsync(
    PurchaseEvaluation evaluation,
    PurchaseEvaluationPhase targetPhase,
    Guid? actorUserId,
    IReadOnlyList<string> actorRoles,
    ApprovalDecision decision,
    string? comment,
    WorkflowReturnMode? returnMode = null,        // ← NEW
    Guid? returnTargetUserId = null,              // ← NEW
    bool skipToFinal = false,                     // ← NEW
    CancellationToken ct = default)

WorkflowReturnMode enum: OneLevel=1, OneStep=2, Assignee=3, Drafter=4.

REJECT branch extend với ApplyReturnModeAsync

// Inside REJECT branch (line 51+)
var effectiveMode = returnMode ?? WorkflowReturnMode.Drafter;
var returnSummary = await ApplyReturnModeAsync(
    evaluation, effectiveMode, returnTargetUserId, isAdmin, ct);
comment = $"{comment} [{returnSummary}]";

Helper ApplyReturnModeAsync switch 4 mode:

// OneLevel — lùi 1 Cấp trong cùng Step. Bước 1 Cấp 1 → fallback Drafter.
if (curLevel > 1) {
    evaluation.CurrentApprovalLevelOrder = curLevel - 1;
    summary = $"Trả về Cấp {curLevel - 1}";
}
else if (curStepIdx > 0) {
    var prevStep = stepsOrdered[curStepIdx - 1];
    evaluation.CurrentWorkflowStepIndex = curStepIdx - 1;
    evaluation.CurrentApprovalLevelOrder = prevStep.Levels.Max(l => l.Order);
    summary = $"Trả về Bước {prevStep.Order} Cấp {maxLevel} (Bước trước)";
}
else { /* fallback Drafter */ }

// OneStep — lùi sang Bước trước, set Level = max của Bước đó. Bước 1 → fallback.
// Assignee — tìm Step+Level match returnTargetUserId trong workflow.
// Drafter — Phase=TraLai clear pointer (S17 backward compat).

3 mode đầu giữ Phase=ChoDuyet + reset SLA 7d. Admin bypass workflow.Allow* flag check.

DRAFTER trình branch extend với F2

if (skipToFinal && evaluation.ApprovalWorkflowId is Guid skipAwId) {
    // Workflow.AllowDrafterSkipToFinal required (non-admin)
    if (!wfSkip.AllowDrafterSkipToFinal)
        throw new ConflictException("Workflow không bật mode 'Gửi thẳng Cấp cuối'.");
    evaluation.CurrentWorkflowStepIndex = wfSkip.Steps.Count - 1;  // 0-based last
    evaluation.CurrentApprovalLevelOrder = finalStep.Levels.Max(l => l.Order);
    comment = $"{comment} [Drafter gửi thẳng Cấp cuối — skip Bước/Cấp trung gian]";
}

Helper edit guard EnsureEditableForDetailsAsync (PurchaseEvaluationDraftGuard class)

public static async Task<PurchaseEvaluation> EnsureEditableForDetailsAsync(
    IApplicationDbContext db, Guid id, ICurrentUser currentUser, CancellationToken ct)
{
    var pe = await db.PurchaseEvaluations.FirstOrDefaultAsync(...);

    // Drafter scope — bypass current Controller [Authorize] handles role
    if (pe.Phase == DangSoanThao || pe.Phase == TraLai) return pe;

    // F3 Approver scope (Mig 28) — chỉ ChoDuyet
    if (pe.Phase == ChoDuyet && currentUser.UserId is Guid actorUserId) {
        if (currentUser.Roles.Contains(Admin)) return pe;  // bypass

        var workflow = await db.ApprovalWorkflows.Include(w => w.Steps)...
        if (!workflow.AllowApproverEditDetails)
            throw new ConflictException("Workflow không bật mode...");

        var level = step.Levels.FirstOrDefault(lv => lv.Order == levelOrder);
        if (level.ApproverUserId != actorUserId)
            throw new ForbiddenException("Chỉ NV phụ trách Bước X / Cấp Y...");

        return pe;
    }

    throw new ConflictException($"Phiếu PE ở Phase={pe.Phase}, không thể chỉnh sửa.");
}

8 handler switch + audit changelog

  • PurchaseEvaluationDetailFeatures.cs: Add/Update/Delete + Quote Upsert/Delete (5)
  • PurchaseEvaluationSupplierFeatures.cs: Add/Update/Remove (3) — bonus security fix: trước đây Supplier handlers HOÀN TOÀN KHÔNG có phase guard!

Update/Delete handlers trước đây silent → thêm changelog PhaseAtChange + UserId + Summary (append [Approver edit khi đang duyệt] khi phase=ChoDuyet).

Command DTO + DTOs extend

  • TransitionPurchaseEvaluationCommand +3 optional field + Validator
  • ApprovalWorkflowOptionsDto NEW sub-record (6 Allow* flag)
  • PurchaseEvaluationDetailBundleDto +WorkflowOptions field (null nếu V1 legacy)
  • AwDefinitionDto +6 Allow* (admin Designer GET)
  • CreateAwDefinitionCommand +6 Allow* param (admin Designer POST)

Verify

  • dotnet build SolutionErp.slnx → 0 err, 2 warn pre-existing DocxRenderer
  • 3 regression test gotcha #45 vẫn PASS (signature backward compat)

Chunk C — FE Admin Designer (a508564)

ApprovalWorkflowsV2Page.tsx Section "Cấu hình nâng cao" 6 checkbox

Container amber-50/30 + border distinct với Steps. 3 sub-group:

  1. Mode Trả lại (4 checkbox):

    • ☐ Trả về 1 Cấp trước (peer review chain trong cùng Bước)
    • ☐ Trả về 1 Bước trước (Cấp cuối Bước trước nhận lại)
    • ☐ Trả về Người chỉ định (pick runtime từ NV đã ký)
    • ☑ Trả về Người soạn thảo (default checked = backward compat S17)
  2. Drafter gửi duyệt (1 checkbox):

    • ☐ Cho phép Drafter gửi thẳng Cấp cuối
  3. Approver chỉnh sửa phiếu (1 checkbox):

    • ☐ Cho phép Approver chỉnh sửa Section 2 (Hạng mục + NCC + Báo giá)

DTO types + state defaults từ cloneFrom (giữ config version trước) hoặc S17 fallback. POST body propagate.

fe-user KHÔNG mirror (Designer admin-only).

Chunk D — FE eOffice (d27caaf) mirror 2 app

Types purchaseEvaluation.ts

export type ApprovalWorkflowOptions = {
  allowReturnOneLevel: boolean
  // ... 5 more
}

export const WorkflowReturnMode = {
  OneLevel: 1, OneStep: 2, Assignee: 3, Drafter: 4,
} as const

export type PeDetailBundle = {
  // ...
  workflowOptions: ApprovalWorkflowOptions | null
}

PeWorkflowPanel.tsx F1 Trả lại radio picker

State returnMode + returnTargetUserId. Dialog render 1-4 radio mode enabled theo wfOptions.Allow*. Assignee mode → submodal Select pick từ evaluation.levelOpinions (NV đã ký, dedupe by userId).

Banner amber rounded dưới mô tả hành vi mode chọn:

  • Drafter: "Phiếu sẽ về 'Trả lại'. Drafter có thể sửa rồi trình lại từ Cấp 1 Bước 1."
  • Assignee: "Phiếu sẽ về Cấp/Bước của NV đã chọn..."
  • OneLevel/OneStep: "Phiếu sẽ lùi pointer (vẫn 'Đã gửi duyệt')..."

Mutation payload +returnMode +returnTargetUserId khi isTraLaiAction.

PeDetailTabs.tsx F2 Drafter skip

State skipToFinal + allowSkipToFinal từ workflowOptions. submitForApproval mutationFn accept opts.skipToFinal. Workspace action bar: checkbox violet conditional. Confirm dialog message + button label dynamic.

PeDetailTabs.tsx F3 Approver edit Section 2

useAuth import + compute approverEditMode:

const approverEditMode = evaluation.phase === ChoDuyet
  && (evaluation.workflowOptions?.allowApproverEditDetails ?? false)
  && actorMatchesLevel  // isAdmin || actor.id in currentApproval.approvers
const itemsReadOnly = readOnly && !approverEditMode

Banner violet "ⓘ Bạn được phép chỉnh sửa..." khi approverEditMode + readOnly (Duyệt menu).

InfoTab / NccSelectorRow / BudgetFieldRow GIỮ strict isEditablePhase (Header + Section 3 KHÔNG trong F3 scope).

Verify

  • npm run build × 2 app pass (fe-user 7.52s + fe-admin 499ms cached)
  • 0 TS6 err, warning chunk size pre-existing

Chunk E — Docs (this commit)

  • docs/database/schema-diagram.md §14 title "Mig 22-28, S17-21" + thêm 6 column Allow* trong Core block với inline comment F1/F2/F3
  • docs/STATUS.md Last updated S21 t4 + count 27→28 mig + UAT defer test count unchanged
  • docs/HANDOFF.md TL;DR S21 t4 đầy đủ (5 chunk narrative + Q&A + pattern reusable)
  • Session log file này

Stats cumulative S21 t4

Metric Trước (S21 t3) Sau (S21 t4) Δ
DB tables 59 59 0
Migrations 27 28 +1 (Mig 28)
Endpoints ~142 ~143 +1 (transitions body extend)
FE pages 34 34 0 (Designer extend)
Unit tests 84 84 0 (UAT defer test-after §7)
Gotchas 45 45 0
Memory entries 17 17 0
Skills 6 6 0
Sub-agents 4 seeds-only 4 seeds-only 0
Commits S21 t4 5 0294693c56024ba508564d27caaf → this

Lessons learned

  1. Backward-compat option pattern. Thêm 6 cột mới với 1 cột default TRUE (AllowReturnToDrafter S17 fallback) + 5 cột default FALSE (admin opt-in) → workflow cũ chạy đúng sau deploy, không breaking change. Pattern reusable cho future feature flags.

  2. Boundary helper pattern (extension thay vì rewrite). Helper cũ EnsureDraftAsync strict DangSoanThao only → thêm helper mới EnsureEditableForDetailsAsync accept thêm Approver scope. KHÔNG rename + KHÔNG break cũ → coexistence safe. 8 handler switch cleanly.

  3. Multi-agent decision tree áp đúng. 3 feature multi-layer (Domain + Mig + Service + 8 handler + 2 FE app + 4 DTO file) — tightly coupled reasoning chain → Implementer REFUSE per Cognition "writes single-threaded". Em main solo decision đúng, KHÔNG split tránh agent thrash.

  4. Per-chunk discipline (5 chunk A-E) mặc dù big-feature multi-layer >1000 LOC. Mỗi chunk verify build pass trước commit → rollback dễ nếu fail. Audit trail commit history rõ ràng cho future debug.

  5. Test-after UAT default Phase 9 chấp nhận được khi feature nhánh enable mới (admin opt-in). Test-before BẮT BUỘC chỉ cho bug fix (gotcha #45 S21 t3 đã làm). Cảnh báo carry: Plan C test-after candidate cumulative ngày càng nhiều — cần dedicated commit "Test catch-up S21 t1-t4" sau UAT ổn.

Handoff

  • Chunk A 0294693 BE schema + Mig 28 committed local
  • Chunk B c56024b BE Service + handlers + DTOs committed local
  • Chunk C a508564 FE Admin Designer committed local
  • Chunk D d27caaf FE eOffice mirror 2 app committed local
  • Chunk E (this) — Docs commit sau khi save session log
  • PENDING bro confirm push remotegit push origin main 8 commit ahead 0a3b747..HEAD (S21 t3 fix gotcha #45 + S21 t4 F1+F2+F3)

User next action expected: UAT test 3 feature mới trong env Prod hoặc local. Sau UAT 2-3 lần ổn → Plan C bundle test-after catch up.

References

  • BE Service: src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs
  • BE entity: src/Backend/SolutionErp.Domain/ApprovalWorkflowsV2/ApprovalWorkflow.cs
  • BE handlers: src/Backend/SolutionErp.Application/PurchaseEvaluations/{PurchaseEvaluationFeatures, PurchaseEvaluationDetailFeatures, PurchaseEvaluationSupplierFeatures}.cs
  • Mig 28: src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260513114505_AddAdvancedOptionsToApprovalWorkflows.cs
  • FE Admin Designer: fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx
  • FE eOffice × 2 app: {fe-admin,fe-user}/src/components/pe/PeWorkflowPanel.tsx + PeDetailTabs.tsx + types/purchaseEvaluation.ts
  • Spec context: gotcha #45 S21 t3 BE guard payload mismatch + Session 17 spec 5 trạng thái
  • Rules: §3.9 mirror 2 FE, §6.5 KEEP narrative, §7 test timing, feedback_uat_skip_verify, feedback_per_chunk_commit