[CLAUDE] Contract: KHKK UAT vong-4 — 3 menu loc hien cay Nam>Du an>Hang muc>phieu (FE-only, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m39s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m39s
Owner: "o cac menu dang duyet/ da duyet/ xoa -> cho hien thi nam -> du an -> hang muc -> phieu KHKK nhe". Day chinh la O5 "can phan" ton tu S174 — nghi van `KhkkListPanel flatMode` doi CAY->PHANG khi co filter. Owner phan: giu CAY. PipelineTreePanel — them `leafMode` OPT-IN: - Cay dung o tang Hang muc roi ve THANG phieu: bo tang folder giai-doan VA bo tang 8 ngan nhom => dung 4 tang owner go. - Panel KHONG import kieu KHKK: host cap ca phep DEM lan phep VE, nen cay dung lai duoc cho giai-doan khac sau nay. - `buildTree(rows, weightOf)` — badge dem PHIEU thay vi GOI o ca 3 tang. Giu `+1` thi 1 goi mang 2 phieu van keu 1 => badge noi doi (dung luat vong-3 "loc TRUOC khi dung cay"). - Tia nhanh 0-phieu: khong de nguoi dung mo Nam>Du an>Hang muc ba lan chi de thay cho trong. - `emptyText` OPT-IN: cau mac dinh "Chua co phieu nao de dung cay" noi ve DUNG CAY, o che-do la se doc thanh he-thong hong thay vi "khong khop loc". - 4 host con lai truyen 0 => di nhanh PipelineStageFolders y nhu cu. KhkkListPanel: - `flatMode` bo 2 ve `deletedView`/`phase`. GIU `!canPe` (cay dung tren xuong song PE, ep cay se lam ho mat trang phieu cua chinh minh) va GIU `term` (tim kiem di server-side co phan trang that = duong thoat cho phieu ngoai cua-so 200). - Thung rac tren cay phai noi NGUON RIENG `/contract-signing-plans/deleted` pageSize 200 — linkage cua cay chi goi endpoint thuong nen KHONG he co phieu da xoa (grep 'deleted' trong hook = 0 hit). - Phieu da xoa tren cay nay cu-xu y het o phang: lam mo + KHONG cho mo. Thieu ve nay thi cung mot phieu mo duoc o cay ma khong mo duoc o phang. 🔴 CAM them ve `group !== null` vao dieu-kien leafMode: CA BON leaf menu KHKK deu mang `?group=1` (Layout.tsx:41-45), ke ca "Danh sach" khong loc — them ve do la keo luon man TOAN CANH vao che-do la, tuc doi mot man owner khong he yeu cau. Nhom la chieu CHIA NGAN, khong phai chieu CHON HINH CAY. 🔸 KHAI GIOI-HAN: cay an cua-so 200 phieu moi nhat => 3 menu nay thu hep tam voi so voi phang-phan-trang-that. Dong khai truncation cua cay van hien; duong thoat = go tim kiem. Volume KHKK hien nho nen chua can. mirror sha256 khop 2/2 cap file; build xanh x2 app; tsc sach x2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -22,12 +22,13 @@
|
||||
// ⇒ ô tìm + `Select` trạng-thái đi SERVER-SIDE thẳng `GET /contract-signing-plans`
|
||||
// (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo
|
||||
// "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt.
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ChevronLeft, ChevronRight, FileCheck, Inbox, Search } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
|
||||
import { usePipelineStages } from '@/hooks/usePipelineStages'
|
||||
import { usePipelineLinkage, usePipelineStages } from '@/hooks/usePipelineStages'
|
||||
import { api } from '@/lib/api'
|
||||
import { cn } from '@/lib/cn'
|
||||
import {
|
||||
@ -40,6 +41,10 @@ import {
|
||||
} from '@/types/khkk'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
// [S176] Cửa-sổ nguồn cho CÂY ở chế-độ thùng rác. Bằng đúng `INDEX_PAGE_SIZE`/
|
||||
// `PE_INDEX_PAGE_SIZE` của 2 index cây (200) để cả cây kể CÙNG một câu chuyện về giới-hạn
|
||||
// — lấy 20/trang như chế-độ phẳng thì cây sẽ cụt mà không có dòng nào khai.
|
||||
const TREE_SOURCE_PAGE_SIZE = 200
|
||||
|
||||
function formatVnd(n: number | null | undefined): string {
|
||||
if (n === null || n === undefined) return '—'
|
||||
@ -198,17 +203,29 @@ export function KhkkListPanel({
|
||||
onOpenPe?: (id: string) => void
|
||||
}) {
|
||||
const { canPe } = usePipelineStages()
|
||||
const linkage = usePipelineLinkage()
|
||||
const term = search.trim()
|
||||
|
||||
// [R-11] Lọc đi SERVER-SIDE, KHÔNG lọc trên cây: có từ khoá / có trạng-thái / xem thùng
|
||||
// rác ⇒ chuyển sang danh-sách phẳng có phân-trang thật (`total` từ server) nên phiếu nằm
|
||||
// ngoài 200 phiếu mới nhất VẪN tới được.
|
||||
// [M-1] `!canPe` ⇒ cũng dùng đúng danh-sách phẳng này — cây khoá không được phép làm
|
||||
// người dùng mất trắng phiếu của chính họ.
|
||||
const flatMode = !canPe || deletedView || term.length > 0 || phase !== null
|
||||
// [S176 owner] "ở các menu đang duyệt/ đã duyệt/ xóa → cho hiển thị năm → dự án → hạng
|
||||
// mục → phiếu KHKK nhé" ⇒ 3 menu đó nay đi CÂY (chế-độ lá 4 tầng), không còn phẳng.
|
||||
//
|
||||
// 🧊 Trước S176: `flatMode` còn gồm `deletedView || phase !== null`. Bỏ 2 vế đó.
|
||||
// [R-11] GIỮ vế từ-khoá: tìm kiếm đi SERVER-SIDE có phân-trang thật (`total` từ server)
|
||||
// nên phiếu nằm ngoài cửa-sổ 200 VẪN tới được — đó là đường thoát khi cây không
|
||||
// với tới, và là lý do KHÔNG bỏ nốt vế này.
|
||||
// [M-1] GIỮ vế `!canPe`: cây dựng trên xương sống phiếu Duyệt NCC (`enabled: canPe`),
|
||||
// ai không có quyền đó thì cây KHÔNG dựng được — ép cây sẽ làm họ mất trắng phiếu
|
||||
// của chính mình. Danh-sách phẳng là lưới an-toàn, không phải tuỳ chọn.
|
||||
const flatMode = !canPe || term.length > 0
|
||||
|
||||
// Xem thùng rác ở chế-độ CÂY: phiếu đã xoá KHÔNG nằm trong index của cây
|
||||
// (`usePipelineLinkage` chỉ gọi `/contract-signing-plans`), nên phải tự lấy từ endpoint
|
||||
// riêng rồi tự gắn vào PE. Lấy đúng cửa-sổ 200 như 2 index kia để cùng một câu chuyện
|
||||
// truncation, thay vì 20/trang của chế-độ phẳng.
|
||||
const treeDeleted = deletedView && !flatMode
|
||||
|
||||
const list = useQuery({
|
||||
queryKey: ['khkk-list', { group, phase, search: term, page, deletedView }],
|
||||
queryKey: ['khkk-list', { group, phase, search: term, page, deletedView, tree: treeDeleted }],
|
||||
queryFn: async () => {
|
||||
const url = deletedView ? '/contract-signing-plans/deleted' : '/contract-signing-plans'
|
||||
return (await api.get<PagedResult<KhkkListItemDto>>(url, {
|
||||
@ -216,14 +233,39 @@ export function KhkkListPanel({
|
||||
phase: deletedView ? undefined : (phase ?? undefined),
|
||||
group: group ?? undefined,
|
||||
search: term || undefined,
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
page: treeDeleted ? 1 : page,
|
||||
pageSize: treeDeleted ? TREE_SOURCE_PAGE_SIZE : PAGE_SIZE,
|
||||
},
|
||||
})).data
|
||||
},
|
||||
enabled: flatMode,
|
||||
enabled: flatMode || treeDeleted,
|
||||
})
|
||||
|
||||
// Phiếu đã xoá gom theo PE — chỉ dựng khi đang xem thùng rác bằng cây.
|
||||
const deletedByPeId = useMemo(() => {
|
||||
const map = new Map<string, KhkkListItemDto[]>()
|
||||
if (!treeDeleted) return map
|
||||
for (const k of list.data?.items ?? []) {
|
||||
const bucket = map.get(k.purchaseEvaluationId)
|
||||
if (bucket) bucket.push(k)
|
||||
else map.set(k.purchaseEvaluationId, [k])
|
||||
}
|
||||
return map
|
||||
}, [treeDeleted, list.data])
|
||||
|
||||
// Phiếu treo dưới một gói thầu, SAU khi áp đủ bộ lọc đang bật (trạng-thái + nhóm).
|
||||
// 🔴 Lọc ở ĐÂY = lọc TRƯỚC khi cây dựng ⇒ badge đếm ở cả 3 tầng tự khớp tập đang hiện.
|
||||
// Đây đúng là luật vòng-3 ("lọc phase TRƯỚC khi dựng cây, lọc sau thì badge nói dối").
|
||||
const plansForPe = useMemo(() => {
|
||||
return (peId: string): KhkkListItemDto[] => {
|
||||
const src = treeDeleted ? (deletedByPeId.get(peId) ?? []) : (linkage.khkkByPeId.get(peId) ?? [])
|
||||
const byGroup = group === null ? src : src.filter(k => k.approvalGroup === group)
|
||||
// Thùng rác KHÔNG lọc theo trạng-thái: phiếu đã xoá giữ nguyên phase lúc bị xoá, lọc
|
||||
// thêm chỉ tổ giấu mất phiếu người dùng đang đi tìm.
|
||||
return treeDeleted || phase === null ? byGroup : byGroup.filter(k => k.phase === phase)
|
||||
}
|
||||
}, [treeDeleted, deletedByPeId, linkage.khkkByPeId, group, phase])
|
||||
|
||||
const items = list.data?.items ?? []
|
||||
const total = list.data?.total ?? 0
|
||||
const totalPages = list.data?.totalPages ?? 1
|
||||
@ -242,11 +284,14 @@ export function KhkkListPanel({
|
||||
return (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{shown.map(k => (
|
||||
<li key={k.id}>
|
||||
// [S176] Thùng rác trên CÂY phải cư-xử y hệt thùng rác ở danh-sách phẳng:
|
||||
// làm mờ + KHÔNG cho mở. Thiếu vế này thì cùng một phiếu đã xoá lại mở được ở
|
||||
// cây mà không mở được ở phẳng — cùng dữ-liệu, hai luật.
|
||||
<li key={k.id} className={cn(deletedView && 'opacity-70')}>
|
||||
<KhkkPlanCard
|
||||
plan={k}
|
||||
selected={selectedId === k.id}
|
||||
onSelect={onSelect}
|
||||
onSelect={deletedView ? () => { /* phiếu đã xóa: chỉ liệt kê, không mở */ } : onSelect}
|
||||
finalize={selectedId === k.id ? selectedFinalize : undefined}
|
||||
/>
|
||||
</li>
|
||||
@ -306,6 +351,24 @@ export function KhkkListPanel({
|
||||
hideSearch
|
||||
bodyMaxHClass="max-h-[28rem] lg:max-h-none"
|
||||
stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect, onOpenPe }}
|
||||
// [S176 owner] ĐÚNG 3 menu anh gọi tên — `filter=ChoDuyet` (Đang duyệt) ·
|
||||
// `filter=DaDuyet` (Đã duyệt) · `view=deleted` (Đã xóa) ⇒ cây rút về 4 tầng
|
||||
// Năm › Dự án › Hạng mục › phiếu KHKK.
|
||||
// 🔴 CẤM thêm vế `group !== null` vào đây: CẢ BỐN leaf menu KHKK đều mang
|
||||
// `?group=1` (`Layout.tsx:41-45`), kể cả "Danh sách" không lọc — thêm vế đó là
|
||||
// kéo luôn menu TOÀN CẢNH vào chế-độ lá, tức đổi một màn owner không hề yêu cầu.
|
||||
// Nhóm là chiều CHIA NGĂN, không phải chiều CHỌN HÌNH CÂY.
|
||||
leafMode={
|
||||
phase !== null || deletedView
|
||||
? {
|
||||
countForPe: pe => plansForPe(pe.id).length,
|
||||
render: peItems => stage2Content(peItems.flatMap(p => plansForPe(p.id))),
|
||||
emptyText: deletedView
|
||||
? 'Không có phiếu đã xóa.'
|
||||
: 'Chưa có kế hoạch ký kết nào khớp bộ lọc.',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user