--- id: "ET-INV-005" title: "Componentes React Frontend" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-004" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-INV-005: Componentes React Frontend **Epic:** OQI-004 Cuentas de Inversión **Versión:** 1.0 **Fecha:** 2025-12-05 **Responsable:** Requirements-Analyst --- ## 1. Descripción Define la implementación frontend para el módulo de cuentas de inversión usando React 18, TypeScript y Zustand: - Páginas principales (Products, Portfolio, AccountDetail) - Componentes reutilizables - Estado global con Zustand - Integración con API backend - Formularios con validación --- ## 2. Arquitectura Frontend ``` ┌─────────────────────────────────────────────────────────────────┐ │ Frontend Architecture │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Pages Components Stores │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ │ │ │ │ │ │ ProductsPage │─────►│ ProductCard │ │ investment │ │ │ │ │ │ │ │ Store │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ▲ ▲ │ │ ┌──────────────┐ │ │ │ │ │ │ │ │ │ │ │ PortfolioPage├───────────────┼────────────────────┤ │ │ │ │ │ │ │ │ └──────────────┘ ┌──────────────┐ │ │ │ │ │ │ │ │ ┌──────────────┐ │DepositForm │ │ │ │ │ │ │ │ │ │ │ │AccountDetail │─────►│PerformanceChart◄──────────┘ │ │ │ Page │ │ │ │ │ └──────────────┘ │WithdrawalForm│ │ │ │ │ │ │ └──────────────┘ │ │ │ │ ┌──────────────┐ │ │ │ API Layer │ │ │ │ (Axios) │ │ │ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Estructura de Archivos ``` src/ ├── pages/ │ └── investment/ │ ├── ProductsPage.tsx │ ├── PortfolioPage.tsx │ ├── AccountDetailPage.tsx │ └── WithdrawalsPage.tsx ├── components/ │ └── investment/ │ ├── ProductCard.tsx │ ├── ProductList.tsx │ ├── AccountCard.tsx │ ├── DepositForm.tsx │ ├── WithdrawalForm.tsx │ ├── PerformanceChart.tsx │ ├── TransactionList.tsx │ └── PortfolioSummary.tsx ├── stores/ │ └── investmentStore.ts ├── api/ │ └── investment.api.ts ├── types/ │ └── investment.types.ts └── hooks/ └── useInvestment.ts ``` --- ## 4. Store con Zustand ### 4.1 Investment Store ```typescript // src/stores/investmentStore.ts import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { investmentApi } from '../api/investment.api'; import { Product, Account, Transaction, WithdrawalRequest, DailyPerformance, } from '../types/investment.types'; interface InvestmentState { // State products: Product[]; accounts: Account[]; selectedAccount: Account | null; transactions: Transaction[]; withdrawalRequests: WithdrawalRequest[]; dailyPerformance: DailyPerformance[]; portfolioSummary: any | null; // Loading states loading: { products: boolean; accounts: boolean; transactions: boolean; portfolio: boolean; }; // Error states error: string | null; // Actions fetchProducts: (filters?: any) => Promise; fetchAccounts: () => Promise; fetchAccountById: (id: string) => Promise; fetchPortfolio: () => Promise; fetchTransactions: (filters?: any) => Promise; fetchWithdrawalRequests: () => Promise; fetchPerformance: (accountId: string, filters?: any) => Promise; createAccount: (data: any) => Promise; deposit: (accountId: string, data: any) => Promise; requestWithdrawal: (accountId: string, data: any) => Promise; clearError: () => void; } export const useInvestmentStore = create()( devtools( (set, get) => ({ // Initial state products: [], accounts: [], selectedAccount: null, transactions: [], withdrawalRequests: [], dailyPerformance: [], portfolioSummary: null, loading: { products: false, accounts: false, transactions: false, portfolio: false, }, error: null, // Actions fetchProducts: async (filters) => { set((state) => ({ loading: { ...state.loading, products: true }, error: null, })); try { const response = await investmentApi.getProducts(filters); set({ products: response.data.products, loading: { ...get().loading, products: false }, }); } catch (error: any) { set({ error: error.message, loading: { ...get().loading, products: false }, }); } }, fetchAccounts: async () => { set((state) => ({ loading: { ...state.loading, accounts: true }, error: null, })); try { const response = await investmentApi.getAccounts(); set({ accounts: response.data.accounts, loading: { ...get().loading, accounts: false }, }); } catch (error: any) { set({ error: error.message, loading: { ...get().loading, accounts: false }, }); } }, fetchAccountById: async (id) => { try { const response = await investmentApi.getAccountById(id); set({ selectedAccount: response.data.account }); } catch (error: any) { set({ error: error.message }); } }, fetchPortfolio: async () => { set((state) => ({ loading: { ...state.loading, portfolio: true }, error: null, })); try { const response = await investmentApi.getPortfolio(); set({ portfolioSummary: response.data, loading: { ...get().loading, portfolio: false }, }); } catch (error: any) { set({ error: error.message, loading: { ...get().loading, portfolio: false }, }); } }, fetchTransactions: async (filters) => { set((state) => ({ loading: { ...state.loading, transactions: true }, error: null, })); try { const response = await investmentApi.getTransactions(filters); set({ transactions: response.data.transactions, loading: { ...get().loading, transactions: false }, }); } catch (error: any) { set({ error: error.message, loading: { ...get().loading, transactions: false }, }); } }, fetchWithdrawalRequests: async () => { try { const response = await investmentApi.getWithdrawalRequests(); set({ withdrawalRequests: response.data.requests }); } catch (error: any) { set({ error: error.message }); } }, fetchPerformance: async (accountId, filters) => { try { const response = await investmentApi.getPerformance(accountId, filters); set({ dailyPerformance: response.data.performance }); } catch (error: any) { set({ error: error.message }); } }, createAccount: async (data) => { set({ error: null }); try { const response = await investmentApi.createAccount(data); return response.data; } catch (error: any) { set({ error: error.message }); throw error; } }, deposit: async (accountId, data) => { set({ error: null }); try { const response = await investmentApi.deposit(accountId, data); return response.data; } catch (error: any) { set({ error: error.message }); throw error; } }, requestWithdrawal: async (accountId, data) => { set({ error: null }); try { const response = await investmentApi.requestWithdrawal(accountId, data); return response.data; } catch (error: any) { set({ error: error.message }); throw error; } }, clearError: () => set({ error: null }), }), { name: 'InvestmentStore' } ) ); ``` --- ## 5. API Layer ### 5.1 Investment API Client ```typescript // src/api/investment.api.ts import axios from './axios-instance'; import { AxiosResponse } from 'axios'; const BASE_PATH = '/api/v1/investment'; export const investmentApi = { // Products getProducts: (params?: any): Promise => { return axios.get(`${BASE_PATH}/products`, { params }); }, getProductById: (id: string): Promise => { return axios.get(`${BASE_PATH}/products/${id}`); }, // Accounts getAccounts: (params?: any): Promise => { return axios.get(`${BASE_PATH}/accounts`, { params }); }, getAccountById: (id: string): Promise => { return axios.get(`${BASE_PATH}/accounts/${id}`); }, createAccount: (data: { product_id: string; initial_investment: number; payment_method_id: string; }): Promise => { return axios.post(`${BASE_PATH}/accounts`, data); }, // Deposits deposit: ( accountId: string, data: { amount: number; payment_method_id: string } ): Promise => { return axios.post(`${BASE_PATH}/accounts/${accountId}/deposit`, data); }, // Withdrawals requestWithdrawal: ( accountId: string, data: { amount: number; withdrawal_method: string; destination_details: any; } ): Promise => { return axios.post(`${BASE_PATH}/accounts/${accountId}/withdraw`, data); }, getWithdrawalRequests: (params?: any): Promise => { return axios.get(`${BASE_PATH}/withdrawal-requests`, { params }); }, // Portfolio getPortfolio: (): Promise => { return axios.get(`${BASE_PATH}/portfolio`); }, getPerformance: (accountId: string, params?: any): Promise => { return axios.get(`${BASE_PATH}/accounts/${accountId}/performance`, { params }); }, // Transactions getTransactions: (params?: any): Promise => { return axios.get(`${BASE_PATH}/transactions`, { params }); }, }; ``` --- ## 6. Páginas Principales ### 6.1 Products Page ```typescript // src/pages/investment/ProductsPage.tsx import React, { useEffect, useState } from 'react'; import { useInvestmentStore } from '../../stores/investmentStore'; import { ProductCard } from '../../components/investment/ProductCard'; import { DepositModal } from '../../components/investment/DepositModal'; import { Product } from '../../types/investment.types'; import './ProductsPage.css'; export const ProductsPage: React.FC = () => { const { products, loading, fetchProducts } = useInvestmentStore(); const [selectedProduct, setSelectedProduct] = useState(null); const [showDepositModal, setShowDepositModal] = useState(false); useEffect(() => { fetchProducts({ status: 'active' }); }, [fetchProducts]); const handleInvest = (product: Product) => { setSelectedProduct(product); setShowDepositModal(true); }; if (loading.products) { return
Loading products...
; } return (

Investment Products

Choose from our AI-powered trading agents

{products.map((product) => ( ))}
{showDepositModal && selectedProduct && ( setShowDepositModal(false)} /> )}
); }; ``` ### 6.2 Portfolio Page ```typescript // src/pages/investment/PortfolioPage.tsx import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useInvestmentStore } from '../../stores/investmentStore'; import { PortfolioSummary } from '../../components/investment/PortfolioSummary'; import { AccountCard } from '../../components/investment/AccountCard'; import './PortfolioPage.css'; export const PortfolioPage: React.FC = () => { const navigate = useNavigate(); const { portfolioSummary, loading, fetchPortfolio } = useInvestmentStore(); useEffect(() => { fetchPortfolio(); }, [fetchPortfolio]); if (loading.portfolio) { return
Loading portfolio...
; } if (!portfolioSummary) { return (

No investments yet

Start investing in AI-powered trading agents

); } return (

My Portfolio

My Accounts

{portfolioSummary.accounts.map((account: any) => ( navigate(`/investment/accounts/${account.account_id}`)} /> ))}

