/** * Investment Dashboard Page * Main entry point for investment module showing account summary and performance * Epic: OQI-004 Cuentas de Inversion */ import React, { useEffect, useState, useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { TrendingUp, Shield, Zap, Wallet, ArrowUpRight, ArrowDownRight, Plus, ArrowRight, RefreshCw, } from 'lucide-react'; import { useInvestmentStore } from '../../../stores/investmentStore'; // ============================================================================ // Types // ============================================================================ type TimeframeKey = '1W' | '1M' | '3M' | '6M' | '1Y' | 'ALL'; interface PerformancePoint { date: string; value: number; } // ============================================================================ // Performance Chart Component // ============================================================================ interface PortfolioPerformanceChartProps { data: PerformancePoint[]; height?: number; } const PortfolioPerformanceChart: React.FC = ({ data, height = 200 }) => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas || data.length < 2) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); const width = rect.width; const chartHeight = rect.height; const padding = { top: 20, right: 20, bottom: 30, left: 60 }; const chartWidth = width - padding.left - padding.right; const innerHeight = chartHeight - padding.top - padding.bottom; ctx.clearRect(0, 0, width, chartHeight); const values = data.map((d) => d.value); const minVal = Math.min(...values); const maxVal = Math.max(...values); const range = maxVal - minVal || 1; const paddedMin = minVal - range * 0.1; const paddedMax = maxVal + range * 0.1; const paddedRange = paddedMax - paddedMin; // Draw grid lines ctx.strokeStyle = '#374151'; ctx.lineWidth = 0.5; for (let i = 0; i <= 4; i++) { const y = padding.top + (innerHeight / 4) * i; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(width - padding.right, y); ctx.stroke(); const val = paddedMax - (paddedRange / 4) * i; ctx.fillStyle = '#9CA3AF'; ctx.font = '11px system-ui'; ctx.textAlign = 'right'; ctx.fillText(`$${val.toLocaleString(undefined, { maximumFractionDigits: 0 })}`, padding.left - 8, y + 4); } // Map points const points = data.map((d, i) => ({ x: padding.left + (i / (data.length - 1)) * chartWidth, y: padding.top + ((paddedMax - d.value) / paddedRange) * innerHeight, })); // Determine color based on performance const isPositive = data[data.length - 1].value >= data[0].value; const lineColor = isPositive ? '#10B981' : '#EF4444'; const fillColor = isPositive ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)'; // Draw fill ctx.beginPath(); ctx.moveTo(points[0].x, chartHeight - padding.bottom); points.forEach((p) => ctx.lineTo(p.x, p.y)); ctx.lineTo(points[points.length - 1].x, chartHeight - padding.bottom); ctx.closePath(); ctx.fillStyle = fillColor; ctx.fill(); // Draw line ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; const midX = (prev.x + curr.x) / 2; ctx.quadraticCurveTo(prev.x, prev.y, midX, (prev.y + curr.y) / 2); } ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y); ctx.strokeStyle = lineColor; ctx.lineWidth = 2; ctx.stroke(); // Draw end dot const lastPt = points[points.length - 1]; ctx.beginPath(); ctx.arc(lastPt.x, lastPt.y, 4, 0, Math.PI * 2); ctx.fillStyle = lineColor; ctx.fill(); // X-axis labels const labelIndices = [0, Math.floor(data.length / 2), data.length - 1]; ctx.fillStyle = '#9CA3AF'; ctx.font = '10px system-ui'; ctx.textAlign = 'center'; labelIndices.forEach((i) => { if (data[i]) { const x = padding.left + (i / (data.length - 1)) * chartWidth; const date = new Date(data[i].date); ctx.fillText(date.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' }), x, chartHeight - 10); } }); }, [data]); if (data.length < 2) { return (
No hay datos de rendimiento disponibles
); } return ; }; // ============================================================================ // Quick Action Button Component // ============================================================================ interface QuickActionProps { icon: React.ReactNode; label: string; description: string; to: string; color: string; } const QuickActionButton: React.FC = ({ icon, label, description, to, color }) => (
{icon}
{label}
{description}
); // ============================================================================ // Account Card Component // ============================================================================ interface AccountCardProps { account: { id: string; product: { code: string; name: string; riskProfile: string }; balance: number; totalDeposited: number; totalEarnings: number; unrealizedPnlPercent: number; status: string; }; } const AccountCard: React.FC = ({ account }) => { const icons: Record = { atlas: , orion: , nova: , }; const isPositive = account.totalEarnings >= 0; return (
{icons[account.product.code] || }
{account.product.name}
{account.product.riskProfile}
{isPositive ? : } {account.unrealizedPnlPercent.toFixed(2)}%
Balance
${account.balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Ganancias
{isPositive ? '+' : ''}${Math.abs(account.totalEarnings).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
); }; // ============================================================================ // Main Component // ============================================================================ export default function Investment() { const navigate = useNavigate(); const { accounts, accountSummary, loadingAccounts, loadingSummary, fetchAccounts, fetchAccountSummary, } = useInvestmentStore(); const [timeframe, setTimeframe] = useState('1M'); const [performanceData, setPerformanceData] = useState([]); const [loadingPerformance, setLoadingPerformance] = useState(false); const timeframes: { key: TimeframeKey; label: string }[] = [ { key: '1W', label: '1S' }, { key: '1M', label: '1M' }, { key: '3M', label: '3M' }, { key: '6M', label: '6M' }, { key: '1Y', label: '1A' }, { key: 'ALL', label: 'Todo' }, ]; useEffect(() => { fetchAccounts(); fetchAccountSummary(); }, [fetchAccounts, fetchAccountSummary]); useEffect(() => { generateMockPerformanceData(); }, [timeframe, accountSummary]); const generateMockPerformanceData = () => { if (!accountSummary || accountSummary.totalBalance === 0) { setPerformanceData([]); return; } setLoadingPerformance(true); const daysMap: Record = { '1W': 7, '1M': 30, '3M': 90, '6M': 180, '1Y': 365, ALL: 730, }; const days = daysMap[timeframe]; const data: PerformancePoint[] = []; const baseValue = accountSummary.totalDeposited; const currentValue = accountSummary.totalBalance; const growthRate = (currentValue - baseValue) / baseValue; for (let i = days; i >= 0; i--) { const date = new Date(); date.setDate(date.getDate() - i); const progress = (days - i) / days; const randomVariation = (Math.random() - 0.5) * 0.02; const value = baseValue * (1 + growthRate * progress + randomVariation); data.push({ date: date.toISOString(), value: Math.max(0, value), }); } data[data.length - 1].value = currentValue; setPerformanceData(data); setLoadingPerformance(false); }; const handleRefresh = () => { fetchAccounts(); fetchAccountSummary(); }; const activeAccounts = accounts.filter((a) => a.status === 'active'); const hasAccounts = accounts.length > 0; const isLoading = loadingAccounts || loadingSummary; const totalReturn = accountSummary ? accountSummary.overallReturnPercent : 0; const isPositiveReturn = totalReturn >= 0; return (
{/* Header */}

