- [ ]🔴🔴CHỜ-ANHMỚI@S170:XOAYpasswordSQLprod`vrapp`—2secretTHÔtrongagent-memory(cicd-runs:48+reviewer-note:22,2giá-trịKHÁCnhau)đãredact`[đã che 71b7740a/4c3c67d5]`NHƯNGnằmtronggitremotetừ~13-05→rotatelàbắtbuộcthực-chất;+A4low-water0-sàn(refineE1)+đổi-chữ/matcherMFEimpl-fe(auditCHẶN-A1)
🔴 **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
> Đườ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).
## §0. Hai đính-chính vào Ground-truth của lead (đọc TRƯỚC §1)
**Đ-1 — Panel 1 của PE KHÔNG phải `PeListPanel`.** Lead ghi "PE panel 1/2/3 = `PeListPanel` · `PeDetailTabs` · `PeWorkflowPanel`". Đo lại: `PurchaseEvaluationsListPage.tsx`**không import**`PeListPanel`; panel 1 được viết **INLINE** trong chính file đó (`:399-639`). `PeListPanel` chỉ có **một** call-site duy nhất: `pages/pe/PurchaseEvaluationWorkspacePage.tsx:84` (màn workspace 2-panel, `grid-cols-[260px_1fr]``:80`).
⇒ Hệ quả cho §5: **không có component list dùng-lại được** để KHKK nhặt. Muốn "giống" panel 1 thì phải TÁCH nó ra trước, hoặc chép khuôn.
**Đ-2 — Đã tồn tại một bản mirror PE 3-panel KHÁC, mới hơn: GĐ3 (Hợp đồng).**`pages/contracts/ContractsListPage.tsx:125` = `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[340px_1fr_360px]`, header `:106-123` sao y PE, Panel 2 = `ContractDetailContent`, Panel 3 = `WorkflowHistoryPanel` (`components/contracts/`), và Panel 3 khi **chưa chọn** thì render `PipelineTreePanel currentStage={3}` (`:249`).
⇒ Đây là **tiền-lệ trong nhà, cùng chuỗi giai-đoạn, làm ở S168**. KHKK là giai-đoạn NẰM GIỮA GĐ1 (PE, 3-panel) và GĐ3 (HĐ, 3-panel) mà lại là trang-giấy-cuộn ⇒ nó là **mắt xích lệch duy nhất** của cả 4 giai-đoạn. Đây là lập-luận mạnh nhất ủng-hộ đề-bài owner, và nó **không nằm trong ảnh** — owner nhìn thấy triệu-chứng, cái này là nguyên-nhân.
---
## §1. Bản-đồ 3 panel PE (mẫu)
_(chưa ghi)_
### 1.0 Khung trang (container)
| Hạng-mục | Sự-thật | `file:line` |
|---|---|---|
| Root | `<div className="flex h-[calc(100vh-4rem)] flex-col">` — trang CHIẾM TRỌN viewport, KHÔNG scroll body | `pages/pe/PurchaseEvaluationsListPage.tsx:381` |
🔴 PE là **một trang duy nhất**; không có route detail riêng cho desktop. `PurchaseEvaluationDetailPage` (`:782-807`) chỉ là fallback fullpage **mobile**`/purchase-evaluations/:id`, và vẫn dựng đúng 2 component đó xếp dọc.
4.`4. Thông tin chọn thầu` — `<section>` trần, header xanh `:423-425`
5.**`<Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)">`** `:426-437` → `LevelOpinionsSectionV2` khi có `approvalWorkflowId`, ngược lại fallback V1 `DepartmentOpinionsSection``:434-436`
1.`h3 Quy trình duyệt` + meta `{code} v{NN} · {name}``:344-354`
2.**Banner kết-thúc-sớm**`evaluation.endsBeforeCeo``:360-377` — 2 nhánh: `DaDuyet` ⇒ "✅ Phiếu đã kết thúc tại {finalizeStepName} · {finalizeLevelName} — không qua CEO."; đang chạy ⇒ "⚑ Quy trình rút gọn…"
3.**Sơ-đồ Bước → Cấp → NV**`:381-478`: `step.status` do **BE precompute**; `isStepAfterFinalize(step.order)` ⇒ `opacity-50``:393` + chip `Kết thúc tại {…}``:411-418`; chip **tên phòng**`step.departmentName``:406-410`; per-Cấp badge `⚑ Duyệt thay CEO` khi `lv.allowApproverFinalize``:441-448`; NV liệt kê `/`-phân-cách `:453-465` + badge `✎ NS PRO` / `✎ NS CCM``:458-463`
4. Fallback V1 "Phiếu này dùng quy trình cũ" `:481-485`
5. Banner "đến lượt bạn" `:489-508` (emerald/amber theo `actorInV2Level`)
9.**`📎 File đính kèm khi duyệt`** `:928-967` — `h3` + phụ-đề "File do người duyệt tải lên trong quá trình duyệt." + hàng file (`Paperclip` · tên · `fmtFileSize` · `Eye` xem trước · `Download`)
10.**`Lịch sử duyệt (N)`** — `<PeApprovalsSection ev>``:969-971`, định-nghĩa ở `PeDetailTabs.tsx:778-785` (export NGƯỢC về panel 3, import `PeWorkflowPanel.tsx:29`); ruột `ApprovalsTab``:3109-3219`: merge `ev.approvals` + synthetic-reject dựng lại từ changelog + dedupe 5s-bucket `:3152-3192`; mỗi entry = badge quyết-định `decisionBadge` + hint `extractNextTargetHint` + timestamp + `tên · ghi-chú``:3197-3216`
11.**`Lịch sử thay đổi`** — `<PeHistorySection ev>``:973-975` → `HistoryTab``:3222-3300` (GET `/purchase-evaluations/{id}/changelogs`, filter TraLai/gửi-lại/ngân-sách)
### 1.4 Overlay "Xem mở rộng" (`expand=1`) — thành-phần THỨ TƯ của mẫu
### 2.1 Panel 1 (KHKK) — ĐANG SAI CHỖ, không phải "đang xấu"
- Ô panel-1 hiện bị `PipelineTreePanel` chiếm. Đó là **cây điều-hướng 4 giai-đoạn dựng từ chỉ-mục PE** (`components/pipeline/PipelineTreePanel.tsx:106-120`, query `['pipeline-pe-index']`, `pageSize 200`), **không phải danh-sách phiếu KHKK**.
- Danh-sách phiếu KHKK nằm ở **bảng 8 cột bên phải** — tức nội-dung thuộc panel 1 đang bị đặt vào ô panel 2.
- ⇒ KHKK **VẮNG** panel-1-theo-nghĩa-PE, và **THỪA** một widget khác nghiệp-vụ ở đúng chỗ đó.
### 2.2 Panel 2 (KHKK) — VẮNG ở màn list
Không ô nào ở `/khkk/list` hiển thị nội-dung phiếu. Nội-dung chỉ tồn-tại sau `navigate('/khkk/:id')`.
### 2.3 Panel 3 (KHKK) — CÓ, nhưng ĐẶT SAI CHỖ và THIẾU 5 khối
`pages/khkk/KhkkWorkflowPanel.tsx` (491 dòng — 🔴 **nằm trong `pages/`, không phải `components/`**) đã mirror **một phần**`PeWorkflowPanel`:
| Khối (theo Ảnh 2) | KHKK | `file:line` |
|---|---|---|
| Header `Quy trình duyệt` + meta | ✅ (thiếu `v{NN}`) | `:194-209` |
| Banner "đã kết thúc tại … không qua CEO" | ❌ **VẮNG** | — (§4 D-1) |
| Sơ-đồ Bước → Cấp ✓/●/○ | ✅ nhưng **FE tự suy** status | `:106-120` · `:213-266` |
| Bộ chọn giá chốt | ❌ cố-ý bỏ (BE chốt ở finalize choke-point) | `:6-8` · `:449-450` |
🔴 **Delta owner nhìn thấy rõ nhất:** khối "Ý kiến cấp duyệt" (Ảnh 3, khoanh đỏ) bên PE là **Section 5 của panel 2**, nhóm **BƯỚC → PHÒNG**, lưới **2 cột**; bên KHKK nó là **list phẳng nhét trong panel 3**. Đây vừa sai **panel**, vừa sai **hình**.
---
## §3. Bảng DELTA per-panel
_(chưa ghi)_
Luật phán (hiến-pháp run): "hiện tại nó đang thế" **KHÔNG** phải lý-do ⇒ phán lại thành GIỐNG-được.
## §4. Data-gap
### 3.A Khung trang
_(chưa ghi)_
| # | Hạng-mục | PE làm sao | KHKK làm sao | Phán | Lý-do |
|---|---|---|---|---|---|
| K1 | Chiều cao | `flex h-[calc(100vh-4rem)] flex-col`, không cuộn body (`:381`) | `space-y-5` giấy-cuộn (`:161`) | **GIỐNG-được** | 3-panel bắt buộc phải chiếm-trọn-viewport, nếu không mỗi panel không có thanh cuộn riêng. GĐ3 đã làm y hệt (`ContractsListPage.tsx:106`) |
| K2 | Header | `<header>` 1 dòng: icon + h1 + badge đếm (`:382-390`) | `<PageHeader>` eyebrow/subtitle/actions (`:162-192`) | **GIỐNG-được** | Cùng chức-năng; GĐ1 và GĐ3 đều dùng header 1 dòng, chỉ GĐ2 lệch. Actions (nút "Lập kế hoạch") giữ ở góc phải header |
| K4 | Chọn phiếu | URL `?id=` in-page, `<lg` mới đi overlay (`:152-158`) | `navigate('/khkk/:id')` rời trang (`:280`) | **GIỐNG-được** | GĐ3 giữ CẢ HAI: desktop `?id=`, `<lg``navigate` (`ContractsListPage.tsx:90-96`) — khuôn sẵn, chép được |
### 3.B Panel 1
| # | Hạng-mục | PE | KHKK | Phán | Lý-do |
|---|---|---|---|---|---|
| P1-1 | Nguồn cây | cây **phiếu PE** 3 tầng + folder GĐ (`:472-637`) | `PipelineTreePanel` cây **điều-hướng** (`:198`) | **GIỐNG-được** | Cùng bộ dựng cây `Năm>Dự án>Hạng mục>4-folder`. Chỉ cần đổ **card phiếu KHKK** vào `content` của folder GĐ2 y như PE đổ vào GĐ1 (§4 D-8) |
| P1-2 | Ô search | trong panel 1, icon-trong-ô (`:401-409`) | card riêng giữa trang (`:222-233`) | **GIỐNG-được** | Cùng chức-năng, khác chỗ đặt |
| P1-3 | Lọc trạng-thái | `<Select>` 1 dòng (`:437-453`) | 6 `KpiCard` hàng ngang (`:202-220`) | **GIỐNG-được** | KpiCard đẹp nhưng ăn hết bề ngang — không sống nổi trong cột 400px. Owner đã nói "đồng nhất" ⇒ `Select` thắng |
| P1-4 | Lọc **nhóm N1-N8** | không có (PE có `awId` dropdown quy-trình `:413-422`) | đọc từ URL `?group=` (`:79-80`), 8 leaf menu | **KHÁC-buộc-phải-khác** | **Nghiệp-vụ SP-002**: 8 nhóm × 8 workflow `KHKK-N{n}` (`types/khkk.ts:163`), rào 409 ở BE (`ContractSigningPlanFeatures.cs:420`). PE không có trục này. 🔸 Nhưng **CHỖ ĐẶT phải giống**: dropdown trong khối filter panel 1, đúng ô mà PE đặt `awId` |
| P1-5 | Dòng phiếu | card 3 hàng + chip (`:546-601`) | `<tr>` 8 cột (`:277-337`) | **GIỐNG-được** | Bảng 8 cột không vừa cột 400px; card 3 hàng chứa đủ: mã · gói thầu · trạng thái · Σ tiền · người soạn · ngày |
| P1-6 | Phân trang | không (pageSize 50 `:101`) | 20/trang, nút Trước/Sau (`:341-355`) | **GIỐNG-được** | Không phải "bỏ phân trang" — GĐ3 đã có **phân trang compact NGAY TRONG panel 1** (`ContractsListPage.tsx:200-222`). Chép khuôn đó |
| P1-7 | Nút "Xem mở rộng" → overlay | có (`:607-620` + `:694-776`) | không có | **GIỐNG-được** | Ưu-tiên P2 (xem §7) — không chặn P1 |
| P1-8 | Persist mở/đóng cây | `pe_list_expanded_projects_v3` (`:269`) | `pipeline_tree_expanded_v1` (`PipelineTreePanel.tsx:21`) | **GIỐNG-được** | Đã có sẵn, chỉ cần giữ key riêng cho GĐ2 |
### 3.C Panel 2
| # | Hạng-mục | PE | KHKK | Phán | Lý-do |
|---|---|---|---|---|---|
| P2-1 | Dải 4 giai-đoạn đầu phiếu | `<PePipelineStrip current={1} />` (`PeDetailTabs.tsx:277`) | không có | **GIỐNG-được** | Component đã dùng-chung được (`ComingSoonPage.tsx:33` truyền `current` động) ⇒ `current={2}` |
| P2-2 | Khung Section | `divide-y divide-slate-200` + `<Section>` h3 `uppercase tracking-wide`**đánh số** (`:397` · `:514-523`) | 4 `SectionCard` card-accent rời, **không đánh số** (`:414 :505 :626 :721`) | **GIỐNG-được** | Đây đúng là "tương tự ⇒ giống nhau". Đổi `SectionCard` → `Section` đánh số + `divide-y` |
| P2-3 | **Nội-dung** các Section | 1 gói thầu · 2 hạng mục+báo giá NCC · 3 chọn NCC · 4 thông tin chọn thầu | Thông tin · Dòng giá per-NCC · Căn cứ hồ sơ b.8-9 (TVGS) · File đính kèm | **KHÁC-buộc-phải-khác** | Nghiệp-vụ GĐ2 ≠ GĐ1: KHKK **không** có báo-giá/so-sánh NCC (đã chốt ở GĐ1), **có** căn-cứ hồ-sơ TVGS (`ContractSigningPlanDossierItem`, `types/khkk.ts:67-99`) |
| P2-4 | **Section CUỐI = Ý kiến cấp duyệt** | Section 5, dưới cùng panel 2 (`:426-437`) | khối nằm trong **panel 3** (`KhkkWorkflowPanel.tsx:376-412`) | **GIỐNG-được** 🔴 | **Chính là ô owner khoanh đỏ Ảnh 3** — "content nhập phía trên, duyệt ghi chú phía dưới". Phải DỜI PANEL |
| P2-5 | Nhóm ý-kiến theo BƯỚC→PHÒNG + meta `x/y cấp · N NV` | `StepOpinionsBox` (`:705-745`) | list phẳng theo `opinions[]` | **GIỐNG-được** | Chỉ cần `workflowSteps` (ĐÃ CÓ) + `departmentName` (§4 D-4) |
| P3-2 | Banner kết-thúc-sớm | `endsBeforeCeo` 2 nhánh (`:360-377`) | VẮNG | **GIỐNG-được** | Cơ-chế ĐÃ port ở K3 (cột `EndedByLevelFinalize` Mig 71), chỉ chưa expose (§4 D-1). Ảnh 2 có banner này ở vị-trí #2 |
| P3-3 | Sơ-đồ Bước→Cấp ✓/●/○ | BE precompute `status` | FE tự suy (`:106-120`) | **ĐÃ GIỐNG** (hình) | Khác ở tầng dữ-liệu, không khác ở tầng nhìn. Không ép BE precompute — rủi-ro > lợi |
| P3-4 | Chip **tên phòng** của Bước | có (`:406-410`) | VẮNG | **GIỐNG-được** | §4 D-4 |
| P3-5 | Badge `⚑ Duyệt thay CEO` per-Cấp | có (`:441-448`) | VẮNG | **GIỐNG-được** | **0 công BE** — `allowApproverFinalize` đã nằm trên `KhkkWorkflowLevelDto` (`types/khkk.ts:272`) |
| P3-6 | Làm mờ Bước/Cấp sau finalize + chip `Kết thúc tại …` | có (`:393``:411-418``:425`) | VẮNG | **GIỐNG-được** | Suy được từ P3-5 + `workflowSteps` — 0 công BE |
| P3-7 | Badge `✎ NS PRO` / `✎ NS CCM` | có (`:458-463`) | VẮNG | **KHÁC-buộc-phải-khác** | Quyền nhập **ngân sách PRO/CCM** là khái-niệm riêng của PE (`PeWorkItemBudgets`, Mig 50). KHKK không có cột ngân sách per-role ⇒ badge sẽ vô nghĩa |
| P3-8 | `📎 File đính kèm khi duyệt` | có, tự-ẩn khi rỗng (`:928-967`) | VẮNG | **GIỐNG-được** (có ngoặc) | Khối + hành-vi tự-ẩn = giống được ngay. 🔸 Nhưng **CHO PHÉP người duyệt upload** là quyết-định owner, không tự làm: cần thêm `ApprovalAttachment` vào enum purpose (§4 D-7). Khuyến-nghị: dựng khối, đọc `purpose=Other`, đợi owner chốt vế upload |
| P3-9 | `Lịch sử duyệt (N)` | có (`:969-971` → `:3109-3219`) | VẮNG | **GIỐNG-được** | 🔴 **Dữ-liệu ĐÃ ĐƯỢC GHI** — `ContractSigningPlanWorkflowService.cs:534`. Chỉ chưa expose (§4 D-2) |
| P3-10 | `Lịch sử thay đổi` (changelog) | có (`:973-975` → `:3222-3300`) | VẮNG | **GIỐNG-được** | 🔴 Changelog ĐÃ GHI (`ContractSigningPlanWorkflowService.cs:547`, và ≥8 chỗ trong `Features.cs`), chỉ thiếu endpoint đọc (§4 D-3) |
| P3-11 | 3 nút + dialog + ô ý-kiến | có | có (`:299-374``:415-488`) | **ĐÃ GIỐNG** | Không đụng |
| P3-13 | Bộ chọn giá chốt khi duyệt | có (`:792`) | không | **KHÁC-buộc-phải-khác** | KHKK chốt giá **per-Line** tại choke-point BE `ApplyApprovedValuesOnFinalize` (`KhkkWorkflowPanel.tsx:449-450`) — không có "1 giá" để người duyệt chọn |
| P3-14 | Nút XÓA phiếu trong panel duyệt | có, `AllowApproverDelete` Mig 68 (`:564`) | có nhưng ở `PageHeader` (`KhkkDetailPage.tsx:388-399`) | **GIỐNG-được** | Chỉ là dời vị-trí + dialog xác-nhận riêng |
| P3-15 | `readOnly={!pendingMe}` ẩn nút thao-tác ở view Danh sách | có (`:680`) | luôn hiện | **GIỐNG-được** | Cần khi panel 3 sống trong màn list |
| P3-16 | Empty-state panel 3 khi chưa chọn | hộp dashed + `X` + câu (`:669-676`) | n/a | **GIỐNG-được** | Ảnh 1 có. 🔸 GĐ3 chọn cách khác: chưa chọn ⇒ **nhét `PipelineTreePanel` vào panel 3** (`ContractsListPage.tsx:249`) — đây là **lối thoát cho cây toàn-trình** mà KHKK đang giữ ở panel 1 |
## §4. Data-gap — KHKK có đủ trường nuôi panel 3 + Section 5 không?
🔴 **Kết-luận đầu tiên, quan trọng nhất:** gần như **toàn bộ dữ-liệu ĐÃ TỒN TẠI Ở DB VÀ ĐANG ĐƯỢC GHI**. Khoảng trống nằm ở **tầng đọc (DTO/endpoint)**, không phải tầng ghi. Đây là tin tốt: chi-phí BE nhỏ hơn nhiều so với ước-lượng "phải làm lại history".
| # | Thiếu cái gì | Trạng-thái thật ở BE | Ai cần | Việc phải làm |
|---|---|---|---|---|
| **D-1** | Banner kết-thúc-sớm: `endedByLevelFinalize` + `finalizeStepName` + `finalizeLevelName` | **Cột `EndedByLevelFinalize` ĐÃ CÓ** (Mig 71). `ContractSigningPlanDetailDto` (`ContractSigningPlanFeatures.cs:145-181`) **KHÔNG expose** — đã tự khai ở `types/khkk.ts:299-300` ("cột Mig 71 nằm im chờ K3 tiêu thụ") | P3-2 | +3 field vào DetailDto (append cuối, đúng convention `:174-176`) + 3 field vào `KhkkDetailDto` |
| **D-2** | `Lịch sử duyệt` — danh-sách transition | 🔴 **ĐANG GHI THẬT**: `ContractSigningPlanWorkflowService.cs:534``db.ContractSigningPlanApprovals.Add(new ContractSigningPlanApproval{FromPhase,ToPhase,Decision,Comment,ApprovalWorkflowLevelId,ApprovedByUserId,ApprovedAt})`. Entity `ContractSigningPlanApproval.cs:17-31`. Bảng `ContractSigningPlanApprovals` + index `(ContractSigningPlanId, ApprovedAt)` (Mig 69 `:247-248`). **DTO không có `Approvals`** | P3-9 | +`List<ContractSigningPlanApprovalDto> Approvals` vào DetailDto. **Cẩn thận:** entity chỉ có `ApprovedByUserId`, **không có tên** ⇒ projection phải join Users (BE đã có `ResolveActorFullNameAsync`, `WorkflowService.cs:552`) |
| **D-3** | `Lịch sử thay đổi` — changelog | 🔴 **ĐANG GHI THẬT** ≥9 chỗ: `WorkflowService.cs:547` + `Features.cs:497,562,639,1078,1116,1224,1304`. Có `UserName`**đã denorm** ⇒ không cần join. **KHÔNG có endpoint đọc** — `ContractSigningPlansController.cs` không có route `changelogs` nào | P3-10 | +`GET /contract-signing-plans/{id}/changelogs` policy `KeHoachKyKet.Read` (mirror PE `PeDetailTabs.tsx:3225`) |
| **D-4** | `departmentName` của Bước | `ContractSigningPlanWorkflowStepDto` chỉ có `DepartmentId` — tự khai ở `types/khkk.ts:275` ("KHÔNG có tên phòng") | P2-5 **và** P3-4 (2 chỗ) | +`DepartmentName` vào StepDto (join `Departments`) |
| **D-5** | `workflowVersion` | DetailDto có `WorkflowCode` + `WorkflowName`, **không có version** (`Features.cs:160-161`) | P3-1 | +1 field |
| **D-6** | Tên người trong lịch-sử duyệt | (xem D-2) `ApprovedByUserId` không kèm tên | P3-9 | Join hoặc denorm giống `Changelog.UserName` |
| **D-7** | Purpose "file khi duyệt" | Enum `ContractSigningPlanAttachmentPurpose` chỉ có `DossierScan=1 · PlanDocument=2 · Other=99` (`ContractSigningPlanAttachment.cs:7-12`) — **không có** giá-trị cho "file người duyệt tải lên" | P3-8 | 🔸 **Quyết-định owner**: (a) dựng khối chỉ-đọc lọc `purpose=Other` — 0 công BE; (b) +`ApprovalAttachment=3` + cho upload trong dialog duyệt — đổi enum + endpoint. **Đừng tự chọn (b)** |
| **D-9** | *chưa đo được* | Cách `usePipelineStages` nạp phiếu KHKK để dựng leaves stage-2 (`:265-315`) — tôi **chưa đọc** đoạn này. Nó quyết-định card KHKK trong folder GĐ2 lấy dữ-liệu từ đâu (query sẵn hay phải thêm) | P1-1 | **PHẢI đọc `hooks/usePipelineStages.ts:150-320` trước khi code panel 1** |
**Trả lời thẳng câu hỏi §4:** workflow steps/levels ✅ đủ · ý-kiến per-cấp ✅ đủ (kể cả cờ "duyệt thay") · lịch-sử duyệt ⚠️ có ở DB, thiếu ở API · file-khi-duyệt ❌ chưa có khái-niệm.
---
## §5. Kiến-trúc đề-xuất
_(chưa ghi)_
### Phương-án A — tạo `components/khkk/` riêng, chép khuôn PE
- **Làm:** `mkdir components/khkk/` → `KhkkListPanel.tsx` · `KhkkDetailContent.tsx` (+ `KhkkLevelOpinionsSection.tsx`) · dời `pages/khkk/KhkkWorkflowPanel.tsx` → `components/khkk/`. `KhkkListPage` trở thành host 3-panel.
- **Được:** 0 rủi-ro cho PE (đang chạy prod, 45 phiếu) và GĐ3. Mỗi module giữ UX riêng — đúng nguyên-tắc đã ghi trong `CLAUDE.md` ("duplicate giữa 2 app là CÓ CHỦ ĐÍCH"). Diff dễ review. Mirror 2-app đơn-giản (copy nguyên file).
- **Mất:** trùng-lặp thật. Sửa "cách hiển-thị ý-kiến cấp duyệt" sau này phải sửa ≥3 nơi (PE · KHKK · HĐ).
### Phương-án B — tổng-quát-hoá component PE thành `components/approval/` dùng chung
- **Làm:** trích `StepOpinionsBox`/`StepOpinionEntry` (`PeDetailTabs.tsx:705-774`) và bộ sơ-đồ Bước→Cấp (`PeWorkflowPanel.tsx:381-478`) thành component generic nhận `steps[] · opinions[] · finalizeInfo`.
- **Được:** 1 nguồn sự-thật cho 3 module (PE · KHKK · HĐ).
- **Mất:** 🔴 **shape KHÁC NHAU THẬT.** PE nhận `approvalFlow`**có `status` do BE precompute** (`PeWorkflowPanel.tsx:384`); KHKK nhận **cây thô, FE tự suy** (`KhkkWorkflowPanel.tsx:22-29,106-120`). 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** — rủi-ro không cân với lợi ích thẩm-mỹ. Thêm nữa PE có ≥6 nhánh riêng (NS PRO/CCM · price-picker · 4 return-mode · CCM-done · skip-to-final · DeptApprovals legacy) mà KHKK không có: generic sẽ đầy `if (module === …)`.
### 🔴 CHỌN: **Phương-án A**, kèm 2 điều-kiện
1.**Điều-kiện 1 — lấy khuôn từ GĐ3, không phải từ PE.**`ContractsListPage.tsx` (255 dòng) đã là **bản PE-3-panel đã được gọt gọn**, làm ở S168, cùng chuỗi giai-đoạn, đã qua gate. Chép từ đó rẻ hơn và ít bẫy hơn chép từ `PurchaseEvaluationsListPage.tsx` (807 dòng, mang theo `deleted`/`pendingMe`/`awId`/overlay/2 hệ `<details>`).
2.**Điều-kiện 2 — nợ kỹ-thuật khai ngay, không giấu.** Ghi vào đầu file `components/khkk/` một dò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Đ `WorkflowHistoryPanel.tsx`). Gộp được khi và chỉ khi BE 3 module thống-nhất shape `status` precompute."* — biến trùng-lặp từ **tai-nạn** thành **quyết-định có điều-kiện thoát**.
`/khkk/:id`**GIỮ NGUYÊN route** (fallback `<lg` + deep-link + bookmark cũ) nhưng ruột đổi thành `KhkkDetailContent` + `KhkkWorkflowPanel` xếp dọc — đúng khuôn `PurchaseEvaluationDetailPage` (`:798-805`).
---
## §6. Rủi-ro / bẫy
_(chưa ghi)_
| # | Bẫy | Bằng-chứng | Phòng |
|---|---|---|---|
| **R-1** 🔴 | **fe-admin KHÔNG hiển thị menu KHKK.**`ADMIN_HIDDEN_KEYS` chứa `'KeHoachKyKet'` (K6/QĐ7 S164) ⇒ toàn nhánh `Khkk_*` bị cắt khỏi sidebar admin. Cũng cắt `PurchaseEvaluations` và `Contracts` | `components/Layout.tsx:227-231` | **Ảnh owner gửi là fe-user, không phải fe-admin.** ⇒ UAT phải nghiệm-thu trên **fe-user:8080**; mirror 2 app là BẮT BUỘC, không phải tuỳ chọn. Deep-link `/khkk/list` bên admin vẫn vào được (`:212-216` — display-layer ≠ authz-layer, gotcha #82) |
| **R-2** | **gotcha #85** — dùng ĐÚNG key policy của endpoint. Mọi route KHKK gate `KeHoachKyKet.{Read/Create/Update/Delete}`; riêng bắc-cầu HĐ gate `Contracts.Create` | `ContractSigningPlansController.cs:29,37,87,96,118,223,244` · `hooks/usePipelineStages.ts:11-17,57` | Panel mới **không** OR key con (`Khkk_List`…). Nếu thêm endpoint changelogs (D-3) ⇒ `[Authorize(Policy="KeHoachKyKet.Read")]`, không để `[Authorize]` trần |
| **R-3** | **Pattern 16-bis 4-place mirror.** Menu-key mới/route mới phải có mặt ở: BE seed · `Layout.tsx` staticMap/regex · `App.tsx` route · resolvePath. Thiếu 1 ⇒ leaf **DROP IM LẶNG** (gotcha #50) | `Layout.tsx:37-45,181-188` · `App.tsx:107-110` | Việc này **không đẻ menu-key mới** (giữ `/khkk/list`) ⇒ rủi-ro thấp. Nhưng nếu ai thêm `?id=` vào staticMap thì phải soát lại menu-highlight |
| **R-4** 🔴 | **Chip lọc CỐ Ý không ghi URL.**`KhkkListPage.tsx:12-13,211-216` — ghi URL sẽ làm sidebar **sáng nhầm leaf** (bug UAT S155) | `:82-102` | Panel 1 mới: `q`/`phase` ghi URL kiểu PE sẽ **hồi-sinh bug S155**. 🔸 Đề-xuất: `?id=` ghi URL (cần bookmark) nhưng `phase`/`group` giữ ở lớp override (state), y như hiện tại. **Đây là chỗ dễ làm hỏng nhất khi "chép khuôn PE cho giống"** |
| **R-5** | **8 nhóm dùng CHUNG 1 route** ⇒ mọi thứ đọc từ URL phải đọc **mỗi render**, không `useState` khởi-tạo-một-lần (đi leaf→leaf không remount) | `:15-19,70-102` (vá-5 K4b) | Host 3-panel mới phải giữ nguyên cơ-chế `navKey`/`lastNavKey` (`:95-101`), đừng viết lại bằng `useEffect` |
| **R-6** | **`PipelineTreePanel``currentStage` + `bodyMaxHClass`.** Trong host 3-panel (Panel-3 tự cuộn) phải truyền `bodyMaxHClass="max-h-[28rem] lg:max-h-none"` nếu không sẽ **2 thanh cuộn lồng nhau** (~26px vượt) | `ContractsListPage.tsx:247-249` · `PipelineTreePanel.tsx:93-97,221` | Chép nguyên chuỗi đó. Nếu đặt cây ở panel 1 thì giữ mặc-định |
| **R-8** | **Không có phiếu V1-legacy ở KHKK.** Module sinh 2026-07-29 (Mig 69), mọi phiếu đều V2 | `types/khkk.ts:1` · `KhkkWorkflowPanel.tsx:261-266` (chỉ fallback "chưa cấu hình Bước/Cấp") | **Không** port nhánh fallback V1 của PE (`PeDetailTabs.tsx:434-436`, `PeWorkflowPanel.tsx:481-485`) — sẽ là code chết |
| **R-9** | **Route hiện có không được phá.**`/khkk/:id` đang là đích của `navigate` ở list (`:280`), của nút "Danh sách" (`KhkkDetailPage.tsx:400`), và có thể của link ngoài | `App.tsx:110` | Giữ route; chỉ đổi ruột. Thêm `?id=` cho desktop, `navigate` cho `<lg` (khuôn `ContractsListPage.tsx:90-96`) |
| **R-10** | **`budgetFrozen` display-gate.** Phiếu `DaDuyet` đọc `ApprovedAmount`, phiếu chưa duyệt đọc `ProposedAmount` | `types/khkk.ts:296-301` · `Features.cs:177-181` | Card panel 1 hiện Σ tiền phải theo đúng cờ này, nếu không sẽ nói **2 con số khác nhau** cho cùng 1 phiếu (đúng lỗi vá-8 K2 đã sửa ở bảng — `KhkkListPage.tsx:247-250`) |
| **R-11** | **Cây tải tối đa 200 phiếu.**`PE_INDEX_PAGE_SIZE = 200`, có dòng cảnh-báo khi chạm trần | `PipelineTreePanel.tsx:22,252-256` | Nếu panel 1 dựng cây từ chỉ-mục PE thì phiếu KHKK của gói cũ hơn **sẽ không xuất hiện**. Phải giữ (và thấy) dòng cảnh-báo đó |
| **R-12** | **`KhkkWorkflowPanel` đang ở `pages/`, không phải `components/`** ⇒ import chéo `pages→pages` (`KhkkDetailPage.tsx:41`) | `KhkkDetailPage.tsx:41` | Dời file = đổi import ở ≥2 nơi × 2 app. Việc nhỏ nhưng dễ sót ở app thứ hai |
---
## §7. Checklist thực-thi ĐO ĐƯỢC
_(chưa ghi)_
Mỗi mục có lệnh/grep kiểm được. Chạy tại repo-root. `APP` ∈ {`fe-admin`,`fe-user`} — mọi mục phải PASS **cả hai**.
### Nhóm 0 — bắt buộc TRƯỚC khi viết dòng code đầu tiên
| # | Việc | Acceptance ĐO ĐƯỢC |
|---|---|---|
| A-0 | Đọc `hooks/usePipelineStages.ts:150-320` để chốt nguồn dữ-liệu phiếu KHKK cho folder GĐ2 (§4 D-9) | Ghi vào spec 1 dòng: query nào nuôi stage-2, có `KhkkListItemDto` sẵn hay phải thêm fetch. Không có dòng này ⇒ **không được bắt đầu A-3** |
| A-2b | Tên người trong `Approvals` không rỗng | Gọi thật 1 phiếu đã duyệt: response `approvals[].approvedByFullName` ≠ null cho ≥1 phần tử. 🔴 Không nghiệm-thu bằng "code trông đúng" |
| A-6 | Bộ lọc trạng-thái/nhóm **KHÔNG** ghi vào URL (R-4) | `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 đó |
### Nhóm 3 — FE nội-dung (từng khối của ảnh)
| # | Khối | Acceptance |
|---|---|---|
| A-7a | Panel 2 có `PePipelineStrip current={2}` | `grep -n "PePipelineStrip" $APP/src/components/khkk/KhkkDetailContent.tsx` → 1 hit kèm `current={2}` |
| A-7b | Panel 2 có **5 Section đánh số**, Section CUỐI = ý-kiến cấp duyệt | `grep -oE 'title="[0-9]\. [^"]+"' $APP/src/components/khkk/KhkkDetailContent.tsx` → 5 dòng, dòng cuối khớp `^title="5\. Ý kiến cấp duyệt` |
| A-7c | Ý-kiến nhóm theo BƯỚC + lưới 2 cột + meta `x/y cấp · N NV` | `grep -n "md:grid-cols-2" …KhkkDetailContent.tsx` ≥1 **và**`grep -c "cấp đã duyệt"` ≥1 **và**`grep -c "NV tham gia"` ≥1 |
| A-7d | Ý-kiến **KHÔNG CÒN** ở panel 3 | `grep -c "Ý kiến cấp duyệt" $APP/src/components/khkk/KhkkWorkflowPanel.tsx` = **0** |
| A-8a | Panel 3 có banner kết-thúc-sớm | `grep -c "không qua CEO" …KhkkWorkflowPanel.tsx` ≥1 |
| A-8b | Panel 3 có chip tên phòng + badge `⚑ Duyệt thay CEO` + làm mờ sau finalize | 3 grep: `departmentName` ≥1 · `Duyệt thay CEO` ≥1 · `opacity-50` ≥1 |
| A-8c | Panel 3 có `📎 File đính kèm khi duyệt` | `grep -c "File đính kèm khi duyệt"` ≥1 |
| A-8d | Panel 3 có `Lịch sử duyệt (N)` + `Lịch sử thay đổi` | `grep -c "Lịch sử duyệt"` ≥1 **và**`grep -c "Lịch sử thay đổi"` ≥1 |
| A-10 | PE và HĐ **không bị đụng** | `git diff --name-only` → **0 đường-dẫn** khớp `components/pe/` · `pages/pe/` · `components/contracts/` · `pages/contracts/`. Nếu có ⇒ dừng, giải-trình |
| A-11 | Build + typecheck sạch | `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) |
| A-12 | Test BE không vỡ | `dotnet test SolutionErp.slnx` → 0 failed |
| A-13 | Quyền không bị nới im-lặng | `grep -rn "Authorize" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs \| grep -c "Policy"` — số này phải **≥ số route**, và **0 dòng**`[Authorize]` trần (gotcha #82) |
| A-14 | Nghiệm-thu THẬT trên **fe-user:8080** (R-1), không phải fe-admin | Chụp 3 màn khớp Ảnh 1/2/3; kiểm bằng **cửa-sổ ẩn-danh** (bài S96 #77: cache ≠ deploy) |
### Nhóm 5 — chờ owner, KHÔNG tự làm
| # | Việc | Vì sao phải hỏi |
|---|---|---|
| A-15 | Cho phép người duyệt **upload file khi duyệt** (D-7, thêm `ApprovalAttachment=3`) | Đổi enum + endpoint + quy-tắc lưu-trữ hồ-sơ. Ảnh 2 có khối này nhưng owner chưa nói KHKK cần upload |
| A-16 | Panel-2 chuyển sang `readOnly` cứng + dựng workspace KHKK riêng (P2-7) | Làm nửa vời = **cắt mất đường sửa duy nhất** của phiếu Nháp |
1.**Không phải "sửa CSS", mà là "đưa GĐ2 về đúng lớp kiến-trúc mà GĐ1 và GĐ3 đang đứng".** GĐ2 là mắt-xích lệch duy nhất trong 4 giai-đoạn (§0 Đ-2). Nói với owner theo cách này thì phạm-vi công-việc tự giải-thích.
2.**Dữ-liệu lịch-sử duyệt của KHKK ĐÃ ĐƯỢC GHI TỪ ĐẦU** (`ContractSigningPlanWorkflowService.cs:534` + `:547`) — chỉ chưa có cửa đọc. Đừng ước-lượng việc này như "phải xây lại history"; nó là ~5 field DTO + 1 endpoint. Đây là chỗ dễ ước-lượng sai **đắt nhất** theo cả hai chiều.
3.**Chỗ dễ làm hỏng nhất khi "chép PE cho giống" là R-4** (ghi bộ lọc vào URL làm sidebar sáng nhầm — bug S155 đã đốt một lần) và **R-1** (nghiệm-thu nhầm app). Cả hai đều **không nhìn thấy được** trong ảnh owner gửi.
# REVIEWER — LĂNG-KÍNH 1/3: TRUNG-THÀNH với Ý-ĐỒ OWNER
> 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.
## 0. Tự tái-dựng con số load-bearing (≥2, đo bằng lệnh của TÔI)
(đang đo)
## 1. Câu (1) — 6 dòng "KHÁC-buộc-phải-khác" có THẬT SỰ buộc không?
## 6. Bảo-vệ chỗ bản invest ĐÚNG mà owner có thể tưởng sai
_(chưa ghi)_
## 7. Bỏ-sót bắt thêm (thách-CLEAN)
_(chưa ghi)_
## 8. Tổng-kết verdict
_(chưa ghi)_
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.