// 3-panel "Hộp thư" — HĐ chờ role tôi xử lý. Stats row + Panel 1 (list
// pending) | Panel 2 (detail content) | Panel 3 (workflow + lịch sử duyệt).
// Selected qua URL ?id= để bookmarkable.
//
// Khác MyContractsPage: data từ /contracts/inbox (BE đã filter HĐ chờ role
// tôi), thêm 4-card stats overdue/dueSoon/total/totalValue ở header.
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 { ContractDetailContent } from '@/components/contracts/ContractDetailContent'
import { WorkflowHistoryPanel } from '@/components/contracts/WorkflowHistoryPanel'
import { PhaseBadge } from '@/components/PhaseBadge'
import { SlaTimer } from '@/components/SlaTimer'
import { EmptyState } from '@/components/EmptyState'
import { Input } from '@/components/ui/Input'
import { useAuth } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
import { cn } from '@/lib/cn'
import type { ContractDetail, ContractListItem } from '@/types/contracts'
import { ContractTypeLabel } from '@/types/forms'
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
function StatPill({
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 bg-red-50' :
tone === 'warn' ? 'text-amber-600 bg-amber-50' :
'text-brand-600 bg-brand-50'
return (
{label}
{value}
)
}
export function InboxPage() {
const navigate = useNavigate()
const { user } = useAuth()
const [searchParams, setSearchParams] = useSearchParams()
const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : null
const selectedId = searchParams.get('id')
const search = searchParams.get('q') ?? ''
const list = useQuery({
queryKey: ['inbox'],
queryFn: async () => (await api.get('/contracts/inbox')).data,
})
const detail = useQuery({
queryKey: ['contract', selectedId],
queryFn: async () => (await api.get(`/contracts/${selectedId}`)).data,
enabled: !!selectedId,
})
const allRows = list.data ?? []
const rows = useMemo(() => {
let items = allRows
if (typeFilter != null) items = items.filter(c => c.type === typeFilter)
if (search.trim()) {
const q = search.toLowerCase()
items = items.filter(c =>
(c.maHopDong ?? '').toLowerCase().includes(q) ||
(c.tenHopDong ?? '').toLowerCase().includes(q) ||
(c.supplierName ?? '').toLowerCase().includes(q),
)
}
return items
}, [allRows, typeFilter, search])
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])
function selectContract(id: string) {
if (typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches) {
const next = new URLSearchParams(searchParams)
next.set('id', id)
setSearchParams(next, { replace: false })
} else {
navigate(`/contracts/${id}`)
}
}
function clearSelection() {
const next = new URLSearchParams(searchParams)
next.delete('id')
setSearchParams(next, { replace: false })
}
function updateSearch(value: string) {
const next = new URLSearchParams(searchParams)
if (value) next.set('q', value)
else next.delete('q')
setSearchParams(next, { replace: true })
}
const typeLabel = typeFilter != null ? ContractTypeLabel[typeFilter] : null
return (
{/* Header — title + stats inline để Panel 1 max chỗ */}
{user?.roles.length ? (
Vai trò bạn đang xử lý: {user.roles.join(', ')}
) : null}
{/* Panel 1 — Pending list */}
{/* Panel 2 — Detail content */}
{!selectedId && (
)}
{selectedId && detail.isLoading && (
Đang tải HĐ…
)}
{selectedId && detail.data && (
)}
{/* Panel 3 — Workflow + history */}
)
}