[OQI-004] feat: Add AccountDetail page and investment service

- Create AccountDetail.tsx with tabs for overview, transactions, distributions, deposit, withdraw
- Create investment.service.ts with full API client for investment endpoints
- Add routes /investment/portfolio, /investment/products, /investment/accounts/:accountId
- Integrate DepositForm and WithdrawForm components
- Add canvas-based performance chart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 09:16:24 -06:00
parent fd54724ede
commit e158d4228e
3 changed files with 861 additions and 0 deletions

View File

@ -27,6 +27,9 @@ const Trading = lazy(() => import('./modules/trading/pages/Trading'));
const MLDashboard = lazy(() => import('./modules/ml/pages/MLDashboard'));
const BacktestingDashboard = lazy(() => import('./modules/backtesting/pages/BacktestingDashboard'));
const Investment = lazy(() => import('./modules/investment/pages/Investment'));
const InvestmentPortfolio = lazy(() => import('./modules/investment/pages/Portfolio'));
const InvestmentProducts = lazy(() => import('./modules/investment/pages/Products'));
const AccountDetail = lazy(() => import('./modules/investment/pages/AccountDetail'));
const Settings = lazy(() => import('./modules/settings/pages/Settings'));
const Assistant = lazy(() => import('./modules/assistant/pages/Assistant'));
@ -87,6 +90,9 @@ function App() {
<Route path="/ml-dashboard" element={<MLDashboard />} />
<Route path="/backtesting" element={<BacktestingDashboard />} />
<Route path="/investment" element={<Investment />} />
<Route path="/investment/portfolio" element={<InvestmentPortfolio />} />
<Route path="/investment/products" element={<InvestmentProducts />} />
<Route path="/investment/accounts/:accountId" element={<AccountDetail />} />
{/* Portfolio Manager */}
<Route path="/portfolio" element={<PortfolioDashboard />} />

View File

@ -0,0 +1,608 @@
/**
* Account Detail Page
* Shows detailed information about a specific investment account
*/
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { ArrowLeft, TrendingUp, TrendingDown, Calendar, Clock, DollarSign, AlertCircle } from 'lucide-react';
import investmentService, { AccountDetail as AccountDetailType, Transaction, Distribution } from '../../../services/investment.service';
import { DepositForm } from '../components/DepositForm';
import { WithdrawForm } from '../components/WithdrawForm';
// ============================================================================
// Types
// ============================================================================
type TabType = 'overview' | 'transactions' | 'distributions' | 'deposit' | 'withdraw';
// ============================================================================
// Subcomponents
// ============================================================================
interface StatCardProps {
label: string;
value: string;
subValue?: string;
icon: React.ReactNode;
trend?: 'up' | 'down' | 'neutral';
}
const StatCard: React.FC<StatCardProps> = ({ label, value, subValue, icon, trend }) => {
const trendColors = {
up: 'text-green-500',
down: 'text-red-500',
neutral: 'text-gray-500',
};
return (
<div className="bg-white dark:bg-gray-800 rounded-xl p-5 shadow-lg">
<div className="flex items-start justify-between">
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30">{icon}</div>
{trend && (
<span className={trendColors[trend]}>
{trend === 'up' ? <TrendingUp size={20} /> : trend === 'down' ? <TrendingDown size={20} /> : null}
</span>
)}
</div>
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">{label}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{value}</p>
{subValue && <p className={`text-sm ${trend ? trendColors[trend] : 'text-gray-500'}`}>{subValue}</p>}
</div>
);
};
interface TransactionRowProps {
transaction: Transaction;
}
const TransactionRow: React.FC<TransactionRowProps> = ({ transaction }) => {
const typeIcons: Record<string, string> = {
deposit: '📥',
withdrawal: '📤',
distribution: '💰',
fee: '📋',
adjustment: '🔄',
};
const statusColors: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
completed: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
failed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
cancelled: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
};
const isCredit = ['deposit', 'distribution'].includes(transaction.type);
return (
<div className="flex items-center justify-between py-4 border-b border-gray-200 dark:border-gray-700 last:border-0">
<div className="flex items-center gap-4">
<span className="text-2xl">{typeIcons[transaction.type] || '📋'}</span>
<div>
<p className="font-medium text-gray-900 dark:text-white capitalize">
{transaction.type.replace('_', ' ')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(transaction.createdAt).toLocaleDateString()} {new Date(transaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
</div>
</div>
<div className="text-right">
<p className={`font-bold ${isCredit ? 'text-green-500' : 'text-red-500'}`}>
{isCredit ? '+' : '-'}${Math.abs(transaction.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}
</p>
<span className={`text-xs px-2 py-1 rounded-full ${statusColors[transaction.status]}`}>
{transaction.status}
</span>
</div>
</div>
);
};
interface DistributionRowProps {
distribution: Distribution;
}
const DistributionRow: React.FC<DistributionRowProps> = ({ distribution }) => {
return (
<div className="flex items-center justify-between py-4 border-b border-gray-200 dark:border-gray-700 last:border-0">
<div className="flex items-center gap-4">
<span className="text-2xl">💰</span>
<div>
<p className="font-medium text-gray-900 dark:text-white">
Distribución de Rendimientos
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(distribution.distributedAt).toLocaleDateString()} - Tasa: {(distribution.rate * 100).toFixed(4)}%
</p>
</div>
</div>
<div className="text-right">
<p className="font-bold text-green-500">
+${distribution.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Balance: ${distribution.balanceAfter.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</p>
</div>
</div>
);
};
// ============================================================================
// Performance Chart (Simple Canvas)
// ============================================================================
interface PerformanceChartProps {
data: Array<{ date: string; balance: number; pnl: number }>;
}
const PerformanceChart: React.FC<PerformanceChartProps> = ({ data }) => {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || data.length === 0) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const width = canvas.width;
const height = canvas.height;
const padding = { top: 20, right: 20, bottom: 30, left: 60 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Calculate min/max
const balances = data.map(d => d.balance);
const minBalance = Math.min(...balances) * 0.99;
const maxBalance = Math.max(...balances) * 1.01;
// Draw grid lines
ctx.strokeStyle = '#374151';
ctx.lineWidth = 0.5;
for (let i = 0; i <= 4; i++) {
const y = padding.top + (chartHeight / 4) * i;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(width - padding.right, y);
ctx.stroke();
// Y-axis labels
const value = maxBalance - ((maxBalance - minBalance) / 4) * i;
ctx.fillStyle = '#9CA3AF';
ctx.font = '12px system-ui';
ctx.textAlign = 'right';
ctx.fillText(`$${value.toLocaleString(undefined, { maximumFractionDigits: 0 })}`, padding.left - 10, y + 4);
}
// Draw line
ctx.beginPath();
ctx.strokeStyle = '#3B82F6';
ctx.lineWidth = 2;
data.forEach((point, i) => {
const x = padding.left + (chartWidth / (data.length - 1)) * i;
const y = padding.top + chartHeight - ((point.balance - minBalance) / (maxBalance - minBalance)) * chartHeight;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Draw gradient fill
const gradient = ctx.createLinearGradient(0, padding.top, 0, height - padding.bottom);
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
gradient.addColorStop(1, 'rgba(59, 130, 246, 0)');
ctx.beginPath();
data.forEach((point, i) => {
const x = padding.left + (chartWidth / (data.length - 1)) * i;
const y = padding.top + chartHeight - ((point.balance - minBalance) / (maxBalance - minBalance)) * chartHeight;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.lineTo(padding.left + chartWidth, height - padding.bottom);
ctx.lineTo(padding.left, height - padding.bottom);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
// X-axis labels (show a few dates)
const labelIndices = [0, Math.floor(data.length / 2), data.length - 1];
ctx.fillStyle = '#9CA3AF';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
labelIndices.forEach(i => {
if (data[i]) {
const x = padding.left + (chartWidth / (data.length - 1)) * i;
const date = new Date(data[i].date);
ctx.fillText(date.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }), x, height - 10);
}
});
}, [data]);
return (
<canvas
ref={canvasRef}
width={800}
height={300}
className="w-full h-auto"
/>
);
};
// ============================================================================
// Main Component
// ============================================================================
export const AccountDetail: React.FC = () => {
const { accountId } = useParams<{ accountId: string }>();
const navigate = useNavigate();
const [account, setAccount] = useState<AccountDetailType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<TabType>('overview');
const [allTransactions, setAllTransactions] = useState<Transaction[]>([]);
const [loadingMore, setLoadingMore] = useState(false);
useEffect(() => {
if (accountId) {
loadAccount();
}
}, [accountId]);
const loadAccount = async () => {
if (!accountId) return;
try {
setLoading(true);
setError(null);
const data = await investmentService.getAccountById(accountId);
setAccount(data);
setAllTransactions(data.recentTransactions || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error loading account');
} finally {
setLoading(false);
}
};
const loadMoreTransactions = async () => {
if (!accountId || loadingMore) return;
try {
setLoadingMore(true);
const { transactions } = await investmentService.getTransactions(accountId, {
limit: 20,
offset: allTransactions.length,
});
setAllTransactions(prev => [...prev, ...transactions]);
} catch (err) {
console.error('Error loading transactions:', err);
} finally {
setLoadingMore(false);
}
};
const handleDepositSuccess = () => {
setActiveTab('overview');
loadAccount();
};
const handleWithdrawSuccess = () => {
setActiveTab('overview');
loadAccount();
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-96">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
if (error || !account) {
return (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-6 text-center">
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-bold text-red-700 dark:text-red-400 mb-2">
Error al cargar la cuenta
</h2>
<p className="text-red-600 dark:text-red-300 mb-4">{error || 'Cuenta no encontrada'}</p>
<button
onClick={() => navigate('/investment/portfolio')}
className="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Volver al Portfolio
</button>
</div>
</div>
);
}
const totalReturn = account.balance - account.totalDeposited + account.totalWithdrawn;
const returnPercent = account.totalDeposited > 0 ? (totalReturn / account.totalDeposited) * 100 : 0;
const tabs: { key: TabType; label: string }[] = [
{ key: 'overview', label: 'Resumen' },
{ key: 'transactions', label: 'Transacciones' },
{ key: 'distributions', label: 'Distribuciones' },
{ key: 'deposit', label: 'Depositar' },
{ key: 'withdraw', label: 'Retirar' },
];
return (
<div className="max-w-6xl mx-auto px-4 py-8">
{/* Header */}
<div className="flex items-center gap-4 mb-8">
<button
onClick={() => navigate('/investment/portfolio')}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<ArrowLeft className="w-5 h-5 text-gray-600 dark:text-gray-400" />
</button>
<div className="flex-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{account.product.name}
</h1>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
account.status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: account.status === 'suspended'
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400'
}`}>
{account.status === 'active' ? 'Activa' : account.status === 'suspended' ? 'Suspendida' : 'Cerrada'}
</span>
</div>
<p className="text-gray-500 dark:text-gray-400">
Cuenta #{account.accountNumber} - Desde {new Date(account.openedAt).toLocaleDateString()}
</p>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<StatCard
label="Balance Actual"
value={`$${account.balance.toLocaleString(undefined, { minimumFractionDigits: 2 })}`}
icon={<DollarSign className="w-5 h-5 text-blue-600" />}
/>
<StatCard
label="Total Invertido"
value={`$${account.totalDeposited.toLocaleString(undefined, { minimumFractionDigits: 2 })}`}
icon={<TrendingUp className="w-5 h-5 text-purple-600" />}
/>
<StatCard
label="Ganancias"
value={`$${Math.abs(totalReturn).toLocaleString(undefined, { minimumFractionDigits: 2 })}`}
subValue={`${returnPercent >= 0 ? '+' : ''}${returnPercent.toFixed(2)}%`}
icon={<TrendingUp className="w-5 h-5 text-green-600" />}
trend={totalReturn >= 0 ? 'up' : 'down'}
/>
<StatCard
label="Total Retirado"
value={`$${account.totalWithdrawn.toLocaleString(undefined, { minimumFractionDigits: 2 })}`}
icon={<Clock className="w-5 h-5 text-orange-600" />}
/>
</div>
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700 mb-6">
<div className="flex gap-1 overflow-x-auto">
{tabs.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.key
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Tab Content */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
{activeTab === 'overview' && (
<div className="space-y-8">
{/* Performance Chart */}
{account.performanceHistory && account.performanceHistory.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Rendimiento Histórico
</h3>
<PerformanceChart data={account.performanceHistory} />
</div>
)}
{/* Account Info */}
<div className="grid md:grid-cols-2 gap-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Información de la Cuenta
</h3>
<dl className="space-y-3">
<div className="flex justify-between">
<dt className="text-gray-500 dark:text-gray-400">Producto</dt>
<dd className="font-medium text-gray-900 dark:text-white">{account.product.name}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500 dark:text-gray-400">Perfil de Riesgo</dt>
<dd className="font-medium text-gray-900 dark:text-white capitalize">{account.product.riskProfile}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500 dark:text-gray-400">Inversión Inicial</dt>
<dd className="font-medium text-gray-900 dark:text-white">
${account.initialInvestment.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500 dark:text-gray-400">Fecha de Apertura</dt>
<dd className="font-medium text-gray-900 dark:text-white">
{new Date(account.openedAt).toLocaleDateString()}
</dd>
</div>
</dl>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Últimas Transacciones
</h3>
{account.recentTransactions && account.recentTransactions.length > 0 ? (
<div>
{account.recentTransactions.slice(0, 5).map(tx => (
<TransactionRow key={tx.id} transaction={tx} />
))}
{account.recentTransactions.length > 5 && (
<button
onClick={() => setActiveTab('transactions')}
className="w-full mt-4 text-center text-blue-600 hover:text-blue-700 text-sm font-medium"
>
Ver todas las transacciones
</button>
)}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
No hay transacciones recientes
</p>
)}
</div>
</div>
</div>
)}
{activeTab === 'transactions' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Historial de Transacciones
</h3>
{allTransactions.length > 0 ? (
<div>
{allTransactions.map(tx => (
<TransactionRow key={tx.id} transaction={tx} />
))}
<button
onClick={loadMoreTransactions}
disabled={loadingMore}
className="w-full mt-4 py-3 text-center text-blue-600 hover:text-blue-700 text-sm font-medium disabled:opacity-50"
>
{loadingMore ? 'Cargando...' : 'Cargar más'}
</button>
</div>
) : (
<p className="text-gray-500 dark:text-gray-400 text-center py-12">
No hay transacciones registradas
</p>
)}
</div>
)}
{activeTab === 'distributions' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Historial de Distribuciones
</h3>
{account.recentDistributions && account.recentDistributions.length > 0 ? (
<div>
{account.recentDistributions.map(dist => (
<DistributionRow key={dist.id} distribution={dist} />
))}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400 text-center py-12">
No hay distribuciones registradas
</p>
)}
</div>
)}
{activeTab === 'deposit' && (
<div className="max-w-lg mx-auto">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Depositar Fondos
</h3>
<DepositForm
accounts={[
{
id: account.id,
accountNumber: account.accountNumber,
productName: account.product.name,
currentBalance: account.balance,
},
]}
onSuccess={handleDepositSuccess}
onCancel={() => setActiveTab('overview')}
/>
</div>
)}
{activeTab === 'withdraw' && (
<div className="max-w-lg mx-auto">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Solicitar Retiro
</h3>
<WithdrawForm
accounts={[
{
id: account.id,
accountNumber: account.accountNumber,
productName: account.product.name,
currentBalance: account.balance,
},
]}
onSuccess={handleWithdrawSuccess}
onCancel={() => setActiveTab('overview')}
/>
</div>
)}
</div>
{/* Quick Actions */}
{activeTab === 'overview' && account.status === 'active' && (
<div className="mt-6 flex flex-wrap gap-4">
<button
onClick={() => setActiveTab('deposit')}
className="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
Depositar
</button>
<button
onClick={() => setActiveTab('withdraw')}
className="px-6 py-3 bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white font-medium rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
Retirar
</button>
<Link
to="/investment/products"
className="px-6 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Ver Productos
</Link>
</div>
)}
</div>
);
};
export default AccountDetail;

View File

@ -0,0 +1,247 @@
/**
* Investment Service
* API client for investment accounts, products, transactions, and withdrawals
*/
import axios from 'axios';
const API_BASE = '/api/v1/investment';
// ============================================================================
// Types
// ============================================================================
export interface Product {
id: string;
code: string;
name: string;
description: string;
riskProfile: 'conservative' | 'moderate' | 'aggressive';
targetReturnMin: number;
targetReturnMax: number;
maxDrawdown: number;
minInvestment: number;
managementFee: number;
performanceFee: number;
isActive: boolean;
}
export interface ProductPerformance {
date: string;
cumulativeReturn: number;
dailyReturn: number;
}
export interface InvestmentAccount {
id: string;
accountNumber: string;
productId: string;
product: {
code: string;
name: string;
riskProfile: string;
};
status: 'active' | 'suspended' | 'closed';
balance: number;
initialInvestment: number;
totalDeposited: number;
totalWithdrawn: number;
totalEarnings: number;
unrealizedPnl: number;
unrealizedPnlPercent: number;
openedAt: string;
closedAt?: string;
}
export interface AccountSummary {
totalBalance: number;
totalEarnings: number;
totalDeposited: number;
totalWithdrawn: number;
overallReturn: number;
overallReturnPercent: number;
accounts: InvestmentAccount[];
}
export interface Transaction {
id: string;
accountId: string;
type: 'deposit' | 'withdrawal' | 'distribution' | 'fee' | 'adjustment';
amount: number;
balanceAfter: number;
status: 'pending' | 'completed' | 'failed' | 'cancelled';
description: string;
reference?: string;
createdAt: string;
completedAt?: string;
}
export interface Withdrawal {
id: string;
accountId: string;
amount: number;
status: 'pending' | 'approved' | 'processing' | 'completed' | 'rejected';
requestedAt: string;
processedAt?: string;
bankInfo?: {
bankName: string;
accountLast4: string;
};
cryptoInfo?: {
network: string;
addressLast8: string;
};
rejectionReason?: string;
}
export interface Distribution {
id: string;
accountId: string;
amount: number;
rate: number;
balanceBefore: number;
balanceAfter: number;
distributedAt: string;
}
export interface AccountDetail extends InvestmentAccount {
recentTransactions: Transaction[];
recentDistributions: Distribution[];
performanceHistory: Array<{
date: string;
balance: number;
pnl: number;
}>;
}
// ============================================================================
// API Functions - Products
// ============================================================================
export async function getProducts(riskProfile?: string): Promise<Product[]> {
const params = riskProfile ? { riskProfile } : {};
const response = await axios.get(`${API_BASE}/products`, { params });
return response.data.data;
}
export async function getProductById(productId: string): Promise<Product> {
const response = await axios.get(`${API_BASE}/products/${productId}`);
return response.data.data;
}
export async function getProductPerformance(
productId: string,
period: 'week' | 'month' | '3months' | 'year' = 'month'
): Promise<ProductPerformance[]> {
const response = await axios.get(`${API_BASE}/products/${productId}/performance`, {
params: { period },
});
return response.data.data;
}
// ============================================================================
// API Functions - Accounts
// ============================================================================
export async function getUserAccounts(): Promise<InvestmentAccount[]> {
const response = await axios.get(`${API_BASE}/accounts`);
return response.data.data;
}
export async function getAccountSummary(): Promise<AccountSummary> {
const response = await axios.get(`${API_BASE}/accounts/summary`);
return response.data.data;
}
export async function getAccountById(accountId: string): Promise<AccountDetail> {
const response = await axios.get(`${API_BASE}/accounts/${accountId}`);
return response.data.data;
}
export async function createAccount(productId: string, initialDeposit: number): Promise<InvestmentAccount> {
const response = await axios.post(`${API_BASE}/accounts`, { productId, initialDeposit });
return response.data.data;
}
export async function closeAccount(accountId: string): Promise<void> {
await axios.post(`${API_BASE}/accounts/${accountId}/close`);
}
// ============================================================================
// API Functions - Transactions
// ============================================================================
export async function getTransactions(
accountId: string,
options?: {
type?: string;
status?: string;
limit?: number;
offset?: number;
}
): Promise<{ transactions: Transaction[]; total: number }> {
const response = await axios.get(`${API_BASE}/accounts/${accountId}/transactions`, {
params: options,
});
return response.data.data;
}
export async function createDeposit(accountId: string, amount: number): Promise<Transaction> {
const response = await axios.post(`${API_BASE}/accounts/${accountId}/deposit`, { amount });
return response.data.data;
}
export async function createWithdrawal(
accountId: string,
amount: number,
destination: {
bankInfo?: { bankName: string; accountNumber: string; routingNumber: string };
cryptoInfo?: { network: string; address: string };
}
): Promise<Withdrawal> {
const response = await axios.post(`${API_BASE}/accounts/${accountId}/withdraw`, {
amount,
...destination,
});
return response.data.data;
}
// ============================================================================
// API Functions - Distributions
// ============================================================================
export async function getDistributions(accountId: string): Promise<Distribution[]> {
const response = await axios.get(`${API_BASE}/accounts/${accountId}/distributions`);
return response.data.data;
}
// ============================================================================
// API Functions - Withdrawals
// ============================================================================
export async function getWithdrawals(status?: string): Promise<Withdrawal[]> {
const params = status ? { status } : {};
const response = await axios.get(`${API_BASE}/withdrawals`, { params });
return response.data.data;
}
export default {
// Products
getProducts,
getProductById,
getProductPerformance,
// Accounts
getUserAccounts,
getAccountSummary,
getAccountById,
createAccount,
closeAccount,
// Transactions
getTransactions,
createDeposit,
createWithdrawal,
// Distributions
getDistributions,
// Withdrawals
getWithdrawals,
};