diff --git a/.claude/WAL.md b/.claude/WAL.md index bf93757..937255d 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -1,34 +1,34 @@ # WAL — auto-generated, không sửa tay updated: 2026-08-06 | session: S176 (phiên-LOGIC L11, window 4) | branch: main -goal: UAT UI module KHKK. Vòng-2 (12 defect) + vòng-3 (4 defect) ĐÃ ship + deploy VERIFY. Dừng @PAUSE-4 chờ anh soi prod rồi gửi vòng-4. +goal: UAT UI module KHKK. Vòng-2 (12) + vòng-3 (4) ĐÃ ship+deploy VERIFY; anh soi prod = "tạm ổn". Vòng-4 = 3 menu lọc đổi sang cây 4 tầng (O5 anh phán) — code xong, chờ ship+soi. chain: - [x] nối mạch /tiep @S176 (window 4): Sàn-3 ⑤=3 + ① kêu **2** orphan + ORPHAN-L(p=4,t=2), WAL sống ⇒ đúng ca /tiep · 7/7 `verify:` KHỚP · tick 49→50 · `_tiep-3.md` cân sổ -- [x] 7 commit sản-phẩm + 4 đợt deploy VERIFY 2-vế ×2 app: `43e1bd7` `a0ccac3` `b811e11` `32cc8d5` `95f72dc` `99cabd8` `0db1b99` -- [x] đợt B (bê style PE) + đợt D (O6·O4·O7·O8) + C3 + O3 vế-2 — mirror sha256 khớp, build xanh ×2 +- [x] 7 commit sản-phẩm + 4 đợt deploy VERIFY 2-vế ×2 app: `43e1bd7` `a0ccac3` `b811e11` `32cc8d5` `95f72dc` `99cabd8` `0db1b99` — gồm đợt B (bê style PE) + đợt D (O6·O4·O7·O8) + C3 + O3 vế-2, mirror sha256 khớp - [x] O3 vế-1 thêm N hạng mục + giá + ghi chú (0-mig): +`AddLine`/`DeleteLine` command + 3 route; `PeReferenceAmount=0` chống đếm-đôi + là DẤU phân biệt dòng-gốc/thêm-tay - [x] UAT vòng-2: anh review TRỰC TIẾP prod, khoanh đỏ **12 defect → land 12/12** (gộp mục 3+4 · gom nút về thanh đáy · mục 2 rút về Hạng mục+Giá→Tổng · hạng mục CỦA DỰ ÁN +`PeWorkItemName` BE · lọc theo nhóm · gỡ dải accent · cây gập/mở) - [x] cây màn Thao tác: **dùng LẠI `PipelineTreePanel`** (Năm›Dự án›Hạng mục›4GĐ) thay vì dựng bản 2 — `+56/−300` dòng - [x] `/fable-real investigator-codebase` `wf_a574cff1-6b9` — spec 3-mục ghi; **BÁC 2 giả-thuyết lead** (cột DỰ ÁN = CHỖ-CHỪA-SẴN không phải di-chứng · 11 ô chứ không phải ≥3) + phát hiện `Project.BudgetTotal` rỗng 61/69 - [x] cổng C2 @PAUSE-3 KÊU (A\B={investigator-codebase, ctx-verifier}) → seed 2 entry → A\B=∅ - [x] `ctx-curator` @PAUSE-3 = `SUA — 6 điểm`, áp đủ 6 rồi mới chèn MIND-4 (bắt được lead ĐẾM SAI commit của chính mình: viết 7 liệt 6) -- [!] 🔴 anh SOI MẮT prod 375/1366/1440 — mọi kết luận giao diện S175 là ĐỌC MÃ (5 lane NO-SCREENSHOT, gotcha #3); chưa soi thì CHƯA được tuyên PASS +- [x] anh soi prod → **vòng-3 "tạm ổn"**; câu (2) cột DỰ ÁN 11 ô + (3) `BudgetTotal` 61/69 = anh **PARK** ("chưa quan tâm lúc này") — đừng hỏi lại, đừng tự làm +- [!] 🔴 **vòng-4 anh giao: 3 menu Đang duyệt / Đã duyệt / Đã xóa → cây Năm›Dự án›Hạng mục›phiếu KHKK** (= chính O5, anh phán GIỮ CÂY). CODE XONG build xanh ×2, **CHƯA anh soi prod** — kết luận giao diện vẫn là ĐỌC MÃ (gotcha #3) - [x] **UAT vòng-3 ship `79936ad` + deploy VERIFIED 2-vế ×2 app** (admin `--p-YD6EX` · eoffice `DNq8Gz8s`; khung thẻ `border-[#e9eef4]`=1/1 · 2 chuỗi cũ=0/0 · control-dương `Hạng mục (dự án)`+`Lưu & Gửi duyệt`=1/1) — 4 defect: ① TRẢ LẠI khung thẻ (lead lượt trước gỡ CẢ `.card-accent` ⇒ mất luôn khung; anh chỉ muốn bỏ dải xanh — dựng lại khung bằng utility giữ đúng `#e9eef4`/`rounded-xl`) · ② cây Thao tác `approvedPeOnly` + `stagesOnly={[1]}` (lọc phase TRƯỚC khi dựng cây để badge đếm không nói dối; 4 host HĐ = 0/0 không đụng) · ③ ô Hạng mục (dự án) sang cột phải, nằm dưới Dự án · ④ bỏ 2 dòng chữ thừa - [x] `ctx-curator` @PAUSE-4 = `SUA — 9 điểm`, áp đủ 9 rồi mới chèn MIND-5 — vai sửa CÁCH LEAD TRÍCH gotcha #66 ("đè là vô hiệu" là OVERCLAIM; `gotchas.md:1165` ghi rõ fix ĐIỂM bằng `!important` THẮNG) + bắt lead đếm sai commit **lần thứ HAI liên tiếp** - [ ] 🔴 2 NỢ MỚI (dư-lượng sau khi gỡ `.card-accent`): ① khung thẻ có **2 nguồn sự-thật** (`index.css:112` ⟂ utility hardcode `#e9eef4`/`rounded-xl` ×2 app) — đổi CSS không kéo theo KHKK · ② comment đã ship overclaim gotcha #66 — 🔴 **ĐO ĐÍCH @S176 = 4 site / 2 CHUỖI KHÁC NHAU** (sổ cũ ghi mờ "≥3 chỗ"): `KhkkWorkflowPanel.tsx:278` ×2 app = *"đè là vô hiệu"* (SAI thẳng) ⟂ `PipelineTreePanel.tsx:226` ×2 app = *"vô hiệu IM LẶNG… Phải GỠ CLASS, không đè"* (chẩn ĐÚNG cho `border-0` trần, chỉ câu kết quá rộng — `border-0!` vẫn thắng). **Grep 1 chuỗi = vá 2/4 rồi tưởng xong** - [x] `ctx-verifier` @/tiep cửa-4 = `NAP-LAI-SAN-SANG — 10 cờ`; lead tự đo lại 3 cờ nặng, **CẢ 3 CONFIRMED** → đã flush bảng Owner-items `runs/…S174…/run.md` **6 dòng + dòng CÒN NỢ** (O1 `43e1bd7` · O3 hai vế `a0ccac3`+`b811e11` · O4/O6/O7/O8 `a0ccac3`) -- [ ] 🔴 **O5 "cần phán" MỒ-CÔI** (`KhkkListPanel:202 flatMode` đổi CÂY→PHẲNG khi có filter) — 0 hit ở WAL/`_mind`/`next:`, chỉ sống trong `runs/…S174…/run.md:26` +- [x] **O5 ĐÓNG — anh phán GIỮ CÂY.** `leafMode` OPT-IN ở `PipelineTreePanel` (4 tầng, bỏ tầng GĐ + 8 ngăn nhóm) + `buildTree(rows, weightOf)` đếm PHIẾU thay vì GÓI + tỉa nhánh 0-phiếu. `flatMode` bỏ 2 vế `deletedView`/`phase`, **GIỮ** `!canPe` (cây cần xương sống PE) + `term` (đường thoát server-side ngoài cửa-sổ 200). Thùng rác nối nguồn riêng `/deleted` pageSize 200 vì linkage cây KHÔNG có phiếu đã xoá +- [ ] 🔴 KHAI GIỚI-HẠN vòng-4 (chưa xử, chờ anh quyết): cây ăn **cửa-sổ 200** phiếu mới nhất ⇒ 3 menu này **thu hẹp tầm với** so với phẳng-phân-trang-thật. Dòng khai truncation của cây vẫn hiện; đường thoát = gõ tìm kiếm (rơi về phẳng server-side). Volume KHKK hiện nhỏ nên chưa cắn - [ ] 🔴 đính-chính `_context-s-11.md:152` — câu *"Lead đã flush (a)"* SAI: flush **1/7** (đúng dòng O2 vai gọi tên, bỏ 6 dòng cùng bảng). `_context` append-only ⇒ đính chính bằng entry PAUSE-5, KHÔNG sửa tại chỗ -- [ ] cột DỰ ÁN 11 ô: anh chốt "ko cần so sánh PRO/CCM" ⇒ phương-án engine KHÔNG land — hỏi anh coi ĐÓNG hẳn hay còn mở -- [ ] `Project.BudgetTotal` rỗng **61/69** dự án — có phải "mục dự án chưa điền vào" anh nói không +- 🅿️ **PARK (anh @S176 "chưa quan tâm lúc này") — KHÔNG hỏi lại, KHÔNG tự làm:** cột DỰ ÁN 11 ô (phương-án engine không land) · `Project.BudgetTotal` rỗng 61/69 - [ ] thăng luật E4 vế ➊ lên khối auto-inject `harness-eval/MEMORY.md ## Luật cứng` - [ ] 🔴 ghi synthesis cho **2** run-folder ORPHAN (0 flat `*-synthesis.md`) — sổ cũ chỉ ghi 1, ground-truth @S176 đếm 2: ① `2026-08-05-S173-bookend-open` · ② `2026-08-05-S175-duan-budget-column` (run `/fable-real` `wf_a574cff1-6b9`, có `run.md`+`spec-*.md` nhưng chưa harvest) - [ ] 5 điểm governance CHƯA ĐỘNG: `STATUS:473` 86→87 · gạch 5 slot HANDOFF · `ROTATE vrapp` · re-stamp Carry 14 slug · slot (70) anchor - [ ] `ring2-audit/MEMORY.md` 26.440 B > cap 25.600 (A1 treo từ PAUSE-1) · monthly drift-audit quá hạn · dọn 9 dir rỗng `runs/.../agent-memory/*` @session-end - [ ] kéo 2 thư `loi-do-luong` (ĐÃ CÓ ở `AI_INFRA/outbox/all`, SE chưa kéo) + 4× `upgrade-pack-phased` thiếu adap-report — đo @S176: `se=0` (23/23 đã vào `_index`), nợ đúng bằng sổ -next: đã /tiep @S176 → CHỜ anh soi prod 375/1366/1440 rồi gửi defect vòng-4 → [đuôi, đừng rơi] E4 ➊ · synthesis **2** run-folder orphan (S173 + S175-duan-budget) · 5 điểm governance · kéo thư + adap-report · A1 ring2 +next: vòng-4 (cây 4 tầng cho 3 menu lọc) ĐÃ code+build xanh ×2 → ship+deploy → CHỜ anh soi prod 375/1366/1440 → [đuôi, đừng rơi] E4 ➊ · synthesis **2** run-folder orphan (S173 + S175-duan-budget) · 5 điểm governance · kéo thư + adap-report · A1 ring2 verify: - `curl -s https://admin.solutions.com.vn/ | grep -oE 'index-[A-Za-z0-9_-]+\.js'` → khác `index--p-YD6EX.js` = đợt SAU đã tới (mốc @PAUSE-4 = bundle `79936ad`) diff --git a/fe-admin/src/components/khkk/KhkkListPanel.tsx b/fe-admin/src/components/khkk/KhkkListPanel.tsx index 8dde983..37398a3 100644 --- a/fe-admin/src/components/khkk/KhkkListPanel.tsx +++ b/fe-admin/src/components/khkk/KhkkListPanel.tsx @@ -22,12 +22,13 @@ // ⇒ ô tìm + `Select` trạng-thái đi SERVER-SIDE thẳng `GET /contract-signing-plans` // (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo // "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt. +import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { ChevronLeft, ChevronRight, FileCheck, Inbox, Search } from 'lucide-react' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel' -import { usePipelineStages } from '@/hooks/usePipelineStages' +import { usePipelineLinkage, usePipelineStages } from '@/hooks/usePipelineStages' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { @@ -40,6 +41,10 @@ import { } from '@/types/khkk' const PAGE_SIZE = 20 +// [S176] Cửa-sổ nguồn cho CÂY ở chế-độ thùng rác. Bằng đúng `INDEX_PAGE_SIZE`/ +// `PE_INDEX_PAGE_SIZE` của 2 index cây (200) để cả cây kể CÙNG một câu chuyện về giới-hạn +// — lấy 20/trang như chế-độ phẳng thì cây sẽ cụt mà không có dòng nào khai. +const TREE_SOURCE_PAGE_SIZE = 200 function formatVnd(n: number | null | undefined): string { if (n === null || n === undefined) return '—' @@ -198,17 +203,29 @@ export function KhkkListPanel({ onOpenPe?: (id: string) => void }) { const { canPe } = usePipelineStages() + const linkage = usePipelineLinkage() const term = search.trim() - // [R-11] Lọc đi SERVER-SIDE, KHÔNG lọc trên cây: có từ khoá / có trạng-thái / xem thùng - // rác ⇒ chuyển sang danh-sách phẳng có phân-trang thật (`total` từ server) nên phiếu nằm - // ngoài 200 phiếu mới nhất VẪN tới được. - // [M-1] `!canPe` ⇒ cũng dùng đúng danh-sách phẳng này — cây khoá không được phép làm - // người dùng mất trắng phiếu của chính họ. - const flatMode = !canPe || deletedView || term.length > 0 || phase !== null + // [S176 owner] "ở các menu đang duyệt/ đã duyệt/ xóa → cho hiển thị năm → dự án → hạng + // mục → phiếu KHKK nhé" ⇒ 3 menu đó nay đi CÂY (chế-độ lá 4 tầng), không còn phẳng. + // + // 🧊 Trước S176: `flatMode` còn gồm `deletedView || phase !== null`. Bỏ 2 vế đó. + // [R-11] GIỮ vế từ-khoá: tìm kiếm đi SERVER-SIDE có phân-trang thật (`total` từ server) + // nên phiếu nằm ngoài cửa-sổ 200 VẪN tới được — đó là đường thoát khi cây không + // với tới, và là lý do KHÔNG bỏ nốt vế này. + // [M-1] GIỮ vế `!canPe`: cây dựng trên xương sống phiếu Duyệt NCC (`enabled: canPe`), + // ai không có quyền đó thì cây KHÔNG dựng được — ép cây sẽ làm họ mất trắng phiếu + // của chính mình. Danh-sách phẳng là lưới an-toàn, không phải tuỳ chọn. + const flatMode = !canPe || term.length > 0 + + // Xem thùng rác ở chế-độ CÂY: phiếu đã xoá KHÔNG nằm trong index của cây + // (`usePipelineLinkage` chỉ gọi `/contract-signing-plans`), nên phải tự lấy từ endpoint + // riêng rồi tự gắn vào PE. Lấy đúng cửa-sổ 200 như 2 index kia để cùng một câu chuyện + // truncation, thay vì 20/trang của chế-độ phẳng. + const treeDeleted = deletedView && !flatMode const list = useQuery({ - queryKey: ['khkk-list', { group, phase, search: term, page, deletedView }], + queryKey: ['khkk-list', { group, phase, search: term, page, deletedView, tree: treeDeleted }], queryFn: async () => { const url = deletedView ? '/contract-signing-plans/deleted' : '/contract-signing-plans' return (await api.get>(url, { @@ -216,14 +233,39 @@ export function KhkkListPanel({ phase: deletedView ? undefined : (phase ?? undefined), group: group ?? undefined, search: term || undefined, - page, - pageSize: PAGE_SIZE, + page: treeDeleted ? 1 : page, + pageSize: treeDeleted ? TREE_SOURCE_PAGE_SIZE : PAGE_SIZE, }, })).data }, - enabled: flatMode, + enabled: flatMode || treeDeleted, }) + // Phiếu đã xoá gom theo PE — chỉ dựng khi đang xem thùng rác bằng cây. + const deletedByPeId = useMemo(() => { + const map = new Map() + if (!treeDeleted) return map + for (const k of list.data?.items ?? []) { + const bucket = map.get(k.purchaseEvaluationId) + if (bucket) bucket.push(k) + else map.set(k.purchaseEvaluationId, [k]) + } + return map + }, [treeDeleted, list.data]) + + // Phiếu treo dưới một gói thầu, SAU khi áp đủ bộ lọc đang bật (trạng-thái + nhóm). + // 🔴 Lọc ở ĐÂY = lọc TRƯỚC khi cây dựng ⇒ badge đếm ở cả 3 tầng tự khớp tập đang hiện. + // Đây đúng là luật vòng-3 ("lọc phase TRƯỚC khi dựng cây, lọc sau thì badge nói dối"). + const plansForPe = useMemo(() => { + return (peId: string): KhkkListItemDto[] => { + const src = treeDeleted ? (deletedByPeId.get(peId) ?? []) : (linkage.khkkByPeId.get(peId) ?? []) + const byGroup = group === null ? src : src.filter(k => k.approvalGroup === group) + // Thùng rác KHÔNG lọc theo trạng-thái: phiếu đã xoá giữ nguyên phase lúc bị xoá, lọc + // thêm chỉ tổ giấu mất phiếu người dùng đang đi tìm. + return treeDeleted || phase === null ? byGroup : byGroup.filter(k => k.phase === phase) + } + }, [treeDeleted, deletedByPeId, linkage.khkkByPeId, group, phase]) + const items = list.data?.items ?? [] const total = list.data?.total ?? 0 const totalPages = list.data?.totalPages ?? 1 @@ -242,11 +284,14 @@ export function KhkkListPanel({ return (