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 ( -
-
-
Tổng giá đề xuất
-
- {formatVnd(sumProposed)} -
-
{plan.lines.length} dòng
-
-
-
Tổng giá duyệt
-
- {approvedLines.length === 0 ? '—' : formatVnd(sumApproved)} -
-
- {pending === 0 - ? 'đã chốt đủ mọi dòng' - : `còn ${pending}/${plan.lines.length} dòng chưa chốt giá`} -
-
-
- ) - })()} + {/* 🧊 [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ứ đó. +

+ )} {mergedRows.length === 0 ? (
Chưa có căn cứ hồ sơ hay file đính kèm nào.
) : ( @@ -854,6 +849,27 @@ export function KhkkDetailContent({ ))}
)} + {/* 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. */} -

- - {flatMode ? : } - - - {deletedView ? 'Phiếu đã xóa' : flatMode ? 'Danh sách kế hoạch' : 'Cây toàn trình'} - - {flatMode && !list.isLoading && !list.isError && ( - - {total} - - )} -
- + {/* 🧊 [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. */} +
- {[...byWorkItem.entries()].map(([workItemName, items]) => ( + + {!pCollapsed && [...byWorkItem.entries()].map(([workItemName, items]) => { + const wKey = `${projectName}${workItemName}` + const wCollapsed = collapsed.has(wKey) + return (
-
+
+ + {!wCollapsed && (
    {items.map((pe) => (
  • @@ -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. */}
{!deletedView && ( - )} - -
+ 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ứ đó. +

+ )} {mergedRows.length === 0 ? (
Chưa có căn cứ hồ sơ hay file đính kèm nào.
) : ( @@ -854,6 +849,27 @@ export function KhkkDetailContent({ ))}
)} + {/* 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. */} -

- - {flatMode ? : } - - - {deletedView ? 'Phiếu đã xóa' : flatMode ? 'Danh sách kế hoạch' : 'Cây toàn trình'} - - {flatMode && !list.isLoading && !list.isError && ( - - {total} - - )} -
- + {/* 🧊 [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. */} +
- {[...byWorkItem.entries()].map(([workItemName, items]) => ( + + {!pCollapsed && [...byWorkItem.entries()].map(([workItemName, items]) => { + const wKey = `${projectName}${workItemName}` + const wCollapsed = collapsed.has(wKey) + return (
-
+
+ + {!wCollapsed && (
    {items.map((pe) => (
  • @@ -239,10 +273,13 @@ export function KhkkWorkListPanel({
  • ))}
+ )}
- ))} + ) + })}
- ))} + ) + })}
diff --git a/fe-user/src/pages/khkk/KhkkListPage.tsx b/fe-user/src/pages/khkk/KhkkListPage.tsx index 7b28f4b..1f5be3d 100644 --- a/fe-user/src/pages/khkk/KhkkListPage.tsx +++ b/fe-user/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. */}
{!deletedView && (