901 lines
27 KiB
Markdown
901 lines
27 KiB
Markdown
# 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<void>;
|
|
fetchAccounts: () => Promise<void>;
|
|
fetchAccountById: (id: string) => Promise<void>;
|
|
fetchPortfolio: () => Promise<void>;
|
|
fetchTransactions: (filters?: any) => Promise<void>;
|
|
fetchWithdrawalRequests: () => Promise<void>;
|
|
fetchPerformance: (accountId: string, filters?: any) => Promise<void>;
|
|
createAccount: (data: any) => Promise<any>;
|
|
deposit: (accountId: string, data: any) => Promise<any>;
|
|
requestWithdrawal: (accountId: string, data: any) => Promise<any>;
|
|
clearError: () => void;
|
|
}
|
|
|
|
export const useInvestmentStore = create<InvestmentState>()(
|
|
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<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/products`, { params });
|
|
},
|
|
|
|
getProductById: (id: string): Promise<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/products/${id}`);
|
|
},
|
|
|
|
// Accounts
|
|
getAccounts: (params?: any): Promise<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/accounts`, { params });
|
|
},
|
|
|
|
getAccountById: (id: string): Promise<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/accounts/${id}`);
|
|
},
|
|
|
|
createAccount: (data: {
|
|
product_id: string;
|
|
initial_investment: number;
|
|
payment_method_id: string;
|
|
}): Promise<AxiosResponse> => {
|
|
return axios.post(`${BASE_PATH}/accounts`, data);
|
|
},
|
|
|
|
// Deposits
|
|
deposit: (
|
|
accountId: string,
|
|
data: { amount: number; payment_method_id: string }
|
|
): Promise<AxiosResponse> => {
|
|
return axios.post(`${BASE_PATH}/accounts/${accountId}/deposit`, data);
|
|
},
|
|
|
|
// Withdrawals
|
|
requestWithdrawal: (
|
|
accountId: string,
|
|
data: {
|
|
amount: number;
|
|
withdrawal_method: string;
|
|
destination_details: any;
|
|
}
|
|
): Promise<AxiosResponse> => {
|
|
return axios.post(`${BASE_PATH}/accounts/${accountId}/withdraw`, data);
|
|
},
|
|
|
|
getWithdrawalRequests: (params?: any): Promise<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/withdrawal-requests`, { params });
|
|
},
|
|
|
|
// Portfolio
|
|
getPortfolio: (): Promise<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/portfolio`);
|
|
},
|
|
|
|
getPerformance: (accountId: string, params?: any): Promise<AxiosResponse> => {
|
|
return axios.get(`${BASE_PATH}/accounts/${accountId}/performance`, { params });
|
|
},
|
|
|
|
// Transactions
|
|
getTransactions: (params?: any): Promise<AxiosResponse> => {
|
|
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<Product | null>(null);
|
|
const [showDepositModal, setShowDepositModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchProducts({ status: 'active' });
|
|
}, [fetchProducts]);
|
|
|
|
const handleInvest = (product: Product) => {
|
|
setSelectedProduct(product);
|
|
setShowDepositModal(true);
|
|
};
|
|
|
|
if (loading.products) {
|
|
return <div className="loading">Loading products...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="products-page">
|
|
<header className="page-header">
|
|
<h1>Investment Products</h1>
|
|
<p>Choose from our AI-powered trading agents</p>
|
|
</header>
|
|
|
|
<div className="products-grid">
|
|
{products.map((product) => (
|
|
<ProductCard
|
|
key={product.id}
|
|
product={product}
|
|
onInvest={handleInvest}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{showDepositModal && selectedProduct && (
|
|
<DepositModal
|
|
product={selectedProduct}
|
|
onClose={() => setShowDepositModal(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 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 <div className="loading">Loading portfolio...</div>;
|
|
}
|
|
|
|
if (!portfolioSummary) {
|
|
return (
|
|
<div className="empty-state">
|
|
<h2>No investments yet</h2>
|
|
<p>Start investing in AI-powered trading agents</p>
|
|
<button onClick={() => navigate('/investment/products')}>
|
|
Browse Products
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="portfolio-page">
|
|
<header className="page-header">
|
|
<h1>My Portfolio</h1>
|
|
</header>
|
|
|
|
<PortfolioSummary summary={portfolioSummary.summary} />
|
|
|
|
<section className="accounts-section">
|
|
<h2>My Accounts</h2>
|
|
<div className="accounts-grid">
|
|
{portfolioSummary.accounts.map((account: any) => (
|
|
<AccountCard
|
|
key={account.account_id}
|
|
account={account}
|
|
onClick={() => navigate(`/investment/accounts/${account.account_id}`)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="allocation-section">
|
|
<h2>Risk Allocation</h2>
|
|
<div className="risk-chart">
|
|
{/* Implementar gráfico de dona con allocation_by_risk */}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 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 <div className="loading">Loading account...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="account-detail-page">
|
|
<header className="account-header">
|
|
<div className="account-info">
|
|
<h1>{selectedAccount.product.name}</h1>
|
|
<span className={`status-badge ${selectedAccount.status}`}>
|
|
{selectedAccount.status}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="account-balance">
|
|
<div className="balance-label">Current Balance</div>
|
|
<div className="balance-amount">
|
|
${selectedAccount.current_balance.toLocaleString('en-US', {
|
|
minimumFractionDigits: 2,
|
|
})}
|
|
</div>
|
|
<div className="balance-return">
|
|
{selectedAccount.total_return_percentage >= 0 ? '+' : ''}
|
|
{selectedAccount.total_return_percentage?.toFixed(2)}%
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<nav className="account-tabs">
|
|
<button
|
|
className={activeTab === 'overview' ? 'active' : ''}
|
|
onClick={() => setActiveTab('overview')}
|
|
>
|
|
Overview
|
|
</button>
|
|
<button
|
|
className={activeTab === 'deposit' ? 'active' : ''}
|
|
onClick={() => setActiveTab('deposit')}
|
|
>
|
|
Deposit
|
|
</button>
|
|
<button
|
|
className={activeTab === 'withdraw' ? 'active' : ''}
|
|
onClick={() => setActiveTab('withdraw')}
|
|
>
|
|
Withdraw
|
|
</button>
|
|
</nav>
|
|
|
|
<div className="account-content">
|
|
{activeTab === 'overview' && (
|
|
<>
|
|
<section className="performance-section">
|
|
<h2>Performance</h2>
|
|
<PerformanceChart data={dailyPerformance} />
|
|
</section>
|
|
|
|
<section className="stats-section">
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Invested</div>
|
|
<div className="stat-value">
|
|
${selectedAccount.total_deposited.toLocaleString()}
|
|
</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Profit</div>
|
|
<div className="stat-value">
|
|
${(selectedAccount.current_balance - selectedAccount.total_deposited).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Annualized Return</div>
|
|
<div className="stat-value">
|
|
{selectedAccount.annualized_return_percentage?.toFixed(2)}%
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="transactions-section">
|
|
<h2>Recent Transactions</h2>
|
|
<TransactionList />
|
|
</section>
|
|
</>
|
|
)}
|
|
|
|
{activeTab === 'deposit' && (
|
|
<DepositForm
|
|
accountId={selectedAccount.id}
|
|
productId={selectedAccount.product_id}
|
|
minAmount={50}
|
|
onSuccess={() => {
|
|
fetchAccountById(selectedAccount.id);
|
|
setActiveTab('overview');
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'withdraw' && (
|
|
<WithdrawalForm
|
|
accountId={selectedAccount.id}
|
|
maxAmount={selectedAccount.current_balance}
|
|
onSuccess={() => {
|
|
fetchAccountById(selectedAccount.id);
|
|
setActiveTab('overview');
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 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<ProductCardProps> = ({ product, onInvest }) => {
|
|
const riskColors = {
|
|
low: 'green',
|
|
medium: 'yellow',
|
|
high: 'orange',
|
|
very_high: 'red',
|
|
};
|
|
|
|
return (
|
|
<div className="product-card">
|
|
<div className="product-header">
|
|
<h3>{product.name}</h3>
|
|
<span className={`risk-badge ${riskColors[product.risk_level]}`}>
|
|
{product.risk_level.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="product-description">{product.description}</p>
|
|
|
|
<div className="product-stats">
|
|
<div className="stat">
|
|
<span className="stat-label">Agent Type</span>
|
|
<span className="stat-value">{product.agent_type}</span>
|
|
</div>
|
|
<div className="stat">
|
|
<span className="stat-label">Target Return</span>
|
|
<span className="stat-value">{product.target_annual_return}%</span>
|
|
</div>
|
|
<div className="stat">
|
|
<span className="stat-label">Performance Fee</span>
|
|
<span className="stat-value">{product.performance_fee_percentage}%</span>
|
|
</div>
|
|
<div className="stat">
|
|
<span className="stat-label">Min Investment</span>
|
|
<span className="stat-value">${product.min_investment}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="product-metrics">
|
|
<div className="metric">
|
|
<span>{product.total_investors} investors</span>
|
|
</div>
|
|
<div className="metric">
|
|
<span>${(product.total_aum / 1000).toFixed(0)}K AUM</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
className="invest-button"
|
|
onClick={() => onInvest(product)}
|
|
disabled={!product.is_accepting_new_investors}
|
|
>
|
|
{product.is_accepting_new_investors ? 'Invest Now' : 'Not Accepting'}
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 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<PerformanceChartProps> = ({ 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 (
|
|
<ResponsiveContainer width="100%" height={400}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" />
|
|
<YAxis yAxisId="left" />
|
|
<YAxis yAxisId="right" orientation="right" />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Line
|
|
yAxisId="left"
|
|
type="monotone"
|
|
dataKey="balance"
|
|
stroke="#8884d8"
|
|
name="Balance ($)"
|
|
/>
|
|
<Line
|
|
yAxisId="right"
|
|
type="monotone"
|
|
dataKey="return"
|
|
stroke="#82ca9d"
|
|
name="Return (%)"
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 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(<ProductCard product={mockProduct} onInvest={jest.fn()} />);
|
|
|
|
expect(screen.getByText('Swing Trader Pro')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onInvest when button clicked', () => {
|
|
const onInvest = jest.fn();
|
|
render(<ProductCard product={mockProduct} onInvest={onInvest} />);
|
|
|
|
fireEvent.click(screen.getByText('Invest Now'));
|
|
expect(onInvest).toHaveBeenCalledWith(mockProduct);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Referencias
|
|
|
|
- React 18 Documentation
|
|
- Zustand State Management
|
|
- Recharts for Charts
|
|
- Stripe React Elements
|