[CLAUDE] FE-User: YC-029 W3 — leaf Đang duyệt ở lại /my-contracts (pendingMe đổi-endpoint /contracts/inbox)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -59,7 +59,11 @@ const INT_TO_PE_CODE: Record<number, string> = Object.fromEntries(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// User-side menu key → route. Differs from admin: Danh sách points to
|
// User-side menu key → route. Differs from admin: Danh sách points to
|
||||||
// /my-contracts (user's own drafts), Duyệt to /inbox (pending THEIR approval).
|
// /my-contracts (user's own drafts).
|
||||||
|
// 🔴 [YC-029 W3 S190 — vá comment stale] Câu cũ "Duyệt to /inbox" HẾT ĐÚNG cho họ `Ct_*`:
|
||||||
|
// leaf "Đang duyệt" nay Ở LẠI `/my-contracts?type=N&pendingMe=1` (xem nhánh `Pending` :164).
|
||||||
|
// `/inbox` + `InboxPage` GIỮ NGUYÊN, đổi vai: hộp thư TỔNG mọi loại, vào bằng mục cố định
|
||||||
|
// "Hộp thư" (`USER_FIXED_TOP` / `staticResolvePath('__inbox')`), không còn là đích của leaf HĐ.
|
||||||
function resolvePath(key: string): string | null {
|
function resolvePath(key: string): string | null {
|
||||||
const staticMap: Record<string, string> = {
|
const staticMap: Record<string, string> = {
|
||||||
// [W2 KHKK S161] GĐ2 hết skeleton — 6 leaf re-point trang thật (gotcha #50:
|
// [W2 KHKK S161] GĐ2 hết skeleton — 6 leaf re-point trang thật (gotcha #50:
|
||||||
@ -161,7 +165,13 @@ function resolvePath(key: string): string | null {
|
|||||||
if (!typeInt) return null
|
if (!typeInt) return null
|
||||||
if (action === 'List') return `/my-contracts?type=${typeInt}`
|
if (action === 'List') return `/my-contracts?type=${typeInt}`
|
||||||
if (action === 'Create') return `/contracts/new?type=${typeInt}`
|
if (action === 'Create') return `/contracts/new?type=${typeInt}`
|
||||||
if (action === 'Pending') return `/inbox?type=${typeInt}`
|
// 🔴 [YC-029 W3 S190] "Đang duyệt" Ở LẠI trang danh sách thay vì nhảy sang `/inbox`
|
||||||
|
// (anh: bấm mục con của loại HĐ mà bị đá sang hộp thư tổng = mất ngữ cảnh loại).
|
||||||
|
// `pendingMe=1` là DANH-TÍNH leaf (KHÔNG vào `TRANSIENT_QUERY_KEYS` — xem chú ở đó),
|
||||||
|
// trang tự ĐỔI ENDPOINT sang `GET /contracts/inbox` khi thấy nó.
|
||||||
|
// GIỮ NGUYÊN route `/inbox` + `InboxPage`: đó là hộp thư TỔNG (mục cố định "Hộp thư"),
|
||||||
|
// không xoá — đường vào khác, dữ liệu cùng nguồn.
|
||||||
|
if (action === 'Pending') return `/my-contracts?type=${typeInt}&pendingMe=1`
|
||||||
if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}`
|
if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}`
|
||||||
// [YC-029 W1 S190] `view=approved` = KHOÁ ĐỊNH DANH (khuôn PE :185, H-4): `phase` nằm
|
// [YC-029 W1 S190] `view=approved` = KHOÁ ĐỊNH DANH (khuôn PE :185, H-4): `phase` nằm
|
||||||
// trong TRANSIENT (là bộ lọc) nên URL "Đã duyệt" trùng hệt URL user tự lọc phase=9 trên
|
// trong TRANSIENT (là bộ lọc) nên URL "Đã duyệt" trùng hệt URL user tự lọc phase=9 trên
|
||||||
|
|||||||
@ -12,6 +12,11 @@
|
|||||||
// ở Panel 2 chứ không bỏ. Delta này plan W2 KHÔNG nêu (plan chỉ nói "panel-2 giữ detail") —
|
// ở Panel 2 chứ không bỏ. Delta này plan W2 KHÔNG nêu (plan chỉ nói "panel-2 giữ detail") —
|
||||||
// khai ở đây để người sau khỏi tưởng ai đó lỡ tay đánh rơi.
|
// khai ở đây để người sau khỏi tưởng ai đó lỡ tay đánh rơi.
|
||||||
//
|
//
|
||||||
|
// [S190 · W3 YC-029] Panel 3 có HAI NGUỒN, chọn bằng `?pendingMe=1` của URL:
|
||||||
|
// • mặc định → `GET /contracts` (phân trang, lọc `type`/`phase`/`search` phía server)
|
||||||
|
// • pendingMe → `GET /contracts/inbox` (0 tham số, trọn gói) — leaf "Đang duyệt" nay ở lại
|
||||||
|
// trang này thay vì nhảy sang `/inbox`. Xem khối `queryFn` để biết vì sao KHÔNG gửi param.
|
||||||
|
//
|
||||||
// Mobile (< lg): GIỮ NGUYÊN 100% hiện trạng — chỉ danh-sách hiện, click row → fullpage
|
// Mobile (< lg): GIỮ NGUYÊN 100% hiện trạng — chỉ danh-sách hiện, click row → fullpage
|
||||||
// /contracts/:id. Cây là khối DESKTOP-ONLY y như TRƯỚC hoán-đổi (nó vốn nằm trong panel
|
// /contracts/:id. Cây là khối DESKTOP-ONLY y như TRƯỚC hoán-đổi (nó vốn nằm trong panel
|
||||||
// `hidden lg:block`), nên đợt này không đổi một pixel nào ở < lg.
|
// `hidden lg:block`), nên đợt này không đổi một pixel nào ở < lg.
|
||||||
@ -52,6 +57,10 @@ export function MyContractsPage() {
|
|||||||
// 'Đã xóa' (?deleted=1 — endpoint /contracts/deleted mới, chỉ-xem).
|
// 'Đã xóa' (?deleted=1 — endpoint /contracts/deleted mới, chỉ-xem).
|
||||||
const phaseFilter = searchParams.get('phase') ? Number(searchParams.get('phase')) : null
|
const phaseFilter = searchParams.get('phase') ? Number(searchParams.get('phase')) : null
|
||||||
const showDeleted = searchParams.get('deleted') === '1'
|
const showDeleted = searchParams.get('deleted') === '1'
|
||||||
|
// [S190 W3 YC-029] leaf "Đang duyệt" nay Ở LẠI trang này (`Layout.tsx` nhánh `Pending`)
|
||||||
|
// thay vì nhảy sang `/inbox`. `pendingMe` là DANH-TÍNH leaf ⇒ CẤM đưa vào
|
||||||
|
// `TRANSIENT_QUERY_KEYS` (strip = 2 leaf "Danh sách"/"Đang duyệt" cùng sáng).
|
||||||
|
const pendingMe = searchParams.get('pendingMe') === '1'
|
||||||
|
|
||||||
// [S190 W2 · Q2 nhánh-đã-chọn] Select loại HĐ ở đầu panel CÂY. Mặc định = `?type=N` của URL
|
// [S190 W2 · Q2 nhánh-đã-chọn] Select loại HĐ ở đầu panel CÂY. Mặc định = `?type=N` của URL
|
||||||
// (menu loại HĐ đang đứng). 🔴 Đổi Select CHỈ đổi bộ lọc CÂY — KHÔNG navigate, KHÔNG đụng
|
// (menu loại HĐ đang đứng). 🔴 Đổi Select CHỈ đổi bộ lọc CÂY — KHÔNG navigate, KHÔNG đụng
|
||||||
@ -73,9 +82,28 @@ export function MyContractsPage() {
|
|||||||
// 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.
|
// 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.
|
||||||
// [S190 W2] +trục `search`: ô tìm nay đi SERVER-SIDE (xem `params.search`) ⇒ mỗi từ khoá
|
// [S190 W2] +trục `search`: ô tìm nay đi SERVER-SIDE (xem `params.search`) ⇒ mỗi từ khoá
|
||||||
// là một payload khác nhau, thiếu trục này là ăn cache của từ khoá trước.
|
// là một payload khác nhau, thiếu trục này là ăn cache của từ khoá trước.
|
||||||
queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted, search],
|
// [S190 W3] +trục `pendingMe`: 2 chế độ đọc 2 NGUỒN khác nhau (`/contracts` phân trang
|
||||||
queryFn: async () =>
|
// ⟂ `/contracts/inbox` trọn gói) ⇒ dùng chung ô cache là trả nhầm danh sách.
|
||||||
(
|
queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted, search, pendingMe],
|
||||||
|
queryFn: async () => {
|
||||||
|
// [S190 W3 — khuôn PE `PurchaseEvaluationsListPage.tsx:77-99`] Nhánh "Đã xóa" đứng
|
||||||
|
// TRƯỚC `pendingMe`: 2 view LOẠI TRỪ nhau, và HĐ đã xóa chỉ thấy được qua endpoint
|
||||||
|
// riêng `/contracts/deleted`. Đảo thứ tự = URL `?deleted=1&pendingMe=1` rơi vào inbox
|
||||||
|
// (inbox không bao giờ trả HĐ đã xóa) ⇒ màn "Đã xóa" rỗng THẦM LẶNG.
|
||||||
|
if (pendingMe && !showDeleted) {
|
||||||
|
// 🔴 ĐỔI ENDPOINT, KHÔNG gửi thêm param: `GET /contracts` KHÔNG có param `pendingMe`
|
||||||
|
// (`ContractsController.cs:17-26` — 8 param, không có nó) và ASP.NET NUỐT param lạ IM
|
||||||
|
// LẶNG ⇒ gửi lên sẽ ra Y HỆT danh sách thường, hỏng câm chứ không đỏ.
|
||||||
|
// `GET /contracts/inbox` (`ContractsController.cs:41-43`) = 0 THAM SỐ, trả
|
||||||
|
// `List<ContractListItemDto>` (không phân trang) → lọc loại ở CLIENT + ép shape.
|
||||||
|
// [carry:w7b-inbox-param] LỆCH-W7 CHẤP NHẬN ĐỢT NÀY: W7 (S187) đã chuyển lọc `type`
|
||||||
|
// lên server cho 2 nhánh kia; nhánh này vẫn lọc client vì BE chưa có param —
|
||||||
|
// `GetMyInboxQuery` là record 0 tham số và còn `Take(100)` cứng. Nâng BE = việc đợt sau.
|
||||||
|
const res = await api.get<ContractListItem[]>('/contracts/inbox')
|
||||||
|
const items = typeFilter != null ? res.data.filter(c => c.type === typeFilter) : res.data
|
||||||
|
return { items, total: items.length, page: 1, pageSize: items.length }
|
||||||
|
}
|
||||||
|
return (
|
||||||
await api.get<Paged<ContractListItem>>(showDeleted ? '/contracts/deleted' : '/contracts', {
|
await api.get<Paged<ContractListItem>>(showDeleted ? '/contracts/deleted' : '/contracts', {
|
||||||
params: {
|
params: {
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -97,7 +125,8 @@ export function MyContractsPage() {
|
|||||||
...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}),
|
...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
).data,
|
).data
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const detail = useQuery({
|
const detail = useQuery({
|
||||||
@ -143,6 +172,19 @@ export function MyContractsPage() {
|
|||||||
|
|
||||||
const typeLabel = typeFilter != null ? ContractTypeLabel[typeFilter] : null
|
const typeLabel = typeFilter != null ? ContractTypeLabel[typeFilter] : null
|
||||||
|
|
||||||
|
// [S190 W3] "Rỗng vì LỌC" ⟂ "rỗng vì CHƯA CÓ GÌ" phải khác câu: dùng chung một câu thì
|
||||||
|
// người đang ở mục "Đang duyệt" đọc thành "loại HĐ này chưa có dữ liệu" — sai nghĩa hẳn.
|
||||||
|
const emptyTitle = pendingMe && !showDeleted ? 'Không có HĐ chờ bạn duyệt' : 'Không có HĐ'
|
||||||
|
const emptyDescription = showDeleted
|
||||||
|
? 'Chưa có HĐ nào bị xóa.'
|
||||||
|
: pendingMe
|
||||||
|
? typeLabel
|
||||||
|
? `Không HĐ "${typeLabel}" nào đang đến lượt bạn duyệt.`
|
||||||
|
: 'Không có HĐ nào đang đến lượt bạn duyệt.'
|
||||||
|
: typeFilter != null
|
||||||
|
? 'Loại HĐ này chưa có dữ liệu.'
|
||||||
|
: 'Tạo HĐ mới để bắt đầu.'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)] flex-col">
|
<div className="flex h-[calc(100vh-4rem)] flex-col">
|
||||||
{/* Compact page header — chiếm ít chiều cao để 3 panel có max chỗ */}
|
{/* Compact page header — chiếm ít chiều cao để 3 panel có max chỗ */}
|
||||||
@ -166,6 +208,13 @@ export function MyContractsPage() {
|
|||||||
Đã phát hành
|
Đã phát hành
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{/* [S190 W3] Chế độ phải NHÌN THẤY ĐƯỢC: cùng URL `/my-contracts`, cùng tiêu đề,
|
||||||
|
nhưng danh-sách đọc từ hộp thư đến ⇒ không có nhãn thì user tưởng HĐ biến mất. */}
|
||||||
|
{!showDeleted && pendingMe && (
|
||||||
|
<span className="rounded-full bg-amber-50 px-2.5 py-1 text-[11px] font-semibold text-amber-700 ring-1 ring-inset ring-amber-200">
|
||||||
|
Đang duyệt — đến lượt tôi
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!showDeleted && (
|
{!showDeleted && (
|
||||||
<Button onClick={() => navigate(typeFilter ? `/contracts/new?type=${typeFilter}` : '/contracts/new')}>
|
<Button onClick={() => navigate(typeFilter ? `/contracts/new?type=${typeFilter}` : '/contracts/new')}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
@ -246,12 +295,22 @@ export function MyContractsPage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" />
|
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" />
|
||||||
<Input
|
<Input
|
||||||
value={search}
|
value={pendingMe ? '' : search}
|
||||||
onChange={e => updateSearch(e.target.value)}
|
onChange={e => updateSearch(e.target.value)}
|
||||||
|
disabled={pendingMe}
|
||||||
placeholder="Tìm theo mã / tên / NCC…"
|
placeholder="Tìm theo mã / tên / NCC…"
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* [S190 W3] KHOÁ ô tìm ở chế độ "Đang duyệt" — và nói VÌ SAO bằng CHỮ HIỆN,
|
||||||
|
KHÔNG bằng `title` (tooltip trên control `disabled` bị Chromium nuốt — bài S182).
|
||||||
|
Trang cũng KHÔNG có UI phân trang (pageSize 100 cố định) nên không có gì để ẩn;
|
||||||
|
inbox trả trọn gói, không phân trang — `[carry:w7b-inbox-param]`. */}
|
||||||
|
{pendingMe && (
|
||||||
|
<p className="mt-1.5 text-[11px] leading-snug text-slate-500">
|
||||||
|
Chế độ “Đang duyệt” đọc hộp thư đến — chưa hỗ trợ tìm kiếm phía máy chủ.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{list.isLoading && (
|
{list.isLoading && (
|
||||||
@ -265,8 +324,8 @@ export function MyContractsPage() {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={FileText}
|
icon={FileText}
|
||||||
title="Không có HĐ"
|
title={emptyTitle}
|
||||||
description={showDeleted ? 'Chưa có HĐ nào bị xóa.' : typeFilter != null ? 'Loại HĐ này chưa có dữ liệu.' : 'Tạo HĐ mới để bắt đầu.'}
|
description={emptyDescription}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user