Risk Allocation

{/* Implementar gráfico de dona con allocation_by_risk */}
); }; ``` ### 6.3 Account Detail Page ```typescript // src/pages/investment/AccountDetailPage.tsx import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useInvestmentStore } from '../../stores/investmentStore'; import { PerformanceChart } from '../../components/investment/PerformanceChart'; import { TransactionList } from '../../components/investment/TransactionList'; import { DepositForm } from '../../components/investment/DepositForm'; import { WithdrawalForm } from '../../components/investment/WithdrawalForm'; import './AccountDetailPage.css'; export const AccountDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const { selectedAccount, dailyPerformance, fetchAccountById, fetchPerformance, fetchTransactions, } = useInvestmentStore(); const [activeTab, setActiveTab] = useState<'overview' | 'deposit' | 'withdraw'>('overview'); useEffect(() => { if (id) { fetchAccountById(id); fetchPerformance(id, { period: 'month' }); fetchTransactions({ account_id: id, limit: 20 }); } }, [id, fetchAccountById, fetchPerformance, fetchTransactions]); if (!selectedAccount) { return
Loading account...
; } return (

{selectedAccount.product.name}

{selectedAccount.status}
Current Balance
${selectedAccount.current_balance.toLocaleString('en-US', { minimumFractionDigits: 2, })}
{selectedAccount.total_return_percentage >= 0 ? '+' : ''} {selectedAccount.total_return_percentage?.toFixed(2)}%
{activeTab === 'overview' && ( <>

Performance

Total Invested
${selectedAccount.total_deposited.toLocaleString()}
Total Profit
${(selectedAccount.current_balance - selectedAccount.total_deposited).toLocaleString()}
Annualized Return
{selectedAccount.annualized_return_percentage?.toFixed(2)}%

Recent Transactions

)} {activeTab === 'deposit' && ( { fetchAccountById(selectedAccount.id); setActiveTab('overview'); }} /> )} {activeTab === 'withdraw' && ( { fetchAccountById(selectedAccount.id); setActiveTab('overview'); }} /> )}
); }; ``` --- ## 7. Componentes Reutilizables ### 7.1 Product Card ```typescript // src/components/investment/ProductCard.tsx import React from 'react'; import { Product } from '../../types/investment.types'; import './ProductCard.css'; interface ProductCardProps { product: Product; onInvest: (product: Product) => void; } export const ProductCard: React.FC = ({ product, onInvest }) => { const riskColors = { low: 'green', medium: 'yellow', high: 'orange', very_high: 'red', }; return (

