Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-implfe-k5.md
2026-08-01 10:27:38 +07:00

11 KiB
Raw Blame History

IMPLFE-K5: DONE

Lane FE K5 — cây toàn-trình per-gói: GĐ2 mở 8 sub-folder NHÓM (S166, 2026-08-01)

Nền: K4a+K4b đã commit 88e7ecf. SPEC PIN: spec-cụm2 vá-9 + checklist K5 + forward-provision B3-A5.

§0 Kế hoạch đo trước khi gõ

  • Grep CHÍNH XÁC bộ file pipeline trong fe-user/src (spec nói "4 file" — đừng tin, đếm)
  • Đọc usePipelineStages.ts: shape DTO builder đang ăn, có lines[].contractId không
  • Đọc component render folder: tiền lệ folder GĐ "SẮP" (hiện-cả-khi-rỗng)
  • SHA baseline từng file ×2 app

§1 ĐO ĐĨA (ground truth) — TRƯỚC khi gõ

1.1 Bộ file pipeline = ĐÚNG 4, và cả 4 ĐANG SHA-pair ×2 app

file sha256 baseline (×2 app IDENTICAL)
hooks/usePipelineStages.ts 7144eeb787a8ff8b5ed861d7278fe31450162c23da51dafb6266a3a9196ab148
components/pipeline/PipelineStageFolders.tsx d449cc8dae55c55aab4a8bd4521e113361e0859de93bdac4a4e7099f8cecdc2d
components/pipeline/PipelineTreePanel.tsx d70ac315bae485e6e0f1fad8327caf063b4b22cabde53c59d9847b0d407e73d3
components/pe/PePipelineStrip.tsx 6fb7337e4bb9c60a579e1dc561f6742785e9526cd8fd4b4bce593eec1c8dda65
2 call-site của buildStages: PipelineTreePanel.tsx:303PurchaseEvaluationsListPage.tsx:524 (cái sau truyền stage1Content).

1.2 Tiền lệ "hiện cả khi rỗng" CÓ SẴN — bám đúng nó, không đẻ khuôn mới

PipelineStageFolders.tsx:149-153: count > 0 ? <badge số> : <SoonChip/>; :166-167 thân rỗng in "Chưa có dữ liệu ở giai đoạn này."; :95 muted làm xám nhãn. Sub-folder nhóm sẽ dùng LẠI đúng 3 thứ này (chip "Sắp" + dòng rỗng + muted), không thêm token màu mới.

1.3 🔴 FORWARD-PROVISION B3-A5: lines[].contractId KHÔNG TỒN TẠI — không bịa

Đo 4 nơi, cả 4 đều âm:

  1. types/purchaseEvaluation.tsPeListItem:112-…contractId: string | null (:126) và 0 hit chữ lines trong TOÀN file.
  2. Domain/PurchaseEvaluations/PurchaseEvaluation.cs:31public Guid? ContractId (1 FK đơn, không phải tập).
  3. types/contracts.ts ContractListItem:84-101KHÔNG có purchaseEvaluationId ⇒ không thể đi ngược từ index HĐ về phiếu.
  4. Domain/Contracts/Contract.cs0 hit PurchaseEvaluationId/SourcePe.

🔴 Bằng chứng đắt nhất, chính BE tự khai (CreateContractFromEvaluationFeatures.cs:143-145):

// Idempotency: pe.ContractId = HĐ ĐẦU (chặn tạo lại). N HĐ liên-danh truy qua changelog +
// Contract (FE nhận list contractIds). 1 phiếu = 1 link chính + N hợp đồng.
pe.ContractId = contracts[0].Id;

Endpoint create TRẢ contractIds: string[] (FE nhận ở PeDetailTabs.tsx:2186) nhưng chỉ HĐ ĐẦU được ghi ngược vào phiếu; HĐ 2..N chỉ còn dấu vết trong changelog. ⇒ Đúng như đề bài lo: HĐ thứ 2+ VÔ HÌNH trên cây — và đó là giới hạn DỮ LIỆU, không phải lỗi builder. ⇒ Vế wire được lượt này: đổi HÌNH DẠNG builder từ "đọc 1 id" sang "gom một TẬP id" (union), nguồn thứ hai để sẵn chỗ cắm 1 dòng. Vế KHÔNG wire được: chính nguồn thứ hai (không có field nào để đọc). FLAG-1.

1.4 GĐ3/GĐ4 "by c.type" — kiểm, KHÔNG đụng

Builder hiện KHÔNG gom GĐ3/4 theo loại HĐ; c.type chỉ dùng ở onOpen GĐ4 → /hard-copies?type=${c.type} (:240, khớp HardCopiesPage.tsx:64-67). Đang đúng ⇒ giữ nguyên, chỉ đổi phần lấy id sang tập (1.3).

