From 502f4d2ea25407043246f9e539a43f4fb649eaf5 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Mon, 10 Aug 2026 18:20:41 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Contract:=20W7=20server-filter=20per?= =?UTF-8?q?-type=20=E2=80=94=20ContractsListPage=20(k=C3=A8m=203=20fix=20b?= =?UTF-8?q?adge/pagination)=20+=20MyContractsPage;=20InboxPage=20blocker-n?= =?UTF-8?q?ote=20(n=E1=BB=A3=20w7b-inbox-param)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../src/pages/contracts/ContractsListPage.tsx | 44 +++++++++++++------ fe-user/src/pages/InboxPage.tsx | 12 +++++ .../src/pages/contracts/MyContractsPage.tsx | 25 +++++++++-- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/fe-admin/src/pages/contracts/ContractsListPage.tsx b/fe-admin/src/pages/contracts/ContractsListPage.tsx index 0f08bda..144c145 100644 --- a/fe-admin/src/pages/contracts/ContractsListPage.tsx +++ b/fe-admin/src/pages/contracts/ContractsListPage.tsx @@ -42,27 +42,43 @@ export function ContractsListPage() { const urlType = searchParams.get('type') const urlPendingMe = searchParams.get('pendingMe') === '1' - const typeFilter = urlType ? Number(urlType) : null + // [W7 S187] `type` nay ĐI THẲNG lên BE (không còn lọc client) ⇒ phải chặn giá trị rác + // TRƯỚC khi lên dây: `Number('abc') = NaN` mà `NaN != null` là true ⇒ nếu không chặn thì + // axios gửi `?type=NaN`, BE bind `ContractType?` hỏng → 400 thay vì "danh sách rỗng". + // Khuôn đã chạy CÙNG THƯ MỤC ở `HardCopiesPage.tsx:66-68` (GĐ4) — dùng lại nguyên. + const parsedType = urlType === null ? Number.NaN : Number(urlType) + const typeFilter = Number.isFinite(parsedType) && parsedType > 0 ? parsedType : null const search = searchParams.get('q') ?? '' const phase = searchParams.get('phase') ?? '' const page = Number(searchParams.get('page') ?? '1') const selectedId = searchParams.get('id') const list = useQuery({ + // `typeFilter` ĐÃ nằm trong key từ trước ⇒ đổi sang lọc server KHÔNG cần thêm trục mới; + // cache cũ (payload chưa lọc) tự hết hiệu lực vì mỗi `type` là 1 ô key riêng sẵn rồi. queryKey: ['contracts', { page, search, phase, typeFilter, urlPendingMe }], - queryFn: async () => { - const res = await api.get>('/contracts', { - params: { - page, - pageSize: 30, - search: search || undefined, - phase: phase || undefined, - }, - }) - let items = res.data.items - if (typeFilter != null) items = items.filter(c => c.type === typeFilter) - return { ...res.data, items } - }, + queryFn: async () => + ( + await api.get>('/contracts', { + params: { + page, + pageSize: 30, + search: search || undefined, + phase: phase || undefined, + // [W7 S187] Lọc loại HĐ CHUYỂN LÊN SERVER. Trước đây lọc client SAU khi đã cắt + // 30 dòng/trang ⇒ 3 cái sai cùng lúc: (a) loại hiếm biến mất IM LẶNG khi nằm + // ngoài trang đang xem; (b) badge đếm `{total}` ở header lấy `res.data.total` + // = tổng CHƯA lọc ⇒ nói dối khi có `?type=`; (c) phân trang tính theo tổng + // chưa lọc ⇒ có trang rỗng hoàn toàn. BE lọc TRƯỚC khi đếm & cắt + // (`ContractFeatures.cs:327` → `CountAsync:340` → `Skip/Take:342`) ⇒ chuyển lên + // server sửa cả 3. Param có sẵn: `ContractsController.cs:25`. + // `?? undefined` ⇒ axios bỏ hẳn key, giữ nguyên hành vi "không chọn loại". + type: typeFilter ?? undefined, + }, + }) + ).data, + // [W7 S187] Vế `items.filter(c => c.type === typeFilter)` ĐÃ GỠ — BE lọc rồi. Giữ lại + // = lọc 2 lần, và che mất lỗi nếu server trả sai loại (sai sẽ THẦM LẶNG). }) const detail = useQuery({ diff --git a/fe-user/src/pages/InboxPage.tsx b/fe-user/src/pages/InboxPage.tsx index 6474c55..1a20cd4 100644 --- a/fe-user/src/pages/InboxPage.tsx +++ b/fe-user/src/pages/InboxPage.tsx @@ -65,6 +65,18 @@ export function InboxPage() { const selectedId = searchParams.get('id') const search = searchParams.get('q') ?? '' + // 🔴 [W7 S187 — BLOCKED, đừng "làm nốt"] Lọc loại HĐ ở màn này CỐ Ý CÒN Ở CLIENT. + // `MyContractsPage`/`ContractsListPage` đã chuyển `?type=` lên server được vì + // `GET /api/contracts` có param (`ContractsController.cs:25`). Endpoint NÀY thì KHÔNG: + // `ContractsController.cs:41-43` là `Inbox(CancellationToken ct)` → `new GetMyInboxQuery()`, + // và `ContractFeatures.cs:479` khai `public record GetMyInboxQuery : IRequest<...>;` — + // KHÔNG THAM SỐ. Gửi `?type=` lên đây thì BE **nuốt im lặng**, còn gỡ vế lọc dưới kia sẽ + // giết CÂM bộ lọc của 7 leaf `Ct_*_Pending` (`Layout.tsx:150` → `/inbox?type=N`). + // ⚠️ Nợ THẬT chưa trả: `ContractFeatures.cs:550` có `q.Take(100)` ⇒ hộp thư >100 dòng thì + // loại hiếm VẪN biến mất im lặng. Vá được CHỈ KHI BE thêm param cho inbox (lane backend); + // xong bên đó thì mới bê nguyên khuôn `MyContractsPage` sang (nhớ chặn `NaN` + thêm `type` + // vào `queryKey` — key ở đây đang là `['inbox']` trống, đổi nguồn mà quên là cache dùng + // chung sai CÂM). const list = useQuery({ queryKey: ['inbox'], queryFn: async () => (await api.get('/contracts/inbox')).data, diff --git a/fe-user/src/pages/contracts/MyContractsPage.tsx b/fe-user/src/pages/contracts/MyContractsPage.tsx index afbc92c..cd03de6 100644 --- a/fe-user/src/pages/contracts/MyContractsPage.tsx +++ b/fe-user/src/pages/contracts/MyContractsPage.tsx @@ -27,7 +27,13 @@ const fmtMoney = (v: number) => v.toLocaleString('vi-VN') export function MyContractsPage() { const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() - const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : null + // [W7 S187] `type` nay ĐI THẲNG lên BE (không còn lọc client) ⇒ phải chặn giá trị rác + // TRƯỚC khi lên dây: `Number('abc') = NaN` mà `NaN != null` là true ⇒ nếu không chặn thì + // axios gửi `?type=NaN`, BE bind `ContractType?` hỏng → 400 thay vì "danh sách rỗng". + // Khuôn đã chạy ở `HardCopiesPage.tsx:67-68` (GĐ4) — dùng lại nguyên, không đẻ khuôn thứ 2. + const rawType = searchParams.get('type') + const parsedType = rawType === null ? Number.NaN : Number(rawType) + const typeFilter = Number.isFinite(parsedType) && parsedType > 0 ? parsedType : null const selectedId = searchParams.get('id') const search = searchParams.get('q') ?? '' // [S159-đợt5] leaf 'Đã duyệt' (?phase=9 — BE /contracts có sẵn param phase) và @@ -36,6 +42,8 @@ export function MyContractsPage() { const showDeleted = searchParams.get('deleted') === '1' const list = useQuery({ + // `typeFilter` ĐÃ nằm trong key từ trước ⇒ đổi sang lọc server KHÔNG cần thêm trục mới; + // cache cũ (payload chưa lọc) tự hết hiệu lực vì mỗi `type` là 1 ô key riêng sẵn rồi. queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted], queryFn: async () => ( @@ -43,7 +51,13 @@ export function MyContractsPage() { params: { page: 1, pageSize: 100, - ...(showDeleted && typeFilter != null ? { type: typeFilter } : {}), + // [W7 S187] Lọc loại HĐ CHUYỂN LÊN SERVER cho CẢ 2 nhánh (trước đây chỉ nhánh + // `/contracts/deleted` gửi `type`, nhánh thường lọc client sau khi cắt 100 dòng + // ⇒ user >100 HĐ thì loại hiếm biến mất IM LẶNG). Param có sẵn 2 chỗ: + // `ContractsController.cs:25` (`/contracts`) + `:34` (`/contracts/deleted`); + // BE lọc TRƯỚC phân trang (`ContractFeatures.cs:325` trước `Skip/Take:340`). + // `?? undefined` ⇒ axios bỏ hẳn key, giữ nguyên hành vi "không chọn loại". + type: typeFilter ?? undefined, ...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}), }, }) @@ -57,8 +71,10 @@ export function MyContractsPage() { }) const rows = useMemo(() => { + // [W7 S187] Vế `items.filter(c => c.type === typeFilter)` ĐÃ GỠ — BE lọc rồi. Giữ lại + // = lọc 2 lần, và che mất lỗi nếu server trả sai loại (sai sẽ THẦM LẶNG). `search` vẫn + // client-side CÓ CHỦ ĐÍCH: ô tìm không đổi request (payload 100 dòng đã tải trọn). let items = list.data?.items ?? [] - if (typeFilter != null) items = items.filter(c => c.type === typeFilter) if (search.trim()) { const q = search.toLowerCase() items = items.filter(c => @@ -68,7 +84,8 @@ export function MyContractsPage() { ) } return items - }, [list.data, typeFilter, search]) + // `typeFilter` gỡ khỏi deps CÙNG LÚC với vế lọc (không còn đọc trong thân hàm). + }, [list.data, search]) function selectContract(id: string) { // [S159-đợt5 FLAG-2 reviewer] Màn "Đã xóa" = CHỈ XEM DANH SÁCH (khuôn PE S155