added ability to add exchanges and a custom delete exchange dialog
This commit is contained in:
parent
0bec1eca83
commit
1d299e52d4
8 changed files with 550 additions and 16 deletions
|
|
@ -714,6 +714,98 @@ exchangeRoutes.get('/provider-exchanges/unmapped/:provider', async c => {
|
|||
}
|
||||
});
|
||||
|
||||
// Create new exchange
|
||||
exchangeRoutes.post('/', async c => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const { code, name, country, currency, active = true } = body;
|
||||
|
||||
if (!code || !name || !country || !currency) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Missing required fields: code, name, country, currency',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Validate currency is 3 characters
|
||||
if (currency.length !== 3) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Currency must be exactly 3 characters (e.g., USD, EUR, CAD)',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Validate country is 2 characters
|
||||
if (country.length !== 2) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Country must be exactly 2 characters (e.g., US, CA, GB)',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, active, visible)
|
||||
VALUES ($1, $2, $3, $4, $5, true)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
code.toUpperCase(),
|
||||
name,
|
||||
country.toUpperCase(),
|
||||
currency.toUpperCase(),
|
||||
active,
|
||||
]);
|
||||
|
||||
logger.info('Exchange created', {
|
||||
exchangeId: result.rows[0].id,
|
||||
code,
|
||||
name,
|
||||
});
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: 'Exchange created successfully',
|
||||
},
|
||||
201
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create exchange', { error });
|
||||
|
||||
// Handle unique constraint violations
|
||||
if (error instanceof Error && error.message.includes('duplicate key')) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Exchange with this code already exists',
|
||||
},
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get exchange statistics
|
||||
exchangeRoutes.get('/stats/summary', async c => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,16 +3,21 @@ import {
|
|||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
XMarkIcon,
|
||||
PlusIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ExchangesTable } from './components/ExchangesTable';
|
||||
import { AddExchangeDialog } from './components/AddExchangeDialog';
|
||||
import { useExchanges } from './hooks/useExchanges';
|
||||
|
||||
export function ExchangesPage() {
|
||||
const { createExchange } = useExchanges();
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [syncStatus, setSyncStatus] = useState<{
|
||||
type: 'success' | 'error' | null;
|
||||
message: string;
|
||||
}>({ type: null, message: '' });
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
|
||||
// Auto-dismiss success messages after 5 seconds
|
||||
useEffect(() => {
|
||||
|
|
@ -54,14 +59,23 @@ export function ExchangesPage() {
|
|||
Configure and manage master exchanges with their data sources and providers.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={syncing}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ArrowPathIcon className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
|
||||
{syncing ? 'Syncing...' : 'Sync Exchanges'}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Exchange
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={syncing}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ArrowPathIcon className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
|
||||
{syncing ? 'Syncing...' : 'Sync Exchanges'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{syncing && (
|
||||
|
|
@ -108,6 +122,26 @@ export function ExchangesPage() {
|
|||
<div className="flex-1 min-h-0">
|
||||
<ExchangesTable />
|
||||
</div>
|
||||
|
||||
<AddExchangeDialog
|
||||
isOpen={showAddDialog}
|
||||
onClose={() => setShowAddDialog(false)}
|
||||
onCreateExchange={async (exchangeRequest) => {
|
||||
try {
|
||||
await createExchange(exchangeRequest);
|
||||
setShowAddDialog(false);
|
||||
setSyncStatus({
|
||||
type: 'success',
|
||||
message: `Exchange "${exchangeRequest.code}" created successfully!`,
|
||||
});
|
||||
} catch (error) {
|
||||
setSyncStatus({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : 'Failed to create exchange',
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,233 @@
|
|||
import { Dialog, DialogContent, DialogHeader, DialogTitle, Button } from '@/components/ui';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { CreateExchangeRequest } from '../types';
|
||||
|
||||
interface AddExchangeDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateExchange: (request: CreateExchangeRequest) => Promise<any>;
|
||||
}
|
||||
|
||||
export function AddExchangeDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateExchange,
|
||||
}: AddExchangeDialogProps) {
|
||||
const [formData, setFormData] = useState<CreateExchangeRequest>({
|
||||
code: '',
|
||||
name: '',
|
||||
country: '',
|
||||
currency: '',
|
||||
active: true,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validateForm = useCallback((): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.code.trim()) {
|
||||
newErrors.code = 'Exchange code is required';
|
||||
} else if (formData.code.length > 10) {
|
||||
newErrors.code = 'Exchange code must be 10 characters or less';
|
||||
}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Exchange name is required';
|
||||
}
|
||||
|
||||
if (!formData.country.trim()) {
|
||||
newErrors.country = 'Country is required';
|
||||
} else if (formData.country.length !== 2) {
|
||||
newErrors.country = 'Country must be exactly 2 characters (e.g., US, CA, GB)';
|
||||
}
|
||||
|
||||
if (!formData.currency.trim()) {
|
||||
newErrors.currency = 'Currency is required';
|
||||
} else if (formData.currency.length !== 3) {
|
||||
newErrors.currency = 'Currency must be exactly 3 characters (e.g., USD, EUR, CAD)';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}, [formData]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await onCreateExchange({
|
||||
...formData,
|
||||
code: formData.code.toUpperCase(),
|
||||
country: formData.country.toUpperCase(),
|
||||
currency: formData.currency.toUpperCase(),
|
||||
});
|
||||
|
||||
// Reset form on success
|
||||
setFormData({
|
||||
code: '',
|
||||
name: '',
|
||||
country: '',
|
||||
currency: '',
|
||||
active: true,
|
||||
});
|
||||
setErrors({});
|
||||
} catch (error) {
|
||||
console.error('Error creating exchange:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[formData, validateForm, onCreateExchange]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setFormData({
|
||||
code: '',
|
||||
name: '',
|
||||
country: '',
|
||||
currency: '',
|
||||
active: true,
|
||||
});
|
||||
setErrors({});
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(field: keyof CreateExchangeRequest, value: string | boolean) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
},
|
||||
[errors]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Exchange</DialogTitle>
|
||||
<p className="text-sm text-text-muted">
|
||||
Create a new master exchange with no provider mappings
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Exchange Code */}
|
||||
<div>
|
||||
<label htmlFor="code" className="block text-sm font-medium text-text-primary mb-1">
|
||||
Exchange Code *
|
||||
</label>
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
value={formData.code}
|
||||
onChange={e => handleInputChange('code', e.target.value)}
|
||||
placeholder="e.g., NASDAQ, NYSE, TSX"
|
||||
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono ${
|
||||
errors.code ? 'border-danger' : 'border-border'
|
||||
}`}
|
||||
maxLength={10}
|
||||
required
|
||||
/>
|
||||
{errors.code && <p className="text-xs text-danger mt-1">{errors.code}</p>}
|
||||
</div>
|
||||
|
||||
{/* Exchange Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-text-primary mb-1">
|
||||
Exchange Name *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={e => handleInputChange('name', e.target.value)}
|
||||
placeholder="e.g., NASDAQ Stock Market, New York Stock Exchange"
|
||||
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 ${
|
||||
errors.name ? 'border-danger' : 'border-border'
|
||||
}`}
|
||||
maxLength={255}
|
||||
required
|
||||
/>
|
||||
{errors.name && <p className="text-xs text-danger mt-1">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
{/* Country */}
|
||||
<div>
|
||||
<label htmlFor="country" className="block text-sm font-medium text-text-primary mb-1">
|
||||
Country Code *
|
||||
</label>
|
||||
<input
|
||||
id="country"
|
||||
type="text"
|
||||
value={formData.country}
|
||||
onChange={e => handleInputChange('country', e.target.value)}
|
||||
placeholder="e.g., US, CA, GB"
|
||||
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono ${
|
||||
errors.country ? 'border-danger' : 'border-border'
|
||||
}`}
|
||||
maxLength={2}
|
||||
required
|
||||
/>
|
||||
{errors.country && <p className="text-xs text-danger mt-1">{errors.country}</p>}
|
||||
</div>
|
||||
|
||||
{/* Currency */}
|
||||
<div>
|
||||
<label htmlFor="currency" className="block text-sm font-medium text-text-primary mb-1">
|
||||
Currency Code *
|
||||
</label>
|
||||
<input
|
||||
id="currency"
|
||||
type="text"
|
||||
value={formData.currency}
|
||||
onChange={e => handleInputChange('currency', e.target.value)}
|
||||
placeholder="e.g., USD, EUR, CAD"
|
||||
className={`w-full px-3 py-2 border rounded-md bg-surface text-text-primary focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono ${
|
||||
errors.currency ? 'border-danger' : 'border-border'
|
||||
}`}
|
||||
maxLength={3}
|
||||
required
|
||||
/>
|
||||
{errors.currency && <p className="text-xs text-danger mt-1">{errors.currency}</p>}
|
||||
</div>
|
||||
|
||||
{/* Active Toggle */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.active}
|
||||
onChange={e => handleInputChange('active', e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-text-primary">Active exchange</span>
|
||||
</label>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Inactive exchanges won't be used for new symbol mappings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={handleClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Creating...' : 'Create Exchange'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { Dialog, DialogContent, DialogHeader, DialogTitle, Button } from '@/components/ui';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { TrashIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface DeleteExchangeDialogProps {
|
||||
isOpen: boolean;
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
providerMappingCount: number;
|
||||
onClose: () => void;
|
||||
onConfirmDelete: (exchangeId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function DeleteExchangeDialog({
|
||||
isOpen,
|
||||
exchangeId,
|
||||
exchangeName,
|
||||
providerMappingCount,
|
||||
onClose,
|
||||
onConfirmDelete,
|
||||
}: DeleteExchangeDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const success = await onConfirmDelete(exchangeId);
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting exchange:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [exchangeId, onConfirmDelete, onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-danger">
|
||||
<ExclamationTriangleIcon className="h-5 w-5" />
|
||||
Delete Exchange
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-danger/10 border border-danger/20 rounded-lg">
|
||||
<p className="text-sm text-text-primary">
|
||||
Are you sure you want to delete <strong>"{exchangeName}"</strong>?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm text-text-secondary">
|
||||
<p className="font-medium text-text-primary">This action will:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li>Hide the exchange from all lists</li>
|
||||
{providerMappingCount > 0 && (
|
||||
<li className="text-yellow-400">
|
||||
Delete {providerMappingCount} provider mapping{providerMappingCount !== 1 ? 's' : ''}
|
||||
</li>
|
||||
)}
|
||||
<li>Make provider exchanges available for remapping</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{providerMappingCount > 0 && (
|
||||
<div className="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-md">
|
||||
<div className="flex items-start gap-2">
|
||||
<ExclamationTriangleIcon className="h-4 w-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-yellow-400">Warning</p>
|
||||
<p className="text-text-secondary">
|
||||
This exchange has {providerMappingCount} provider mapping{providerMappingCount !== 1 ? 's' : ''}.
|
||||
Deleting will permanently remove these mappings and make the provider exchanges
|
||||
available for mapping to other exchanges.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-surface-secondary p-3 rounded-md">
|
||||
<p className="text-xs text-text-muted">
|
||||
<strong>Note:</strong> This action cannot be undone. The exchange will be hidden
|
||||
but can be restored by directly updating the database if needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleConfirm}
|
||||
disabled={loading}
|
||||
className="bg-danger hover:bg-danger/90 text-white"
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 mr-1" />
|
||||
{loading ? 'Deleting...' : 'Delete Exchange'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { useCallback, useMemo, useState, useEffect } from 'react';
|
|||
import { useExchanges } from '../hooks/useExchanges';
|
||||
import { Exchange, ProviderMapping } from '../types';
|
||||
import { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
||||
import { DeleteExchangeDialog } from './DeleteExchangeDialog';
|
||||
|
||||
export function ExchangesTable() {
|
||||
const {
|
||||
|
|
@ -24,6 +25,11 @@ export function ExchangesTable() {
|
|||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
} | null>(null);
|
||||
const [deleteDialog, setDeleteDialog] = useState<{
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
providerMappingCount: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleCellEdit = useCallback(
|
||||
async (id: string, field: string, value: string) => {
|
||||
|
|
@ -47,14 +53,16 @@ export function ExchangesTable() {
|
|||
setAddProviderDialog({ exchangeId, exchangeName });
|
||||
}, []);
|
||||
|
||||
const handleDeleteExchange = useCallback(async (exchangeId: string, exchangeName: string) => {
|
||||
if (confirm(`Are you sure you want to delete "${exchangeName}"? This will hide the exchange and make all its provider mappings available for remapping.`)) {
|
||||
const success = await updateExchange(exchangeId, { visible: false });
|
||||
if (success) {
|
||||
// Optionally refresh the list or show a success message
|
||||
refetch();
|
||||
}
|
||||
const handleDeleteExchange = useCallback((exchangeId: string, exchangeName: string, providerMappingCount: number) => {
|
||||
setDeleteDialog({ exchangeId, exchangeName, providerMappingCount });
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async (exchangeId: string) => {
|
||||
const success = await updateExchange(exchangeId, { visible: false });
|
||||
if (success) {
|
||||
refetch();
|
||||
}
|
||||
return success;
|
||||
}, [updateExchange, refetch]);
|
||||
|
||||
const handleToggleProviderMapping = useCallback(
|
||||
|
|
@ -292,7 +300,11 @@ export function ExchangesTable() {
|
|||
Add
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteExchange(row.original.id, row.original.name)}
|
||||
onClick={() => handleDeleteExchange(
|
||||
row.original.id,
|
||||
row.original.name,
|
||||
parseInt(row.original.provider_mapping_count) || 0
|
||||
)}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-danger/20 text-danger rounded text-xs hover:bg-danger/30 transition-colors whitespace-nowrap"
|
||||
title="Delete Exchange"
|
||||
>
|
||||
|
|
@ -321,6 +333,7 @@ export function ExchangesTable() {
|
|||
handleToggleActive,
|
||||
handleAddProviderMapping,
|
||||
handleDeleteExchange,
|
||||
handleConfirmDelete,
|
||||
handleRowExpand,
|
||||
]);
|
||||
|
||||
|
|
@ -433,6 +446,17 @@ export function ExchangesTable() {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteDialog && (
|
||||
<DeleteExchangeDialog
|
||||
isOpen={true}
|
||||
exchangeId={deleteDialog.exchangeId}
|
||||
exchangeName={deleteDialog.exchangeName}
|
||||
providerMappingCount={deleteDialog.providerMappingCount}
|
||||
onClose={() => setDeleteDialog(null)}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export { AddSourceDialog } from './AddSourceDialog';
|
||||
export { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
||||
export { AddExchangeDialog } from './AddExchangeDialog';
|
||||
export { DeleteExchangeDialog } from './DeleteExchangeDialog';
|
||||
export { ExchangesTable } from './ExchangesTable';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
CreateProviderMappingRequest,
|
||||
CreateExchangeRequest,
|
||||
Exchange,
|
||||
ExchangeDetails,
|
||||
ExchangeStats,
|
||||
|
|
@ -252,6 +253,35 @@ export function useExchanges() {
|
|||
[]
|
||||
);
|
||||
|
||||
const createExchange = useCallback(async (request: CreateExchangeRequest) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create exchange: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to create exchange');
|
||||
}
|
||||
|
||||
// Refresh the exchanges list
|
||||
await fetchExchanges();
|
||||
return result.data;
|
||||
} catch (err) {
|
||||
console.error('Error creating exchange:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to create exchange');
|
||||
throw err;
|
||||
}
|
||||
}, [fetchExchanges]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('useExchanges useEffect triggered');
|
||||
fetchExchanges();
|
||||
|
|
@ -263,6 +293,7 @@ export function useExchanges() {
|
|||
error,
|
||||
refetch: fetchExchanges,
|
||||
updateExchange,
|
||||
createExchange,
|
||||
fetchExchangeDetails,
|
||||
fetchStats,
|
||||
fetchProviderMappings,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ export interface CreateProviderMappingRequest {
|
|||
verified?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateExchangeRequest {
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface ExchangeStats {
|
||||
total_exchanges: string;
|
||||
active_exchanges: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue