// Generic Workflow Apps List Page — Phase 10.3 G-O4+G-O5+G-O6 (S38 2026-05-28). // Wave 3a (S42 2026-05-30): row click → Detail page (workflow actions + opinion timeline). // Re-skin S69 (2026-06-17): PURO layout (ui/PageHeader teal + KpiCard status-filter row) // + Hồ sơ Nhân sự visual language (accent rail card, slate table chrome). Status filter is // a CLIENT-SIDE view over the already-fetched items (no query/endpoint/navigation change). // Handles 4 module via URL `:kind` param: leave / ot / travel / vehicle. // File MIRROR SHA256 identical fe-user counterpart. import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useNavigate, useParams } from 'react-router-dom' import { CalendarOff, Clock, Plane, Car, FileSignature, Layers, Send, RotateCcw, CheckCircle2 } from 'lucide-react' import { PageHeader } from '@/components/ui/PageHeader' import { KpiCard } from '@/components/ui/KpiCard' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { WORKFLOW_APP_STATUS_BADGE, WORKFLOW_APP_STATUS_LABELS, WorkflowAppStatus, type PagedResult, } from '@/types/workflowApps' type Kind = 'leave' | 'ot' | 'travel' | 'vehicle' const KIND_CONFIG: Record React.ReactNode }> }> = { leave: { title: 'Đơn xin nghỉ phép', description: 'Danh sách đơn nghỉ phép — Workflow V2 ApplicableType=5', endpoint: '/leave-requests', columns: [ { key: 'maDonTu', label: 'Mã', render: (x) => {x.maDonTu ?? '—'} }, { key: 'requesterFullName', label: 'Người xin', render: (x) => x.requesterFullName }, { key: 'startDate', label: 'Từ', render: (x) => new Date(x.startDate).toLocaleDateString('vi-VN') }, { key: 'endDate', label: 'Đến', render: (x) => new Date(x.endDate).toLocaleDateString('vi-VN') }, { key: 'numDays', label: 'Ngày', render: (x) => x.numDays }, { key: 'reason', label: 'Lý do', render: (x) => {x.reason} }, ], }, ot: { title: 'Đơn đăng ký OT', description: 'Danh sách đơn OT — Workflow V2 ApplicableType=6', endpoint: '/ot-requests', columns: [ { key: 'maDonTu', label: 'Mã', render: (x) => {x.maDonTu ?? '—'} }, { key: 'requesterFullName', label: 'Người xin', render: (x) => x.requesterFullName }, { key: 'otDate', label: 'Ngày OT', render: (x) => new Date(x.otDate).toLocaleDateString('vi-VN') }, { key: 'hours', label: 'Giờ', render: (x) => x.hours }, { key: 'reason', label: 'Lý do', render: (x) => {x.reason} }, ], }, travel: { title: 'Đơn đi công tác', description: 'Danh sách đăng ký công tác', endpoint: '/travel-requests', columns: [ { key: 'maDonTu', label: 'Mã', render: (x) => {x.maDonTu ?? '—'} }, { key: 'requesterFullName', label: 'Người xin', render: (x) => x.requesterFullName }, { key: 'destination', label: 'Địa điểm', render: (x) => x.destination }, { key: 'startDate', label: 'Từ', render: (x) => new Date(x.startDate).toLocaleDateString('vi-VN') }, { key: 'endDate', label: 'Đến', render: (x) => new Date(x.endDate).toLocaleDateString('vi-VN') }, { key: 'purpose', label: 'Mục đích', render: (x) => {x.purpose} }, ], }, vehicle: { title: 'Đặt xe công', description: 'Danh sách booking xe — Workflow V2 ApplicableType=7', endpoint: '/vehicle-bookings', columns: [ { key: 'maDonTu', label: 'Mã', render: (x) => {x.maDonTu ?? '—'} }, { key: 'requesterFullName', label: 'Người đặt', render: (x) => x.requesterFullName }, { key: 'vehicleLicense', label: 'Biển số', render: (x) => {x.vehicleLicense} }, { key: 'destination', label: 'Đến', render: (x) => x.destination }, { key: 'startAt', label: 'Bắt đầu', render: (x) => new Date(x.startAt).toLocaleString('vi-VN') }, { key: 'driverName', label: 'Tài xế', render: (x) => x.driverName ?? '—' }, ], }, } const ICON_MAP: Record = { leave: CalendarOff, ot: Clock, travel: Plane, vehicle: Car, } // Status filter chips (presentation): null = Tất cả. Each maps to a KpiCard accent. type StatusFilter = number | null export function WorkflowAppsListPage() { const { kind = 'leave' } = useParams<{ kind: Kind }>() const navigate = useNavigate() const config = KIND_CONFIG[kind as Kind] const Icon = ICON_MAP[kind as Kind] ?? FileSignature // Client-side status filter — a view over the fetched list (no extra query / endpoint). const [statusFilter, setStatusFilter] = useState(null) const list = useQuery({ queryKey: [config.endpoint, { page: 1 }], queryFn: async () => (await api.get>(config.endpoint, { params: { page: 1, pageSize: 50 } })).data, enabled: !!config, }) const items = list.data?.items ?? [] // Counts per status + the visible (filtered) rows — derived presentation only. const counts = useMemo(() => { const c = { all: items.length, submitted: 0, returned: 0, approved: 0 } for (const it of items) { if (it.status === WorkflowAppStatus.DaGuiDuyet) c.submitted++ else if (it.status === WorkflowAppStatus.TraLai) c.returned++ else if (it.status === WorkflowAppStatus.DaDuyet) c.approved++ } return c }, [items]) const visibleItems = useMemo( () => (statusFilter == null ? items : items.filter((it: any) => it.status === statusFilter)), [items, statusFilter], ) if (!config) { return
Module không tồn tại: {kind}
} return (
} accent="teal" /> {/* Status filter — row of KpiCards (PURO pattern, replaces tabs) */}
} accent="teal" active={statusFilter == null} onClick={() => setStatusFilter(null)} /> } accent="amberx" active={statusFilter === WorkflowAppStatus.DaGuiDuyet} onClick={() => setStatusFilter(WorkflowAppStatus.DaGuiDuyet)} /> } accent="violet" active={statusFilter === WorkflowAppStatus.TraLai} onClick={() => setStatusFilter(WorkflowAppStatus.TraLai)} /> } accent="greenx" active={statusFilter === WorkflowAppStatus.DaDuyet} onClick={() => setStatusFilter(WorkflowAppStatus.DaDuyet)} />
{config.columns.map((c) => ( ))} {list.isLoading && ( )} {!list.isLoading && visibleItems.length === 0 && ( )} {visibleItems.map((item: any) => ( navigate(`/workflow-apps/${kind}/${item.id}`)} > {config.columns.map((c) => ( ))} ))}
{c.label}Trạng thái
Đang tải...
{items.length === 0 ? 'Chưa có dữ liệu.' : 'Không có đơn nào ở trạng thái này.'}
{c.render(item)} {WORKFLOW_APP_STATUS_LABELS[item.status]}
) }