// Đề xuất danh sách — Phase 10.3 G-O3 (S37 2026-05-28). // Table 6 cột với Status badge color + filter status + search. // File MIRROR SHA256 identical với fe-user counterpart. import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useNavigate, useSearchParams } from 'react-router-dom' import { Plus, Search } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { PROPOSAL_STATUS_BADGE, PROPOSAL_STATUS_LABELS, type PagedResult, type ProposalListItemDto, type ProposalStatusValue, } from '@/types/proposal' const PAGE_SIZE = 20 function formatVnd(n: number | null): string { if (n === null || n === undefined) return '—' return n.toLocaleString('vi-VN') + ' đ' } function formatDate(iso: string): string { const d = new Date(iso) return d.toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric' }) } export function ProposalsListPage() { const navigate = useNavigate() const [searchParams] = useSearchParams() const initialStatus = searchParams.get('status') const initialInbox = searchParams.get('inboxOnly') === 'true' const [status, setStatus] = useState(initialStatus ? Number(initialStatus) : null) const [inboxOnly, setInboxOnly] = useState(initialInbox) const [search, setSearch] = useState('') const [page, setPage] = useState(1) const list = useQuery({ queryKey: ['proposals', { status, inboxOnly, search, page }], queryFn: async () => (await api.get>('/proposals', { params: { status: status || undefined, inboxOnly: inboxOnly || undefined, search: search.trim() || undefined, page, pageSize: PAGE_SIZE, }, })).data, }) const items = list.data?.items ?? [] const total = list.data?.total ?? 0 const totalPages = list.data?.totalPages ?? 1 const statusOptions: Array<{ value: number | null; label: string }> = useMemo( () => [ { value: null, label: 'Tất cả' }, { value: 1, label: PROPOSAL_STATUS_LABELS[1] }, { value: 2, label: PROPOSAL_STATUS_LABELS[2] }, { value: 3, label: PROPOSAL_STATUS_LABELS[3] }, { value: 4, label: PROPOSAL_STATUS_LABELS[4] }, { value: 5, label: PROPOSAL_STATUS_LABELS[5] }, ], [], ) return (
navigate('/proposals/new')}> Tạo Đề xuất } />
{statusOptions.map((opt) => ( ))}
{ setSearch(e.target.value) setPage(1) }} placeholder="Tìm mã hoặc tiêu đề..." className="w-64" />
{list.isLoading && ( )} {!list.isLoading && items.length === 0 && ( )} {items.map((p) => ( navigate(`/proposals/${p.id}`)} className="cursor-pointer border-b transition hover:bg-accent/50" > ))}
Tiêu đề Trạng thái Số tiền dự kiến Người soạn Ngày tạo
Đang tải...
Chưa có đề xuất nào.
{p.maDeXuat ?? '—'} {p.title} {PROPOSAL_STATUS_LABELS[p.status as ProposalStatusValue]} {p.currentApprovalLevelOrder && ( Cấp {p.currentApprovalLevelOrder} )} {formatVnd(p.amountEstimate)} {p.drafterFullName ?? '—'} {formatDate(p.createdAt)}
{totalPages > 1 && (
{total} đề xuất — Trang {page} / {totalPages}
)}
) }