"use client" import * as React from 'react' import { useRouter } from 'next/navigation' import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy' import type { SortingState } from '@tanstack/react-table' import { Page, PageBody } from '@open-mercato/ui/backend/Page' import { DataTable } from '@open-mercato/ui/backend/DataTable' import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail' import { Avatar } from '@open-mercato/ui/primitives/avatar' import { agentAvatarIcon } from '../../components/agentChips' import { useAgentIconMap } from '../../components/useAgentIcons' import { StatusBadge } from '@open-mercato/ui/primitives/status-badge' import { SearchInput } from '@open-mercato/ui/primitives/search-input' import { apiCall } from '@open-mercato/ui/backend/utils/apiCall' import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent' import { cn } from '@open-mercato/shared/lib/utils' import { useT } from '@open-mercato/shared/lib/i18n/context' import { formatCostMinor, formatRelativeAge } from '../../components/types' import { useCoalescedReload } from '../../components/useCoalescedReload' import { PROCESS_HEADER_SORT_FIELDS, serverSortToSorting, sortingToServerSort, type ServerSort, } from '../../components/serverSort' import { mapProcessListRow, PROCESS_STATUS_LABEL_KEY, PROCESS_STATUS_TONE, type ProcessListRow, } from '../../components/processTypes' type Facet = 'all' | 'needs_decision' | 'stuck' | 'high_value' | 'fraud' /** Facet tab → server-side list scope (spec 2026-06-25 §API Contracts). */ const FACET_SCOPE: Record, string> = { needs_decision: 'needs_decision', stuck: 'stuck_24h', high_value: 'high_value', fraud: 'fraud_flagged', } const FACETS: Facet[] = ['all', 'needs_decision', 'stuck', 'high_value', 'fraud'] type ListResponse = { items?: Array> total?: number totalPages?: number totalIsCapped?: boolean } function listPath( facet: Facet, page: number, pageSize: number, q: string, sort: ServerSort | null, ): string { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }) if (facet !== 'all') params.set('scope', FACET_SCOPE[facet]) if (q) params.set('q', q) if (sort) { params.set('sortField', sort.field) params.set('sortDir', sort.dir) } return `/api/agent_orchestrator/executions?${params.toString()}` } export default function ProcessesListPage() { const t = useT() const router = useRouter() const [facet, setFacet] = React.useState('all') const [page, setPage] = React.useState(1) const [pageSize, setPageSize] = React.useState(50) const [rows, setRows] = React.useState([]) const [total, setTotal] = React.useState(0) const [totalIsCapped, setTotalIsCapped] = React.useState(false) const [totalPages, setTotalPages] = React.useState(1) const [facetCounts, setFacetCounts] = React.useState>>({}) const [isLoading, setIsLoading] = React.useState(true) const [error, setError] = React.useState(null) const [search, setSearch] = React.useState('') // Debounced 300 ms before hitting the server `q` (subject reference, or the // process id the identifier column shows when there is no subject). const [q, setQ] = React.useState('') // Header sort → server sortField/sortDir; null = the route's default order. const [sort, setSort] = React.useState(null) React.useEffect(() => { const handle = setTimeout(() => setQ(search.trim()), 300) return () => clearTimeout(handle) }, [search]) React.useEffect(() => { setPage(1) }, [q, sort]) const reload = React.useCallback(async () => { setError(null) const [listCall, ...countCalls] = await Promise.all([ apiCall(listPath(facet, page, pageSize, q, sort), undefined, { fallback: {} }), // Facet counts share the search filter so the tab badges always describe // the same result set the table shows. ...FACETS.map((tab) => apiCall(listPath(tab, 1, 1, q, null), undefined, { fallback: {} }), ), ]) if (!listCall.ok) { setError(t('agent_orchestrator.process.list.error')) setIsLoading(false) return } const items = Array.isArray(listCall.result?.items) ? listCall.result.items : [] setRows(items.map(mapProcessListRow).filter((row): row is ProcessListRow => !!row)) setTotal(typeof listCall.result?.total === 'number' ? listCall.result.total : items.length) setTotalIsCapped(listCall.result?.totalIsCapped === true) setTotalPages(typeof listCall.result?.totalPages === 'number' ? listCall.result.totalPages : 1) const counts: Partial> = {} FACETS.forEach((tab, index) => { const call = countCalls[index] if (call?.ok && typeof call.result?.total === 'number') counts[tab] = call.result.total }) setFacetCounts(counts) setIsLoading(false) }, [facet, page, pageSize, q, sort, t]) React.useEffect(() => { setIsLoading(true) void reload() }, [reload]) const coalescedReload = useCoalescedReload(reload) useAppEvent('agent_orchestrator.process.updated', () => { coalescedReload() }) const agentIcons = useAgentIconMap() const columns = React.useMemo[]>( () => [ { accessorKey: 'subjectLabel', header: t('agent_orchestrator.process.list.col.claim'), // No entry in the route's sortFieldMap — a header sort would only // reorder the visible page, which the audit flagged as a lying sort. enableSorting: false, cell: ({ row }) => (
{row.original.subjectLabel}
{row.original.subjectTitle}
), }, { accessorKey: 'subjectType', header: t('agent_orchestrator.process.list.col.type'), enableSorting: false, cell: ({ row }) => {row.original.subjectType}, }, { accessorKey: 'currentStage', header: t('agent_orchestrator.process.list.col.stage'), enableSorting: false, cell: ({ row }) => {row.original.currentStage}, }, { accessorKey: 'agentIds', header: t('agent_orchestrator.process.list.col.agents'), enableSorting: false, cell: ({ row }) => (
{row.original.agentIds.slice(0, 4).map((agent) => { const info = agentIcons.get(agent) return ( ) })} {row.original.agentIds.length > 4 ? ( +{row.original.agentIds.length - 4} ) : null}
), }, { accessorKey: 'status', header: t('agent_orchestrator.process.list.col.status'), cell: ({ row }) => ( {t(PROCESS_STATUS_LABEL_KEY[row.original.status])} ), }, { accessorKey: 'openedAt', header: t('agent_orchestrator.process.list.col.age'), cell: ({ row }) => ( {formatRelativeAge(row.original.openedAt) ?? '—'} ), }, { accessorKey: 'costMinor', header: t('agent_orchestrator.process.list.col.cost'), cell: ({ row }) => ( {formatCostMinor(row.original.costMinor, row.original.currency) ?? '—'} ), }, ], [t, agentIcons], ) return (

{t('agent_orchestrator.process.list.title')}

{t('agent_orchestrator.process.list.subtitle')}

{t('agent_orchestrator.process.list.searchHint')}

{FACETS.map((tab) => { const active = facet === tab const label = t(`agent_orchestrator.process.facet.${ tab === 'all' ? 'all' : tab === 'needs_decision' ? 'needsDecision' : tab === 'stuck' ? 'stuck' : tab === 'high_value' ? 'highValue' : 'fraud' }`) return ( ) })}
{isLoading ? ( ) : error ? ( ) : rows.length === 0 ? (

{q ? t('agent_orchestrator.process.list.searchEmpty') : facet !== 'all' ? t('agent_orchestrator.process.list.facetEmpty') : t('agent_orchestrator.process.list.empty')}

) : ( columns={columns} data={rows} sortable manualSorting sorting={serverSortToSorting(sort, PROCESS_HEADER_SORT_FIELDS)} onSortingChange={(next: SortingState) => setSort(sortingToServerSort(next, PROCESS_HEADER_SORT_FIELDS)) } onRowClick={(row) => router.push(`/backend/processes/${encodeURIComponent(row.id)}`)} pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage, pageSizeOptions: [20, 50, 100], onPageSizeChange: (next) => { setPageSize(next) setPage(1) }, }} /> )}
) }