diff --git a/app/pax8/page.tsx b/app/pax8/page.tsx new file mode 100644 index 0000000..48f286f --- /dev/null +++ b/app/pax8/page.tsx @@ -0,0 +1,208 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { PageHeader } from '@/components/navigation/page-header'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; +import DataTable, { type Column } from '@/components/admin/DataTable'; +import DetailModal from '@/components/admin/DetailModal'; + +interface Pax8CompanyListItem { + id: string; + name: string; + status: string | null; + city: string | null; + stateOrProvince: string | null; + country: string | null; + autotaskCompanyId: number | null; + matchConfidence: number | null; + matchMethod: string | null; + matchedCompanyName: string | null; + activeSubscriptionCount: number; +} + +// Maps DataTable column keys (what the table's onSort callback reports) to +// the /api/pax8/companies `sort` query values (see 14-01-SUMMARY.md). +const SORT_KEY_MAP: Record = { + name: 'name', + matched: 'match', + activeSubscriptionCount: 'subscriptions', + location: 'city', +}; + +const PAGE_SIZE = 25; + +export default function Pax8Page() { + const [activeTab, setActiveTab] = useState<'companies' | 'needs-review'>('companies'); + + const [companies, setCompanies] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [sortColumn, setSortColumn] = useState(undefined); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | undefined>(undefined); + const [searchQuery, setSearchQuery] = useState(undefined); + + const [selectedCompany, setSelectedCompany] = useState | null>(null); + const [modalOpen, setModalOpen] = useState(false); + + const fetchCompanies = async ( + currentPage: number, + search?: string, + sortBy?: string, + order?: 'asc' | 'desc' + ) => { + setIsLoading(true); + try { + const offset = (currentPage - 1) * PAGE_SIZE; + const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(offset) }); + if (search) params.append('search', search); + if (sortBy) params.append('sort', SORT_KEY_MAP[sortBy] ?? sortBy); + if (order) params.append('order', order); + + const res = await fetch(`/api/pax8/companies?${params}`); + const data = await res.json(); + setCompanies(data.items ?? []); + setTotalCount(data.total ?? 0); + } catch (err) { + console.error('Failed to fetch PAX8 companies:', err); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchCompanies(page, searchQuery, sortColumn, sortOrder); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + const handleSort = (column: string, direction: 'asc' | 'desc') => { + setSortColumn(column); + setSortOrder(direction); + setPage(1); + fetchCompanies(1, searchQuery, column, direction); + }; + + const handleSearch = (query: string) => { + setSearchQuery(query); + setPage(1); + fetchCompanies(1, query, sortColumn, sortOrder); + }; + + const handlePageChange = (nextPage: number) => { + setPage(nextPage); + fetchCompanies(nextPage, searchQuery, sortColumn, sortOrder); + }; + + // Fetch-then-open: keeps the loading indicator on the DataTable (the row's + // parent) rather than inside an already-open, empty DetailModal. + const handleRowClick = async (row: Pax8CompanyListItem) => { + setIsLoading(true); + try { + const res = await fetch(`/api/pax8/companies/${row.id}`); + const data = await res.json(); + setSelectedCompany({ + ...data.company, + subscriptions: data.subscriptions, + costTotal: data.costTotal, + }); + setModalOpen(true); + } catch (err) { + console.error('Failed to fetch PAX8 company detail:', err); + } finally { + setIsLoading(false); + } + }; + + const columns: Column[] = [ + { + key: 'name', + label: 'Name', + sortable: true, + render: (value: string) => {value}, + }, + { + key: 'matched', + label: 'Matched Company', + sortable: true, + render: (_value, row) => + row.matchedCompanyName ? ( + {row.matchedCompanyName} + ) : ( + Unmatched + ), + }, + { + key: 'activeSubscriptionCount', + label: 'Subs', + sortable: true, + render: (value: number) => {value ?? 0}, + }, + { + key: 'location', + label: 'Location', + sortable: true, + render: (_value, row) => { + const parts = [row.city, row.stateOrProvince, row.country].filter(Boolean); + return parts.length > 0 ? ( + parts.join(', ') + ) : ( + — + ); + }, + }, + ]; + + return ( + <> + +
+ setActiveTab(v as 'companies' | 'needs-review')} + className="space-y-6" + > + + Companies + Needs Review + + + + + + + + {/* Needs Review tab implemented in Plan 14-05 */} +

Loading…

+
+
+
+ + + + ); +}