import { useState, useMemo } from 'react'; import { Title, Text, Stack, Group, Button, Table, Badge, Card, ActionIcon, Loader, Center, Select, Modal, TextInput, Alert, SimpleGrid, Tooltip, } from '@mantine/core'; import { DateInput } from '@mantine/dates'; import { IconPlus, IconArrowLeft, IconTrash, IconEdit, IconPlayerPlay, IconCoin, IconTrendingUp, } from '@tabler/icons-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import { notifications } from '@mantine/notifications'; import api from '../../services/api'; import { InvestmentForm } from './components/InvestmentForm'; import { ProjectionChart } from './components/ProjectionChart'; import { InvestmentTimeline } from './components/InvestmentTimeline'; const fmt = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }); const fmtDec = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }); const statusColors: Record = { draft: 'gray', active: 'blue', approved: 'green', archived: 'red', }; export function InvestmentScenarioDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const [addOpen, setAddOpen] = useState(false); const [editInv, setEditInv] = useState(null); const [executeInv, setExecuteInv] = useState(null); const [executionDate, setExecutionDate] = useState(new Date()); const { data: scenario, isLoading } = useQuery({ queryKey: ['board-planning-scenario', id], queryFn: async () => { const { data } = await api.get(`/board-planning/scenarios/${id}`); return data; }, }); const { data: projection, isLoading: projLoading, dataUpdatedAt: projUpdatedAt } = useQuery({ queryKey: ['board-planning-projection', id], queryFn: async () => { const { data } = await api.get(`/board-planning/scenarios/${id}/projection`); return data; }, enabled: !!id, }); // When projection refreshes (which may create auto-renew records on the backend), // re-fetch the scenario so the investments list picks up any new renewal records. const [lastProjUpdate, setLastProjUpdate] = useState(0); if (projUpdatedAt && projUpdatedAt !== lastProjUpdate) { setLastProjUpdate(projUpdatedAt); if (lastProjUpdate > 0) { // Only re-fetch after a real update (not the initial load) queryClient.invalidateQueries({ queryKey: ['board-planning-scenario', id] }); } } const addMutation = useMutation({ mutationFn: (dto: any) => api.post(`/board-planning/scenarios/${id}/investments`, dto), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['board-planning-scenario', id] }); queryClient.invalidateQueries({ queryKey: ['board-planning-projection', id] }); setAddOpen(false); notifications.show({ message: 'Investment added', color: 'green' }); }, }); const updateMutation = useMutation({ mutationFn: ({ invId, ...dto }: any) => api.put(`/board-planning/investments/${invId}`, dto), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['board-planning-scenario', id] }); queryClient.invalidateQueries({ queryKey: ['board-planning-projection', id] }); setEditInv(null); notifications.show({ message: 'Investment updated', color: 'green' }); }, }); const removeMutation = useMutation({ mutationFn: (invId: string) => api.delete(`/board-planning/investments/${invId}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['board-planning-scenario', id] }); queryClient.invalidateQueries({ queryKey: ['board-planning-projection', id] }); notifications.show({ message: 'Investment removed', color: 'orange' }); }, }); const executeMutation = useMutation({ mutationFn: ({ invId, executionDate }: { invId: string; executionDate: string }) => api.post(`/board-planning/investments/${invId}/execute`, { executionDate }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['board-planning-scenario', id] }); queryClient.invalidateQueries({ queryKey: ['board-planning-projection', id] }); setExecuteInv(null); notifications.show({ message: 'Investment executed and recorded', color: 'green' }); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Execution failed', color: 'red' }); }, }); const statusMutation = useMutation({ mutationFn: (status: string) => api.put(`/board-planning/scenarios/${id}`, { status }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['board-planning-scenario', id] }); queryClient.invalidateQueries({ queryKey: ['board-planning-scenarios'] }); }, }); // Compute shared time range for aligned charts (must be above early returns to satisfy Rules of Hooks) const investments = scenario?.investments || []; const summary = projection?.summary; const { sharedStartDate, sharedEndDate } = useMemo(() => { const allDates: Date[] = []; // Dates from investments for (const inv of investments) { if (inv.purchase_date) allDates.push(new Date(inv.purchase_date)); if (inv.maturity_date) allDates.push(new Date(inv.maturity_date)); } // Dates from projection datapoints const dps = projection?.datapoints || []; if (dps.length > 0) { allDates.push(new Date(dps[0].year, dps[0].monthNum - 1, 1)); const last = dps[dps.length - 1]; allDates.push(new Date(last.year, last.monthNum - 1, 1)); } if (allDates.length === 0) return { sharedStartDate: undefined, sharedEndDate: undefined }; const min = new Date(Math.min(...allDates.map((d) => d.getTime()))); const max = new Date(Math.max(...allDates.map((d) => d.getTime()))); return { sharedStartDate: new Date(min.getFullYear(), min.getMonth(), 1), sharedEndDate: new Date(max.getFullYear(), max.getMonth(), 1), }; }, [investments, projection]); if (isLoading) return
; if (!scenario) return
Scenario not found
; // Build a lookup of per-investment interest from the projection const interestDetailMap: Record = {}; if (summary?.investment_interest_details) { for (const d of summary.investment_interest_details) { interestDetailMap[d.id] = { interest: d.interest, principal: d.principal }; } } return ( {/* Header */} navigate('/board-planning/investments')}>
{scenario.name} {scenario.status} {scenario.description && {scenario.description}}