dev1/src/components/RetirementPlanner.js
2025-06-26 15:43:49 +00:00

209 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 } 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';
/* ------------------------------------------------------------------
* 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();
/* ----------------------- 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) {
/* bring over the original long clone implementation here or import
from a helper if you already abstracted it. Leaving a stub so
the UI compiles. */
alert('Clone scenario not wired yet');
}
/* ------------------ 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}
onSelect={() => { setSelectedScenario(sc); if (isMobile) setChatOpen(true); }}
onSimDone={(id, yrs) => {
setSimYearsMap(prev => ({ ...prev, [id]: yrs }));
}}
/>
))}
</div>
</main>
{/* ================= CHAT RAIL ============================ */}
<aside
className={cn(
'fixed md:static top-0 right-0 h-full bg-white border-l shadow-lg',
'transition-transform duration-300',
'w-11/12 max-w-xs md:w-[340px] z-30',
chatOpen ? 'translate-x-0' : 'translate-x-full md:translate-x-0'
)}
>
{selectedScenario ? (
<RetirementChatBar
scenario={selectedScenario}
financialProfile={financialProfile}
onScenarioPatch={applyPatch}
/>
) : (
<div className="h-full flex items-center justify-center text-gray-400 text-sm p-4">
Select a scenario to chat
</div>
)}
</aside>
{/* ================= MOBILE FABS ========================== */}
{isMobile && (
<>
{/* chat toggle */}
<button
onClick={() => setChatOpen(o => !o)}
className="fixed bottom-20 right-4 rounded-full bg-blue-600 p-3 text-white shadow-md z-40"
aria-label="Toggle chat"
>
💬
</button>
{/* add scenario */}
<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>
);
}