diff --git a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx index 3379f83..8483836 100644 --- a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx +++ b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx @@ -24,9 +24,9 @@ // con). Người chỉ có quyền PE mà không có KHKK sẽ ăn 403 rải khắp cột. ⇒ gate bằng ĐÚNG key // của endpoint, và khi thiếu quyền thì NÓI RA thay vì để cột trắng trông như "chưa có gì". import { useQuery } from '@tanstack/react-query' -import { ChevronRight, FileCheck, FileSignature, FolderTree, Lock, Pencil } from 'lucide-react' -import { useState } from 'react' +import { FileSignature, FolderTree, Lock, Pencil } from 'lucide-react' import { usePermission } from '@/hooks/usePermission' +import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { @@ -34,15 +34,11 @@ import { KHKK_PHASE_LABELS, KhkkPhase, type KhkkListItemDto, - type KhkkPeCandidateDto, type PagedResult, } from '@/types/khkk' const KHKK_TIER_PAGE_SIZE = 30 -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric' }) -} /** Vỏ 1 tầng — `min-h-0 flex-1` ở CẢ HAI tầng là thứ tạo ra tỉ lệ 50/50 thật; thiếu * `min-h-0` thì con cuộn được sẽ đẩy tầng phình theo nội dung, hỏng tỉ lệ. */ @@ -78,7 +74,6 @@ function Tier({ export function KhkkWorkListPanel({ onPickPe, onPickPlan, - selectedPeId, selectedPlanId, onEditPlan, group, @@ -88,7 +83,6 @@ export function KhkkWorkListPanel({ /** Bấm KHKK ở tầng dưới → host mở phiếu đó ở panel 2, CHỈ XEM. Anh chốt: "click vào phiếu * nào thì cho hiện phiếu đó luôn, chỉ view là đc, ko re-direct về phiếu gốc". */ onPickPlan: (planId: string) => void - selectedPeId?: string | null selectedPlanId?: string | null /** [S175 · O7] Bấm bút chì trên card KHKK → host mở phiếu ở chế-độ SỬA. Không truyền ⇒ * không vẽ bút chì (cột giữ đúng hình cũ). */ @@ -98,22 +92,15 @@ export function KhkkWorkListPanel({ * N1/N7 la doc nham "nhom nay dang co viec". `null` = khong loc. */ group?: number | null }) { - // [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') - const candidates = useQuery({ - // Key dùng CHUNG với `KhkkCreatePage` là CỐ Ý: tạo kế hoạch xong quay lại đây thì - // cột tự tươi, không phải nhớ invalidate thêm một key thứ hai. - queryKey: ['khkk-pe-candidates'], - queryFn: async () => - (await api.get('/contract-signing-plans/approved-pe-awaiting-plan')).data, - enabled: canKhkk, - }) + // 🧊 [S175] Query `khkk-pe-candidates` ĐÃ GỠ khỏi panel này — tầng trên nay là + // `PipelineTreePanel`, nó TỰ nạp cây từ index PE của riêng nó. Giữ query cũ = gọi mạng + // cho một danh sách không còn ai vẽ. + // 🔸 `KhkkCreatePage` VẪN dùng key này cho ô chọn PE của form tạo — cache dùng chung, + // nên gỡ ở đây không làm form tạo mất dữ-liệu. const plans = useQuery({ queryKey: ['khkk-list', { side: true, page: 1, pageSize: KHKK_TIER_PAGE_SIZE }], @@ -124,30 +111,7 @@ export function KhkkWorkListPanel({ enabled: canKhkk, }) - // "Chưa được tạo phiếu KHKK" = chưa dùng nhóm duyệt NÀO. `?? []` phòng payload cũ còn - // trong cache React Query (DTO trước K2 không có field này) — thiếu guard thì `.length` nổ. - const awaiting = (candidates.data ?? []).filter((p) => (p.existingGroups ?? []).length === 0) - // [S175 · O8] CÂY 2 CẤP **Dự án › Hạng mục** — anh chốt "cho cây thư mục dự án hạng mục - // ra luôn nhé". Trước đây chỉ gom 1 cấp theo dự án, nên một dự án nhiều gói thầu đổ ra - // một danh sách phẳng dài, không thấy được ranh giới hạng mục. - // 🔴 0 ENDPOINT MỚI: `workItemName` ĐÃ có sẵn trong `KhkkPeCandidateDto` (`khkk.ts:423`) - // ⇒ cấp 2 là gom lại trên dữ-liệu đang có, không phải gọi thêm cửa nào. - // `Map` giữ thứ tự chèn ở CẢ HAI cấp nên dự án/hạng mục có phiếu mới nhất nổi lên trước - // (BE đã trả sẵn theo thứ tự). - const byProject = new Map>() - for (const pe of awaiting) { - const pKey = pe.projectName ?? '(chưa gán dự án)' - const wKey = pe.workItemName ?? '(chưa gán hạng mục)' - let byWorkItem = byProject.get(pKey) - if (!byWorkItem) { byWorkItem = new Map(); byProject.set(pKey, byWorkItem) } - const bucket = byWorkItem.get(wKey) - if (bucket) bucket.push(pe) - else byWorkItem.set(wKey, [pe]) - } - /** Đếm phiếu của cả một dự án (cộng mọi hạng mục con) — số ở header cấp 1. */ - const countProject = (byWorkItem: Map) => - [...byWorkItem.values()].reduce((s, arr) => s + arr.length, 0) // [S174 owner] "chỗ thao tác chỉ hiển trả lại và nháp, từ chối thôi nhé" ⇒ DANH-SÁCH-TRẮNG // 3 trạng thái, KHÔNG phải loại-trừ. 🔴 Đổi từ loại-trừ sang danh-sách-trắng là CÓ CHỦ ĐÍCH: @@ -179,115 +143,30 @@ export function KhkkWorkListPanel({ ) : ( <> + {/* [S175 — anh: "cho hiển thị nó giống thế này nhé" + "Hiển thị theo ngày tháng dự + án hạng mục phiếu duyệt ncc và phiếu KHKK tương ứng"] Tầng trên nay DÙNG LẠI + CHÍNH `PipelineTreePanel` — cây 📅 Năm › 📁 Dự án › 🧱 Hạng mục › 4 giai đoạn, + đúng cây anh chỉ sang ở màn Danh sách. + 🔴 DÙNG LẠI thay vì dựng bản thứ hai: cây đó đã có sẵn tầng NĂM + gom hạng mục + + 4 folder giai đoạn; chép lại là đẻ 2 sự-thật, sửa 1 chỗ quên chỗ kia. + 🔴 2 callback đã có sẵn từ đợt trước: `onOpenPe` (O6) → nạp PE thẳng vào form tạo · + `onOpenKhkk` → mở phiếu KHKK ở panel 2. Không truyền thì cây tự `navigate` rời màn + — đúng thứ anh cấm ("ko re-direct"). + 🔸 `bodyMaxHClass="max-h-none"`: `Tier` đã tự cuộn rồi, để trần mặc-định của cây + thì thành 2 thanh cuộn lồng nhau. */} } + icon={} > - {candidates.isLoading &&
Đang tải…
} - {candidates.isError && ( -
-

Không tải được danh sách.

- -
- )} - {!candidates.isLoading && !candidates.isError && awaiting.length === 0 && ( -

- Mọi phiếu đã duyệt đều đã có kế hoạch ký kết. -

- )} - {/* [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]) => { - 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. */} - - {!pCollapsed && [...byWorkItem.entries()].map(([workItemName, items]) => { - const wKey = `${projectName}${workItemName}` - const wCollapsed = collapsed.has(wKey) - return ( -
- - {!wCollapsed && ( -
    - {items.map((pe) => ( -
  • - -
  • - ))} -
- )} -
- ) - })} -
- ) - })} +
diff --git a/fe-admin/src/pages/khkk/KhkkCreatePage.tsx b/fe-admin/src/pages/khkk/KhkkCreatePage.tsx index 59f4e87..065c5fd 100644 --- a/fe-admin/src/pages/khkk/KhkkCreatePage.tsx +++ b/fe-admin/src/pages/khkk/KhkkCreatePage.tsx @@ -198,7 +198,6 @@ export function KhkkCreatePage() { { setViewPlanId(id); setEditPlan(false) }} - selectedPeId={purchaseEvaluationId || null} selectedPlanId={viewPlanId} onEditPlan={(id) => { setViewPlanId(id); setEditPlan(true) }} group={approvalGroup} diff --git a/fe-user/src/components/khkk/KhkkWorkListPanel.tsx b/fe-user/src/components/khkk/KhkkWorkListPanel.tsx index 3379f83..8483836 100644 --- a/fe-user/src/components/khkk/KhkkWorkListPanel.tsx +++ b/fe-user/src/components/khkk/KhkkWorkListPanel.tsx @@ -24,9 +24,9 @@ // con). Người chỉ có quyền PE mà không có KHKK sẽ ăn 403 rải khắp cột. ⇒ gate bằng ĐÚNG key // của endpoint, và khi thiếu quyền thì NÓI RA thay vì để cột trắng trông như "chưa có gì". import { useQuery } from '@tanstack/react-query' -import { ChevronRight, FileCheck, FileSignature, FolderTree, Lock, Pencil } from 'lucide-react' -import { useState } from 'react' +import { FileSignature, FolderTree, Lock, Pencil } from 'lucide-react' import { usePermission } from '@/hooks/usePermission' +import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { @@ -34,15 +34,11 @@ import { KHKK_PHASE_LABELS, KhkkPhase, type KhkkListItemDto, - type KhkkPeCandidateDto, type PagedResult, } from '@/types/khkk' const KHKK_TIER_PAGE_SIZE = 30 -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric' }) -} /** Vỏ 1 tầng — `min-h-0 flex-1` ở CẢ HAI tầng là thứ tạo ra tỉ lệ 50/50 thật; thiếu * `min-h-0` thì con cuộn được sẽ đẩy tầng phình theo nội dung, hỏng tỉ lệ. */ @@ -78,7 +74,6 @@ function Tier({ export function KhkkWorkListPanel({ onPickPe, onPickPlan, - selectedPeId, selectedPlanId, onEditPlan, group, @@ -88,7 +83,6 @@ export function KhkkWorkListPanel({ /** Bấm KHKK ở tầng dưới → host mở phiếu đó ở panel 2, CHỈ XEM. Anh chốt: "click vào phiếu * nào thì cho hiện phiếu đó luôn, chỉ view là đc, ko re-direct về phiếu gốc". */ onPickPlan: (planId: string) => void - selectedPeId?: string | null selectedPlanId?: string | null /** [S175 · O7] Bấm bút chì trên card KHKK → host mở phiếu ở chế-độ SỬA. Không truyền ⇒ * không vẽ bút chì (cột giữ đúng hình cũ). */ @@ -98,22 +92,15 @@ export function KhkkWorkListPanel({ * N1/N7 la doc nham "nhom nay dang co viec". `null` = khong loc. */ group?: number | null }) { - // [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') - const candidates = useQuery({ - // Key dùng CHUNG với `KhkkCreatePage` là CỐ Ý: tạo kế hoạch xong quay lại đây thì - // cột tự tươi, không phải nhớ invalidate thêm một key thứ hai. - queryKey: ['khkk-pe-candidates'], - queryFn: async () => - (await api.get('/contract-signing-plans/approved-pe-awaiting-plan')).data, - enabled: canKhkk, - }) + // 🧊 [S175] Query `khkk-pe-candidates` ĐÃ GỠ khỏi panel này — tầng trên nay là + // `PipelineTreePanel`, nó TỰ nạp cây từ index PE của riêng nó. Giữ query cũ = gọi mạng + // cho một danh sách không còn ai vẽ. + // 🔸 `KhkkCreatePage` VẪN dùng key này cho ô chọn PE của form tạo — cache dùng chung, + // nên gỡ ở đây không làm form tạo mất dữ-liệu. const plans = useQuery({ queryKey: ['khkk-list', { side: true, page: 1, pageSize: KHKK_TIER_PAGE_SIZE }], @@ -124,30 +111,7 @@ export function KhkkWorkListPanel({ enabled: canKhkk, }) - // "Chưa được tạo phiếu KHKK" = chưa dùng nhóm duyệt NÀO. `?? []` phòng payload cũ còn - // trong cache React Query (DTO trước K2 không có field này) — thiếu guard thì `.length` nổ. - const awaiting = (candidates.data ?? []).filter((p) => (p.existingGroups ?? []).length === 0) - // [S175 · O8] CÂY 2 CẤP **Dự án › Hạng mục** — anh chốt "cho cây thư mục dự án hạng mục - // ra luôn nhé". Trước đây chỉ gom 1 cấp theo dự án, nên một dự án nhiều gói thầu đổ ra - // một danh sách phẳng dài, không thấy được ranh giới hạng mục. - // 🔴 0 ENDPOINT MỚI: `workItemName` ĐÃ có sẵn trong `KhkkPeCandidateDto` (`khkk.ts:423`) - // ⇒ cấp 2 là gom lại trên dữ-liệu đang có, không phải gọi thêm cửa nào. - // `Map` giữ thứ tự chèn ở CẢ HAI cấp nên dự án/hạng mục có phiếu mới nhất nổi lên trước - // (BE đã trả sẵn theo thứ tự). - const byProject = new Map>() - for (const pe of awaiting) { - const pKey = pe.projectName ?? '(chưa gán dự án)' - const wKey = pe.workItemName ?? '(chưa gán hạng mục)' - let byWorkItem = byProject.get(pKey) - if (!byWorkItem) { byWorkItem = new Map(); byProject.set(pKey, byWorkItem) } - const bucket = byWorkItem.get(wKey) - if (bucket) bucket.push(pe) - else byWorkItem.set(wKey, [pe]) - } - /** Đếm phiếu của cả một dự án (cộng mọi hạng mục con) — số ở header cấp 1. */ - const countProject = (byWorkItem: Map) => - [...byWorkItem.values()].reduce((s, arr) => s + arr.length, 0) // [S174 owner] "chỗ thao tác chỉ hiển trả lại và nháp, từ chối thôi nhé" ⇒ DANH-SÁCH-TRẮNG // 3 trạng thái, KHÔNG phải loại-trừ. 🔴 Đổi từ loại-trừ sang danh-sách-trắng là CÓ CHỦ ĐÍCH: @@ -179,115 +143,30 @@ export function KhkkWorkListPanel({
) : ( <> + {/* [S175 — anh: "cho hiển thị nó giống thế này nhé" + "Hiển thị theo ngày tháng dự + án hạng mục phiếu duyệt ncc và phiếu KHKK tương ứng"] Tầng trên nay DÙNG LẠI + CHÍNH `PipelineTreePanel` — cây 📅 Năm › 📁 Dự án › 🧱 Hạng mục › 4 giai đoạn, + đúng cây anh chỉ sang ở màn Danh sách. + 🔴 DÙNG LẠI thay vì dựng bản thứ hai: cây đó đã có sẵn tầng NĂM + gom hạng mục + + 4 folder giai đoạn; chép lại là đẻ 2 sự-thật, sửa 1 chỗ quên chỗ kia. + 🔴 2 callback đã có sẵn từ đợt trước: `onOpenPe` (O6) → nạp PE thẳng vào form tạo · + `onOpenKhkk` → mở phiếu KHKK ở panel 2. Không truyền thì cây tự `navigate` rời màn + — đúng thứ anh cấm ("ko re-direct"). + 🔸 `bodyMaxHClass="max-h-none"`: `Tier` đã tự cuộn rồi, để trần mặc-định của cây + thì thành 2 thanh cuộn lồng nhau. */} } + icon={} > - {candidates.isLoading &&
Đang tải…
} - {candidates.isError && ( -
-

Không tải được danh sách.

- -
- )} - {!candidates.isLoading && !candidates.isError && awaiting.length === 0 && ( -

- Mọi phiếu đã duyệt đều đã có kế hoạch ký kết. -

- )} - {/* [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]) => { - 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. */} - - {!pCollapsed && [...byWorkItem.entries()].map(([workItemName, items]) => { - const wKey = `${projectName}${workItemName}` - const wCollapsed = collapsed.has(wKey) - return ( -
- - {!wCollapsed && ( -
    - {items.map((pe) => ( -
  • - -
  • - ))} -
- )} -
- ) - })} -
- ) - })} +
diff --git a/fe-user/src/pages/khkk/KhkkCreatePage.tsx b/fe-user/src/pages/khkk/KhkkCreatePage.tsx index 59f4e87..065c5fd 100644 --- a/fe-user/src/pages/khkk/KhkkCreatePage.tsx +++ b/fe-user/src/pages/khkk/KhkkCreatePage.tsx @@ -198,7 +198,6 @@ export function KhkkCreatePage() { { setViewPlanId(id); setEditPlan(false) }} - selectedPeId={purchaseEvaluationId || null} selectedPlanId={viewPlanId} onEditPlan={(id) => { setViewPlanId(id); setEditPlan(true) }} group={approvalGroup}