All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m31s
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>
317 lines
14 KiB
Markdown
317 lines
14 KiB
Markdown
# 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`
|