Implements Phase 11 Forecasting Tools - a "what-if" scenario planning system for HOA boards to model financial decisions before committing. Backend: - 3 new tenant-scoped tables: board_scenarios, scenario_investments, scenario_assessments - Migration script (013) for existing tenants - Full CRUD service for scenarios, investments, and assessments - Projection engine adapted from cash flow forecast with investment/assessment deltas - Scenario comparison endpoint (up to 4 scenarios) - Investment execution flow: converts planned → real investment_accounts + journal entry Frontend: - New "Board Planning" sidebar section with 3 pages - Investment Scenarios: list, create, detail with investments table + timeline - Assessment Scenarios: list, create, detail with changes table - Scenario Comparison: multi-select overlay chart + summary metrics - Shared components: ProjectionChart, InvestmentTimeline, ScenarioCard, forms - AI Recommendation → Investment Plan integration (Story 1A) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
129 lines
5.3 KiB
TypeScript
129 lines
5.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { Title, Text, Stack, Group, Button, SimpleGrid, Modal, TextInput, Textarea, Loader, Center } from '@mantine/core';
|
|
import { IconPlus } from '@tabler/icons-react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { notifications } from '@mantine/notifications';
|
|
import api from '../../services/api';
|
|
import { ScenarioCard } from './components/ScenarioCard';
|
|
|
|
export function InvestmentScenariosPage() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [editScenario, setEditScenario] = useState<any>(null);
|
|
const [form, setForm] = useState({ name: '', description: '' });
|
|
|
|
const { data: scenarios, isLoading } = useQuery<any[]>({
|
|
queryKey: ['board-planning-scenarios', 'investment'],
|
|
queryFn: async () => {
|
|
const { data } = await api.get('/board-planning/scenarios?type=investment');
|
|
return data;
|
|
},
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (dto: any) => api.post('/board-planning/scenarios', dto),
|
|
onSuccess: (res) => {
|
|
queryClient.invalidateQueries({ queryKey: ['board-planning-scenarios'] });
|
|
setCreateOpen(false);
|
|
setForm({ name: '', description: '' });
|
|
notifications.show({ message: 'Scenario created', color: 'green' });
|
|
navigate(`/board-planning/investments/${res.data.id}`);
|
|
},
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, ...dto }: any) => api.put(`/board-planning/scenarios/${id}`, dto),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['board-planning-scenarios'] });
|
|
setEditScenario(null);
|
|
notifications.show({ message: 'Scenario updated', color: 'green' });
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => api.delete(`/board-planning/scenarios/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['board-planning-scenarios'] });
|
|
notifications.show({ message: 'Scenario archived', color: 'orange' });
|
|
},
|
|
});
|
|
|
|
if (isLoading) return <Center h={400}><Loader size="lg" /></Center>;
|
|
|
|
return (
|
|
<Stack>
|
|
<Group justify="space-between" align="flex-start">
|
|
<div>
|
|
<Title order={2}>Investment Scenarios</Title>
|
|
<Text c="dimmed" size="sm">
|
|
Model different investment strategies and compare their impact on liquidity and income
|
|
</Text>
|
|
</div>
|
|
<Button leftSection={<IconPlus size={16} />} onClick={() => setCreateOpen(true)}>
|
|
New Scenario
|
|
</Button>
|
|
</Group>
|
|
|
|
{scenarios && scenarios.length > 0 ? (
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
|
|
{scenarios.map((s) => (
|
|
<ScenarioCard
|
|
key={s.id}
|
|
scenario={s}
|
|
onClick={() => navigate(`/board-planning/investments/${s.id}`)}
|
|
onEdit={() => { setEditScenario(s); setForm({ name: s.name, description: s.description || '' }); }}
|
|
onDelete={() => deleteMutation.mutate(s.id)}
|
|
/>
|
|
))}
|
|
</SimpleGrid>
|
|
) : (
|
|
<Center py="xl">
|
|
<Stack align="center" gap="sm">
|
|
<Text c="dimmed">No investment scenarios yet</Text>
|
|
<Text size="sm" c="dimmed" maw={400} ta="center">
|
|
Create a scenario to model investment allocations, timing, and their impact on reserves and liquidity.
|
|
</Text>
|
|
</Stack>
|
|
</Center>
|
|
)}
|
|
|
|
{/* Create Modal */}
|
|
<Modal opened={createOpen} onClose={() => setCreateOpen(false)} title="New Investment Scenario">
|
|
<Stack>
|
|
<TextInput label="Name" required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Conservative CD Ladder" />
|
|
<Textarea label="Description" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Describe this investment strategy..." />
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={() => setCreateOpen(false)}>Cancel</Button>
|
|
<Button
|
|
onClick={() => createMutation.mutate({ name: form.name, description: form.description, scenarioType: 'investment' })}
|
|
loading={createMutation.isPending}
|
|
disabled={!form.name}
|
|
>
|
|
Create Scenario
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
{/* Edit Modal */}
|
|
<Modal opened={!!editScenario} onClose={() => setEditScenario(null)} title="Edit Scenario">
|
|
<Stack>
|
|
<TextInput label="Name" required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
|
<Textarea label="Description" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={() => setEditScenario(null)}>Cancel</Button>
|
|
<Button
|
|
onClick={() => updateMutation.mutate({ id: editScenario.id, name: form.name, description: form.description })}
|
|
loading={updateMutation.isPending}
|
|
>
|
|
Save Changes
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
</Stack>
|
|
);
|
|
}
|