import { DataTable } from '@/components/ui'; import { PlusIcon, XMarkIcon, CheckIcon } from '@heroicons/react/24/outline'; import { ColumnDef } from '@tanstack/react-table'; import { useCallback, useMemo, useState, useEffect } from 'react'; import { useExchanges } from '../hooks/useExchanges'; import { Exchange, ProviderMapping } from '../types'; import { AddProviderMappingDialog } from './AddProviderMappingDialog'; export function ExchangesTable() { const { exchanges, loading, error, updateExchange, fetchExchangeDetails, fetchProviderMappings, updateProviderMapping, createProviderMapping, refetch } = useExchanges(); const [editingCell, setEditingCell] = useState<{ id: string; field: string } | null>(null); const [editValue, setEditValue] = useState(''); const [addProviderDialog, setAddProviderDialog] = useState<{ exchangeId: string; exchangeName: string; } | null>(null); const [expandedRows, setExpandedRows] = useState>(new Set()); const [expandedRowData, setExpandedRowData] = useState>({}); const handleCellEdit = useCallback( async (id: string, field: string, value: string) => { if (field === 'name') { await updateExchange(id, { name: value }); } setEditingCell(null); setEditValue(''); }, [updateExchange] ); const handleToggleActive = useCallback( async (id: string, currentStatus: boolean) => { await updateExchange(id, { active: !currentStatus }); }, [updateExchange] ); const handleAddProviderMapping = useCallback(async (exchangeId: string, exchangeName: string) => { setAddProviderDialog({ exchangeId, exchangeName }); }, []); const handleToggleProviderMapping = useCallback( async (mappingId: string, currentStatus: boolean) => { const success = await updateProviderMapping(mappingId, { active: !currentStatus }); if (success) { refetch(); } }, [updateProviderMapping, refetch] ); const handleToggleExpandRow = useCallback(async (rowId: string) => { setExpandedRows(prev => { const next = new Set(prev); if (next.has(rowId)) { next.delete(rowId); } else { next.add(rowId); // Load provider mappings for this exchange if (!expandedRowData[rowId]) { fetchExchangeDetails(rowId).then(details => { if (details) { setExpandedRowData(prev => ({ ...prev, [rowId]: details.provider_mappings })); } }); } } return next; }); }, [fetchExchangeDetails, expandedRowData]); const columns = useMemo[]>(() => { return [ { id: 'expand', header: '', size: 30, enableResizing: false, cell: ({ row }) => { const isExpanded = expandedRows.has(row.original.id); return ( ); }, }, { id: 'id', header: 'ID', accessorKey: 'id', size: 50, enableResizing: false, cell: ({ getValue, cell }) => ( {getValue() as string} ), }, { id: 'code', header: 'Code', accessorKey: 'code', size: 80, enableResizing: false, cell: ({ getValue, cell }) => ( {getValue() as string} ), }, { id: 'name', header: 'Name', accessorKey: 'name', size: 200, maxSize: 300, enableResizing: true, cell: ({ getValue, row, cell }) => { const isEditing = editingCell?.id === row.original.id && editingCell?.field === 'name'; if (isEditing) { return ( setEditValue(e.target.value)} onBlur={() => handleCellEdit(row.original.id, 'name', editValue)} onKeyDown={e => { if (e.key === 'Enter') { handleCellEdit(row.original.id, 'name', editValue); } else if (e.key === 'Escape') { setEditingCell(null); setEditValue(''); } }} className="w-full bg-surface border border-border rounded px-2 py-1 text-sm" autoFocus /> ); } return (
{ setEditingCell({ id: row.original.id, field: 'name' }); setEditValue(getValue() as string); }} > {getValue() as string}
); }, }, { id: 'country', header: 'Country', accessorKey: 'country', size: 80, maxSize: 80, cell: ({ getValue }) => ( {getValue() as string} ), }, { id: 'currency', header: 'Currency', accessorKey: 'currency', size: 70, cell: ({ getValue, cell }) => ( {getValue() as string} ), }, { id: 'active', header: 'Active', accessorKey: 'active', size: 80, maxSize: 80, cell: ({ getValue, row, cell }) => { const isActive = getValue() as boolean; return ( ); }, }, { id: 'provider_mappings', header: 'Provider Mappings', accessorKey: 'provider_mapping_count', size: 150, cell: ({ getValue, row }) => { const totalMappings = parseInt(getValue() as string) || 0; const activeMappings = parseInt(row.original.active_mapping_count) || 0; const verifiedMappings = parseInt(row.original.verified_mapping_count) || 0; const providers = row.original.providers; return (
{totalMappings} total
{activeMappings} active ✓ {verifiedMappings} verified
{providers && (
{providers}
)}
); }, }, { id: 'actions', header: 'Actions', size: 100, cell: ({ row }) => ( ), }, { id: 'updated_at', header: 'Last Updated', accessorKey: 'updated_at', size: 120, maxSize: 120, cell: ({ getValue }) => ( {new Date(getValue() as string).toLocaleDateString()} ), }, ]; }, [ editingCell, editValue, expandedRows, handleCellEdit, handleToggleActive, handleAddProviderMapping, handleToggleExpandRow, ]); if (error) { return (

Error Loading Exchanges

{error}

Make sure the web-api service is running on localhost:4000

); } const renderExpandedRow = (exchange: Exchange) => { const mappings = expandedRowData[exchange.id] || []; if (mappings.length === 0) { return (
No provider mappings found for this exchange.
); } return (

Provider Mappings

{mappings.map((mapping) => (
{mapping.provider.toUpperCase()} {mapping.provider_exchange_code} {mapping.provider_exchange_name} {mapping.country_code && ( {mapping.country_code} )}
Confidence: {mapping.confidence} Created: {new Date(mapping.created_at).toLocaleDateString()} {mapping.auto_mapped && Auto-mapped}
{mapping.verified && ( )}
))}
); }; return ( <>
{/* Expanded rows */} {Array.from(expandedRows).map(exchangeId => { const exchange = exchanges?.find(e => e.id === exchangeId); if (!exchange) return null; return (
{renderExpandedRow(exchange)}
); })}
{addProviderDialog && ( setAddProviderDialog(null)} onCreateMapping={async (mappingRequest) => { const result = await createProviderMapping(mappingRequest); if (result) { setAddProviderDialog(null); refetch(); } }} /> )} ); }