// screens-roi.jsx — ROI Calculator screen (CP-16) // // Calculation flow (migrated to Python backend): // 1. User changes any input → debounce fires after 400 ms. // 2. Keeper.api.calculateRoi(inputs) sends POST /api/v1/roi/calculate to the // api_gateway, which proxies to the reporting_service Python domain logic. // 3. On success the backend result replaces the local result state. // 4. On network/backend error the screen falls back silently to the local // Keeper.roi.calculate() formula so the UI remains interactive. // // Local formula (Keeper.roi.calculate) is still used for: // - The initial synchronous render before the first API response arrives. // - Any transient backend failure (debounce cycle restores backend path). // // The formula note at the bottom now reflects the backend source. // // Pattern mirrors other screens (screens-overview.jsx, screens-work.jsx): // - React.useState / React.useEffect for local state // - Keeper.api.calculateRoi for async backend calculation // - Keeper.roi.calculate as synchronous fallback // - Keeper.roi.formatNumber() for consistent number display // - Exported to window.RoiCalculator so App.jsx can reference it /* ─── CP-16 ROI Calculator ────────────────────────────────── */ function RoiCalculator() { // ── Demo defaults (snake_case keys match the backend POST body) ────────── const DEFAULTS = { hectares: 4, yield_per_ha: 280, price_per_ton: 900, gain_pct: 6, cost_per_ha: 38000, }; const [inputs, setInputs] = React.useState(DEFAULTS); // result: current calculation output. // Initialised synchronously from the local formula so the first render is // instant; replaced by the backend response once the first API call returns. const [result, setResult] = React.useState(() => _localCalc(DEFAULTS)); // Loading / error state — user feedback without blocking the UI. const [loading, setLoading] = React.useState(false); const [backendError, setBackendError] = React.useState(null); const fmt = Keeper.roi.formatNumber; // ── Local fallback helper ───────────────────────────────────────────────── // Adapts snake_case inputs → roi.js camelCase params and returns the result // in the display shape (camelCase) expected by the JSX below. function _localCalc(inp) { return Keeper.roi.calculate({ hectares: inp.hectares, yieldPerHa: inp.yield_per_ha, pricePerTon: inp.price_per_ton, gainPct: inp.gain_pct, costPerHa: inp.cost_per_ha, }); } // ── Backend → display shape normalisation ──────────────────────────────── // The backend returns snake_case; the JSX was originally written against the // roi.js camelCase shape. We adapt here so both paths share one render tree. function _normaliseBackendResult(r) { return { baselineRevenue: r.baseline_revenue, incrementalRevenue: r.incremental_revenue, implementationCost: r.implementation_cost, paybackRatio: r.payback_ratio, payback: r.payback ? { status: r.payback.status, months: r.payback.months, years: r.payback.years, reason: r.payback.reason, } : null, breakdown: r.breakdown ? { revenue: (r.breakdown.revenue || []).map(item => ({ id: item.id, label: item.label, value: item.value, sharePct: item.share_pct, })), cost: r.breakdown.cost || [], chart: { incrementalVsBaselinePct: r.breakdown.chart ? r.breakdown.chart.incremental_vs_baseline_pct : 0, }, totals: r.breakdown.totals || { revenue: 0, cost: 0 }, } : null, }; } // ── Backend calculation (debounced 400 ms) ──────────────────────────────── React.useEffect(() => { let cancelled = false; const timer = setTimeout(async () => { if (cancelled) return; // Guard: skip if API module is not available (e.g. tests or offline dev). if (!window.Keeper || !window.Keeper.api || typeof window.Keeper.api.calculateRoi !== "function") { setResult(_localCalc(inputs)); return; } setLoading(true); setBackendError(null); try { const backendResult = await Keeper.api.calculateRoi(inputs); if (!cancelled) { setResult(_normaliseBackendResult(backendResult)); setLoading(false); } } catch (err) { if (!cancelled) { // Silently fall back — the user still sees numbers, just from the // local formula. A small banner tells them backend is unavailable. setResult(_localCalc(inputs)); setBackendError( (err && err.backendMessage) ? err.backendMessage : "Backend unavailable — using local calculation." ); setLoading(false); } } }, 400); return () => { cancelled = true; clearTimeout(timer); }; }, [inputs]); function handleChange(field, raw) { const value = parseFloat(raw) || 0; setInputs(prev => ({ ...prev, [field]: value })); } const paybackMonths = result.payback ? result.payback.months : null; return (