diff --git a/fe-admin/src/components/khkk/KhkkDetailContent.tsx b/fe-admin/src/components/khkk/KhkkDetailContent.tsx
index b1cdcbc..a674f0a 100644
--- a/fe-admin/src/components/khkk/KhkkDetailContent.tsx
+++ b/fe-admin/src/components/khkk/KhkkDetailContent.tsx
@@ -361,6 +361,28 @@ export function KhkkDetailContent({
{plan.maKeHoach ?? '(chưa cấp mã)'}{plan.peTenGoiThau ?? '—'}
+ {/* [S175 — anh: "Đưa lên trên phiếu — NCC"] Mã phiếu PE nguồn + NCC trúng thầu dời từ
+ mục 1 lên ĐÂY. Đây là 2 thứ người xem cần biết ngay khi mở phiếu, không phải cuộn
+ xuống bảng mới thấy. NCC lấy DUY-NHẤT từ `lines` (liên danh N NCC ⇒ liệt kê hết).
+ 🔴 CÒN NỢ: "số tiền CCM và PRO duyệt" anh yêu cầu CHƯA đưa lên được — `KhkkDetailDto`
+ hiện 0 trường ngân sách (đo: `grep -c ccm khkk.ts` = 0); cần BE trả thêm. */}
+ {plan.peMaPhieu && (
+
+ {plan.peMaPhieu}
+
+ )}
+ {[...new Set(plan.lines.map(l => l.supplierName).filter(Boolean))].map(n => (
+
+ {n}
+
+ ))}
{plan.approvalGroup != null && (
+ {/* [S175 — anh chốt trên ảnh khoanh đỏ] Mục 1 RÚT GỌN còn 4 ô:
+ · "Bỏ hết đi" → GỠ **Phòng ban · Quy trình · Hạn xử lý**
+ 🔸 Quy trình vẫn xem được ở PANEL 3 (`KhkkWorkflowPanel` đầu khối) — gỡ ở đây
+ là bỏ chỗ vẽ TRÙNG, không mất thông tin.
+ · "Bỏ đi" → GỠ **Link hồ sơ · Ghi chú** khỏi ô xem (form Sửa vẫn còn, xem dưới)
+ · "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`) */}
-
- {plan.peMaPhieu ?? '—'}
-
-
-
-
- {plan.workflowCode ? (
-
- {plan.workflowCode}
- {plan.workflowVersion != null && v{plan.workflowVersion}}
- {plan.workflowName ? ` · ${plan.workflowName}` : ''}
-
- ) : (
- — Chưa chọn —
- )}
+ {/* 1 phiếu nay được N hạng mục (S175) ⇒ liệt kê DUY-NHẤT, giữ thứ tự dòng.
+ Dòng chưa gán KHÔNG bịa tên — đếm riêng để người soạn biết còn nợ mấy dòng,
+ vì submit-guard sẽ chặn trình duyệt khi còn dòng chưa gán. */}
+
+ {(() => {
+ const names = [...new Set(plan.lines.map(l => l.tenHangMuc).filter(Boolean))] as string[]
+ const unassigned = plan.lines.filter(l => !l.catalogEntryId).length
+ if (names.length === 0 && unassigned === 0) return DASH
+ return (
+
+ {names.map(n => (
+
+ {n}
+
+ ))}
+ {unassigned > 0 && (
+
+ còn {unassigned} dòng chưa gán
+
+ )}
+
+ )
+ })()}
+
-
{editHeader ? (
@@ -468,49 +505,9 @@ export function KhkkDetailContent({
Phiếu nguồn chưa có NCC trúng thầu.
) : (
<>
- {/* [S175 · O3 — ANH CHỐT] TỔNG đặt PHÍA TRÊN bảng, đúng 2 con số:
- ① trục = CHỈ TỔNG TOÀN PHIẾU (anh bác phương án tổng-mỗi-NCC);
- ② hiện 2 TỔNG RIÊNG cạnh nhau — Σ giá ĐỀ XUẤT ⟂ Σ giá DUYỆT.
- 🔴 KHÔNG cộng gộp 2 cột thành một số "hiện hành": đề-xuất và duyệt là hai
- con số có ý nghĩa pháp-lý khác nhau (một cái chưa ai chốt, một cái đã qua
- choke-point `ApplyApprovedValuesOnFinalize`); trộn lại là bịa ra một số
- không tồn tại trong hồ-sơ nào.
- 🔸 Dòng chưa có giá duyệt ⇒ `null`, `moneyOrDash` in em-dash mờ; tổng duyệt
- chỉ cộng phần ĐÃ có, và nói rõ còn bao nhiêu dòng chưa chốt. */}
- {(() => {
- const sumProposed = plan.lines.reduce((s, l) => s + (l.proposedAmount ?? 0), 0)
- const approvedLines = plan.lines.filter(l => l.approvedAmount != null)
- const sumApproved = approvedLines.reduce((s, l) => s + (l.approvedAmount ?? 0), 0)
- const pending = plan.lines.length - approvedLines.length
- return (
-
- )
- })()}
+ {/* 🧊 [S175] 2 ô TỔNG (Σ đề xuất ⟂ Σ duyệt) đã GỠ — anh xem trên màn rồi chốt "Bỏ đi".
+ Trước đó chính anh chọn "hiện 2 tổng riêng" khi em hỏi; nhìn thật thì thừa. Giữ 1 dòng
+ vết để lần sau khỏi ai dựng lại tưởng là thiếu. */}
{/* [S175 · B2 · P0] `table-fixed` + `
` + `min-w-[880px]`: bảng co giãn
tự-do trước đây để cột tiền nhảy bề rộng theo độ dài tên NCC ⇒ 3 cột số không
thẳng hàng giữa các phiếu. Chốt bề rộng rồi thì hẹp màn hình sẽ CUỘN NGANG
@@ -729,11 +726,9 @@ export function KhkkDetailContent({
)}
>
)}
-
- Giá tham chiếu là SNAPSHOT tổng báo giá được chọn của NCC trên phiếu Duyệt NCC tại thời điểm lập kế hoạch.
- {' '}Cột Hạng mục gán ngay tại bảng khi phiếu ở Nháp/Trả lại — mọi dòng phải gán xong mới trình duyệt được.
- {' '}🔴 Dòng THÊM TAY không có giá tham chiếu (hiện “—”) — số tham chiếu là của cả NCC, đã nằm ở dòng gốc.
-
+ {/* 🧊 [S175] 3 dòng chú-thích dưới bảng ĐÃ GỠ — anh: "Bỏ mấy dòng này đi".
+ Nội dung không mất: ràng-buộc "phải gán hạng mục mới trình duyệt được" vẫn do
+ submit-guard BE chặn thật, và ô Hạng mục ở mục 1 đã đếm "còn N dòng chưa gán". */}
{/* ─────── 3 — GỘP "Căn cứ hồ sơ" + "File đính kèm" (anh chốt S175) ─────── */}
@@ -752,22 +747,22 @@ export function KhkkDetailContent({
chipFg="var(--color-teal-700)"
count={mergedRows.length}
actions={
- readOnly ? undefined : (
-
- {isDraft && (
-
- )}
-
-
+ readOnly || !isDraft ? undefined : (
+
)
}
>
+ {/* [S175 — anh chốt] Chỗ tải file bê ĐÚNG khuôn Duyệt NCC (`PeDetailTabs.tsx:3527-3531`
+ + `: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ứ đó.
+
)}
+ {/* Nút tải lên — viền ĐỨT, đặt DƯỚI danh sách, y khuôn PE `:3601-3608`.
+ `accept` bê nguyên danh-sách của PE: chặn ngay ở hộp chọn file đỡ được vòng
+ upload-rồi-mới-biết-sai-định-dạng. */}
+ {!readOnly && (
+
+ )}
{previewAtt && (
{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}
))}
- {flatMode && !deletedView && canPe && (
-
- Đang lọc — danh sách lấy thẳng từ máy chủ (không giới hạn 200 phiếu của cây).
-
- )}
+ {/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng
+ này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200
+ phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */}
{!canPe && (
Bạn chưa có quyền xem phiếu Duyệt NCC nên cây toàn trình không dựng được — danh
@@ -298,20 +296,8 @@ export function KhkkListPanel({
không số đếm ⇒ cột "cụt đầu": lệch cả với màn Thao tác (`KhkkWorkListPanel` có
`Tier` header) LẪN với chính chế-độ phẳng (có dòng đếm ở chân). Mirror đúng khuôn
`Tier` để 2 màn KHKK cùng một hình. */}
-
-
+ {/* 🧊 [S175] Dải "ĐẦU CỘT" (thêm ở đợt B để chữa "cột cụt đầu") ĐÃ GỠ — anh khoanh đỏ
+ cùng dòng "Đang lọc" và bảo bỏ. Tiêu-đề cột đã có ở header trang ngay trên. */}
{!flatMode && (
void
}) {
+ // [S175 — anh: "Cho hiển thị theo cây nằm dự án hạng mục"] Tập KHOÁ đang GẬP.
+ // Dùng tập-gập (không phải tập-mở) để mặc-định là MỞ HẾT: dự án/hạng mục mới xuất hiện sau
+ // này tự mở, không bị ẩn im lặng vì chưa ai bấm mở bao giờ.
+ const [collapsed, setCollapsed] = useState>(new Set())
const { can } = usePermission()
// 🔴 ĐÚNG key policy của endpoint — KHÔNG dùng `Khkk_*` (đó là key MENU, tầng hiển thị).
const canKhkk = can('KeHoachKyKet', 'Read')
@@ -193,23 +198,52 @@ export function KhkkWorkListPanel({
{/* [S175 · O8] CẤP 1 = Dự án (sticky, `z-20`) · CẤP 2 = Hạng mục (sticky dưới nó,
`z-10` + thụt lề + vạch trái). 🔴 Hai `z` khác nhau là BẮT BUỘC: cùng `z` thì
header hạng mục trượt ĐÈ lên header dự án khi cuộn, mất mất cấp cha. */}
- {[...byProject.entries()].map(([projectName, byWorkItem]) => (
+ {[...byProject.entries()].map(([projectName, byWorkItem]) => {
+ const pCollapsed = collapsed.has(projectName)
+ return (
-
+ {/* [S175 — anh: "Cho hiển thị theo cây nằm dự án hạng mục"] Header CẤP 1 nay là
+ NÚT gập/mở. Mặc-định MỞ: gập sẵn thì mở panel ra thấy toàn thư-mục đóng,
+ người dùng tưởng chưa có việc nào. */}
+
@@ -239,10 +273,13 @@ export function KhkkWorkListPanel({
))}
+ )}
- ))}
+ )
+ })}
- ))}
+ )
+ })}
diff --git a/fe-admin/src/pages/khkk/KhkkListPage.tsx b/fe-admin/src/pages/khkk/KhkkListPage.tsx
index 7b28f4b..1f5be3d 100644
--- a/fe-admin/src/pages/khkk/KhkkListPage.tsx
+++ b/fe-admin/src/pages/khkk/KhkkListPage.tsx
@@ -41,7 +41,6 @@ import { useAuth } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
import {
KhkkPhase,
- khkkGroupMenuLabel,
type KhkkDetailDto,
type KhkkPhaseValue,
} from '@/types/khkk'
@@ -159,9 +158,9 @@ export function KhkkListPage() {
{headerTitle}
-
- Giai đoạn 2{group !== null ? ` · ${khkkGroupMenuLabel(group)}` : ''}
-
+ {/* 🧊 [S175] Chip "Giai đoạn 2 · " ĐÃ GỠ — anh: "Bỏ mấy dòng này đi".
+ Tên nhóm đã có trong `headerTitle` ngay bên trái (dạng ngắn "— N2"), và dải 4 giai
+ đoạn ở panel giữa đã chỉ rõ đang đứng ở GĐ2 ⇒ chip này là lần kể thứ ba. */}