// "Tổng quan" cho user — overview KPI cá nhân + recent contracts. Khác Inbox // (chờ duyệt) — Dashboard cho user nhìn nhanh tất cả HĐ liên quan. import { useQuery } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { FileText, CheckCircle2, AlertTriangle, Pencil, Clock, Inbox, Coins } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { PhaseBadge } from '@/components/PhaseBadge' import { SlaTimer } from '@/components/SlaTimer' import { EmptyState } from '@/components/EmptyState' import { Button } from '@/components/ui/Button' import { useAuth } from '@/contexts/AuthContext' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import type { Paged } from '@/types/master' import type { ContractListItem } from '@/types/contracts' import { ContractTypeLabel } from '@/types/forms' import { PeFinalizeChip } from '@/components/pe/PeFinalizeChip' import { getPeDisplayStatus, PeDisplayStatusColor, PeDisplayStatusLabel, type PeListItem, } from '@/types/purchaseEvaluation' type MyDashboard = { draftsInProgress: number pendingMyApproval: number dueSoon: number overdue: number draftsTotalValue: number } const fmtMoney = (v: number) => { if (v >= 1_000_000_000) return (v / 1_000_000_000).toFixed(1) + ' tỷ' if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + ' tr' return v.toLocaleString('vi-VN') } // 2026-06-16 redesign (anh "nâng màu, bắt mắt hơn"): KPI cards now carry a // coloured LEFT rail (.card-accent --accent) + a tinted icon-chip + a big stat // number. Each tone owns a distinct accent from the new palette so the row // scans as 5 colours, not 5 greys. `tone` extended (brand/teal/warn/good/danger // /violet) — StatCard is local to this page, no external call-site. type StatTone = 'default' | 'teal' | 'warn' | 'good' | 'danger' | 'violet' const STAT_TONE: Record = { default: { rail: 'var(--color-brand-500)', chipBg: 'var(--color-brand-50)', chipFg: 'var(--color-brand-600)' }, teal: { rail: 'var(--color-teal-500)', chipBg: 'var(--color-teal-50)', chipFg: 'var(--color-teal-700)' }, warn: { rail: 'var(--color-amberx-500)', chipBg: 'var(--color-amberx-50)', chipFg: 'var(--color-amberx-700)' }, good: { rail: 'var(--color-greenx-500)', chipBg: 'var(--color-greenx-50)', chipFg: 'var(--color-greenx-700)' }, danger: { rail: 'var(--color-accent-500)', chipBg: '#fdecec', chipFg: 'var(--color-accent-600)' }, violet: { rail: 'var(--color-violet-500)', chipBg: 'var(--color-violet-50)', chipFg: 'var(--color-violet-700)' }, } function StatCard({ icon: Icon, label, value, hint, tone = 'default', onClick, }: { icon: React.ComponentType<{ className?: string }> label: string value: React.ReactNode hint?: string tone?: StatTone onClick?: () => void }) { const t = STAT_TONE[tone] return ( ) } export function UserDashboardPage() { const navigate = useNavigate() const { user } = useAuth() const stats = useQuery({ queryKey: ['my-dashboard'], queryFn: async () => (await api.get('/reports/my-dashboard')).data, }) const recent = useQuery({ queryKey: ['my-contracts-recent'], queryFn: async () => (await api.get>('/contracts', { params: { page: 1, pageSize: 5 } })).data, }) // [S89 — anh Kiệt FDC] 5 Phiếu Duyệt NCC gần đây, kèm chip "Quy trình kết thúc" // (PeFinalizeChip) để user nhìn nhanh phiếu nào duyệt-đến-đây-là-xong không lên CEO. const recentPe = useQuery({ queryKey: ['my-pe-recent'], queryFn: async () => (await api.get>('/purchase-evaluations', { params: { page: 1, pageSize: 5 } })).data, }) const s = stats.data return (

Của tôi

navigate('/my-contracts')} /> navigate('/inbox')} /> navigate('/inbox')} /> navigate('/inbox')} /> navigate('/my-contracts')} />

HĐ gần đây

{recent.isLoading && (
{Array.from({ length: 3 }).map((_, i) => (
))}
)} {!recent.isLoading && (recent.data?.items?.length ?? 0) === 0 && (
navigate('/contracts/new')}> Tạo HĐ mới } />
)}
    {recent.data?.items?.map(c => (
  • ))}
{/* [S89 — anh Kiệt FDC] Phiếu Duyệt NCC gần đây — mỗi row có PhaseBadge (PE display-status) + PeFinalizeChip ("quy trình kết thúc, không lên CEO") + tên gói + mã phiếu. PE phase ≠ Contract phase nên dùng PE display-status pill. */}

Phiếu Duyệt NCC gần đây

{recentPe.isLoading && (
{Array.from({ length: 3 }).map((_, i) => (
))}
)} {!recentPe.isLoading && (recentPe.data?.items?.length ?? 0) === 0 && (
)}
    {recentPe.data?.items?.map(p => (
  • ))}
) }