feat: add Board Planning module with investment/assessment scenario modeling

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>
This commit is contained in:
2026-03-16 09:52:10 -04:00
parent b13fbfe8c7
commit c8d77aaa48
20 changed files with 2901 additions and 1 deletions

View File

@@ -0,0 +1,74 @@
import { Card, Group, Text, Badge, ActionIcon, Menu } from '@mantine/core';
import { IconDots, IconTrash, IconEdit, IconPlayerPlay } from '@tabler/icons-react';
const fmt = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
const statusColors: Record<string, string> = {
draft: 'gray',
active: 'blue',
approved: 'green',
archived: 'red',
};
interface Props {
scenario: any;
onClick: () => void;
onEdit: () => void;
onDelete: () => void;
}
export function ScenarioCard({ scenario, onClick, onEdit, onDelete }: Props) {
return (
<Card withBorder p="lg" style={{ cursor: 'pointer' }} onClick={onClick}>
<Group justify="space-between" mb="xs">
<Group gap="xs">
<Text fw={600}>{scenario.name}</Text>
<Badge size="xs" color={statusColors[scenario.status] || 'gray'}>
{scenario.status}
</Badge>
</Group>
<Menu withinPortal position="bottom-end" shadow="sm">
<Menu.Target>
<ActionIcon variant="subtle" color="gray" onClick={(e: any) => e.stopPropagation()}>
<IconDots size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconEdit size={14} />} onClick={(e: any) => { e.stopPropagation(); onEdit(); }}>
Edit
</Menu.Item>
<Menu.Item leftSection={<IconTrash size={14} />} color="red" onClick={(e: any) => { e.stopPropagation(); onDelete(); }}>
Archive
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
{scenario.description && (
<Text size="sm" c="dimmed" mb="sm" lineClamp={2}>{scenario.description}</Text>
)}
<Group gap="lg">
{scenario.scenario_type === 'investment' && (
<>
<div>
<Text size="xs" c="dimmed">Investments</Text>
<Text fw={600}>{scenario.investment_count || 0}</Text>
</div>
<div>
<Text size="xs" c="dimmed">Total Principal</Text>
<Text fw={600} ff="monospace">{fmt(parseFloat(scenario.total_principal) || 0)}</Text>
</div>
</>
)}
{scenario.scenario_type === 'assessment' && (
<div>
<Text size="xs" c="dimmed">Changes</Text>
<Text fw={600}>{scenario.assessment_count || 0}</Text>
</div>
)}
</Group>
<Text size="xs" c="dimmed" mt="sm">
Updated {new Date(scenario.updated_at).toLocaleDateString()}
</Text>
</Card>
);
}