1.5 Ghi chú kiểu dữ liệu

KHKK_APPROVAL_GROUPS là tuple as const.includes(g: number) sẽ đỏ TS; phải ép as readonly number[]. k.approvalGroup khai number nhưng payload cũ trong cache React Query có thể thiếu (K2 để != null guard ở 2 page) ⇒ lọc bằng typeof g === 'number'. import.meta.env có sẵn (5 call-site), dùng .DEV cho assert Σ.

§2 THỰC THI — 2/4 file pipeline đổi (2 file kia KHÔNG cần đụng, xem §3C)

F1 — components/pipeline/PipelineStageFolders.tsx ×2 (+165 / 54)

  • Type mới PipelineGroup {key,label,leaves,onOpenAll?} + PipelineStage.groups?. 🔴 stage.leaves GIỮ vai trò danh sách PHẲNG canonical (nguồn duy nhất để đếm — stageCount, hideEmpty, chip số đều không phải sửa); groups chỉ chia lại chính nó. Nhờ vậy bất-biến Σ là chuyện CẤU TRÚC chứ không phải kỷ luật.
  • Tách LeafList (dùng chung thân folder ⟂ thân ngăn — 0 nhân bản markup) + thêm GroupFolder.
  • GroupFolder: <details> mặc định ĐÓNG (8 ngăn mở sẵn = tường chữ ở cột trái 19rem); summary = chevron + nút tên ngăn (preventDefault+stopPropagation ⇒ đi trang mà không kèm một nhịp đóng/mở nhấp nháy) + chip số hoặc SoonChip khi rỗng; thân rỗng in "Chưa có phiếu ở nhóm này."
  • Tái dùng 100% token cũ (SoonChip, style.head/count/code, mutedslate-500 giữ tương phản AA) — 0 màu mới, 0 icon mới.

F2 — hooks/usePipelineStages.ts ×2 (+122 / 54)

  • GĐ2 chia ngăn: toKhkkLeaf tách ra dùng 2 nơi; phân hoạch theo k.approvalGroup; 8 ngăn dựng từ KHKK_APPROVAL_GROUPS + nhãn khkkGroupMenuLabel(n) (types, K4a); onOpenAll → /khkk/list?group=${n} khớp ĐÚNG URL leaf sidebar K4b ⇒ bấm ngăn hay bấm menu ra cùng trang, và trang tự sáng đúng mục.
  • Hiện đủ 8 kể cả rỗng (.map trên hằng, không .filter) — mirror tiền lệ folder GĐ chưa có gì.
  • Ngăn "(chưa phân nhóm)" chỉ mọc khi THẬT SỰ có phiếu lạc (nhóm null/undefined/thiếu field/ngoài 1..8). Khác 8 ngăn kia: 8 nhóm là khung nghiệp vụ cố định, đây là chỗ chứa bất thường — hiện thường trực một ngăn luôn rỗng là nhiễu.
  • Bất-biến Σ: bảo đảm bằng vòng phân hoạch; thêm chuông import.meta.env.DEV && console.warn phòng người sau sửa vòng lặp thành lọc-trùng/lọc-sót.
  • Forward-provision B3-A5: contractIdsOfPackage = new Set<string>() gom id (union) rồi mới dựng GĐ3/GĐ4; chỗ cắm nguồn thứ hai để sẵn kèm ghi chú vì-sao + trích nguyên văn BE. seen cũ bỏ (Set tự khử trùng). Hành vi GĐ3/4 0 đổi (cùng tập HĐ, cùng thứ tự, cùng dedup).
  • GĐ3/GĐ4 theo c.type: kiểm — builder KHÔNG gom theo loại; c.type chỉ dùng ở onOpen GĐ4 → /hard-copies?type= (khớp HardCopiesPage.tsx:64-67). Đang đúng ⇒ KHÔNG đụng (đúng lệnh mục 2).

§3 CHỨNG (mọi phép đo kèm CONTROL)

A. Phân hoạch + Σ + "hiện đủ 8" — chạy CHÍNH khối text đã ship (indexOf('const khkkByGroup') → slice → new Function; 4 phép thế khai minh bạch: 3 bỏ type-annotation + import.meta.env.DEV→cờ tiêm, mỗi phép thế đều assert khớp trước khi chạy — không khớp thì script tự chết chứ không âm thầm chạy tiếp). 15/15 OK:

CA1 rải đều      8 ngăn · Σ 5=5 · nhóm-2 có 2 · nhóm-3 RỖNG VẪN HIỆN · 0 cảnh báo
                 route /khkk/list?group=1 … ?group=8 · nhãn "4. nhan-N4" đúng khuôn helper
