feat: investment scenario UX improvements and interest calculations
- Refresh Recommendations now shows inline processing banner with animated progress bar while keeping existing results visible (dimmed). Auto-scrolls to AI section and shows titled notification on completion. - Investment recommendations now auto-calculate purchase and maturity dates from a configurable start date (defaults to today) in the "Add to Plan" modal, so scenarios build projections immediately. - Projection engine computes per-investment and total interest earned, ROI percentage, and total principal invested. Summary cards on the Investment Scenario detail page display these metrics prominently. - Replaced dropdown action menu with inline Edit/Execute/Remove icon buttons matching the assessment scenarios pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Title,
|
||||
Text,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Modal,
|
||||
Select,
|
||||
TextInput,
|
||||
Progress,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBulb,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
IconChevronUp,
|
||||
IconPlaylistAdd,
|
||||
} from '@tabler/icons-react';
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -381,6 +383,7 @@ export function InvestmentPlanningPage() {
|
||||
const [selectedRec, setSelectedRec] = useState<Recommendation | null>(null);
|
||||
const [targetScenarioId, setTargetScenarioId] = useState<string | null>(null);
|
||||
const [newScenarioName, setNewScenarioName] = useState('');
|
||||
const [investmentStartDate, setInvestmentStartDate] = useState<Date | null>(new Date());
|
||||
|
||||
// Load investment scenarios for the "Add to Plan" modal
|
||||
const { data: investmentScenarios } = useQuery<any[]>({
|
||||
@@ -403,6 +406,7 @@ export function InvestmentPlanningPage() {
|
||||
bankName: rec.bank_name,
|
||||
rationale: rec.rationale,
|
||||
components: rec.components || undefined,
|
||||
startDate: investmentStartDate ? investmentStartDate.toISOString().split('T')[0] : null,
|
||||
});
|
||||
return scenarioId;
|
||||
},
|
||||
@@ -433,6 +437,7 @@ export function InvestmentPlanningPage() {
|
||||
bankName: rec.bank_name,
|
||||
rationale: rec.rationale,
|
||||
components: rec.components || undefined,
|
||||
startDate: investmentStartDate ? investmentStartDate.toISOString().split('T')[0] : null,
|
||||
});
|
||||
return scenario.id;
|
||||
},
|
||||
@@ -453,6 +458,7 @@ export function InvestmentPlanningPage() {
|
||||
setSelectedRec(rec);
|
||||
setTargetScenarioId(null);
|
||||
setNewScenarioName('');
|
||||
setInvestmentStartDate(new Date());
|
||||
setPlanModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -507,20 +513,31 @@ export function InvestmentPlanningPage() {
|
||||
}
|
||||
}, [savedRec?.status, isTriggering]);
|
||||
|
||||
// Ref for scrolling to AI section on completion
|
||||
const aiSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Show notification when processing completes (transition from processing)
|
||||
const prevStatusRef = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
const [prevStatus, setPrevStatus] = prevStatusRef;
|
||||
if (prevStatus === 'processing' && savedRec?.status === 'complete') {
|
||||
notifications.show({
|
||||
title: 'AI Analysis Complete',
|
||||
message: `Generated ${savedRec.recommendations.length} investment recommendations`,
|
||||
color: 'green',
|
||||
autoClose: 8000,
|
||||
});
|
||||
// Scroll the AI section into view so user sees the new results
|
||||
setTimeout(() => {
|
||||
aiSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 300);
|
||||
}
|
||||
if (prevStatus === 'processing' && savedRec?.status === 'error') {
|
||||
notifications.show({
|
||||
title: 'AI Analysis Failed',
|
||||
message: savedRec.error_message || 'AI recommendation analysis failed',
|
||||
color: 'red',
|
||||
autoClose: 8000,
|
||||
});
|
||||
}
|
||||
setPrevStatus(savedRec?.status || null);
|
||||
@@ -791,7 +808,7 @@ export function InvestmentPlanningPage() {
|
||||
<Divider />
|
||||
|
||||
{/* ── Section 4: AI Investment Recommendations ── */}
|
||||
<Card withBorder p="lg">
|
||||
<Card withBorder p="lg" ref={aiSectionRef}>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" color="grape" size="md">
|
||||
@@ -815,19 +832,22 @@ export function InvestmentPlanningPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Processing State */}
|
||||
{/* Processing State - shown as banner when refreshing with existing results */}
|
||||
{isProcessing && (
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="sm">
|
||||
<Loader size="lg" type="dots" />
|
||||
<Text c="dimmed" size="sm">
|
||||
Analyzing your financial data and market rates...
|
||||
</Text>
|
||||
<Text c="dimmed" size="xs">
|
||||
You can navigate away — results will appear when ready
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
<Alert variant="light" color="grape" mb="md" styles={{ root: { overflow: 'visible' } }}>
|
||||
<Group gap="sm">
|
||||
<Loader size="sm" color="grape" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{aiResult ? 'Refreshing AI analysis...' : 'Running AI analysis...'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Analyzing your financial data, accounts, budgets, and current market rates
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Progress value={100} animated color="grape" size="xs" mt="xs" />
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Error State (no cached data) */}
|
||||
@@ -839,17 +859,19 @@ export function InvestmentPlanningPage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Results (with optional failure watermark) */}
|
||||
{aiResult && !isProcessing && (
|
||||
<RecommendationsDisplay
|
||||
aiResult={aiResult}
|
||||
lastUpdated={savedRec?.created_at || undefined}
|
||||
lastFailed={lastFailed}
|
||||
onAddToPlan={handleAddToPlan}
|
||||
/>
|
||||
{/* Results - keep visible even while refreshing (with optional failure watermark) */}
|
||||
{aiResult && (
|
||||
<div style={isProcessing ? { opacity: 0.5, pointerEvents: 'none' } : undefined}>
|
||||
<RecommendationsDisplay
|
||||
aiResult={aiResult}
|
||||
lastUpdated={savedRec?.created_at || undefined}
|
||||
lastFailed={lastFailed}
|
||||
onAddToPlan={handleAddToPlan}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{/* Empty State - only when no results and not processing */}
|
||||
{!aiResult && !isProcessing && !hasError && (
|
||||
<Paper p="xl" radius="sm" style={{ textAlign: 'center' }}>
|
||||
<ThemeIcon variant="light" color="grape" size={48} mx="auto" mb="md">
|
||||
@@ -890,6 +912,14 @@ export function InvestmentPlanningPage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DateInput
|
||||
label="Start Date"
|
||||
description="Purchase date for the investment(s). Maturity dates are calculated automatically from term length."
|
||||
value={investmentStartDate}
|
||||
onChange={setInvestmentStartDate}
|
||||
clearable
|
||||
/>
|
||||
|
||||
{investmentScenarios && investmentScenarios.length > 0 && (
|
||||
<Select
|
||||
label="Add to existing scenario"
|
||||
|
||||
Reference in New Issue
Block a user