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>
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|