Migrated pitch-deck from breakpilot-pwa to breakpilot-core. Container: bp-core-pitch-deck on port 3012. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
import { FMScenario, FMResult, FMComputeResponse } from '../types'
|
|
|
|
export function useFinancialModel() {
|
|
const [scenarios, setScenarios] = useState<FMScenario[]>([])
|
|
const [activeScenarioId, setActiveScenarioId] = useState<string | null>(null)
|
|
const [compareMode, setCompareMode] = useState(false)
|
|
const [results, setResults] = useState<Map<string, FMComputeResponse>>(new Map())
|
|
const [loading, setLoading] = useState(true)
|
|
const [computing, setComputing] = useState(false)
|
|
const computeTimer = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
// Load scenarios on mount
|
|
useEffect(() => {
|
|
async function load() {
|
|
try {
|
|
const res = await fetch('/api/financial-model')
|
|
if (res.ok) {
|
|
const data: FMScenario[] = await res.json()
|
|
setScenarios(data)
|
|
const defaultScenario = data.find(s => s.is_default) || data[0]
|
|
if (defaultScenario) {
|
|
setActiveScenarioId(defaultScenario.id)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load financial model:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [])
|
|
|
|
// Compute when active scenario changes
|
|
useEffect(() => {
|
|
if (activeScenarioId && !results.has(activeScenarioId)) {
|
|
compute(activeScenarioId)
|
|
}
|
|
}, [activeScenarioId]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const compute = useCallback(async (scenarioId: string) => {
|
|
setComputing(true)
|
|
try {
|
|
const res = await fetch('/api/financial-model/compute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ scenarioId }),
|
|
})
|
|
if (res.ok) {
|
|
const data: FMComputeResponse = await res.json()
|
|
setResults(prev => new Map(prev).set(scenarioId, data))
|
|
}
|
|
} catch (err) {
|
|
console.error('Compute failed:', err)
|
|
} finally {
|
|
setComputing(false)
|
|
}
|
|
}, [])
|
|
|
|
const updateAssumption = useCallback(async (scenarioId: string, key: string, value: number | number[]) => {
|
|
// Optimistic update in local state
|
|
setScenarios(prev => prev.map(s => {
|
|
if (s.id !== scenarioId) return s
|
|
return {
|
|
...s,
|
|
assumptions: s.assumptions.map(a => a.key === key ? { ...a, value } : a),
|
|
}
|
|
}))
|
|
|
|
// Save to DB
|
|
await fetch('/api/financial-model/assumptions', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ scenarioId, key, value }),
|
|
})
|
|
|
|
// Debounced recompute
|
|
if (computeTimer.current) clearTimeout(computeTimer.current)
|
|
computeTimer.current = setTimeout(() => compute(scenarioId), 300)
|
|
}, [compute])
|
|
|
|
const computeAll = useCallback(async () => {
|
|
for (const s of scenarios) {
|
|
await compute(s.id)
|
|
}
|
|
}, [scenarios, compute])
|
|
|
|
const activeScenario = scenarios.find(s => s.id === activeScenarioId) || null
|
|
const activeResults = activeScenarioId ? results.get(activeScenarioId) || null : null
|
|
|
|
return {
|
|
scenarios,
|
|
activeScenario,
|
|
activeScenarioId,
|
|
setActiveScenarioId,
|
|
activeResults,
|
|
results,
|
|
loading,
|
|
computing,
|
|
compareMode,
|
|
setCompareMode,
|
|
compute,
|
|
computeAll,
|
|
updateAssumption,
|
|
}
|
|
}
|