From fd54724edefceb7e01a1930e2136d725c0a58931 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 08:47:46 -0600 Subject: [PATCH] [OQI-008] feat: Add portfolio store with WebSocket real-time updates - Create portfolioStore.ts with Zustand for state management - Add portfolioWS instance for real-time portfolio updates - Add usePortfolioUpdates hook for WebSocket subscriptions - Refactor PortfolioDashboard to use store instead of local state - Add WebSocket connection status indicator (Live/Offline) - Add last update timestamp display Co-Authored-By: Claude Opus 4.5 --- .../portfolio/pages/PortfolioDashboard.tsx | 165 ++++---- src/services/websocket.service.ts | 70 ++++ src/stores/portfolioStore.ts | 396 ++++++++++++++++++ 3 files changed, 540 insertions(+), 91 deletions(-) create mode 100644 src/stores/portfolioStore.ts diff --git a/src/modules/portfolio/pages/PortfolioDashboard.tsx b/src/modules/portfolio/pages/PortfolioDashboard.tsx index df89c73..d7ec413 100644 --- a/src/modules/portfolio/pages/PortfolioDashboard.tsx +++ b/src/modules/portfolio/pages/PortfolioDashboard.tsx @@ -3,7 +3,7 @@ * Main dashboard for portfolio management with allocations, rebalancing, and goals */ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { PlusIcon, @@ -13,18 +13,10 @@ import { CurrencyDollarIcon, ArrowTrendingUpIcon, ArrowTrendingDownIcon, + SignalIcon, + SignalSlashIcon, } from '@heroicons/react/24/solid'; -import { - getUserPortfolios, - getPortfolioStats, - getRebalanceRecommendations, - executeRebalance, - getUserGoals, - type Portfolio, - type PortfolioStats, - type RebalanceRecommendation, - type PortfolioGoal, -} from '../../../services/portfolio.service'; +import { usePortfolioStore } from '../../../stores/portfolioStore'; import { AllocationChart } from '../components/AllocationChart'; import { AllocationTable } from '../components/AllocationTable'; import { RebalanceCard } from '../components/RebalanceCard'; @@ -74,90 +66,52 @@ const StatCard: React.FC = ({ label, value, change, icon, color } // ============================================================================ export default function PortfolioDashboard() { - const [portfolios, setPortfolios] = useState([]); - const [selectedPortfolio, setSelectedPortfolio] = useState(null); - const [stats, setStats] = useState(null); - const [recommendations, setRecommendations] = useState([]); - const [goals, setGoals] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [isRebalancing, setIsRebalancing] = useState(false); const [activeTab, setActiveTab] = useState<'overview' | 'goals'>('overview'); - // Fetch all data - const fetchData = useCallback(async () => { - try { - setLoading(true); - setError(null); - - const [portfolioData, goalsData] = await Promise.all([ - getUserPortfolios(), - getUserGoals(), - ]); - - setPortfolios(portfolioData); - setGoals(goalsData); - - if (portfolioData.length > 0) { - const primary = portfolioData[0]; - setSelectedPortfolio(primary); - - const [statsData, rebalanceData] = await Promise.all([ - getPortfolioStats(primary.id), - getRebalanceRecommendations(primary.id), - ]); - - setStats(statsData); - setRecommendations(rebalanceData); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Error loading data'); - } finally { - setLoading(false); - } - }, []); + // Store state + const { + portfolios, + selectedPortfolio, + stats, + recommendations, + goals, + loading, + isRebalancing, + error, + wsConnected, + lastUpdate, + fetchPortfolios, + selectPortfolio, + executeRebalance, + deleteGoal, + connectWebSocket, + disconnectWebSocket, + clearError, + } = usePortfolioStore(); + // Initial data fetch and WebSocket connection useEffect(() => { - fetchData(); - }, [fetchData]); + fetchPortfolios(); + connectWebSocket(); - // Handle portfolio selection - const handlePortfolioSelect = async (portfolio: Portfolio) => { - setSelectedPortfolio(portfolio); - try { - const [statsData, rebalanceData] = await Promise.all([ - getPortfolioStats(portfolio.id), - getRebalanceRecommendations(portfolio.id), - ]); - setStats(statsData); - setRecommendations(rebalanceData); - } catch (err) { - console.error('Error loading portfolio data:', err); - } - }; + return () => { + disconnectWebSocket(); + }; + }, [fetchPortfolios, connectWebSocket, disconnectWebSocket]); // Handle rebalance execution const handleRebalance = async () => { - if (!selectedPortfolio) return; - - try { - setIsRebalancing(true); - await executeRebalance(selectedPortfolio.id); - await fetchData(); - } catch (err) { - console.error('Error executing rebalance:', err); - } finally { - setIsRebalancing(false); - } + await executeRebalance(); }; // Handle goal deletion const handleDeleteGoal = async (goalId: string) => { - // TODO: Implement delete confirmation modal - console.log('Delete goal:', goalId); + if (confirm('¿Estás seguro de eliminar esta meta?')) { + await deleteGoal(goalId); + } }; - if (loading) { + if (loading && portfolios.length === 0) { return (
@@ -165,12 +119,15 @@ export default function PortfolioDashboard() { ); } - if (error) { + if (error && portfolios.length === 0) { return (

{error}

+ {/* Last update indicator */} + {lastUpdate && ( +
+ Última actualización: {new Date(lastUpdate).toLocaleTimeString()} +
+ )} + {/* Portfolio Selector (if multiple) */} {portfolios.length > 1 && (
{portfolios.map((p) => (