getting close to having exchanges done

This commit is contained in:
Boki 2025-06-18 09:12:51 -04:00
parent 6a34d1140f
commit 4f4f615a62
3 changed files with 68 additions and 61 deletions

View file

@ -9,12 +9,13 @@ import { getMongoDBClient } from '@stock-bot/mongodb-client';
const logger = getLogger('exchange-routes'); const logger = getLogger('exchange-routes');
export const exchangeRoutes = new Hono(); export const exchangeRoutes = new Hono();
// Get all exchanges with provider mapping counts // Get all exchanges with provider mapping counts and mappings
exchangeRoutes.get('/', async c => { exchangeRoutes.get('/', async c => {
try { try {
const postgresClient = getPostgreSQLClient(); const postgresClient = getPostgreSQLClient();
const query = ` // First get all exchanges with counts
const exchangesQuery = `
SELECT SELECT
e.id, e.id,
e.code, e.code,
@ -34,12 +35,49 @@ exchangeRoutes.get('/', async c => {
ORDER BY e.code ORDER BY e.code
`; `;
const result = await postgresClient.query(query); const exchangesResult = await postgresClient.query(exchangesQuery);
// Then get all provider mappings
const mappingsQuery = `
SELECT
pem.*,
e.code as master_exchange_code,
e.name as master_exchange_name
FROM provider_exchange_mappings pem
JOIN exchanges e ON pem.master_exchange_id = e.id
ORDER BY pem.master_exchange_id, pem.provider, pem.provider_exchange_code
`;
const mappingsResult = await postgresClient.query(mappingsQuery);
// Group mappings by exchange ID
const mappingsByExchange = mappingsResult.rows.reduce((acc, mapping) => {
const exchangeId = mapping.master_exchange_id;
if (!acc[exchangeId]) {
acc[exchangeId] = [];
}
acc[exchangeId].push(mapping);
return acc;
}, {} as Record<string, any[]>);
// Attach mappings to exchanges
const exchangesWithMappings = exchangesResult.rows.map(exchange => {
const mappings = mappingsByExchange[exchange.id] || [];
logger.info('Exchange mapping debug', {
exchangeId: exchange.id,
exchangeCode: exchange.code,
mappingsCount: mappings.length,
availableExchangeIds: Object.keys(mappingsByExchange)
});
return {
...exchange,
provider_mappings: mappings
};
});
return c.json({ return c.json({
success: true, success: true,
data: result.rows, data: exchangesWithMappings,
total: result.rows.length, total: exchangesWithMappings.length,
}); });
} catch (error) { } catch (error) {
logger.error('Failed to get exchanges', { error }); logger.error('Failed to get exchanges', { error });
@ -343,6 +381,9 @@ exchangeRoutes.post('/provider-mappings', async c => {
); );
} }
// Validate and clean currency field (must be 3 characters or null)
const cleanCurrency = currency && currency.length <= 3 ? currency : null;
const query = ` const query = `
INSERT INTO provider_exchange_mappings INSERT INTO provider_exchange_mappings
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id, (provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
@ -357,7 +398,7 @@ exchangeRoutes.post('/provider-mappings', async c => {
provider_exchange_name, provider_exchange_name,
master_exchange_id, master_exchange_id,
country_code, country_code,
currency, cleanCurrency,
confidence, confidence,
active, active,
verified, verified,

View file

@ -24,7 +24,6 @@ export function ExchangesTable() {
exchangeId: string; exchangeId: string;
exchangeName: string; exchangeName: string;
} | null>(null); } | null>(null);
const [expandedRowData, setExpandedRowData] = useState<Record<string, ProviderMapping[]>>({});
const handleCellEdit = useCallback( const handleCellEdit = useCallback(
async (id: string, field: string, value: string) => { async (id: string, field: string, value: string) => {
@ -62,20 +61,7 @@ export function ExchangesTable() {
async (mappingId: string, currentStatus: boolean) => { async (mappingId: string, currentStatus: boolean) => {
const success = await updateProviderMapping(mappingId, { active: !currentStatus }); const success = await updateProviderMapping(mappingId, { active: !currentStatus });
if (success) { if (success) {
// Update the local expanded row data immediately // Refresh the main table data to get updated counts and mappings
setExpandedRowData(prev => {
const updated = { ...prev };
Object.keys(updated).forEach(exchangeId => {
updated[exchangeId] = updated[exchangeId].map(mapping =>
mapping.id === mappingId
? { ...mapping, active: !currentStatus }
: mapping
);
});
return updated;
});
// Also refresh the main table data
refetch(); refetch();
} }
}, },
@ -84,18 +70,10 @@ export function ExchangesTable() {
const handleRowExpand = useCallback( const handleRowExpand = useCallback(
async (row: any) => { async (row: any) => {
const exchangeId = row.original.id; // Row expansion is now handled automatically by TanStack Table
if (!expandedRowData[exchangeId]) { // No need to fetch data since all mappings are already loaded
const details = await fetchExchangeDetails(exchangeId);
if (details) {
setExpandedRowData(prev => ({
...prev,
[exchangeId]: details.provider_mappings
}));
}
}
}, },
[fetchExchangeDetails] []
); );
const columns = useMemo<ColumnDef<Exchange>[]>(() => { const columns = useMemo<ColumnDef<Exchange>[]>(() => {
@ -258,27 +236,18 @@ export function ExchangesTable() {
const activeMappings = parseInt(row.original.active_mapping_count) || 0; const activeMappings = parseInt(row.original.active_mapping_count) || 0;
const verifiedMappings = parseInt(row.original.verified_mapping_count) || 0; const verifiedMappings = parseInt(row.original.verified_mapping_count) || 0;
// Parse provider mappings from the expanded data or from a summary field // Get provider mappings directly from the exchange data
const exchangeId = row.original.id; const mappings = row.original.provider_mappings || [];
const mappings = expandedRowData[exchangeId] || [];
const handleLoadMappings = async () => { // Sort mappings to show active ones first
if (mappings.length === 0 && totalMappings > 0) { const sortedMappings = [...mappings].sort((a, b) => {
const details = await fetchExchangeDetails(exchangeId); if (a.active && !b.active) return -1;
if (details) { if (!a.active && b.active) return 1;
setExpandedRowData(prev => ({ return 0;
...prev, });
[exchangeId]: details.provider_mappings
}));
}
}
};
return ( return (
<div <div className="flex flex-col gap-1">
className="flex flex-col gap-1 cursor-pointer"
onMouseEnter={handleLoadMappings}
>
<div className="text-sm"> <div className="text-sm">
<span className="text-text-primary font-medium">{totalMappings}</span> <span className="text-text-primary font-medium">{totalMappings}</span>
<span className="text-text-muted"> total</span> <span className="text-text-muted"> total</span>
@ -290,18 +259,16 @@ export function ExchangesTable() {
</div> </div>
{mappings.length > 0 ? ( {mappings.length > 0 ? (
<div className="flex flex-wrap gap-1 text-xs"> <div className="flex flex-wrap gap-1 text-xs">
{mappings.slice(0, 3).map((mapping, index) => ( {sortedMappings.slice(0, 3).map((mapping, index) => (
<span key={index} className={`${mapping.active ? 'text-text-primary' : 'text-text-muted'}`}> <span key={index} className={mapping.active ? 'text-green-500' : 'text-text-muted'}>
<span className="font-bold text-primary-400">{mapping.provider.toLowerCase()}</span> <span className={mapping.active ? 'font-bold text-green-500' : 'font-bold text-text-muted'}>{mapping.provider.toLowerCase()}</span>
<span className="text-text-secondary">({mapping.provider_exchange_code})</span> <span className={mapping.active ? 'text-green-500' : 'text-text-muted'}>({mapping.provider_exchange_code})</span>
</span> </span>
))} ))}
{mappings.length > 3 && ( {sortedMappings.length > 3 && (
<span className="text-text-muted">+{mappings.length - 3} more</span> <span className="text-text-muted">+{sortedMappings.length - 3} more</span>
)} )}
</div> </div>
) : totalMappings > 0 ? (
<div className="text-xs text-text-muted">Hover to load mappings...</div>
) : ( ) : (
<div className="text-xs text-text-muted">No mappings</div> <div className="text-xs text-text-muted">No mappings</div>
)} )}
@ -350,13 +317,11 @@ export function ExchangesTable() {
}, [ }, [
editingCell, editingCell,
editValue, editValue,
expandedRowData,
handleCellEdit, handleCellEdit,
handleToggleActive, handleToggleActive,
handleAddProviderMapping, handleAddProviderMapping,
handleDeleteExchange, handleDeleteExchange,
handleRowExpand, handleRowExpand,
fetchExchangeDetails,
]); ]);
if (error) { if (error) {
@ -373,7 +338,7 @@ export function ExchangesTable() {
const renderSubComponent = ({ row }: { row: any }) => { const renderSubComponent = ({ row }: { row: any }) => {
const exchange = row.original as Exchange; const exchange = row.original as Exchange;
const mappings = expandedRowData[exchange.id] || []; const mappings = exchange.provider_mappings || [];
if (mappings.length === 0) { if (mappings.length === 0) {
return ( return (

View file

@ -30,6 +30,7 @@ export interface Exchange {
active_mapping_count: string; active_mapping_count: string;
verified_mapping_count: string; verified_mapping_count: string;
providers: string | null; providers: string | null;
provider_mappings: ProviderMapping[];
} }
export interface ExchangeDetails { export interface ExchangeDetails {