From e158d4228efa1b2fcf2dedf9bf87e4498b764d9e Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 09:16:24 -0600 Subject: [PATCH] [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 --- src/App.tsx | 6 + .../investment/pages/AccountDetail.tsx | 608 ++++++++++++++++++ src/services/investment.service.ts | 247 +++++++ 3 files changed, 861 insertions(+) create mode 100644 src/modules/investment/pages/AccountDetail.tsx create mode 100644 src/services/investment.service.ts diff --git a/src/App.tsx b/src/App.tsx index 28af05d..c1df3b1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> + } /> + } /> + } /> {/* Portfolio Manager */} } /> diff --git a/src/modules/investment/pages/AccountDetail.tsx b/src/modules/investment/pages/AccountDetail.tsx new file mode 100644 index 0000000..759bce6 --- /dev/null +++ b/src/modules/investment/pages/AccountDetail.tsx @@ -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 = ({ label, value, subValue, icon, trend }) => { + const trendColors = { + up: 'text-green-500', + down: 'text-red-500', + neutral: 'text-gray-500', + }; + + return ( +
+
+
{icon}
+ {trend && ( + + {trend === 'up' ? : trend === 'down' ? : null} + + )} +
+

{label}

+

{value}

+ {subValue &&

{subValue}

} +
+ ); +}; + +interface TransactionRowProps { + transaction: Transaction; +} + +const TransactionRow: React.FC = ({ transaction }) => { + const typeIcons: Record = { + deposit: '📥', + withdrawal: '📤', + distribution: '💰', + fee: '📋', + adjustment: '🔄', + }; + + const statusColors: Record = { + 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 ( +
+
+ {typeIcons[transaction.type] || '📋'} +
+

+ {transaction.type.replace('_', ' ')} +

+

+ {new Date(transaction.createdAt).toLocaleDateString()} {new Date(transaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+
+
+
+

+ {isCredit ? '+' : '-'}${Math.abs(transaction.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })} +

+ + {transaction.status} + +
+
+ ); +}; + +interface DistributionRowProps { + distribution: Distribution; +} + +const DistributionRow: React.FC = ({ distribution }) => { + return ( +
+
+ 💰 +
+

+ Distribución de Rendimientos +

+

+ {new Date(distribution.distributedAt).toLocaleDateString()} - Tasa: {(distribution.rate * 100).toFixed(4)}% +

+
+
+
+

+ +${distribution.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })} +

+

+ Balance: ${distribution.balanceAfter.toLocaleString(undefined, { minimumFractionDigits: 2 })} +

+
+
+ ); +}; + +// ============================================================================ +// Performance Chart (Simple Canvas) +// ============================================================================ + +interface PerformanceChartProps { + data: Array<{ date: string; balance: number; pnl: number }>; +} + +const PerformanceChart: React.FC = ({ data }) => { + const canvasRef = React.useRef(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 ( + + ); +}; + +// ============================================================================ +// Main Component +// ============================================================================ + +export const AccountDetail: React.FC = () => { + const { accountId } = useParams<{ accountId: string }>(); + const navigate = useNavigate(); + + const [account, setAccount] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState('overview'); + const [allTransactions, setAllTransactions] = useState([]); + 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 ( +
+
+
+ ); + } + + if (error || !account) { + return ( +
+
+ +

+ Error al cargar la cuenta +

+

{error || 'Cuenta no encontrada'}

+ +
+
+ ); + } + + 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 ( +
+ {/* Header */} +
+ +
+
+

+ {account.product.name} +

+ + {account.status === 'active' ? 'Activa' : account.status === 'suspended' ? 'Suspendida' : 'Cerrada'} + +
+

+ Cuenta #{account.accountNumber} - Desde {new Date(account.openedAt).toLocaleDateString()} +

+
+
+ + {/* Stats Grid */} +
+ } + /> + } + /> + = 0 ? '+' : ''}${returnPercent.toFixed(2)}%`} + icon={} + trend={totalReturn >= 0 ? 'up' : 'down'} + /> + } + /> +
+ + {/* Tabs */} +
+
+ {tabs.map(tab => ( + + ))} +
+
+ + {/* Tab Content */} +
+ {activeTab === 'overview' && ( +
+ {/* Performance Chart */} + {account.performanceHistory && account.performanceHistory.length > 0 && ( +
+

+ Rendimiento Histórico +

+ +
+ )} + + {/* Account Info */} +
+
+

+ Información de la Cuenta +

+
+
+
Producto
+
{account.product.name}
+
+
+
Perfil de Riesgo
+
{account.product.riskProfile}
+
+
+
Inversión Inicial
+
+ ${account.initialInvestment.toLocaleString(undefined, { minimumFractionDigits: 2 })} +
+
+
+
Fecha de Apertura
+
+ {new Date(account.openedAt).toLocaleDateString()} +
+
+
+
+ +
+

+ Últimas Transacciones +

+ {account.recentTransactions && account.recentTransactions.length > 0 ? ( +
+ {account.recentTransactions.slice(0, 5).map(tx => ( + + ))} + {account.recentTransactions.length > 5 && ( + + )} +
+ ) : ( +

+ No hay transacciones recientes +

+ )} +
+
+
+ )} + + {activeTab === 'transactions' && ( +
+

+ Historial de Transacciones +

+ {allTransactions.length > 0 ? ( +
+ {allTransactions.map(tx => ( + + ))} + +
+ ) : ( +

+ No hay transacciones registradas +

+ )} +
+ )} + + {activeTab === 'distributions' && ( +
+

+ Historial de Distribuciones +

+ {account.recentDistributions && account.recentDistributions.length > 0 ? ( +
+ {account.recentDistributions.map(dist => ( + + ))} +
+ ) : ( +

+ No hay distribuciones registradas +

+ )} +
+ )} + + {activeTab === 'deposit' && ( +
+

+ Depositar Fondos +

+ setActiveTab('overview')} + /> +
+ )} + + {activeTab === 'withdraw' && ( +
+

+ Solicitar Retiro +

+ setActiveTab('overview')} + /> +
+ )} +
+ + {/* Quick Actions */} + {activeTab === 'overview' && account.status === 'active' && ( +
+ + + + Ver Productos + +
+ )} +
+ ); +}; + +export default AccountDetail; diff --git a/src/services/investment.service.ts b/src/services/investment.service.ts new file mode 100644 index 0000000..4a548f2 --- /dev/null +++ b/src/services/investment.service.ts @@ -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 { + const params = riskProfile ? { riskProfile } : {}; + const response = await axios.get(`${API_BASE}/products`, { params }); + return response.data.data; +} + +export async function getProductById(productId: string): Promise { + 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 { + const response = await axios.get(`${API_BASE}/products/${productId}/performance`, { + params: { period }, + }); + return response.data.data; +} + +// ============================================================================ +// API Functions - Accounts +// ============================================================================ + +export async function getUserAccounts(): Promise { + const response = await axios.get(`${API_BASE}/accounts`); + return response.data.data; +} + +export async function getAccountSummary(): Promise { + const response = await axios.get(`${API_BASE}/accounts/summary`); + return response.data.data; +} + +export async function getAccountById(accountId: string): Promise { + const response = await axios.get(`${API_BASE}/accounts/${accountId}`); + return response.data.data; +} + +export async function createAccount(productId: string, initialDeposit: number): Promise { + const response = await axios.post(`${API_BASE}/accounts`, { productId, initialDeposit }); + return response.data.data; +} + +export async function closeAccount(accountId: string): Promise { + 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 { + 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 { + 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 { + const response = await axios.get(`${API_BASE}/accounts/${accountId}/distributions`); + return response.data.data; +} + +// ============================================================================ +// API Functions - Withdrawals +// ============================================================================ + +export async function getWithdrawals(status?: string): Promise { + 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, +};