diff --git a/src/modules/investment/components/AccountSettingsPanel.tsx b/src/modules/investment/components/AccountSettingsPanel.tsx new file mode 100644 index 0000000..d42fa90 --- /dev/null +++ b/src/modules/investment/components/AccountSettingsPanel.tsx @@ -0,0 +1,524 @@ +/** + * AccountSettingsPanel Component + * Account-level configuration for investment accounts + * OQI-004: Cuentas de Inversion + */ + +import React, { useState } from 'react'; +import { + Settings, + Bell, + RefreshCw, + Calendar, + Shield, + DollarSign, + AlertTriangle, + CheckCircle, + Info, + ChevronRight, + Save, + X, + TrendingUp, +} from 'lucide-react'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface AccountSettings { + distributionFrequency: 'weekly' | 'biweekly' | 'monthly' | 'quarterly'; + autoReinvest: boolean; + reinvestPercentage: number; + notifications: { + distributionAlert: boolean; + performanceAlert: boolean; + riskAlert: boolean; + newsAlert: boolean; + }; + riskAlerts: { + enabled: boolean; + drawdownThreshold: number; + dailyLossThreshold: number; + }; + withdrawalSettings: { + preferredMethod: 'bank' | 'crypto' | 'wallet'; + autoWithdraw: boolean; + autoWithdrawThreshold: number; + }; +} + +export interface AccountForSettings { + id: string; + accountNumber: string; + productName: string; + currentBalance: number; +} + +interface AccountSettingsPanelProps { + account: AccountForSettings; + settings: AccountSettings; + onSave?: (settings: AccountSettings) => void; + onCancel?: () => void; + isLoading?: boolean; + compact?: boolean; +} + +// ============================================================================ +// Sub-Components +// ============================================================================ + +interface ToggleSwitchProps { + enabled: boolean; + onChange: (value: boolean) => void; + disabled?: boolean; +} + +const ToggleSwitch: React.FC = ({ enabled, onChange, disabled = false }) => ( + +); + +interface SettingRowProps { + icon: React.ReactNode; + label: string; + description?: string; + children: React.ReactNode; +} + +const SettingRow: React.FC = ({ icon, label, description, children }) => ( +
+
+
{icon}
+
+
{label}
+ {description &&
{description}
} +
+
+
{children}
+
+); + +// ============================================================================ +// Component +// ============================================================================ + +export const AccountSettingsPanel: React.FC = ({ + account, + settings: initialSettings, + onSave, + onCancel, + isLoading = false, + compact = false, +}) => { + const [settings, setSettings] = useState(initialSettings); + const [activeSection, setActiveSection] = useState('distribution'); + const [hasChanges, setHasChanges] = useState(false); + + const updateSettings = ( + key: K, + value: AccountSettings[K] + ) => { + setSettings((prev) => ({ ...prev, [key]: value })); + setHasChanges(true); + }; + + const updateNestedSettings = ( + key: K, + nestedKey: keyof AccountSettings[K], + value: AccountSettings[K][keyof AccountSettings[K]] + ) => { + setSettings((prev) => ({ + ...prev, + [key]: { ...prev[key], [nestedKey]: value }, + })); + setHasChanges(true); + }; + + const handleSave = () => { + onSave?.(settings); + setHasChanges(false); + }; + + const sections = [ + { id: 'distribution', label: 'Distribution', icon: Calendar }, + { id: 'reinvest', label: 'Auto-Reinvest', icon: RefreshCw }, + { id: 'notifications', label: 'Notifications', icon: Bell }, + { id: 'risk', label: 'Risk Alerts', icon: Shield }, + { id: 'withdrawal', label: 'Withdrawal', icon: DollarSign }, + ]; + + if (compact) { + return ( +
+
+
+ +
+
+

Account Settings

+

{account.accountNumber}

+
+
+ +
+ {sections.map((section) => { + const Icon = section.icon; + return ( + + ); + })} +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

Account Settings

+

+ {account.productName} • {account.accountNumber} +

