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
|
// Get exchange statistics
|
||||||
exchangeRoutes.get('/stats/summary', async c => {
|
exchangeRoutes.get('/stats/summary', async c => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,21 @@ import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
|
PlusIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ExchangesTable } from './components/ExchangesTable';
|
import { ExchangesTable } from './components/ExchangesTable';
|
||||||
|
import { AddExchangeDialog } from './components/AddExchangeDialog';
|
||||||
|
import { useExchanges } from './hooks/useExchanges';
|
||||||
|
|
||||||
export function ExchangesPage() {
|
export function ExchangesPage() {
|
||||||
|
const { createExchange } = useExchanges();
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const [syncStatus, setSyncStatus] = useState<{
|
const [syncStatus, setSyncStatus] = useState<{
|
||||||
type: 'success' | 'error' | null;
|
type: 'success' | 'error' | null;
|
||||||
message: string;
|
message: string;
|
||||||
}>({ type: null, message: '' });
|
}>({ type: null, message: '' });
|
||||||
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
|
|
||||||
// Auto-dismiss success messages after 5 seconds
|
// Auto-dismiss success messages after 5 seconds
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -54,14 +59,23 @@ export function ExchangesPage() {
|
||||||
Configure and manage master exchanges with their data sources and providers.
|
Configure and manage master exchanges with their data sources and providers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={handleSync}
|
<button
|
||||||
disabled={syncing}
|
onClick={() => setShowAddDialog(true)}
|
||||||
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"
|
className="inline-flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowPathIcon className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
|
<PlusIcon className="h-4 w-4" />
|
||||||
{syncing ? 'Syncing...' : 'Sync Exchanges'}
|
Add Exchange
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{syncing && (
|
{syncing && (
|
||||||
|
|
@ -108,6 +122,26 @@ export function ExchangesPage() {
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<ExchangesTable />
|
<ExchangesTable />
|
||||||
</div>
|
</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>
|
</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 { useExchanges } from '../hooks/useExchanges';
|
||||||
import { Exchange, ProviderMapping } from '../types';
|
import { Exchange, ProviderMapping } from '../types';
|
||||||
import { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
import { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
||||||
|
import { DeleteExchangeDialog } from './DeleteExchangeDialog';
|
||||||
|
|
||||||
export function ExchangesTable() {
|
export function ExchangesTable() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -24,6 +25,11 @@ export function ExchangesTable() {
|
||||||
exchangeId: string;
|
exchangeId: string;
|
||||||
exchangeName: string;
|
exchangeName: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [deleteDialog, setDeleteDialog] = useState<{
|
||||||
|
exchangeId: string;
|
||||||
|
exchangeName: string;
|
||||||
|
providerMappingCount: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const handleCellEdit = useCallback(
|
const handleCellEdit = useCallback(
|
||||||
async (id: string, field: string, value: string) => {
|
async (id: string, field: string, value: string) => {
|
||||||
|
|
@ -47,14 +53,16 @@ export function ExchangesTable() {
|
||||||
setAddProviderDialog({ exchangeId, exchangeName });
|
setAddProviderDialog({ exchangeId, exchangeName });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDeleteExchange = useCallback(async (exchangeId: string, exchangeName: string) => {
|
const handleDeleteExchange = useCallback((exchangeId: string, exchangeName: string, providerMappingCount: number) => {
|
||||||
if (confirm(`Are you sure you want to delete "${exchangeName}"? This will hide the exchange and make all its provider mappings available for remapping.`)) {
|
setDeleteDialog({ exchangeId, exchangeName, providerMappingCount });
|
||||||
const success = await updateExchange(exchangeId, { visible: false });
|
}, []);
|
||||||
if (success) {
|
|
||||||
// Optionally refresh the list or show a success message
|
const handleConfirmDelete = useCallback(async (exchangeId: string) => {
|
||||||
refetch();
|
const success = await updateExchange(exchangeId, { visible: false });
|
||||||
}
|
if (success) {
|
||||||
|
refetch();
|
||||||
}
|
}
|
||||||
|
return success;
|
||||||
}, [updateExchange, refetch]);
|
}, [updateExchange, refetch]);
|
||||||
|
|
||||||
const handleToggleProviderMapping = useCallback(
|
const handleToggleProviderMapping = useCallback(
|
||||||
|
|
@ -292,7 +300,11 @@ export function ExchangesTable() {
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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"
|
title="Delete Exchange"
|
||||||
>
|
>
|
||||||
|
|
@ -321,6 +333,7 @@ export function ExchangesTable() {
|
||||||
handleToggleActive,
|
handleToggleActive,
|
||||||
handleAddProviderMapping,
|
handleAddProviderMapping,
|
||||||
handleDeleteExchange,
|
handleDeleteExchange,
|
||||||
|
handleConfirmDelete,
|
||||||
handleRowExpand,
|
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 { AddSourceDialog } from './AddSourceDialog';
|
||||||
export { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
export { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
||||||
|
export { AddExchangeDialog } from './AddExchangeDialog';
|
||||||
|
export { DeleteExchangeDialog } from './DeleteExchangeDialog';
|
||||||
export { ExchangesTable } from './ExchangesTable';
|
export { ExchangesTable } from './ExchangesTable';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
CreateProviderMappingRequest,
|
CreateProviderMappingRequest,
|
||||||
|
CreateExchangeRequest,
|
||||||
Exchange,
|
Exchange,
|
||||||
ExchangeDetails,
|
ExchangeDetails,
|
||||||
ExchangeStats,
|
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(() => {
|
useEffect(() => {
|
||||||
console.log('useExchanges useEffect triggered');
|
console.log('useExchanges useEffect triggered');
|
||||||
fetchExchanges();
|
fetchExchanges();
|
||||||
|
|
@ -263,6 +293,7 @@ export function useExchanges() {
|
||||||
error,
|
error,
|
||||||
refetch: fetchExchanges,
|
refetch: fetchExchanges,
|
||||||
updateExchange,
|
updateExchange,
|
||||||
|
createExchange,
|
||||||
fetchExchangeDetails,
|
fetchExchangeDetails,
|
||||||
fetchStats,
|
fetchStats,
|
||||||
fetchProviderMappings,
|
fetchProviderMappings,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ export interface CreateProviderMappingRequest {
|
||||||
verified?: boolean;
|
verified?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateExchangeRequest {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
country: string;
|
||||||
|
currency: string;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExchangeStats {
|
export interface ExchangeStats {
|
||||||
total_exchanges: string;
|
total_exchanges: string;
|
||||||
active_exchanges: string;
|
active_exchanges: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue