App gỡ nút "Từ chối" từ S60 (Khu vực Quyết định chỉ còn Duyệt/Trả lại) nhưng guide
phát toàn công ty (đính kèm email go-live anh Kiệt 26/06) vẫn ghi "Từ chối" 2 chỗ:
- Bước 4 (thân bài): bỏ ' / "Từ chối"'
- Caption Hình 9: "Duyệt / Trả lại / Từ chối" -> "Duyệt / Trả lại" (caption mâu thuẫn với ảnh)
+ Sửa 12 caption "Hinh N." -> "Hình N." (thiếu dấu từ S90 ASCII-gen, gotcha #30/#61 họ hàng).
Sửa qua unpack -> edit XML -> repack (giữ nguyên 13 ảnh: 1 logo + 12 màn).
Pack --validate false (validator dính bug cp1252 đọc UTF-8 VN, kể cả numbering.xml không sửa).
Verify độc-lập python-docx: 'Từ chối'=0, caption=12, media=13, open-strict OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
anh Kiệt FDC: "Xóa hết các User test ko liên quan, lấy User Solution thật vào".
Recon prod: 55 user → GIỮ 17 (admin + 16 real) → XÓA 38 (37 test rõ + 1 typo
chuong.phan@solution.com.vn — anh chốt xóa, anh Chương dùng bản @solutions).
An toàn FK: KHÔNG user test nào là ApproverUserId (NO_ACTION không bị chặn — recon verified);
5 bảng con CASCADE (EmployeeProfiles/UserRoles/Claims/Logins/Tokens); nv.test 1 PE loose-Guid xóa kèm.
- DbInitializer: gỡ 16 user mẫu khỏi SeedDemoUsersAsync + gỡ LockDemoSampleUsersAsync (call+method).
Seed giờ chỉ tạo/reconcile 17 user thật. SeedItDepartmentStaff + sample-workflow seeds null-guard
(skip user đã xóa) → deploy KHÔNG resurrect/crash. ⚠ PHẢI deploy để binary mới không tái tạo test.
- scripts/s89-delete-test-users.sql: whitelist-keep 17 + BACKUP + XACT_ABORT + count-guard[30,45].
ĐÃ CHẠY prod (backup SolutionErp_S89_preuserdelete.bak): count=38, PE=1, còn đúng 17 user. Build slnx xanh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WorkflowMatrixViewPage (màn "Luồng duyệt" read-only của user, build S24 với 7 flag) thiếu flag
thứ 8 allowApproverFinalize ("Duyệt là KẾT THÚC quy trình, không trình tiếp CEO") thêm ở Mig 58
(S87) → matrix "chưa hiển thị chính xác" QT-DN-V2-001 (anh Kiệt FDC). BE overview DTO đã gửi sẵn
field này (ApprovalWorkflowV2AdminFeatures.cs:41/188), chỉ FE matrix + AwLevelDto type thiếu.
Thêm allowApproverFinalize vào AwLevelDto + FlagRow thứ 8 (colSpan2, mirror admin Designer).
FE-user only (matrix là view user-app, không có bản fe-admin). Build xanh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trong khối "Quy trình duyệt" của phiếu (PeWorkflowPanel, sơ đồ Bước→Cấp→NV), cấp được cấu hình
AllowApproverFinalize (Mig 58) giờ hiện badge tím "⚑ Duyệt thay CEO" + tooltip; các Bước/Cấp SAU
cấp finalize (vd Ban Giám đốc / CEO) làm mờ + nhãn "không trình tới" → người xem phiếu biết NGAY
trong sơ đồ rằng quy trình DỪNG ở cấp này (duyệt thay CEO), không lên CEO (anh Kiệt FDC).
Dùng field allowApproverFinalize đã expose ở flow-level (S89 #3). first-reachable theo Step.Order→
Level.Order (khớp BE finalize). Bổ sung cho callout note (#3) = chỉ-dẫn trong chính sơ đồ flow.
FE-only, 2 app SHA-mirror, build xanh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phiếu mà quy trình duyệt có cấp bật AllowApproverFinalize (Mig 58) sẽ kết thúc tại cấp đó,
KHÔNG trình CEO. Hiển thị note cho MỌI NGƯỜI NẮM ở 3 surface (anh Kiệt FDC). KHÔNG migration
(cột AllowApproverFinalize đã có Mig 58, prod ở Mig 59).
BE (3 file, computed DTO):
- PurchaseEvaluationDtos.cs: +bool EndsBeforeCeo +string? FinalizeStepName +string? FinalizeLevelName
trên DetailBundle + ListItem; flow-level +bool AllowApproverFinalize.
- PurchaseEvaluationFeatures.cs: detail compute first-reachable (Step.Order↑→Level.Order↑, dept-name
in-memory); list + inbox projection qua 3 subquery EF-safe (lv.Step!.Name). V1 (workflowId==null) → false.
- CreateContractFromEvaluationFeatures.cs: ListApproved projection cùng DTO → +3 subquery y hệt.
FE 2-app (fe-user + fe-admin SHA-mirror component):
- PeFinalizeChip.tsx (MỚI, chip tím) ở 3 danh sách: PeListPanel + PurchaseEvaluationsListPage (cây) + InboxPage.
- PeWorkflowPanel.tsx: note callout trong phiếu, branch DaDuyet (past-tense) / đang-chạy; copy boundary-aware
"...là KẾT THÚC, không trình CEO. Các cấp trước vẫn duyệt như thường." (tránh cấp giữa hiểu nhầm).
- UserDashboardPage.tsx (chỉ fe-user): dải "Phiếu Duyệt NCC gần đây" + chip (admin dashboard để nguyên).
- types: PeListItem/PeDetailBundle/PeApprovalFlowLevel +field.
Tests: +PeFinalizeProjectionTests (EF-translate smoke 3 subquery + first-reachable + V1-false). Full suite 421→422.
em-main self-gate sau workflow (impl-frontend return rỗng #53 → mirror fe-admin + verify 5 site recover-disk).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Màn hình CEO ở "chỗ thao tác" hiện bản nháp / phiếu chưa xong của NGƯỜI KHÁC (anh Kiệt
FDC). Phiếu nháp (DangSoanThao) lộ qua 2 đường: cond #2 role-eligible (Drafter/DeptManager
→ DangSoanThao, mọi Drafter thấy nháp nhau) + cond #3 V2-approver (workflow pin lúc TẠO
phiếu nên approver như CEO thấy mọi nháp trước khi gửi duyệt = leak chính).
Luật mới: nháp = RIÊNG TƯ người tạo (DrafterUserId) + Admin thấy hết. 4 chỗ:
- GetEligiblePhases: bỏ DangSoanThao khỏi role-eligible (cond #2).
- Inbox (mảng admin): bỏ DangSoanThao — nháp khỏi Inbox hẳn kể cả admin (anh Kiệt chốt).
- List query cond #3: guard != DangSoanThao (approver không thấy nháp).
- Detail authz: guard != DangSoanThao cho V2-approver (không mở nháp người khác bằng link).
Phiếu ĐÃ gửi duyệt giữ nguyên (approver vẫn thấy). Phần admin-screen tinh chỉnh sâu để sau.
+PeDraftVisibilityTests (2 test, test-before security): owner thấy nháp mình · drafter khác
không · CEO/V2-approver không · admin thấy hết · phiếu đã gửi vẫn hiện (no regression).
Full suite 419→421 PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ô "Giá đề xuất (PRO/CCM)" trong SuggestedPriceRows bị khoá thừa tầng !readOnly mà ô
ngân sách song song (editable={bs.canEditCcm}) không có. Hệ quả: CCM (role CostControl)
không nhập được giá đề xuất khi xem phiếu ở view readOnly=true (preview danh sách, hoặc
màn Duyệt chưa tới lượt mình) — đúng báo cáo anh Kiệt FDC "PRO oki nhưng CCM không nhập".
Gỡ !readOnly, gate thuần theo role-capability (ev.canEdit{Pro,Ccm}SuggestedPrice do BE
tính). BE setter vẫn fail-closed theo role (ForbiddenException) nên gỡ gate FE = an toàn,
khớp hành vi ô ngân sách + intent handler "KHÔNG ràng Phase, chỉnh như tài liệu sống".
Bỏ luôn prop readOnly không dùng khỏi signature + call-site. 2 app SHA-mirror, build xanh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iis-deploy-runbook SKILL corrected (discovered during the index.html-hardening task):
- physical paths apps\SolutionErp\{Api,Admin,User} were STALE (empty dir) -> real
solution-erp\{api,fe-admin,fe-user} (verified via appcmd list site).
- web.config rule list fixed to the ACTUAL live config (HTTP->HTTPS + SPA-Routes excl
/api + .webmanifest + 3 security headers; NO /api|/hubs ARR proxy -- FE calls api via CORS)
+ documented the S88 cache hardening (index.html no-cache, assets immutable).
- log/appsettings/scp/backup paths swept to the real base.
Session log: index.html no-cache hardening DONE (ca136d8) + lesson reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
S88 white-screen-incident hardening. An open tab could serve a stale index.html across
a deploy (IIS served index.html cacheable: only ETag/Last-Modified, no Cache-Control).
Add version-controlled web.config to fe-*/public (Vite -> dist -> deploy Copy-Item
overwrites the previously manually-managed server file):
- root staticContent clientCache=DisableCache -> Cache-Control: no-cache for the SPA
shell (index.html served at / and via SPA-rewrite for /dashboard etc.) = always revalidate.
- <location path="assets"> UseMaxAge 365d + custom immutable -> hashed assets cached 1y.
Replicates the EXACT live rules (HTTP->HTTPS, SPA-Routes rewrite excl /api, .webmanifest
mime, 3 security headers) so routing is preserved. Applied + verified on live before commit
(index.html no-cache; asset immutable,max-age; / /dashboard /login 200; http->https 301).
api = .NET API (no SPA index.html) -> N/A.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anh Kiet: 2 bang "TONG HOP NGAN SACH TRINH KY" (A. NGAN SACH / B. THUC HIEN) lech cot
Du an/PRO/CCM giua 2 section. Root: A dung w-40 cho PRO/CCM, B dung w-44, ca 2 auto-layout
-> cot khong thang hang. Fix: ca 2 table -> table-fixed + cung widths (Du an w-20 · PRO
w-44 · CCM w-44) -> cot A va B thang hang 1 luoi.
FE-only, 2-app SHA-identical (93a4e8a1), both builds PASS. No BE/test/migration change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the Mig 58 multi-winner (>=2 cung trung) wiring — the audit's joint-winner
dead-ends, CEO-approved scope ("1 HD per winner"):
- create-contract 1->N: one draft HD per IsWinner supplier (was 409 dead-end for >=2).
Command -> List<Guid>. ContractCodeGenerator self-commits its sequence per call, so
gen all N codes first (contracts built local) then atomic add + single final
SaveChanges (no partial create on failure). GiaTri: single = details ThanhTienNganSach
sum (legacy preserved); >=2 = per-winner quote total (DangSoanThao draft, editable —
anh Kiet UAT the basis). pe.ContractId = first (idempotency). Controller -> {contractIds}.
- list-card winner names: PurchaseEvaluationListItemDto +WinnerSupplierNames (EF
collection subquery over IsWinner suppliers; translates + runs on SQLite — test-verified).
FE PeListPanel shows joined names for >=2 (was blank). Dialog -> contractIds[] + "Da tao N HD".
Tests 413 -> 419 (+6: multi-contract 5 [per-winner GiaTri 140/260 vs single legacy 800,
idempotency, 0-winner, not-DaDuyet] + EF-subquery translation smoke). Full suite GREEN.
FE 2-app SHA-identical. reviewer PASS (codegen-sequencing, positional-record arg-order,
no-partial-create all independently verified).
UAT (anh Kiet): open a >=2-winner phieu -> "Tao HD" creates N draft contracts (1/winner);
confirm per-winner contract GiaTri (quote total) is the wanted basis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PE stability audit (frozen CEO-approved spec) — D1 behavior-preserving fixes plus
one approved joint-winner financial correction:
- Block B "Gia tri ky nay" + cross-PE peer rollup re-keyed SelectedSupplierId -> IsWinner
(reported 0 / excluded peers for >=2 joint winners; now matches winnerQuoteTotal).
- CEO-notify-on-urgent: +SaveChanges after NotifyManyAsync (Director notify was added
after the only flush -> never persisted; now persists + SignalR push fires).
- HoSoLink: UpdatePeDraft null-safe + clear-via-empty (header edit no longer wipes a
set link; HoSoLinkRow sends "" to clear; header forms omit = preserve).
- #70 stale-echo guard added to PRO budget row3/row8 (|| peFetching).
- Relocate SetPeCcmBudgetPeriodCommand -> PeWorkItemBudgetFeatures (was misplaced in
PeSuggestedPriceFeatures). DTO/comment cleanups.
Tests 402 -> 413 (+11: HoSoLink preserve/clear/set, Block B >=2-winner, CEO-notify
persist x6). Full suite GREEN. FE 2-app SHA-identical. test-specialist + reviewer
returns truncated (#53, over-budget MEMORY) -> em-main self-gate from disk + suite run.
D2 deferred (create-contract 1-HD-per-winner + list-card winner names) — needs focused
care + anh Kiet UAT on per-winner contract value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cicd Run #346 (0d3c0cf, FE-only PE layout tách khối nổi bật "4. Thông tin chọn
thầu") PASS ~5m: test gate 402 unchanged, no migration (Mig 59 HEAD), both
bundles rotated admin C8lWxEQK->BABhPh2s / user Kz5cSW2u->DLulO6uP (js+css),
smoke 3x200. STATUS/HANDOFF bundle flushed + UAT note. Docs-only -> CI skip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tách c/d/e (Giá chào thầu + Đề xuất PRO/CCM + Bảng so sánh + Link hồ sơ) khỏi
Section 3 thành khối NỔI BẬT riêng "4. Thông tin chọn thầu" — header xanh #1F7DC1
giống bảng ngân sách "giá trị thực hiện". Đánh số lại 1->5:
- 1. Tên NCC/TP + Giá trị (chào thầu): winner "mang từ trên xuống", hiển-thị nổi bật
(Section 3 giữ ô CHỌN NccSelectorRow; multi-winner -> tên cách dấu phẩy)
- 2-3. Giá đề xuất PRO/CCM (SuggestedPriceRows) · 4. Bảng so sánh giá · 5. Link hồ sơ
- Ý kiến cấp duyệt: Section 4 -> 5.
FE-only (PeDetailTabs ChonNccSection + new ThongTinChonThauSection), 2 app SHA-mirror,
build sạch. Mockup anh Kiệt duyệt trước khi build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CCM (Lưu Trần / anh Kiệt FDC) — làm 3/4 mục (mục 3 "CCM sửa ngân sách" để riêng):
1. Multi-winner: chọn NHIỀU NTP/NCC/ĐVDV cùng trúng → winnerQuoteTotal = SUM
ThanhTien MỌI đơn vị isWinner (vd 3 ông 37.8+9+10 = 56.8tr). Cột mới
PurchaseEvaluationSupplier.IsWinner (Mig 58, backfill từ SelectedSupplierId).
SelectWinner → multi-toggle SupplierIds[]. FE: bảng so sánh tick nhiều ô +
dòng "Tổng chi phí đã chọn"; mọi guard/checklist/computeGiaChaoThau/tạo-HĐ
đổi sang isWinner (selectedSupplierId = null khi liên-danh ≥2).
2. Ngân sách = 0 hợp lệ: nới submit-guard + 3 validator (chỉ chặn khi NULL,
số 0 cố ý đi qua; fallback ProInitial chỉ khi null). Mirror BE + FE.
3. Cấp KẾT THÚC quy trình: ApprovalWorkflowLevel.AllowApproverFinalize (Mig 58)
— admin bật per-cấp trong Workflow Designer; duyệt tới cấp đó TỰ DaDuyet,
bỏ qua CEO (AUTO theo cấu hình quy trình, không ô-tích runtime — anh Kiệt
chốt). FE: banner cảnh báo + bắt chọn giá chốt.
Mig 58 AddPeMultiWinnerAndLevelFinalize (2 AddColumn bool + backfill IsWinner,
additive/reversible). Test 377→395 (+PeMultiWinnerTests +PeApproverFinalizeTests
+budget=0; auto-model gọn bớt 2 test bypass/opt-out). FE 2 app SHA-mirror.
Build sạch BE + 2 FE local.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Người TẠO phiếu (drafter) không còn nhận chuông "Phiếu cần bạn duyệt" cho
phiếu mình tạo — kể cả khi được cấu hình làm approver ở một Cấp. Chỉ approver
KHÁC người tạo ở Cấp hiện tại mới nhận. Drafter vẫn nhận chuông KẾT-QUẢ
(đã duyệt / trả lại) như cũ (block :1060 giữ nguyên).
- PurchaseEvaluationWorkflowService.LogTransitionAsync: thêm filter
`l.ApproverUserId != evaluation.DrafterUserId` (in-memory LINQ, nullable-safe).
- test-specialist: +3 regression PeApproverNotifyExcludesDrafterTests
(drafter-excluded · co-approver-vẫn-nhận · result-notify-intact; RED-on-old
đã chứng minh). Test 374 -> 377.
- docs/STATUS.md: re-ground canonical (S85-closeout sót: test 356->377,
gotcha 71->73, bundle #341) + dòng S86 +3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
anh Kiệt báo "mới tạo phiếu mà báo cách 7h trước" — lỗi timezone: timestamps lưu UTC
(DateTime.UtcNow) nhưng EF đọc datetime2 = DateTimeKind.Unspecified → System.Text.Json default
serialize KHÔNG có 'Z' (vd "2026-06-23T08:20:00") → FE new Date(naive) hiểu là LOCAL → lệch +7h
(VN UTC+7).
Fix: UtcDateTimeJsonConverter (global qua AddControllers.AddJsonOptions) ép serialize UTC ISO-8601
CÓ 'Z' cho MỌI timestamp API → FE new Date("...Z") convert đúng về giờ local. Áp dụng cả DateTime?
(System.Text.Json tự dùng converter cho non-null của Nullable).
BE 2 file (converter mới + Program.cs đăng ký). Build slnx 0 err. KHÔNG migration. Bundle FE frozen.
Note: SignalR realtime push dùng serializer riêng — nếu giờ realtime còn lệch sẽ thêm AddJsonProtocol.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root-cause "chưa chọn quy trình duyệt" lặp lại (long.chau + nhiều account dù SQL-fix nhiều đợt):
form Sửa header (PeHeaderForm → PUT /purchase-evaluations/{id}) KHÔNG gửi approvalWorkflowId →
UpdatePurchaseEvaluationDraftCommandHandler absolute-set entity.ApprovalWorkflowId = null → XÓA
workflow của phiếu (tạo CÓ workflow → bấm Sửa = MẤT → kẹt submit). ApprovalWorkflowId bị SÓT khỏi
pattern null-safe (bug-class S42) mà BudgetPeriodAmount/ExpectedRemainingAmount/WorkItemId ngay
dưới đã áp dụng ("client không gửi → GIỮ giá trị cũ").
Fix: null-safe — chỉ set ApprovalWorkflowId khi client CÓ gửi (preserve nếu không gửi). Đổi quy
trình = FE phải gửi giá trị mới. + prod-data: gán QT-DN-V2-001 cho 5 phiếu bị clear (2 đợt 3+2).
BE 1 file (UpdateDraftHandler). Test 371 PASS (no break — submit-guard S83 giữ nguyên).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phiếu cũ thiếu workflow (tạo lúc bundle cũ chưa có D5 auto-chọn) → checklist D4 toàn xanh
nhưng submit chặn "chưa chọn quy trình" = MISLEADING. Bổ sung dòng "Quy trình duyệt" vào
checklist (xanh ✓ khi có workflow, đỏ ✗ khi thiếu) → người dùng thấy đúng blocker.
3 phiếu thiếu workflow đã fix prod-data (gán QT-DN-V2-001 cho type-1). R4 auto-assign-at-submit
đã thử nhưng REVERT: phá 2 test S83 (test-DB có sẵn workflow → auto-gán thay vì block) + nới
block ngược ý "ràng". SQL fix + create-block/D5 (R1) đã đủ chặn phiếu thiếu workflow.
FE-only PeDetailTabs ×2 app SHA-identical. Both build PASS, test 371 (revert verified).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anh chốt "không chọn quy trình thì không cho tạo phiếu, ràng mấy cái rủi ro lại hết":
- Form TẠO: nút "Tạo phiếu" disabled khi thiếu (Quy trình duyệt / Hạng mục / Dự án) + hint
ĐỎ liệt kê rõ còn thiếu gì (✓ xanh khi đủ). Defense-in-depth với BE validator NotEmpty (S83).
- Detail (D4): checklist xanh ✓ "đã khai" / đỏ ✗ "chưa khai" cho 4 trường bắt buộc gửi duyệt
(NCC winner · giá>0 · ngân sách · bảng so sánh) hiện phía trên nút "Lưu & Gửi Duyệt"
(mirror missingForApproval predicate, khớp annotation anh Kiệt "xanh=đã khai, đỏ=chưa khai").
FE-only 2 component × 2 app SHA-identical. Both build PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
D2 — Người KHAI phiếu tự GỠ cờ GẤP của mình (nới luật S77 "GỠ=chỉ TP"). BE handler
restructure SET/UNSET: GẮN giữ role-only (PRO/CCM/Admin); GỠ +drafter (DrafterUserId==actor)
→ drafter/Admin clear CẢ 2 cờ, TP-vai clear cờ vai mình. FE canToggle +isDrafter.
D3 — "Ngân sách kỳ này" (row3) tự nhảy từ "Ngân sách Ban hành lần đầu"
(PeWorkItemBudget.ProInitialAmount) khi chưa nhập tay. Submit-guard BE + FE missingForApproval
dùng effective = budgetPeriodAmount ?? proInitialAmount (mirror 2 tầng, chống bypass gotcha #44).
BE 2 file (Urgent handler + Workflow submit-guard). FE PeDetailTabs ×2 app SHA-identical.
Test +5 PeUrgentToggleAuthzTests (drafter-self-unset authz matrix) → 371 PASS. BE slnx + 2 FE build PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test 354->356, bundle CsJetgZH/BVS0ApIm Run #333 (unchanged, #69-REFINE: no fe-src=Vite-deterministic-same-hash). spawn-records 3 sub (tooling/harvest/cicd all verified). State Mig 57/88 bang/356 test/gotcha 71.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Modal Duyet them picker multi-file -> upload TRUOC khi chuyen phase (file loi/>20MB = throw, khong duyet). File hien o muc 'File dinh kem khi duyet' (download/preview) trong Panel quy trinh, gan ten nguoi tai + thoi gian. Fix 2 filter dung supplierId=null lam proxy Bang-so-sanh (banSoSanhAttachments + submit-guard) loai purpose=5 tranh lan section + false-pass. FE 2 app SHA-identical. authz: approver upload duoc (handler khong guard drafter-only). Test 354 PASS, no migration. UAT Tra Sol / 5 tester.
Bich Phuong hoi 'chi Tra tra ve thi bam nop lan 2 cho nao'. Phieu Tra lai o che do XEM (readOnly) khong co nut 'Luu & Gui Duyet' (nut do chi hien khi Sua) -> NV khong biet gui lai. Them banner amber khi phase==TraLai && readOnly: huong dan ra danh sach -> ✏️ Sua (but chi) -> dieu chinh -> 'Luu & Gui Duyet ->' o cuoi phieu; ly do tra lai xem o Lich su. Khop dung cau tra loi cua anh cho Bich Phuong. FE-only, 2 app SHA256-identical PeDetailTabs. Build PASS x2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tra Sol (Zalo): 'khong thay chuong bao - viec co ho so can duyet'. PE workflow truoc chi notify DRAFTER luc terminal; nguoi DUYET khong duoc bao khi phieu toi cap cua ho. Fix: block trong LogTransitionAsync (sau drafter-notify) - khi toPhase==ChoDuyet, resolve approver cap hien tai (workflow.Steps[CurrentWorkflowStepIndex].Levels Order==CurrentApprovalLevelOrder -> ApproverUserId OR-of-N, exclude actor) -> NotifyManyAsync Generic 'Phieu can ban duyet'. Fire moi luc phieu vao cap ChoDuyet: submit (cap 1) + moi approve-advance (cap ke) - 5 call-site advance deu log toPhase=ChoDuyet sau khi set pointer (verified). Best-effort try/catch (notify fail KHONG rollback transition). V2-only guard. Bell render Generic san (urgent->CEO da chung minh). BE-only, no-mig. Build slnx 0/0, test 354 PASS (khong vo transition cu). Notify = test-after (UAT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>