[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.
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).
<div className="overflow-hidden">
// [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.
<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). */}
<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
· "Đư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">
{/* [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="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
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". */}
<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)} />
</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
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 && (
<p className="mb-2 text-[12px] text-slate-500">
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ứ đó.
</p>
)}
{/* 🧊 [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 ? (
<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"
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
currentStage={2}
hideHeader
hideSearch
approvedPeOnly
stagesOnly={[1]}
bodyMaxHClass="max-h-none"
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ố đó
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 && (
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-500">
Đã n kế hoạch trạng thái Đã duyệt xem đ menu Kế hoạch kết .
</p>
)}
{/* 🧊 [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. */}
</Tier>
</>
)}

View File

@ -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
})()}
/>
</details>
)