refactored monorepo for more projects
This commit is contained in:
parent
4632c174dc
commit
9492f1b15e
180 changed files with 1438 additions and 424 deletions
|
|
@ -1,448 +0,0 @@
|
|||
import { DataTable } from '@/components/ui';
|
||||
import { PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useExchanges } from '../hooks/useExchanges';
|
||||
import { Exchange, 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 {
|
||||
exchanges,
|
||||
loading,
|
||||
error,
|
||||
updateExchange,
|
||||
updateProviderMapping,
|
||||
createProviderMapping,
|
||||
refetch
|
||||
} = useExchanges();
|
||||
|
||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [addProviderDialog, setAddProviderDialog] = useState<AddProviderMappingDialogState | null>(null);
|
||||
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
|
||||
|
||||
const handleCellEdit = useCallback(
|
||||
async (id: string, field: string, value: string) => {
|
||||
if (field === 'name' || field === 'code') {
|
||||
await updateExchange(id, { [field]: value });
|
||||
}
|
||||
setEditingCell(null);
|
||||
setEditValue('');
|
||||
},
|
||||
[updateExchange]
|
||||
);
|
||||
|
||||
const handleToggleActive = useCallback(
|
||||
async (id: string, currentStatus: boolean) => {
|
||||
await updateExchange(id, { active: !currentStatus });
|
||||
},
|
||||
[updateExchange]
|
||||
);
|
||||
|
||||
const handleAddProviderMapping = useCallback(async (exchangeId: string, exchangeName: string) => {
|
||||
setAddProviderDialog({ exchangeId, exchangeName });
|
||||
}, []);
|
||||
|
||||
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(
|
||||
async (mappingId: string, currentStatus: boolean) => {
|
||||
const success = await updateProviderMapping(mappingId, { active: !currentStatus });
|
||||
if (success) {
|
||||
// Refresh the main table data to get updated counts and mappings
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[updateProviderMapping, refetch]
|
||||
);
|
||||
|
||||
const handleRowExpand = useCallback(
|
||||
async (_row: any) => {
|
||||
// Row expansion is now handled automatically by TanStack Table
|
||||
// No need to fetch data since all mappings are already loaded
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const columns = useMemo<ColumnDef<Exchange>[]>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'expander',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return row.getCanExpand() ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
row.getToggleExpandedHandler()();
|
||||
handleRowExpand(row);
|
||||
}}
|
||||
className="text-text-secondary hover:text-text-primary transition-colors w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
{row.getIsExpanded() ? '▼' : '▶'}
|
||||
</button>
|
||||
) : null;
|
||||
},
|
||||
size: 40,
|
||||
enableResizing: false,
|
||||
disableTooltip: true,
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
header: 'Code',
|
||||
accessorKey: 'code',
|
||||
size: 120,
|
||||
cell: ({ getValue, row, cell: _cell }) => {
|
||||
const isEditing =
|
||||
editingCell?.id === row.original.id && editingCell?.field === 'code';
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => handleCellEdit(row.original.id, 'code', editValue)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCellEdit(row.original.id, 'code', editValue);
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingCell(null);
|
||||
setEditValue('');
|
||||
}
|
||||
}}
|
||||
className="w-full bg-surface border border-border rounded px-2 py-1 text-sm font-mono"
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer hover:bg-surface-secondary rounded px-2 py-1 transition-colors text-sm font-mono font-medium truncate overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
onClick={() => {
|
||||
setEditingCell({ id: row.original.id, field: 'code' });
|
||||
setEditValue(getValue() as string);
|
||||
}}
|
||||
>
|
||||
{getValue() as string}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
size: 250,
|
||||
cell: ({ getValue, row, cell: _cell }) => {
|
||||
const isEditing =
|
||||
editingCell?.id === row.original.id && editingCell?.field === 'name';
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={e => setEditValue(e.target.value)}
|
||||
onBlur={() => handleCellEdit(row.original.id, 'name', editValue)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCellEdit(row.original.id, 'name', editValue);
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingCell(null);
|
||||
setEditValue('');
|
||||
}
|
||||
}}
|
||||
className="w-full bg-surface border border-border rounded px-2 py-1 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer hover:bg-surface-secondary rounded px-2 py-1 transition-colors text-sm truncate overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
onClick={() => {
|
||||
setEditingCell({ id: row.original.id, field: 'name' });
|
||||
setEditValue(getValue() as string);
|
||||
}}
|
||||
>
|
||||
{getValue() as string}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'country',
|
||||
header: 'Country',
|
||||
accessorKey: 'country',
|
||||
size: 80,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-text-secondary text-sm block truncate">{getValue() as string}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'currency',
|
||||
header: 'Currency',
|
||||
accessorKey: 'currency',
|
||||
size: 80,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="font-mono text-text-secondary text-sm block truncate">
|
||||
{getValue() as string}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
header: 'Active',
|
||||
accessorKey: 'active',
|
||||
size: 80,
|
||||
disableTooltip: true,
|
||||
cell: ({ getValue, row }) => {
|
||||
const isActive = getValue() as boolean;
|
||||
return (
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isActive}
|
||||
onChange={() => handleToggleActive(row.original.id, isActive)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-surface-secondary peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary-500"></div>
|
||||
</label>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'provider_mappings',
|
||||
header: 'Provider Mappings',
|
||||
accessorKey: 'provider_mapping_count',
|
||||
size: 180,
|
||||
disableTooltip: true,
|
||||
cell: ({ getValue, row }) => {
|
||||
const totalMappings = parseInt(getValue() as string) || 0;
|
||||
const activeMappings = parseInt(row.original.active_mapping_count) || 0;
|
||||
const _verifiedMappings = parseInt(row.original.verified_mapping_count) || 0;
|
||||
|
||||
// Get provider mappings directly from the exchange data
|
||||
const mappings = row.original.provider_mappings || [];
|
||||
const sortedMappings = sortProviderMappings(mappings);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-sm">
|
||||
<span className="text-text-primary font-medium">{totalMappings}</span>
|
||||
<span className="text-text-muted"> total</span>
|
||||
{activeMappings > 0 && (
|
||||
<span className="ml-2 text-green-400 text-xs">
|
||||
{activeMappings} active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{mappings.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1 text-xs">
|
||||
{sortedMappings.slice(0, 3).map((mapping, index) => (
|
||||
<span key={index} className={getProviderMappingColor(mapping)}>
|
||||
{formatProviderMapping(mapping)}
|
||||
</span>
|
||||
))}
|
||||
{sortedMappings.length > 3 && (
|
||||
<span className="text-text-muted">+{sortedMappings.length - 3} more</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-text-muted">No mappings</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
size: 160,
|
||||
disableTooltip: true,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleAddProviderMapping(row.original.id, row.original.name)}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-primary-500/20 text-primary-400 rounded text-xs hover:bg-primary-500/30 transition-colors whitespace-nowrap"
|
||||
title="Add Provider Mapping"
|
||||
>
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'updated_at',
|
||||
header: 'Last Updated',
|
||||
accessorKey: 'updated_at',
|
||||
size: 120,
|
||||
cell: ({ getValue }) => (
|
||||
<span className="text-xs text-text-muted">
|
||||
{formatDate(getValue() as string)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
editingCell,
|
||||
editValue,
|
||||
handleCellEdit,
|
||||
handleToggleActive,
|
||||
handleAddProviderMapping,
|
||||
handleDeleteExchange,
|
||||
handleConfirmDelete,
|
||||
handleRowExpand,
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-danger/10 border border-danger/20 rounded-lg p-4">
|
||||
<h3 className="text-danger font-medium mb-2">Error Loading Exchanges</h3>
|
||||
<p className="text-text-secondary text-sm">{error}</p>
|
||||
<p className="text-text-muted text-xs mt-2">
|
||||
Make sure the web-api service is running on localhost:4000
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderSubComponent = ({ row }: { row: any }) => {
|
||||
const exchange = row.original as Exchange;
|
||||
const mappings = exchange.provider_mappings || [];
|
||||
|
||||
if (mappings.length === 0) {
|
||||
return (
|
||||
<div className="p-4 text-center text-text-muted">
|
||||
<div className="text-sm">No provider mappings found for this exchange.</div>
|
||||
<button
|
||||
onClick={() => handleAddProviderMapping(exchange.id, exchange.name)}
|
||||
className="mt-2 text-primary-400 hover:text-primary-300 text-sm underline"
|
||||
>
|
||||
Add the first provider mapping
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-surface-secondary/50">
|
||||
<h4 className="text-sm font-medium text-text-primary mb-3">Provider Mappings</h4>
|
||||
<div className="space-y-2">
|
||||
{mappings.map((mapping) => (
|
||||
<div key={mapping.id} className="flex items-center justify-between p-3 bg-surface rounded border border-border">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-xs bg-primary-500/20 text-primary-400 px-2 py-1 rounded">
|
||||
{mapping.provider.toUpperCase()}
|
||||
</span>
|
||||
<span className="font-medium text-text-primary">
|
||||
{mapping.provider_exchange_code}
|
||||
</span>
|
||||
<span className="text-text-secondary">
|
||||
{mapping.provider_exchange_name}
|
||||
</span>
|
||||
{mapping.country_code && (
|
||||
<span className="text-xs text-text-muted">
|
||||
{mapping.country_code}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-xs text-text-muted">
|
||||
<span>Confidence: {mapping.confidence}</span>
|
||||
<span>Created: {formatDate(mapping.created_at)}</span>
|
||||
{mapping.auto_mapped && <span className="text-yellow-400">Auto-mapped</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{mapping.verified && (
|
||||
<span className="text-blue-400" title="Verified">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mapping.active}
|
||||
onChange={() => handleToggleProviderMapping(mapping.id, mapping.active)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-6 h-3 bg-surface-secondary peer-focus:outline-none peer-focus:ring-1 peer-focus:ring-primary-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] after:left-[1px] after:bg-white after:rounded-full after:h-2.5 after:w-2.5 after:transition-all peer-checked:bg-primary-500"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={exchanges || []}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
getRowCanExpand={() => true}
|
||||
renderSubComponent={renderSubComponent}
|
||||
/>
|
||||
|
||||
{addProviderDialog && (
|
||||
<AddProviderMappingDialog
|
||||
isOpen={true}
|
||||
exchangeId={addProviderDialog.exchangeId}
|
||||
exchangeName={addProviderDialog.exchangeName}
|
||||
onClose={() => setAddProviderDialog(null)}
|
||||
onCreateMapping={async (mappingRequest) => {
|
||||
const result = await createProviderMapping(mappingRequest);
|
||||
if (result) {
|
||||
setAddProviderDialog(null);
|
||||
refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteDialog && (
|
||||
<DeleteExchangeDialog
|
||||
isOpen={true}
|
||||
exchangeId={deleteDialog.exchangeId}
|
||||
exchangeName={deleteDialog.exchangeName}
|
||||
providerMappingCount={deleteDialog.providerMappingCount}
|
||||
onClose={() => setDeleteDialog(null)}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue