From 332a90f601b41dd6f637edf08f4880857f67e3ce Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 6 May 2026 15:57:24 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User:=20Inbox=20th=C3=AAm=20secti?= =?UTF-8?q?on=20Phi=E1=BA=BFu=20Duy=E1=BB=87t=20NCC=20ch=E1=BB=9D=20t?= =?UTF-8?q?=C3=B4i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional polish (HANDOFF §C — "khi UAT phát sinh"). Drafter + TPB sẽ thấy HĐ + Phiếu PE pending cùng InboxPage thay vì phải vào /purchase-evaluations riêng. Changes: - useQuery thứ 2 cho /purchase-evaluations/inbox (endpoint đã sẵn) - peRows filter theo search query (mã / tên gói thầu / project) - Stats overdue/dueSoon đếm cả PE rows. totalValue chỉ HĐ (PE không có giá trị). - Panel 1 chia 2 section sticky header: - "Hợp đồng (N)" — giữ behavior cũ, click → inline detail Panel 2 - "Phiếu Duyệt NCC (M)" — click → navigate /purchase-evaluations/:id (page riêng, không inline vì PE entity shape khác Contract) - EmptyState mới: "Không có HĐ hoặc Phiếu Duyệt NCC nào chờ" Note: chỉ fe-user (đối tượng dùng Inbox), fe-admin có /system/inbox riêng nếu cần — defer. Build: fe-user pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-user/src/pages/InboxPage.tsx | 169 +++++++++++++++++++++++++------- 1 file changed, 131 insertions(+), 38 deletions(-) diff --git a/fe-user/src/pages/InboxPage.tsx b/fe-user/src/pages/InboxPage.tsx index 5ee4a38..2eb6b74 100644 --- a/fe-user/src/pages/InboxPage.tsx +++ b/fe-user/src/pages/InboxPage.tsx @@ -7,7 +7,7 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { useNavigate, useSearchParams } from 'react-router-dom' -import { Inbox, AlertTriangle, Clock, FileText, Search, X } from 'lucide-react' +import { Inbox, AlertTriangle, Clock, FileText, Search, X, ClipboardList } from 'lucide-react' import { ContractDetailContent } from '@/components/contracts/ContractDetailContent' import { WorkflowHistoryPanel } from '@/components/contracts/WorkflowHistoryPanel' import { PhaseBadge } from '@/components/PhaseBadge' @@ -19,6 +19,12 @@ import { api } from '@/lib/api' import { cn } from '@/lib/cn' import type { ContractDetail, ContractListItem } from '@/types/contracts' import { ContractTypeLabel } from '@/types/forms' +import { + PurchaseEvaluationPhaseColor, + PurchaseEvaluationPhaseLabel, + PurchaseEvaluationTypeLabel, + type PeListItem, +} from '@/types/purchaseEvaluation' const fmtMoney = (v: number) => v.toLocaleString('vi-VN') @@ -62,6 +68,14 @@ export function InboxPage() { queryFn: async () => (await api.get('/contracts/inbox')).data, }) + // PE inbox — phiếu Duyệt NCC chờ user xử lý (Optional polish §6.B HANDOFF). + // Click row → navigate /purchase-evaluations/:id thay vì inline detail (đỡ + // duplicate ContractDetailContent component cho PE entity khác shape). + const peList = useQuery({ + queryKey: ['pe-inbox'], + queryFn: async () => (await api.get('/purchase-evaluations/inbox')).data, + }) + const detail = useQuery({ queryKey: ['contract', selectedId], queryFn: async () => (await api.get(`/contracts/${selectedId}`)).data, @@ -84,6 +98,19 @@ export function InboxPage() { return items }, [allRows, typeFilter, search]) + const peRows = useMemo(() => { + let items = peList.data ?? [] + if (search.trim()) { + const q = search.toLowerCase() + items = items.filter(p => + (p.maPhieu ?? '').toLowerCase().includes(q) || + (p.tenGoiThau ?? '').toLowerCase().includes(q) || + (p.projectName ?? '').toLowerCase().includes(q), + ) + } + return items + }, [peList.data, search]) + const stats = useMemo(() => { const now = Date.now() const dayMs = 24 * 60 * 60 * 1000 @@ -97,8 +124,15 @@ export function InboxPage() { if (diff < 0) overdue++ else if (diff < dayMs) dueSoon++ } - return { total: rows.length, overdue, dueSoon, totalValue } - }, [rows]) + // PE rows tham gia stats overdue/dueSoon (KHÔNG totalValue — PE không có giá trị HĐ) + for (const p of peRows) { + if (!p.slaDeadline) continue + const diff = new Date(p.slaDeadline).getTime() - now + if (diff < 0) overdue++ + else if (diff < dayMs) dueSoon++ + } + return { total: rows.length + peRows.length, overdue, dueSoon, totalValue } + }, [rows, peRows]) function selectContract(id: string) { if (typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches) { @@ -171,7 +205,7 @@ export function InboxPage() { ))} )} - {!list.isLoading && rows.length === 0 && ( + {!list.isLoading && rows.length === 0 && peRows.length === 0 && (
)} -
    - {rows.map(c => { - const overdue = c.slaDeadline && new Date(c.slaDeadline).getTime() < Date.now() - return ( -
  • - -
  • - ) - })} -
+ + + ) + })} + + + )} + + {/* Section 2 — Phiếu Duyệt NCC chờ tôi (PE) */} + {peRows.length > 0 && ( + <> +
+ + Phiếu Duyệt NCC ({peRows.length}) +
+
    + {peRows.map(p => { + const overdue = p.slaDeadline && new Date(p.slaDeadline).getTime() < Date.now() + return ( +
  • + +
  • + ) + })} +
+ + )}