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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user