huge refactor on web-api and web-app
This commit is contained in:
parent
1d299e52d4
commit
265e10a658
23 changed files with 1545 additions and 1233 deletions
|
|
@ -1,114 +1,55 @@
|
|||
import { Dialog, DialogContent, DialogHeader, DialogTitle, Button } from '@/components/ui';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { CreateExchangeRequest } from '../types';
|
||||
import { useCallback } from 'react';
|
||||
import { CreateExchangeRequest, AddExchangeDialogProps } from '../types';
|
||||
import { validateExchangeForm } from '../utils/validation';
|
||||
import { useFormValidation } from '../hooks/useFormValidation';
|
||||
|
||||
interface AddExchangeDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreateExchange: (request: CreateExchangeRequest) => Promise<any>;
|
||||
}
|
||||
const initialFormData: CreateExchangeRequest = {
|
||||
code: '',
|
||||
name: '',
|
||||
country: '',
|
||||
currency: '',
|
||||
active: true,
|
||||
};
|
||||
|
||||
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 {
|
||||
formData,
|
||||
errors,
|
||||
isSubmitting,
|
||||
updateField,
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useFormValidation(initialFormData, validateExchangeForm);
|
||||
|
||||
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);
|
||||
}
|
||||
const onSubmit = useCallback(
|
||||
async (data: CreateExchangeRequest) => {
|
||||
await onCreateExchange({
|
||||
...data,
|
||||
code: data.code.toUpperCase(),
|
||||
country: data.country.toUpperCase(),
|
||||
currency: data.currency.toUpperCase(),
|
||||
});
|
||||
},
|
||||
[formData, validateForm, onCreateExchange]
|
||||
[onCreateExchange]
|
||||
);
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(onSubmit, onClose);
|
||||
},
|
||||
[handleSubmit, onSubmit, onClose]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setFormData({
|
||||
code: '',
|
||||
name: '',
|
||||
country: '',
|
||||
currency: '',
|
||||
active: true,
|
||||
});
|
||||
setErrors({});
|
||||
reset();
|
||||
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]
|
||||
);
|
||||
}, [reset, onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
|
|
@ -120,7 +61,7 @@ export function AddExchangeDialog({
|
|||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<form onSubmit={handleFormSubmit} className="space-y-4">
|
||||
{/* Exchange Code */}
|
||||
<div>
|
||||
<label htmlFor="code" className="block text-sm font-medium text-text-primary mb-1">
|
||||
|
|
@ -130,7 +71,7 @@ export function AddExchangeDialog({
|
|||
id="code"
|
||||
type="text"
|
||||
value={formData.code}
|
||||
onChange={e => handleInputChange('code', e.target.value)}
|
||||
onChange={e => updateField('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'
|
||||
|
|
@ -150,7 +91,7 @@ export function AddExchangeDialog({
|
|||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={e => handleInputChange('name', e.target.value)}
|
||||
onChange={e => updateField('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'
|
||||
|
|
@ -170,7 +111,7 @@ export function AddExchangeDialog({
|
|||
id="country"
|
||||
type="text"
|
||||
value={formData.country}
|
||||
onChange={e => handleInputChange('country', e.target.value)}
|
||||
onChange={e => updateField('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'
|
||||
|
|
@ -190,7 +131,7 @@ export function AddExchangeDialog({
|
|||
id="currency"
|
||||
type="text"
|
||||
value={formData.currency}
|
||||
onChange={e => handleInputChange('currency', e.target.value)}
|
||||
onChange={e => updateField('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'
|
||||
|
|
@ -207,7 +148,7 @@ export function AddExchangeDialog({
|
|||
<input
|
||||
type="checkbox"
|
||||
checked={formData.active}
|
||||
onChange={e => handleInputChange('active', e.target.checked)}
|
||||
onChange={e => updateField('active', e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-text-primary">Active exchange</span>
|
||||
|
|
@ -219,11 +160,11 @@ export function AddExchangeDialog({
|
|||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={handleClose} disabled={loading}>
|
||||
<Button type="button" variant="outline" onClick={handleClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Creating...' : 'Create Exchange'}
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Creating...' : 'Create Exchange'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function AddProviderMappingDialog({
|
|||
if (isOpen) {
|
||||
loadProviders();
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, loadProviders]);
|
||||
|
||||
// Load unmapped exchanges when provider changes
|
||||
useEffect(() => {
|
||||
|
|
@ -42,7 +42,7 @@ export function AddProviderMappingDialog({
|
|||
setUnmappedExchanges([]);
|
||||
setSelectedProviderExchange('');
|
||||
}
|
||||
}, [selectedProvider]);
|
||||
}, [selectedProvider, loadUnmappedExchanges]);
|
||||
|
||||
const loadProviders = useCallback(async () => {
|
||||
setProvidersLoading(true);
|
||||
|
|
@ -50,7 +50,7 @@ export function AddProviderMappingDialog({
|
|||
const providersData = await fetchProviders();
|
||||
setProviders(providersData);
|
||||
} catch (error) {
|
||||
console.error('Error loading providers:', error);
|
||||
// Error loading providers - could add toast notification here
|
||||
} finally {
|
||||
setProvidersLoading(false);
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ export function AddProviderMappingDialog({
|
|||
const exchangesData = await fetchUnmappedProviderExchanges(provider);
|
||||
setUnmappedExchanges(exchangesData);
|
||||
} catch (error) {
|
||||
console.error('Error loading unmapped exchanges:', error);
|
||||
// Error loading unmapped exchanges - could add toast notification here
|
||||
} finally {
|
||||
setExchangesLoading(false);
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ export function AddProviderMappingDialog({
|
|||
|
||||
await onCreateMapping(request);
|
||||
} catch (error) {
|
||||
console.error('Error creating provider mapping:', error);
|
||||
// Error creating provider mapping - could add toast notification here
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function DeleteExchangeDialog({
|
|||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting exchange:', error);
|
||||
// Error deleting exchange - could add toast notification here
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { DataTable } from '@/components/ui';
|
||||
import { PlusIcon, XMarkIcon, CheckIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useExchanges } from '../hooks/useExchanges';
|
||||
import { Exchange, ProviderMapping } from '../types';
|
||||
import { Exchange, ProviderMapping, EditingCell, AddProviderMappingDialogState, DeleteDialogState } from '../types';
|
||||
import { AddProviderMappingDialog } from './AddProviderMappingDialog';
|
||||
import { DeleteExchangeDialog } from './DeleteExchangeDialog';
|
||||
import { sortProviderMappings, getProviderMappingColor, formatProviderMapping, formatDate } from '../utils/formatters';
|
||||
|
||||
export function ExchangesTable() {
|
||||
const {
|
||||
|
|
@ -19,17 +20,10 @@ export function ExchangesTable() {
|
|||
refetch
|
||||
} = useExchanges();
|
||||
|
||||
const [editingCell, setEditingCell] = useState<{ id: string; field: string } | null>(null);
|
||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [addProviderDialog, setAddProviderDialog] = useState<{
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
} | null>(null);
|
||||
const [deleteDialog, setDeleteDialog] = useState<{
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
providerMappingCount: number;
|
||||
} | null>(null);
|
||||
const [addProviderDialog, setAddProviderDialog] = useState<AddProviderMappingDialogState | null>(null);
|
||||
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
|
||||
|
||||
const handleCellEdit = useCallback(
|
||||
async (id: string, field: string, value: string) => {
|
||||
|
|
@ -246,13 +240,7 @@ export function ExchangesTable() {
|
|||
|
||||
// Get provider mappings directly from the exchange data
|
||||
const mappings = row.original.provider_mappings || [];
|
||||
|
||||
// Sort mappings to show active ones first
|
||||
const sortedMappings = [...mappings].sort((a, b) => {
|
||||
if (a.active && !b.active) return -1;
|
||||
if (!a.active && b.active) return 1;
|
||||
return 0;
|
||||
});
|
||||
const sortedMappings = sortProviderMappings(mappings);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
@ -268,9 +256,8 @@ export function ExchangesTable() {
|
|||
{mappings.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1 text-xs">
|
||||
{sortedMappings.slice(0, 3).map((mapping, index) => (
|
||||
<span key={index} className={mapping.active ? 'text-green-500' : 'text-text-muted'}>
|
||||
<span className={mapping.active ? 'font-bold text-green-500' : 'font-bold text-text-muted'}>{mapping.provider.toLowerCase()}</span>
|
||||
<span className={mapping.active ? 'text-green-500' : 'text-text-muted'}>({mapping.provider_exchange_code})</span>
|
||||
<span key={index} className={getProviderMappingColor(mapping)}>
|
||||
{formatProviderMapping(mapping)}
|
||||
</span>
|
||||
))}
|
||||
{sortedMappings.length > 3 && (
|
||||
|
|
@ -321,7 +308,7 @@ export function ExchangesTable() {
|
|||
size: 120,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-xs text-text-muted">
|
||||
{new Date(getValue() as string).toLocaleDateString()}
|
||||
{formatDate(getValue() as string)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
|
@ -392,7 +379,7 @@ export function ExchangesTable() {
|
|||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-xs text-text-muted">
|
||||
<span>Confidence: {mapping.confidence}</span>
|
||||
<span>Created: {new Date(mapping.created_at).toLocaleDateString()}</span>
|
||||
<span>Created: {formatDate(mapping.created_at)}</span>
|
||||
{mapping.auto_mapped && <span className="text-yellow-400">Auto-mapped</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
CreateProviderMappingRequest,
|
||||
CreateExchangeRequest,
|
||||
Exchange,
|
||||
ExchangeDetails,
|
||||
ExchangeStats,
|
||||
ProviderMapping,
|
||||
ProviderExchange,
|
||||
CreateExchangeRequest,
|
||||
UpdateExchangeRequest,
|
||||
CreateProviderMappingRequest,
|
||||
UpdateProviderMappingRequest,
|
||||
} from '../types';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:4000/api';
|
||||
import { exchangeApi } from '../services/exchangeApi';
|
||||
|
||||
export function useExchanges() {
|
||||
const [exchanges, setExchanges] = useState<Exchange[]>([]);
|
||||
|
|
@ -19,29 +18,13 @@ export function useExchanges() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchExchanges = useCallback(async () => {
|
||||
console.log('fetchExchanges called');
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log('Making fetch request to:', `${API_BASE_URL}/exchanges`);
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch exchanges: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API response:', data);
|
||||
|
||||
if (data.success) {
|
||||
console.log('Setting exchanges:', data.data);
|
||||
setExchanges(data.data || []);
|
||||
} else {
|
||||
throw new Error(data.error || 'API returned error status');
|
||||
}
|
||||
const data = await exchangeApi.getExchanges();
|
||||
setExchanges(data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching exchanges:', err);
|
||||
// Error fetching exchanges - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch exchanges');
|
||||
setExchanges([]);
|
||||
} finally {
|
||||
|
|
@ -50,30 +33,13 @@ export function useExchanges() {
|
|||
}, []);
|
||||
|
||||
const updateExchange = useCallback(
|
||||
async (id: string, updates: UpdateExchangeRequest) => {
|
||||
async (id: string, updates: UpdateExchangeRequest): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update exchange: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update exchange');
|
||||
}
|
||||
|
||||
// Refresh the exchanges list
|
||||
await fetchExchanges();
|
||||
await exchangeApi.updateExchange(id, updates);
|
||||
await fetchExchanges(); // Refresh the list
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error updating exchange:', err);
|
||||
// Error updating exchange - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to update exchange');
|
||||
return false;
|
||||
}
|
||||
|
|
@ -81,43 +47,39 @@ export function useExchanges() {
|
|||
[fetchExchanges]
|
||||
);
|
||||
|
||||
const fetchExchangeDetails = useCallback(async (id: string): Promise<ExchangeDetails | null> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch exchange details: ${response.statusText}`);
|
||||
const createExchange = useCallback(
|
||||
async (request: CreateExchangeRequest): Promise<Exchange> => {
|
||||
try {
|
||||
const exchange = await exchangeApi.createExchange(request);
|
||||
await fetchExchanges(); // Refresh the list
|
||||
return exchange;
|
||||
} catch (err) {
|
||||
// Error creating exchange - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to create exchange');
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[fetchExchanges]
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch exchange details');
|
||||
const fetchExchangeDetails = useCallback(
|
||||
async (id: string): Promise<ExchangeDetails | null> => {
|
||||
try {
|
||||
return await exchangeApi.getExchangeById(id);
|
||||
} catch (err) {
|
||||
// Error fetching exchange details - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch exchange details');
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (err) {
|
||||
console.error('Error fetching exchange details:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch exchange details');
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchStats = useCallback(async (): Promise<ExchangeStats | null> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/stats/summary`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch stats: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch stats');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
return await exchangeApi.getExchangeStats();
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
// Error fetching stats - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch stats');
|
||||
return null;
|
||||
}
|
||||
|
|
@ -126,24 +88,9 @@ export function useExchanges() {
|
|||
const fetchProviderMappings = useCallback(
|
||||
async (provider?: string): Promise<ProviderMapping[]> => {
|
||||
try {
|
||||
const url = provider
|
||||
? `${API_BASE_URL}/exchanges/provider-mappings/${provider}`
|
||||
: `${API_BASE_URL}/exchanges/provider-mappings/all`;
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch provider mappings: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch provider mappings');
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
return await exchangeApi.getProviderMappings(provider);
|
||||
} catch (err) {
|
||||
console.error('Error fetching provider mappings:', err);
|
||||
// Error fetching provider mappings - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch provider mappings');
|
||||
return [];
|
||||
}
|
||||
|
|
@ -152,28 +99,12 @@ export function useExchanges() {
|
|||
);
|
||||
|
||||
const updateProviderMapping = useCallback(
|
||||
async (id: string, updates: UpdateProviderMappingRequest) => {
|
||||
async (id: string, updates: UpdateProviderMappingRequest): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/provider-mappings/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update provider mapping: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update provider mapping');
|
||||
}
|
||||
|
||||
await exchangeApi.updateProviderMapping(id, updates);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error updating provider mapping:', err);
|
||||
// Error updating provider mapping - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to update provider mapping');
|
||||
return false;
|
||||
}
|
||||
|
|
@ -181,49 +112,24 @@ export function useExchanges() {
|
|||
[]
|
||||
);
|
||||
|
||||
const createProviderMapping = useCallback(async (request: CreateProviderMappingRequest) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/provider-mappings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create provider mapping: ${response.statusText}`);
|
||||
const createProviderMapping = useCallback(
|
||||
async (request: CreateProviderMappingRequest): Promise<ProviderMapping | null> => {
|
||||
try {
|
||||
return await exchangeApi.createProviderMapping(request);
|
||||
} catch (err) {
|
||||
// Error creating provider mapping - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to create provider mapping');
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to create provider mapping');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
} catch (err) {
|
||||
console.error('Error creating provider mapping:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to create provider mapping');
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchProviders = useCallback(async (): Promise<string[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/providers/list`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch providers: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch providers');
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
return await exchangeApi.getProviders();
|
||||
} catch (err) {
|
||||
console.error('Error fetching providers:', err);
|
||||
// Error fetching providers - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch providers');
|
||||
return [];
|
||||
}
|
||||
|
|
@ -232,20 +138,9 @@ export function useExchanges() {
|
|||
const fetchUnmappedProviderExchanges = useCallback(
|
||||
async (provider: string): Promise<ProviderExchange[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/exchanges/provider-exchanges/unmapped/${provider}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch unmapped exchanges: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to fetch unmapped exchanges');
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
return await exchangeApi.getUnmappedProviderExchanges(provider);
|
||||
} catch (err) {
|
||||
console.error('Error fetching unmapped exchanges:', err);
|
||||
// Error fetching unmapped exchanges - error state will show in UI
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch unmapped exchanges');
|
||||
return [];
|
||||
}
|
||||
|
|
@ -253,37 +148,7 @@ 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();
|
||||
}, [fetchExchanges]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { FormErrors } from '../types';
|
||||
|
||||
export function useFormValidation<T>(
|
||||
initialData: T,
|
||||
validateFn: (data: T) => FormErrors
|
||||
) {
|
||||
const [formData, setFormData] = useState<T>(initialData);
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const updateField = useCallback((field: keyof T, value: T[keyof T]) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[field as string]) {
|
||||
setErrors(prev => ({ ...prev, [field as string]: '' }));
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
const validate = useCallback((): boolean => {
|
||||
const newErrors = validateFn(formData);
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
}, [formData, validateFn]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFormData(initialData);
|
||||
setErrors({});
|
||||
setIsSubmitting(false);
|
||||
}, [initialData]);
|
||||
|
||||
const handleSubmit = useCallback(async (
|
||||
onSubmit: (data: T) => Promise<void>,
|
||||
onSuccess?: () => void,
|
||||
onError?: (error: unknown) => void
|
||||
) => {
|
||||
if (!validate()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSubmit(formData);
|
||||
reset();
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [formData, validate, reset]);
|
||||
|
||||
return {
|
||||
formData,
|
||||
errors,
|
||||
isSubmitting,
|
||||
updateField,
|
||||
validate,
|
||||
reset,
|
||||
handleSubmit,
|
||||
setIsSubmitting,
|
||||
};
|
||||
}
|
||||
135
apps/web-app/src/features/exchanges/services/exchangeApi.ts
Normal file
135
apps/web-app/src/features/exchanges/services/exchangeApi.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import {
|
||||
ApiResponse,
|
||||
Exchange,
|
||||
ExchangeDetails,
|
||||
ExchangeStats,
|
||||
ProviderMapping,
|
||||
ProviderExchange,
|
||||
CreateExchangeRequest,
|
||||
UpdateExchangeRequest,
|
||||
CreateProviderMappingRequest,
|
||||
UpdateProviderMappingRequest,
|
||||
} from '../types';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:4000/api';
|
||||
|
||||
class ExchangeApiService {
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<ApiResponse<T>> {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'API request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Exchanges
|
||||
async getExchanges(): Promise<Exchange[]> {
|
||||
const response = await this.request<Exchange[]>('/exchanges');
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
async getExchangeById(id: string): Promise<ExchangeDetails | null> {
|
||||
const response = await this.request<ExchangeDetails>(`/exchanges/${id}`);
|
||||
return response.data || null;
|
||||
}
|
||||
|
||||
async createExchange(data: CreateExchangeRequest): Promise<Exchange> {
|
||||
const response = await this.request<Exchange>('/exchanges', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.data) {
|
||||
throw new Error('No exchange data returned');
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateExchange(id: string, data: UpdateExchangeRequest): Promise<Exchange> {
|
||||
const response = await this.request<Exchange>(`/exchanges/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.data) {
|
||||
throw new Error('No exchange data returned');
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Provider Mappings
|
||||
async getProviderMappings(provider?: string): Promise<ProviderMapping[]> {
|
||||
const endpoint = provider
|
||||
? `/exchanges/provider-mappings/${provider}`
|
||||
: '/exchanges/provider-mappings/all';
|
||||
|
||||
const response = await this.request<ProviderMapping[]>(endpoint);
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
async createProviderMapping(data: CreateProviderMappingRequest): Promise<ProviderMapping> {
|
||||
const response = await this.request<ProviderMapping>('/exchanges/provider-mappings', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.data) {
|
||||
throw new Error('No provider mapping data returned');
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateProviderMapping(
|
||||
id: string,
|
||||
data: UpdateProviderMappingRequest
|
||||
): Promise<ProviderMapping> {
|
||||
const response = await this.request<ProviderMapping>(`/exchanges/provider-mappings/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.data) {
|
||||
throw new Error('No provider mapping data returned');
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Providers and Utilities
|
||||
async getProviders(): Promise<string[]> {
|
||||
const response = await this.request<string[]>('/exchanges/providers/list');
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
async getUnmappedProviderExchanges(provider: string): Promise<ProviderExchange[]> {
|
||||
const response = await this.request<ProviderExchange[]>(
|
||||
`/exchanges/provider-exchanges/unmapped/${provider}`
|
||||
);
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
async getExchangeStats(): Promise<ExchangeStats> {
|
||||
const response = await this.request<ExchangeStats>('/exchanges/stats/summary');
|
||||
if (!response.data) {
|
||||
throw new Error('No exchange stats data returned');
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const exchangeApi = new ExchangeApiService();
|
||||
69
apps/web-app/src/features/exchanges/types/api.types.ts
Normal file
69
apps/web-app/src/features/exchanges/types/api.types.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// API Response types
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
// Base entity types
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProviderMapping extends BaseEntity {
|
||||
provider: string;
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name: string;
|
||||
master_exchange_id: string;
|
||||
country_code: string | null;
|
||||
currency: string | null;
|
||||
confidence: number;
|
||||
active: boolean;
|
||||
verified: boolean;
|
||||
auto_mapped: boolean;
|
||||
master_exchange_code?: string;
|
||||
master_exchange_name?: string;
|
||||
master_exchange_active?: boolean;
|
||||
}
|
||||
|
||||
export interface Exchange extends BaseEntity {
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active: boolean;
|
||||
visible: boolean;
|
||||
provider_mapping_count: string;
|
||||
active_mapping_count: string;
|
||||
verified_mapping_count: string;
|
||||
providers: string | null;
|
||||
provider_mappings: ProviderMapping[];
|
||||
}
|
||||
|
||||
export interface ExchangeDetails {
|
||||
exchange: Exchange;
|
||||
provider_mappings: ProviderMapping[];
|
||||
}
|
||||
|
||||
export interface ProviderExchange {
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name: string;
|
||||
country_code: string | null;
|
||||
currency: string | null;
|
||||
symbol_count: number | null;
|
||||
}
|
||||
|
||||
export interface ExchangeStats {
|
||||
total_exchanges: string;
|
||||
active_exchanges: string;
|
||||
countries: string;
|
||||
currencies: string;
|
||||
total_provider_mappings: string;
|
||||
active_provider_mappings: string;
|
||||
verified_provider_mappings: string;
|
||||
providers: string;
|
||||
}
|
||||
43
apps/web-app/src/features/exchanges/types/component.types.ts
Normal file
43
apps/web-app/src/features/exchanges/types/component.types.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Component-specific types
|
||||
export interface EditingCell {
|
||||
id: string;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface AddProviderMappingDialogState {
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
}
|
||||
|
||||
export interface DeleteDialogState {
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
providerMappingCount: number;
|
||||
}
|
||||
|
||||
export interface FormErrors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// Dialog props interfaces
|
||||
export interface BaseDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface AddExchangeDialogProps extends BaseDialogProps {
|
||||
onCreateExchange: (request: import('./request.types').CreateExchangeRequest) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AddProviderMappingDialogProps extends BaseDialogProps {
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
onCreateMapping: (request: import('./request.types').CreateProviderMappingRequest) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface DeleteExchangeDialogProps extends BaseDialogProps {
|
||||
exchangeId: string;
|
||||
exchangeName: string;
|
||||
providerMappingCount: number;
|
||||
onConfirmDelete: (exchangeId: string) => Promise<boolean>;
|
||||
}
|
||||
|
|
@ -1,100 +1,7 @@
|
|||
export interface ProviderMapping {
|
||||
id: string;
|
||||
provider: string;
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name: string;
|
||||
master_exchange_id: string;
|
||||
country_code: string | null;
|
||||
currency: string | null;
|
||||
confidence: number;
|
||||
active: boolean;
|
||||
verified: boolean;
|
||||
auto_mapped: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
master_exchange_code?: string;
|
||||
master_exchange_name?: string;
|
||||
master_exchange_active?: boolean;
|
||||
}
|
||||
// Re-export all types from organized files
|
||||
export * from './api.types';
|
||||
export * from './request.types';
|
||||
export * from './component.types';
|
||||
|
||||
export interface Exchange {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active: boolean;
|
||||
visible: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
provider_mapping_count: string;
|
||||
active_mapping_count: string;
|
||||
verified_mapping_count: string;
|
||||
providers: string | null;
|
||||
provider_mappings: ProviderMapping[];
|
||||
}
|
||||
|
||||
export interface ExchangeDetails {
|
||||
exchange: Exchange;
|
||||
provider_mappings: ProviderMapping[];
|
||||
}
|
||||
|
||||
export interface ExchangesApiResponse {
|
||||
success: boolean;
|
||||
data: Exchange[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UpdateExchangeRequest {
|
||||
name?: string;
|
||||
active?: boolean;
|
||||
visible?: boolean;
|
||||
country?: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProviderMappingRequest {
|
||||
active?: boolean;
|
||||
verified?: boolean;
|
||||
confidence?: number;
|
||||
master_exchange_id?: string;
|
||||
}
|
||||
|
||||
export interface CreateProviderMappingRequest {
|
||||
provider: string;
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name?: string;
|
||||
master_exchange_id: string;
|
||||
country_code?: string;
|
||||
currency?: string;
|
||||
confidence?: number;
|
||||
active?: boolean;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateExchangeRequest {
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface ExchangeStats {
|
||||
total_exchanges: string;
|
||||
active_exchanges: string;
|
||||
countries: string;
|
||||
currencies: string;
|
||||
total_provider_mappings: string;
|
||||
active_provider_mappings: string;
|
||||
verified_provider_mappings: string;
|
||||
providers: string;
|
||||
}
|
||||
|
||||
export interface ProviderExchange {
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name: string;
|
||||
country_code: string | null;
|
||||
currency: string | null;
|
||||
symbol_count: number | null;
|
||||
}
|
||||
// Legacy compatibility - can be removed later
|
||||
export type ExchangesApiResponse<T = unknown> = import('./api.types').ApiResponse<T>;
|
||||
|
|
|
|||
35
apps/web-app/src/features/exchanges/types/request.types.ts
Normal file
35
apps/web-app/src/features/exchanges/types/request.types.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Request types for API calls
|
||||
export interface CreateExchangeRequest {
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateExchangeRequest {
|
||||
name?: string;
|
||||
active?: boolean;
|
||||
visible?: boolean;
|
||||
country?: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export interface CreateProviderMappingRequest {
|
||||
provider: string;
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name?: string;
|
||||
master_exchange_id: string;
|
||||
country_code?: string;
|
||||
currency?: string;
|
||||
confidence?: number;
|
||||
active?: boolean;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateProviderMappingRequest {
|
||||
active?: boolean;
|
||||
verified?: boolean;
|
||||
confidence?: number;
|
||||
master_exchange_id?: string;
|
||||
}
|
||||
35
apps/web-app/src/features/exchanges/utils/formatters.ts
Normal file
35
apps/web-app/src/features/exchanges/utils/formatters.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { ProviderMapping } from '../types';
|
||||
|
||||
export function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
}
|
||||
|
||||
export function formatProviderMapping(mapping: ProviderMapping): string {
|
||||
return `${mapping.provider.toLowerCase()}(${mapping.provider_exchange_code})`;
|
||||
}
|
||||
|
||||
export function getProviderMappingColor(mapping: ProviderMapping): string {
|
||||
return mapping.active ? 'text-green-500' : 'text-text-muted';
|
||||
}
|
||||
|
||||
export function sortProviderMappings(mappings: ProviderMapping[]): ProviderMapping[] {
|
||||
return [...mappings].sort((a, b) => {
|
||||
// Active mappings first
|
||||
if (a.active && !b.active) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.active && b.active) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Then by provider name
|
||||
return a.provider.localeCompare(b.provider);
|
||||
});
|
||||
}
|
||||
|
||||
export function truncateText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, maxLength) + '...';
|
||||
}
|
||||
38
apps/web-app/src/features/exchanges/utils/validation.ts
Normal file
38
apps/web-app/src/features/exchanges/utils/validation.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { FormErrors } from '../types';
|
||||
|
||||
export function validateExchangeForm(data: {
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
}): FormErrors {
|
||||
const errors: FormErrors = {};
|
||||
|
||||
if (!data.code.trim()) {
|
||||
errors.code = 'Exchange code is required';
|
||||
} else if (data.code.length > 10) {
|
||||
errors.code = 'Exchange code must be 10 characters or less';
|
||||
}
|
||||
|
||||
if (!data.name.trim()) {
|
||||
errors.name = 'Exchange name is required';
|
||||
}
|
||||
|
||||
if (!data.country.trim()) {
|
||||
errors.country = 'Country is required';
|
||||
} else if (data.country.length !== 2) {
|
||||
errors.country = 'Country must be exactly 2 characters (e.g., US, CA, GB)';
|
||||
}
|
||||
|
||||
if (!data.currency.trim()) {
|
||||
errors.currency = 'Currency is required';
|
||||
} else if (data.currency.length !== 3) {
|
||||
errors.currency = 'Currency must be exactly 3 characters (e.g., USD, EUR, CAD)';
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function hasValidationErrors(errors: FormErrors): boolean {
|
||||
return Object.keys(errors).length > 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue