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:
2026-03-16 16:18:40 -04:00
parent 7ba5c414b1
commit 159c59734e
4 changed files with 213 additions and 88 deletions

View File

@@ -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"