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:
2026-03-24 14:41:02 -04:00
parent ae856bfb2f
commit 2b331bb3ef
15 changed files with 801 additions and 21 deletions

View File

@@ -13,9 +13,12 @@ const typeColors: Record<string, string> = {
interface Props {
investments: any[];
/** Optional shared time range to align with ProjectionChart */
sharedStartDate?: Date;
sharedEndDate?: Date;
}
export function InvestmentTimeline({ investments }: Props) {
export function InvestmentTimeline({ investments, sharedStartDate, sharedEndDate }: Props) {
const { items, startDate, endDate, totalMonths } = useMemo(() => {
const now = new Date();
const items = investments
@@ -28,16 +31,24 @@ export function InvestmentTimeline({ investments }: Props) {
if (!items.length) return { items: [], startDate: now, endDate: now, totalMonths: 1 };
const allDates = items.flatMap((i: any) => [i.start, i.end].filter(Boolean)) as Date[];
const startDate = new Date(Math.min(...allDates.map((d) => d.getTime())));
const endDate = new Date(Math.max(...allDates.map((d) => d.getTime())));
// Use shared range if provided (to align with ProjectionChart), otherwise compute from investments
let startDate: Date;
let endDate: Date;
if (sharedStartDate && sharedEndDate) {
startDate = sharedStartDate;
endDate = sharedEndDate;
} else {
const allDates = items.flatMap((i: any) => [i.start, i.end].filter(Boolean)) as Date[];
startDate = new Date(Math.min(...allDates.map((d) => d.getTime())));
endDate = new Date(Math.max(...allDates.map((d) => d.getTime())));
}
const totalMonths = Math.max(
(endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth()) + 1,
1,
);
return { items, startDate, endDate, totalMonths };
}, [investments]);
}, [investments, sharedStartDate, sharedEndDate]);
if (!items.length) return null;

View File

@@ -23,18 +23,31 @@ interface Props {
datapoints: Datapoint[];
title?: string;
summary?: any;
/** Optional shared time range to align with InvestmentTimeline */
sharedStartDate?: Date;
sharedEndDate?: Date;
}
export function ProjectionChart({ datapoints, title = 'Financial Projection', summary }: Props) {
export function ProjectionChart({ datapoints, title = 'Financial Projection', summary, sharedStartDate, sharedEndDate }: Props) {
const [fundFilter, setFundFilter] = useState('all');
const chartData = useMemo(() => {
return datapoints.map((d) => ({
let filtered = datapoints;
// If shared range provided, filter datapoints to match
if (sharedStartDate && sharedEndDate) {
const startKey = sharedStartDate.getFullYear() * 12 + sharedStartDate.getMonth();
const endKey = sharedEndDate.getFullYear() * 12 + sharedEndDate.getMonth();
filtered = datapoints.filter((d) => {
const dpKey = d.year * 12 + (d.monthNum - 1);
return dpKey >= startKey && dpKey <= endKey;
});
}
return filtered.map((d) => ({
...d,
label: `${d.month}`,
total: d.operating_cash + d.operating_investments + d.reserve_cash + d.reserve_investments,
}));
}, [datapoints]);
}, [datapoints, sharedStartDate, sharedEndDate]);
// Find first forecast month for reference line
const forecastStart = chartData.findIndex((d) => d.is_forecast);