[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:
@ -8,7 +8,7 @@
|
||||
// 🔴 Cây trang Duyệt NCC (`pages/pe/PurchaseEvaluationsListPage.tsx:210-252`) KHÔNG
|
||||
// bị đụng vào: nó có bộ lọc `pendingMe` + `?deleted=1` riêng. Bộ dựng nhóm ở đây
|
||||
// là bản độc lập (duplicate CÓ CHỦ ĐÍCH) để trang đang chạy giữ nguyên hành vi.
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo, useState, type ReactNode } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ListTree, Search } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
@ -31,7 +31,13 @@ type ProjectNode = {
|
||||
}
|
||||
type YearNode = { year: number; projects: ProjectNode[]; totalCount: number }
|
||||
|
||||
function buildTree(rows: PeListItem[]): YearNode[] {
|
||||
// [S176 owner] `weightOf` = mỗi PE đóng góp BAO NHIÊU vào badge đếm. Mặc định 1 (đếm
|
||||
// gói thầu) ⇒ 6 host cũ không đổi một số nào. Chế-độ lá (`leafMode`) truyền số PHIẾU
|
||||
// khớp bộ lọc, vì ở 3 menu Đang duyệt / Đã duyệt / Đã xóa thứ người dùng đang đếm là
|
||||
// PHIẾU chứ không phải gói — và 1 gói có thể mang nhiều phiếu.
|
||||
// 🔴 Cùng luật vòng-3: đếm phải khớp ĐÚNG tập đang hiện. Giữ `+1` ở chế-độ lá thì badge
|
||||
// nói dối theo chiều ngược lại (gói có 2 phiếu vẫn kêu 1).
|
||||
function buildTree(rows: PeListItem[], weightOf: (p: PeListItem) => number = () => 1): YearNode[] {
|
||||
const yearMap = new Map<number, YearNode>()
|
||||
for (const p of rows) {
|
||||
const year = new Date(p.createdAt).getFullYear()
|
||||
@ -63,8 +69,9 @@ function buildTree(rows: PeListItem[]): YearNode[] {
|
||||
pg.workItems.push(wg)
|
||||
}
|
||||
wg.items.push(p)
|
||||
pg.totalCount++
|
||||
yg.totalCount++
|
||||
const w = weightOf(p)
|
||||
pg.totalCount += w
|
||||
yg.totalCount += w
|
||||
}
|
||||
const arr = Array.from(yearMap.values())
|
||||
arr.sort((a, b) => b.year - a.year)
|
||||
@ -92,6 +99,7 @@ export function PipelineTreePanel({
|
||||
hideSearch,
|
||||
approvedPeOnly,
|
||||
stagesOnly,
|
||||
leafMode,
|
||||
}: {
|
||||
currentStage: PipelineStageNo
|
||||
className?: string
|
||||
@ -126,6 +134,24 @@ export function PipelineTreePanel({
|
||||
// [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[]
|
||||
// [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é"] CHẾ-ĐỘ LÁ: cây dừng ở tầng Hạng mục rồi vẽ THẲNG phiếu,
|
||||
// bỏ tầng folder giai-đoạn VÀ tầng 8 ngăn nhóm. Đúng 4 tầng anh gõ.
|
||||
// 🔴 Panel này KHÔNG biết gì về kiểu KHKK — host cấp cả phép ĐẾM lẫn phép VẼ, nên cây
|
||||
// dùng lại được cho giai-đoạn khác sau này mà không kéo theo import chéo.
|
||||
// 🔴 OPT-IN: 6 host còn lại không truyền ⇒ đi nhánh `PipelineStageFolders` y như cũ.
|
||||
leafMode?: {
|
||||
/** Số phiếu của PE này SAU bộ lọc. Trả 0 ⇒ nhánh bị TỈA khỏi cây (không hiện folder rỗng). */
|
||||
countForPe: (pe: PeListItem) => number
|
||||
/** Thân tầng Hạng mục = phiếu của đúng các PE trong hạng mục đó. */
|
||||
render: (peItems: PeListItem[]) => ReactNode
|
||||
/**
|
||||
* Câu hiển thị khi bộ lọc tỉa sạch cây. Mặc định "Chưa có phiếu nào để dựng cây" nói
|
||||
* về DỰNG CÂY — ở chế-độ lá nó đánh lừa: người dùng lọc "Đã duyệt" mà rỗng sẽ tưởng
|
||||
* hệ-thống hỏng chứ không hiểu là không phiếu nào khớp bộ lọc.
|
||||
*/
|
||||
emptyText?: string
|
||||
}
|
||||
}) {
|
||||
const { canPe, buildStages } = usePipelineStages()
|
||||
const [search, setSearch] = useState('')
|
||||
@ -170,14 +196,23 @@ export function PipelineTreePanel({
|
||||
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, approvedPeOnly])
|
||||
const searched = !term
|
||||
? all
|
||||
: all.filter(p =>
|
||||
[p.projectCode, p.projectName, p.workItemName, p.maPhieu, p.tenGoiThau]
|
||||
.some(v => (v ?? '').toLowerCase().includes(term)),
|
||||
)
|
||||
// [S176] TỈA nhánh rỗng ở chế-độ lá: gói thầu không còn phiếu nào khớp bộ lọc thì
|
||||
// KHÔNG dựng nhánh cho nó. Cùng lý-do "lọc TRƯỚC khi dựng" ở trên — để lại nhánh rỗng
|
||||
// thì người dùng mở Năm→Dự án→Hạng mục ba lần chỉ để thấy chỗ trống.
|
||||
return leafMode ? searched.filter(p => leafMode.countForPe(p) > 0) : searched
|
||||
}, [peIndex.data, term, approvedPeOnly, leafMode])
|
||||
|
||||
const years = useMemo(() => buildTree(rows), [rows])
|
||||
// [S176] Chế-độ lá đếm PHIẾU (`countForPe`); mặc định đếm GÓI (mỗi PE = 1) như cũ.
|
||||
const years = useMemo(
|
||||
() => buildTree(rows, leafMode ? p => leafMode.countForPe(p) : undefined),
|
||||
[rows, leafMode],
|
||||
)
|
||||
const packageCount = years.reduce((n, y) => n + y.projects.reduce((m, p) => m + p.workItems.length, 0), 0)
|
||||
// Đang tìm kiếm ⇒ chỉ hiện folder CÓ nội dung khớp (không đổ 4 folder rỗng).
|
||||
const hideEmpty = term.length > 0
|
||||
@ -296,7 +331,9 @@ export function PipelineTreePanel({
|
||||
)}
|
||||
{canPe && !peIndex.isLoading && !peIndex.isError && years.length === 0 && (
|
||||
<p className="px-3 py-4 text-[12px] text-slate-500">
|
||||
{term ? 'Không có gói thầu nào khớp từ khoá.' : 'Chưa có phiếu nào để dựng cây.'}
|
||||
{term
|
||||
? 'Không có gói thầu nào khớp từ khoá.'
|
||||
: (leafMode?.emptyText ?? 'Chưa có phiếu nào để dựng cây.')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -351,19 +388,34 @@ export function PipelineTreePanel({
|
||||
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/wi:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
<span className="text-sm">🧱</span>
|
||||
<span className={cn('min-w-0 flex-1 truncate text-[12px]', wg.workItemId ? 'font-medium text-slate-700' : 'italic text-slate-400')}>{wg.workItemName}</span>
|
||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{wg.items.length}</span>
|
||||
{/* [S176] Chế-độ lá đếm PHIẾU, mặc định đếm GÓI — khớp đúng
|
||||
thứ đang hiện ngay dưới nó. */}
|
||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">
|
||||
{leafMode
|
||||
? wg.items.reduce((n, p) => n + leafMode.countForPe(p), 0)
|
||||
: wg.items.length}
|
||||
</span>
|
||||
</summary>
|
||||
<PipelineStageFolders
|
||||
className="ml-3 border-l border-slate-200"
|
||||
currentStage={currentStage}
|
||||
hideEmpty={hideEmpty}
|
||||
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
|
||||
})()}
|
||||
/>
|
||||
{leafMode ? (
|
||||
// [S176 owner] 4 tầng: Năm › Dự án › Hạng mục › PHIẾU.
|
||||
// Không đi qua `PipelineStageFolders` ⇒ không có tầng GĐ,
|
||||
// không có 8 ngăn nhóm.
|
||||
<div className="ml-3 border-l border-slate-200">
|
||||
{leafMode.render(wg.items)}
|
||||
</div>
|
||||
) : (
|
||||
<PipelineStageFolders
|
||||
className="ml-3 border-l border-slate-200"
|
||||
currentStage={currentStage}
|
||||
hideEmpty={hideEmpty}
|
||||
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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user