- Root cause: o "Gia tri thuc hien du kien con lai" (row 8 bang Tong hop ngan sach) khi gia tri NCC vuot ngan sach -> so du con lai ra AM; BE validator ExpectedRemainingAmount>=0 + FE VndInlineEdit khong bat allowNegative -> chan cung "am ko luu duoc" (testing bao qua anh Kiet)
- BE: AdjustPurchaseEvaluationBudgetCommandValidator GO rule ExpectedRemainingAmount.GreaterThanOrEqualTo(0) -> cho luu so am (mirror tien le LeaveBalance AllowsNegativeRemaining). GIU BudgetPeriodAmount>0 + submit-guard "da nhap NS ky nay" khong doi
- FE x2 app SHA256 identical: (a) allowNegative cho VndInlineEdit row 8; (b) banner amber "Vuot ngan sach - van luu & gui duyet duoc" trong PeBudgetSummaryTable khi cmpPeriod<0 || cmpFull<0. Tang to mau do cu GIU NGUYEN
- Spec change: flip test AdjustBudget_Validator_ExpectedRemainingNegative_FailsValidation -> _PassesValidation (am gio hop le); test BudgetPeriodZero_FailsValidation GIU (budget>0 van enforced)
- Build FE x2 PASS + test 263 PASS (45 Domain + 218 Infra, 0 fail/skip). Reviewer PASS 0 issue (row8 am an toan arithmetic additive-only, submit guard nguyen, mirror byte-identical, no scope creep)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: bang moi PeWorkItemBudgets (1 record/cap Du an x Hang muc, UNIQUE filtered [IsDeleted]=0) + drop 5 bang Budget cu + PE/Contracts drop BudgetId + backfill BudgetManualAmount->BudgetPeriodAmount TRUOC DropColumn (phieu UAT giu so) + DELETE menu/permission Bg_* IN-list children-first
- BE: PUT {id}/budget/pro (role Procurement) + {id}/budget/ccm (role CostControl, Adjustment cho phep AM) fail-closed Forbidden-truoc-side-effect + EnsureTrackedAsync race-safe (catch unique -> re-fetch winner, loi khac rethrow) + auto-create record khi tao phieu + budgetSummary DTO (luy ke trinh-truoc/chon-thau-truoc/de-xuat-ky-nay + full fallback du-tru-PRO + canEdit flags) + submit-guard (3) doi predicate BudgetPeriodAmount -> "chua nhap Ngan sach ky nay" + PATCH budget-adjust absolute-set 2 field moi + Contract GIU BudgetManual* (HD nhap tay khong doi) + ke thua HD map BudgetPeriodAmount
- FE x2 app SHA256 identical: bang "TONG HOP NGAN SACH TRINH KY" block A (full dam + ban hanh + V0 hieu chinh + du tru PRO + ghi chu, editable theo canEditPro/canEditCcm) + block B 9 dong cong thuc Excel (5=1+3, 6=2+4, 7=full-5, 8 tu nhap default 7, 9=4+8) + to mau vuot ngan sach #C00000 / am do / red-soft row8>row7 + "Chua chon" khi count=0 + banner phieu chua gan Hang muc + o "Ngan sach ky nay" o create/header + XOA pages/components/types budgets + routes + menuKeys + Layout staticMap 4-place
- Tests: +22 PeWorkItemBudgetTests (auto-create x3, ensure/race x2, authz matrix PRO x5 + CCM x3, budgetSummary aggregates x5, adjust x4) - 14 BudgetPolicyTests xoa theo module - 1 test via-BudgetId -> 263 PASS (45 Domain + 218 Infra, 0 fail)
- database-agent advise adopted: khong FK vat ly PE/Contracts->Budgets (DropColumn khong can DropForeignKey) + DropIndex truoc DropColumn (SQL 5074) + IN-list thay LIKE Bg_% (underscore wildcard + miss root) + khong Serializable wrap (nested-tx conflict codegen)
- Reviewer PASS-with-minor 0 blocker (verdict-first survived); 2 minor da sua truoc commit (comment adjustMut absolute-set + dead key budgetId); note: F4 approver-edit-budget UI entry tam drafter-only, BE van cho approver scope - cho UAT anh Kiet
- Scaffold-bug caught: EF tu sinh RenameColumn BudgetManualAmount->ExpectedRemainingAmount (SAI semantics) -> thay bang Add+UPDATE+Drop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Domain policy: xoa MOI transition -> TuChoi o ca 4 policy (NccOnly + NccWithPlan + ForV2Schema + FromDefinition) -> NextPhases het tra TuChoi, nut FE tu bien mat
- Service guard S60: chan targetPhase=TuChoi moi caller ke ca Admin (dung truoc moi branch — spec bo han, khong escape hatch); message huong dan dung Tra lai / Xoa nhap
- FE x2 app: filter phong thu next.filter(p != TuChoi) PeWorkflowPanel (SHA256 identical); dialog/isCancel giu dead-safe de flip lai de
- Enum TuChoi + phieu TuChoi cu + tab filter "Tu choi" GIU display (data cu render binh thuong)
- SlaExpiryJob chi dung Contract — PE khong auto-TuChoi, khong anh huong
- Tests spec-change cung commit: Domain flip BothPolicies_TuChoi_RemovedFromAllTransitions_S60 + NEW V2SchemaPolicy fact; Infra NEW TargetTuChoi_WithRejectDecision_Throws_TuChoiRemoved_S60 (guard #45 test cu giu nguyen PASS — van dung truoc)
- Test 254 -> 256 PASS (59 Domain + 197 Infra)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rename muc 3: "Chon NCC / TP thang thau" -> "Don vi NCC/TP duoc chon" (anh Kiet chot chu) x2 app + wording phu nhat quan
- Guard gui duyet du CA 4 (anh chot): don vi duoc chon + gia chao thau >0 + ngan sach (Budget link HOAC nhap tay) + bang so sanh dinh kem
+ BE ConflictException gop moi muc thieu 1 lan, ap ca Admin (TransitionAsync submit branch)
+ FE pre-check missingForApproval cung predicate -> disable nut + tooltip liet ke du (computeGiaChaoThau extract single-source)
- Bypass drafter-in-chain (luat GENERIC theo cap, anh chot): V2-only, BUOC DAU only - nguoi soan la approver cap k -> auto qua Cap 1..k khi gui
+ Audit 3 tang: Approval row AutoApprove per cap + LevelOpinion CHI slot chinh chu (khong gan chu ky NV bi skip) + Changelog
+ Pointer: k<max -> Cap k+1; het buoc -> Buoc 2 Cap 1; workflow 1 buoc -> terminal DaDuyet
+ TraLai resubmit ap lai idempotent (opinion UPSERT)
- Tests: +14 PeSubmitGuardAndBypassTests (240 -> 254 PASS)
- Reviewer die mid-run (gotcha #53 class) -> em main self-gate evidence-checklist PASS 0 blocker
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 1 phieu = 1 hang muc chon tu header (S57bis/S58, hang muc dau auto-seed khi tao
phieu) -> nut them hang muc thu 2+ sai mo hinh nghiep vu, go khoi ItemsTab header.
- AddItemDialog giu (dead code) de flip lai de neu doi y.
- SHA256 mirror x2 app IDENTICAL, build x2 PASS.
- PeDetailTabs Section 5 Dieu chinh ngan sach: bo input "Ten (khong bat buoc)"
(user khong hieu "y nghia du phong la gi") - manual budget chi con So tien (VND).
State manualName drop, payload budgetManualName: null. Ten cu phieu truoc van
hien read-only, ve null khi Luu dieu chinh lan toi.
- PeHeaderForm: payload budgetManualName null + hasManual detect theo CA amount
(phieu moi name=null sau khi bo o Ten -> van nhan dung manual mode).
- PeWorkspaceCreateView: khong doi (chua tung co o Ten, payload '' || null = null san).
- SHA256 mirror x2 app IDENTICAL, build tsc+vite x2 PASS.
- PeWorkflowPanel x2: nguoi duyet == nguoi soan (drafterUserId == currentUser.id)
-> an ca "Tra lai" + "Tu choi" (anh chot: tra cho chinh minh vo nghia, huy phieu
= nho cap khac tu choi / xoa phieu Nhap).
- SuppliersController: POST tao NCC mo cho moi user dang nhap (anh chot - nghiep vu
di thau phat sinh NTP moi lien tuc); PUT/DELETE van khoa Admin+CatalogManager (S57).
- PeDetailTabs AddSupplierDialog x2: Select -> SearchableSelect (go-tim bo dau,
sort A-Z theo ma) + nut "+ NCC moi" quick-create (Ma/Ten/Loai/SDT/Email) ->
POST /suppliers -> auto-select vao phieu.
- Upload file bao gia + bang so sanh x2: input multiple + upload tuan tu tung file
(UAT "moi lan chi chon duoc 1 file").
- SHA256 mirror x2 app, build tsc+vite x2 PASS, BE 0 err, test 240/240 PASS local.
- NEW ui/SearchableSelect: combobox tu render, go loc theo label, match BO DAU
tieng Viet (go "be tong" trung "Be tong"), keyboard arrows/Enter/Esc, clear (x),
style mirror ui/Input density S55. Khong them lib ngoai.
- PeWorkspaceCreateView + PeHeaderForm: Hang muc + Du an doi Select -> SearchableSelect
(UAT: "nen co loc de tu danh chu" / "nen co tu go chu" - 70+ muc kho do).
- Auto dia diem (UAT "dia chi nen tu auto"): chon Du an tu dien diaDiem tu
Project.Location (S55), chi ghi de khi user chua go tay (track lastAutoLoc ref).
- Dieu khoan thanh toan nhap tay: Input 1 dong -> Textarea 3 dong (UAT "khong cho
xuong dong?") o CreateView + PeDetailTabs inline-edit; render detail da pre-wrap san.
- SHA256 mirror x2 app (4 file IDENTICAL), build tsc+vite x2 PASS.
Bro UAT 2026-05-19 post-Plan AE: phiếu cũ entries vẫn show "Hệ thống" thay
vì user name. Plan AE chỉ forward fix — entries CŨ pre-deploy có
userName="" empty, FE fallback "Hệ thống".
Fix Plan AF — Option A bro chốt (FE fallback lookup, no DB write):
ApprovalsTab + HistoryTab build userMap useMemo từ PeDetailBundle data
có sẵn (KHÔNG cần extra fetch /api/users admin permission):
- ev.drafterUserId + ev.drafterName
- ev.approvals[].approverUserId + approverName
- ev.approvalFlow.steps[].levels[].approvers[].userId + fullName
- ev.levelOpinions[].signedByUserId + signedByFullName
- ev.departmentOpinions[].userId + userName
resolveUserName / resolveActorName helper:
1. Trust entry.userName nếu non-empty
2. Lookup userMap qua entry.userId
3. Fallback 'Hệ thống' nếu không match
Cover gần hết users tham gia phiếu (drafter + approver + signer). Edge
case: user edit phiếu nhưng KHÔNG xuất hiện trong workflow → vẫn fallback.
Pattern reusable: synthetic data recovery cho audit trail từ embedded
domain data sources, no extra API contract change.
Mirror 2 app §3.9 identical logic.
Verify:
- npm build × fe-user PASS 0 TS err (9.12s)
- npm build × fe-admin PASS 0 TS err (8.91s)
- BE unchanged from 9ea62be
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bro UAT 2026-05-19 phản hồi sau Plan AC deploy: phiếu cũ PE/2026/A/032 vẫn KHÔNG
show events Trả lại pre-deploy (Bro test trả lại Phan Văn Chương → Trà từ TRƯỚC
cdfd542 không có trong Lịch sử duyệt).
Root cause: Plan AC chỉ add Approval row cho events POST-deploy. Events
pre-deploy chỉ có Changelog (LogTransitionAsync) — Approval table miss.
Fix Plan AC2 — FE merge view (Option 2A bro chọn):
ApprovalsTab fetch BOTH approvals + changelogs (cùng endpoint HistoryTab dùng):
- Reconstruct synthetic PeApproval rows từ Changelog Workflow+Reject events:
- Filter: entityType=Workflow(5) + summary "→ TraLai"/"→ TuChoi" OR
contextNote chứa "Trả về"/"không lùi được" (3 mode OneLevel/OneStep/Assignee
giữ ChoDuyet → distinguish qua ContextNote keywords)
- Parse fromPhase/toPhase từ summary regex "Chuyển phase X → Y"
- id prefix "syn-" để distinct vs real Approval rows
- Dedupe synthetic vs real Reject Approval (post-Plan AC) qua
approverUserId + timestamp 5s bucket key
- Merge approvals + dedupedSynthetic → sort by approvedAt → render
Reversible: KHÔNG touch DB, KHÔNG migration. FE-only fix recover history
cho mọi PE cũ trước deploy.
Mirror 2 app §3.9 identical logic.
Verify:
- npm build × fe-user + fe-admin PASS 0 TS err
- BE/test unchanged from a734bf2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bro UAT 2026-05-19 screenshot: panel "Lịch sử duyệt" KHÔNG show Return mode
events (Bro Trả lại từ Phan Văn Chương → Trà missing) + KHÔNG distinct
event Duyệt vượt cấp (skipToFinal F2).
Root cause:
- PurchaseEvaluationApprovals.Add() chỉ ở Approve branch (line 472 V2 + 660 V1)
- Reject branch line 75-103 NEVER adds Approval row — chỉ log Changelog
- skipToFinal advance branch line 532-572 dùng existing line 472 row nhưng
comment KHÔNG distinct "vượt cấp" semantic vs approve thường
Fix Plan AC:
1. BE Service.cs Reject branch (line 75-103): capture pre-call Step/Level
trước ApplyReturnModeAsync mutate pointer, add Approval row sau khi mutate:
Decision=Reject + FromPhase + ToPhase=evaluation.Phase + Comment carry
from-position + mode summary. Cover cả Trả lại (TraLai+pointer-mode) +
Từ chối (TuChoi terminal).
2. BE Service.cs line 472 Approve branch: enrich Comment với prefix
"[Duyệt vượt cấp tới Cấp cuối]" khi skipToFinal=true để Lịch sử duyệt
distinguish vượt cấp với approve thường.
3. FE PeDetailTabs.tsx × 2 app ApprovalsTab: add Decision badge phân biệt
Approve (emerald) / Trả lại (amber) / Từ chối (rose). Vì 3/4 mode Trả
lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase→toPhase
badge giống Approve. Decision badge bù visual phân biệt.
Verify:
- dotnet build clean 0 err 2 warn (pre-existing DocxRenderer)
- dotnet test 111/111 PASS
- npm build × fe-user + fe-admin PASS 0 TS err
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bro UAT screenshot 2026-05-15 sau Plan O+P deploy: PE/2026/A/025 Phase=ChoDuyet
actor NV Test có F3 AllowApproverEditDetails=TRUE — banner violet "Bạn được
phép chỉnh sửa Hạng mục / NCC / Báo giá" render ĐÚNG nhưng layout:
```
[Section padding px-5 = 20px]
[Banner mx-5 inset 20px both sides] ← gap 20px right edge
[ItemsTab header flex justify-between]
[text "1 hạng mục..."] [Button "+ Thêm hạng mục"]
```
Banner mx-5 đẩy inset 20px khỏi Section padding x-5 → tạo gap visual 20px
bên phải banner. Phía dưới gap đó là button right-aligned (full Section
width) → trông button "lệch" so với banner end + có khoảng trắng phía trên.
Fix mirror 2 app (rule §3.9):
```diff
- <div className="mx-5 mt-2 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[11px] text-violet-800">
+ <div className="mb-3 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[11px] text-violet-800">
```
- `mx-5` → drop (banner full Section padding width)
- `mt-2` → `mb-3` (consistent spacing với ItemsTab header `mb-3` style)
Visual sau fix:
```
[Section padding px-5]
[Banner full width]
[ItemsTab header: text + button align Section right edge]
```
Button "+ Thêm hạng mục" align cùng phải edge với banner. KHÔNG còn gap visual.
Files (2 mirror):
- fe-user/src/components/pe/PeDetailTabs.tsx:218-223
- fe-admin/src/components/pe/PeDetailTabs.tsx:213-218
Verify:
- npm run build fe-user PASS clean (0 TS err, 7.67s)
- npm run build fe-admin PASS clean (0 TS err, 7.50s)
KHÔNG đụng BE. KHÔNG đụng logic. CSS layout polish only.
Pending: bro UAT verify layout fix + Plan P CICD Monitor verify F1+F2 wire
(spawn earlier, vẫn running).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bro UAT S23 t2 catch screenshot: tick AllowReturnToAssignee + untick AllowReturnToDrafter
cho slot Approver → user click "Trả lại" → dialog mở với default state `returnMode=Drafter`
(S17 backward compat fallback). Radio Drafter HIDDEN vì allowReturnToDrafter=false
→ user thấy radio Assignee đã pick + Bùi Lê Thủy Trà từ dropdown → click Xác nhận →
BE receive `returnMode: 4` (Drafter từ initial state) → throw "Cấp Approver hiện tại
không bật mode 'Drafter'. Liên hệ Admin Designer".
Bro intent: "cho duyệt trong muốn cho trả lại trong mode đang gửi duyệt chứ ko phải
draft, draft chỉ khi trả lại cho người soạn thôi" — 3 F1 modes (OneLevel/OneStep/
Assignee) là "trả lại trong mode đang gửi duyệt" (Phase=ChoDuyet lùi pointer);
Drafter mode = trả về Người soạn (Phase=TraLai), CHỈ default khi không có F1 nào.
Fix FE × 2 app PeWorkflowPanel.tsx (mirror rule §3.9):
- Import useEffect
- useEffect khi target=TraLai → compute first available F1 mode:
- allowReturnOneLevel ? OneLevel
- : allowReturnOneStep ? OneStep
- : allowReturnToAssignee ? Assignee
- : Drafter (fallback)
- setReturnMode(firstAvailable)
→ Dialog mở với mode đúng selected → user click Xác nhận → BE receive correct
mode → ApplyReturnModeAsync check correct flag → PASS.
Pattern lesson saved: dialog initial state phải compute từ permission flags
KHÔNG hardcode default — admin có thể disable mọi mode khác Drafter, hoặc
ngược lại enable F1 only.
Verify:
- npm run build × 2 app pass (0 TS err)
- Bundle hash rotate × 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bro UAT S23 t2 catch: Plan K K2 implement F2 SAI semantic — set
Phase=DaDuyet terminal auto-approve. Bro intent: "Duyệt thẳng đến CEO,
bỏ qua các bước khác chứ ko phải chuyển sang đã duyệt."
Refactor Service.cs ApproveV2Async F2 branch:
- Resolve lastStepIdx = steps.Count - 1, lastLevelMaxOrder = max(LevelOrder)
trong Step cuối
- Advance pointer: CurrentWorkflowStepIndex = lastStepIdx + CurrentApprovalLevelOrder = lastLevelMaxOrder
- Phase GIỮ NGUYÊN ChoDuyet — NV cuối (CEO/last approver) vẫn cần ký thật
để tiến DaDuyet
- Audit log "Approver skip thẳng tới Bước X Cấp Y (NV cuối) — bỏ qua các Bước/Cấp trung gian"
- Guard no-op: actor đã ở slot cuối → fall through advance logic (normal → DaDuyet)
(KHÔNG double-advance khi skipToFinal=true ngay slot cuối)
- Reset SLA 7d cho NV cuối nhận lại
FE × 2 app PeWorkflowPanel.tsx (mirror rule §3.9):
- Description text update: "Phiếu sẽ skip tới NV cuối (CEO/cấp ký cuối) —
NV cuối vẫn cần duyệt thật để hoàn tất."
- Amber warning update: "Bỏ qua mọi Cấp/Bước trung gian, phiếu chuyển thẳng
tới NV cuối. NV cuối vẫn phải ký duyệt thật để phiếu thành 'Đã duyệt'."
Verify:
- dotnet build production projects clean (0 err, 2 pre-existing warn)
- npm run build × 2 app pass
Pattern lesson saved memory: Service skipToFinal semantic = advance pointer
NOT terminate. K7 tests TODO update: 3 Approver F2 tests assert pointer
moved to last slot, NOT Phase=DaDuyet. Defer test fix sau UAT confirm UX.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan K Chunk E mirror 2 app rule §3.9. Refactor F2 UX flow:
DROP fe-admin + fe-user Workspace Drafter checkbox:
- PeDetailTabs.tsx Workspace action bar: REMOVE "Gửi thẳng Cấp cuối (skip trung gian)"
violet label + state skipToFinal + allowSkipToFinal lookup + skipToFinal payload
- submitForApproval mutation signature simplify: opts: { skipToFinal: boolean } → void
- Confirm dialog text + button label drop skipToFinal conditional
ADD fe-admin + fe-user Approver toggle trong PeWorkflowPanel dialog:
- State skipToFinalApprover default false
- Visible khi Approve forward (NOT Cancel + NOT SendBack) + currentLevelOptions?.allowApproverSkipToFinal
- Checkbox violet panel với description "Phiếu sẽ tiến thẳng tới Đã duyệt (terminal)"
- Amber warning khi checked: "Hành động KHÔNG quay lại được"
- Mutation payload +skipToFinal: !isReject && skipToFinalApprover
- onSuccess reset state
Type ApprovalWorkflowOptions × 2 app: +allowApproverSkipToFinal: boolean (7th)
Type PeDetailBundle × 2 app: REMOVE drafterAllowSkipToFinal field + comment Mig 29+30+31
UX design Dialog approach (consistent với Trả lại Mode picker pattern):
- Skip thẳng Cấp cuối = destructive action → confirm dialog amber warning
- Mirror Mig 28 Trả lại 4 mode picker UX consistency
- Em main solo K6 per UX flow decision criteria
Per bro decision Plan K S23 t1: "Chỗ cấu hình cho phép skip → duyệt thẳng cho phép
trong trạng thái đang duyệt" + "Tất cả đều cấu hình ngay trong chỗ setup quy trình duyệt".
Verify:
- npm run build × 2 app pass clean (0 TS err)
- Pre-existing warnings unchanged (chunk size + INEFFECTIVE_DYNAMIC_IMPORT)
- Bundle hash rotated × 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User UAT feedback: "Nếu đã không được quyền thao tác thì ko được quyền thao tác
hết tất cả các hành động" — trước đây chỉ "Duyệt" disabled, "Trả lại" + "Từ chối"
vẫn enabled (design intent S17 cũ).
FE 2 app mirror (PeWorkflowPanel.tsx):
- `isDisabled = blockedByV2Level` (drop `isForwardApprove &&` qualifier)
- Tooltip update "mới thao tác được (Duyệt / Trả lại / Từ chối)"
- Comment refresh ghi UAT S22+1 spec + cross-ref BE EnsureCanRejectV2Async
BE defense-in-depth (PurchaseEvaluationWorkflowService.cs):
- Helper mới `EnsureCanRejectV2Async` mirror FE actorInV2Level logic:
Skip silent khi admin/V1/non-ChoDuyet/no actor/no pointer. Throw
ForbiddenException khi V2 + ChoDuyet + actor != currentLevel.ApproverUserId.
- Invoke ở top Reject branch (cover cả TuChoi + Trả lại sub-branches).
- Chặn request forge: non-approver gọi PATCH /transitions direct sẽ 403.
Test (test-before §7 — security guard critical algorithm):
- ReturnMode tests existing 7/7 vẫn PASS (a2.Id = currentLevel approver, guard accept)
- +1 NEW test `Reject_NonApprover_V2_Throws_ForbiddenException` — outsider
Drafter role gọi Reject phiếu V2 → throw + Phase không mutate
Verify:
- dotnet test SolutionErp.slnx — 104/104 PASS (+1 guard regression)
Δ: 103 → 104
- npm run build × 2 app — pass (482ms + 583ms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Types (fe-{admin,user}/src/types/purchaseEvaluation.ts):
- ApprovalWorkflowOptions type (6 boolean Allow* flag)
- WorkflowReturnMode const-object {OneLevel,OneStep,Assignee,Drafter}
- PeDetailBundle +workflowOptions field (null nếu V1 legacy)
PeWorkflowPanel.tsx F1 (mirror 2 app):
- State returnMode + returnTargetUserId thêm vào transition mutation payload
- Dialog Trả lại render radio list 1-4 mode enabled theo workflowOptions:
• Trả về 1 Cấp trước (lùi pointer trong cùng Bước, peer review)
• 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 từ dropdown NV đã ký levelOpinions)
• Trả về Người soạn thảo (default Drafter S17 fallback)
- Banner amber rounded box dưới radio list mô tả hành vi mode chọn
- onSuccess reset returnMode về Drafter + returnTargetUserId null
PeDetailTabs.tsx F2 (mirror 2 app):
- State skipToFinal + allowSkipToFinal (từ workflowOptions)
- submitForApproval mutationFn accept opts.skipToFinal → POST body
- Workspace action bar: thêm checkbox violet "Gửi thẳng Cấp cuối (skip trung gian)"
hiển thị conditional theo allowSkipToFinal + canSubmitForApproval
- Confirm dialog message dynamic: "Gửi thẳng" warning vs default tuần tự
- Button label dynamic: "Lưu & Gửi thẳng CẤP CUỐI →" vs "Lưu & Gửi Duyệt →"
PeDetailTabs.tsx F3 (mirror 2 app):
- useAuth import + compute approverEditMode (phase=ChoDuyet +
workflow.AllowApproverEditDetails + actor match currentApproval.approvers)
- itemsReadOnly = readOnly && !approverEditMode → ItemsTab nhận
- Banner violet "ⓘ Bạn được phép chỉnh sửa Hạng mục/NCC/Báo giá" khi
approverEditMode + readOnly (Duyệt menu) — UX nhắc về quyền extended
InfoTab + NccSelectorRow + BudgetFieldRow GIỮ strict isEditablePhase (KHÔNG
trong F3 scope — Header section + Section 3 winner KHÔNG cho Approver edit).
Verify:
- npm run build × 2 app pass (fe-user 7.52s, fe-admin 499ms cached)
- 0 TS6 err, warning chunk size pre-existing
- BE Chunk B đã accept skipToFinal + returnMode + returnTargetUserId trong
TransitionPurchaseEvaluationCommand → wire E2E complete
Pending Chunk E: Docs schema-diagram §14 update + STATUS + HANDOFF + session log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User Session 20 turn 10: chọn NCC từ dropdown master → auto-load các field
đã có sẵn (contactPerson/phone/email/note) vào form, đỡ phải nhập tay lại.
FE-only mirror fe-admin + fe-user.
AddSupplierDialog dropdown "NCC (master)" onChange:
- Lookup suppliers.data find(s => s.id === selectedId) → master row
- setForm prev → ghi đè 4 field:
* contactName ← picked.contactPerson ?? ''
* contactPhone ← picked.phone ?? ''
* contactEmail ← picked.email ?? ''
* note ← picked.note ?? ''
- KHÔNG đụng displayName / paymentTermText / thanhTien (manual cho user)
- Hint "✓ Đã tự điền từ Master — bạn có thể sửa lại nếu cần." text-[10px]
text-emerald-600 dưới dropdown khi đã chọn supplier
Mapping master Supplier → PE.Supplier fields (skip address vì không có
field tương ứng — có thể nhét vào note nếu user cần, manual).
User vẫn override các field auto-fill được sau đó (input bình thường).
Đổi supplier giữa lúc đã chỉnh tay → re-fill từ master mới (mặc định ghi đè).
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User Session 20 turn 6 screenshot: chế độ "Nhập tay (không link)" Section 2
b. Ngân sách vẫn còn input "Tên (vd Tạm tính T11/2025)" cùng số tiền. User
chỉ cần nhập số tiền — bỏ Tên + áp VND format consistent.
3 file × 2 app = 6 file FE update:
- PeDetailTabs.tsx BudgetFieldRow (Section 2 detail editor)
- PeWorkspaceCreateView.tsx (workspace mode "new")
- PeHeaderForm.tsx (Create/Edit header page)
Mỗi file:
- Drop Input "Tên ngân sách" UI khỏi manual mode (state field giữ '' để
backward compat — BE save luôn null)
- Manual mode UI giờ chỉ 1 input số tiền (max-w-xs):
* type="text" inputMode="numeric" + value={formatVndInput(amount)}
* onChange={parseVnd} strip non-digit → number
* Suffix "đ" tuyệt đối inset-y-0 right-3
* Hint "VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)"
- Helpers parseVnd + formatVndInput inline mỗi file (mirror PeDetailTabs)
PeDetailTabs BudgetFieldRow cleanup:
- Drop state manualName + setManualName
- Drop manualName từ dirty check
- Save payload: budgetManualName: null luôn (không phụ thuộc state)
- Hủy thay đổi: drop reset manualName line
Read-only display (legacy data) giữ ev.budgetManualName nếu data cũ có tên
(đoạn render khi !canEdit) — không xóa hiển thị, chỉ ẩn input UI.
BE schema KHÔNG đụng — endpoint PUT /pe/:id vẫn nhận budgetManualName field,
chỉ FE luôn gửi null.
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User Session 20 turn 3: "Tạm thời chỉ cần nhập số tiền vào là đc, không cần
3 cột có VAT / ko VAT / tổng. 2 cột kia ẩn đi, chỉ 1 cột nhập tiền duy nhất."
FE-only mirror fe-admin + fe-user:
NCC inline table HangMucCard — bỏ 2 th + 2 td:
Trước: NCC | Liên hệ | Điều khoản TT | File báo giá | ĐG chưa VAT | ĐG có VAT | Thành tiền | Action
Sau: NCC | Liên hệ | Điều khoản TT | File báo giá | Số tiền | Action
QuoteDialog — đơn giản hóa form:
Trước: 3 input (Đơn giá chưa VAT / ĐG có VAT / Thành tiền auto-calc) + Ghi chú
+ display khoiLuong info
Sau: 1 input "Số tiền" (autoFocus) — map thẳng vào thanhTien field
Schema BE giữ nguyên (bgVat / chuaVat / note vẫn POST):
- Row mới: bgVat=0, chuaVat=0, note=''
- Existing: giữ giá trị cũ
Bỏ prop khoiLuong (không dùng — không còn auto-calc thanhTien = chuaVat × khoiLuong)
Bỏ updateAndRecalc helper
KHÔNG đụng schema BE — endpoint POST /purchase-evaluations/{id}/quotes giữ
nguyên payload shape, chỉ FE rút gọn input mặt người dùng nhập.
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback Session 20 turn 2:
1. "Chỗ ý kiến vẫn hiển thị ô vuông như trước nhé" — revert visual về cards
grid-cols-2 mirror S19 (Chunk C cũ dùng vertical list inline không phải
ô vuông như trước).
2. "Số bước duyệt khác số người duyệt trong 1 bước, check lại" — counter cũ
`{opinions.length}/{totalApprovers}` sai semantic vì OR-of-N (mỗi Cấp chỉ
cần 1 NV ký, không cần ký tất cả NV). totalApprovers đếm tổng NV gây hiểu
lầm.
Fix (FE-only mirror fe-admin + fe-user):
- StepOpinionsBox body chuyển từ `space-y-2` (vertical list) sang
`grid grid-cols-1 md:grid-cols-2 gap-3` — mỗi opinion = 1 card đầy đủ
border-emerald-200 + bg-white + p-3 (mirror visual S19 LevelOpinionBox).
- StepOpinionEntry restore styling đầy đủ:
- Header: "Cấp N — Tên NV" font-semibold + admin override badge amber +
"✓ Đã duyệt" emerald rounded-full badge
- Body: comment text-sm
- Footer: signedAt border-t separator (như S19)
- Counter mới: `{signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers}
NV tham gia` — đếm Cấp distinct (Set unique levelOrder) thay vì count NV.
Tooltip giải thích "OR-of-N" cho user hiểu.
- KHÔNG đụng schema Mig 26 (vẫn UPSERT 1 row / Level qua Service).
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
- Test pass mặc định skip (Q4 UAT iteration)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure Section 5 (rename Section 4 sau Chunk B) "Ý kiến cấp duyệt".
User Session 20 Q3=a: gộp các comment đồng cấp cùng Phòng → 1 ô / bước
(dù bước có nhiều người), CHỈ hiển thị comment của NV đã duyệt.
Trước (Mig 26 S19 LevelOpinionsSectionV2):
forEach step → grid-cols-2 cho forEach Level × forEach Approver → 1 box / NV
Hiển thị cả NV chưa duyệt với placeholder "— chưa duyệt"
Sau (Chunk C):
forEach step → 1 StepOpinionsBox (đại diện Phòng)
Box body: filter opinions có stepOrder == step.order
→ sort theo levelOrder asc, signedAt asc
→ render StepOpinionEntry per signed opinion
NV chưa duyệt KHÔNG hiển thị
Header box: "Bước N — Tên · {dept badge} · X/Y đã duyệt"
FE (mirror fe-admin + fe-user):
- LevelOpinionsSectionV2 forEach step → StepOpinionsBox (replace grid-cols-2)
- StepOpinionsBox: header phòng + body list signed opinions
- StepOpinionEntry: tên NV + Cấp badge + Admin override badge nếu có
+ timestamp + comment
- Drop LevelOpinionBox function (per-NV pattern bỏ)
- KHÔNG đụng schema Mig 26 (PE Service ApproveV2Async UPSERT giữ 1 row /
Level — chỉ FE re-group render)
Verify:
- npm run build × fe-admin pass · fe-user pass
- Test pass mặc định skip (Phase 9 UAT iteration, Q4 user public luôn)
Pending Chunk D: Docs S20 changelog + STATUS + HANDOFF
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hai yêu cầu UAT 2026-05-08:
1. Bỏ "(clone)" auto-append khi clone version mới — version đã đủ phân biệt.
2. Thêm pin toggle để admin chọn workflows nào cho user pick lúc tạo phiếu.
Migration 25 AddIsUserSelectableToApprovalWorkflows:
- ALTER ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0
- UPDATE backfill SET IsUserSelectable=1 WHERE IsActive=1 (giữ behavior cũ
cho active versions, archived = false default — admin tự pin nếu cần)
BE:
- Domain ApprovalWorkflow +property IsUserSelectable
- DTO AwDefinitionDto +field
- CreateAwDefinitionCommandHandler set default true cho version mới
- New SetAwUserSelectableCommand + Handler
- API PATCH /api/approval-workflows-v2/{id}/user-selectable (Workflows.Create policy)
- DbInitializer SeedSampleApprovalWorkflowsV2Async set IsUserSelectable=true
FE Designer (fe-admin):
- DefinitionDto +isUserSelectable
- Badge amber "Pin Cho user chọn" khi true (cạnh Đang áp dụng/Archived)
- Button "Pin/PinOff Ghim cho user / Bỏ ghim" trong action group + mutation toggle
- Auto-fill name khi clone: bỏ "(clone)" suffix → giữ nguyên name
FE Workspace (fe-admin + fe-user):
- approvalWorkflows query filter w.isUserSelectable === true
- User dropdown chỉ thấy workflows admin đã pin
Verify: dotnet build pass · 81 test pass · npm build × 2 pass · Mig 25 apply LocalDB OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User UAT 2026-05-08: bỏ "trạng thái duyệt" (Cấp 1 → 2 → DaDuyet) +
bỏ thay đổi trước Trả lại lần đầu. Chỉ giữ:
- Workflow transition về TraLai (Reject)
- Workflow transition từ TraLai → ChoDuyet (Drafter gửi lại)
- Mọi sửa nội dung khi phaseAtChange = TraLai (giai đoạn chờ gửi lại)
Filter ở FE (PeDetailTabs HistoryTab). BE giữ audit data đầy đủ —
chỉ thay logic display, reversible. Mirror fe-admin + fe-user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "bỏ luôn cái quy trình phía trên đi nhé, vì nó là trạng
thái rồi (đã có badge), update cái flow quy trình mới vào bên panel 3
đang đến ai".
BE — ApprovalFlow DTO mới (full snapshot Bước → Cấp → NV với Status):
- PurchaseEvaluationApprovalFlowDto { CurrentStepIndex, CurrentLevelOrder,
Steps[] }
- PurchaseEvaluationApprovalFlowStepDto { Order, Name, DepartmentId/Name,
Status, Levels[] }
- PurchaseEvaluationApprovalFlowLevelDto { Order, Name, Approvers[], Status }
- Status: "Done" | "Current" | "Pending"
Handler GetById compute Status logic:
- Phase=DaDuyet → tất cả Steps/Levels "Done"
- Phase=Nháp/Trả lại/Từ chối → tất cả "Pending"
- Phase=ChoDuyet:
* Step.Index < currentIdx → all Levels "Done"
* Step.Index == currentIdx:
Level.Order < currentLevelOrder → "Done"
Level.Order == currentLevelOrder → "Current"
Level.Order > currentLevelOrder → "Pending"
* Step.Index > currentIdx → all "Pending"
- Load Approvers info (FullName + Email) qua UserManager batch query
FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeApprovalFlow + Step + Level + Status union
PeDetail.approvalFlow optional
- PeWorkflowPanel:
* BỎ phase cards section (4 ô Nháp/TraLai/ChoDuyet/DaDuyet) — đã
duplicate với status badge ở header
* Header mới: "Quy trình duyệt" + Code + Version + Name workflow pin
* Render Flow vertical: Bước (icon ✓/●/○) → border + bg theo status
+ dept badge → list Cấp (icon nhỏ) với label "đang chờ" / "đã
duyệt" + tên NV duyệt
* Phiếu V1 legacy (no flow): show note "dùng quy trình cũ — không
khả dụng chi tiết"
* Bỏ helper isPastPhase() (orphan sau khi xóa cards)
Verify: BE build 0 error · 2 FE builds OK.
Test eoffice:
1. Mở phiếu V2 đang ChoDuyet → thấy flow Bước 1 (Phòng A):
✓ Cấp 1 NV X (đã duyệt)
● Cấp 2 NV Y (đang chờ) ← highlight
○ Cấp 3 NV Z (chưa)
2. Phase=DaDuyet → all Steps/Levels green ✓
3. Phase=Nháp/TraLai → all greyed ○
4. V1 legacy → fallback note
User feedback: "Nếu không đúng bước duyệt thì nút duyệt cho Disable luôn cũng đc."
BE — DTO + Handler populate "Bước/Cấp đang chờ duyệt":
- Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs:
+PurchaseEvaluationApprovalLevelApproverDto { UserId, FullName, Email }
+PurchaseEvaluationCurrentApprovalDto { StepIndex, StepName,
StepDepartmentId/Name, LevelOrder, LevelName, Approvers[] }
PurchaseEvaluationDetailBundleDto +CurrentApproval? optional field
- Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs handler
GetById: khi pin V2 + Phase=ChoDuyet → load AW.Steps.Levels Include
3-level + group by Order = Cấp + resolve user names → populate
CurrentApproval. Null khi V1 legacy hoặc không phải ChoDuyet.
FE — types + PeWorkflowPanel (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeCurrentApproval + PeCurrentApprovalLevelApprover
+ PeDetail.currentApproval optional
- PeWorkflowPanel:
* Banner V2 hiển thị "Đang chờ Bước N (TênBước · Phòng X) — Cấp K"
+ list NV được duyệt + status emerald (đến lượt) / amber (không phải lượt)
* useAuth() để check currentUser.id ∈ approvers + Admin bypass
* Button "Duyệt forward" disabled khi V2 pin + actor không khớp.
Title tooltip "Cấp K chỉ {NV X / NV Y} mới duyệt được."
* Button "Trả lại" + "Từ chối" vẫn enabled (BE không gating 2 hành
động này theo Cấp — Approver có thể reject bất cứ lúc nào).
* Send-back logic update: target = DangSoanThao OR TraLai (V2 dùng TraLai)
- Admin role bypass mọi check.
Verify: 81 test pass · npm build × 2 OK · BE 0 error.
Test thử:
1. NV X (approver Cấp 1 V2) login → banner emerald "Đến lượt bạn duyệt"
+ nút "✓ Duyệt → ChoDuyet" enabled
2. NV Y (không phải approver) login → banner amber "Không phải lượt
bạn — chỉ NV X mới duyệt được" + nút Duyệt grey disabled, hover tooltip
3. Admin login → bypass, button enabled
User feedback: thay field "Loại quy trình (theo menu — khóa)" disabled
→ Select dropdown cho User pick quy trình ApprovalWorkflowsV2 (Mig 22)
ngay từ workspace tạo mới. Hiển thị "Mã + Tên + Version".
BE Domain:
- PurchaseEvaluation +ApprovalWorkflowId Guid? (nullable, FK Restrict)
- EF Configuration: Index + FK Restrict to ApprovalWorkflows
- Migration 23 `AddApprovalWorkflowIdToPurchaseEvaluation` (1 ALTER +
1 IX + 1 FK), applied cả _Design + _Dev LocalDB
- Field WorkflowDefinitionId (Mig 21 legacy) giữ song song để Service
PE chạy logic cũ tới khi Session sau wire qua schema mới
BE Application:
- CreatePurchaseEvaluationCommand +ApprovalWorkflowId? Guid? optional
param (default null)
- Validate: nếu set, phải tồn tại + ApplicableType khớp PE.Type
(DuyetNcc=1 → ApprovalWorkflowApplicableType.DuyetNcc, etc)
- Handler set entity.ApprovalWorkflowId từ request
- UpdatePurchaseEvaluationDraftCommand mirror — cho User đổi quy trình
khi sửa Nháp/Trả lại (validate same)
- PurchaseEvaluationDetailBundleDto +ApprovalWorkflowId/Code/Name/Version
- GetPurchaseEvaluationByIdQuery handler load workflow info join
- Update Phase guard: cho sửa cả DangSoanThao + TraLai (Trả lại =
editable per Session 17 spec)
FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: PeDetail +approvalWorkflowId/Code/Name/Version
- PeWorkspaceCreateView.tsx:
- Replace field disabled "Loại quy trình" → Select bắt buộc
- useQuery `/api/approval-workflows-v2?applicableType=N` filter theo
defaultType (1=DuyetNcc / 2=DuyetNccPhuongAn)
- Display option: "QT-DN-V2-001 v01 — Quy trình Duyệt NCC (đang áp dụng)"
- List cả version active + archived (UAT cần test compare)
- Empty state hint amber "Chưa có quy trình, vào /system/approval-workflows-v2"
- canSubmit require approvalWorkflowId set
- POST payload include approvalWorkflowId
Verify: dotnet build OK · 81 test pass · npm build × 2 OK · Mig 23 applied
cả 2 LocalDB.
Logic Service PE chưa wire qua ApprovalWorkflowId — vẫn pin
WorkflowDefinitionId Mig 21 legacy chạy. Session sau wire Service iterate
ApprovalWorkflowSteps + match approver theo schema V2 + drop legacy.