Dashboard de Inversion

Gestiona tus cuentas de inversion con agentes IA

Nueva Inversion
{/* Account Summary Cards */}
Valor Total
${(accountSummary?.totalBalance || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })}
Total Invertido
${(accountSummary?.totalDeposited || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })}
Rendimiento Total
{isPositiveReturn ? '+' : ''}{totalReturn.toFixed(2)}%
{isPositiveReturn ? '+' : ''}${Math.abs(accountSummary?.overallReturn || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })}
Cuentas Activas
{activeAccounts.length}
de {accounts.length} total
{/* Performance Chart */}

Rendimiento del Portfolio

{timeframes.map((tf) => ( ))}
{loadingPerformance ? (
) : ( )}
{/* My Accounts Section */}

Mis Cuentas

{hasAccounts && ( Ver todas )}
{isLoading ? (
) : hasAccounts ? (
{activeAccounts.slice(0, 3).map((account) => ( ))}
) : (

No tienes cuentas de inversion activas

Comienza a invertir con nuestros agentes de IA

Abrir Nueva Cuenta
)}
{/* Quick Actions */}

Acciones Rapidas

} label="Depositar" description="Agregar fondos a tus cuentas" to="/investment/portfolio" color="bg-blue-500/20" /> } label="Retirar" description="Solicitar retiro de fondos" to="/investment/withdrawals" color="bg-purple-500/20" /> } label="Ver Productos" description="Explorar opciones de inversion" to="/investment/products" color="bg-emerald-500/20" />
{/* Risk Warning */}

Aviso de Riesgo: El trading e inversion conlleva riesgos significativos. Los rendimientos objetivo no estan garantizados. Puede perder parte o la totalidad de su inversion. Solo invierta capital que pueda permitirse perder.

); }