import React, { useState, useMemo, useEffect } from 'react'; import { fetchRealData } from '../data/historicalData'; import { mockPortfolios } from '../data/mockData'; import { tickerMetrics } from '../data/tickerMetrics'; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; import Indicators from '../components/Indicators'; import FeeTransparencyModule from '../components/FeeTransparencyModule'; import RebalancingEngine from '../components/RebalancingEngine'; import TransparencyModal from '../components/TransparencyModal'; import MacroTracker from '../components/MacroTracker'; import StockPopup from '../components/StockPopup'; import PortfolioHeatmap from '../components/PortfolioHeatmap'; import FinancialCalculators from '../components/FinancialCalculators'; import { LayoutGrid, Plus, PieChart as PieIcon, BarChart3, X, ChevronRight } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import StockSearch from '../components/StockSearch'; import HoldingsList from '../components/HoldingsList'; export default function Dashboard({ riskProfile }) { // Initialize with a cached portfolio if available, otherwise an empty one const [currentPortfolio, setCurrentPortfolio] = useState(() => { const saved = localStorage.getItem('gs_portfolio'); if (saved) { try { return JSON.parse(saved); } catch (e) { console.error("Error loading saved portfolio:", e); } } return mockPortfolios[riskProfile] || { allocation: [] }; }); // Save to cache whenever portfolio changes useEffect(() => { localStorage.setItem('gs_portfolio', JSON.stringify(currentPortfolio)); }, [currentPortfolio]); const [modalData, setModalData] = useState(null); // State for popups const [activePopupAsset, setActivePopupAsset] = useState(null); const [showHeatmap, setShowHeatmap] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false); const [isHoldingsOpen, setIsHoldingsOpen] = useState(false); const [totals, setTotals] = useState({ value: 0, change: 0, percent: 0, rawValue: 0, loading: true }); const [prices, setPrices] = useState({}); // Dynamic Calculation of Health, Risk, and Weights based on real metrics const displayPortfolio = useMemo(() => { let totalBeta = 0; let totalExpense = 0; const numAssets = currentPortfolio.allocation.length; // Calculate current market total let marketTotal = 0; currentPortfolio.allocation.forEach(asset => { const priceData = prices[asset.ticker] || { price: 0 }; marketTotal += asset.shares * priceData.price; }); const updatedAllocation = currentPortfolio.allocation.map(asset => { const priceData = prices[asset.ticker] || { price: 0, percent: 0 }; const marketVal = asset.shares * priceData.price; const weight = marketTotal > 0 ? (marketVal / marketTotal) * 100 : 0; const totalCost = asset.shares * (asset.buyPrice || priceData.price); const gainLoss = marketVal - totalCost; const returnPct = (totalCost > 0 && !isNaN(gainLoss) && isFinite(gainLoss)) ? (gainLoss / totalCost) * 100 : 0; const metrics = tickerMetrics[asset.ticker] || { beta: 1, expenseRatio: 0.1 }; totalBeta += (metrics.beta || 1) * (weight / 100); totalExpense += (metrics.expenseRatio || 0.1) * (weight / 100); return { ...asset, value: Number(weight.toFixed(2)), dayChange: priceData.percent, dollarChange: priceData.change, returnPct }; }); if (numAssets === 0) { return { ...currentPortfolio, healthScore: 0, riskLevel: 'None', avgBeta: '0.00', stockSplit: 0, mfSplit: 0 }; } // Determine Risk Level let riskLevel = 'Medium'; if (totalBeta < 0.6) riskLevel = 'Low'; else if (totalBeta > 1.2) riskLevel = 'High'; // Determine Health Score (Base 100) let healthScore = 100; healthScore -= Math.min(totalExpense * 100 * 2, 20); if (numAssets >= 5) healthScore += 5; const profileMap = { 'Cautious': 'Low', 'Balanced': 'Medium', 'Bold': 'High' }; if (riskLevel !== profileMap[riskProfile]) healthScore -= 15; healthScore = Math.min(Math.max(Math.round(healthScore), 0), 100); // Calculate Asset Type Split let stockWeight = 0; let mfWeight = 0; updatedAllocation.forEach(asset => { if (asset.type === 'mf') mfWeight += asset.value; else stockWeight += asset.value; }); return { ...currentPortfolio, allocation: updatedAllocation, healthScore, riskLevel, avgBeta: totalBeta.toFixed(2), stockSplit: stockWeight, mfSplit: mfWeight }; }, [currentPortfolio, riskProfile, prices]); const handleRebalance = (scenario) => { setModalData(scenario); }; const closeModal = () => setModalData(null); const closeStockPopup = () => setActivePopupAsset(null); const handleAddStock = (newAsset) => { setCurrentPortfolio(prev => { // If portfolio is empty, the first stock gets 100% allocation if (prev.allocation.length === 0) { return { ...prev, allocation: [{ ...newAsset, value: 100 }] }; } const scale = (100 - newAsset.value) / 100; const updatedExisting = prev.allocation.map(a => ({ ...a, value: Number((a.value * scale).toFixed(2)) })); // Ensure sum is exactly 100 const currentSum = updatedExisting.reduce((acc, a) => acc + a.value, 0) + newAsset.value; if (currentSum !== 100 && updatedExisting.length > 0) { updatedExisting[0].value += Number((100 - currentSum).toFixed(2)); } return { ...prev, allocation: [...updatedExisting, newAsset] }; }); }; const handleRemoveAsset = (ticker) => { setCurrentPortfolio(prev => { const updatedAllocation = prev.allocation.filter(a => a.ticker !== ticker); // Redistribute value to maintain 100% if (updatedAllocation.length > 0) { const currentSum = updatedAllocation.reduce((acc, a) => acc + a.value, 0); const scale = 100 / currentSum; updatedAllocation.forEach(a => { a.value = Number((a.value * scale).toFixed(2)); }); // Final adjust for rounding const finalSum = updatedAllocation.reduce((acc, a) => acc + a.value, 0); if (finalSum !== 100) { updatedAllocation[0].value += Number((100 - finalSum).toFixed(2)); } } return { ...prev, allocation: updatedAllocation }; }); }; useEffect(() => { async function calculateTotals() { if (currentPortfolio.allocation.length === 0) { setTotals({ value: '$0.00', change: '$0.00', percent: '0.00', isPositive: true, loading: false }); return; } setTotals(prev => ({ ...prev, loading: true })); const pricePromises = currentPortfolio.allocation.map(asset => fetchRealData(asset.ticker)); const results = await Promise.all(pricePromises); let newTotalValue = 0; let newTotalChange = 0; const priceMap = {}; results.forEach((data, index) => { const ticker = currentPortfolio.allocation[index].ticker; priceMap[ticker] = { price: data.currentPrice, change: data.change, percent: (data.change / (data.currentPrice - data.change)) * 100, history: data.history }; newTotalValue += currentPortfolio.allocation[index].shares * data.currentPrice; newTotalChange += (currentPortfolio.allocation[index].shares * data.change); }); setPrices(priceMap); const percent = newTotalValue > 0 ? (newTotalChange / (newTotalValue - newTotalChange)) * 100 : 0; // Update totals setTotals({ value: newTotalValue.toLocaleString('en-US', { style: 'currency', currency: 'USD' }), change: newTotalChange.toLocaleString('en-US', { style: 'currency', currency: 'USD' }), percent: percent.toFixed(2), rawValue: newTotalValue, isPositive: newTotalChange >= 0, loading: false }); } calculateTotals(); }, [JSON.stringify(currentPortfolio.allocation.map(a => `${a.ticker}-${a.shares}`))]); return (
Built for your goals. Transparently managed.
Total Value
{totals.loading ? ( ) : ({totals.value}
)}Day Change
{totals.loading ? ( ) : ({totals.isPositive ? '+' : ''}{totals.change} ({totals.isPositive ? '+' : ''}{totals.percent}%)
)}Portfolio optimized for a {riskProfile} objective.