getting close to having exchanges done
This commit is contained in:
parent
6a34d1140f
commit
4f4f615a62
3 changed files with 68 additions and 61 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue