dev1/src/components/RetirementPlanner.js
2025-09-18 13:26:16 +00:00

194 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/RetirementPlanner.js
import React, { useEffect, useState, useCallback, useContext } from 'react';
import authFetch from '../utils/authFetch.js';
import ScenarioContainer from './ScenarioContainer.js';
import { Button } from './ui/button.js';
import RetirementChatBar from './RetirementChatBar.js';
import ScenarioDiffDrawer from './ScenarioDiffDrawer.js';
import ChatCtx from '../contexts/ChatCtx.js';
/* ------------------------------------------------------------------
* tiny classname helper
* ---------------------------------------------------------------- */
const cn = (...cls) => cls.filter(Boolean).join(' ');
/* ------------------------------------------------------------------
* responsive helper “mobile” = < 768px
* ---------------------------------------------------------------- */
function useIsMobile () {
const [mobile, setMobile] = useState(() => window.innerWidth < 768);
useEffect(() => {
const handler = () => setMobile(window.innerWidth < 768);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return mobile;
}
/* ==================================================================
* RetirementPlanner
* ================================================================= */
export default function RetirementPlanner () {
/* ---------------------------- state ----------------------------- */
const [loading, setLoading ] = useState(false);
const [error, setError ] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null);
const [scenarios, setScenarios] = useState([]);
const [selectedScenario, setSelectedScenario] = useState(null);
const [chatOpen, setChatOpen] = useState(false); // slidein flag
const [diff, setDiff] = useState(null);
const [simYearsMap, setSimYearsMap] = useState({});
const isMobile = useIsMobile();
const { openRetire } = useContext(ChatCtx);
/* ----------------------- data loading -------------------------- */
const loadAll = useCallback(async () => {
try {
setLoading(true); setError(null);
/* financial profile ------------------------------------------------ */
const finRes = await authFetch('/api/premium/financial-profile');
if (!finRes.ok) throw new Error(`Financial profile error (${finRes.status})`);
const finJson = await finRes.json();
/* scenarios -------------------------------------------------------- */
const scRes = await authFetch('/api/premium/career-profile/all');
if (!scRes.ok) throw new Error(`Scenario error (${scRes.status})`);
const scJson = await scRes.json();
setFinancialProfile(finJson);
setScenarios(scJson.careerProfiles || []);
} catch (e) {
console.error('RetirementPlanner → loadAll', e);
setError(e.message || 'Failed to load');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadAll(); }, [loadAll]);
/* ------------------ scenario CRUD helpers ---------------------- */
async function handleAddScenario () {
try {
const body = {
career_name : `New Scenario ${new Date().toLocaleDateString()}`,
status : 'planned',
start_date : new Date().toISOString().slice(0, 10),
college_enrollment_status : 'not_enrolled',
currently_working : 'no'
};
const r = await authFetch('/api/premium/career-profile', {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(body)
});
if (!r.ok) throw new Error(r.status);
await loadAll();
} catch (e) {
alert(`Add scenario failed (${e.message})`);
}
}
async function handleRemoveScenario (id) {
if (!window.confirm('Delete this scenario?')) return;
const r = await authFetch(`/api/premium/career-profile/${id}`, { method: 'DELETE' });
if (!r.ok) return alert(`Delete error (${r.status})`);
await loadAll();
}
async function handleCloneScenario (src) {
if (!src?.id) return;
try {
const r = await authFetch('/api/premium/career-profile/clone', {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({ sourceId: src.id, overrides: {} })
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
await loadAll(); // refresh scenarios list in state
} catch (e) {
alert(`Clone scenario failed (${e.message})`);
}
}
/* ------------------ chat patch helper -------------------------- */
const applyPatch = (id, patch) => {
setScenarios(prev => {
const base = prev.find(s => s.id === id);
const next = prev.map(s => (s.id === id ? { ...s, ...patch } : s));
setDiff({ base, patch });
return next;
});
};
/* --------------------------- guards ---------------------------- */
if (loading) return <p className="p-6">Loading scenarios</p>;
if (error) return <p className="p-6 text-red-600">{error}</p>;
/* ----------------------- render body --------------------------- */
const visibleTwo = scenarios.slice(0, 2);
const baselineId = visibleTwo[0]?.id;
const baselineYears = simYearsMap[baselineId] ?? null; // renamed
return (
<div className="flex flex-col md:flex-row h-full relative">
{/* ================= MAIN COLUMN =========================== */}
<main className="flex-1 p-4 overflow-y-auto">
{/* desktop add */}
{!isMobile && (
<Button onClick={handleAddScenario} className="mb-4">
+ Add Scenario
</Button>
)}
<div className="mx-auto max-w-screen-lg grid gap-6 md:grid-cols-2">
{visibleTwo.map(sc => (
<ScenarioContainer
key={sc.id}
scenario={sc}
financialProfile={financialProfile}
baselineYears={baselineYears}
onClone={handleCloneScenario}
onRemove={handleRemoveScenario}
onAskAI={() => { /* ← only fires from the new button */
setSelectedScenario(sc);
openRetire({
scenario: sc,
scenarios, // ← pass this
financialProfile,
onScenarioPatch: applyPatch
});
}}
onSimDone={(id, yrs) => {
setSimYearsMap(prev => ({ ...prev, [id]: yrs }));
}}
/>
))}
</div>
</main>
{/* ================= MOBILE FABS ========================== */}
{isMobile && (
<button
onClick={handleAddScenario}
className="fixed bottom-4 right-4 rounded-full bg-green-600 p-4 text-white text-xl shadow-md z-40"
aria-label="Add scenario"
>
</button>
)}
{/* ================= DIFF DRAWER ========================== */}
{diff && (
<ScenarioDiffDrawer
base={diff.base}
patch={diff.patch}
onClose={() => setDiff(null)}
/>
)}
</div>
);
}