wal: flush 20260804T1628

This commit is contained in:
pqhuy1987
2026-08-04 16:28:33 +07:00
parent 612328d56d
commit 31e47dcc54
6 changed files with 482 additions and 11 deletions

View File

@ -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 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 /3T (3T=sai-số-đo 13B-tiền-redact+làm-tròn; ADDITIVE vững; T-1..T-5 tồn-dư 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 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 tiepcloseout) + **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). Stage A `/fable-real investigator-codebase` tier=fable **ĐANG CHẠY** run-id `wf_b76692d4-3a4` B `/fable-clone reviewer` ×3 lăng-kính C lead 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 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 lăng-kính (fidelity·feasibility·regression) **ĐANG CHẠY** `wf_832dadb2-93f` C lead refute+synthesizeghi 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 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 kênh) · (64) user TEST · (66) dọn 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 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)

View File

@ -70,7 +70,9 @@ Opus 5 MAX theo `spec-khkk-ui-mirror-pe-04-08-2026.md` (lead ghi SAU khi verify
## Stages
- [ ] A — `/fable-real investigator-codebase` deep-pass
- [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

View File

@ -1,32 +1,365 @@
# sub-investigator-codebase-invest-s171 — KHKK UI mirror PE (fable-real LỆNH-A)
> ENGINE-ĐẮT single top-model deep-pass. PROPOSE-ONLY. Mọi khẳng-định kèm `file:line`.
> Trạng-thái: KHUNG GHI LƯỢT 1 — nội-dung append dần.
> Đườ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` |
| Header | 1 dòng `<header>`: icon `ClipboardCheck` + `h1` tiêu-đề động + badge đếm `rounded-full bg-slate-100` | `:382-390` |
| Tiêu-đề động | `headerTitle` đổi theo `deletedView`/`typeFilter`/`pendingMe` | `:289-293` |
| Grid | `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]` | `:397` |
| URL-param lái toàn màn | `type` · `pendingMe` · `deleted` · `q` · `phase` · `awId` · `id` · `expand` | `:38-50` |
| 2 query nuôi 3 panel | `list` (`['pe-list',{…}]`) + `detail` (`['pe-detail', selectedId]`, `enabled: !!selectedId`) | `:74-117` |
🔴 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.
### 1.1 Panel 1 — LIST (trái, `400px`)
`<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">` `:399`
1. Khối filter `space-y-2 border-b border-slate-200 p-3` `:400`
- Search icon-trong-ô: `Search` absolute + `<Input className="pl-8">`, placeholder `"Tìm mã / tên gói thầu / dự án…"` `:401-409`
- `Select` quy-trình duyệt — **chỉ khi `pendingMe`** `:413-422`
- Banner đỏ "Phiếu đã xóa" `:427-431` / banner amber "Lọc cố định: Đã gửi duyệt" `:432-435` / `Select` trạng-thái `:437-453` (ba-ngả loại-trừ)
2. Vùng cuộn `flex-1 overflow-y-auto` `:457`
- Skeleton 6× `h-16 animate-pulse` `:458-464` · `EmptyState icon={ClipboardCheck}` `:465-469`
- **Cây 3 tầng `<details>`**: `📅 Năm``📁 Dự án``🧱 Hạng mục` `:472-637`; chevron `group-open/year|proj|wi:rotate-90`; badge đếm mỗi tầng; dựng ở `yearGroups` `:220-262`
- Tầng 4 = `<PipelineStageFolders currentStage={1} hideEmpty={filterActive} stages={buildStages(wg.items,{stage1Content:<ul>…</ul>})} />` `:524-625` — 4 folder giai-đoạn; danh-sách phiếu bị RE-PARENT vào folder GĐ1
- Card phiếu `:530-621`: hàng 1 `PeUrgentChips` + `PeFinalizeChip` + tên gói thầu + badge trạng-thái; hàng 2 mã `font-mono` + timestamp; hàng 3 `👤 drafter · dept` + `✓ HĐ`
- Nút `Maximize2` "Xem mở rộng" absolute, `opacity-0 … group-hover/row:opacity-100` `:607-620`
3. Persist mở/đóng: `localStorage` key `pe_list_expanded_projects_v3` `:269-287`
### 1.2 Panel 2 — DETAIL (giữa, `1fr`)
`<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">` `:643`
- Chưa chọn ⇒ `EmptyState` "Chọn phiếu ở danh sách" `:644-652` (khớp Ảnh 1)
- `detail.data && !isExpand``<PeDetailTabs evaluation onBack={closeDetail} onDelete readOnly={true} />` `:657-664`
**Ruột `PeDetailTabs`** (`components/pe/PeDetailTabs.tsx`, 3613 dòng):
- Đầu phiếu: `<PePipelineStrip current={1} />` `:277` — dải 4 giai-đoạn
- Banner phiếu Trả-lại (readOnly) `:386-395`
- `<div className="divide-y divide-slate-200">` `:397` chứa **5 khối đánh số**, đúng thứ-tự dọc:
1. `<Section title="1. Thông tin gói thầu">` `:402`
2. `2. Hạng mục + Báo giá NCC (…)` `:405`
3. `3. Đơn vị NCC/TP được chọn` `:418`
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`
- `<Section>` = `<section className="px-3 py-3 sm:px-5 sm:py-4">` + `h3 mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500` `:514-523`
**Section 5 (khối owner khoanh đỏ ở Ảnh 3)**`:666-774`:
- `LevelOpinionsSectionV2` lặp `flow.steps`; mỗi Step tính `totalLevels` · `totalApprovers` · `signedLevels` `:680-700`
- `StepOpinionsBox` `:705-745`: header `Bước {n} — {name}` uppercase bold + chip `departmentName` nền `bg-emerald-50` + meta phải `ml-auto` `{signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers} NV tham gia` `:718-733`; rỗng ⇒ `— Chưa có ý kiến duyệt.` `:736`; có ⇒ `grid grid-cols-1 gap-3 md:grid-cols-2` `:738`
- `StepOpinionEntry` `:747-774`: card `rounded-lg border border-emerald-200 bg-white p-3`; `Cấp {n} — {tên}`; badge `⚠ Admin {X} duyệt thay` khi `signedByUserId !== approverUserId` `:748,756-760`; badge `✓ Đã duyệt` `:762-764`; body `whitespace-pre-wrap` `:766`; footer timestamp `:769-771`
### 1.3 Panel 3 — WORKFLOW (phải, `360px`)
`<aside className="hidden overflow-y-auto border-l border-slate-200 bg-white p-4 lg:block">` `:668`
- Chưa chọn ⇒ hộp `border-dashed` + icon `X` + "Quy trình duyệt sẽ hiện khi chọn phiếu." `:669-676` (khớp Ảnh 1)
- Có phiếu ⇒ `<PeWorkflowPanel evaluation readOnly={!pendingMe} onApproved onDeleted />` `:677-684`
**Ruột `PeWorkflowPanel`** (`components/pe/PeWorkflowPanel.tsx`, 1062 dòng) — thứ-tự dọc ĐÚNG như Ảnh 2:
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`)
6. Banner S134 phiếu-trước-chưa-duyệt `:512-515`
7. 3 nút + nút XÓA (S155, CẠNH `next.map`) `:564` · dialog transition + 4 return-mode `:633-707` + toggle skip-to-final `:723-745` + CCM-done `:749` + ô-tích level-finalize `:769-791` + bộ chọn giá chốt `:792` + upload file-khi-duyệt `:821`
8. `DeptApprovalsSection` (legacy 2-cấp phòng ban) `:920-924`
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
`:694-776`. `fixed inset-0 z-50`, backdrop `bg-slate-900/40`, panel `translate-x-full → translate-x-0` 200ms, mount-then-animate 2×rAF `:308-336`, khoá `body.overflow` `:340-349`, Esc + focus-trap `:352-378`. Body = 2 cột `lg:flex-row`: `<main>` `PeDetailTabs` + `<aside lg:w-[24rem] xl:w-[26rem]>` `PeWorkflowPanel` `:753-772`.
**Cùng 2 component, 3 khung khác nhau** (inline · overlay · route mobile). Đây là bằng-chứng cho §5.
---
## §2. Bản-đồ KHKK hiện-trạng
_(chưa ghi)_
### 2.0 Khung — KHÁC LOẠI, không phải khác CSS
| Hạng-mục | KHKK | `file:line` |
|---|---|---|
| Root list | `<div className="space-y-5">` — trang **cuộn theo body** | `pages/khkk/KhkkListPage.tsx:161` |
| Header | `<PageHeader eyebrow="Giai đoạn 2" title subtitle icon accent actions>` (khuôn PURO) | `:162-192` |
| Grid | `grid gap-5 lg:grid-cols-[19rem_minmax(0,1fr)] xl:grid-cols-[21rem_minmax(0,1fr)]` = **2 panel** | `:197` |
| Panel trái | `<PipelineTreePanel currentStage={2} />` — cây TOÀN TRÌNH điều-hướng, **không chứa phiếu KHKK** | `:198` |
| Panel phải | 6 `KpiCard` lọc `:202-220` + card search `:222-233` + **bảng 8 cột** `:239-339` + phân trang `:341-355` |
| Chọn phiếu | `onClick={() => navigate('/khkk/'+k.id)}`**rời trang** | `:280` |
| Route | `/khkk/list` · `/khkk/create` · `/khkk/:id` | `App.tsx:107-110` |
| Detail | Trang riêng, `KhkkWorkflowPanel` render INLINE **ngay dưới `PageHeader`, TRÊN mọi nội-dung** | `pages/khkk/KhkkDetailPage.tsx:411` |
| Detail sections | 4 `SectionCard` rời: Thông tin `:414` · Dòng giá per-NCC `:505` · Căn cứ hồ sơ `:626` · File đính kèm `:721` | |
### 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` |
| Chip **tên phòng** của Bước | ❌ **VẮNG** | `types/khkk.ts:275-282` (chỉ `departmentId`) |
| Badge `⚑ Duyệt thay CEO` per-Cấp | ❌ **VẮNG** (cờ `allowApproverFinalize` ĐÃ CÓ trên DTO, chỉ dùng cho ô-tích) | `types/khkk.ts:272` · `:143` |
| Làm mờ Bước/Cấp sau finalize | ❌ **VẮNG** | — |
| Badge vai `✎ NS PRO/CCM` | ❌ VẮNG | — |
| `📎 File đính kèm khi duyệt` | ❌ **VẮNG** | — (§4 D-7) |
| `Lịch sử duyệt (N)` | ❌ **VẮNG** | — (§4 D-2) |
| `Lịch sử thay đổi` | ❌ **VẮNG** | — (§4 D-3) |
| Ý kiến cấp duyệt | ⚠️ **CÓ NHƯNG SAI PANEL** — list phẳng 1 cột trong panel duyệt | `:376-412` |
| 3 nút + Dialog | ✅ | `:299-374` · `:415-488` |
| Ô-tích "Cấp này KẾT THÚC" (opt-in) | ✅ | `:451-470` |
| 4 return-mode | ❌ cố-ý bỏ (W3 = TraLai toàn phần) | `:6-8` · `:290` |
| 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 |
| K3 | Grid | `[400px_1fr_360px]` (`:397`) | `[19rem_minmax(0,1fr)]` (`:197`) | **GIỐNG-được** | Đề-bài trực-tiếp |
| 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) |
| P2-6 | Lưới 2 cột card ý-kiến | `md:grid-cols-2` + card emerald + badge `✓ Đã duyệt` + `⚠ … duyệt thay` + placeholder `(duyệt — không ý kiến)` (`:738-772`) | `<li>` 1 cột `text-[11px]` (đã có nhãn `duyệt thay` + placeholder) | **GIỐNG-được** | Dữ-liệu đã đủ 100% (`KhkkLevelOpinionDto` `types/khkk.ts:245-258``approverUserId`/`signedByUserId`/`comment`/`signedAt`) |
| P2-7 | Sửa nội-dung | panel 2 `readOnly={true}` **cứng** (`:663`); sửa ở workspace riêng (`PurchaseEvaluationWorkspacePage`) | sửa **inline** ngay trong detail (nút Sửa/Lưu, `KhkkDetailPage.tsx:422-427`) | **KHÁC-buộc-phải-khác** (có điều-kiệ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. 🔸 Điều-kiện: khi nào có workspace KHKK thì dòng này lật lại thành GIỐNG-được. Phải khai trong spec, đừng để thành di-sản im-lặng |
### 3.D Panel 3
| # | Hạng-mục | PE | KHKK | Phán | Lý-do |
|---|---|---|---|---|---|
| P3-1 | Meta workflow | `{code} v{NN} · {name}` (`:348-353`) | `{code} · {name}` (`:203-208`) | **GIỐNG-được** | Thiếu `version` (§4 D-5) |
| 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-12 | 4 return-mode (Cấp / Bước / NV / Drafter) | có (`:633-707`) | 1 mode (về Bước 1·Cấp 1) (`:290`) | **KHÁC-buộc-phải-khác** | Chuỗi KHKK là **3 trạm ngắn** (PRO→CCM→CEO) — "lùi 1 cấp" gần trùng "về đầu". Thêm 4 mode = đổi **state machine BE**, ngoài phạm-vi UI. 🔸 Khai rõ, không giấu |
| 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 |
**Tổng:** 27 GIỐNG-được · 6 KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG (không việc).
---
## §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-8** | `stage2Content` / `stage2Count` | `BuildStagesOptions` (`hooks/usePipelineStages.ts:163`) **chỉ có `stage1Content`/`stage1Count`**; stage 2 dựng bằng `leaves` (link) `:265`. `PipelineStageFolders` **đã hỗ-trợ** `content` tổng-quát (`PipelineStageFolders.tsx:88` + render `:285-286`) | P1-1 | Mở rộng ĐỐI-XỨNG: `stage2Content`/`stage2Count` — sửa **1 hook**, 0 đụng component trình-bày |
| **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**.
### Bố-cục đích (đề-xuất)
```
/khkk/list ← host 3-panel, khuôn ContractsListPage
├─ Panel 1 400px KhkkListPanel cây Năm>Dự án>Hạng mục>folder GĐ2 → card phiếu KHKK
│ + search + Select trạng-thái + Select nhóm N1-N8
├─ Panel 2 1fr KhkkDetailContent PePipelineStrip current={2}
│ Section 1 Thông tin kế hoạch
│ Section 2 Dòng giá per-NCC trúng thầu
│ Section 3 Căn cứ hồ sơ (b.8-9)
│ Section 4 File đính kèm
│ Section 5 Ý KIẾN CẤP DUYỆT ← Ảnh 3 khoanh đỏ
└─ Panel 3 360px KhkkWorkflowPanel chưa chọn → PipelineTreePanel currentStage={2}
(khuôn ContractsListPage.tsx:249)
đã chọn → 1 Quy trình duyệt + meta
2 Banner kết-thúc-sớm
3 Sơ-đồ Bước→Cấp (+chip phòng, +⚑, +mờ-sau-finalize)
4 Nút Duyệt/Trả lại/Từ chối + Dialog
5 📎 File đính kèm khi duyệt
6 Lịch sử duyệt (N)
7 Lịch sử thay đổi
```
`/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``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-7** | **Mirror SHA256 2 app.** 4 file KHKK + `PipelineTreePanel` + `usePipelineStages` đều khai "MIRROR SHA256 identical" | `KhkkListPage.tsx:3-4` · `types/khkk.ts:2` · `PipelineTreePanel.tsx:6` | Acceptance §7 A-9: `sha256` từng cặp, không "nhìn thấy giống" |
| **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** |
### Nhóm 1 — BE (nhỏ, 1 lượt)
| # | Việc | Acceptance |
|---|---|---|
| A-1 | DetailDto +5 field: `WorkflowVersion` · `EndedByLevelFinalize` · `FinalizeStepName` · `FinalizeLevelName` · `List<…ApprovalDto> Approvals`; StepDto +`DepartmentName` | `grep -n "record ContractSigningPlanDetailDto" -A 70 src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs` → thấy đủ 5 tên; `grep -c "DepartmentName" …WorkflowStepDto` ≥1 |
| A-2 | `GET /contract-signing-plans/{id}/changelogs` policy `KeHoachKyKet.Read` | `grep -n "changelogs" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs` → 1 hit, và dòng `[Authorize(Policy = "KeHoachKyKet.Read")]` đứng ngay trên (gotcha #85 / #82) |
| 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" |
### Nhóm 2 — FE cấu-trúc
| # | Việc | Acceptance |
|---|---|---|
| A-3 | `$APP/src/components/khkk/` tồn-tại, chứa `KhkkListPanel.tsx` · `KhkkDetailContent.tsx` · `KhkkWorkflowPanel.tsx` (dời từ `pages/`) | `ls $APP/src/components/khkk/` → 3 file; `grep -rn "pages/khkk/KhkkWorkflowPanel" $APP/src`**0 hit** |
| A-4 | `KhkkListPage` là host 3-panel | `grep -n "lg:grid-cols-\[" $APP/src/pages/khkk/KhkkListPage.tsx` → khớp `^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\]` ; và `grep -c "h-\[calc(100vh-4rem)\]"` = 1 |
| A-5 | Chọn phiếu = `?id=` (desktop) / `navigate` (`<lg`), route `/khkk/:id` CÒN SỐNG | `grep -n "matchMedia('(min-width: 1024px)')" $APP/src/pages/khkk/KhkkListPage.tsx` ≥1 · `grep -n 'path="/khkk/:id"' $APP/src/App.tsx` = 1 |
| 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-8e | Panel 3 chưa chọn ⇒ `PipelineTreePanel currentStage={2} bodyMaxHClass=…` (R-6) | `grep -n "bodyMaxHClass" $APP/src/pages/khkk/KhkkListPage.tsx` → 1 hit chứa `lg:max-h-none` |
### Nhóm 4 — chống hồi-quy
| # | Việc | Acceptance |
|---|---|---|
| A-9 | Mirror 2 app byte-identical | Với mỗi file mới/sửa: `sha256sum fe-admin/src/<p>` = `sha256sum fe-user/src/<p>`**PASS 100%**, không trừ file nào |
| 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 |
| A-17 | Overlay "Xem mở rộng" cho KHKK (P1-7) | Chi-phí cao (focus-trap + 2×rAF + khoá scroll), owner chưa nêu. Đề-xuất P2 |
---
## §8. Ba câu tôi muốn lead đọc kỹ nhất
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.
---
END investigator-codebase-invest-s171 — VERDICT=27 giống-được / 6 buộc-khác / 8 data-gap

View File

@ -0,0 +1,46 @@
# reviewer — LĂNG-KÍNH 2/3: KHẢ-THI KỸ-THUẬT + SỰ-THẬT DỮ-LIỆU
> 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)
_(chưa đo)_
## 1. D-2 — `ContractSigningPlanWorkflowService.cs:534` ghi Approvals: LUÔN hay 1-NHÁNH?
_(chưa đo)_
## 2. D-3 — changelog ≥9 chỗ ghi + `UserName` denorm + 0 endpoint đọc
_(chưa đo)_
## 3. D-9 / D-8 — `usePipelineStages.ts:150-320` (lỗ hổng bản invest TỰ KHAI)
_(chưa đo)_
## 4. Chi-phí BE thật (`ContractSigningPlanFeatures.cs:145-181`) + D-6 join Users
_(chưa đo)_
## 5. §5 Phương-án A vs B — lý-do bác B có đứng vững?
_(chưa đo)_
## 6. §7 Checklist — mục nào GOODHART-able?
_(chưa đo)_
## 7. Bỏ-sót lane này bắt thêm (thách-CLEAN)
_(chưa đo)_
## 8. Chỗ bản invest ĐÚNG mà owner có thể tưởng sai
_(chưa đo)_
## 9. Chưa đo được / lệnh cần chạy
_(chưa đo)_
END reviewer-lens-feasibility-s171 — VERDICT=? HELD / ? BROKE / ? bỏ-sót-bắt-thêm

View File

@ -0,0 +1,50 @@
# 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?
(đang đo)
### 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
## 2. Câu (2) — 27 dòng "GIỐNG-được" có PHỦ HẾT 3 ảnh không?
(đang đo)
### 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
## 3. Câu (3) — Bố-cục đích §5 có đúng "content trên, duyệt+ghi-chú dưới"?
(đang đo)
## 4. Câu (4) — Nhóm 5 "chờ owner" (A-15/A-16/A-17): hoãn đúng hay hoãn lẩn?
(đang đo)
## 5. Chỗ bản đó ĐÚNG mà owner có thể tưởng sai (bảo vệ kết-luận đúng)
(đang đo)
## 6. Bảng tổng-kết verdict
| # | Claim | Verdict | Bằng-chứng |
|---|---|---|---|
## 7. Lỗ-hổng tôi KHÔNG đo được (khai thật)
(đang đo)
END reviewer-lens-fidelity-s171 — VERDICT=? HELD / ? BROKE / ? bỏ-sót-bắt-thêm

View File

@ -0,0 +1,40 @@
# 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.
## 0. Tái-dựng số load-bearing (bắt buộc ≥2, tự đo)
_(chưa ghi)_
## 1. R-4 — URL-param vs sidebar-highlight (claim nguy nhất)
_(chưa ghi)_
## 2. R-1 — fe-admin ẩn menu KHKK (mirror còn bắt buộc?)
_(chưa ghi)_
## 3. Quyền — gotcha #85 + #82, 2 tầng ĐỘC LẬP (endpoint `changelogs` A-2 / A-13)
_(chưa ghi)_
## 4. Cái gì bị ĐỘNG mà bản invest KHÔNG liệt (blast-radius tự quét)
_(chưa ghi)_
## 5. R-10 `budgetFrozen` + R-11 trần 200 phiếu (phán lại mức nghiêm-trọng)
_(chưa ghi)_
## 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)_