CA2 phiếu lạc    9 ngăn (8 + "(chưa phân nhóm)") · gom đủ 4 ca lạc (null/9/undefined/thiếu field) · Σ 5=5
CA3 gói rỗng     VẪN 8 ngăn rỗng · KHÔNG đẻ ngăn lạc thừa
CA4 CONTROL-ÂM   tiêm lỗi nhân-đôi vào chính đoạn ⇒ console.warn NỔ:
                 "Σ ngăn (4) ≠ Σ folder (2) — có phiếu bị nhân đôi hoặc rơi mất khi chia nhóm."

CA4 là thứ chứng chuông Σ có răng — không có nó thì "0 cảnh báo" ở CA1-3 vô nghĩa (im lặng vì đúng ⟂ im lặng vì câm).

B. Build ×2 PASS, 0 TS error (tsc -b chạy riêng trước khi mirror cũng exit 0)

app modules bundle js css time
fe-user index-CZUcTUUg.js index-CHYZ6n3Q.css 19.81s
fe-admin 1961 index-BZWvx0Y8.js 1,725.90 kB (gzip 429.20) index-AgG72YRZ.css 86.93 kB 10.88s
Warning >500KB + INEFFECTIVE_DYNAMIC_IMPORT = PRE-EXISTING.

C. SHA-pair 4/4 file pipeline ×2 app

file trước K5 sau K5 ×2
hooks/usePipelineStages.ts 7144eeb7… edff99314e8b75de39b0123bcdad5d98d8081a5bd294e2e05b1ef784094a19ac IDENTICAL
pipeline/PipelineStageFolders.tsx d449cc8d… bed52f39d595c86a24afd27180146871d52e29943731ffb52f7caf23e66de936 IDENTICAL
pipeline/PipelineTreePanel.tsx d70ac315… d70ac315… KHÔNG ĐỔI IDENTICAL
pe/PePipelineStrip.tsx 6fb7337e… 6fb7337e… KHÔNG ĐỔI IDENTICAL
🔴 2 file cuối cố ý không đổi: PipelineTreePanel chỉ gọi buildStages(wg.items) rồi đưa cho PipelineStageFolders — ngăn nhóm chảy qua nó mà không cần biết; PePipelineStrip là DẢI 4 giai đoạn, không phải cây. Hai hash bằng baseline = đối chứng dương cho phép đo (nếu phép so hash sai thì 2 dòng này cũng phải đổi). diff-stat đối xứng 165+/122+ ×2 app.

D. Ràogit status: đúng 4 file (2 cặp). 0 src/Backend/**, 0 KhkkListPage/KhkkCreatePage/Layout.tsx (K4b đã chốt), 0 commit.

§4 FLAG cho lead

FLAG-1 (MAJOR — chặn hoàn thiện B3-A5, cần lead quyết + lane BE): lines[].contractId không tồn tại ở bất cứ đâu (4 phép đo §1.3, cả FE lẫn BE). Union hôm nay chỉ có 1 nguồn ⇒ với phiếu liên danh đẻ N hợp đồng, HĐ thứ 2+ vẫn vô hình trên cây — tôi đã dựng đúng hình dạng để cắm, nhưng KHÔNG bịa field. Bằng chứng BE tự khai: CreateContractFromEvaluationFeatures.cs:143-145 pe.ContractId = contracts[0].Id + "N HĐ liên-danh truy qua changelog". Hai đường lead chọn: (a) PE list DTO trả contractIds: string[] (rẻ — dữ liệu đã có, chỉ là không được ghi ngược) · (b) K7 Line.ContractId ghi-luôn (đắt hơn, nhưng đúng hạt "1 dòng phiếu → 1 HĐ" mà OG-1 UI-1-1 mô tả). Cắm vào builder = 1 dòng, chỗ đã đánh dấu.

FLAG-2 (MINOR — đã quyết, báo để khỏi tưởng sót): ngăn nhóm mặc định ĐÓNG. 8 ngăn mở sẵn dưới mỗi gói thầu, nhân với N gói trên cây cột trái = không đọc được gì. Nếu owner muốn mở sẵn ngăn có phiếu thì 1 dòng (defaultOpen = leaves.length > 0) — không tự quyết vì đó là UX.

FLAG-3 (MINOR — giới hạn kế thừa, không phải mới): đếm ngăn vẫn nằm trong cửa sổ index 200 (INDEX_PAGE_SIZE) — gói có nhiều phiếu KHKK thì truncated đã cảnh báo sẵn ở chân folder cha, nhưng cảnh báo đó không lặp lại ở từng ngăn. Nếu thấy cần thì nhắc, tôi thêm.

IMPLFE-K5-END