// 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ỗ */}

Hộp thư {typeLabel && · {typeLabel}}

{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 */}
) }