{product.name}

{product.risk_level.toUpperCase()}

{product.description}

Agent Type {product.agent_type}
Target Return {product.target_annual_return}%
Performance Fee {product.performance_fee_percentage}%
Min Investment ${product.min_investment}
{product.total_investors} investors
${(product.total_aum / 1000).toFixed(0)}K AUM
); }; ``` ### 7.2 Performance Chart ```typescript // src/components/investment/PerformanceChart.tsx import React from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts'; import { DailyPerformance } from '../../types/investment.types'; interface PerformanceChartProps { data: DailyPerformance[]; } export const PerformanceChart: React.FC = ({ data }) => { const chartData = data.map((item) => ({ date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', }), balance: item.closing_balance, return: item.cumulative_return_percentage, })); return ( ); }; ``` --- ## 8. Hooks Personalizados ```typescript // src/hooks/useInvestment.ts import { useEffect } from 'react'; import { useInvestmentStore } from '../stores/investmentStore'; export const usePortfolio = () => { const { portfolioSummary, loading, fetchPortfolio } = useInvestmentStore(); useEffect(() => { fetchPortfolio(); }, [fetchPortfolio]); return { portfolio: portfolioSummary, loading: loading.portfolio }; }; export const useAccount = (accountId: string) => { const { selectedAccount, fetchAccountById } = useInvestmentStore(); useEffect(() => { if (accountId) { fetchAccountById(accountId); } }, [accountId, fetchAccountById]); return { account: selectedAccount }; }; ``` --- ## 9. Configuración ### 9.1 Variables de Entorno ```bash # Frontend .env REACT_APP_API_URL=http://localhost:3000 REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_... ``` --- ## 10. Testing ### 10.1 Component Tests ```typescript // tests/components/ProductCard.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { ProductCard } from '../src/components/investment/ProductCard'; describe('ProductCard', () => { const mockProduct = { id: '1', name: 'Swing Trader Pro', risk_level: 'medium', // ... otros campos }; it('renders product information', () => { render(); expect(screen.getByText('Swing Trader Pro')).toBeInTheDocument(); }); it('calls onInvest when button clicked', () => { const onInvest = jest.fn(); render(); fireEvent.click(screen.getByText('Invest Now')); expect(onInvest).toHaveBeenCalledWith(mockProduct); }); }); ``` --- ## 11. Referencias - React 18 Documentation - Zustand State Management - Recharts for Charts - Stripe React Elements