feat: investment chart alignment, auto-renew records, fund transfers, capital planning report, and upcoming activities (v2026.3.24)
- Lock InvestmentTimeline and ProjectionChart to shared X axis range - Auto-create renewal scenario_investments records when auto_renew is true - Add fund transfer mechanism between asset accounts with journal entries - Add Capital Planning Report (5-year forecast grouped by category) - Add Upcoming Investment Activities dashboard card (maturities + planned purchases) - Bump version to 2026.3.24 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
IconHeartbeat,
|
||||
IconRefresh,
|
||||
IconInfoCircle,
|
||||
IconCoin,
|
||||
IconCalendarEvent,
|
||||
} from '@tabler/icons-react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -362,6 +364,16 @@ export function DashboardPage() {
|
||||
enabled: !!currentOrg,
|
||||
});
|
||||
|
||||
const { data: investmentActivities } = useQuery<{
|
||||
maturing_investments: any[];
|
||||
upcoming_scenario_investments: any[];
|
||||
total_activities: number;
|
||||
}>({
|
||||
queryKey: ['upcoming-investment-activities'],
|
||||
queryFn: async () => { const { data } = await api.get('/reports/upcoming-investment-activities'); return data; },
|
||||
enabled: !!currentOrg,
|
||||
});
|
||||
|
||||
const { data: healthScores } = useQuery<HealthScoresData>({
|
||||
queryKey: ['health-scores'],
|
||||
queryFn: async () => { const { data } = await api.get('/health-scores/latest'); return data; },
|
||||
@@ -531,6 +543,97 @@ export function DashboardPage() {
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Upcoming Investment Activities */}
|
||||
{(investmentActivities?.total_activities || 0) > 0 && (
|
||||
<Card withBorder padding="lg" radius="md">
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon color="teal" variant="light" size={28} radius="md">
|
||||
<IconCalendarEvent size={16} />
|
||||
</ThemeIcon>
|
||||
<Title order={4}>Upcoming Investment Activities</Title>
|
||||
</Group>
|
||||
<Badge variant="light" color="teal">{investmentActivities?.total_activities} upcoming</Badge>
|
||||
</Group>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Activity</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Fund</Table.Th>
|
||||
<Table.Th ta="right">Amount</Table.Th>
|
||||
<Table.Th>Date</Table.Th>
|
||||
<Table.Th>Timeline</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{(investmentActivities?.maturing_investments || []).map((inv: any) => (
|
||||
<Table.Tr key={`mat-${inv.id}`}>
|
||||
<Table.Td>
|
||||
<Group gap={6}>
|
||||
<IconCoin size={14} color="var(--mantine-color-orange-6)" />
|
||||
<Text size="sm" fw={500}>{inv.name}</Text>
|
||||
</Group>
|
||||
{inv.institution && <Text size="xs" c="dimmed">{inv.institution}</Text>}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" color="orange" variant="light">Maturing</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" color={inv.fund_type === 'reserve' ? 'violet' : 'blue'} variant="light">
|
||||
{inv.fund_type}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right" ff="monospace">
|
||||
<Text size="sm" fw={500}>{fmt(inv.maturity_value)}</Text>
|
||||
<Text size="xs" c="green">+{fmt(inv.interest_earned)} interest</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{new Date(inv.maturity_date).toLocaleDateString()}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" color={inv.days_remaining <= 14 ? 'red' : inv.days_remaining <= 30 ? 'yellow' : 'gray'} variant="light">
|
||||
{inv.days_remaining} days
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
{(investmentActivities?.upcoming_scenario_investments || []).map((si: any) => (
|
||||
<Table.Tr key={`plan-${si.id}`}>
|
||||
<Table.Td>
|
||||
<Group gap={6}>
|
||||
<IconTrendingUp size={14} color="var(--mantine-color-blue-6)" />
|
||||
<Text size="sm" fw={500}>{si.label}</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">Scenario: {si.scenario_name}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" color="blue" variant="light">Planned Purchase</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" color={si.fund_type === 'reserve' ? 'violet' : 'blue'} variant="light">
|
||||
{si.fund_type}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right" ff="monospace">
|
||||
<Text size="sm" fw={500}>{fmt(si.principal)}</Text>
|
||||
{si.interest_rate && <Text size="xs" c="dimmed">{parseFloat(si.interest_rate).toFixed(2)}% APY</Text>}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{new Date(si.purchase_date).toLocaleDateString()}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" color={si.days_until <= 14 ? 'red' : si.days_until <= 30 ? 'yellow' : 'gray'} variant="light">
|
||||
{si.days_until} days
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
<Card withBorder padding="lg" radius="md">
|
||||
<Title order={4}>Quick Stats</Title>
|
||||
|
||||
Reference in New Issue
Block a user