rfunds-online/components/mortgage/MortgageSimulator.tsx

195 lines
7.6 KiB
TypeScript

'use client'
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
import type { MortgageSimulatorConfig, MortgageTranche } from '@/lib/mortgage-types'
import { DEFAULT_CONFIG } from '@/lib/mortgage-types'
import { runFullSimulation, computeSummary } from '@/lib/mortgage-engine'
import MortgageControls from './MortgageControls'
import MortgageFlowViz from './MortgageFlowViz'
import LenderGrid from './LenderGrid'
import MycelialNetworkViz from './MycelialNetworkViz'
import LenderDetail from './LenderDetail'
import LenderReturnCalculator from './LenderReturnCalculator'
import ComparisonPanel from './ComparisonPanel'
type ViewMode = 'mycelial' | 'flow' | 'grid' | 'lender'
export default function MortgageSimulator() {
const [config, setConfig] = useState<MortgageSimulatorConfig>(DEFAULT_CONFIG)
const [currentMonth, setCurrentMonth] = useState(DEFAULT_CONFIG.startMonth)
const [playing, setPlaying] = useState(false)
const [speed, setSpeed] = useState(1)
const [selectedTranche, setSelectedTranche] = useState<MortgageTranche | null>(null)
const [viewMode, setViewMode] = useState<ViewMode>('mycelial')
const [controlsOpen, setControlsOpen] = useState(true)
const playRef = useRef(playing)
const speedRef = useRef(speed)
// Run full simulation when config changes
const states = useMemo(() => runFullSimulation(config), [config])
const maxMonth = states.length - 1
const currentState = states[Math.min(currentMonth, maxMonth)]
const summary = useMemo(() => computeSummary(states), [states])
// Keep refs in sync
useEffect(() => { playRef.current = playing }, [playing])
useEffect(() => { speedRef.current = speed }, [speed])
// Playback
useEffect(() => {
if (!playing) return
const interval = setInterval(() => {
setCurrentMonth(prev => {
if (prev >= maxMonth) {
setPlaying(false)
return prev
}
return prev + speedRef.current
})
}, 100)
return () => clearInterval(interval)
}, [playing, maxMonth])
// Reset month when config changes
useEffect(() => {
setCurrentMonth(Math.min(config.startMonth, maxMonth))
setPlaying(false)
setSelectedTranche(null)
}, [config, maxMonth])
const handleSelectTranche = useCallback((t: MortgageTranche | null) => {
setSelectedTranche(t)
}, [])
const handleToggleForSale = useCallback((trancheId: string) => {
// In a real app this would update the backend; for the simulator we just toggle locally
setSelectedTranche(prev => {
if (!prev || prev.id !== trancheId) return prev
return {
...prev,
listedForSale: !prev.listedForSale,
askingPrice: !prev.listedForSale ? prev.principalRemaining : undefined,
}
})
}, [])
// Keep selected tranche in sync with current state
const selectedTrancheState = useMemo(() => {
if (!selectedTranche) return null
const fromState = currentState.tranches.find(t => t.id === selectedTranche.id)
if (!fromState) return null
// Merge any local toggles (listedForSale) with simulation state
return {
...fromState,
listedForSale: selectedTranche.listedForSale,
askingPrice: selectedTranche.askingPrice,
}
}, [currentState, selectedTranche])
return (
<div className="min-h-screen bg-[#0f172a] text-slate-200">
{/* Header */}
<header className="border-b border-slate-800 px-6 py-4">
<div className="max-w-[1600px] mx-auto flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white">(you)rMortgage</h1>
<p className="text-sm text-slate-400">
{currentState.tranches.length} lenders &times; {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(config.trancheSize)} tranches
</p>
</div>
<div className="flex gap-2">
{(['mycelial', 'flow', 'grid', 'lender'] as const).map(mode => (
<button
key={mode}
onClick={() => setViewMode(mode)}
className={`px-3 py-1.5 rounded text-sm transition-colors ${
viewMode === mode ? 'bg-sky-600 text-white' : 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
{mode === 'mycelial' ? 'Network' : mode === 'flow' ? 'Flow' : mode === 'grid' ? 'Grid' : 'Lender'}
</button>
))}
</div>
</div>
</header>
{/* Main Layout */}
<div className="max-w-[1600px] mx-auto flex gap-0 min-h-[calc(100vh-73px)]">
{/* Left: Controls (collapsible) */}
<aside className={`flex-shrink-0 border-r border-slate-800 transition-all duration-300 ${controlsOpen ? 'w-64' : 'w-10'} relative overflow-hidden`}>
<button
onClick={() => setControlsOpen(o => !o)}
className="absolute top-2 right-2 z-10 w-6 h-6 rounded bg-slate-700 hover:bg-slate-600 flex items-center justify-center text-xs text-slate-300 transition-colors"
title={controlsOpen ? 'Minimize settings' : 'Expand settings'}
>
{controlsOpen ? '\u2039' : '\u203A'}
</button>
{!controlsOpen && (
<button
onClick={() => setControlsOpen(true)}
className="absolute inset-0 w-full h-full flex items-start justify-center pt-12 text-slate-500 hover:text-slate-300 transition-colors"
title="Expand settings"
>
<span className="[writing-mode:vertical-lr] text-xs tracking-widest uppercase font-semibold">Loan Settings</span>
</button>
)}
<div className={`p-4 overflow-y-auto h-full transition-opacity duration-200 ${controlsOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
<MortgageControls
config={config}
onChange={setConfig}
currentMonth={Math.min(currentMonth, maxMonth)}
maxMonth={maxMonth}
onMonthChange={setCurrentMonth}
playing={playing}
onPlayToggle={() => setPlaying(p => !p)}
speed={speed}
onSpeedChange={setSpeed}
/>
</div>
</aside>
{/* Center: Visualization */}
<main className="flex-1 p-4 overflow-y-auto">
{viewMode === 'mycelial' ? (
<MycelialNetworkViz
state={currentState}
selectedTrancheId={selectedTranche?.id ?? null}
onSelectTranche={handleSelectTranche}
/>
) : viewMode === 'flow' ? (
<MortgageFlowViz
state={currentState}
selectedTrancheId={selectedTranche?.id ?? null}
onSelectTranche={handleSelectTranche}
/>
) : viewMode === 'grid' ? (
<LenderGrid
tranches={currentState.tranches}
onSelectTranche={handleSelectTranche}
selectedTrancheId={selectedTranche?.id ?? null}
/>
) : (
<LenderReturnCalculator config={config} />
)}
{/* Lender Detail (below viz when selected) */}
{selectedTrancheState && (
<div className="mt-4 max-w-xl">
<LenderDetail
tranche={selectedTrancheState}
onClose={() => setSelectedTranche(null)}
onToggleForSale={handleToggleForSale}
/>
</div>
)}
</main>
{/* Right: Comparison */}
<aside className="w-72 flex-shrink-0 border-l border-slate-800 p-4 overflow-y-auto">
<ComparisonPanel state={currentState} summary={summary} />
</aside>
</div>
</div>
)
}