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

317 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```csharp
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`
```csharp
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)
```csharp
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`
```csharp
// 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:
```csharp
// 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
```csharp
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)
```csharp
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`
```typescript
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`:
```typescript
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** | `0294693``c56024b``a508564``d27caaf` → 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 remote**`git 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`