From 79936ad4d3d9043043ff0fc21e20ded196430fcf Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 5 Aug 2026 22:26:45 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Contract:=20KHKK=20UAT=20vong-3=20?= =?UTF-8?q?=E2=80=94=20khung=20the,=20cay=20chi=20PE=20da=20duyet,=20don?= =?UTF-8?q?=20chu=20thua=20(0-mig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 defect anh gui khi soi prod: === 1. TRA LAI KHUNG THE (anh: "no mat luon cai khung ben ngoai roi, y tao bo cai vien xanh thoi, con bo khung table thuan van nen giu lai") === 🔴 LOI CUA LEAD, khai thang: `.card-accent` (`index.css:112`) goi HAI thu — ① KHUNG the: bo goc 0.75rem + vien 1px #e9eef4 + nen trang + do bong -> anh GIU ② DAI ACCENT doc 3px ben trai (`border-left`) -> anh BO Luot truoc lead go CA CLASS => mat luon ①, panel troi khong khung. Nay dung lai ① bang utility thuan, GIU DUNG so do goc (#e9eef4, rounded-xl) de khong lech tong voi cac the khac con dung `.card-accent`. 🔸 Bo luon hover-nhac-the cua class goc: hieu-ung do thiet-ke cho KPI-card nho; dat len panel noi-dung cao ca man thi nhap-nhay vo co moi lan re chuot. 🔸 Bai: "go class" ≠ "go thuoc tinh". Class gop N thuoc tinh thi go ca class la go ca N — phai tach ra cai nao owner thuc su khong muon. === 2. CAY MAN THAO TAC CHI GIU PE DA DUYET (anh: "cho nay chi can NCC da duyet la dc roi nhe, con lai bo het di") === `PipelineTreePanel` +2 prop OPT-IN: · `approvedPeOnly` — chi phieu PE phase DaDuyet. Man nay de CHON PE ma lap ke hoach; phieu chua duyet xong thi lap khong duoc, hien ra la moi bam nham. 🔴 Loc phase TRUOC khi dung cay => so dem o moi tang (Nam/Du an/Hang muc) tu khop tap da loc. Loc SAU khi dung thi badge dem se NOI DOI. · `stagesOnly={[1]}` — chi folder Duyet NCC; 3 giai-doan kia (Ke hoach HD + 8 ngan nhom · Duyet HD · HD cung) la nhieu o man nay. Loc HIEN THI, KHONG dung bo dung. 🔴 CA HAI OPT-IN. Hau-kiem 4 host cay module Hop dong: approvedPeOnly=0, stagesOnly=0 => 0 doi hanh vi. === 3. DUA O HANG MUC (DU AN) SANG COT PHAI (anh: "dua qua ben phai nhe") === Luoi 2 cot doc theo chieu NGANG nen THU TU KHAI quyet dinh cot. Xep lai de Hang muc (du an) nam NGAY DUOI Du an o cot PHAI — hai thu la mot cap (du an -> goi viec cua no), tach 2 cot thi mat phai nhay cheo. === 4. BO 2 DONG CHU THUA (anh: "Bo het may chu thua nay di") === · "Da an ke hoach o trang thai Da duyet — xem du o menu…" (chan tang KHKK): giai-thich CO-CHE loc ben trong; nguoi dung chi can thay dung phieu. · "File can cu ho so / shopdrawing cua ke hoach. Gan vao 1 can cu…" (dau muc 3): nut ⬆ moi dong can cu da tu noi no lam gi (co `title`), khong can cau huong-dan chinh inh tren dau khoi. Verify dist bang NOI DUNG: khung the `border-[#e9eef4]` = 1/1 ca 2 app; 2 chuoi CU = 0/0. Co-Authored-By: Claude Opus 5 --- .../src/components/khkk/KhkkDetailContent.tsx | 30 +++++++++++-------- .../src/components/khkk/KhkkWorkListPanel.tsx | 16 ++++++---- .../components/pipeline/PipelineTreePanel.tsx | 28 ++++++++++++++--- .../src/components/khkk/KhkkDetailContent.tsx | 30 +++++++++++-------- .../src/components/khkk/KhkkWorkListPanel.tsx | 16 ++++++---- .../components/pipeline/PipelineTreePanel.tsx | 28 ++++++++++++++--- 6 files changed, 106 insertions(+), 42 deletions(-) diff --git a/fe-admin/src/components/khkk/KhkkDetailContent.tsx b/fe-admin/src/components/khkk/KhkkDetailContent.tsx index d8dd535..6f9441e 100644 --- a/fe-admin/src/components/khkk/KhkkDetailContent.tsx +++ b/fe-admin/src/components/khkk/KhkkDetailContent.tsx @@ -359,12 +359,16 @@ export function KhkkDetailContent({ // `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây. return ( - // [S175 — anh: "Viền xanh này trong thao tác → Chọn kế hoạch"] GỠ `card-accent`. - // Class này vẽ DẢI ACCENT DỌC 3px bên trái (`index.css:112`); ở màn Thao tác nó nằm sát - // mép panel 2 nên đọc ra thành một vệt xanh đậm chạy dọc màn hình, không ra "thẻ". - // 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`: `.card-accent` nằm NGOÀI `@layer` nên thắng - // utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật ở đợt B với panel duyệt). -
+ // [S175 — anh: "Viền xanh này trong thao tác" → rồi "ý tao bỏ cái viền xanh thôi, còn bộ + // khung table thuần vẫn nên giữ lại"] `.card-accent` (`index.css:112`) gói **HAI thứ**: + // ① KHUNG thẻ — bo góc 0.75rem + viền 1px `#e9eef4` + nền trắng + đổ bóng → anh GIỮ + // ② DẢI ACCENT DỌC 3px bên trái (`border-left`) → anh BỎ + // 🔴 Lượt trước lead gỡ CẢ CLASS ⇒ mất luôn ①, panel trôi không khung. Nay dựng lại ① + // bằng utility thuần, giữ đúng số đo gốc (`#e9eef4`, `rounded-xl`) để không lệch tông với + // các thẻ khác còn dùng `.card-accent`. + // 🔸 Bỏ luôn `hover` nhấc-thẻ của class gốc: hiệu-ứng đó thiết-kế cho KPI-card nhỏ, đặt + // lên panel nội-dung cao cả màn thì nhấp-nháy vô cớ mỗi lần rê chuột. +
{/* Dải 4 giai-đoạn — GĐ2 đang sáng. Cùng component với màn Duyệt NCC (không nhân bản). */} @@ -429,15 +433,19 @@ export function KhkkDetailContent({ · "Thêm hạng mục vào đây" → +ô **Hạng mục** ngay cạnh Dự án · "Đưa lên trên phiếu" → **NCC** + mã phiếu PE dời lên dải header (xem `:361`) */}
+ {/* [S175 — anh: "đưa qua bên phải nhé"] Lưới 2 cột đọc theo chiều NGANG + (`grid-cols-2` xếp trái→phải), nên THỨ TỰ KHAI quyết định cột. + Xếp lại để **Hạng mục (dự án) nằm NGAY DƯỚI Dự án ở cột PHẢI** — hai thứ này + là một cặp (dự án ⟶ gói việc của nó), tách 2 cột thì mắt phải nhảy chéo. */} + {/* [S175 — anh: "hạng mục của Dự án, Duyệt NCC ấy"] Đây là HẠNG MỤC CÔNG VIỆC của dự án, lấy từ phiếu Duyệt NCC nguồn (`PE.WorkItemId`). 🔴 KHÁC hẳn `line.tenHangMuc` — cái đó là danh-mục SP-002 dùng phân loại HỢP ĐỒNG (86 dòng, chọn ở mục 2). Hai thứ trùng tên tiếng Việt nhưng khác trục; lượt trước lead bày nhầm cái SP-002 ở đây nên anh bảo "Bỏ đi". */} -
@@ -701,11 +709,9 @@ export function KhkkDetailContent({ + `:3601-3608`): dòng chữ mô-tả ở TRÊN, nút viền ĐỨT nền brand-nhạt ở DƯỚI danh sách — thay cho nút đặc nằm ở góc header. Nút viền đứt đọc ra là "chỗ thả thêm vào", nút đặc ở header đọc ra là "hành động của cả khối"; đây là cái thứ nhất. */} - {!readOnly && ( -

- File căn cứ hồ sơ / shopdrawing của kế hoạch. Gắn vào 1 căn cứ cụ thể bằng nút ⬆ ở dòng căn cứ đó. -

- )} + {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng mô-tả "File căn cứ hồ sơ / + shopdrawing của kế hoạch…" ĐÃ GỠ. Nút ⬆ ở mỗi dòng căn cứ đã tự nói nó làm gì + (có `title`), không cần một câu hướng-dẫn nằm chình ình trên đầu khối. */} {mergedRows.length === 0 ? (
Chưa có căn cứ hồ sơ hay file đính kèm nào.
) : ( diff --git a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx index 8483836..28a6554 100644 --- a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx +++ b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx @@ -160,10 +160,18 @@ export function KhkkWorkListPanel({ accent="bg-amber-50 text-amber-600" icon={} > + {/* [S175 — anh: "chỗ này chỉ cần NCC đã duyệt là đc rồi nhé, còn lại bỏ hết đi"] + · `approvedPeOnly` ⇒ chỉ phiếu PE phase `DaDuyet`. Màn này để CHỌN PE mà lập + kế hoạch; phiếu chưa duyệt xong thì lập không được, hiện ra là mời bấm nhầm. + · `stagesOnly={[1]}` ⇒ chỉ folder **Duyệt NCC**; 3 giai-đoạn kia (Kế hoạch HĐ + + 8 ngăn nhóm · Duyệt HĐ · HĐ cứng) là nhiễu ở màn này. + 🔴 CẢ HAI đều OPT-IN — 5 host cây còn lại không truyền nên giữ nguyên hành vi. */} @@ -269,11 +277,9 @@ export function KhkkWorkListPanel({ {/* 🔴 KHÔNG in "x/total" nữa: `total` gồm cả phiếu đã duyệt vừa bị lọc ⇒ tỉ số đó sẽ đọc thành "đang cắt bớt" trong khi thật ra là "đang lọc bỏ". Nói đúng việc đang làm, và chỉ nói khi thật sự có cái bị giấu. */} - {plans.data && plans.data.items.length > planItems.length && ( -

- Đã ẩn kế hoạch ở trạng thái “Đã duyệt” — xem đủ ở menu Kế hoạch ký kết HĐ. -

- )} + {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng "Đã ẩn kế hoạch ở trạng + thái Đã duyệt…" ĐÃ GỠ. Nó giải-thích CƠ-CHẾ lọc bên trong; người dùng chỉ cần + thấy đúng phiếu, không cần biết cái gì bị ẩn vì sao. */} )} diff --git a/fe-admin/src/components/pipeline/PipelineTreePanel.tsx b/fe-admin/src/components/pipeline/PipelineTreePanel.tsx index bc0387b..e7176ec 100644 --- a/fe-admin/src/components/pipeline/PipelineTreePanel.tsx +++ b/fe-admin/src/components/pipeline/PipelineTreePanel.tsx @@ -16,7 +16,7 @@ import { cn } from '@/lib/cn' import { usePipelineStages, type BuildStagesOptions } from '@/hooks/usePipelineStages' import { PipelineStageFolders, type PipelineStageNo } from '@/components/pipeline/PipelineStageFolders' import type { Paged } from '@/types/master' -import type { PeListItem } from '@/types/purchaseEvaluation' +import { PurchaseEvaluationPhase, type PeListItem } from '@/types/purchaseEvaluation' const STORAGE_KEY = 'pipeline_tree_expanded_v1' const PE_INDEX_PAGE_SIZE = 200 @@ -90,6 +90,8 @@ export function PipelineTreePanel({ stage2Options, hideHeader, hideSearch, + approvedPeOnly, + stagesOnly, }: { currentStage: PipelineStageNo className?: string @@ -116,6 +118,14 @@ export function PipelineTreePanel({ // dùng tới — người dùng gõ phát nào là cây biến mất phát đó. // 6 host còn lại KHÔNG truyền ⇒ giữ nguyên ô lọc cây như cũ. hideSearch?: boolean + // [S175 — anh: "chỗ này chỉ cần NCC đã duyệt là đc rồi nhé, còn lại bỏ hết đi"] + // Chỉ giữ phiếu Duyệt NCC ở phase DaDuyet. Màn Thao tác dùng cây này để CHỌN PE mà lập + // kế hoạch — phiếu chưa duyệt xong thì lập không được, hiện ra chỉ tổ mời bấm nhầm. + // 🔴 OPT-IN: 5 host còn lại không truyền ⇒ cây giữ NGUYÊN mọi phase như cũ. + approvedPeOnly?: boolean + // [S175] Chỉ vẽ các folder giai-đoạn trong danh sách này (vd `[1]` = chỉ Duyệt NCC). + // Không truyền ⇒ vẽ đủ 4 giai-đoạn như cũ. + stagesOnly?: PipelineStageNo[] }) { const { canPe, buildStages } = usePipelineStages() const [search, setSearch] = useState('') @@ -154,13 +164,18 @@ export function PipelineTreePanel({ const term = search.trim().toLowerCase() const rows = useMemo(() => { - const all = peIndex.data?.items ?? [] + // [S175] Lọc phase TRƯỚC khi dựng cây ⇒ số đếm ở mọi tầng (Năm/Dự án/Hạng mục) tự khớp + // tập đã lọc. Lọc SAU khi dựng thì badge đếm sẽ nói dối. + const base = peIndex.data?.items ?? [] + const all = approvedPeOnly + ? base.filter(p => p.phase === PurchaseEvaluationPhase.DaDuyet) + : base if (!term) return all return all.filter(p => [p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau] .some(v => (v ?? '').toLowerCase().includes(term)), ) - }, [peIndex.data, term]) + }, [peIndex.data, term, approvedPeOnly]) const years = useMemo(() => buildTree(rows), [rows]) const packageCount = years.reduce((n, y) => n + y.projects.reduce((m, p) => m + p.workItems.length, 0), 0) @@ -342,7 +357,12 @@ export function PipelineTreePanel({ className="ml-3 border-l border-slate-200" currentStage={currentStage} hideEmpty={hideEmpty} - stages={buildStages(wg.items, stage2Options ?? {})} + stages={(() => { + const built = buildStages(wg.items, stage2Options ?? {}) + // [S175] `stagesOnly` = lọc HIỂN THỊ, KHÔNG đụng bộ dựng — + // 5 host kia không truyền thì `built` đi thẳng như cũ. + return stagesOnly ? built.filter(st => stagesOnly.includes(st.n)) : built + })()} /> ) diff --git a/fe-user/src/components/khkk/KhkkDetailContent.tsx b/fe-user/src/components/khkk/KhkkDetailContent.tsx index d8dd535..6f9441e 100644 --- a/fe-user/src/components/khkk/KhkkDetailContent.tsx +++ b/fe-user/src/components/khkk/KhkkDetailContent.tsx @@ -359,12 +359,16 @@ export function KhkkDetailContent({ // `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây. return ( - // [S175 — anh: "Viền xanh này trong thao tác → Chọn kế hoạch"] GỠ `card-accent`. - // Class này vẽ DẢI ACCENT DỌC 3px bên trái (`index.css:112`); ở màn Thao tác nó nằm sát - // mép panel 2 nên đọc ra thành một vệt xanh đậm chạy dọc màn hình, không ra "thẻ". - // 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`: `.card-accent` nằm NGOÀI `@layer` nên thắng - // utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật ở đợt B với panel duyệt). -
+ // [S175 — anh: "Viền xanh này trong thao tác" → rồi "ý tao bỏ cái viền xanh thôi, còn bộ + // khung table thuần vẫn nên giữ lại"] `.card-accent` (`index.css:112`) gói **HAI thứ**: + // ① KHUNG thẻ — bo góc 0.75rem + viền 1px `#e9eef4` + nền trắng + đổ bóng → anh GIỮ + // ② DẢI ACCENT DỌC 3px bên trái (`border-left`) → anh BỎ + // 🔴 Lượt trước lead gỡ CẢ CLASS ⇒ mất luôn ①, panel trôi không khung. Nay dựng lại ① + // bằng utility thuần, giữ đúng số đo gốc (`#e9eef4`, `rounded-xl`) để không lệch tông với + // các thẻ khác còn dùng `.card-accent`. + // 🔸 Bỏ luôn `hover` nhấc-thẻ của class gốc: hiệu-ứng đó thiết-kế cho KPI-card nhỏ, đặt + // lên panel nội-dung cao cả màn thì nhấp-nháy vô cớ mỗi lần rê chuột. +
{/* Dải 4 giai-đoạn — GĐ2 đang sáng. Cùng component với màn Duyệt NCC (không nhân bản). */} @@ -429,15 +433,19 @@ export function KhkkDetailContent({ · "Thêm hạng mục vào đây" → +ô **Hạng mục** ngay cạnh Dự án · "Đưa lên trên phiếu" → **NCC** + mã phiếu PE dời lên dải header (xem `:361`) */}
+ {/* [S175 — anh: "đưa qua bên phải nhé"] Lưới 2 cột đọc theo chiều NGANG + (`grid-cols-2` xếp trái→phải), nên THỨ TỰ KHAI quyết định cột. + Xếp lại để **Hạng mục (dự án) nằm NGAY DƯỚI Dự án ở cột PHẢI** — hai thứ này + là một cặp (dự án ⟶ gói việc của nó), tách 2 cột thì mắt phải nhảy chéo. */} + {/* [S175 — anh: "hạng mục của Dự án, Duyệt NCC ấy"] Đây là HẠNG MỤC CÔNG VIỆC của dự án, lấy từ phiếu Duyệt NCC nguồn (`PE.WorkItemId`). 🔴 KHÁC hẳn `line.tenHangMuc` — cái đó là danh-mục SP-002 dùng phân loại HỢP ĐỒNG (86 dòng, chọn ở mục 2). Hai thứ trùng tên tiếng Việt nhưng khác trục; lượt trước lead bày nhầm cái SP-002 ở đây nên anh bảo "Bỏ đi". */} -
@@ -701,11 +709,9 @@ export function KhkkDetailContent({ + `:3601-3608`): dòng chữ mô-tả ở TRÊN, nút viền ĐỨT nền brand-nhạt ở DƯỚI danh sách — thay cho nút đặc nằm ở góc header. Nút viền đứt đọc ra là "chỗ thả thêm vào", nút đặc ở header đọc ra là "hành động của cả khối"; đây là cái thứ nhất. */} - {!readOnly && ( -

- File căn cứ hồ sơ / shopdrawing của kế hoạch. Gắn vào 1 căn cứ cụ thể bằng nút ⬆ ở dòng căn cứ đó. -

- )} + {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng mô-tả "File căn cứ hồ sơ / + shopdrawing của kế hoạch…" ĐÃ GỠ. Nút ⬆ ở mỗi dòng căn cứ đã tự nói nó làm gì + (có `title`), không cần một câu hướng-dẫn nằm chình ình trên đầu khối. */} {mergedRows.length === 0 ? (
Chưa có căn cứ hồ sơ hay file đính kèm nào.
) : ( diff --git a/fe-user/src/components/khkk/KhkkWorkListPanel.tsx b/fe-user/src/components/khkk/KhkkWorkListPanel.tsx index 8483836..28a6554 100644 --- a/fe-user/src/components/khkk/KhkkWorkListPanel.tsx +++ b/fe-user/src/components/khkk/KhkkWorkListPanel.tsx @@ -160,10 +160,18 @@ export function KhkkWorkListPanel({ accent="bg-amber-50 text-amber-600" icon={} > + {/* [S175 — anh: "chỗ này chỉ cần NCC đã duyệt là đc rồi nhé, còn lại bỏ hết đi"] + · `approvedPeOnly` ⇒ chỉ phiếu PE phase `DaDuyet`. Màn này để CHỌN PE mà lập + kế hoạch; phiếu chưa duyệt xong thì lập không được, hiện ra là mời bấm nhầm. + · `stagesOnly={[1]}` ⇒ chỉ folder **Duyệt NCC**; 3 giai-đoạn kia (Kế hoạch HĐ + + 8 ngăn nhóm · Duyệt HĐ · HĐ cứng) là nhiễu ở màn này. + 🔴 CẢ HAI đều OPT-IN — 5 host cây còn lại không truyền nên giữ nguyên hành vi. */} @@ -269,11 +277,9 @@ export function KhkkWorkListPanel({ {/* 🔴 KHÔNG in "x/total" nữa: `total` gồm cả phiếu đã duyệt vừa bị lọc ⇒ tỉ số đó sẽ đọc thành "đang cắt bớt" trong khi thật ra là "đang lọc bỏ". Nói đúng việc đang làm, và chỉ nói khi thật sự có cái bị giấu. */} - {plans.data && plans.data.items.length > planItems.length && ( -

- Đã ẩn kế hoạch ở trạng thái “Đã duyệt” — xem đủ ở menu Kế hoạch ký kết HĐ. -

- )} + {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng "Đã ẩn kế hoạch ở trạng + thái Đã duyệt…" ĐÃ GỠ. Nó giải-thích CƠ-CHẾ lọc bên trong; người dùng chỉ cần + thấy đúng phiếu, không cần biết cái gì bị ẩn vì sao. */} )} diff --git a/fe-user/src/components/pipeline/PipelineTreePanel.tsx b/fe-user/src/components/pipeline/PipelineTreePanel.tsx index bc0387b..e7176ec 100644 --- a/fe-user/src/components/pipeline/PipelineTreePanel.tsx +++ b/fe-user/src/components/pipeline/PipelineTreePanel.tsx @@ -16,7 +16,7 @@ import { cn } from '@/lib/cn' import { usePipelineStages, type BuildStagesOptions } from '@/hooks/usePipelineStages' import { PipelineStageFolders, type PipelineStageNo } from '@/components/pipeline/PipelineStageFolders' import type { Paged } from '@/types/master' -import type { PeListItem } from '@/types/purchaseEvaluation' +import { PurchaseEvaluationPhase, type PeListItem } from '@/types/purchaseEvaluation' const STORAGE_KEY = 'pipeline_tree_expanded_v1' const PE_INDEX_PAGE_SIZE = 200 @@ -90,6 +90,8 @@ export function PipelineTreePanel({ stage2Options, hideHeader, hideSearch, + approvedPeOnly, + stagesOnly, }: { currentStage: PipelineStageNo className?: string @@ -116,6 +118,14 @@ export function PipelineTreePanel({ // dùng tới — người dùng gõ phát nào là cây biến mất phát đó. // 6 host còn lại KHÔNG truyền ⇒ giữ nguyên ô lọc cây như cũ. hideSearch?: boolean + // [S175 — anh: "chỗ này chỉ cần NCC đã duyệt là đc rồi nhé, còn lại bỏ hết đi"] + // Chỉ giữ phiếu Duyệt NCC ở phase DaDuyet. Màn Thao tác dùng cây này để CHỌN PE mà lập + // kế hoạch — phiếu chưa duyệt xong thì lập không được, hiện ra chỉ tổ mời bấm nhầm. + // 🔴 OPT-IN: 5 host còn lại không truyền ⇒ cây giữ NGUYÊN mọi phase như cũ. + approvedPeOnly?: boolean + // [S175] Chỉ vẽ các folder giai-đoạn trong danh sách này (vd `[1]` = chỉ Duyệt NCC). + // Không truyền ⇒ vẽ đủ 4 giai-đoạn như cũ. + stagesOnly?: PipelineStageNo[] }) { const { canPe, buildStages } = usePipelineStages() const [search, setSearch] = useState('') @@ -154,13 +164,18 @@ export function PipelineTreePanel({ const term = search.trim().toLowerCase() const rows = useMemo(() => { - const all = peIndex.data?.items ?? [] + // [S175] Lọc phase TRƯỚC khi dựng cây ⇒ số đếm ở mọi tầng (Năm/Dự án/Hạng mục) tự khớp + // tập đã lọc. Lọc SAU khi dựng thì badge đếm sẽ nói dối. + const base = peIndex.data?.items ?? [] + const all = approvedPeOnly + ? base.filter(p => p.phase === PurchaseEvaluationPhase.DaDuyet) + : base if (!term) return all return all.filter(p => [p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau] .some(v => (v ?? '').toLowerCase().includes(term)), ) - }, [peIndex.data, term]) + }, [peIndex.data, term, approvedPeOnly]) const years = useMemo(() => buildTree(rows), [rows]) const packageCount = years.reduce((n, y) => n + y.projects.reduce((m, p) => m + p.workItems.length, 0), 0) @@ -342,7 +357,12 @@ export function PipelineTreePanel({ className="ml-3 border-l border-slate-200" currentStage={currentStage} hideEmpty={hideEmpty} - stages={buildStages(wg.items, stage2Options ?? {})} + stages={(() => { + const built = buildStages(wg.items, stage2Options ?? {}) + // [S175] `stagesOnly` = lọc HIỂN THỊ, KHÔNG đụng bộ dựng — + // 5 host kia không truyền thì `built` đi thẳng như cũ. + return stagesOnly ? built.filter(st => stagesOnly.includes(st.n)) : built + })()} /> )