[CLAUDE] Contract: KHKK UAT vong-3 — khung the, cay chi PE da duyet, don chu thua (0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m29s

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 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-05 22:26:45 +07:00
parent 4f99961247
commit 79936ad4d3
6 changed files with 106 additions and 42 deletions

View File

@ -359,12 +359,16 @@ export function KhkkDetailContent({
// `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây. // `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây.
return ( return (
// [S175 — anh: "Viền xanh này trong thao tác → Chọn kế hoạch"] GỠ `card-accent`. // [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ộ
// 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 // khung table thuần vẫn nên giữ lại"] `.card-accent` (`index.css:112`) gói **HAI thứ**:
// 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ẻ". // ① KHUNG thẻ — bo góc 0.75rem + viền 1px `#e9eef4` + nền trắng + đổ bóng → anh GIỮ
// 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`: `.card-accent` nằm NGOÀI `@layer` nên thắng // ② DẢI ACCENT DỌC 3px bên trái (`border-left`) → anh BỎ
// utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật ở đợt B với panel duyệt). // 🔴 Lượt trước lead gỡ CẢ CLASS ⇒ mất luôn ①, panel trôi không khung. Nay dựng lại ①
<div className="overflow-hidden"> // 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.
<div className="overflow-hidden rounded-xl border border-[#e9eef4] bg-white shadow-sm">
{/* 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). */} {/* 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). */}
<PePipelineStrip current={2} /> <PePipelineStrip current={2} />
@ -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 · "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`) */} · "Đưa lên trên phiếu" → **NCC** + mã phiếu PE dời lên dải header (xem `:361`) */}
<div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2">
{/* [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. */}
<Field label="Mã kế hoạch" value={plan.maKeHoach ?? '—'} mono /> <Field label="Mã kế hoạch" value={plan.maKeHoach ?? '—'} mono />
<Field label="Dự án" value={plan.projectName ?? '—'} /> <Field label="Dự án" value={plan.projectName ?? '—'} />
<Field label="Người soạn" value={plan.drafterFullName ?? '—'} />
{/* [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 {/* [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`). 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 🔴 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; ĐỒ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". */} lượt trước lead bày nhầm cái SP-002 ở đây nên anh bảo "Bỏ đi". */}
<Field label="Hạng mục (dự án)" value={plan.peWorkItemName ?? '—'} /> <Field label="Hạng mục (dự án)" value={plan.peWorkItemName ?? '—'} />
<Field label="Người soạn" value={plan.drafterFullName ?? '—'} />
<Field label="Ngày tạo" value={formatDateTime(plan.createdAt)} /> <Field label="Ngày tạo" value={formatDateTime(plan.createdAt)} />
</div> </div>
@ -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 + `: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 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. */} 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 && ( {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng mô-tả "File căn cứ hồ sơ /
<p className="mb-2 text-[12px] text-slate-500"> shopdrawing của kế hoạch…" ĐÃ GỠ. Nút ⬆ ở mỗi dòng căn cứ đã tự nói nó làm gì
File căn cứ hồ / 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ứ đó. (có `title`), không cần một câu hướng-dẫn nằm chình ình trên đầu khối. */}
</p>
)}
{mergedRows.length === 0 ? ( {mergedRows.length === 0 ? (
<div className="text-sm italic text-slate-400">Chưa căn cứ hồ hay file đính kèm nào.</div> <div className="text-sm italic text-slate-400">Chưa căn cứ hồ hay file đính kèm nào.</div>
) : ( ) : (

View File

@ -160,10 +160,18 @@ export function KhkkWorkListPanel({
accent="bg-amber-50 text-amber-600" accent="bg-amber-50 text-amber-600"
icon={<FolderTree className="h-3 w-3" />} icon={<FolderTree className="h-3 w-3" />}
> >
{/* [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. */}
<PipelineTreePanel <PipelineTreePanel
currentStage={2} currentStage={2}
hideHeader hideHeader
hideSearch hideSearch
approvedPeOnly
stagesOnly={[1]}
bodyMaxHClass="max-h-none" bodyMaxHClass="max-h-none"
stage2Options={{ onOpenPe: onPickPe, onOpenKhkk: onPickPlan }} stage2Options={{ onOpenPe: onPickPe, onOpenKhkk: onPickPlan }}
/> />
@ -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ố đó {/* 🔴 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 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. */} đ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 && ( {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng "Đã ẩn kế hoạch ở trạng
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-500"> thái Đã duyệt…" ĐÃ GỠ. Nó giải-thích CƠ-CHẾ lọc bên trong; người dùng chỉ cần
Đã n kế hoạch trạng thái Đã duyệt xem đ menu Kế hoạch kết . thấy đúng phiếu, không cần biết cái gì bị ẩn vì sao. */}
</p>
)}
</Tier> </Tier>
</> </>
)} )}

View File

@ -16,7 +16,7 @@ import { cn } from '@/lib/cn'
import { usePipelineStages, type BuildStagesOptions } from '@/hooks/usePipelineStages' import { usePipelineStages, type BuildStagesOptions } from '@/hooks/usePipelineStages'
import { PipelineStageFolders, type PipelineStageNo } from '@/components/pipeline/PipelineStageFolders' import { PipelineStageFolders, type PipelineStageNo } from '@/components/pipeline/PipelineStageFolders'
import type { Paged } from '@/types/master' 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 STORAGE_KEY = 'pipeline_tree_expanded_v1'
const PE_INDEX_PAGE_SIZE = 200 const PE_INDEX_PAGE_SIZE = 200
@ -90,6 +90,8 @@ export function PipelineTreePanel({
stage2Options, stage2Options,
hideHeader, hideHeader,
hideSearch, hideSearch,
approvedPeOnly,
stagesOnly,
}: { }: {
currentStage: PipelineStageNo currentStage: PipelineStageNo
className?: string 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 đó. // 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ũ. // 6 host còn lại KHÔNG truyền ⇒ giữ nguyên ô lọc cây như cũ.
hideSearch?: boolean 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 { canPe, buildStages } = usePipelineStages()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
@ -154,13 +164,18 @@ export function PipelineTreePanel({
const term = search.trim().toLowerCase() const term = search.trim().toLowerCase()
const rows = useMemo(() => { 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 if (!term) return all
return all.filter(p => return all.filter(p =>
[p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau] [p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau]
.some(v => (v ?? '').toLowerCase().includes(term)), .some(v => (v ?? '').toLowerCase().includes(term)),
) )
}, [peIndex.data, term]) }, [peIndex.data, term, approvedPeOnly])
const years = useMemo(() => buildTree(rows), [rows]) const years = useMemo(() => buildTree(rows), [rows])
const packageCount = years.reduce((n, y) => n + y.projects.reduce((m, p) => m + p.workItems.length, 0), 0) 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" className="ml-3 border-l border-slate-200"
currentStage={currentStage} currentStage={currentStage}
hideEmpty={hideEmpty} 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
})()}
/> />
</details> </details>
) )

View File

@ -359,12 +359,16 @@ export function KhkkDetailContent({
// `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây. // `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây.
return ( return (
// [S175 — anh: "Viền xanh này trong thao tác → Chọn kế hoạch"] GỠ `card-accent`. // [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ộ
// 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 // khung table thuần vẫn nên giữ lại"] `.card-accent` (`index.css:112`) gói **HAI thứ**:
// 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ẻ". // ① KHUNG thẻ — bo góc 0.75rem + viền 1px `#e9eef4` + nền trắng + đổ bóng → anh GIỮ
// 🔴 GỠ HẲN CLASS, KHÔNG đè `border-0`: `.card-accent` nằm NGOÀI `@layer` nên thắng // ② DẢI ACCENT DỌC 3px bên trái (`border-left`) → anh BỎ
// utility Tailwind v4 — đè là vô hiệu (gotcha #66, đã cắn thật ở đợt B với panel duyệt). // 🔴 Lượt trước lead gỡ CẢ CLASS ⇒ mất luôn ①, panel trôi không khung. Nay dựng lại ①
<div className="overflow-hidden"> // 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.
<div className="overflow-hidden rounded-xl border border-[#e9eef4] bg-white shadow-sm">
{/* 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). */} {/* 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). */}
<PePipelineStrip current={2} /> <PePipelineStrip current={2} />
@ -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 · "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`) */} · "Đưa lên trên phiếu" → **NCC** + mã phiếu PE dời lên dải header (xem `:361`) */}
<div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2">
{/* [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. */}
<Field label="Mã kế hoạch" value={plan.maKeHoach ?? '—'} mono /> <Field label="Mã kế hoạch" value={plan.maKeHoach ?? '—'} mono />
<Field label="Dự án" value={plan.projectName ?? '—'} /> <Field label="Dự án" value={plan.projectName ?? '—'} />
<Field label="Người soạn" value={plan.drafterFullName ?? '—'} />
{/* [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 {/* [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`). 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 🔴 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; ĐỒ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". */} lượt trước lead bày nhầm cái SP-002 ở đây nên anh bảo "Bỏ đi". */}
<Field label="Hạng mục (dự án)" value={plan.peWorkItemName ?? '—'} /> <Field label="Hạng mục (dự án)" value={plan.peWorkItemName ?? '—'} />
<Field label="Người soạn" value={plan.drafterFullName ?? '—'} />
<Field label="Ngày tạo" value={formatDateTime(plan.createdAt)} /> <Field label="Ngày tạo" value={formatDateTime(plan.createdAt)} />
</div> </div>
@ -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 + `: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 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. */} 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 && ( {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng mô-tả "File căn cứ hồ sơ /
<p className="mb-2 text-[12px] text-slate-500"> shopdrawing của kế hoạch…" ĐÃ GỠ. Nút ⬆ ở mỗi dòng căn cứ đã tự nói nó làm gì
File căn cứ hồ / 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ứ đó. (có `title`), không cần một câu hướng-dẫn nằm chình ình trên đầu khối. */}
</p>
)}
{mergedRows.length === 0 ? ( {mergedRows.length === 0 ? (
<div className="text-sm italic text-slate-400">Chưa căn cứ hồ hay file đính kèm nào.</div> <div className="text-sm italic text-slate-400">Chưa căn cứ hồ hay file đính kèm nào.</div>
) : ( ) : (

View File

@ -160,10 +160,18 @@ export function KhkkWorkListPanel({
accent="bg-amber-50 text-amber-600" accent="bg-amber-50 text-amber-600"
icon={<FolderTree className="h-3 w-3" />} icon={<FolderTree className="h-3 w-3" />}
> >
{/* [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. */}
<PipelineTreePanel <PipelineTreePanel
currentStage={2} currentStage={2}
hideHeader hideHeader
hideSearch hideSearch
approvedPeOnly
stagesOnly={[1]}
bodyMaxHClass="max-h-none" bodyMaxHClass="max-h-none"
stage2Options={{ onOpenPe: onPickPe, onOpenKhkk: onPickPlan }} stage2Options={{ onOpenPe: onPickPe, onOpenKhkk: onPickPlan }}
/> />
@ -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ố đó {/* 🔴 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 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. */} đ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 && ( {/* 🧊 [S175 — anh: "Bỏ hết mấy chữ thừa này đi"] Dòng "Đã ẩn kế hoạch ở trạng
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-500"> thái Đã duyệt…" ĐÃ GỠ. Nó giải-thích CƠ-CHẾ lọc bên trong; người dùng chỉ cần
Đã n kế hoạch trạng thái Đã duyệt xem đ menu Kế hoạch kết . thấy đúng phiếu, không cần biết cái gì bị ẩn vì sao. */}
</p>
)}
</Tier> </Tier>
</> </>
)} )}

View File

@ -16,7 +16,7 @@ import { cn } from '@/lib/cn'
import { usePipelineStages, type BuildStagesOptions } from '@/hooks/usePipelineStages' import { usePipelineStages, type BuildStagesOptions } from '@/hooks/usePipelineStages'
import { PipelineStageFolders, type PipelineStageNo } from '@/components/pipeline/PipelineStageFolders' import { PipelineStageFolders, type PipelineStageNo } from '@/components/pipeline/PipelineStageFolders'
import type { Paged } from '@/types/master' 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 STORAGE_KEY = 'pipeline_tree_expanded_v1'
const PE_INDEX_PAGE_SIZE = 200 const PE_INDEX_PAGE_SIZE = 200
@ -90,6 +90,8 @@ export function PipelineTreePanel({
stage2Options, stage2Options,
hideHeader, hideHeader,
hideSearch, hideSearch,
approvedPeOnly,
stagesOnly,
}: { }: {
currentStage: PipelineStageNo currentStage: PipelineStageNo
className?: string 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 đó. // 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ũ. // 6 host còn lại KHÔNG truyền ⇒ giữ nguyên ô lọc cây như cũ.
hideSearch?: boolean 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 { canPe, buildStages } = usePipelineStages()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
@ -154,13 +164,18 @@ export function PipelineTreePanel({
const term = search.trim().toLowerCase() const term = search.trim().toLowerCase()
const rows = useMemo(() => { 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 if (!term) return all
return all.filter(p => return all.filter(p =>
[p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau] [p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau]
.some(v => (v ?? '').toLowerCase().includes(term)), .some(v => (v ?? '').toLowerCase().includes(term)),
) )
}, [peIndex.data, term]) }, [peIndex.data, term, approvedPeOnly])
const years = useMemo(() => buildTree(rows), [rows]) const years = useMemo(() => buildTree(rows), [rows])
const packageCount = years.reduce((n, y) => n + y.projects.reduce((m, p) => m + p.workItems.length, 0), 0) 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" className="ml-3 border-l border-slate-200"
currentStage={currentStage} currentStage={currentStage}
hideEmpty={hideEmpty} 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
})()}
/> />
</details> </details>
) )