[CLAUDE] Contract: W7 server-filter per-type — ContractsListPage (kèm 3 fix badge/pagination) + MyContractsPage; InboxPage blocker-note (nợ w7b-inbox-param)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -42,27 +42,43 @@ export function ContractsListPage() {
|
|||||||
|
|
||||||
const urlType = searchParams.get('type')
|
const urlType = searchParams.get('type')
|
||||||
const urlPendingMe = searchParams.get('pendingMe') === '1'
|
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 search = searchParams.get('q') ?? ''
|
||||||
const phase = searchParams.get('phase') ?? ''
|
const phase = searchParams.get('phase') ?? ''
|
||||||
const page = Number(searchParams.get('page') ?? '1')
|
const page = Number(searchParams.get('page') ?? '1')
|
||||||
const selectedId = searchParams.get('id')
|
const selectedId = searchParams.get('id')
|
||||||
|
|
||||||
const list = useQuery({
|
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 }],
|
queryKey: ['contracts', { page, search, phase, typeFilter, urlPendingMe }],
|
||||||
queryFn: async () => {
|
queryFn: async () =>
|
||||||
const res = await api.get<Paged<ContractListItem>>('/contracts', {
|
(
|
||||||
params: {
|
await api.get<Paged<ContractListItem>>('/contracts', {
|
||||||
page,
|
params: {
|
||||||
pageSize: 30,
|
page,
|
||||||
search: search || undefined,
|
pageSize: 30,
|
||||||
phase: phase || undefined,
|
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
|
||||||
let items = res.data.items
|
// 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
|
||||||
if (typeFilter != null) items = items.filter(c => c.type === typeFilter)
|
// ngoài trang đang xem; (b) badge đếm `{total}` ở header lấy `res.data.total`
|
||||||
return { ...res.data, items }
|
// = 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({
|
const detail = useQuery({
|
||||||
|
|||||||
@ -65,6 +65,18 @@ export function InboxPage() {
|
|||||||
const selectedId = searchParams.get('id')
|
const selectedId = searchParams.get('id')
|
||||||
const search = searchParams.get('q') ?? ''
|
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({
|
const list = useQuery({
|
||||||
queryKey: ['inbox'],
|
queryKey: ['inbox'],
|
||||||
queryFn: async () => (await api.get<ContractListItem[]>('/contracts/inbox')).data,
|
queryFn: async () => (await api.get<ContractListItem[]>('/contracts/inbox')).data,
|
||||||
|
|||||||
@ -27,7 +27,13 @@ const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
|||||||
export function MyContractsPage() {
|
export function MyContractsPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
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 selectedId = searchParams.get('id')
|
||||||
const search = searchParams.get('q') ?? ''
|
const search = searchParams.get('q') ?? ''
|
||||||
// [S159-đợt5] leaf 'Đã duyệt' (?phase=9 — BE /contracts có sẵn param phase) và
|
// [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 showDeleted = searchParams.get('deleted') === '1'
|
||||||
|
|
||||||
const list = useQuery({
|
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],
|
queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
(
|
(
|
||||||
@ -43,7 +51,13 @@ export function MyContractsPage() {
|
|||||||
params: {
|
params: {
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 100,
|
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 } : {}),
|
...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -57,8 +71,10 @@ export function MyContractsPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
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 ?? []
|
let items = list.data?.items ?? []
|
||||||
if (typeFilter != null) items = items.filter(c => c.type === typeFilter)
|
|
||||||
if (search.trim()) {
|
if (search.trim()) {
|
||||||
const q = search.toLowerCase()
|
const q = search.toLowerCase()
|
||||||
items = items.filter(c =>
|
items = items.filter(c =>
|
||||||
@ -68,7 +84,8 @@ export function MyContractsPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return items
|
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) {
|
function selectContract(id: string) {
|
||||||
// [S159-đợt5 FLAG-2 reviewer] Màn "Đã xóa" = CHỈ XEM DANH SÁCH (khuôn PE S155
|
// [S159-đợt5 FLAG-2 reviewer] Màn "Đã xóa" = CHỈ XEM DANH SÁCH (khuôn PE S155
|
||||||
|
|||||||
Reference in New Issue
Block a user