[CLAUDE] FE-User: stat cards trên Inbox (total, sắp hạn, quá hạn, tổng giá trị)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled

This commit is contained in:
pqhuy1987
2026-04-21 15:05:42 +07:00
parent 290936a0ca
commit 0e5b5cd670

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { Inbox } from 'lucide-react'
import { Inbox, AlertTriangle, Clock, FileText } from 'lucide-react'
import { PageHeader } from '@/components/PageHeader'
import { DataTable, type Column } from '@/components/DataTable'
import { PhaseBadge } from '@/components/PhaseBadge'
@ -12,6 +13,30 @@ import { ContractTypeLabel } from '@/types/forms'
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
function StatCard({
icon: Icon,
label,
value,
tone = 'default',
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
tone?: 'default' | 'warn' | 'danger'
}) {
const toneClass =
tone === 'danger' ? 'text-red-600' : tone === 'warn' ? 'text-amber-600' : 'text-brand-600'
return (
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex items-center justify-between">
<div className="text-xs font-medium text-slate-500">{label}</div>
<Icon className={`h-4 w-4 ${toneClass}`} />
</div>
<div className="mt-2 text-2xl font-bold text-slate-900">{value}</div>
</div>
)
}
export function InboxPage() {
const navigate = useNavigate()
const { user } = useAuth()
@ -21,14 +46,58 @@ export function InboxPage() {
queryFn: async () => (await api.get<ContractListItem[]>('/contracts/inbox')).data,
})
const rows = list.data ?? []
const stats = useMemo(() => {
const now = Date.now()
const dayMs = 24 * 60 * 60 * 1000
let overdue = 0
let dueSoon = 0
let totalValue = 0
for (const c of rows) {
totalValue += c.giaTri ?? 0
if (!c.slaDeadline) continue
const diff = new Date(c.slaDeadline).getTime() - now
if (diff < 0) overdue++
else if (diff < dayMs) dueSoon++
}
return { total: rows.length, overdue, dueSoon, totalValue }
}, [rows])
const columns: Column<ContractListItem>[] = [
{ key: 'maHopDong', header: 'Mã HĐ', width: 'w-48', render: c => <span className="font-mono text-xs">{c.maHopDong ?? '—'}</span> },
{
key: 'maHopDong',
header: 'Mã HĐ',
width: 'w-48',
render: c => <span className="font-mono text-xs">{c.maHopDong ?? '—'}</span>,
},
{ key: 'tenHopDong', header: 'Tên HĐ', render: c => c.tenHopDong ?? '—' },
{ key: 'type', header: 'Loại', width: 'w-32', render: c => ContractTypeLabel[c.type] ?? '—' },
{ key: 'phase', header: 'Phase hiện tại', width: 'w-36', render: c => <PhaseBadge phase={c.phase} /> },
{
key: 'type',
header: 'Loại',
width: 'w-32',
render: c => ContractTypeLabel[c.type] ?? '—',
},
{
key: 'phase',
header: 'Phase hiện tại',
width: 'w-36',
render: c => <PhaseBadge phase={c.phase} />,
},
{ key: 'supplierName', header: 'NCC', render: c => c.supplierName },
{ key: 'giaTri', header: 'Giá trị', align: 'right', width: 'w-32', render: c => fmtMoney(c.giaTri) },
{ key: 'slaDeadline', header: 'SLA', width: 'w-40', render: c => <SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} /> },
{
key: 'giaTri',
header: 'Giá trị',
align: 'right',
width: 'w-32',
render: c => fmtMoney(c.giaTri),
},
{
key: 'slaDeadline',
header: 'SLA',
width: 'w-40',
render: c => <SlaTimer deadline={c.slaDeadline} createdAt={c.createdAt} />,
},
]
return (
@ -43,9 +112,16 @@ export function InboxPage() {
description={`HĐ chờ vai trò ${user?.roles.join(', ') ?? ''} xử lý. Click row để xem chi tiết.`}
/>
<div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-4">
<StatCard icon={Inbox} label="Cần xử lý" value={stats.total} />
<StatCard icon={Clock} label="Sắp quá hạn (24h)" value={stats.dueSoon} tone="warn" />
<StatCard icon={AlertTriangle} label="Đã quá hạn" value={stats.overdue} tone="danger" />
<StatCard icon={FileText} label="Tổng giá trị" value={fmtMoney(stats.totalValue)} />
</div>
<DataTable
columns={columns}
rows={list.data ?? []}
rows={rows}
getRowKey={c => c.id}
isLoading={list.isLoading}
empty="Không có HĐ nào chờ bạn xử lý."