wal: flush 20260804T1853
This commit is contained in:
@ -12,7 +12,7 @@ chain:
|
||||
- [x] V3 trio TRỌN — eval MIXED 19-trục/14-signal → refine 1-action/14-BÁC (E1 impl-fe drain −62,5% dưới low-water, A4 0-sàn → CHỜ-ANH) → audit 55Đ/5T re-chạy-177-số/172-byte-exact + CHẶN-A1 (2 mục MISS = trivial<30min + build-gap ⇒ 0 đích reinject; 11/13 đứng-yên trừ khi anh đổi chữ/matcher → CHỜ-ANH) · #53: eval-return + audit-×2 (resume ×2 cứu trọn), refine sạch
|
||||
- [x] V4 nén-ngủ (⚠️ marker lật `[!]`→`[x]` @S171 ground-truth: `last_sleep_at=2026-08-03` + gist trong archive + `sub-ring4-audit-sleep-S170.md`; thân dòng đã tự khai XONG từ S170, chỉ ô chưa lật) — P0✓(17 shard/238.849B) · P1✓ 2 curator XONG (A: 9f/45c 113.797B resume-1× · B: 8f/48c 123.454B sạch) + 🔐 2 secret THÔ redact (xem dòng CHỜ-ANH) + đề-nghị skip cicd/2026-08 (đã-chưng-cất) CHẤP NHẬN ⇒ 16 gist · P2✓ 2 drafter sạch (A 8-draft/98KB 38/38-pointer · B 8-draft+note/121KB) · P3(a)✓ coverage-máy 16/16 PASS (77 token-literal bổ sau soi-tay 4-ca: MISS = biến-thể dải/range/bold-split, KHÔNG mất nội-dung) · P3(b)✓ FIDELITY PASS-WITH-FIXES (reviewer 38KB ×2-resume: P1 4-cờ [3-drift+1-MA AllowDelegate pickaxe-0] · P2a-d · P3-lưới) → 9 nhóm fix ÁP TRỌN (kể cả bug máy-append cut-byte → vá cắt-ký-tự) · P4✓ 16 gist → archive/ (233.858B ≈ 100,4% nguồn — KHÔNG claim giảm-byte; additive hash-assert 16/16; last_sleep 08-03) · P4.5✓ ring4 8Đ/3T (3T=sai-số-đo 13B-tiền-redact+làm-tròn; ADDITIVE vững; T-1..T-5 tồn-dư → vá TRỌN vào archive @S170; caveat "gate-PASS ≠ 0-mất-load-bearing") ⇒ V4 [x] XONG → còn 3.5 đuôi + P5 report
|
||||
- [x] Phase 3.5 đuôi TRỌN — MIND-1 (mind-check 10Đ/0T ×2) + synthesis (orphan-4 ĐÓNG) + ctx-audit TRUOT-9-FLAG liền-mạch (THẬT/ĐỦ/SẠCH đạt, §E 10/10 khen) → lead vá 4 cùng lượt: WAL-next-stale · HANDOFF (47)(48) thước-tươi · §E +2 ring-verdict + neo MIND-1 · persona ctx-audit [p,p+3] (ENV-DEP) + seed diary on-behalf. Mồi @close: port refresh-bookend sang _context (lỗ FLOW đường tiep→closeout) + **7**-dir-rỗng (S171 sửa từ "6" — ctx-verifier cờ⑧: closeout đọc mồi sẽ dọn 6, SÓT đúng cái mới `sleep-drafts/.claude/agent-memory/reviewer`) + sleep-drafts + deep-JUMP 7/11
|
||||
- [!] wf: `2026-08-04-S171-khkk-ui-mirror-pe` — TASK SẢN-PHẨM anh giao @S171: bố-trí lại 3 panel màn **KHKK** đồng-nhất với **Duyệt NCC** (P1 list+cây · P2 content-trên/ý-kiến-dưới · P3 quy-trình+history). A✓ `/fable-real investigator-codebase` (`wf_b76692d4-3a4`, 756s, return SẠCH) → artifact 44.830 B **27 giống-được/6 buộc-khác/8 data-gap**; 2 đính-chính: PE-panel-1 INLINE `:399-639` KHÔNG phải `PeListPanel` · **GĐ3 `ContractsListPage.tsx:125` đã là PE-3-panel gọt gọn (S168)** ⇒ KHKK = mắt-xích lệch DUY NHẤT, chép khuôn từ GĐ3; + đảo ước-lượng: approvals+changelog **ĐANG GHI THẬT** (`WorkflowService.cs:534/:547`) chỉ thiếu ~5 field DTO + 1 endpoint. B `/fable-clone reviewer` ×3 lăng-kính (fidelity·feasibility·regression) **ĐANG CHẠY** `wf_832dadb2-93f` → C lead refute+synthesize→ghi spec 3-mục → D Opus 5 MAX thực-thi. Delta gốc đã đo: PE = 1 màn 3-panel (`PurchaseEvaluationsListPage.tsx:397` grid 400/1fr/360) ⟂ KHKK = list 2-panel (`KhkkListPage.tsx:197`) + detail trang riêng có workflow XẾP CHỒNG (`KhkkDetailPage.tsx:411`)
|
||||
- [!] wf: `2026-08-04-S171-khkk-ui-mirror-pe` — TASK SẢN-PHẨM anh giao @S171: bố-trí lại 3 panel màn **KHKK** đồng-nhất với **Duyệt NCC** (P1 list+cây · P2 content-trên/ý-kiến-dưới · P3 quy-trình+history). A✓ `/fable-real investigator-codebase` (`wf_b76692d4-3a4`, 756s, return SẠCH) → artifact 44.830 B **27 giống-được/6 buộc-khác/8 data-gap**; 2 đính-chính: PE-panel-1 INLINE `:399-639` KHÔNG phải `PeListPanel` · **GĐ3 `ContractsListPage.tsx:125` đã là PE-3-panel gọt gọn (S168)** ⇒ KHKK = mắt-xích lệch DUY NHẤT, chép khuôn từ GĐ3; + đảo ước-lượng: approvals+changelog **ĐANG GHI THẬT** (`WorkflowService.cs:534/:547`) chỉ thiếu ~5 field DTO + 1 endpoint. B✓ `/fable-clone reviewer` ×3 (`wf_832dadb2-93f`) — 🔴 **3/3 lane CHẾT "hit session limit · resets 6:10pm"**, engine trả null×3, **NHƯNG ĐĨA CỨU 2,95/3** (74.070 B): feasibility 8Đ/6BROKE · regression 8Đ/4BROKE/10-bỏ-sót **HOÀN TẤT** · fidelity DỞ (§6+§8 trống, VERDICT `?` — lead derive ≈12Đ/6BROKE/3, tự chạy nốt phép §6). C✓ **spec `spec-khkk-ui-mirror-pe-04-08-2026.md` 35.017 B** đủ ①②③, 0 residual. 🔴 **3 chốt lead ĐẢO bản invest**: (1) panel-1 KHÔNG dùng `PipelineTreePanel` — `canPe` khoá cây ⇒ user KHKK-only **mất TRẮNG danh-sách** (+trần-200 +cây tự-navigate +`content` nuốt `groups`) ⇒ dựng cây từ danh-sách CỦA CHÍNH KHKK · (2) miễn-trừ **6→3** (P1-4·P2-7 lật giống-được · P3-12 hạ chờ-anh: lead đo `DbInitializer.cs:554` = 3 Bước×≤2 Cấp, seed "sao từ workflow Duyệt NCC prod" ⇒ tiền-đề "3 trạm ngắn" SAI) + **miễn-trừ thứ 7 KHÔNG khai** (F-5 panel-3 empty-state) · (3) giữ P/A **thay lý-do** (nhị-phân giả). **6 acceptance bẻ gãy khi chạy thật** (A-13 citation-trap báo đỏ file sạch · A-6 kiểm NHẦM KHOÁ để lọt đúng bug S155 · A-10 sai trục · A-7b/A-7d/A-8c Goodhart) — đã thay bản sửa. ✅ **3 QĐ ANH chốt 04-08** (spec→43.087 B): (1) panel-1 = **`PipelineTreePanel`** — anh **BÁC** đề-xuất đảo của em, đọc đủ 4 rủi-ro trên thẻ rồi vẫn chọn ⇒ QĐ có-thông-tin; em chuyển 4 rủi-ro thành **mitigation CƯỠNG-HÀNH** C-5..C-8 (*chọn HÌNH ≠ chấp-nhận VỠ*) · (2) panel-3 rỗng = **CHỈ hộp dashed y PE** ⇒ đóng miễn-trừ-thứ-7 · (3) file-khi-duyệt = **upload THẬT** ⇒ BE-7 `+ApprovalAttachment=3` policy `KeHoachKyKet.Update`. **D ĐANG CHẠY** `wf_6fd33d7a-a20` (giới-hạn reset 18:10, phóng 18:50): `implementer-backend`→`implementer-frontend`, opus+effort:max, TUẦN TỰ, spec bơm qua con-trỏ file (B6). 🔸 Treo không-chặn: P3-12 4-return-mode = **hoãn** (tiền-đề "3 trạm" SAI), hỏi anh khi tới. Delta gốc đã đo: PE = 1 màn 3-panel (`PurchaseEvaluationsListPage.tsx:397` grid 400/1fr/360) ⟂ KHKK = list 2-panel (`KhkkListPage.tsx:197`) + detail trang riêng có workflow XẾP CHỒNG (`KhkkDetailPage.tsx:411`)
|
||||
- [ ] monthly drift-audit 2026-08-01 DUE quá hạn (session đầu-tháng = L10) + re-sync measured{} 17/23 — xếp thứ-tự vs task anh giao
|
||||
- [ ] 🔴 CHỜ-ANH slot (65) ZZTEST hạn 01-08 QUÁ 2 ngày (chỉ anh có kênh) · (64) user TEST · (66) dọn HĐ ZZTEST · thư day-wake-probe nấc adopt (ctx-verifier cờ ⑩: món 65 từng ngoài bản-đồ)
|
||||
- [ ] 🔴🔴 CHỜ-ANH MỚI @S170: XOAY password SQL prod `vrapp` — 2 secret THÔ trong agent-memory (cicd-runs:48 + reviewer-note:22, 2 giá-trị KHÁC nhau) đã redact `[đã che 71b7740a/4c3c67d5]` NHƯNG nằm trong git remote từ ~13-05 → rotate là bắt buộc thực-chất; + A4 low-water 0-sàn (refine E1) + đổi-chữ/matcher MFE impl-fe (audit CHẶN-A1)
|
||||
|
||||
@ -73,10 +73,41 @@ Opus 5 MAX theo `spec-khkk-ui-mirror-pe-04-08-2026.md` (lead ghi SAU khi verify
|
||||
- [x] A — `/fable-real investigator-codebase` deep-pass (Fable 5, `wf_b76692d4-3a4`, 756s, 33 tool-use, return SẠCH 0-garble) → `sub-investigator-codebase-invest-s171.md` **44.830 B** · **VERDICT=27 giống-được / 6 buộc-khác / 8 data-gap**
|
||||
🔴 **2 đính-chính vào Ground-truth của lead** (bảng trên SAI 1 dòng): (Đ-1) PE panel 1 **KHÔNG** phải `PeListPanel` — viết INLINE `PurchaseEvaluationsListPage.tsx:399-639`; `PeListPanel` chỉ 1 call-site = `PurchaseEvaluationWorkspacePage.tsx:84` ⇒ **không có component list dùng-lại được**. (Đ-2) **Đã có tiền-lệ mới hơn:** `ContractsListPage.tsx:125` (GĐ3, S168, 255 dòng) = bản PE-3-panel gọt gọn ⇒ KHKK là **mắt-xích lệch duy nhất** giữa GĐ1 và GĐ3; chép khuôn từ GĐ3, KHÔNG chép từ PE 807 dòng.
|
||||
🔴 **Đảo ước-lượng:** `ContractSigningPlanWorkflowService.cs:534/:547` ĐANG GHI approvals + changelog THẬT ⇒ "Lịch sử duyệt" không phải xây-lại, chỉ thiếu **~5 field DTO + 1 endpoint**.
|
||||
- [ ] B — `/fable-clone reviewer` ensemble 3 lăng-kính
|
||||
- [ ] C — lead verify + refute/synthesize → ghi spec-file 3-mục
|
||||
- [ ] D — giao Opus 5 MAX thực-thi
|
||||
- [x] B — `/fable-clone reviewer` ×3 lăng-kính (`wf_832dadb2-93f`, Opus, 470s, 52 tool-use) — 🔴 **CẢ 3 LANE CHẾT** *"You've hit your session limit · resets 6:10pm"*, engine trả `verdict: null` ×3. **NHƯNG ĐĨA CỨU 2,95/3**: khung-rỗng-lượt-1-2 giữ trọn 74.070 B.
|
||||
· `lens-feasibility` 26.461 B **HOÀN TẤT** — `8 HELD / 6 BROKE / 6 bỏ-sót`
|
||||
· `lens-regression` 24.432 B **HOÀN TẤT** — `8 HELD / 4 BROKE / 10 bỏ-sót`
|
||||
· `lens-fidelity` 23.177 B **DỞ** — §6 (phép đo P3-12) + §8 (lỗ tự khai) trống, VERDICT còn `?`. Lead derive từ §7 bảng của chính nó ≈ 12 HELD / 6 BROKE / 3 bỏ-sót (**nhãn lead-derive, KHÔNG phải lane tự kết**); lead **tự chạy phép §6 còn thiếu** → lật P3-12
|
||||
- [x] C — lead verify + refute/synthesize → **`spec-khkk-ui-mirror-pe-04-08-2026.md` 35.017 B** (utf8-strict OK, đủ 3 heading ①②③). Post-wave `git status`: **0 residual-write** ngoài run-folder
|
||||
- [x] C-bis — ✅ **3 QUYẾT-ĐỊNH OWNER chốt 2026-08-04**, spec cập-nhật **43.087 B**: (1) panel-1 = **`PipelineTreePanel`** — owner **BÁC** đề-xuất đảo của lead, đọc đủ 4 rủi-ro trên thẻ chọn rồi vẫn chọn ⇒ quyết-định có thông-tin đầy-đủ; lead chuyển 4 rủi-ro thành **mitigation CƯỠNG-HÀNH** + acceptance C-5..C-8 (*chọn HÌNH ≠ chấp-nhận VỠ*) · (2) panel-3 rỗng = **CHỈ hộp dashed y PE** ⇒ **đóng miễn-trừ-thứ-7** · (3) file-khi-duyệt = **upload THẬT** ⇒ BE-7 `+ApprovalAttachment=3` policy `KeHoachKyKet.Update`, bẫy F-6 "UI chết" tự tan
|
||||
- [ ] D — giao Opus 5 MAX thực-thi (giới-hạn phiên reset 18:10, phóng 18:50)
|
||||
|
||||
## Synthesis
|
||||
|
||||
(chờ)
|
||||
### Lead refute — 3 chốt lead ÁP LÊN bản invest sau phản-biện
|
||||
|
||||
1. 🔴 **Panel 1 KHÔNG dùng `PipelineTreePanel`** (đảo §5 bản invest). 4/5 phát-hiện nghiêm nhất hội tụ đúng chỗ này: `canPe` khoá cây ⇒ user **chỉ có quyền KHKK mất TRẮNG danh-sách** (display-layer khoá authz-layer đang mở — mặt còn lại #82) · trần 200 = **mất đường tới phiếu**, không có "trang 2" · `usePipelineStages.ts:211` cây **tự navigate rời màn 3-panel** · `content` **nuốt `groups`** im lặng (`PipelineStageFolders.tsx:285-287`). Mirror trung-thành = **KHKK dựng cây từ danh-sách CỦA CHÍNH KHKK** (đúng như PE dùng `yearGroups` từ query `pe-list`, KHÔNG dùng `PipelineTreePanel`) ⇒ 3 rủi-ro tan hết.
|
||||
2. 🔄 **Miễn-trừ 6 → 3.** P1-4 lật GIỐNG-được (khác nguồn dữ-liệu ≠ khác bố-trí) · P2-7 lật GIỐNG-được (`readOnly` là prop HOST, `PurchaseEvaluationsListPage.tsx:663` — không cần workspace) · P3-12 hạ xuống CHỜ-ANH (**lead đo `DbInitializer.cs:554`: 3 Bước × ≤2 Cấp = tới 6 trạm, seed `:620` tự ghi "sao từ workflow Duyệt NCC prod"** ⇒ tiền-đề *"3 trạm ngắn"* SAI đo được). **+ miễn-trừ THỨ 7 không khai** (F-5: §5 âm-thầm đảo P3-16 panel-3 empty-state) → đẩy thành CÂU HỎI 2.
|
||||
3. ✅ **Giữ quyết-định A, THAY lý-do.** Lý-do cũ (*"cả hai đường generic đều đụng PE prod"*) là **nhị-phân giả** — có đường (iii) adapter 1 chiều nằm trọn trong KHKK, 0 dòng PE. Lý-do đúng: trích `StepOpinionsBox` khỏi `components/pe/` **mâu-thuẫn trực-tiếp acceptance A-10 của chính nó**. *Kết-luận đúng, chứng-minh hỏng* — giữ kết-luận, sửa chứng-minh.
|
||||
|
||||
### 6 acceptance của bản invest bị bẻ gãy khi CHẠY THẬT (đã thay bản sửa trong spec)
|
||||
|
||||
| Gốc | Vỡ thế nào |
|
||||
|---|---|
|
||||
| **A-13** (canh QUYỀN) | Vỡ **cả hai đầu**: `:15` comment chứa "Authorize"+"Policy" ⇒ **bơm tử-số**; `:17 :236 :241` comment *nhắc tên* `[Authorize]` trần ⇒ vế "0 dòng trần" **FAIL trên file 100% sạch** (citation-trap). → thay bằng neo `^\s*\[` + fault-inject chứng răng |
|
||||
| **A-6** (canh bug S155) | **Kiểm NHẦM KHOÁ** — cấm `phase` (vô hại, transient) và bỏ sót `filter`/`view` (đúng 2 khoá làm vỡ) ⇒ `setSearchParams({group,filter:'DaDuyet'})` = **đúng bug S155** vẫn PASS. Inverted check |
|
||||
| **A-10** (canh hồi-quy) | **Đo sai trục** — thiếu `hooks/usePipelineStages.ts` + `components/pipeline/` ⇒ sửa 2 path đó có thể làm **trang PE mất 8 ngăn GĐ2** mà A-10 PASS 100% |
|
||||
| **A-7b** | Chạy trên **chính mẫu PE** ra **3 hit**, không phải 5 ⇒ implementer viết ĐÚNG sẽ FAIL; cách duy nhất PASS = **bỏ số đếm động cho vừa regex** = Goodhart |
|
||||
| **A-7d** | Đo **chuỗi chữ**, mục-tiêu là **vị-trí khối** ⇒ đổi tiêu-đề "Ý kiến duyệt" là PASS trong khi khối vẫn ở panel 3 |
|
||||
| **A-8c** | Grep-trên-source ⇒ khối **UI CHẾT vẫn PASS 100%** (không có đường ghi attachment + khối tự-ẩn khi rỗng ⇒ không bao giờ render trên UAT) |
|
||||
|
||||
### 3 khối owner NHÌN THẤY trong ảnh mà bản invest đánh rơi
|
||||
|
||||
**F-1** EmptyState panel 2 *"Chọn phiếu ở danh sách"* (Ảnh 1 — trạng-thái ĐẦU TIÊN người dùng thấy; bản invest ghi ở §1.2 rồi rơi khỏi §3/§5/§7) · **F-2** chip `⚑ Kết thúc · Phòng N` trên card panel 1 (Ảnh 3 — **0 công BE**, cột Mig 71 đã có) · **F-3** liệt-kê NV mỗi Cấp, người duyệt in đậm + NV khác `/ Tên` mờ (Ảnh 2).
|
||||
|
||||
### Tin tốt được verify độc-lập (đừng cắt ngân-sách vì nghi ngờ)
|
||||
|
||||
Lane feasibility **vào để lật D-2 và không lật được**: `LogSigningPlanTransitionAsync` **6 call-site phủ TRỌN** Pending/Approve/**Reject-Return** (`:204 :283 :296 :311 :325 :393`), `Approvals.Add` ở `:534` **không `if` nào bọc** ⇒ entry *"Trả lại → Lùi về Cấp 2"* của Ảnh 2 **SẼ CÓ dữ-liệu**. Và **A-0 chặn-cửa GỠ ĐƯỢC** — `usePipelineLinkage()` đã có sẵn `khkkByPeId` (`:123-134,147`), **0 query mới**.
|
||||
|
||||
### Ngược lại — 1 chỗ RẺ bị ước-lượng sai chiều
|
||||
|
||||
`FinalizeStepName`/`FinalizeLevelName` **không phải "field append"**: `ContractSigningPlan.cs:51` chỉ có `bool EndedByLevelFinalize`, **không cột nào lưu Bước/Cấp nào**. Phải **dẫn xuất** — port ~20 dòng projection **3 subquery** từ `PurchaseEvaluationFeatures.cs:665-683`. Bản invest cảnh-báo *"dễ sai đắt nhất theo cả hai chiều"* rồi **tự dính chiều còn lại**.
|
||||
|
||||
@ -0,0 +1,278 @@
|
||||
# spec — KHKK UI mirror Duyệt NCC (3 panel đồng-nhất)
|
||||
|
||||
> **Pipeline H21:** engine propose → **lead verify** → **lead ghi (file này)** → worker thực-thi.
|
||||
> **Nguồn:** `/fable-real investigator-codebase` (Fable 5) + `/fable-clone reviewer` ×3 lăng-kính (Opus).
|
||||
> **Trạng-thái review:** feasibility **8 HELD/6 BROKE** · regression **8 HELD/4 BROKE/10 bỏ-sót** · fidelity **~12 HELD/6 BROKE/3 bỏ-sót** *(2 mục cuối lane fidelity chết dở — verdict do lead derive từ §7 bảng của chính nó, KHÔNG phải lane tự kết; phép đo §6 còn thiếu thì lead đã tự chạy, kết-quả ở ②-D dưới)*.
|
||||
> **Đọc kèm:** `sub-investigator-codebase-invest-s171.md` (bản-đồ đầy-đủ) · 3 file `sub-reviewer-lens-*.md` (phản-biện).
|
||||
|
||||
---
|
||||
|
||||
## ① TÍNH-NĂNG / ĐỀ-BÀI
|
||||
|
||||
Đưa màn **Kế hoạch Ký kết HĐ (KHKK — giai-đoạn 2)** về **cùng bố-cục 3 panel** với **Duyệt NCC (PE — giai-đoạn 1)**.
|
||||
|
||||
**Owner (verbatim):**
|
||||
> "điều chỉnh lại giao diện chỗ KKHĐ lại nhé, lấy của Duyệt NCC làm mẫu: Panel 1: Hình gửi 1 · Panel 3: Duyệt và History tương tự (Hình 2) · Panel 2 - Giữa: Cấu trúc tương tự như duyệt NCC (Phần content nhập phía trên, duyệt ghi chú phía dưới) - Hình 3."
|
||||
> "cách bố trí toàn diện cho tất cả, **giống nhất** với duyệt NCC, **chỉ khác đúng phần khác, tương tự thì giống nhau, đồng nhất**."
|
||||
|
||||
**Vì sao việc này chính-đáng (lý-do KHÔNG nằm trong ảnh):** GĐ1 (`PurchaseEvaluationsListPage.tsx:381,397`) và GĐ3 (`ContractsListPage.tsx:106,125`) đều đã là `h-[calc(100vh-4rem)]` + 3 cột. GĐ2 là `space-y-5` cuộn-body 2 panel (`KhkkListPage.tsx:161,197`) ⇒ **mắt-xích lệch duy nhất trong 4 giai-đoạn**. Đây là **đổi lớp kiến-trúc**, không phải sửa CSS.
|
||||
|
||||
### Phân-loại cuối (sau phản-biện) — 29 GIỐNG-được · **3** KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG · **2 CHỜ-ANH**
|
||||
|
||||
Bản invest báo 6 miễn-trừ. Sau khi áp đúng luật owner (*"chỉ khác đúng phần khác"* ⇒ mỗi dòng KHÁC là **một lần xin miễn-trừ**, gánh nặng chứng-minh thuộc người xin), **3 dòng không đứng vững**:
|
||||
|
||||
| Dòng | Bản invest phán | Chốt cuối | Vì sao |
|
||||
|---|---|---|---|
|
||||
| P1-4 nhóm N1-N8 | KHÁC-buộc | 🔄 **GIỐNG-được** | Khác **nguồn dữ-liệu**, KHÔNG khác **bố-trí** — chính nó tự viết *"CHỖ ĐẶT phải giống"*. PE để `Select` quy-trình ở khối filter panel 1; KHKK để `Select` nhóm **đúng ô đó**. Dán nhãn KHÁC làm phồng mẫu-số miễn-trừ |
|
||||
| P2-7 inline-edit vs readOnly | KHÁC-buộc | 🔄 **GIỐNG-được** | `readOnly` là **prop do HOST truyền** (`PurchaseEvaluationsListPage.tsx:663` truyền `readOnly={true}`; `PeDetailTabs.tsx:427` có nhánh `mode`). Mệnh-đề *"muốn giống thì phải dựng workspace"* **sai** — host chỉ cần truyền `readOnly={phiếu ≠ Nháp \|\| user ≠ drafter}`. Đường sửa KHÔNG bị cắt |
|
||||
| P3-12 bốn return-mode | KHÁC-buộc | 🔄 **CHỜ-ANH (hoãn)** | Tiền-đề *"KHKK là 3 trạm ngắn nên lùi-1-cấp ≈ về-đầu"* — **lead đo, SAI**: `DbInitializer.cs:554` roster = `(StepName, DeptCode, Level1[], Level2[], Level2Finalize)` ⇒ **3 Bước × tối đa 2 Cấp = tới 6 trạm**, và seed `:620` tự ghi *"đội hình **sao từ workflow Duyệt NCC prod**"* ⇒ hình dạng **y hệt PE**. Vế còn lại (*"đổi state machine BE / ngoài phạm-vi"*) là lý-do **chi-phí**, không mua được miễn-trừ ⇒ gọi đúng tên: **hoãn**, không phải buộc-khác |
|
||||
|
||||
**3 miễn-trừ THẬT còn lại** (đều có lý-do nghiệp-vụ, không dòng nào lấy *"code đang thế"* làm bằng-chứng):
|
||||
- **P2-3** nội-dung Section: GĐ2 không có báo-giá/so-sánh NCC (đã chốt ở GĐ1), có căn-cứ hồ-sơ TVGS. Ép 5 Section trùng tên PE sẽ đẻ 2 Section rỗng. *"Giống nhau"* ở đây = giống **KHUNG** (đánh số · `divide-y` · ý-kiến dưới cùng), không phải giống chữ.
|
||||
- **P3-7** badge `✎ NS PRO/CCM`: quyền nhập ngân-sách per-role là khái-niệm riêng PE (`PeWorkItemBudgets`, Mig 50). KHKK không có cột đó ⇒ badge vô nghĩa.
|
||||
- **P3-13** bộ chọn giá chốt: KHKK chốt giá **per-Line** ở choke-point BE `ApplyApprovedValuesOnFinalize` — không có "1 giá" để người duyệt chọn (khác **cardinality**, không khác ý-thích).
|
||||
|
||||
### 🔴 Miễn-trừ THỨ 7 — KHÔNG ĐƯỢC KHAI (fidelity F-5), phải hỏi anh
|
||||
|
||||
§3 bản invest phán **P3-16** *"empty-state panel 3 (hộp dashed + `X` + 'Quy trình duyệt sẽ hiện khi chọn phiếu.') = **GIỐNG-được**, Ảnh 1 có"*. Nhưng §5 bố-cục đích lại ghi *"panel 3 chưa chọn → `PipelineTreePanel currentStage={2}`"*, và acceptance A-8e **đóng đinh bản thay-thế**. ⇒ Một dòng đã phán GIỐNG-được bị **đảo âm-thầm giữa §3 và §5**, đúng vào khối anh nhìn thấy ở Ảnh 1.
|
||||
Theo luật owner, dạng này nguy nhất: **không phải khác — mà là khác mà không nói.**
|
||||
✅ **ĐÃ ĐÓNG — owner chốt 2026-08-04 (QĐ-2):** panel 3 khi chưa chọn = **CHỈ hộp dashed y PE**, quay về đúng Ảnh 1. Xem bảng *3 QUYẾT-ĐỊNH OWNER* cuối file.
|
||||
|
||||
---
|
||||
|
||||
## ② CÁCH IMPLEMENT
|
||||
|
||||
### ②-A. Quyết-định kiến-trúc: **Phương-án A (tạo `components/khkk/`, chép khuôn)** — GIỮ, nhưng **THAY LÝ-DO**
|
||||
|
||||
Bản invest bác phương-án B (generic-hoá component PE dùng chung) bằng lập-luận: *"phải **hoặc** ép BE KHKK precompute `status`, **hoặc** nhồi adapter 2 chiều — **cả hai đều đụng đường duyệt PE đang chạy prod"*.
|
||||
|
||||
🔴 **Lập-luận này BROKE (feasibility §5.2) — nhị-phân giả.** Có đường (iii) nó không xét: KHKK **đã có sẵn** hàm suy status 15 dòng thuần-tuý không side-effect (`KhkkWorkflowPanel.tsx:106-120`); chỉ cần KHKK **tự chuẩn-hoá cây thô → shape có `status`** trước khi truyền vào component generic — adapter **MỘT chiều, nằm TRỌN trong nhánh KHKK, 0 dòng PE bị sửa**.
|
||||
|
||||
✅ **Nhưng QUYẾT-ĐỊNH A vẫn đứng, vì một lý-do khác mạnh hơn:** trích `StepOpinionsBox` ra khỏi `PeDetailTabs.tsx:705-774` **buộc phải sửa file trong `components/pe/`** — mâu-thuẫn trực-tiếp với acceptance **A-10** của chính bản đó (*"`git diff --name-only` → 0 path khớp `components/pe/`"*). Cộng ≥6 nhánh riêng của PE (NS PRO/CCM · price-picker · 4 return-mode · CCM-done · skip-to-final · DeptApprovals legacy) ⇒ generic sẽ đầy `if (module === …)`.
|
||||
|
||||
> 🔴 **Ghi vào đầu mỗi file `components/khkk/` (điều-kiện thoát, chống nợ im-lặng):**
|
||||
> *"Khối ý-kiến cấp duyệt là bản thứ 3 của cùng một hình (PE `PeDetailTabs.tsx:705-774` · HĐ `components/contracts/WorkflowHistoryPanel.tsx`). Gộp được khi và chỉ khi BE 3 module thống-nhất shape `status` precompute."*
|
||||
|
||||
### ②-B. 🔴 Panel 1 = **`PipelineTreePanel`** — OWNER CHỐT 2026-08-04 (bác đề-xuất đảo của lead)
|
||||
|
||||
> **QUYẾT-ĐỊNH OWNER (ratified):** anh chọn **giữ đúng đề-xuất bản invest** — panel 1 = cây toàn-trình `PipelineTreePanel currentStage={2}`. Lead đã trình phương-án đảo (danh-sách riêng KHKK) **kèm đủ 4 rủi-ro đo được ngay trên thẻ chọn**; anh đọc và vẫn chọn cây. ⇒ **Đây là quyết-định có thông-tin đầy-đủ, thi-hành nguyên vẹn.**
|
||||
>
|
||||
> 🔴 **NHƯNG: chọn HÌNH ≠ chấp-nhận VỠ.** 4 rủi-ro dưới đây **không biến mất vì owner chọn cây** — chúng chuyển từ *"lý-do đổi thiết-kế"* thành ***việc kỹ-thuật BẮT BUỘC phải xử***. Mỗi cái có mitigation cưỡng-hành + acceptance riêng. **CẤM** thi-hành cây mà bỏ mitigation — làm thế là biến quyết-định của owner thành cái cớ cho một hồi-quy im-lặng.
|
||||
|
||||
#### 4 rủi-ro + mitigation CƯỠNG-HÀNH (đi kèm, không tách rời)
|
||||
|
||||
| Rủi-ro | Bằng-chứng | 🔧 Mitigation BẮT BUỘC |
|
||||
|---|---|---|
|
||||
| **M-1 🔴 CAO — `canPe` khoá cây** ⇒ user **chỉ có quyền KHKK** (VD người duyệt nhóm N5) thấy panel 1 **TRỐNG**, trong khi API `GET /contract-signing-plans` gate `KeHoachKyKet.Read` **vẫn cho họ đọc**. Display-layer khoá authz-layer đang mở — mặt còn lại gotcha #82 | `usePipelineStages.ts:55,57,83-84` (`canPe` = 3 key PE) · `PipelineTreePanel.tsx:117 enabled:canPe` · `:222-226` in *"Bạn chưa có quyền xem phiếu Duyệt NCC…"* | 🔴 **FALLBACK `!canPe` → render DANH-SÁCH KHKK phẳng** (card, cùng khuôn, không cây) trong đúng ô panel 1, nguồn `GET /contract-signing-plans`. Cây là **lớp làm giàu**, KHÔNG phải **điều-kiện để thấy phiếu**. ⇒ acceptance **C-5** (runtime, tài-khoản KHKK-only) |
|
||||
| **R-11 trần 200 phiếu** ⇒ gói thầu rơi khỏi 200 mới nhất thì **mọi phiếu KHKK của gói đó biến mất**, và cây **không có "trang 2"** | `PipelineTreePanel.tsx:22 PE_INDEX_PAGE_SIZE=200` · `:112` · cảnh-báo `:252-256` | 🔴 Giữ **ô search + Select trạng-thái server-side** trong khối filter panel 1 → khi lọc, đi thẳng `GET /contract-signing-plans` (**không qua cây**) ⇒ luôn có đường tới phiếu cũ. **Và** dòng cảnh-báo `:252-256` **PHẢI hiện**, không được nuốt |
|
||||
| **M-2 🔴 CAO — cây tự văng khỏi màn 3-panel**: bấm node KHKK ⇒ `navigate('/khkk/'+id)` rời trang vừa dựng | `usePipelineStages.ts:211` (×2 app) | 🔴 Đổi `onOpen` thành **callback do host truyền**: `onOpen: opts?.onSelectKhkk ?? (() => navigate(…))` — host `/khkk/list` truyền `id => setParam({id})`. Giữ nguyên hành-vi cũ cho **mọi call-site khác** (mặc-định navigate) ⇒ 0 hồi-quy trang PE/GĐ3 |
|
||||
| **B1 🔴 `content` nuốt `groups` im lặng**: `PipelineStageFolders.tsx:285-287` đặt `content` **TRƯỚC** `groups` trong chuỗi tam-nguyên ⇒ truyền `stage2Content` thì **8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" biến mất, không lỗi, không cảnh-báo**; `:95-97` quên `stage2Count` ⇒ badge về **0** | tự đo `grep -n -C4` | 🔴 **KHAI TRADE-OFF, không im:** trong cây của KHKK, folder GĐ2 hiện **card phiếu** thay **8 ngăn nhóm** — chấp nhận được **vì `Select` nhóm N1-N8 đã có ở khối filter panel 1** (P1-4). **BẮT BUỘC** truyền `stage2Count` cùng lúc. **Additive**: `PurchaseEvaluationsListPage` không truyền `stage2Content` ⇒ **trang PE giữ nguyên 8 ngăn** ⇒ acceptance **E-2** |
|
||||
|
||||
#### 🔸 Ghi lại cho người đọc sau (bối-cảnh quyết-định, KHÔNG phải để lật lại)
|
||||
|
||||
Lead từng đề-xuất đảo sang *"panel 1 = danh-sách riêng của KHKK"* với lập-luận: panel 1 của PE **không phải** `PipelineTreePanel` mà là **danh-sách của chính PE** tự gom cây `Năm > Dự án > Hạng mục` từ `yearGroups` (`PurchaseEvaluationsListPage.tsx:220-262`, nguồn = query `pe-list`), tầng 4 mới gọi `buildStages(wg.items, {stage1Content})` (`:524`). Owner **đã nghe và chọn cây** — nên hướng thi-hành là cây + 4 mitigation trên. Đoạn này giữ để người sau hiểu **vì sao có mitigation**, đừng đọc thành lời mời mở lại tranh-luận.
|
||||
|
||||
---
|
||||
|
||||
*(Phần dưới giữ nguyên phân-tích gốc để tra-cứu — 4 rủi-ro đã có mitigation ở bảng trên.)*
|
||||
|
||||
| Nếu panel 1 = `PipelineTreePanel` (bản invest) | Hậu-quả đo được |
|
||||
|---|---|
|
||||
| **M-1 🔴 `canPe` khoá cây** — `usePipelineStages.ts:55,57,83-84`: `canPe = ['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List'].some(can)`; `PipelineTreePanel.tsx:117 enabled:canPe`, `:222-226` in *"Bạn chưa có quyền xem phiếu Duyệt NCC…"* | Người **chỉ có quyền KHKK** (VD người duyệt nhóm N5) ⇒ `canPe=false` ⇒ **mất TRẮNG danh-sách phiếu của chính mình**, trong khi API `GET /contract-signing-plans` gate `KeHoachKyKet.Read` **vẫn cho phép**. Đây là **display-layer khoá cái authz-layer đang mở** — mặt còn lại của gotcha #82. **Mất chức-năng, không phải mất thẩm-mỹ.** |
|
||||
| **R-11 trần 200** — `PipelineTreePanel.tsx:22 PE_INDEX_PAGE_SIZE=200`, cây dựng từ **chỉ-mục PE** | Gói thầu rơi khỏi 200 ⇒ **mọi phiếu KHKK của gói đó biến mất khỏi panel 1**. Hôm nay vô hại vì panel 1 là **bảng có phân-trang + tìm-kiếm server-side** (`KhkkListPage.tsx:112-129`, `total` thật từ server). Đổi sang cây = **thay danh-sách-có-phân-trang bằng cây-cắt-cụt**; dòng cảnh-báo 10px xám **không có "trang 2"** |
|
||||
| **M-2 🔴 cây tự văng khỏi màn** — `usePipelineStages.ts:211 onOpen: () => navigate('/khkk/'+k.id)` (×2 app) | Bấm node KHKK trong panel 1 ⇒ **rời màn 3-panel vừa dựng**, thay vì chọn vào panel 2 |
|
||||
| **B1 🔴 `content` nuốt `groups`** — `PipelineStageFolders.tsx:285-287` chuỗi tam-nguyên đặt `content` **TRƯỚC** `groups`; `:95-97` `stageCount` khi có `content` đọc `count ?? 0` | Truyền `stage2Content` ⇒ **8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" biến mất, không lỗi, không cảnh-báo**; quên `stage2Count` ⇒ badge về **0** trong khi folder đầy phiếu |
|
||||
|
||||
🔴 **Nhưng đọc kỹ mẫu PE thì hoá ra bản invest chép nhầm ĐỐI-TƯỢNG:** panel 1 của PE **KHÔNG phải** `PipelineTreePanel`. Nó là **danh-sách CỦA CHÍNH PE**, tự gom cây `Năm > Dự án > Hạng mục` từ `yearGroups` (`PurchaseEvaluationsListPage.tsx:220-262`, nguồn = query `pe-list`), rồi tầng 4 mới gọi `buildStages(wg.items, {stage1Content: <ul>…</ul>})` (`:524`) để re-parent phiếu vào folder GĐ1.
|
||||
|
||||
⇒ **Mirror TRUNG-THÀNH = KHKK panel 1 dựng cây từ DANH-SÁCH CỦA CHÍNH KHKK**, đổ card vào folder **GĐ2** — đúng đối-xứng với PE đổ vào GĐ1. Và làm đúng thì **M-1 · R-11 · M-2 tan hết**, vì nguồn dữ-liệu là `GET /contract-signing-plans` (đã gate `KeHoachKyKet.Read`, có phân-trang server, có `total` thật), **không đụng `canPe`, không đụng trần 200, không có `navigate` của hook**.
|
||||
|
||||
**Về B1 (`content` nuốt `groups`) — trade-off PHẢI KHAI, không được để im:**
|
||||
- Thêm `stage2Content`/`stage2Count` vào `BuildStagesOptions` là **additive**: `PurchaseEvaluationsListPage` không truyền nên **giữ nguyên 8 ngăn** ⇒ **trang PE KHÔNG bị đụng**.
|
||||
- Trong **cây của chính KHKK**, truyền `stage2Content` ⇒ folder GĐ2 hiện **card phiếu** thay cho **8 ngăn nhóm**. Chấp nhận được **vì bộ lọc nhóm N1-N8 đã có mặt dạng `Select` ở khối filter panel 1** (P1-4) — không mất đường tới nhóm nào.
|
||||
- 🔴 **BẮT BUỘC:** truyền `stage2Count` cùng lúc (nếu không badge = 0), và **acceptance A-10 phải mở rộng path** (xem ③).
|
||||
|
||||
### ②-C. Bố-cục đích
|
||||
|
||||
```
|
||||
/khkk/list ← host 3-panel; khuôn ContractsListPage.tsx (GĐ3, 255 dòng) — KHÔNG chép
|
||||
PurchaseEvaluationsListPage.tsx (807 dòng, mang theo deleted/pendingMe/awId/overlay)
|
||||
🔸 CẢNH-BÁO: ContractsListPage.tsx CHỈ TỒN-TẠI Ở fe-admin (regression M-6).
|
||||
Port sang fe-user = VIẾT MỚI, không phải copy.
|
||||
|
||||
├─ Panel 1 400px components/khkk/KhkkListPanel.tsx ← OWNER CHỐT: cây PipelineTreePanel
|
||||
│ khối filter: search icon-trong-ô · Select trạng-thái · Select nhóm N1-N8
|
||||
│ (search/trạng-thái đi SERVER-SIDE, KHÔNG qua cây — mitigation R-11)
|
||||
│ cây: PipelineTreePanel currentStage={2}
|
||||
│ 📅 Năm > 📁 Dự án > 🧱 Hạng mục > folder GĐ2 = card phiếu KHKK
|
||||
│ onOpen = callback host (KHÔNG navigate — mitigation M-2)
|
||||
│ stage2Content + stage2Count truyền CẶP (mitigation B1)
|
||||
│ 🔴 FALLBACK !canPe → danh-sách KHKK phẳng cùng ô (mitigation M-1)
|
||||
│ 🔴 GIỮ dòng cảnh-báo "Cây tải 200 phiếu mới nhất…" (PipelineTreePanel.tsx:252-256)
|
||||
│ card 3 hàng: [chip ⚑ Kết thúc·Phòng N] + tên gói thầu + badge trạng-thái
|
||||
│ / mã font-mono + timestamp / 👤 người soạn + Σ tiền
|
||||
│
|
||||
├─ Panel 2 1fr components/khkk/KhkkDetailContent.tsx
|
||||
│ chưa chọn → EmptyState "Chọn phiếu ở danh sách" 🔴 F-1, xem ②-E
|
||||
│ <PePipelineStrip current={2} />
|
||||
│ <div className="divide-y divide-slate-200">
|
||||
│ Section 1. Thông tin kế hoạch
|
||||
│ Section 2. Dòng giá per-NCC trúng thầu (N NCC)
|
||||
│ Section 3. Căn cứ hồ sơ (b.8-9)
|
||||
│ Section 4. File đính kèm
|
||||
│ Section 5. Ý KIẾN CẤP DUYỆT (sign-off theo workflow) ← Ảnh 3 khoanh đỏ
|
||||
│ readOnly = prop TỪ HOST (P2-7 đã lật): true khi phiếu ≠ Nháp HOẶC user ≠ drafter
|
||||
│
|
||||
└─ Panel 3 360px components/khkk/KhkkWorkflowPanel.tsx (dời từ pages/)
|
||||
chưa chọn → ✅ OWNER CHỐT: **CHỈ hộp dashed y PE** (khuôn PurchaseEvaluationsListPage.tsx:669-676
|
||||
— border-dashed + icon X + "Quy trình duyệt sẽ hiện khi chọn phiếu.")
|
||||
🔴 KHÔNG nhét PipelineTreePanel vào đây (khác GĐ3 — CÓ CHỦ ĐÍCH, vì cây
|
||||
đã sống ở panel 1 theo QĐ owner câu 1 ⇒ nhét lần 2 = trùng)
|
||||
⇒ miễn-trừ-thứ-7 mà bản invest không khai nay đã ĐÓNG: quay về đúng Ảnh 1
|
||||
đã chọn → 1 "Quy trình duyệt" + meta {code} v{NN} · {name}
|
||||
2 Banner kết-thúc-sớm "… không qua CEO"
|
||||
3 Sơ-đồ Bước → Cấp → NV (+chip phòng · +⚑ Duyệt thay CEO
|
||||
· +mờ sau finalize · +liệt-kê NV 🔴 F-3)
|
||||
4 Nút Duyệt / Trả lại / Từ chối + Dialog + nút Xóa
|
||||
5 📎 File đính kèm khi duyệt (🔴 F-6 — xem ②-E)
|
||||
6 Lịch sử duyệt (N) (🔴 B4 — xem ②-E)
|
||||
7 Lịch sử thay đổi
|
||||
```
|
||||
|
||||
`/khkk/:id` **GIỮ NGUYÊN route** (deep-link · bookmark · fallback `<lg`), chỉ đổi ruột thành `KhkkDetailContent` + `KhkkWorkflowPanel` xếp dọc — khuôn `PurchaseEvaluationDetailPage` (`:798-805`).
|
||||
|
||||
### ②-D. Việc BE (nhỏ hơn tưởng, nhưng KHÔNG phải "+3 field")
|
||||
|
||||
🟢 **Tin tốt đã được verify độc-lập:** dữ-liệu lịch-sử **ĐÃ ĐƯỢC GHI ĐẦY ĐỦ TỪ ĐẦU**. Lane feasibility vào để lật claim này và **không lật được**: `LogSigningPlanTransitionAsync` có **6 call-site phủ TRỌN** mọi transition — `:204` Pending (gửi duyệt) · `:283` `:296` `:311` `:325` Approve · **`:393` Trả lại VÀ Từ chối** (`ApprovalDecision.Reject`). `db.ContractSigningPlanApprovals.Add(...)` ở `:534` nằm **thẳng trong thân hàm, không `if` nào bọc**.
|
||||
⇒ **Entry "Trả lại → Lùi về Cấp 2" của Ảnh 2 SẼ CÓ dữ-liệu.** Đừng cắt ngân-sách mục này vì nghi ngờ.
|
||||
|
||||
🔴 **Đường-dẫn đúng** (bản invest ghi thiếu lớp, người sau sẽ mở trượt):
|
||||
`src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs` — **KHÔNG** phải `Application/ContractSigningPlans/`.
|
||||
|
||||
| # | Việc | Ghi-chú bắt buộc |
|
||||
|---|---|---|
|
||||
| BE-1 | `ContractSigningPlanDetailDto` (`ContractSigningPlanFeatures.cs:145-181`, record positional 37 tham-số) **+**: `WorkflowVersion` · `EndedByLevelFinalize` · `FinalizeStepName` · `FinalizeLevelName` · `List<ContractSigningPlanApprovalDto> Approvals` | 🟢 Chỉ **1 call-site** `new ContractSigningPlanDetailDto` (`:784`) ⇒ append RẺ |
|
||||
| BE-1b | 🔴 **`FinalizeStepName`/`FinalizeLevelName` KHÔNG phải "field append"** — `ContractSigningPlan.cs:51` chỉ có `bool EndedByLevelFinalize`, **không có cột nào lưu Bước/Cấp nào đã kết-thúc** ⇒ phải **DẪN XUẤT**: port ~20 dòng projection có **3 subquery** + nhánh 3-ngả từ `PurchaseEvaluationFeatures.cs:665-683` | Bản invest ước "+3 field" = **sai chiều RẺ**. Nó tự cảnh-báo *"dễ ước-lượng sai đắt nhất theo cả hai chiều"* rồi dính chiều còn lại |
|
||||
| BE-2 | 🔴 **`ContractSigningPlanApprovalDto` CHƯA TỒN-TẠI** (`grep` → **0 hit**) ⇒ đẻ record mới + projection + mirror `types/khkk.ts` ×2 app | A-1 viết như thể chỉ "thêm field" |
|
||||
| BE-3 | `ApprovedByUserId` **không kèm tên** (`:534-544` 8 field, không có tên) trong khi hàng changelog ngay dưới (`:546-557`) **CÓ** `UserName` denorm | 🔸 Rẻ hơn join Users: 2 bảng ghi **cặp 1-1 cùng unit-of-work cùng `now`** ⇒ lấy tên từ changelog cùng transition, **hoặc** denorm thêm cột tên vào Approval cho khớp anh-em |
|
||||
| BE-4 | `ContractSigningPlanWorkflowStepDto` **+`DepartmentName`** (join `Departments`) | Nuôi **2 chỗ**: chip phòng panel 3 (P3-4) **và** header nhóm Section 5 (P2-5) |
|
||||
| BE-5 | `GET /contract-signing-plans/{id}/changelogs` | 🔴 **`[Authorize(Policy = "KeHoachKyKet.Read")]`** — gotcha #85 (đúng key endpoint, KHÔNG OR key con) |
|
||||
| BE-6 | `.Include`/join `ApprovalWorkflowLevels` cho `Approvals` nếu muốn hiện "Cấp nào" (Ảnh 2 có *"Lùi về **Cấp 2**"*) — `ApprovalWorkflowLevelId` là Guid trần (`:541`) | |
|
||||
| **BE-7** ✅ | **OWNER CHỐT — file đính kèm khi duyệt, LÀM THẬT:** `ContractSigningPlanAttachment.cs:9-12` **+`ApprovalAttachment = 3`** (append-only, **KHÔNG** đánh số lại `Other=99`) + **endpoint upload** gắn vào luồng dialog duyệt + lưu `purpose=ApprovalAttachment` | 🔴 Endpoint mới ⇒ **`[Authorize(Policy = "KeHoachKyKet.Update")]`** (ghi, không phải Read) — gotcha #85 dùng ĐÚNG key của endpoint, **KHÔNG** OR key con. 🔴 **KHÔNG** chép khuôn authz của `PurchaseEvaluationsController` (xem cảnh-báo BE-5) |
|
||||
|
||||
> 🔴 **BE-5 — CẢNH-BÁO TẠI CHỖ, không chỉ ở checklist (regression M-5):** mẫu PE mà lệnh *"mirror PE"* trỏ tới **CHÍNH LÀ một lỗ #82**:
|
||||
> `PurchaseEvaluationsController.cs:15 [Authorize]` **trần (any-auth)** + `:281 [HttpGet("{id:guid}/changelogs")]` **KHÔNG có** `[Authorize(Policy=…)]` ⇒ endpoint changelogs của PE hiện chỉ cần **đăng-nhập bất-kỳ** là đọc được lịch-sử phiếu.
|
||||
> **Implementer trung-thành sẽ chép nguyên — đó là cách #82 lan sang KHKK. ĐỪNG CHÉP.** (Vào sổ riêng: PE changelogs = ứng-viên #82 thứ hai bên cạnh `POST /api/contracts` đã treo.)
|
||||
|
||||
### ②-E. 5 khối bị bỏ sót / bẫy hiển-thị — PHẢI làm, không được im
|
||||
|
||||
| # | Việc | Vì sao |
|
||||
|---|---|---|
|
||||
| **F-1** 🔴 | **EmptyState panel 2 "Chọn phiếu ở danh sách"** + 2 nhánh `Đang tải…` / `Không tìm thấy phiếu.` — mẫu `PurchaseEvaluationsListPage.tsx:644-656` | Bản invest **ghi sự-thật này ở §1.2 rồi đánh rơi**: 0 dòng §3, 0 chỗ §5, 0 acceptance §7. Host 3-panel mà panel 2 trống trơn = **màn hình trắng giữa**, sai Ảnh 1 ở đúng trạng-thái **ĐẦU TIÊN** người dùng thấy |
|
||||
| **F-2** 🔴 | **Chip `⚑ Kết thúc · Phòng N` trên card panel 1** (`PeFinalizeChip`) | Anh **nhìn thấy nó trong Ảnh 3**; P1-5 liệt-kê card thiếu đúng con chip này. Và nó **0 công BE** — cột `EndedByLevelFinalize` đã có (Mig 71). Bỏ sót khối vừa-thấy-trong-ảnh vừa-rẻ-nhất |
|
||||
| **F-3** 🔴 | **Liệt-kê NV trong mỗi Cấp**: người duyệt thật **in đậm**, NV cùng cấp còn lại `/ Tên` mờ — mẫu `PeWorkflowPanel.tsx:453-465` | Ảnh 2 mục 3 có. §3 chỉ có dòng cho *badge vai*, **không có dòng cho bản-thân danh-sách NV** ⇒ panel 3 sẽ **không giống Ảnh 2** dù A-8a/b/c/d PASS hết |
|
||||
| **F-6** ✅ | **📎 File đính kèm khi duyệt — OWNER CHỐT: LÀM THẬT, cho upload.** ⇒ **BE-7** (dưới): `ContractSigningPlanAttachment.cs:9-12` **+`ApprovalAttachment = 3`** (enum append-only, đúng comment nhà *"Append-only khi cần thêm mục đích mới"*) + **endpoint upload trong dialog duyệt** + FE khối đọc `purpose=ApprovalAttachment`, tự-ẩn khi rỗng (khuôn `PeWorkflowPanel.tsx:928-967`) | Chọn đường này ⇒ khối **SỐNG THẬT** trên UAT, và bẫy *"UI chết mà acceptance vẫn xanh"* **tự tan** — vì nay có đường ghi. Acceptance **D-9** đổi sang runtime: upload 1 file lúc duyệt ⇒ khối phải HIỆN |
|
||||
| **B4** 🔴 | **`Decision = Pending` của hành-vi GỬI duyệt (`:204-207`) nằm THẲNG trong `Approvals`** ⇒ "Lịch sử duyệt (N)" của KHKK sẽ có **nhiều dòng hơn** số lần thật sự bấm Duyệt/Trả/Từ-chối. PE chống bằng merge + dedupe 5s-bucket + synthetic-reject (`PeDetailTabs.tsx:3152-3192`) | Không xử ⇒ số `(N)` của KHKK **không cùng nghĩa** với số `(N)` của PE — trong khi cả đề-bài là *"cho đồng nhất"*. **Phải quyết: lọc `Pending` (giống PE) hay hiện đủ (nhật-ký).** Khuyến-nghị: **lọc**, cho khớp mẫu |
|
||||
| **B3** | Changelog ghi **10 chỗ** (bản invest kể 8). Chỗ bị bỏ đáng kể: **`CreateContractFromSigningPlanFeatures.cs:226`** = bắc-cầu **KHKK→HĐ (K7)** | ⇒ "Lịch sử thay đổi" sẽ hiện **entry tạo Hợp đồng** — loại dòng FE **chưa có nhãn**. Thiếu ⇒ hiện chuỗi thô hoặc rơi im lặng |
|
||||
|
||||
### ②-F. 4 bẫy hồi-quy — làm sai là vỡ cái đang chạy
|
||||
|
||||
| # | Bẫy | Luật |
|
||||
|---|---|---|
|
||||
| **M-4** 🔴 | **A-6 của bản invest kiểm SAI KHOÁ.** Cơ-chế thật: `Layout.tsx:350 TRANSIENT_QUERY_KEYS = {id,q,editHeader,page,phase,awId}`; `:369 ignorable = transient && !pinnedByTarget`. KHKK đọc URL đúng **3 khoá**: `filter`(`:74`) · `view`(`:76`) · `group`(`:79`) — **`phase` KHÔNG hề nằm trên URL** | Ghi `filter`/`view`/`group` vào URL ⇒ **hồi-sinh bug S155** (đứng leaf `?group=1` bấm chip "Đã duyệt" → URL khớp CHÍNH XÁC leaf "Đã duyệt" ⇒ sidebar nhảy). Ghi `phase` ⇒ **vô hại** (transient, 0 leaf ghim). ⇒ Acceptance phải cấm **`filter`/`view`/`group`**, KHÔNG phải `phase` — bản cũ **chặn cái vô hại, cho lọt cái nguy hiểm** |
|
||||
| **M-2** 🔴 | `usePipelineStages.ts:211 onOpen: () => navigate('/khkk/'+k.id)` (×2 app) | Nếu còn dùng hook cho panel 1 ⇒ đổi thành **callback do host truyền** (`onSelect(id)` → `?id=`), nếu không cây tự văng khỏi màn 3-panel |
|
||||
| **M-8** | `budgetFrozen` nằm ở **DetailDto**, **KHÔNG có** ở `KhkkListItemDto` (`types/khkk.ts:296-301` — *"CỜ HIỂN THỊ dẫn xuất từ phase, KHÔNG phải cột DB"*) | Card panel 1 in **một** số Σ tiền ⇒ phải **suy từ `phase === DaDuyet`** (đúng như BE suy), **đừng** thêm field vào ListItemDto. Bảng cũ né được vì in **cả hai cột** (vá-8 K2 `:246-251`); chọn sai = tái-hiện đúng lỗi đã sửa |
|
||||
| **M-7** | Bảng 8 cột (`:241-252`: Mã · Gói thầu · **Dự án** · Trạng thái · **Tổng đề xuất** · **Tổng duyệt** · **Người soạn** · **Ngày tạo**) + 6 `KpiCard` + phân-trang ép vào card 400px | Card 3 hàng chứa **~4-5 mẩu tin** ⇒ **PHẢI có bảng "cột nào chết, chuyển đi đâu"** trong PR. Không có bảng đó = mất-chức-năng-im-lặng |
|
||||
| **M-10** | `KhkkDetailPage.tsx:258` xóa xong `navigate('/khkk/list')` **rụng `?group=`** | Ném người dùng về "tất cả nhóm". Giữ `?group=` khi quay lại |
|
||||
| **R-5** | `:97-101` là **set-state-trong-render CÓ CHỦ ĐÍCH** (khuôn React "reset state khi input đổi"), **không phải** code lỗi | Ai refactor cho "sạch" thành `useEffect` sẽ **tái-hiện vá-5** (phải F5 mới đúng). ĐỪNG ĐỘNG |
|
||||
| **R-8** | KHKK **không có phiếu V1-legacy** (module sinh 2026-07-29) | **KHÔNG** port nhánh fallback V1 của PE (`PeDetailTabs.tsx:434-436`, `PeWorkflowPanel.tsx:481-485`) — code chết ngay ngày đầu |
|
||||
|
||||
---
|
||||
|
||||
## ③ CHECKLIST (vai · deliverable · acceptance ĐO ĐƯỢC)
|
||||
|
||||
**Vai thực-thi:** `implementer-backend` (nhóm 1) → `implementer-frontend` (nhóm 2-3) → `reviewer` (gate) → `cicd-monitor` (deploy).
|
||||
`APP` ∈ {`fe-admin`, `fe-user`} — **mọi mục FE phải PASS CẢ HAI**.
|
||||
|
||||
> 🔴 **4 acceptance của bản invest đã bị bẻ gãy khi CHẠY THẬT — dùng bản sửa dưới đây, KHÔNG dùng bản gốc.** Bài học: *chưa ai chạy thử chính các lệnh acceptance đó*; hai cái được viết riêng để canh hai rủi-ro nguy nhất thì **một cái kiểm nhầm khoá, một cái báo đỏ trên file sạch**.
|
||||
|
||||
### Nhóm 0 — trước khi viết dòng code đầu tiên
|
||||
|
||||
| # | Việc | Acceptance |
|
||||
|---|---|---|
|
||||
| ~~A-0~~ | 🟢 **GỠ CHẶN — feasibility đã trả lời dứt.** `usePipelineLinkage()` **đã có sẵn** query `khkkIndex` + `khkkByPeId: Map<peId, KhkkListItemDto[]>` (`usePipelineStages.ts:123-134,147`); `buildStages` gom bằng `:204`; `toKhkkLeaf` `:205-212`; `stage2` `:265-273` | **Không cần query mới, không cần endpoint mới cho panel 1.** 🔸 Nếu ai vẫn đọc theo A-0 gốc: khoảng đúng là **`:100-320`**, không phải `:150-320` — nguồn thật ở `:123-134` nằm **NGOÀI** khoảng cũ |
|
||||
|
||||
### Nhóm 1 — BE
|
||||
|
||||
| # | Acceptance |
|
||||
|---|---|
|
||||
| **B-1** | `grep -n "record ContractSigningPlanDetailDto" -A 80 src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` → thấy đủ **5** tên mới |
|
||||
| **B-2** | `grep -c "ContractSigningPlanApprovalDto" src/Backend` **≥ 2** (định-nghĩa + call-site) — hôm nay là **0** |
|
||||
| **B-3** | 🔴 **RUNTIME, không phải đọc code:** gọi thật 1 phiếu `DaDuyet` → `approvals[]` có **≥1 phần tử `Decision=Reject`** (chứng chuỗi Trả lại được ghi) **và** `approvedByFullName ≠ null` ∀ phần tử |
|
||||
| **B-4** | `grep -c "DepartmentName" …WorkflowStepDto` ≥ 1 |
|
||||
| **B-5** | `grep -n "changelogs" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` → 1 hit, **và** dòng `[Authorize(Policy = "KeHoachKyKet.Read")]` đứng ngay trên |
|
||||
| **B-6** 🔴 | **A-13 THAY BẢN GỐC** (bản gốc **vỡ cả hai đầu**: `:15` là **comment** chứa "Authorize"+"Policy" nên bơm tử-số; và `:17` `:236` `:241` là **comment nhắc tên** `[Authorize]` trần nên vế "0 dòng trần" **FAIL trên file 100% sạch** — citation-trap). Bản sửa **neo đầu dòng, loại comment**: <br>`grep -cE '^\s*\[Http(Get\|Post\|Put\|Delete\|Patch)' <file>` = **R** <br>`grep -cE '^\s*\[Authorize\(Policy' <file>` phải = **1 + R** (hiện 19 = 1+18) <br>`grep -cE '^\s*\[Authorize\]\s*$' <file>` phải = **0** <br>🧪 **Chứng thước có RĂNG:** cắm tạm 1 action `[Authorize]` trần vào **cây tạm** ⇒ số thứ 3 phải nhảy lên 1. Không nhảy = thước mù |
|
||||
|
||||
### Nhóm 2 — FE cấu-trúc
|
||||
|
||||
| # | Acceptance |
|
||||
|---|---|
|
||||
| **C-1** | `ls $APP/src/components/khkk/` → **≥3** file; `grep -rn "pages/khkk/KhkkWorkflowPanel" $APP/src` → **0 hit** |
|
||||
| **C-2** 🔴 | **A-4 THAY BẢN GỐC** (gốc chấp **mọi con số** kể cả `[200px_1fr_200px]` ⇒ đo "có 3 panel", không đo "giống PE"; và **FAIL nếu ai dùng `rem`** như KHKK đang dùng `19rem/21rem`). Bản sửa **ghim literal**: `grep -c 'lg:grid-cols-\[400px_1fr_360px\]' $APP/src/pages/khkk/KhkkListPage.tsx` = **1**, **và** `grep -c 'h-\[calc(100vh-4rem)\]'` = **1** <br>🔸 *Giải mâu-thuẫn F-7: §5 bản invest ghi `400px` (số PE) nhưng Điều-kiện-1 bảo chép GĐ3 = `340px`. Hiến-pháp owner là **"giống nhất với duyệt NCC"** ⇒ **chốt `400px`** (số PE), khuôn code lấy GĐ3* |
|
||||
| **C-3** | `grep -c "matchMedia('(min-width: 1024px)')" $APP/src/pages/khkk/KhkkListPage.tsx` ≥1 · `grep -c 'path="/khkk/:id"' $APP/src/App.tsx` = **1** (route CÒN SỐNG) |
|
||||
| **C-4** 🔴 | **A-6 THAY BẢN GỐC** (gốc cấm `'phase'`/`'group'` — `phase` **không phải khoá URL của KHKK** và **vô hại**; `filter`/`view` **mới là 2 khoá làm vỡ** thì gốc **không nhắc chữ nào** ⇒ `setSearchParams({group, filter:'DaDuyet'})` = **đúng bug S155** vẫn PASS). Bản sửa: **0 hit** `'filter'` / `'view'` / `'group'` bên trong **mọi** lời gọi `setSearchParams`/`setSp`; chỉ `'id'`/`'q'`/`'page'` được phép (∈ `TRANSIENT_QUERY_KEYS`, `Layout.tsx:350`) <br>🧪 **Negative-control:** cắm tạm `setSearchParams({filter:'DaDuyet'})` ⇒ acceptance PHẢI TRƯỢT |
|
||||
| **C-5** 🔴 | **Mitigation M-1 — acceptance QUAN-TRỌNG NHẤT của cả spec.** Owner chốt panel 1 = cây ⇒ `canPe` **sẽ** khoá fetch cho user KHKK-only ⇒ **BẮT BUỘC có fallback**. <br>🔴 **RUNTIME:** đăng-nhập tài-khoản **chỉ có `KeHoachKyKet.*`** (KHÔNG có `PurchaseEvaluations`) ⇒ panel 1 **vẫn liệt ĐỦ phiếu** (dạng danh-sách phẳng, không cây). <br>🧪 **Negative-control bắt buộc:** gỡ tạm fallback ⇒ acceptance PHẢI TRƯỢT (panel 1 trống). Không trượt = thước mù, vì `canPe` của tài-khoản test có thể đang `true` do thừa quyền |
|
||||
| **C-6** 🔴 | **Mitigation B1** — truyền `stage2Content` thì **BẮT BUỘC** kèm `stage2Count` (`PipelineStageFolders.tsx:95-97` — thiếu ⇒ badge **0** trong khi folder đầy phiếu). Và **khai trade-off trong PR**: folder GĐ2 của cây KHKK hiện **card phiếu** thay **8 ngăn N1-N8** (lối vào nhóm giữ ở `Select` panel 1) |
|
||||
| **C-7** 🔴 | **Mitigation M-2** — `grep -c "navigate(\`/khkk/" $APP/src/hooks/usePipelineStages.ts` phải cho thấy `onOpen` nhận **callback tuỳ chọn từ host**, mặc-định giữ `navigate` cũ. <br>🧪 **Chứng 0 hồi-quy:** mở **trang PE** và **trang GĐ3**, bấm node KHKK trên cây ⇒ **vẫn navigate như cũ** (chỉ host `/khkk/list` mới chặn lại thành `?id=`) |
|
||||
| **C-8** 🔴 | **Mitigation R-11** — ô search + Select trạng-thái panel 1 đi **server-side** thẳng `GET /contract-signing-plans`, **KHÔNG lọc trên cây**: `grep -c "PE_INDEX_PAGE_SIZE\|200" …/KhkkListPanel.tsx` không được là đường duy nhất tới phiếu. **Và** dòng cảnh-báo `PipelineTreePanel.tsx:252-256` *"Cây tải 200 phiếu mới nhất…"* **PHẢI còn hiện** (`grep -c "200 phiếu mới nhất"` ≥1) — nuốt cảnh-báo = giấu giới-hạn |
|
||||
|
||||
### Nhóm 3 — FE nội-dung (từng khối của ảnh)
|
||||
|
||||
| # | Acceptance |
|
||||
|---|---|
|
||||
| **D-1** | `grep -c "PePipelineStrip" …/KhkkDetailContent.tsx` = 1 **kèm** `current={2}` |
|
||||
| **D-2** 🔴 | **A-7b THAY BẢN GỐC** (gốc đòi **5 hit** `title="N. …"` nhưng chạy trên **chính mẫu PE** chỉ ra **3** — vì Section 2 dùng template-literal `` title={`2. …(${n} hạng mục)`} `` và Section 4 là `<section>` trần ⇒ implementer viết ĐÚNG sẽ **FAIL**, và cách duy nhất để PASS là **bỏ số đếm động cho vừa regex** = Goodhart thuần). Bản sửa **chấp cả literal lẫn template**: `grep -c '<Section title=' file` = **5** **và** `grep -oE 'title=[{"]`?[1-5]\.' file \| wc -l` = **5** |
|
||||
| **D-3** 🔴 | **A-7d THAY BẢN GỐC** (gốc `grep -c "Ý kiến cấp duyệt" …WorkflowPanel.tsx` = 0 ⇒ **đổi tiêu-đề thành "Ý kiến duyệt" là PASS** trong khi khối vẫn nằm nguyên panel 3 — đo **chuỗi chữ**, mục-tiêu là **vị-trí khối**). Bản sửa đo **luồng dữ-liệu**: `grep -c "opinions" …/KhkkWorkflowPanel.tsx` = **0** **và** `grep -c "levelOpinions\|opinions" …/KhkkDetailContent.tsx` ≥ **1** |
|
||||
| **D-4** | Section 5 nhóm BƯỚC→PHÒNG, **lưới 2 cột**, meta `x/y cấp · N NV`: `grep -c "md:grid-cols-2"` ≥1 · `grep -c "cấp đã duyệt"` ≥1 · `grep -c "NV tham gia"` ≥1 |
|
||||
| **D-5** 🔴 | **F-1** — `grep -c "Chọn phiếu ở danh sách" $APP/src/pages/khkk/KhkkListPage.tsx` ≥ **1** (+ 2 nhánh `Đang tải…` / `Không tìm thấy phiếu.`) |
|
||||
| **D-6** 🔴 | **F-2** — `grep -c "PeFinalizeChip\|Kết thúc" …/KhkkListPanel.tsx` ≥ **1** |
|
||||
| **D-7** 🔴 | **F-3** — panel 3 liệt-kê NV mỗi Cấp, người duyệt **in đậm** + NV khác `/ Tên` mờ: `grep -c "font-semibold" …/KhkkWorkflowPanel.tsx` ≥1 **kèm** đối-chiếu mắt với Ảnh 2 |
|
||||
| **D-8** | `grep -c "không qua CEO"` ≥1 · `grep -c "departmentName"` ≥1 · `grep -c "Duyệt thay CEO"` ≥1 · `grep -c "opacity-50"` ≥1 |
|
||||
| **D-9** ✅🔴 | **A-8c THAY BẢN GỐC** (gốc grep-trên-source ⇒ khối UI CHẾT vẫn PASS 100%). Owner đã chốt **làm thật** ⇒ bẫy đó tan, nhưng thước vẫn phải **RUNTIME**: duyệt 1 phiếu **có upload kèm 1 file** ⇒ khối `📎 File đính kèm khi duyệt` **HIỆN trên màn** với đúng file đó (tên · size · nút xem · nút tải). <br>🧪 Và kiểm **vế tự-ẩn**: phiếu duyệt **không** kèm file ⇒ khối **KHÔNG hiện** (khuôn PE `:928-967`) |
|
||||
| **D-10** | `grep -c "Lịch sử duyệt"` ≥1 · `grep -c "Lịch sử thay đổi"` ≥1 **và** `grep -c "/changelogs"` ≥1 (khối vô nghĩa nếu không fetch) |
|
||||
| **D-11** 🔴 | **B4** — `Approvals` lọc bỏ `Decision=Pending` (hoặc khai rõ quyết-định ngược lại): số `(N)` của KHKK phải **cùng nghĩa** với số `(N)` của PE |
|
||||
|
||||
### Nhóm 4 — chống hồi-quy
|
||||
|
||||
| # | Acceptance |
|
||||
|---|---|
|
||||
| **E-1** | Mirror 2 app: `sha256sum fe-admin/src/<p>` = `sha256sum fe-user/src/<p>` ∀ file mới/sửa → **PASS 100%**, không trừ file nào |
|
||||
| **E-2** 🔴 | **A-10 THAY BẢN GỐC** (gốc liệt 4 path `components/pe/` `pages/pe/` `components/contracts/` `pages/contracts/` — **thiếu đúng 2 path nguy hiểm**: sửa `hooks/usePipelineStages.ts` hoặc `components/pipeline/PipelineStageFolders.tsx` có thể làm **cây trang PE mất 8 ngăn GĐ2** mà A-10 **vẫn PASS 100%**). Bản sửa: giữ 4 path cũ = **0 hit**; **thêm** `hooks/usePipelineStages.ts` + `components/pipeline/` với nghĩa **"chạm ⇒ BẮT BUỘC mở trang PE và trang HĐ đếm lại folder GĐ2 bằng mắt"** (không cấm chạm, nhưng cấm chạm-mà-không-kiểm) |
|
||||
| **E-3** | `npm --prefix fe-admin run build` **và** `npm --prefix fe-user run build` → exit 0. 🔴 Chỉ tin build chạy **SAU** khi mọi agent nền dừng (gotcha #68) |
|
||||
| **E-4** | `dotnet test SolutionErp.slnx` → 0 failed |
|
||||
| **E-5** 🔴 | **UAT trên `fe-user:8080`** (R-1: `Layout.tsx:227-231` `ADMIN_HIDDEN_KEYS` chứa `KeHoachKyKet` ⇒ **menu KHKK không tồn tại bên admin**). Chụp 3 màn khớp Ảnh 1/2/3, **cửa-sổ ẩn-danh** (bài S96 #77: cache ≠ deploy) |
|
||||
| **E-6** | **fe-admin phải UAT bằng DEEP-LINK** `http://localhost:8082/khkk/list?group=1` — menu ẩn nên không vào được bằng sidebar. Thiếu câu này ⇒ sẽ có báo-cáo **"không thấy menu ⇒ chưa deploy"** giả |
|
||||
| **E-7** | PR có **bảng "cột nào chết, chuyển đi đâu"** (M-7): 8 cột bảng cũ + 6 KpiCard → card 400px |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 3 QUYẾT-ĐỊNH OWNER — chốt 2026-08-04, THI-HÀNH NGUYÊN VẸN
|
||||
|
||||
| # | Câu hỏi | Owner chốt | Hệ-quả lên spec |
|
||||
|---|---|---|---|
|
||||
| **1** | Panel 1 lấy nguồn nào? | 🔴 **`PipelineTreePanel`** (giữ đề-xuất bản invest) — **bác** phương-án đảo của lead | ②-B viết lại: cây + **4 mitigation CƯỠNG-HÀNH** (fallback `!canPe` · search server-side · `onOpen` callback · `stage2Count` cặp) + acceptance **C-5 · C-6 · C-7 · C-8**. 🔴 Lead đã trình đủ 4 rủi-ro **ngay trên thẻ chọn**; owner đọc và vẫn chọn ⇒ **quyết-định có thông-tin đầy-đủ**. Nhưng *chọn HÌNH ≠ chấp-nhận VỠ* — mitigation là **bắt buộc**, không phải tuỳ chọn |
|
||||
| **2** | Panel 3 khi chưa chọn hiện gì? | ✅ **CHỈ hộp dashed y PE** (giống Ảnh 1 nhất) | Đóng **miễn-trừ-thứ-7** mà bản invest không khai: §5 gốc âm-thầm thay bằng `PipelineTreePanel`, nay quay về đúng Ảnh 1. **Không** nhét cây vào panel 3 (cây đã sống ở panel 1 theo QĐ-1 ⇒ nhét lần 2 là trùng). Khác GĐ3 ở điểm này = **CÓ CHỦ ĐÍCH**, ghi rõ trong PR |
|
||||
| **3** | File đính kèm khi duyệt — hiện hay upload? | ✅ **Cho upload THẬT** (thêm enum + endpoint) | **BE-7** mới: `+ApprovalAttachment = 3` (append-only) + endpoint upload trong dialog duyệt, policy **`KeHoachKyKet.Update`**. Bẫy *"UI chết mà acceptance vẫn xanh"* (F-6) **tự tan** vì nay có đường ghi; acceptance **D-9** đổi sang runtime 2 chiều (có file ⇒ hiện · không file ⇒ tự ẩn) |
|
||||
|
||||
### 🔸 Còn treo (KHÔNG chặn thi-hành, hỏi khi tới)
|
||||
|
||||
**P3-12 — 4 return-mode (Cấp / Bước / NV / Drafter).** Bản invest xếp "buộc phải khác" với tiền-đề *"KHKK là 3 trạm ngắn"*; lead đo `DbInitializer.cs:554` ⇒ **3 Bước × ≤2 Cấp = tới 6 trạm**, seed `:620` tự ghi *"sao từ workflow Duyệt NCC prod"* ⇒ **tiền-đề sai, hình dạng y hệt PE**. ⇒ Nó là **hoãn**, không phải buộc-khác. Đợt này **giữ 1 mode** (về Bước 1 · Cấp 1) như hiện tại; nếu anh muốn đủ 4 mode như PE thì đó là **món riêng** (đổi state machine BE), không nhét vào đợt UI này.
|
||||
|
||||
---
|
||||
|
||||
## Nợ kỹ-thuật khai ngay (không giấu)
|
||||
|
||||
- `ContractsListPage.tsx` (khuôn được chép) **CHỈ TỒN-TẠI Ở fe-admin** — port sang fe-user = **viết mới**, không phải copy (M-6).
|
||||
- Trích-dẫn hỏng trong bản invest, đã sửa ở spec này: path `WorkflowService` thiếu lớp `Infrastructure/Services/` (B6) · `:552` `ResolveActorFullNameAsync` **không trỏ vào gì** (call-site thật `:485`/`:554`, định-nghĩa ở file khác) · `:547` changelog lệch 1 dòng (thật `:546`) · A-0 sai khoảng dòng (B5).
|
||||
- Lane `reviewer lens-fidelity` **chết dở** (hết giới-hạn phiên): §6 (phép đo P3-12) và §8 (lỗ-hổng tự khai) còn trống, dòng VERDICT còn `?`. **Lead đã tự chạy phép đo §6** (kết-quả ở ①, lật P3-12) nhưng **§8 thì chưa ai làm** ⇒ **danh-sách "chưa đo được" của lăng-kính fidelity hiện KHÔNG TỒN-TẠI**. Đừng đọc bản đó thành "đã soi hết trục trung-thành".
|
||||
- Chưa đo: `fe-user` twin của `usePipelineStages.ts` / `PipelineStageFolders.tsx` (feasibility C-1) · số phiếu KHKK thật ở prod để biết trần 200 có cắn không (C-4) · `hideEmpty` tương-tác với `content`/`groups` (C-3).
|
||||
@ -0,0 +1,34 @@
|
||||
# sub-implementer-backend-s171 — nhật-ký lane BE (Nhóm 1, BE-1 → BE-7)
|
||||
|
||||
> Run: `2026-08-04-S171-khkk-ui-mirror-pe` · vai `implementer-backend` · spec §②-D + §③ Nhóm 1
|
||||
> Luật ghi: sau MỖI file sửa xong ⇒ append ngay 1 dòng `<path> — <đã làm gì> — <acceptance nào phủ>`.
|
||||
|
||||
## 0. Trạng thái
|
||||
|
||||
- [ ] BE-1 DetailDto +5 field
|
||||
- [ ] BE-1b FinalizeStepName/FinalizeLevelName DẪN XUẤT (3 subquery + 3-ngả)
|
||||
- [ ] BE-2 ContractSigningPlanApprovalDto (đẻ mới) + projection
|
||||
- [ ] BE-3 tên người duyệt cho Approval
|
||||
- [ ] BE-4 WorkflowStepDto +DepartmentName
|
||||
- [ ] BE-5 changelogs endpoint policy KeHoachKyKet.Read
|
||||
- [ ] BE-7 enum +ApprovalAttachment=3 + endpoint upload
|
||||
- [ ] Mirror `types/khkk.ts` ×2 app
|
||||
- [ ] `dotnet build` sạch
|
||||
|
||||
## 1. Đọc / khảo sát
|
||||
|
||||
(chưa ghi)
|
||||
|
||||
## 2. Nhật-ký file sửa
|
||||
|
||||
(chưa ghi)
|
||||
|
||||
## 3. Tự kiểm B-1 · B-2 · B-4 · B-5 · B-6
|
||||
|
||||
(chưa ghi)
|
||||
|
||||
## 4. Chưa làm / lý do
|
||||
|
||||
(chưa ghi)
|
||||
|
||||
END implementer-backend-s171 — 0/7 mục BE xong · build CHƯA CHẠY
|
||||
@ -3,44 +3,239 @@
|
||||
> Lane 2/3 của ENSEMBLE `/fable-clone` S171. Chấm bản `sub-investigator-codebase-invest-s171.md`.
|
||||
> PROPOSE-ONLY / READ-ONLY. Mọi con số dưới đây do lane này TỰ đo, không chép từ bản invest.
|
||||
|
||||
## 0. Tái-dựng số load-bearing (bắt buộc ≥2)
|
||||
## 0. Tái-dựng số load-bearing (bắt buộc ≥2 — tôi làm 4)
|
||||
|
||||
_(chưa đo)_
|
||||
| # | Số bản invest nêu | Lệnh tôi chạy | Đo được | Phán |
|
||||
|---|---|---|---|---|
|
||||
| R1 | `WorkflowService.cs:534` ghi `ContractSigningPlanApprovals` | `grep -n "ContractSigningPlanApprovals\.Add"` | `:534` **đúng y** | **KHỚP** |
|
||||
| R2 | changelog ghi **≥9 chỗ** (liệt kê 8: `:547` + `Features.cs:497,562,639,1078,1116,1224,1304`) | `grep -n "ContractSigningPlanChangelogs\.Add" src/Backend` | **10 chỗ**: `WorkflowService.cs:546` · `Features.cs:497,562,639,1078,1116,1224,1304,1372` · `CreateContractFromSigningPlanFeatures.cs:226` | **KHỚP** về mệnh-đề (10 ≥ 9) — nhưng **liệt-kê thiếu 2** (xem §7-B3) |
|
||||
| R3 | `BuildStagesOptions` chỉ `stage1Content`/`stage1Count` (`:163`) | đọc `usePipelineStages.ts:161-165` | đúng 2 field, đúng dòng | **KHỚP** |
|
||||
| R4 | `DetailDto` ở `Features.cs:145-181` | đọc `:145-181` | record đóng đúng tại `:181` (`bool BudgetFrozen);`), **37 tham-số** | **KHỚP** |
|
||||
|
||||
⇒ 4/4 con-số load-bearing của bản invest **tái-dựng được**. Bản đó **không bịa số**. Chỗ nó vỡ là **suy-luận từ số**, không phải số. Ghi rõ để lead đừng hạ tín-nhiệm nhầm chỗ.
|
||||
|
||||
## 1. D-2 — `ContractSigningPlanWorkflowService.cs:534` ghi Approvals: LUÔN hay 1-NHÁNH?
|
||||
|
||||
_(chưa đo)_
|
||||
### 1.0 Đính-chính ĐƯỜNG-DẪN (bản invest ghi thiếu, người sau sẽ mở trượt)
|
||||
|
||||
File **KHÔNG** nằm ở `Application/ContractSigningPlans/`. Đường-dẫn thật:
|
||||
`src/Backend/SolutionErp.Infrastructure/Services/ContractSigningPlanWorkflowService.cs`
|
||||
(`find src -iname "*ContractSigningPlan*"` → chỉ 1 file mang tên đó). Bản invest trích `ContractSigningPlanWorkflowService.cs:534` trần, không path; ai đoán theo hàng-xóm `ContractSigningPlanFeatures.cs` (Application) sẽ tìm không ra. Lỗi nhẹ, nhưng A-1/A-2 là checklist thi-công nên phải đúng path.
|
||||
|
||||
### 1.1 🟢 Giả-thuyết "chỉ ghi 1 nhánh" của lane tôi: **SAI**. Claim D-2 **HELD**.
|
||||
|
||||
Tôi vào săn kẽ này và **không có kẽ**. Đo bằng `grep -n "LogSigningPlanTransitionAsync"` — **6 call-site**, phủ TRỌN mọi transition:
|
||||
|
||||
| call-site | Ngữ-cảnh | `Decision` truyền vào |
|
||||
|---|---|---|
|
||||
| `:204` | Gửi duyệt / gửi lại sau Trả lại | `ApprovalDecision.Pending` |
|
||||
| `:283` | Duyệt + level-finalize (kết thúc sớm) | `Approve` |
|
||||
| `:296` | Duyệt → chuyển Cấp kế | `Approve` |
|
||||
| `:311` | Duyệt → hoàn tất Bước, sang Bước kế | `Approve` |
|
||||
| `:325` | Duyệt Cấp cuối → `DaDuyet` | `Approve` |
|
||||
| **`:393`** | **Trả lại VÀ Từ chối** — `targetPhase` động, `comment`, `isReturn ? … : …` chọn summary | **`ApprovalDecision.Reject`** |
|
||||
|
||||
`:534` `db.ContractSigningPlanApprovals.Add(...)` nằm **thẳng trong thân** `LogSigningPlanTransitionAsync` (`:521-544`), **không có `if` nào bọc** — mọi tham-số quyết-định (`fromPhase/toPhase/decision/actingLevelId/summary`) đều là **đối-số**, không phải hằng.
|
||||
⇒ **Entry "Trả lại → Lùi về Cấp 2" của Ảnh 2 SẼ CÓ.** Kẽ mà lệnh của lead nghi ngờ **không tồn tại**; ước-lượng chi-phí D-2 **không bị lật**.
|
||||
🔸 Ghi rõ để lead khỏi mất công: đây là chỗ **bản invest đúng mà owner/lead có thể tưởng sai** — nó đúng vì tác-giả `LogSigningPlanTransitionAsync` đã cố-ý gom 2 bảng vào **một unit-of-work chung** (docstring `:515-520`), chứ không phải vì may.
|
||||
|
||||
### 1.2 Nhưng có 1 điều D-2 **không nói** mà ảnh-hưởng đến hình hiển-thị
|
||||
|
||||
`Approvals` cũng chứa dòng `Decision = Pending` của **hành-vi GỬI duyệt** (`:204-207`), tức "Lịch sử duyệt (N)" của KHKK sẽ có **nhiều dòng hơn** số lần ai đó thật sự bấm Duyệt/Trả/Từ-chối. PE xử việc này bằng lớp merge + dedupe 5s-bucket + synthetic-reject (`PeDetailTabs.tsx:3152-3192`, bản invest có ghi ở §1.3 mục 10 nhưng **không kéo hệ-quả** sang §4/§7). ⇒ FE KHKK phải quyết: hiện dòng `Pending` (nhật-ký đầy-đủ) hay lọc (giống PE hơn). Đây là **việc không có trong bảng D**, và nó nằm trên đường tới A-8d.
|
||||
|
||||
### 1.3 Tái-dựng số load-bearing #1: **KHỚP tuyệt-đối**
|
||||
|
||||
`:534` = `db.ContractSigningPlanApprovals.Add(...)` ✅ · `:547` = `db.ContractSigningPlanChangelogs.Add(...)` **lệch 1 dòng** — đo thật là `:546` (`:547` là `{`). Sai-số 1 dòng, vô-hại, ghi ra cho đủ.
|
||||
Còn `ResolveActorFullNameAsync` bản invest ghi `WorkflowService.cs:552`: đo thật trong file KHKK là **call-site `:485` và `:554`**; **định-nghĩa** hàm nằm ở file KHÁC (`PurchaseEvaluationWorkflowService.cs:1301`, `ContractWorkflowService.cs:446`). `:552` **không trỏ vào cái gì cả** → trích-dẫn hỏng (dạng nhẹ). Nội-dung claim ("BE đã có sẵn helper") vẫn **HELD**.
|
||||
|
||||
## 2. D-3 — changelog ≥9 chỗ ghi + `UserName` denorm + 0 endpoint đọc
|
||||
|
||||
_(chưa đo)_
|
||||
**Cả 3 vế: HELD.**
|
||||
|
||||
1. **≥9 chỗ ghi → HELD (thật 10).** Xem R2 §0.
|
||||
2. **`UserName` đã denorm → HELD.** `ContractSigningPlanWorkflowService.cs:554`: `UserName = Truncate(await ResolveActorFullNameAsync(actorId, ct), 200)` — tên **ghi thẳng vào hàng changelog**, không cần join lúc đọc. Đúng như bản invest nói.
|
||||
3. **0 endpoint đọc → HELD.** `grep -in "changelog"` trên `src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` → **0 hit**. Liệt-kê đủ 18 route (`HttpGet/Post/Put/Delete` tại `:36,54,60,73,79,86,95,108,117,129,140,152,162,182,191,205,222,243`), **không route nào** là `changelogs`. Cửa đọc thật sự chưa có.
|
||||
|
||||
**Kèm 1 dữ-kiện làm A-13 vỡ (xem §6):** đếm thật trên controller — **19** dòng `[Authorize(Policy = …)]` (1 class-level `:29` + 18 action) / **18** route ⇒ tỷ-lệ 19 ≥ 18 và **0** `[Authorize]` trần THẬT. Tức hôm nay file này **sạch theo gotcha #82**; nhưng thước A-13 mà bản invest viết lại **không đo được điều đó** (§6-G1).
|
||||
|
||||
## 3. D-9 / D-8 — `usePipelineStages.ts:150-320` (lỗ hổng bản invest TỰ KHAI)
|
||||
|
||||
_(chưa đo)_
|
||||
### 3.1 Trả lời DỨT câu hỏi D-9 (unknown → known)
|
||||
|
||||
**Đã đọc `fe-admin/src/hooks/usePipelineStages.ts:120-350`.** Trả lời:
|
||||
|
||||
**Phiếu KHKK cho folder GĐ2 lấy từ QUERY ĐÃ CÓ SẴN — KHÔNG phải thêm fetch mới.**
|
||||
- Hook `usePipelineLinkage()` đã chạy query `khkkIndex` và trả `khkkByPeId: Map<peId, KhkkListItemDto[]>` (`usePipelineStages.ts:123-134`, `:147`).
|
||||
- `buildStages` gom phiếu bằng `peItems.flatMap(p => linkage.khkkByPeId.get(p.id) ?? [])` (`:204`) — **đúng tập phiếu KHKK của gói thầu**, kiểu `KhkkListItemDto` (đủ `maKeHoach` `peTenGoiThau` `phase` `approvalGroup` `contractIds`).
|
||||
- `toKhkkLeaf` (`:205-212`) đã dựng sẵn leaf. `stage2` (`:265-273`) đã có `leaves` + `groups` + `locked/failed/loading/truncated`.
|
||||
⇒ **Chặn-cửa A-0 GỠ ĐƯỢC NGAY.** Không cần query mới, không cần endpoint mới cho panel 1. Đây là điểm bản invest bỏ ngỏ và nó rơi về phía **RẺ HƠN** ước-lượng.
|
||||
|
||||
### 3.2 D-8 — claim `BuildStagesOptions` chỉ có stage1: **HELD** (tự đo)
|
||||
|
||||
`usePipelineStages.ts:161-165`:
|
||||
```ts
|
||||
export type BuildStagesOptions = {
|
||||
/** GĐ1 tự vẽ (trang Duyệt NCC truyền khối leaf phiếu CŨ vào đây). */
|
||||
stage1Content?: ReactNode
|
||||
stage1Count?: number
|
||||
}
|
||||
```
|
||||
Đúng 2 field, đúng số dòng bản invest nêu (`:163`). **HELD.**
|
||||
|
||||
### 3.3 🔴 D-8 claim *"sửa 1 hook, 0 đụng component trình-bày"*: **BROKE** — bản invest bỏ sót `groups`
|
||||
|
||||
Đây là chỗ vỡ **thật**, không phải bắt bẻ chữ:
|
||||
|
||||
- Stage 1 (PE) là folder **PHẲNG**: `{ n:1, leaves:[], content, count }` (`:183-186`) — không có `groups`. Cho nên `stage1Content` **thay chỗ** `leaves` là an-toàn.
|
||||
- Stage 2 (KHKK) **KHÔNG PHẲNG**: nó có `groups: khkkGroups` = **8 ngăn cố định N1-N8 + ngăn "(chưa phân nhóm)"** (`:220-252`, `:268`), là feature K5 S166 cố ý *"HIỆN ĐỦ 8 NGĂN KỂ CẢ RỖNG"* (comment `:232-234`), mỗi ngăn có `onOpenAll → /khkk/list?group=n` khớp URL leaf sidebar (`:239-241`), và có **bất-biến Σ** cảnh báo DEV khi tổng ngăn ≠ tổng folder (`:254-263`).
|
||||
- ⇒ Nhét `stage2Content` theo khuôn đối-xứng stage1 (`content` thay `leaves`) sẽ đặt **card phiếu KHKK** và **8 ngăn nhóm** vào tranh-chấp cùng một ô. Hoặc mất 8 ngăn, hoặc phải quyết-định thứ-tự content ⟂ groups — **đó là đụng vào `PipelineStageFolders`**, và đụng vào một bất-biến có chuông báo.
|
||||
- Đối-xứng "stage1 ↔ stage2" mà bản invest dựa vào là **đối-xứng giả**: hai stage khác CARDINALITY (phẳng vs 2 tầng). Đây đúng lớp bài học `feedback_cardinality_change_grep_consumers`.
|
||||
|
||||
**Hệ quả chi-phí:** D-8 không phải "sửa 1 hook". Tối thiểu là 1 hook + 1 quyết-định trình-bày (giữ 8 ngăn hay bỏ) + đo lại bất-biến Σ. Vẫn RẺ, nhưng **không phải 0 đụng component**.
|
||||
|
||||
### 3.3-bis 🔴 BẰNG-CHỨNG CỨNG: `content` THẮNG `groups` — 8 ngăn bị nuốt IM LẶNG
|
||||
|
||||
`fe-admin/src/components/pipeline/PipelineStageFolders.tsx:285-295` (tự đo, `grep -n -C4`):
|
||||
```
|
||||
285: ) : stage.content !== undefined ? (
|
||||
286: stage.content
|
||||
287: ) : stage.groups !== undefined ? (
|
||||
...
|
||||
292: {stage.groups.map(g => (
|
||||
```
|
||||
`content` đứng **TRƯỚC** `groups` trong chuỗi tam-nguyên ⇒ hễ truyền `stage2Content` thì nhánh `groups` **không bao giờ chạy**. 8 ngăn N1-N8 + ngăn "(chưa phân nhóm)" **biến mất, không lỗi, không cảnh-báo**.
|
||||
Và `stageCount` (`:95-97`) khi có `content` thì đọc `count ?? 0` ⇒ quên truyền `stage2Count` là badge đếm về **0** trong khi folder đầy phiếu — lại một dạng "vắng-mặt trông giống ổn".
|
||||
⇒ Claim D-8 *"0 đụng component trình-bày"*: **BROKE** (2/2 bằng-chứng tại `:285-287` và `:95-97`).
|
||||
🔸 Đường ra rẻ: **đừng dùng `content` cho stage 2**. Panel 1 KHKK có thể giữ nguyên `leaves`/`groups` sẵn có và chỉ làm giàu `PipelineLeaf` (thêm badge Σ tiền / người soạn), hoặc thêm ô `groupContent` per-ngăn. Cả hai đều KHÔNG giẫm lên nhánh `content` của GĐ1.
|
||||
|
||||
### 3.4 Sai địa-chỉ dòng của A-0 (nhỏ nhưng làm người sau đọc trượt)
|
||||
|
||||
A-0 và D-9 bảo đọc `:150-320` và mô-tả *"stage 2 dựng bằng `leaves` (link) `:265`"*. Đúng dòng `:265` là `const stage2: PipelineStage = {`, nhưng **nguồn dữ-liệu thật** nằm ở `:204` (`khkkByPeId`) và `:123-134` (map dựng trong `usePipelineLinkage`) — tức **NGOÀI** khoảng `:150-320` mà A-0 chỉ định (`:123-134` nằm trước `:150`). Ai đọc đúng khoảng A-0 vẫn có thể không thấy query nguồn. Sửa A-0 thành `:100-320`.
|
||||
|
||||
## 4. Chi-phí BE thật (`ContractSigningPlanFeatures.cs:145-181`) + D-6 join Users
|
||||
|
||||
_(chưa đo)_
|
||||
### 4.1 Chỗ RẺ HƠN ước-lượng (nói ra cho công bằng)
|
||||
|
||||
`grep -n "new ContractSigningPlanDetailDto"` → **đúng 1 call-site**: `ContractSigningPlanFeatures.cs:784`. Record **positional** 37 tham-số ⇒ thêm field bình thường sẽ vỡ mọi call-site, nhưng ở đây chỉ có **một**. ⇒ Chi-phí "append field vào DetailDto" thật sự **thấp**, đúng như bản invest hàm-ý. **HELD.**
|
||||
|
||||
### 4.2 D-6 *"entity chỉ có `ApprovedByUserId`, không có tên ⇒ phải join Users"*: **HELD**
|
||||
|
||||
Đọc thân `ContractSigningPlanApproval` được khởi-tạo tại `WorkflowService.cs:534-544`: 8 field `ContractSigningPlanId · FromPhase · ToPhase · Decision · Comment · ApprovalWorkflowLevelId · ApprovedByUserId · ApprovedAt` — **không có** field tên. Đối-chiếu: hàng changelog ngay dưới (`:546-557`) **CÓ** `UserName`. Hai bảng ghi cùng lúc, **một denorm tên một không** ⇒ D-6 đúng, và đúng vì lý-do cấu-trúc chứ không phải suy đoán.
|
||||
🔸 Gợi-ý rẻ hơn "join Users" mà bản invest không nêu: `Approvals` và `Changelogs` **ghi cặp 1-1 trong cùng unit-of-work** (`:534` + `:546`, cùng `now`) ⇒ có thể lấy tên từ changelog cùng transition, hoặc **denorm thêm cột tên vào Approval** cho khớp anh-em. Cả hai đều tránh join lúc đọc.
|
||||
|
||||
### 4.3 🔴 **BROKE** — D-1 *"+3 field (append cuối)"* ước-lượng THIẾU phần đắt nhất
|
||||
|
||||
`ContractSigningPlan.cs:51` chỉ có **`public bool EndedByLevelFinalize`** — một cờ boolean. **Không có** cột nào lưu *Bước nào / Cấp nào* đã kết-thúc.
|
||||
⇒ `FinalizeStepName` và `FinalizeLevelName` **không phải "field append"**, chúng là **giá-trị phải DẪN XUẤT**. PE làm việc đó bằng **subquery `where lv.AllowApproverFinalize && lv.Step!.ApprovalWorkflowId == …` lặp 3 lần** trong projection (`PurchaseEvaluationFeatures.cs:665-683`), cộng nhánh 3-ngả `phase == DaDuyet ? runtime-flag : suy-config` (`:665-673`).
|
||||
⇒ D-1 thật sự = **1 field append (`EndedByLevelFinalize`) + port ~20 dòng projection có 3 subquery**. Vẫn làm được, nhưng gọi nó là "+3 field" là **ước-lượng sai chiều RẺ** — đúng loại sai mà §8-2 của chính bản invest cảnh-báo ("dễ ước-lượng sai đắt nhất theo **cả hai** chiều"). Nó cảnh-báo đúng rồi tự dính chiều còn lại.
|
||||
|
||||
### 4.4 Chỗ phải sửa mà bản invest **không đếm** (BE)
|
||||
|
||||
| # | Việc bị bỏ khỏi bảng D / A-1 | Bằng-chứng |
|
||||
|---|---|---|
|
||||
| U-1 | **`ContractSigningPlanApprovalDto` CHƯA TỒN TẠI** — phải tạo record mới + projection | `grep -n "ContractSigningPlanApprovalDto" src/Backend` → **0 hit** |
|
||||
| U-2 | Projection `Approvals` phải `.Include`/join `ApprovalWorkflowLevels` nếu muốn hiện "Cấp nào duyệt" (Ảnh 2 có "Lùi về **Cấp 2**") — `ApprovalWorkflowLevelId` là Guid trần | `WorkflowService.cs:541` |
|
||||
| U-3 | `types/khkk.ts` phải mirror **×2 app** cho mọi field mới; A-9 có phủ, nhưng A-1 (BE) không nhắc ⇒ dễ làm 1 app rồi tick A-1 xong | `R-7` của chính bản invest |
|
||||
| U-4 | Hàng `Decision = Pending` (hành-vi **gửi duyệt**, `:204-207`) sẽ nằm trong `Approvals` ⇒ phải quyết lọc hay không trước khi dựng "Lịch sử duyệt (N)" | `:204-207` |
|
||||
|
||||
---
|
||||
|
||||
## 5. §5 Phương-án A vs B — lý-do bác B có đứng vững?
|
||||
|
||||
_(chưa đo)_
|
||||
### 5.1 Nửa DỮ-KIỆN: **HELD** (tôi tự mở 2 file)
|
||||
|
||||
- `fe-admin/src/components/pe/PeWorkflowPanel.tsx:384` — `const stepIcon = step.status === 'Done' ? '✓' : step.status === 'Current' ? '●' : '○'` ⇒ PE **đọc** `step.status` có sẵn trên DTO. ✅
|
||||
- `fe-admin/src/pages/khkk/KhkkWorkflowPanel.tsx:106-120` — `stepStatus(idx)` / `levelStatus(idx, order)` **tự tính** từ `currentWorkflowStepIndex` + `currentApprovalLevelOrder`. ✅
|
||||
⇒ "shape KHÁC NHAU THẬT" là **sự-thật đo được**, không phải cái cớ. **HELD.**
|
||||
|
||||
### 5.2 Nửa LẬP-LUẬN: **BROKE** — nhị-phân giả (false dilemma)
|
||||
|
||||
Bản invest viết: *"Generic hoá = phải **hoặc** (i) ép BE KHKK precompute, **hoặc** (ii) nhồi adapter 2 chiều. Cả hai đều **đụng vào đường duyệt của PE đang chạy prod**."*
|
||||
|
||||
Hai lỗi:
|
||||
1. **Có đường (iii) mà nó không xét:** KHKK **đã có sẵn** hàm suy status — 15 dòng thuần-tuý, không side-effect (`:106-120`). Chỉ cần KHKK **tự chuẩn-hoá cây thô → shape có `status`** ngay trước khi truyền vào component generic. Đó là adapter **MỘT chiều, nằm TRỌN trong nhánh KHKK**, và **0 dòng PE bị đọc lại hay sửa**.
|
||||
2. **Câu "cả hai đều đụng vào đường duyệt PE" SAI với (ii):** adapter ở phía KHKK không chạm PE. Câu này gán rủi-ro của (i) cho cả (ii) rồi dùng nó để đóng cửa B.
|
||||
|
||||
### 5.3 Nhưng **KẾT-LUẬN chọn A vẫn ĐỨNG** — vì một lý-do khác, mạnh hơn
|
||||
|
||||
Lý-do thật sự để bác B **không phải** shape, mà là: trích `StepOpinionsBox` ra khỏi `PeDetailTabs.tsx:705-774` **buộc phải sửa file trong `components/pe/`**, mà chính bản invest đặt A-10 = *"`git diff --name-only` → **0 đường-dẫn** khớp `components/pe/`"*. ⇒ **B mâu-thuẫn trực-tiếp với acceptance của chính nó.** Cộng thêm ≥6 nhánh riêng của PE (đúng, và đây là lý-do có sức nặng).
|
||||
🔸 Khuyến-nghị lead: **giữ quyết-định A, thay lý-do.** Lý-do hiện tại sai kỹ-thuật; nếu owner (hoặc reviewer sau) bẻ được lý-do, quyết-định đúng sẽ bị lật oan. Đây là dạng "kết-luận đúng, chứng-minh hỏng".
|
||||
|
||||
---
|
||||
|
||||
## 6. §7 Checklist — mục nào GOODHART-able?
|
||||
|
||||
_(chưa đo)_
|
||||
### G1 🔴 **A-13 VỠ CẢ HAI ĐẦU** (nghiêm nhất — nó là mục canh **quyền**)
|
||||
|
||||
Thước viết: `grep -rn "Authorize" …Controller.cs | grep -c "Policy"` phải **≥ số route**, và **0 dòng `[Authorize]` trần**.
|
||||
|
||||
- **Đầu 1 — tử-số bơm bằng COMMENT.** Dòng `:15` là **chú-thích**: ``// tầng 1 = class `[Authorize(Policy = "KeHoachKyKet.Read")]` (mọi endpoint tối thiểu Read)`` — chứa cả "Authorize" lẫn "Policy" ⇒ **được đếm**. Viết thêm vài dòng chú-thích là con-số tự tăng. Ai xoá 1 `[Authorize(Policy=…)]` thật rồi thêm 1 dòng comment ⇒ **A-13 vẫn PASS, API mở toang**.
|
||||
- **Đầu 2 — vế "0 dòng `[Authorize]` trần" FAIL NGAY HÔM NAY.** `:17` và `:241` chứa **literal** ``[Authorize]` trần`` trong chú-thích cảnh-báo. Đo thật: file **KHÔNG có** `[Authorize]` trần nào (19 dòng Policy / 18 route, §2). ⇒ thước báo đỏ trên một file **đang sạch** — đây đúng lớp bẫy `feedback_citation_trap_selfreference` (doc/nguồn **nói VỀ** anti-pattern bị matcher bắt như **là** anti-pattern).
|
||||
- **Thước thay thế (đề-xuất):** bỏ grep chuỗi, đếm trên **AST-lite** bằng cách chỉ soi dòng **bắt đầu bằng `[`** sau khi cắt comment:
|
||||
```
|
||||
grep -nE '^\s*\[(Http(Get|Post|Put|Delete))' <file> | wc -l # R = số route
|
||||
grep -nE '^\s*\[Authorize\(Policy' <file> | wc -l # P
|
||||
grep -nE '^\s*\[Authorize\]\s*$' <file> | wc -l # T phải = 0
|
||||
```
|
||||
Neo `^\s*\[` loại sạch comment `//`. **Chứng thước có RĂNG:** thêm tạm 1 action `[Authorize]` trần vào cây tạm ⇒ `T` phải nhảy lên 1 (fault-injection, `feedback_faultinjection_proves_teeth`).
|
||||
|
||||
### G2 🔴 **A-10 đo SAI TRỤC** — đo *đường-dẫn chạm*, không đo *hành-vi đổi*
|
||||
|
||||
A-10: *"`git diff --name-only` → 0 path khớp `components/pe/` · `pages/pe/` · `components/contracts/` · `pages/contracts/`"*.
|
||||
Đo thật, `buildStages` có **3 consumer** trong fe-admin:
|
||||
```
|
||||
components/pipeline/PipelineTreePanel.tsx:309 stages={buildStages(wg.items)}
|
||||
pages/pe/PurchaseEvaluationsListPage.tsx:524 stages={buildStages(wg.items, { stage1Content: … })}
|
||||
```
|
||||
(và `PipelineTreePanel` lại là thứ KHKK dùng ở panel-1 lẫn GĐ3 dùng ở panel-3).
|
||||
⇒ Việc D-8 **sửa `hooks/usePipelineStages.ts`** hoặc `components/pipeline/PipelineStageFolders.tsx` — **hai path đều KHÔNG nằm trong danh-sách A-10** — có thể làm **cây của trang PE mất 8 ngăn GĐ2** (§3.3-bis: `content` thắng `groups`) mà **A-10 vẫn PASS 100%**. Hồi-quy đi vòng qua cửa mà thước không canh.
|
||||
**Thước thay thế:** thêm 2 path vào A-10 (`hooks/usePipelineStages.ts`, `components/pipeline/`) với ý nghĩa **"chạm ⇒ phải mở trang PE và trang HĐ đếm lại folder GĐ2 bằng mắt"**, chứ không cấm chạm.
|
||||
|
||||
### G3 **A-7d Goodhart-able trực-diện**
|
||||
`grep -c "Ý kiến cấp duyệt" …KhkkWorkflowPanel.tsx` = **0**. Đổi tiêu-đề thành "Ý kiến duyệt" / "Ý kiến các cấp" ⇒ **PASS** trong khi khối vẫn nằm nguyên panel 3. Thước đo **chuỗi chữ**, mục-tiêu là **vị-trí khối**.
|
||||
**Thay bằng:** `grep -c "opinions" …KhkkWorkflowPanel.tsx` = 0 **và** `grep -c "levelOpinions" …KhkkDetailContent.tsx` ≥ 1 — đo **luồng dữ-liệu**, thứ không đổi được bằng cách sửa nhãn.
|
||||
|
||||
### G4 A-7c / A-8a / A-8b / A-8c / A-8d — cùng một lớp, nhẹ hơn
|
||||
Đều là `grep -c "<cụm tiếng Việt>" ≥1`. Gõ đúng cụm chữ vào JSX **chết** (khối render sau `return null`, hoặc điều-kiện luôn false) vẫn PASS. Không cần thay hết — nhưng **A-8d** ("Lịch sử duyệt" + "Lịch sử thay đổi") nên buộc thêm **1 dòng gọi API**: `grep -c "/changelogs" …` ≥1, vì khối đó **vô nghĩa nếu không fetch**.
|
||||
|
||||
### G5 A-4 regex sai cú-pháp — sẽ FAIL trên chính bố-cục nó đề-xuất
|
||||
A-4 đòi khớp `^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]`. Nhưng §5 "Bố-cục đích" ghi Panel 1 = **400px** … trong khi khuôn nó bảo chép là `ContractsListPage.tsx:125` dùng `lg:grid-cols-[340px_1fr_360px]` — khớp. **Rủi ro thật:** nếu ai dùng `rem` (như KHKK đang dùng `19rem/21rem`, `KhkkListPage.tsx:197`) thì regex `[0-9]+px` **FAIL** dù bố-cục đúng. Nới thành `\[[^]]*_1fr_[^]]*\]`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Bỏ-sót lane này bắt thêm (thách-CLEAN)
|
||||
|
||||
_(chưa đo)_
|
||||
**B1 🔴 `content` nuốt `groups` — 8 nhóm N1-N8 biến mất im lặng.** §3.3-bis. Đây là món nặng nhất: nó vừa lật claim D-8, vừa là **hồi-quy chéo sang trang PE**, vừa **không có thước nào trong §7 bắt được** (A-10 miss, A-11 build vẫn xanh, A-14 chụp màn KHKK chứ không chụp PE).
|
||||
|
||||
**B2 `ContractSigningPlanApprovalDto` chưa tồn tại** (0 hit) — A-1 viết như thể chỉ cần "thêm field", thực tế phải **đẻ record + projection + mirror types ×2 app**. (U-1 §4.4)
|
||||
|
||||
**B3 D-3 liệt-kê thiếu 2 chỗ ghi changelog, một trong đó có nghĩa nghiệp-vụ.** Đo được 10, bản invest kể 8. Chỗ bị bỏ đáng kể nhất: **`CreateContractFromSigningPlanFeatures.cs:226`** — đó là **bắc-cầu KHKK→HĐ (K7)**. Nghĩa là "Lịch sử thay đổi" của KHKK sẽ hiện **entry tạo Hợp đồng**, một loại dòng mà spec §7 không hề nhắc và FE chưa có nhãn cho nó. Thiếu ⇒ hoặc hiện chuỗi thô, hoặc rơi im lặng.
|
||||
|
||||
**B4 Dòng `Decision = Pending` (gửi duyệt) vào thẳng "Lịch sử duyệt (N)".** PE chống bằng merge + dedupe 5s + synthetic-reject (`PeDetailTabs.tsx:3152-3192`); bản invest **có mô-tả** lớp đó ở §1.3 nhưng **không kéo hệ-quả** vào §4/§7 ⇒ số `(N)` của KHKK sẽ **không cùng nghĩa** với số `(N)` của PE, trong khi cả đề-bài là "cho đồng nhất".
|
||||
|
||||
**B5 A-0 chỉ sai khoảng dòng.** Nguồn dữ-liệu thật ở `:123-134` + `:204`, **nằm ngoài** `:150-320` mà A-0 bắt đọc (§3.4). Người làm đúng theo A-0 vẫn có thể không thấy query nguồn ⇒ chặn-cửa tự vô-hiệu.
|
||||
|
||||
**B6 Path `ContractSigningPlanWorkflowService.cs` ghi thiếu lớp.** Nó ở **`Infrastructure/Services/`**, không phải `Application/ContractSigningPlans/` như ngữ-cảnh xung quanh gợi ý (§1.0). Và `:552` (`ResolveActorFullNameAsync`) **không trỏ vào gì** — call-site thật `:485`/`:554`, định-nghĩa ở file khác.
|
||||
|
||||
---
|
||||
|
||||
## 8. Chỗ bản invest ĐÚNG mà owner có thể tưởng sai
|
||||
|
||||
_(chưa đo)_
|
||||
1. 🟢 **"Lịch sử duyệt của KHKK ĐÃ được ghi đầy đủ, kể cả Trả lại"** — tôi vào để lật và **không lật được**. 6/6 call-site phủ trọn Pending/Approve/Reject-Return (§1.1). Entry `Trả lại → Lùi về Cấp 2` ở Ảnh 2 **sẽ có dữ-liệu**. Đừng cắt ngân-sách mục này vì nghi ngờ.
|
||||
2. 🟢 **"Không ép BE KHKK precompute `status`" (P3-3)** — đúng. Hàm suy FE `:106-120` là 15 dòng thuần-tuý, đọc `currentWorkflowStepIndex`/`currentApprovalLevelOrder` **đã có trên DTO** (`Features.cs:162-163`). Ép BE precompute là đổi hợp-đồng API để lấy về một thứ FE tính được miễn phí.
|
||||
3. 🟢 **Đ-2 (GĐ3 đã 3-panel ⇒ GĐ2 là mắt-xích lệch duy nhất)** — tôi không kiểm trực-tiếp `ContractsListPage.tsx:125` (ngoài lăng-kính, lane 1/3 nên chấm), nhưng **gián-tiếp KHỚP**: `PipelineTreePanel` được cả KHKK và GĐ3 dùng, và `buildStages` chỉ có 3 consumer đúng như bố-cục đó hàm-ý.
|
||||
4. 🟢 **Chi-phí append DetailDto thật sự RẺ** — 1 call-site duy nhất `:784` (§4.1). Owner có thể nghĩ "sửa record 37 tham-số = đụng khắp nơi"; không phải.
|
||||
|
||||
---
|
||||
|
||||
## 9. Chưa đo được / lệnh cần chạy
|
||||
|
||||
_(chưa đo)_
|
||||
| # | Chưa đo | Lệnh cần chạy |
|
||||
|---|---|---|
|
||||
| C-1 | **Toàn bộ đo của tôi chỉ trên `fe-admin/`.** Không xác-nhận `fe-user/` identical | `for f in hooks/usePipelineStages.ts components/pipeline/PipelineStageFolders.tsx pages/khkk/KhkkWorkflowPanel.tsx; do sha256sum fe-admin/src/$f fe-user/src/$f; done` |
|
||||
| C-2 | A-2b (tên người trong `Approvals` ≠ null) — cần gọi API thật | `curl -H "Authorization: Bearer …" https://api…/api/contract-signing-plans/{id}` trên 1 phiếu `DaDuyet` |
|
||||
| C-3 | `hideEmpty` tương-tác thế nào với `content`/`groups` khi lọc bật (PE truyền `hideEmpty={filterActive}`) | đọc `PipelineStageFolders.tsx:230-311` |
|
||||
| C-4 | Số phiếu KHKK thật ở prod (bản invest nói PE 45 phiếu, không nói KHKK) — quyết-định trần 200 (R-11) có cắn không | đếm `SELECT COUNT(*) FROM ContractSigningPlans WHERE IsDeleted=0` |
|
||||
| C-5 | `ContractsListPage.tsx` (khuôn được đề-xuất chép) — tôi **không đọc**, để lane khác chấm | `sed -n '80,260p' fe-admin/src/pages/contracts/ContractsListPage.tsx` |
|
||||
|
||||
END reviewer-lens-feasibility-s171 — VERDICT=? HELD / ? BROKE / ? bỏ-sót-bắt-thêm
|
||||
END reviewer-lens-feasibility-s171 — VERDICT=8 HELD / 6 BROKE / 6 bỏ-sót-bắt-thêm
|
||||
|
||||
@ -2,49 +2,205 @@
|
||||
|
||||
> Vật chấm: `sub-investigator-codebase-invest-s171.md` (44.830 B)
|
||||
> Hiến-pháp owner: *"cách bố trí toàn diện cho tất cả, **giống nhất** với duyệt NCC, **chỉ khác đúng phần khác, tương tự thì giống nhau, đồng nhất**."*
|
||||
> Vai: cố làm bản đó SAI. Sức thuyết phục ≠ bằng chứng.
|
||||
> Vai: cố làm bản đó SAI. Sức thuyết phục ≠ bằng chứng. Mọi `file:line` dưới đây do TÔI tự đo.
|
||||
|
||||
## 0. Tự tái-dựng con số load-bearing (≥2, đo bằng lệnh của TÔI)
|
||||
## 0. Tự tái-dựng con số load-bearing (đo bằng lệnh của TÔI)
|
||||
|
||||
(đang đo)
|
||||
| # | Con số bản đó khẳng-định | Tôi đo lại bằng gì | Kết quả |
|
||||
|---|---|---|---|
|
||||
| N-1 | "**27 GIỐNG-được · 6 KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG**" (`:206`) | `rg -o '^\| \*?\*?(K[0-9]+\|P[123]-[0-9]+)\*?\*? \|.*\| \*\*([^*]+)\*\*[^\|]*\|'` → 35 dòng, tôi tự phân loại từng dòng | **KHỚP.** 35 dòng = K1-4 (4G) + P1-1..8 (7G+1K) + P2-1..7 (5G+2K) + P3-1..16 (11G+3K+2ĐÃ) ⇒ **27 / 6 / 2**. `grep -c` dòng-bảng = 35. ✅ HELD |
|
||||
| N-2 | `ContractsListPage.tsx:125` = `lg:grid-cols-[340px_1fr_360px]`, `:249` PipelineTreePanel ở panel 3 | `grep -nE 'lg:grid-cols-\[\|PipelineTreePanel\|100vh' fe-admin/src/pages/contracts/ContractsListPage.tsx` | **KHỚP CHÍNH XÁC** — `:106` `flex h-[calc(100vh-4rem)] flex-col` · `:125` `…lg:grid-cols-[340px_1fr_360px]` · `:249` `{!selectedId && <PipelineTreePanel currentStage={3} bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}`. ✅ HELD |
|
||||
| N-3 | PE panel 2 có "**5 khối đánh số**", Section 5 = ý-kiến (`PeDetailTabs.tsx:397-437`) | `sed -n '395,440p'` + `grep -nE 'title="[0-9]'` | **HELD về THỨ-TỰ** (1→2→3→4→5, ý-kiến dưới cùng `:426`) nhưng **con số hình-thức lệch**: chỉ **3** hit `title="N.` (`:402 :418 :426`); Section 2 dùng template-literal `title={\`2. Hạng mục + Báo giá NCC (${…})\`}` `:405`, Section 4 là `<section>` trần `:423`. ⇒ xem F-4 |
|
||||
| N-4 | Khối ý-kiến KHKK ở panel 3, `KhkkWorkflowPanel.tsx:376-412` | `sed -n '370,415p' fe-admin/src/pages/khkk/KhkkWorkflowPanel.tsx` | **KHỚP** — `:376` comment, `:377` `{opinions.length > 0 && (`, `<ul>` 1 cột `text-[11px]` `:384-410`, placeholder `(duyệt — không ý kiến)` `:407`. ✅ HELD |
|
||||
|
||||
**Kết luận mục 0:** 4/4 con số load-bearing tôi tự đo đều đứng (N-3 lệch ở dạng-thức, không lệch ở kết-luận). Bản đó **không bịa số**. Vấn-đề của nó nằm chỗ khác — ở **đoạn từ §3 sang §5/§7**, xem dưới.
|
||||
|
||||
---
|
||||
|
||||
## 1. Câu (1) — 6 dòng "KHÁC-buộc-phải-khác" có THẬT SỰ buộc không?
|
||||
|
||||
(đang đo)
|
||||
Luật chấm: mỗi dòng KHÁC = **một lần xin miễn-trừ**; gánh nặng chứng-minh thuộc người xin. Lý-do hợp-lệ = **nghiệp-vụ** (trục dữ-liệu/quy-tắc GĐ2 không tồn-tại ở GĐ1). Lý-do KHÔNG hợp-lệ = "code đang thế" · "làm sẽ tốn" · "ngoài phạm-vi".
|
||||
|
||||
### P1-4 — nhóm N1-N8
|
||||
### P2-3 — nội-dung Section
|
||||
### P2-7 — inline-edit vs readOnly
|
||||
### P3-7 — badge NS PRO/CCM
|
||||
### P3-12 — 4 return-mode
|
||||
### P3-13 — price-picker
|
||||
| Dòng | Lý-do bản đó nêu | Loại lý-do | Verdict |
|
||||
|---|---|---|---|
|
||||
| **P1-4** nhóm N1-N8 | 8 nhóm × 8 workflow SP-002, rào 409 BE | **nghiệp-vụ THẬT** | ⚠️ **HELD nhưng DÁN NHÃN SAI** — xem 1.1 |
|
||||
| **P2-3** nội-dung Section | GĐ2 không có báo-giá NCC, có căn-cứ TVGS | **nghiệp-vụ THẬT** | ✅ HELD |
|
||||
| **P2-7** inline-edit vs readOnly | "KHKK không có workspace ⇒ ép readOnly = cắt đường sửa" | **"code đang thế" ngụy-trang** | 🔴 **BROKE** — xem 1.2 |
|
||||
| **P3-7** badge NS PRO/CCM | quyền nhập ngân-sách per-role là khái-niệm riêng PE | **nghiệp-vụ THẬT** | ✅ HELD |
|
||||
| **P3-12** 4 return-mode | "3 trạm ngắn" + "đổi state machine BE, ngoài phạm-vi UI" | **nửa nghiệp-vụ / nửa phạm-vi** | 🔴 **BROKE tiền-đề** — xem 1.3 |
|
||||
| **P3-13** price-picker | KHKK chốt giá per-Line ở choke-point BE | **nghiệp-vụ THẬT (cardinality)** | ✅ HELD |
|
||||
|
||||
### 1.1 P1-4 — HELD về lý-do, SAI về nhãn (làm phồng số miễn-trừ)
|
||||
|
||||
Lý-do nghiệp-vụ có thật. Nhưng **kết-quả thị-giác thì GIỐNG HỆT**: PE đặt `<Select>` quy-trình duyệt (`awId`) trong khối filter panel 1; KHKK đặt `<Select>` nhóm N1-N8 **đúng ô đó** — chính dòng P1-4 tự viết *"CHỖ ĐẶT phải giống"*. Đây là **khác NGUỒN DỮ-LIỆU, không khác BỐ-TRÍ**, mà cả tài-liệu này là về bố-trí.
|
||||
⇒ Dán nhãn KHÁC-buộc-phải-khác cho nó làm **mẫu-số miễn-trừ phồng lên 6**, trong khi số miễn-trừ THẬT về bố-trí chỉ là 5. Với owner đang đếm "chỉ khác đúng phần khác", con số 6 là **báo cáo bi-quan sai chiều**. Đề-nghị: chuyển P1-4 → **GIỐNG-được (khác nội-dung tùy-chọn)**.
|
||||
|
||||
### 1.2 P2-7 — 🔴 BROKE: nguỵ-biện lưỡng-nan (false dichotomy)
|
||||
|
||||
Bản đó lập-luận: *"KHKK không có trang workspace. Ép `readOnly` mà chưa dựng workspace = cắt mất đường sửa duy nhất ⇒ mất chức-năng."*
|
||||
|
||||
Đo lại **chính component mẫu**, `PeDetailTabs.tsx:395-440`:
|
||||
- `readOnly` là **prop do HOST truyền**, không phải hằng-số trong component (`PurchaseEvaluationsListPage.tsx:657-663` truyền `readOnly={true}`; component còn có nhánh `mode === 'workspace'` `:427-431` in banner khác).
|
||||
⇒ Mệnh-đề "muốn giống thì phải dựng workspace" **sai**: host `/khkk/list` chỉ cần truyền `readOnly={phiếu không phải Nháp || user không phải drafter}`. Đường sửa **không bị cắt** — nó nằm cùng chỗ, chỉ đổi điều-kiện bật.
|
||||
⇒ Lý-do thật đằng sau P2-7 là **chi-phí + "hiện tại nó đang thế"**, đúng thứ mà hiến-pháp run cấm (`:149`). Đây là dòng **duy nhất** trong 6 dòng mà chính tác-giả gắn nhãn *"(có điều-kiện)"* — tự-khai đúng, nhưng kết-luận vẫn để ở cột KHÁC.
|
||||
|
||||
**Đề-nghị lật:** P2-7 → **GIỐNG-được**, với 1 câu spec: *"panel 2 nhận `readOnly` từ host; host list truyền `true` khi phiếu ≠ Nháp hoặc người xem ≠ drafter, `false` ngược lại — KHÔNG dựng workspace riêng."* Nếu owner vẫn muốn workspace, đó là món **P2 riêng**, không phải điều-kiện để được miễn-trừ hôm nay.
|
||||
|
||||
### 1.3 P3-12 — 🔴 BROKE tiền-đề + lý-do "ngoài phạm-vi" không hợp-lệ
|
||||
|
||||
Hai vế:
|
||||
- Vế "ngoài phạm-vi UI / đổi state machine BE" — **không phải lý-do nghiệp-vụ**, là lý-do chi-phí. Theo luật owner, vế này tự nó không mua được miễn-trừ.
|
||||
- Vế "chuỗi KHKK là **3 trạm ngắn** (PRO→CCM→CEO) nên lùi-1-cấp ≈ về-đầu" — đây MỚI là vế nghiệp-vụ, nhưng nó là **khẳng-định chưa đo**. Trong khi đó chính bản đó ghi KHKK có **8 workflow `KHKK-N{n}`** cấu-hình được (P1-4, `types/khkk.ts:163`) và Mig 71 thêm **nhóm duyệt N1-N8**. Số Bước × Cấp là **dữ-liệu cấu-hình**, không phải hằng-số "3". Nếu tồn-tại workflow KHKK ≥3 Bước hoặc ≥2 Cấp/Bước thì "lùi 1 cấp ≈ về đầu" **sai**, và miễn-trừ sụp.
|
||||
|
||||
*(đo cụ-thể xem mục 6 — tôi có chạy, kết quả ghi ở đó)*
|
||||
|
||||
**Đề-nghị:** giữ P3-12 ở cột KHÁC **chỉ khi** đo được số Bước/Cấp tối-đa trong 8 workflow KHKK ≤ 3 tổng-cấp. Nếu không đo được ⇒ hạ xuống **"chờ owner"**, đừng để ở KHÁC-buộc-phải-khác. Lý-do "ngoài phạm-vi" phải viết đúng tên: *hoãn*, không phải *buộc khác*.
|
||||
|
||||
---
|
||||
|
||||
## 2. Câu (2) — 27 dòng "GIỐNG-được" có PHỦ HẾT 3 ảnh không?
|
||||
|
||||
(đang đo)
|
||||
### 2.1 Đối-chiếu từng khối thị-giác owner NHÌN THẤY (nguồn: `run.md:13-29`)
|
||||
|
||||
### 2.1 Liệt-kê khối thị-giác trong ảnh (từ run.md)
|
||||
### 2.2 Khối KHÔNG xuất hiện trong §3/§5 — BỎ SÓT
|
||||
| Ảnh | Khối owner thấy | Có dòng trong §3? | Có trong §5 bố-cục đích? | Có acceptance §7? |
|
||||
|---|---|---|---|---|
|
||||
| 1 | header icon + tên + badge đếm `45` | K2 | ✔ (ngầm) | ❌ **không có** |
|
||||
| 1 | ô search trong panel 1 | P1-2 | ✔ | ❌ không có |
|
||||
| 1 | dropdown `Tất cả trạng thái` | P1-3 | ✔ | ❌ không có |
|
||||
| 1 | cây `📅 Năm 2026` + badge | P1-1 | ✔ | ❌ (chỉ A-0 đọc hook) |
|
||||
| 1 | **panel 2 rỗng: "Chọn phiếu ở danh sách"** | 🔴 **KHÔNG có dòng nào** | 🔴 **không nhắc** | 🔴 **không có** |
|
||||
| 1 | panel 3 rỗng: "Quy trình duyệt sẽ hiện khi chọn phiếu." | P3-16 (GIỐNG-được) | 🔴 **§5 thay bằng PipelineTreePanel** | A-8e **khoá luôn bản thay-thế** |
|
||||
| 2 | tiêu-đề + meta `v04` | P3-1 | ✔ | ❌ |
|
||||
| 2 | banner xanh kết-thúc-sớm | P3-2 | ✔ | A-8a ✔ |
|
||||
| 2 | Bước + chip phòng | P3-4 | ✔ | A-8b ✔ |
|
||||
| 2 | **liệt-kê NV/Cấp, người duyệt in ĐẬM, NV khác `/ Tên` mờ** | 🔴 **không có dòng** | 🔴 không nhắc | 🔴 không có |
|
||||
| 2 | badge vai `NS PRO/CCM` | P3-7 (KHÁC) | — | — |
|
||||
| 2 | mờ Bước chưa tới + chip `Kết thúc tại Cấp 2` | P3-6 | ✔ | A-8b ✔ |
|
||||
| 2 | badge `⚑ Duyệt thay CEO` | P3-5 | ✔ | A-8b ✔ |
|
||||
| 2 | `File đính kèm khi duyệt` + phụ-đề + hàng file | P3-8 | ✔ | A-8c ⚠️ (xem F-6) |
|
||||
| 2 | `Lịch sử duyệt (6)` + hint `→ Lùi về Cấp 2` | P3-9 (hint không nhắc) | ✔ | A-8d ✔ |
|
||||
| 3 | bảng số-liệu dòng 8/9 (tổng nền xanh, âm đỏ ngoặc) | P2-3 (KHÁC nội-dung) | ✔ | ❌ |
|
||||
| 3 | card `THÔNG TIN CHỌN THẦU` | P2-3 | ✔ | ❌ |
|
||||
| 3 | **Section 5 khoanh đỏ: nhóm BƯỚC→PHÒNG, lưới 2 cột, meta `x/y cấp · N NV`** | P2-4/5/6 | ✔ | A-7b/c/d ✔ |
|
||||
| 3 | cây trái 4 folder GĐ + badge `SẮP` | P1-1 / D-8 | ✔ | ❌ (không ai gỡ `SẮP` cho GĐ2) |
|
||||
| 3 | **card phiếu: chip `⚑ Kết thúc · Phòng 2` + `Đã duyệt` + mã + giờ + tên** | P1-5 liệt-kê **THIẾU chip `⚑ Kết thúc`** | 🔴 không nhắc | ❌ không có |
|
||||
|
||||
## 3. Câu (3) — Bố-cục đích §5 có đúng "content trên, duyệt+ghi-chú dưới"?
|
||||
### 2.2 🔴 3 khối BỊ THIẾU (bỏ-sót tôi bắt thêm)
|
||||
|
||||
(đang đo)
|
||||
**F-1 — Empty-state panel 2 "Chọn phiếu ở danh sách" (Ảnh 1) BIẾN MẤT hoàn-toàn.**
|
||||
Đo: `PurchaseEvaluationsListPage.tsx:644-652` — `<EmptyState icon={ClipboardCheck} title="Chọn phiếu ở danh sách" description="Chi tiết NCC + báo giá + duyệt sẽ hiển thị ở đây." />`, cộng 2 nhánh `Đang tải…` `:653` và `Không tìm thấy phiếu.` `:654-656`.
|
||||
Bản đó **có ghi sự-thật này ở §1.2** (`:50`, kèm chữ "khớp Ảnh 1") rồi **đánh rơi**: không có dòng §3.C, không có trong sơ-đồ §5, không có acceptance §7. Đây đúng là loại lỗi "vắng-mặt trông giống ổn" — không ai đọc §3 mà phát-hiện được thứ **không có dòng**.
|
||||
Với KHKK, host 3-panel mà panel 2 trống trơn khi chưa chọn = **màn hình trắng giữa** — sai Ảnh 1 ngay ở trạng-thái ĐẦU TIÊN người dùng nhìn thấy.
|
||||
|
||||
## 4. Câu (4) — Nhóm 5 "chờ owner" (A-15/A-16/A-17): hoãn đúng hay hoãn lẩn?
|
||||
**F-2 — Chip `⚑ Kết thúc · Phòng N` trên card phiếu ở panel 1 (Ảnh 3) không được port.**
|
||||
P1-5 liệt-kê card 3 hàng gồm *"mã · gói thầu · trạng thái · Σ tiền · người soạn · ngày"* — **thiếu** đúng con chip mà owner nhìn thấy trong ảnh (`PeFinalizeChip`, bản đó tự ghi ở §1.1 `:43`). Trớ trêu: đây là khối **0-công-BE** cho KHKK, vì cột `EndedByLevelFinalize` đã có sẵn (chính D-1 của bản đó khẳng-định, Mig 71). Bỏ sót một khối vừa-thấy-trong-ảnh vừa-rẻ-nhất.
|
||||
|
||||
(đang đo)
|
||||
**F-3 — Danh-sách NV trong mỗi Cấp (người duyệt thật in đậm, NV còn lại `/ Tên` mờ) — Ảnh 2 mục 3.**
|
||||
Bản đó ghi ở §1.3 (`:78`, `PeWorkflowPanel.tsx:453-465`) rồi **không đưa thành dòng delta**. §2.3 chỉ có dòng cho *badge vai*, không có dòng cho *bản-thân danh-sách NV*. Nếu KHKK panel 3 chỉ vẽ Bước→Cấp mà không liệt-kê NV, hình sẽ **không giống Ảnh 2** dù A-8a/b/c/d PASS hết.
|
||||
|
||||
## 5. Chỗ bản đó ĐÚNG mà owner có thể tưởng sai (bảo vệ kết-luận đúng)
|
||||
---
|
||||
|
||||
(đang đo)
|
||||
## 3. Câu (3) — Bố-cục đích §5 có đúng "content nhập phía trên, duyệt ghi chú phía dưới"?
|
||||
|
||||
## 6. Bảng tổng-kết verdict
|
||||
### 3.1 Thứ-tự dọc panel 2 — ✅ HELD
|
||||
|
||||
| # | Claim | Verdict | Bằng-chứng |
|
||||
Đo mẫu PE, `PeDetailTabs.tsx:397-437`: `<div className="divide-y divide-slate-200">` → `1. Thông tin gói thầu` `:402` → `2. Hạng mục + Báo giá NCC (…)` `:405` (template-literal) → `3. Đơn vị NCC/TP được chọn` `:418` → `<section>` trần "4. Thông tin chọn thầu" `:423` → **`5. Ý kiến cấp duyệt (sign-off theo workflow)` `:426`** (dưới cùng).
|
||||
§5 của bản đó xếp KHKK: Section 1 Thông tin → 2 Dòng giá → 3 Căn cứ hồ sơ → 4 File đính kèm → **5 Ý KIẾN CẤP DUYỆT** (dưới cùng). **Đúng thứ-tự mẫu, đúng câu owner.** ✅
|
||||
|
||||
### 3.2 Claim P2-4 "dời khối ý-kiến từ panel 3 sang panel 2" — ✅ HELD
|
||||
|
||||
Đo: khối ý-kiến KHKK hiện nằm trong `KhkkWorkflowPanel.tsx:376-412` (panel duyệt), render `<ul>` **1 cột** `text-[11px]`, mỗi `<li>` gộp `signedByFullName · Bước n — tên · Cấp m · giờ`, badge `duyệt thay` `:398-402`, placeholder `(duyệt — không ý kiến)` `:407`. Mẫu PE là `md:grid-cols-2` card emerald (`PeDetailTabs.tsx:738-772`).
|
||||
⇒ Vừa **sai panel** vừa **sai hình**, đúng như bản đó nói. Claim đứng vững, và đây chính là ô owner khoanh đỏ. ✅
|
||||
|
||||
### 3.3 🔴 F-4 — nhưng acceptance A-7b ĐO SAI: chạy trên chính MẪU thì FAIL
|
||||
|
||||
A-7b: *"`grep -oE 'title="[0-9]\. [^"]+"' KhkkDetailContent.tsx` → **5 dòng**"*.
|
||||
Tôi chạy đúng lệnh đó trên **mẫu** `PeDetailTabs.tsx`: **3 hit** (`:402 :418 :426`), không phải 5 — vì Section 2 dùng `title={\`2. …(${n} hạng mục)\`}` và Section 4 là `<section>` trần.
|
||||
Hệ-quả THẬT (không phải bắt bẻ chữ): Section 2 của KHKK gần như chắc-chắn cũng cần đếm động (*"Dòng giá per-NCC (N NCC)"*) ⇒ implementer viết ĐÚNG sẽ **FAIL A-7b**, và cách duy-nhất để PASS là **bỏ số đếm động đi cho vừa regex** — tức Goodhart: sửa hiện-vật cho vừa thước đo.
|
||||
Sửa: `rg -c '<Section title=' file` = 5 **và** `rg -o 'title=[{"]`\?[1-5]\.' | wc -l` = 5, chấp cả literal lẫn template.
|
||||
|
||||
### 3.4 🔴 F-5 — §5 LẶNG LẼ ĐẢO một dòng đã phán GIỐNG-được (panel 3 khi chưa chọn)
|
||||
|
||||
- §3 **P3-16**: *"Empty-state panel 3 khi chưa chọn — PE: hộp dashed + `X` + câu (`:669-676`) — **GIỐNG-được** — Ảnh 1 có."*
|
||||
- §5 bố-cục đích (`:261-262`): *"Panel 3 … **chưa chọn → `PipelineTreePanel currentStage={2}`** (khuôn `ContractsListPage.tsx:249`)"*.
|
||||
- §7 **A-8e** khoá luôn bản thay-thế: *"`grep -n "bodyMaxHClass"` → 1 hit chứa `lg:max-h-none`"*. **Không có acceptance nào** cho hộp dashed của Ảnh 1.
|
||||
|
||||
Tôi xác-nhận cả hai đầu bằng đo: PE `PurchaseEvaluationsListPage.tsx:668-676` = `<aside …>{!selectedId && (<div className="rounded-lg border border-dashed … "><X …/>{'Quy trình duyệt sẽ hiện khi chọn phiếu.'}</div>)}`; GĐ3 `ContractsListPage.tsx:249` = `{!selectedId && <PipelineTreePanel currentStage={3} bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}`.
|
||||
|
||||
⇒ Đây là **miễn-trừ THỨ 7, không được khai**, và nó nằm đúng vào khối owner nhìn thấy ở Ảnh 1. Cả 6 dòng KHÁC kia đều được nêu tên và biện-hộ; dòng này thì **đổi âm-thầm giữa §3 và §5**, rồi acceptance đóng đinh bản đã đổi. Đây là dạng nguy nhất theo luật owner: *không phải khác — mà là khác mà không nói*.
|
||||
|
||||
Cách vá 2 dòng, không tốn gì: panel 3 khi `!selectedId` render **hộp dashed y PE** (đúng Ảnh 1) và đẩy `PipelineTreePanel currentStage={2}` xuống **dưới hộp đó** (hoặc để nguyên ở panel 1 như KHKK đang làm). Nếu owner thích bản GĐ3 hơn thì đó là **quyết-định của owner**, phải hỏi — không phải mặc-định của implementer.
|
||||
|
||||
### 3.5 🔴 F-7 — §5 tự mâu-thuẫn về bề-rộng panel 1, và A-4 không bắt được
|
||||
|
||||
§5 sơ-đồ ghi `Panel 1 **400px**` (`:253`) = số của PE. Nhưng Điều-kiện 1 (`:246`) bảo *"lấy khuôn từ GĐ3, không phải từ PE"*, mà GĐ3 tôi đo là **`340px`** (`ContractsListPage.tsx:125`). Hai chỉ-dẫn cho hai số khác nhau.
|
||||
A-4 acceptance: `grep -nE "lg:grid-cols-\[" … → khớp ^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]` — **chấp mọi con số**, kể cả `[200px_1fr_200px]`. Với hiến-pháp *"giống nhất với duyệt NCC"*, thước đo phải ghim **`[400px_1fr_360px]`** literal. Hiện tại acceptance đo "có 3 panel", không đo "giống PE".
|
||||
|
||||
---
|
||||
|
||||
## 4. Câu (4) — Nhóm 5 "chờ owner": hoãn đúng hay hoãn lẩn?
|
||||
|
||||
| Món | Owner đã ngầm yêu-cầu qua ảnh? | Verdict |
|
||||
|---|---|---|
|
||||
| **A-15** upload file khi duyệt | Ảnh 2 CÓ khối `File đính kèm khi duyệt` — nhưng ảnh chỉ chứng-minh **khối hiển-thị**, không chứng-minh **quyền upload** | ✅ **hoãn ĐÚNG** (tách hiển-thị ⟂ năng-lực là tách đúng) — nhưng có lỗ, xem F-6 |
|
||||
| **A-16** readOnly cứng + workspace KHKK | Đây là P2-7 đóng gói lại | ⚠️ **hoãn HỢP-LỆ, lý-do SAI** — hỏi owner thì đúng, nhưng phải hỏi kèm phương-án "host truyền `readOnly`" (mục 1.2), đừng trình như lưỡng-nan |
|
||||
| **A-17** overlay "Xem mở rộng" | Nút `Maximize2` là `opacity-0 group-hover` (`:607-620`) ⇒ **không hiện trong screenshot**; owner chưa hề thấy | ✅ **hoãn ĐÚNG**, và bản đó còn cross-ref P1-7 → §7. Sạch |
|
||||
|
||||
### 🔴 F-6 — A-15 hoãn đúng, nhưng acceptance A-8c biến khối đó thành **UI CHẾT mà vẫn PASS**
|
||||
|
||||
Đo enum: `src/Backend/SolutionErp.Domain/ContractSigningPlans/ContractSigningPlanAttachment.cs:9-12` — `DossierScan = 1 · PlanDocument = 2 · Other = 99`, comment ghi rõ *"Append-only khi cần thêm mục đích mới"*. Không có giá-trị nào mang nghĩa "file người duyệt tải lên".
|
||||
Bản đó khuyến-nghị (P3-8): *"dựng khối, đọc `purpose=Other`, đợi owner chốt vế upload"* + P3-8 ghi khối PE **tự-ẩn khi rỗng** (`PeWorkflowPanel.tsx:928-967`).
|
||||
Ghép 3 sự-thật: (a) không có đường ghi attachment lúc duyệt ở KHKK (§2.3 tự khai "VẮNG"), (b) khối tự-ẩn khi rỗng, (c) A-8c chỉ đo `grep -c "File đính kèm khi duyệt" ≥1` — **trên mã nguồn**.
|
||||
⇒ Khối sẽ **không bao giờ render** trên UAT, trong khi acceptance **PASS 100%**. Owner mở fe-user, không thấy khối trong Ảnh 2, mà checklist báo xanh. Đây đúng lớp bài học *"claim mạnh hơn việc đã làm"*.
|
||||
Sửa rẻ: A-8c phải là acceptance **RUNTIME** — *"phiếu đã duyệt có ≥1 attachment `purpose=Other` ⇒ khối hiện; nếu prod chưa có bản-ghi nào thì ghi thẳng vào spec: **khối này chưa sống được cho tới khi owner chốt A-15**"*. Đừng để nó nằm im ở nhóm 3 như một món đã-làm-xong.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chỗ bản đó ĐÚNG mà owner có thể tưởng là SAI (bảo vệ kết-luận đúng)
|
||||
|
||||
1. **"Việc này không phải sửa CSS mà là đổi lớp kiến-trúc" (§8.1) — ĐÚNG, và tôi xác-nhận độc-lập.** KHKK list hiện là `space-y-5` cuộn-body 2 panel (`KhkkListPage.tsx:161,197`), trong khi cả GĐ1 (`PurchaseEvaluationsListPage.tsx:381,397`) lẫn GĐ3 (`ContractsListPage.tsx:106,125`) đều là `h-[calc(100vh-4rem)]` + 3 cột. Owner nhìn ảnh sẽ nghĩ "đổi bố-cục" — chi-phí thật lớn hơn cảm-giác, và **lý-do lớn hơn đó là chính-đáng**, không phải agent vẽ việc.
|
||||
2. **P2-3 (nội-dung Section khác) không phải là "làm khác cho tiện".** GĐ2 thật-sự không có báo-giá/so-sánh NCC (đã chốt ở GĐ1) — ép 5 Section trùng tên PE sẽ đẻ ra 2 Section rỗng. "Giống nhau" ở đây là **giống KHUNG** (đánh số, `divide-y`, ý-kiến dưới cùng), không phải giống chữ. Bản đó chọn đúng.
|
||||
3. **P3-3 phán "ĐÃ GIỐNG (hình), không ép BE precompute status" — ĐÚNG.** Hình giống là điều owner yêu-cầu; ép đồng-bộ tầng dữ-liệu sẽ chạm đường duyệt PE đang chạy prod. Đây là chỗ dễ bị chê "làm ẩu" nhưng thực-ra là kiềm-chế đúng chỗ.
|
||||
4. **Phương-án A (chép khuôn) thay vì B (generic hoá) — ĐÚNG với `CLAUDE.md`** (*"duplicate giữa 2 app là CÓ CHỦ ĐÍCH"*), và Điều-kiện 2 (ghi nợ kỹ-thuật vào đầu file kèm điều-kiện thoát) biến trùng-lặp thành quyết-định có kỳ-hạn. Owner có thể tưởng đây là lười; nó không phải.
|
||||
|
||||
---
|
||||
|
||||
## 6. Đo bổ-sung cho P3-12 (tiền-đề "3 trạm ngắn")
|
||||
|
||||
*(kết quả đo ghi ở đây — xem mục 8 nếu chưa đo được)*
|
||||
|
||||
---
|
||||
|
||||
## 7. Bảng tổng-kết verdict
|
||||
|
||||
| # | Claim của bản đó | Verdict | Bằng-chứng tôi tự đo |
|
||||
|---|---|---|---|
|
||||
| N-1 | tally 27/6/2 | **HELD** | 35 dòng bảng §3, tự phân loại lại: 27/6/2 |
|
||||
| N-2 | khuôn GĐ3 `:125` `:249` | **HELD** | `ContractsListPage.tsx:106,125,249` |
|
||||
| N-3 | PE 5 khối, ý-kiến dưới cùng | **HELD** (thứ-tự) | `PeDetailTabs.tsx:402,405,418,423,426` |
|
||||
| N-4 | ý-kiến KHKK ở panel 3, list phẳng | **HELD** | `KhkkWorkflowPanel.tsx:376-412` |
|
||||
| P1-4 | KHÁC-buộc-phải-khác | **HELD lý-do / SAI nhãn** | dòng tự viết "CHỖ ĐẶT phải giống" |
|
||||
| P2-3 | KHÁC-buộc-phải-khác | **HELD** | — |
|
||||
| P2-7 | KHÁC-buộc-phải-khác | 🔴 **BROKE** | `readOnly` là prop của host (`PurchaseEvaluationsListPage.tsx:663`, `PeDetailTabs.tsx:427`) |
|
||||
| P3-7 | KHÁC-buộc-phải-khác | **HELD** | — |
|
||||
| P3-12 | KHÁC-buộc-phải-khác | 🔴 **BROKE tiền-đề** | "ngoài phạm-vi" = lý-do chi-phí; "3 trạm" chưa đo (mục 6) |
|
||||
| P3-13 | KHÁC-buộc-phải-khác | **HELD** | cardinality per-Line ≠ 1-giá |
|
||||
| P2-4 | ý-kiến phải dời panel | **HELD** | đo 2 đầu |
|
||||
| §5 thứ-tự Section | content trên / ý-kiến dưới | **HELD** | — |
|
||||
| §5 panel-3 empty | *(âm-thầm đảo P3-16)* | 🔴 **BROKE** (F-5) | `:204` vs `:261-262` vs A-8e |
|
||||
| A-7b | acceptance 5 dòng | 🔴 **BROKE** (F-4) | mẫu PE chỉ 3 hit |
|
||||
| A-4 | acceptance grid | ⚠️ **YẾU** (F-7) | regex chấp mọi số; §5 400px vs GĐ3 340px |
|
||||
| A-8c | acceptance file-khi-duyệt | 🔴 **BROKE** (F-6) | enum `:9-12` không có purpose; khối tự-ẩn ⇒ PASS mà không hiện |
|
||||
| A-15/A-17 | hoãn | **HELD** | — |
|
||||
| A-16 | hoãn | **HELD (hành-vi) / BROKE (lý-do)** | mục 1.2 |
|
||||
| — | **bỏ-sót F-1** empty-state panel 2 | 🔴 **bắt thêm** | `PurchaseEvaluationsListPage.tsx:644-656` |
|
||||
| — | **bỏ-sót F-2** chip `⚑ Kết thúc` trên card | 🔴 **bắt thêm** | P1-5 vs §1.1 `:43`; dữ-liệu có sẵn (D-1) |
|
||||
| — | **bỏ-sót F-3** liệt-kê NV/Cấp | 🔴 **bắt thêm** | `PeWorkflowPanel.tsx:453-465` có, §3 không có dòng |
|
||||
|
||||
## 7. Lỗ-hổng tôi KHÔNG đo được (khai thật)
|
||||
---
|
||||
|
||||
(đang đo)
|
||||
## 8. Lỗ-hổng tôi KHÔNG đo được (khai thật)
|
||||
|
||||
(đang bổ-sung)
|
||||
|
||||
END reviewer-lens-fidelity-s171 — VERDICT=? HELD / ? BROKE / ? bỏ-sót-bắt-thêm
|
||||
|
||||
@ -1,40 +1,208 @@
|
||||
# reviewer — LĂNG-KÍNH 3/3: RỦI-RO VỠ CÁI ĐANG CHẠY (S171 KHKK UI mirror PE)
|
||||
|
||||
> Lane 3/3 của ENSEMBLE `/fable-clone`. PROPOSE-ONLY, READ-ONLY. Vật chấm: `sub-investigator-codebase-invest-s171.md`.
|
||||
> Quy tắc lane: mỗi claim → `HELD` / `BROKE` + bằng-chứng `file:line` TỰ ĐO.
|
||||
> Lane 3/3 của ENSEMBLE `/fable-clone`. PROPOSE-ONLY, READ-ONLY. Vật chấm: `sub-investigator-codebase-invest-s171.md` (365 dòng, 44.830 B).
|
||||
> Mọi số dưới đây tôi TỰ đo bằng lệnh của mình, không chép từ bản invest. Đường dẫn ghi rõ app (`fe-user/` hay `fe-admin/`) — bản invest mặc-định `fe-admin/src/`, đó là một phần của vấn-đề (xem §4.1).
|
||||
|
||||
## 0. Tái-dựng số load-bearing (bắt buộc ≥2, tự đo)
|
||||
## 0. Tái-dựng số load-bearing (tự đo, ≥2 bắt buộc)
|
||||
|
||||
_(chưa ghi)_
|
||||
| # | Con số bản invest | Lệnh tôi chạy | Kết quả tôi đo | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| T-1 | `PE_INDEX_PAGE_SIZE = 200` tại `PipelineTreePanel.tsx:22` (R-11) | `grep -n "PE_INDEX_PAGE_SIZE" fe-user/src/components/pipeline/PipelineTreePanel.tsx` | `22:const PE_INDEX_PAGE_SIZE = 200` · dùng tại `:112 params:{page:1,pageSize:PE_INDEX_PAGE_SIZE}` · dòng cảnh-báo `:252-256` "Cây tải 200 phiếu mới nhất — gói cũ hơn không hiển thị." | **KHỚP** |
|
||||
| T-2 | `ADMIN_HIDDEN_KEYS` chứa `'KeHoachKyKet'` tại `Layout.tsx:227-231` (R-1) | `grep -n "ADMIN_HIDDEN_KEYS" -A 8 fe-admin/src/components/Layout.tsx` | `227:const ADMIN_HIDDEN_KEYS = new Set<string>([` · `229: 'KeHoachKyKet',` · +`PurchaseEvaluations`(228) `Contracts`(230) `HopDongCung`(231) · `:248 return ADMIN_HIDDEN_KEYS.has(key) \|\| key.startsWith('Ct_')` | **KHỚP** |
|
||||
| T-3 | `navKey`/`lastNavKey` `:95-101` (R-5) | đọc `fe-user/src/pages/khkk/KhkkListPage.tsx:95-102` | `95: const navKey = \`${group ?? ''}|${urlFilter ?? ''}|${deletedView ? 'del' : ''}\`` → `97-101` reset `chipPhase=undefined`, `page=1` | **KHỚP** |
|
||||
| T-4 | Bảng "8 cột" (R-6/§3) | đọc `KhkkListPage.tsx:240-254` + `colSpan={8}` `:258,266` | 8 `<th>`: Mã kế hoạch · Gói thầu · Dự án · Trạng thái · Tổng đề xuất · **Tổng duyệt** · Người soạn · Ngày tạo | **KHỚP** |
|
||||
| T-5 | `PipelineTreePanel` 2 app mirror | `find … -name PipelineTreePanel.tsx -exec wc -l` | fe-user 327 dòng · fe-admin 327 dòng | **KHỚP (dòng)** |
|
||||
|
||||
## 1. R-4 — URL-param vs sidebar-highlight (claim nguy nhất)
|
||||
4/4 số load-bearing tôi kiểm đều khớp. Bản invest **đo đúng**. Chỗ nó hỏng không phải số — là **suy-luận từ số** và **răng của acceptance** (§3, §4).
|
||||
|
||||
_(chưa ghi)_
|
||||
---
|
||||
|
||||
## 2. R-1 — fe-admin ẩn menu KHKK (mirror còn bắt buộc?)
|
||||
## 1. R-4 — URL-param vs sidebar-highlight (claim nguy nhất) → **HELD, nhưng lý-do bản invest đưa ra QUÁ THÔ, và A-6 chặn NHẦM KHOÁ**
|
||||
|
||||
_(chưa ghi)_
|
||||
### 1.1 Cơ-chế thật (tôi đo, bản invest không mở ra)
|
||||
|
||||
## 3. Quyền — gotcha #85 + #82, 2 tầng ĐỘC LẬP (endpoint `changelogs` A-2 / A-13)
|
||||
`fe-user/src/components/Layout.tsx`:
|
||||
```
|
||||
350: const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', 'awId'])
|
||||
368: const pinnedByTarget = new Set(b.keys())
|
||||
369: const ignorable = (k: string) => TRANSIENT_QUERY_KEYS.has(k) && !pinnedByTarget.has(k)
|
||||
373: if (aKeys.length !== bKeys.length) return false
|
||||
390: const isActive = pathnameMatches && qMatches
|
||||
```
|
||||
(fe-admin y hệt, `:308` cùng 6 phần-tử.)
|
||||
|
||||
_(chưa ghi)_
|
||||
⇒ Luật thật **KHÔNG phải** "ghi URL = sáng nhầm". Luật thật là: **ghi một khoá KHÔNG-transient (hoặc transient nhưng ĐANG bị leaf đích ghim) vào URL ⇒ vỡ so-khớp**.
|
||||
|
||||
## 4. Cái gì bị ĐỘNG mà bản invest KHÔNG liệt (blast-radius tự quét)
|
||||
### 1.2 Áp vào KHKK — đo tên khoá thật
|
||||
|
||||
_(chưa ghi)_
|
||||
`fe-user/src/pages/khkk/KhkkListPage.tsx` đọc từ URL đúng **3 khoá**: `:74 get('filter')` · `:76 get('view')` · `:79 get('group')`. **Không hề có `phase` trên URL** — `phase` chỉ là biến nội-bộ `:102`.
|
||||
Leaf KHKK ghim gì (`fe-user/src/components/Layout.tsx:63-87` + regex `:213-216`): `?group=n` · `&filter=ChoDuyet|DaDuyet` · `&view=deleted`.
|
||||
|
||||
## 5. R-10 `budgetFrozen` + R-11 trần 200 phiếu (phán lại mức nghiêm-trọng)
|
||||
| Khoá | Transient? | Leaf ghim? | Ghi vào URL thì sao |
|
||||
|---|---|---|---|
|
||||
| `filter` | **KHÔNG** | CÓ (Pending/Approved) | 🔴 **hồi-sinh S155** — đứng leaf "Danh sách" (`?group=1`) bấm chip "Đã duyệt" → URL `?group=1&filter=DaDuyet` khớp CHÍNH XÁC leaf "Đã duyệt" ⇒ sidebar nhảy sang mục khác |
|
||||
| `view` | KHÔNG | CÓ (Deleted) | 🔴 như trên |
|
||||
| `group` | KHÔNG | CÓ (mọi leaf) | 🔴 đổi nhóm bằng chip = nhảy leaf |
|
||||
| `phase` | **CÓ** | KHÔNG leaf KHKK nào ghim | 🟢 **vô hại** — bị `ignorable()` lọc, highlight giữ nguyên |
|
||||
| `id`, `q`, `page` | CÓ | KHÔNG | 🟢 vô hại |
|
||||
|
||||
_(chưa ghi)_
|
||||
**Verdict R-4 = HELD** (kết-luận "đừng chép `setParam` của PE vào chip lọc" là ĐÚNG và tôi tái-dựng được đường vỡ từng bước).
|
||||
|
||||
## 6. Bảo-vệ chỗ bản invest ĐÚNG mà owner có thể tưởng sai
|
||||
### 1.3 🔴 BROKE — A-6 kiểm SAI KHOÁ (lỗ thủng ngay trong hàng rào bảo-vệ R-4)
|
||||
|
||||
_(chưa ghi)_
|
||||
A-6 (`invest:320`) viết nguyên văn: *"`grep -n "setSearchParams\|setSp(" $APP/src/pages/khkk/KhkkListPage.tsx` → chỉ xuất hiện trong hàm set `id`; **0 hit** cho `'phase'`/`'group'` bên trong lời gọi đó"*.
|
||||
|
||||
## 7. Bỏ-sót bắt thêm (thách-CLEAN)
|
||||
Tôi chạy literal: `grep -n "setSearchParams\|setSp(" fe-user/src/pages/khkk/KhkkListPage.tsx | wc -l` → **0** (file hiện chỉ `const [searchParams] = useSearchParams()` `:68`, destructure 1 phần-tử).
|
||||
|
||||
_(chưa ghi)_
|
||||
Vấn-đề: A-6 cấm `'phase'` và `'group'`.
|
||||
- `'phase'` — **không phải khoá URL của KHKK**, và kể cả có ghi cũng **vô hại** (transient, không leaf nào ghim).
|
||||
- `'filter'` và `'view'` — **đúng hai khoá làm vỡ**, A-6 **không nhắc chữ nào**.
|
||||
|
||||
## 8. Tổng-kết verdict
|
||||
⇒ Một implementer viết `setSearchParams({ group: g, filter: 'DaDuyet' })` — tức **chính xác cái bug S155** — sẽ **PASS A-6** (0 hit `'phase'`; `'group'` thì… có hit, nửa may). Acceptance được viết ra để chặn R-4 lại **chặn cái vô hại và cho lọt cái nguy hiểm**. Đây là inverted check, không phải "thiếu chặt".
|
||||
|
||||
_(chưa ghi)_
|
||||
**Sửa đề-nghị (1 dòng):** đổi A-6 thành — *0 hit cho `'filter'` / `'view'` / `'group'` bên trong mọi lời gọi `setSearchParams`; `'id'`/`'q'`/`'page'` được phép vì nằm trong `TRANSIENT_QUERY_KEYS` (`Layout.tsx` — grep tên hằng)*.
|
||||
|
||||
### 1.4 A-6 "hỗn-hợp `?id=` ghi URL, `phase/group` thì không" — **an-toàn, tôi bảo-vệ nó**
|
||||
|
||||
Owner có thể ngờ đây là bẫy. Tôi đo: `id` ∈ `TRANSIENT_QUERY_KEYS` (`Layout.tsx:350`), và **không leaf KHKK nào ghim `id`** ⇒ `/khkk/list?group=1&id=<guid>` vẫn khớp leaf `?group=1` (`aKeys=[group]` = `bKeys=[group]`, `:371-374`) ⇒ **highlight giữ nguyên**.
|
||||
Thêm nữa: `id` **không nằm trong `navKey`** (`:95`) ⇒ chọn dòng KHÔNG kích reset `chipPhase`/`page` (`:97-101`) ⇒ chọn phiếu không nhả bộ lọc, không nhảy về trang 1. **Hỗn-hợp này ĐÚNG, không phải bẫy.** (Điều kiện: đừng thêm `id` vào `navKey`, và đừng để leaf nào ghim `id`.)
|
||||
|
||||
---
|
||||
|
||||
## 2. R-1 — fe-admin ẩn menu KHKK → **HELD**, và hệ-quả nặng hơn bản invest kết luận
|
||||
|
||||
`fe-admin/src/components/Layout.tsx:227-231` xác nhận: `KeHoachKyKet`(229) bị ẩn, cùng `PurchaseEvaluations`(228), `Contracts`(230), `HopDongCung`(231); `:248` còn cắt thêm mọi key `Ct_*`. ⇒ **cả 4 giai-đoạn nghiệp-vụ đều vô-hình bên fe-admin.**
|
||||
|
||||
Bản invest kết: *"mirror 2 app là BẮT BUỘC, không phải tuỳ chọn"*. Tôi **giữ kết-luận đó** (SHA256-mirror là quy-ước sống, R-7/A-9), nhưng bổ-sung 2 điểm nó bỏ:
|
||||
|
||||
1. **UAT bên fe-admin là BẤT KHẢ qua đường menu.** Nghiệm-thu phải deep-link `http://localhost:8082/khkk/list?group=1`. Nếu checklist §7 không nói câu này, người chạy UAT sẽ báo "không thấy menu ⇒ chưa deploy" (đúng lớp lỗi `feedback_mechanism_right_data_absent`).
|
||||
2. **Rủi-ro thật của mirror-mù:** vì bên admin không ai nhìn thấy màn này, một lỗi render chỉ-có-ở-admin (VD panel 3 vỡ lưới) sẽ **sống câm** cho tới khi có người deep-link. Đề-nghị: A-9 (sha256) giữ nguyên, nhưng thêm 1 dòng acceptance *"chụp màn `:8082/khkk/list?group=1` sau khi sửa"* — chứ không chỉ so hash.
|
||||
|
||||
---
|
||||
|
||||
## 3. Quyền — 2 tầng ĐỘC-LẬP (#85 + #82): R-2/A-2 **HELD**, **A-13 KHÔNG CÓ RĂNG (BROKE)**
|
||||
|
||||
### 3.1 Convention hiện tại của KHKK — sạch, R-2 HELD
|
||||
|
||||
`grep -n "\[Authorize" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs`:
|
||||
class `:29 [Authorize(Policy="KeHoachKyKet.Read")]` + **18 attribute action-level** (`:37,55,61,74,80,87,96,109,118,130,141,153,163,183,192,206,223,244`), khớp **18 route** (`grep -cE "^\s*\[Http(Get|Post|Put|Delete|Patch)"` = **18**). Read/Create/Update/Delete phân đúng; riêng `:244 [Authorize(Policy="Contracts.Create")]` cho endpoint bắc-cầu HĐ. **0 `[Authorize]` trần thật.** ⇒ R-2 HELD, A-2 (endpoint `changelogs` mới phải mang `KeHoachKyKet.Read`) là yêu-cầu đúng.
|
||||
|
||||
### 3.2 🔴 BROKE — A-13 chạy literal thì **BÁO ĐỎ trên file đang SẠCH**
|
||||
|
||||
A-13 (`invest:344`) nguyên văn: *"…`| grep -c "Policy"` — số này phải ≥ số route, và **0 dòng** `[Authorize]` trần"*. Tôi chạy đúng như viết:
|
||||
|
||||
- `grep -rn "Authorize" …ContractSigningPlansController.cs | grep -c "Policy"` → **20**. Số route = 18 ⇒ 18 ≤ 20, "PASS". Nhưng 20 ≠ 19 attribute thật: **dòng 15 là COMMENT** (`// tầng 1 = class [Authorize(Policy = "KeHoachKyKet.Read")]`) cũng bị đếm. Vế này pass **nhờ đếm nhầm bình luận**, không phải nhờ code đúng.
|
||||
- `grep -n "\[Authorize\]"` → **3 hit: `:17`, `:236`, `:241` — TẤT CẢ đều là bình luận** đang *nhắc tên* anti-pattern (`:241` = "`ContractsController` `[Authorize]` trần, #82 treo"). ⇒ vế "0 dòng `[Authorize]` trần" **FAIL ngay trên file 100% policy-gated**.
|
||||
|
||||
Đây đúng lớp **citation-trap self-reference** (`feedback_citation_trap_selfreference.md`): matcher khớp chính tài-liệu mô-tả cái xấu. Hậu-quả không phải "báo thừa cho vui" — checker kêu oan sẽ bị **miễn** ở lượt sau, và lúc đó lỗ thật đi lọt. **A-13 hiện là hàng rào giả.**
|
||||
|
||||
**Sửa (đo được):** neo vào đầu dòng, bỏ bình luận —
|
||||
`grep -nE "^\s*\[Authorize\]\s*$" <file>` → phải **0 hit**; và `grep -cE "^\s*\[Authorize\(Policy" <file>` phải **= 1 + số route** (hiện 19 = 1 + 18).
|
||||
|
||||
### 3.3 🔴 Bỏ-sót của bản invest: mẫu PE mà A-2 bảo "mirror" **CHÍNH LÀ một lỗ #82**
|
||||
|
||||
`src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs`:
|
||||
```
|
||||
15: [Authorize] ← class, TRẦN (any-auth)
|
||||
281: [HttpGet("{id:guid}/changelogs")] ← KHÔNG có [Authorize(Policy=…)]
|
||||
282: public async Task<List<PurchaseEvaluationChangelogDto>> GetChangelogs(…)
|
||||
```
|
||||
⇒ endpoint changelogs của PE hiện chỉ cần **đăng-nhập bất-kỳ** là đọc được lịch-sử phiếu. Đề-bài lượt này là *"mirror PE"*; một implementer trung-thành sẽ **chép nguyên** — và đó là cách #82 lan sang KHKK.
|
||||
Bản invest có nói A-2 phải gắn `KeHoachKyKet.Read` (tốt), nhưng **không nói mẫu nguồn bẩn** ⇒ người đọc §5 mà bỏ qua §7 sẽ copy thẳng. **Cần một câu cảnh-báo tại chỗ mô-tả mirror, không phải chỉ ở checklist.**
|
||||
(Ngoài phạm-vi lượt này nhưng nên vào sổ: PE changelogs là ứng-viên #82 thứ hai bên cạnh `POST /api/contracts` mà `:241` đã treo.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Cái gì bị ĐỘNG mà bản invest KHÔNG liệt (tôi tự quét)
|
||||
|
||||
### 4.1 🔴 BROKE — "khuôn GĐ3" (Đ-2, R-6, R-9) **chỉ tồn tại ở fe-admin; fe-user KHÔNG CÓ file đó**
|
||||
|
||||
`find fe-user/src fe-admin/src -iname "ContractsListPage.tsx"` → **duy nhất `fe-admin/src/pages/contracts/ContractsListPage.tsx`**. fe-user chỉ có `pages/contracts/ContractCreatePage.tsx` + `MyContractsPage.tsx`.
|
||||
|
||||
Bản invest mở đầu: *"Đường-dẫn tương-đối gốc `fe-admin/src/` trừ khi ghi rõ khác. fe-user SHA256-identical (lead đã đo 4/4)"* — câu "4/4" chỉ phủ 4 file KHKK, nhưng đặt ở **header toàn văn** nên đọc thành "mọi path dưới đây đều có twin". Hệ-quả cụ-thể:
|
||||
- R-6 dặn *"chép nguyên chuỗi `bodyMaxHClass` từ `ContractsListPage.tsx:247-249`"* → người làm fe-user grep sẽ **không tìm thấy file**.
|
||||
- R-9 dặn *"khuôn `ContractsListPage.tsx:90-96`"* → như trên.
|
||||
- Đ-2 — **lập-luận mạnh nhất ủng-hộ đề-bài owner** ("GĐ3 đã 3-panel, KHKK là mắt xích lệch") — dựa trên một trang **nằm ở app đang ẩn menu Contracts** (`Layout.tsx:230` + `:248 startsWith('Ct_')`). Tiền-lệ có thật, nhưng nó là tiền-lệ **fe-admin-only, menu-invisible**; dùng nó để suy ra "vậy KHKK fe-user cũng phải thế" là bắc cầu qua một chỗ gãy.
|
||||
⇒ Không làm sập luận-điểm, nhưng **phải khai**: port khuôn GĐ3 sang fe-user = **viết mới**, không phải copy; và sẽ tạo ra pattern 3-panel **chỉ có ở fe-user** trừ khi làm cả 2 app (mà A-9 buộc làm cả 2).
|
||||
|
||||
### 4.2 🔴 BROKE — R-9 liệt THIẾU call-site `/khkk/:id`: **cây toàn-trình tự nó điều-hướng**
|
||||
|
||||
R-9 chỉ kể `KhkkListPage:280` và `KhkkDetailPage:400`. Tôi grep cả 2 app:
|
||||
```
|
||||
fe-user/src/hooks/usePipelineStages.ts:211 onOpen: () => navigate(`/khkk/${k.id}`)
|
||||
fe-user/src/hooks/usePipelineStages.ts:241 onOpenAll: () => navigate(`/khkk/list?group=${n}`)
|
||||
fe-user/src/hooks/usePipelineStages.ts:250 onOpenAll: () => navigate('/khkk/list')
|
||||
fe-admin/src/hooks/usePipelineStages.ts:211/241/250 (y hệt)
|
||||
fe-user|fe-admin/src/pages/khkk/KhkkCreatePage.tsx:126 navigate(`/khkk/${data.id}`)
|
||||
fe-user|fe-admin/src/pages/khkk/KhkkDetailPage.tsx:258 navigate('/khkk/list') (sau khi xóa)
|
||||
+ App.tsx:80 (user) / :107 (admin) <Route path="/khkk" → Navigate /khkk/list>
|
||||
```
|
||||
Đây là bẫy **tự-cắn**: nếu panel 1 dựng bằng `PipelineTreePanel`, thì **chính cây trong panel 1** khi bấm node KHKK sẽ `navigate('/khkk/<id>')` — **văng khỏi màn 3-panel vừa dựng**, thay vì chọn phiếu vào panel 2. Hai đường chọn phiếu (`?id=` mới vs `navigate` cũ) sống song song trong CÙNG một khung nhìn. Phải sửa `usePipelineStages.ts:211` ở **cả 2 app** (hoặc truyền callback `onOpen` từ host) — R-12 chỉ nói tới việc dời `KhkkWorkflowPanel`, không nói tới cái này.
|
||||
Thêm: `KhkkDetailPage.tsx:258` sau khi xóa → `navigate('/khkk/list')` **mất `?group=`** ⇒ người dùng bị ném về "tất cả nhóm" (đã là lỗi UX nhẹ hiện nay, thành lỗi rõ hơn khi detail nằm trong panel).
|
||||
|
||||
### 4.3 R-12 — đo lại: **1 import/app, không phải "≥2 nơi × 2 app"**
|
||||
|
||||
`grep -rn "KhkkWorkflowPanel"` → mỗi app đúng **2 dòng**: `KhkkDetailPage.tsx:41` (import) + `:411` (dùng), cộng `KhkkWorkflowPanel.tsx:73` (export). Tức dời file = sửa **1 dòng import × 2 app**. Bản invest **phóng đại nhẹ** ("≥2 nơi × 2 app"); vô hại, nhưng là dấu hiệu con số ở cột "hệ-quả" không phải lúc nào cũng được đo. **Mức: nhỏ.**
|
||||
|
||||
### 4.4 Bảng 8 cột — cái gì **mất im lặng** khi ép vào card 400px
|
||||
|
||||
8 `<th>` đo tại `KhkkListPage.tsx:241-252`: Mã kế hoạch · Gói thầu · Dự án · Trạng thái · **Tổng đề xuất** · **Tổng duyệt** · Người soạn · Ngày tạo (`colSpan={8}` `:258,266` xác nhận). Ngoài ra badge **nhóm duyệt N1-N8** được nhét vào cột 1 (`:296-300`) kèm ghi chú K2 S164: *đã cố ý KHÔNG đẻ cột thứ 9 vì bảng đã cuộn ngang ở 1366 khi có cây bên trái*.
|
||||
⇒ Panel 1 kiểu PE (card 3 hàng, `400px`) chứa **tối đa ~4-5 mẩu tin**. Nếu bê nguyên khuôn PE, các thứ sau **biến mất mà không ai báo**: cột **Dự án**, **Người soạn**, **Ngày tạo**, và nguy nhất là cặp **Tổng đề xuất / Tổng duyệt** (xem §5.1). Khối **6 `KpiCard`** (`:202-219`, `xl:grid-cols-6`) cũng không nhét vừa 400px — PE thay bằng `Select` trạng-thái (`PurchaseEvaluationsListPage.tsx:437-453` theo bản invest §1.1). **Phân trang** (`PAGE_SIZE=20`, `:45`) trong PE được thay bằng cây-tải-1-phát-200 ⇒ đổi mô-hình tải, không phải đổi CSS. §3/§5 của bản invest có bàn layout nhưng **không có bảng "cột nào chết, chuyển đi đâu"** — đây là chỗ mất-chức-năng-im-lặng dễ xảy ra nhất.
|
||||
|
||||
---
|
||||
|
||||
## 5. R-10 `budgetFrozen` + R-11 trần 200 → HELD, nhưng **xếp hạng sai** ở R-11
|
||||
|
||||
### 5.1 R-10 — HELD, kèm một cái bẫy bản invest chưa nói
|
||||
|
||||
Đo `fe-user/src/types/khkk.ts:296-301`: `budgetFrozen: boolean` nằm **trong DTO CHI-TIẾT** (comment `:296-300` ghi rõ *"`ContractSigningPlanDetailDto:166` — CỜ HIỂN THỊ dẫn xuất từ phase (`phase == DaDuyet`), KHÔNG phải cột DB"*).
|
||||
⇒ **`budgetFrozen` KHÔNG có trong `KhkkListItemDto`.** Panel 1 render **list item**, nên lời khuyên R-10 *"card panel 1 hiện Σ tiền phải theo đúng cờ này"* không thực-thi được như viết: hoặc phải **suy từ `phase === DaDuyet`** (đúng, vì BE cũng suy thế), hoặc ai đó sẽ **thêm field vào ListItemDto** — một thay-đổi BE **không nằm trong A-1**. Bảng hiện tại né được vì nó in **cả hai cột** (vá-8 K2, `:246-251`); card 400px in một số ⇒ phải chọn, và chọn sai = tái-hiện đúng lỗi vá-8 đã sửa. **Verdict: HELD + phải sửa cách phát-biểu.**
|
||||
|
||||
### 5.2 🔴 R-11 — tôi **NÂNG mức nghiêm-trọng**: không phải "cảnh-báo", mà là **mất danh-sách**
|
||||
|
||||
Đo `fe-user/src/components/pipeline/PipelineTreePanel.tsx`: `:22` `const PE_INDEX_PAGE_SIZE = 200` · `:112` `params:{page:1,pageSize:PE_INDEX_PAGE_SIZE}` · `:117` `enabled: canPe` · `:252-256` dòng chữ xám 10px *"Cây tải 200 phiếu mới nhất — gói cũ hơn không hiển thị."*
|
||||
|
||||
Ba tầng hỏng, tăng dần:
|
||||
|
||||
1. **Trần 200 = mất phiếu, không phải mất "gói cũ".** Cây bắt nguồn từ **chỉ-mục PE** (GĐ1). Phiếu KHKK treo dưới gói thầu; gói rơi khỏi 200 ⇒ **mọi phiếu KHKK của gói đó biến mất khỏi panel 1**. Hôm nay vô hại vì panel 1 vẫn là **bảng có phân trang + tìm-kiếm server-side** (`KhkkListPage.tsx:112-129`, `total` thật từ server). Đổi panel 1 thành cây = **thay danh-sách-có-phân-trang bằng cây-cắt-cụt**. Dòng cảnh-báo 10px xám nhạt (`text-[10px] italic text-slate-400`) **không** bù được việc mất đường tới phiếu — không có "trang 2".
|
||||
2. 🔴 **Nặng hơn — quyền: `enabled: canPe`.** `usePipelineStages.ts:83-84` đo được:
|
||||
`const canPe = PE_MENU_KEYS.some(k => can(k))` với `PE_MENU_KEYS = ['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List']` (`:55`), còn `canKhkk = ['KeHoachKyKet'].some(...)` (`:57,84`).
|
||||
⇒ **Người chỉ có quyền KHKK (VD người duyệt nhóm N5) có `canPe = false`** ⇒ `:117 enabled:canPe` không fetch ⇒ `:222-226` in *"Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình chưa hiển thị được."*
|
||||
Hôm nay chấp-nhận được vì đó chỉ là **cột phụ bên trái**, bảng KHKK vẫn đầy đủ bên phải. **Nếu panel 1 = cây, người dùng đó mất TRẮNG danh-sách phiếu KHKK của chính mình** — trong khi API `GET /contract-signing-plans` gate `KeHoachKyKet.Read` (`ContractSigningPlansController.cs:29,37`) **vẫn cho phép**. Đây là **display-layer khoá cái authz-layer đang mở** — mặt còn lại của gotcha #82, và là **mất chức-năng, không phải mất thẩm-mỹ**.
|
||||
**Bản invest không có dòng nào về `canPe` ⇒ đây là bỏ-sót nghiêm-trọng nhất tôi tìm được.**
|
||||
3. Nếu vẫn muốn cây ở panel 1: **bắt buộc** giữ một đường vào không-qua-PE (giữ bảng dưới cây, hoặc cây KHKK dựng từ `GET /contract-signing-plans` chứ không từ chỉ-mục PE, hoặc fallback `!canPe → render bảng cũ`). Cần acceptance riêng: *"đăng-nhập tài-khoản chỉ có `KeHoachKyKet.*`, panel 1 vẫn liệt đủ phiếu"*.
|
||||
|
||||
---
|
||||
|
||||
## 6. Chỗ bản invest ĐÚNG mà owner có thể tưởng sai (bảo-vệ)
|
||||
|
||||
1. **A-6 "`?id=` ghi URL nhưng `phase`/`group` thì không" — hỗn-hợp này AN-TOÀN.** Nhìn thì như tiêu-chuẩn kép, nhưng `TRANSIENT_QUERY_KEYS` (`Layout.tsx:350`) làm nó có cơ-sở cơ-học: `id` transient + không leaf nào ghim ⇒ highlight không đổi; `id` cũng không nằm trong `navKey` (`KhkkListPage.tsx:95`) ⇒ không nhả bộ lọc. (Chỉ cần đổi tên khoá bị cấm — §1.3.)
|
||||
2. **R-5 "đọc URL mỗi render, đừng `useState` khởi-tạo-một-lần"** — đúng nguyên-văn cơ-chế `:70-102`; và `:97-101` là **set-state-trong-render** có chủ-đích (khuôn React "reset state khi input đổi"), **không phải** code lỗi cần "dọn thành `useEffect`". Ai refactor cho "sạch" sẽ tái-hiện vá-5 (phải F5 mới đúng).
|
||||
3. **Đ-1 "PE panel 1 viết INLINE, không có `PeListPanel` dùng-lại được"** — nhất-quán với những gì tôi thấy khi grep; hệ-quả *"muốn giống thì phải tách ra hoặc chép khuôn"* là cảnh-báo chi-phí đúng, đừng bỏ.
|
||||
4. **R-8 "KHKK không có phiếu V1-legacy, đừng port nhánh fallback"** — hợp lý; module sinh 2026-07-29, port nhánh V1 = code chết ngay ngày đầu.
|
||||
|
||||
---
|
||||
|
||||
## 7. Tổng-hợp bỏ-sót tôi bắt thêm (ngoài R-1..R-12)
|
||||
|
||||
| # | Bỏ-sót | Bằng-chứng | Mức |
|
||||
|---|---|---|---|
|
||||
| M-1 | **`canPe` khoá cây** ⇒ user KHKK-only mất trắng panel 1 | `usePipelineStages.ts:55,57,83-84` · `PipelineTreePanel.tsx:117,222-226` | 🔴 **CAO** (mất chức-năng, ngược #82) |
|
||||
| M-2 | **`usePipelineStages.ts:211` `navigate('/khkk/:id')`** — cây trong panel 1 tự văng khỏi màn 3-panel; ×2 app | grep call-site §4.2 | 🔴 CAO |
|
||||
| M-3 | **A-13 báo đỏ trên file sạch** (khớp 3 dòng bình luận) ⇒ hàng rào giả | chạy literal §3.2 | 🔴 CAO |
|
||||
| M-4 | **A-6 cấm nhầm khoá** (`phase` vô hại / `filter`,`view` mới vỡ) | §1.3 | 🔴 CAO |
|
||||
| M-5 | **Mẫu PE `GetChangelogs` là lỗ #82** (class `[Authorize]` trần, action không policy) — "mirror PE" = chép lỗ | `PurchaseEvaluationsController.cs:15,281-283` | TRUNG-CAO |
|
||||
| M-6 | **`ContractsListPage.tsx` không tồn tại ở fe-user** — R-6/R-9/Đ-2 trỏ file người làm fe-user không mở được | `find` §4.1 | TRUNG |
|
||||
| M-7 | **Không có bảng "cột nào chết"** khi 8 cột + 6 KpiCard + phân trang ép vào 400px | §4.4 | TRUNG |
|
||||
| M-8 | **`budgetFrozen` nằm ở DetailDto, không có ở ListItemDto** ⇒ R-10 không thực-thi được như viết | `types/khkk.ts:294-301` | TRUNG |
|
||||
| M-9 | **UAT fe-admin phải deep-link** (menu ẩn) — thiếu câu này sẽ có báo-cáo "chưa deploy" giả | `Layout.tsx:227-231,248` | THẤP-TRUNG |
|
||||
| M-10 | `KhkkDetailPage.tsx:258` xóa xong `navigate('/khkk/list')` **rụng `?group=`** | grep §4.2 | THẤP |
|
||||
|
||||
## 8. Kết-luận lane
|
||||
|
||||
- Bản invest **đo rất chắc** (5/5 số tôi tái-dựng đều khớp) và **kết-luận R-4/R-5/R-1/R-8 đứng vững**.
|
||||
- Chỗ nó vỡ **không nằm ở phân-tích, nằm ở HÀNG RÀO**: hai acceptance được viết riêng để chặn hai rủi-ro nguy nhất (**A-6** chặn R-4, **A-13** chặn #82) thì **một cái kiểm nhầm khoá, một cái báo đỏ trên file sạch**. Sức thuyết-phục của văn-bản che mất việc chưa ai **chạy thử chính các lệnh acceptance đó** — tôi chạy, cả hai đều không đứng.
|
||||
- Rủi-ro **vỡ cái đang chạy** lớn nhất **không phải** URL/sidebar (R-4 đã được canh), mà là **M-1 `canPe`** + **R-11 trần-200**: cả hai cùng biến "panel 1 = cây" thành **mất đường tới phiếu**, im-lặng, chỉ với một nhóm người dùng nhất-định — kiểu lỗi khó thấy trong demo và chỉ nổ ở prod.
|
||||
- Khuyến-nghị tối-thiểu trước khi cho làm: (a) sửa A-6 + A-13 theo §1.3/§3.2; (b) thêm acceptance "tài-khoản KHKK-only vẫn thấy đủ phiếu ở panel 1"; (c) giữ **một đường danh-sách có phân-trang** song song với cây, hoặc dựng cây từ API KHKK thay vì chỉ-mục PE.
|
||||
|
||||
END reviewer-lens-regression-s171 — VERDICT=8 HELD / 4 BROKE / 10 bỏ-sót-bắt-thêm
|
||||
|
||||
Reference in New Issue
Block a user