+
+
+ {hasChanges && ( + + Unsaved Changes + + )} +
+
+ + {/* Navigation Tabs */} +
+ {sections.map((section) => { + const Icon = section.icon; + const isActive = activeSection === section.id; + + return ( + + ); + })} +
+ + {/* Content */} +
+ {/* Distribution Settings */} + {activeSection === 'distribution' && ( +
+ } + label="Distribution Frequency" + description="How often you receive profit distributions" + > + + + +
+
+ +

+ Distributions are processed on the last business day of each period. + Changes take effect from the next distribution cycle. +

+
+
+
+ )} + + {/* Auto-Reinvest Settings */} + {activeSection === 'reinvest' && ( +
+ } + label="Auto-Reinvest Profits" + description="Automatically reinvest your distributions" + > + updateSettings('autoReinvest', value)} + /> + + + {settings.autoReinvest && ( + } + label="Reinvest Percentage" + description="Percentage of profits to reinvest" + > +
+ updateSettings('reinvestPercentage', parseInt(e.target.value))} + className="w-24" + /> + + {settings.reinvestPercentage}% + +
+
+ )} +
+ )} + + {/* Notifications Settings */} + {activeSection === 'notifications' && ( +
+ } + label="Distribution Alerts" + description="Get notified when distributions are processed" + > + updateNestedSettings('notifications', 'distributionAlert', value)} + /> + + + } + label="Performance Alerts" + description="Weekly performance summary notifications" + > + updateNestedSettings('notifications', 'performanceAlert', value)} + /> + + + } + label="Risk Alerts" + description="Get notified about significant drawdowns" + > + updateNestedSettings('notifications', 'riskAlert', value)} + /> + + + } + label="News & Updates" + description="Product updates and market news" + > + updateNestedSettings('notifications', 'newsAlert', value)} + /> + +
+ )} + + {/* Risk Alerts Settings */} + {activeSection === 'risk' && ( +
+ } + label="Enable Risk Alerts" + description="Receive alerts when thresholds are breached" + > + updateNestedSettings('riskAlerts', 'enabled', value)} + /> + + + {settings.riskAlerts.enabled && ( + <> + } + label="Drawdown Threshold" + description="Alert when drawdown exceeds this percentage" + > +
+ + updateNestedSettings('riskAlerts', 'drawdownThreshold', parseInt(e.target.value)) + } + className="w-20 px-3 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-right" + /> + % +
+
+ + } + label="Daily Loss Threshold" + description="Alert when daily loss exceeds this percentage" + > +
+ + updateNestedSettings('riskAlerts', 'dailyLossThreshold', parseInt(e.target.value)) + } + className="w-20 px-3 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-right" + /> + % +
+
+ + )} +
+ )} + + {/* Withdrawal Settings */} + {activeSection === 'withdrawal' && ( +
+ } + label="Preferred Withdrawal Method" + description="Default method for withdrawals" + > + + + + } + label="Auto-Withdraw" + description="Automatically withdraw when balance exceeds threshold" + > + updateNestedSettings('withdrawalSettings', 'autoWithdraw', value)} + /> + + + {settings.withdrawalSettings.autoWithdraw && ( + } + label="Auto-Withdraw Threshold" + description="Withdraw when balance exceeds this amount" + > +
+ $ + + updateNestedSettings('withdrawalSettings', 'autoWithdrawThreshold', parseInt(e.target.value)) + } + className="w-28 px-3 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-right" + /> +
+
+ )} +
+ )} +
+ + {/* Footer Actions */} +
+
+ {onCancel && ( + + )} + +
+
+
+ ); +}; + +export default AccountSettingsPanel; diff --git a/src/modules/investment/components/AccountSummaryCard.tsx b/src/modules/investment/components/AccountSummaryCard.tsx new file mode 100644 index 0000000..f7db40a --- /dev/null +++ b/src/modules/investment/components/AccountSummaryCard.tsx @@ -0,0 +1,285 @@ +/** + * AccountSummaryCard Component + * Reusable investment account summary card with key metrics + * OQI-004: Cuentas de Inversion + */ + +import React from 'react'; +import { + Wallet, + TrendingUp, + TrendingDown, + Percent, + Calendar, + ArrowUpRight, + ArrowDownRight, + MoreVertical, + Eye, + Settings, + Clock, +} from 'lucide-react'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface InvestmentAccount { + id: string; + accountNumber: string; + productName: string; + productType: 'atlas' | 'orion' | 'nova' | 'custom'; + currentBalance: number; + initialDeposit: number; + totalGains: number; + totalGainsPercent: number; + monthlyReturn: number; + monthlyReturnPercent: number; + status: 'active' | 'paused' | 'pending' | 'closed'; + riskLevel: 'low' | 'medium' | 'high'; + createdAt: string; + lastDistributionDate?: string; + nextDistributionDate?: string; +} + +interface AccountSummaryCardProps { + account: InvestmentAccount; + onViewDetails?: (accountId: string) => void; + onManageSettings?: (accountId: string) => void; + compact?: boolean; + showActions?: boolean; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const getProductColor = (productType: InvestmentAccount['productType']) => { + switch (productType) { + case 'atlas': + return 'from-blue-500 to-blue-600'; + case 'orion': + return 'from-purple-500 to-purple-600'; + case 'nova': + return 'from-amber-500 to-amber-600'; + default: + return 'from-slate-500 to-slate-600'; + } +}; + +const getStatusBadge = (status: InvestmentAccount['status']) => { + switch (status) { + case 'active': + return { label: 'Active', className: 'bg-emerald-500/20 text-emerald-400' }; + case 'paused': + return { label: 'Paused', className: 'bg-yellow-500/20 text-yellow-400' }; + case 'pending': + return { label: 'Pending', className: 'bg-blue-500/20 text-blue-400' }; + case 'closed': + return { label: 'Closed', className: 'bg-slate-500/20 text-slate-400' }; + default: + return { label: status, className: 'bg-slate-500/20 text-slate-400' }; + } +}; + +const getRiskColor = (riskLevel: InvestmentAccount['riskLevel']) => { + switch (riskLevel) { + case 'low': + return 'text-emerald-400'; + case 'medium': + return 'text-yellow-400'; + case 'high': + return 'text-red-400'; + default: + return 'text-slate-400'; + } +}; + +const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); +}; + +const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +}; + +// ============================================================================ +// Component +// ============================================================================ + +export const AccountSummaryCard: React.FC = ({ + account, + onViewDetails, + onManageSettings, + compact = false, + showActions = true, +}) => { + const [showMenu, setShowMenu] = React.useState(false); + const status = getStatusBadge(account.status); + const isPositive = account.totalGains >= 0; + + if (compact) { + return ( +
onViewDetails?.(account.id)} + className="p-4 bg-slate-800/50 rounded-xl border border-slate-700 hover:border-slate-600 cursor-pointer transition-all" + > +
+
+
+ +
+
+
{account.productName}
+
{account.accountNumber}
+
+
+ + {status.label} + +
+
+
+
{formatCurrency(account.currentBalance)}
+
+ {isPositive ? : } + {isPositive ? '+' : ''}{formatCurrency(account.totalGains)} ({account.totalGainsPercent.toFixed(2)}%) +
+
+
+
+ ); + } + + return ( +
+ {/* Header with gradient */} +
+ +
+ {/* Top Row */} +
+
+
+ +
+
+

{account.productName}

+
+ {account.accountNumber} + + {status.label} + +
+
+
+ + {showActions && ( +
+ + {showMenu && ( +
+ + +
+ )} +
+ )} +
+ + {/* Balance */} +
+
Current Balance
+
{formatCurrency(account.currentBalance)}
+
+ + {/* Stats Grid */} +
+ {/* Total Gains */} +
+
+ {isPositive ? ( + + ) : ( + + )} + Total Gains +
+
+ {isPositive ? '+' : ''}{formatCurrency(account.totalGains)} +
+
+ {isPositive ? '+' : ''}{account.totalGainsPercent.toFixed(2)}% +
+
+ + {/* Monthly Return */} +
+
+ + Monthly Return +
+
= 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {account.monthlyReturn >= 0 ? '+' : ''}{formatCurrency(account.monthlyReturn)} +
+
= 0 ? 'text-emerald-400/70' : 'text-red-400/70'}`}> + {account.monthlyReturnPercent >= 0 ? '+' : ''}{account.monthlyReturnPercent.toFixed(2)}% +
+
+
+ + {/* Footer Info */} +
+
+ + Opened {formatDate(account.createdAt)} +
+
+ {account.riskLevel} Risk +
+
+ + {/* Next Distribution */} + {account.nextDistributionDate && ( +
+
+ + Next distribution: {formatDate(account.nextDistributionDate)} +
+
+ )} +
+
+ ); +}; + +export default AccountSummaryCard; diff --git a/src/modules/investment/components/PerformanceWidgetChart.tsx b/src/modules/investment/components/PerformanceWidgetChart.tsx new file mode 100644 index 0000000..ecb45d5 --- /dev/null +++ b/src/modules/investment/components/PerformanceWidgetChart.tsx @@ -0,0 +1,238 @@ +/** + * PerformanceWidgetChart Component + * Compact sparkline-style performance chart for embedding in cards + * OQI-004: Cuentas de Inversion + */ + +import React, { useRef, useEffect, useMemo } from 'react'; +import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface PerformanceDataPoint { + date: string; + value: number; + balance?: number; +} + +interface PerformanceWidgetChartProps { + data: PerformanceDataPoint[]; + period?: 'week' | 'month' | 'quarter' | 'year' | 'all'; + height?: number; + showTrend?: boolean; + showValue?: boolean; + lineColor?: string; + fillColor?: string; + compact?: boolean; + onClick?: () => void; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const calculateTrend = (data: PerformanceDataPoint[]) => { + if (data.length < 2) return { direction: 'neutral' as const, change: 0, changePercent: 0 }; + + const first = data[0].value; + const last = data[data.length - 1].value; + const change = last - first; + const changePercent = first !== 0 ? (change / Math.abs(first)) * 100 : 0; + + return { + direction: change > 0 ? 'up' as const : change < 0 ? 'down' as const : 'neutral' as const, + change, + changePercent, + }; +}; + +const formatValue = (value: number) => { + if (Math.abs(value) >= 1000000) { + return `$${(value / 1000000).toFixed(1)}M`; + } + if (Math.abs(value) >= 1000) { + return `$${(value / 1000).toFixed(1)}K`; + } + return `$${value.toFixed(0)}`; +}; + +// ============================================================================ +// Component +// ============================================================================ + +export const PerformanceWidgetChart: React.FC = ({ + data, + period = 'month', + height = 60, + showTrend = true, + showValue = true, + lineColor, + fillColor, + compact = false, + onClick, +}) => { + const canvasRef = useRef(null); + + const trend = useMemo(() => calculateTrend(data), [data]); + + const defaultLineColor = trend.direction === 'up' ? '#10b981' : trend.direction === 'down' ? '#ef4444' : '#6b7280'; + const defaultFillColor = trend.direction === 'up' ? 'rgba(16, 185, 129, 0.1)' : trend.direction === 'down' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(107, 114, 128, 0.1)'; + + const actualLineColor = lineColor || defaultLineColor; + const actualFillColor = fillColor || defaultFillColor; + + 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 = 2; + + // Calculate min/max with some padding + const values = data.map((d) => d.value); + const minValue = Math.min(...values); + const maxValue = Math.max(...values); + const valueRange = maxValue - minValue || 1; + const paddedMin = minValue - valueRange * 0.1; + const paddedMax = maxValue + valueRange * 0.1; + const paddedRange = paddedMax - paddedMin; + + // Map data points to canvas coordinates + const points = data.map((d, i) => ({ + x: padding + (i / (data.length - 1)) * (width - 2 * padding), + y: chartHeight - padding - ((d.value - paddedMin) / paddedRange) * (chartHeight - 2 * padding), + })); + + // Clear canvas + ctx.clearRect(0, 0, width, chartHeight); + + // Draw fill area + ctx.beginPath(); + ctx.moveTo(points[0].x, chartHeight); + points.forEach((p) => ctx.lineTo(p.x, p.y)); + ctx.lineTo(points[points.length - 1].x, chartHeight); + ctx.closePath(); + ctx.fillStyle = actualFillColor; + ctx.fill(); + + // Draw line + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + + // Smooth curve using quadratic bezier + 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 = actualLineColor; + ctx.lineWidth = 2; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.stroke(); + + // Draw end point dot + const lastPoint = points[points.length - 1]; + ctx.beginPath(); + ctx.arc(lastPoint.x, lastPoint.y, 3, 0, Math.PI * 2); + ctx.fillStyle = actualLineColor; + ctx.fill(); + ctx.strokeStyle = '#1e293b'; + ctx.lineWidth = 1.5; + ctx.stroke(); + }, [data, actualLineColor, actualFillColor]); + + const TrendIcon = trend.direction === 'up' ? TrendingUp : trend.direction === 'down' ? TrendingDown : Minus; + + if (compact) { + return ( +
+ + {showTrend && ( + + {trend.changePercent > 0 ? '+' : ''}{trend.changePercent.toFixed(1)}% + + )} +
+ ); + } + + return ( +
+ {/* Header */} + {(showValue || showTrend) && ( +
+ {showValue && data.length > 0 && ( +
+ {formatValue(data[data.length - 1].value)} +
+ )} + {showTrend && ( +
+ + + {trend.changePercent > 0 ? '+' : ''}{trend.changePercent.toFixed(1)}% + +
+ )} +
+ )} + + {/* Chart */} + + + {/* Period Label */} +
+ {period === 'all' ? 'All Time' : `Last ${period}`} +
+
+ ); +}; + +export default PerformanceWidgetChart; diff --git a/src/modules/investment/components/ProductComparisonTable.tsx b/src/modules/investment/components/ProductComparisonTable.tsx new file mode 100644 index 0000000..633f66a --- /dev/null +++ b/src/modules/investment/components/ProductComparisonTable.tsx @@ -0,0 +1,396 @@ +/** + * ProductComparisonTable Component + * Side-by-side comparison of investment products + * OQI-004: Cuentas de Inversion + */ + +import React, { useState } from 'react'; +import { + Check, + X, + ChevronDown, + ChevronUp, + Star, + TrendingUp, + Shield, + DollarSign, + Percent, + Clock, + Users, + Zap, + Info, +} from 'lucide-react'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface ProductFeature { + name: string; + description?: string; + values: Record; +} + +export interface InvestmentProduct { + id: string; + name: string; + type: 'atlas' | 'orion' | 'nova' | 'custom'; + description: string; + targetReturn: { min: number; max: number }; + maxDrawdown: number; + managementFee: number; + performanceFee: number; + minCapital: number; + lockPeriod: number; + riskLevel: 'low' | 'medium' | 'high'; + strategies: string[]; + features: string[]; + isPopular?: boolean; + historicalReturn?: number; + activeAccounts?: number; +} + +interface ProductComparisonTableProps { + products: InvestmentProduct[]; + selectedProductId?: string; + onSelectProduct?: (productId: string) => void; + onViewDetails?: (productId: string) => void; + compact?: boolean; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const getProductColor = (type: InvestmentProduct['type']) => { + switch (type) { + case 'atlas': + return { bg: 'bg-blue-500', text: 'text-blue-400', border: 'border-blue-500', gradient: 'from-blue-500 to-blue-600' }; + case 'orion': + return { bg: 'bg-purple-500', text: 'text-purple-400', border: 'border-purple-500', gradient: 'from-purple-500 to-purple-600' }; + case 'nova': + return { bg: 'bg-amber-500', text: 'text-amber-400', border: 'border-amber-500', gradient: 'from-amber-500 to-amber-600' }; + default: + return { bg: 'bg-slate-500', text: 'text-slate-400', border: 'border-slate-500', gradient: 'from-slate-500 to-slate-600' }; + } +}; + +const getRiskBadge = (riskLevel: InvestmentProduct['riskLevel']) => { + switch (riskLevel) { + case 'low': + return { label: 'Low Risk', className: 'bg-emerald-500/20 text-emerald-400' }; + case 'medium': + return { label: 'Medium Risk', className: 'bg-yellow-500/20 text-yellow-400' }; + case 'high': + return { label: 'High Risk', className: 'bg-red-500/20 text-red-400' }; + default: + return { label: riskLevel, className: 'bg-slate-500/20 text-slate-400' }; + } +}; + +const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); +}; + +// ============================================================================ +// Component +// ============================================================================ + +export const ProductComparisonTable: React.FC = ({ + products, + selectedProductId, + onSelectProduct, + onViewDetails, + compact = false, +}) => { + const [expandedSection, setExpandedSection] = useState('returns'); + const [hoveredProduct, setHoveredProduct] = useState(null); + + const toggleSection = (section: string) => { + setExpandedSection(expandedSection === section ? null : section); + }; + + const comparisonSections = [ + { + id: 'returns', + label: 'Returns & Performance', + icon: TrendingUp, + rows: [ + { label: 'Target Return', key: 'targetReturn', format: (p: InvestmentProduct) => `${p.targetReturn.min}% - ${p.targetReturn.max}%` }, + { label: 'Historical Return', key: 'historicalReturn', format: (p: InvestmentProduct) => p.historicalReturn ? `${p.historicalReturn.toFixed(1)}%` : 'N/A' }, + { label: 'Max Drawdown', key: 'maxDrawdown', format: (p: InvestmentProduct) => `${p.maxDrawdown}%` }, + ], + }, + { + id: 'fees', + label: 'Fees & Costs', + icon: DollarSign, + rows: [ + { label: 'Management Fee', key: 'managementFee', format: (p: InvestmentProduct) => `${p.managementFee}%/year` }, + { label: 'Performance Fee', key: 'performanceFee', format: (p: InvestmentProduct) => `${p.performanceFee}%` }, + { label: 'Min Capital', key: 'minCapital', format: (p: InvestmentProduct) => formatCurrency(p.minCapital) }, + ], + }, + { + id: 'terms', + label: 'Terms & Conditions', + icon: Clock, + rows: [ + { label: 'Lock Period', key: 'lockPeriod', format: (p: InvestmentProduct) => `${p.lockPeriod} months` }, + { label: 'Risk Level', key: 'riskLevel', format: (p: InvestmentProduct) => getRiskBadge(p.riskLevel).label }, + { label: 'Active Accounts', key: 'activeAccounts', format: (p: InvestmentProduct) => p.activeAccounts?.toLocaleString() || 'N/A' }, + ], + }, + ]; + + if (compact) { + return ( +
+ + + + + + + + + + + + {products.map((product) => { + const colors = getProductColor(product.type); + const risk = getRiskBadge(product.riskLevel); + const isSelected = selectedProductId === product.id; + + return ( + + + + + + + + ); + })} + +
ProductTarget ReturnMin CapitalRiskAction
+
+
+ +
+
+
+ {product.name} + {product.isPopular && } +
+
{product.strategies.length} strategies
+
+
+
+ + {product.targetReturn.min}%-{product.targetReturn.max}% + + + {formatCurrency(product.minCapital)} + + + {risk.label} + + + +
+
+ ); + } + + return ( +
+ {/* Header Row - Product Names */} +
+
+ Compare Products +
+ {products.map((product) => { + const colors = getProductColor(product.type); + const isSelected = selectedProductId === product.id; + const isHovered = hoveredProduct === product.id; + + return ( +
setHoveredProduct(product.id)} + onMouseLeave={() => setHoveredProduct(null)} + className={`p-4 text-center border-l border-slate-700 transition-colors ${ + isSelected ? 'bg-slate-700/50' : isHovered ? 'bg-slate-800' : '' + }`} + > +
+ +
+
+ {product.name} + {product.isPopular && } +
+
{product.description}
+
+ ); + })} +
+ + {/* Comparison Sections */} + {comparisonSections.map((section) => { + const Icon = section.icon; + const isExpanded = expandedSection === section.id; + + return ( +
+ {/* Section Header */} + + + {/* Section Rows */} + {isExpanded && section.rows.map((row, rowIdx) => ( +
+
+ {row.label} +
+ {products.map((product) => { + const isSelected = selectedProductId === product.id; + const isHovered = hoveredProduct === product.id; + + return ( +
+ {row.format(product)} +
+ ); + })} +
+ ))} +
+ ); + })} + + {/* Strategies Section */} +
+ + + {expandedSection === 'strategies' && ( +
+
+ Included +
+ {products.map((product) => { + const isSelected = selectedProductId === product.id; + + return ( +
+
+ {product.strategies.map((strategy) => ( + + {strategy} + + ))} +
+
+ ); + })} +
+ )} +
+ + {/* Action Row */} +
+
+ {products.map((product) => { + const isSelected = selectedProductId === product.id; + const colors = getProductColor(product.type); + + return ( +
+ + {onViewDetails && ( + + )} +
+ ); + })} +
+
+ ); +}; + +export default ProductComparisonTable; diff --git a/src/modules/investment/components/index.ts b/src/modules/investment/components/index.ts new file mode 100644 index 0000000..9d9a4c9 --- /dev/null +++ b/src/modules/investment/components/index.ts @@ -0,0 +1,21 @@ +/** + * Investment Module Components + * Barrel export for all investment-related components + */ + +// Form Components +export { DepositForm } from './DepositForm'; +export { WithdrawForm } from './WithdrawForm'; + +// Account Components (OQI-004) +export { default as AccountSummaryCard } from './AccountSummaryCard'; +export type { InvestmentAccount } from './AccountSummaryCard'; + +export { default as ProductComparisonTable } from './ProductComparisonTable'; +export type { InvestmentProduct, ProductFeature } from './ProductComparisonTable'; + +export { default as PerformanceWidgetChart } from './PerformanceWidgetChart'; +export type { PerformanceDataPoint } from './PerformanceWidgetChart'; + +export { default as AccountSettingsPanel } from './AccountSettingsPanel'; +export type { AccountSettings, AccountForSettings } from './AccountSettingsPanel';