Cross-app navigation UI fixes
This commit is contained in:
parent
2728378041
commit
0bad162d52
@ -20,7 +20,7 @@ import annotationPlugin from 'chartjs-plugin-annotation';
|
|||||||
import MilestonePanel from './MilestonePanel.js';
|
import MilestonePanel from './MilestonePanel.js';
|
||||||
import MilestoneEditModal from './MilestoneEditModal.js';
|
import MilestoneEditModal from './MilestoneEditModal.js';
|
||||||
import buildChartMarkers from '../utils/buildChartMarkers.js';
|
import buildChartMarkers from '../utils/buildChartMarkers.js';
|
||||||
import getMissingFields from '../utils/MissingFields.js';
|
import getMissingFields from '../utils/getMissingFields.js';
|
||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||||
@ -37,6 +37,9 @@ import './MilestoneTimeline.css';
|
|||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
// Register ChartJS Plugins
|
// Register ChartJS Plugins
|
||||||
// --------------
|
// --------------
|
||||||
@ -58,6 +61,7 @@ ChartJS.register(
|
|||||||
* Helpers for “remember last career” logic
|
* Helpers for “remember last career” logic
|
||||||
* ----------------------------------------------------------- */
|
* ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
// (A) getAllCareerProfiles – one small wrapper around the endpoint
|
// (A) getAllCareerProfiles – one small wrapper around the endpoint
|
||||||
async function getAllCareerProfiles() {
|
async function getAllCareerProfiles() {
|
||||||
const res = await authFetch('/api/premium/career-profile/all');
|
const res = await authFetch('/api/premium/career-profile/all');
|
||||||
@ -116,6 +120,17 @@ async function createCareerProfileFromSearch(selCareer) {
|
|||||||
// --------------
|
// --------------
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// --------------
|
// --------------
|
||||||
|
|
||||||
|
function shouldSkipModalOnce(profileId) {
|
||||||
|
const key = `skipMissingModalFor`;
|
||||||
|
const stored = sessionStorage.getItem(key);
|
||||||
|
if (stored && stored === String(profileId)) {
|
||||||
|
sessionStorage.removeItem(key); // one-time use
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function stripSocCode(fullSoc) {
|
function stripSocCode(fullSoc) {
|
||||||
if (!fullSoc) return '';
|
if (!fullSoc) return '';
|
||||||
return fullSoc.split('.')[0];
|
return fullSoc.split('.')[0];
|
||||||
@ -338,7 +353,6 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
|||||||
const [projectionData, setProjectionData] = useState([]);
|
const [projectionData, setProjectionData] = useState([]);
|
||||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||||
const [milestoneForModal, setMilestoneForModal] = useState(null);
|
const [milestoneForModal, setMilestoneForModal] = useState(null);
|
||||||
const [hasPrompted, setHasPrompted] = useState(false);
|
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
|
const [simulationYearsInput, setSimulationYearsInput] = useState('20');
|
||||||
@ -361,6 +375,24 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
|||||||
loanPayoffMonth: initLoanMonth = null
|
loanPayoffMonth: initLoanMonth = null
|
||||||
} = location.state || {};
|
} = location.state || {};
|
||||||
|
|
||||||
|
const reloadScenarioAndCollege = useCallback(async () => {
|
||||||
|
if (!careerProfileId) return;
|
||||||
|
const s = await authFetch(
|
||||||
|
`${apiURL}/premium/career-profile/${careerProfileId}`
|
||||||
|
);
|
||||||
|
if (s.ok) {
|
||||||
|
const row = await s.json();
|
||||||
|
if (!row.college_enrollment_status)
|
||||||
|
row.college_enrollment_status = "not_enrolled";
|
||||||
|
setScenarioRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = await authFetch(
|
||||||
|
`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`
|
||||||
|
);
|
||||||
|
if (c.ok) setCollegeProfile(await c.json());
|
||||||
|
}, [careerProfileId, apiURL]);
|
||||||
|
|
||||||
const milestoneGroups = useMemo(() => {
|
const milestoneGroups = useMemo(() => {
|
||||||
if (!scenarioMilestones.length) return [];
|
if (!scenarioMilestones.length) return [];
|
||||||
|
|
||||||
@ -435,44 +467,57 @@ const xAndYScales = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────
|
||||||
|
* ONE-TIME “MISSING FIELDS” GUARD
|
||||||
|
* modalGuard.current = { checked: bool, skip: bool }
|
||||||
|
* • checked → we already ran the test for this profile
|
||||||
|
* • skip → suppress first check (set by onboarding OR
|
||||||
|
* by sessionStorage flag for this profile)
|
||||||
|
* ────────────────────────────────────────────────────────────── */
|
||||||
|
const modalGuard = useRef({ checked: false, skip: false });
|
||||||
|
|
||||||
// 1) Fetch user + financial
|
/* -------------------------------------------------------------
|
||||||
useEffect(() => {
|
* 0) If we landed here via onboarding, skip the very first check
|
||||||
async function fetchUser() {
|
* ------------------------------------------------------------*/
|
||||||
try {
|
useEffect(() => {
|
||||||
const r = await authFetch('/api/user-profile');
|
if (location.state?.fromOnboarding) {
|
||||||
if (r.ok) setUserProfile(await r.json());
|
modalGuard.current.skip = true; // suppress once
|
||||||
} catch (err) {
|
window.history.replaceState({}, '', location.pathname);
|
||||||
console.error('Error user-profile =>', err);
|
}
|
||||||
}
|
}, [location.state, location.pathname]);
|
||||||
}
|
|
||||||
async function fetchFin() {
|
|
||||||
try {
|
|
||||||
const r = await authFetch(`${apiURL}/premium/financial-profile`);
|
|
||||||
if (r.ok) setFinancialProfile(await r.json());
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error financial =>', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchUser();
|
|
||||||
fetchFin();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const userSalary = parseFloatOrZero(financialProfile?.current_salary, 0);
|
/* -------------------------------------------------------------
|
||||||
const userArea = userProfile?.area || 'U.S.';
|
* 1) Fetch user + financial on first mount
|
||||||
const userState = getFullStateName(userProfile?.state || '') || 'United States';
|
* ------------------------------------------------------------*/
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const up = await authFetch('/api/user-profile');
|
||||||
|
if (up.ok) setUserProfile(await up.json());
|
||||||
|
|
||||||
useEffect(() => {
|
const fp = await authFetch(`${apiURL}/premium/financial-profile`);
|
||||||
if (careerId) {
|
if (fp.ok) setFinancialProfile(await fp.json());
|
||||||
setCareerProfileId(careerId);
|
})();
|
||||||
localStorage.setItem('lastSelectedCareerProfileId', careerId);
|
}, [apiURL]);
|
||||||
} else {
|
|
||||||
// first visit with no id → try LS fallback
|
|
||||||
const stored = localStorage.getItem('lastSelectedCareerProfileId');
|
|
||||||
if (stored) setCareerProfileId(stored);
|
|
||||||
}
|
|
||||||
}, [careerId]);
|
|
||||||
|
|
||||||
|
/* quick derived helpers */
|
||||||
|
const userSalary = parseFloatOrZero(financialProfile?.current_salary);
|
||||||
|
const userArea = userProfile?.area || 'U.S.';
|
||||||
|
const userState = getFullStateName(userProfile?.state || '') || 'United States';
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------
|
||||||
|
* 2) Determine the active careerProfileId once
|
||||||
|
* ------------------------------------------------------------*/
|
||||||
|
useEffect(() => {
|
||||||
|
let id = careerId;
|
||||||
|
if (!id) id = localStorage.getItem('lastSelectedCareerProfileId');
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
setCareerProfileId(id);
|
||||||
|
localStorage.setItem('lastSelectedCareerProfileId', id);
|
||||||
|
// one-shot modal skip from sessionStorage
|
||||||
|
modalGuard.current.skip ||= shouldSkipModalOnce(id);
|
||||||
|
}
|
||||||
|
}, [careerId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer;
|
let timer;
|
||||||
@ -482,40 +527,59 @@ const xAndYScales = {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [buttonDisabled]);
|
}, [buttonDisabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
/* ------------------------------------------------------------------
|
||||||
const storedRecs = localStorage.getItem('aiRecommendations');
|
* 1) Restore AI recommendations (unchanged behaviour)
|
||||||
if (storedRecs) {
|
* -----------------------------------------------------------------*/
|
||||||
try {
|
useEffect(() => {
|
||||||
const arr = JSON.parse(storedRecs);
|
const json = localStorage.getItem('aiRecommendations');
|
||||||
arr.forEach((m) => {
|
if (!json) return;
|
||||||
if (!m.id) {
|
|
||||||
m.id = crypto.randomUUID();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setRecommendations(arr);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error parsing stored AI recs =>', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
// Wait until all three profiles have loaded at least once
|
const arr = JSON.parse(json).map((m) => ({
|
||||||
if (!scenarioRow || !financialProfile || collegeProfile === null) return;
|
...m,
|
||||||
|
id: m.id || crypto.randomUUID()
|
||||||
if (hasPrompted) return; // don’t pop it again
|
}));
|
||||||
|
setRecommendations(arr);
|
||||||
const missing = getMissingFields({
|
} catch (err) {
|
||||||
scenario : scenarioRow,
|
console.error('Error parsing stored AI recs', err);
|
||||||
financial: financialProfile,
|
|
||||||
college : collegeProfile
|
|
||||||
});
|
|
||||||
|
|
||||||
if (missing.length > 0) {
|
|
||||||
setShowEditModal(true); // open modal
|
|
||||||
setHasPrompted(true); // flag so it’s one-time
|
|
||||||
}
|
}
|
||||||
}, [scenarioRow, financialProfile, collegeProfile, hasPrompted]);
|
}, []);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* 2) Whenever the careerProfileId changes, clear the modal check flag
|
||||||
|
* -----------------------------------------------------------------*/
|
||||||
|
useEffect(() => {
|
||||||
|
modalGuard.current.checked = false;
|
||||||
|
}, [careerProfileId]);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* 3) Missing-fields modal – single authoritative effect
|
||||||
|
* -----------------------------------------------------------------*/
|
||||||
|
const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const guard = modalGuard.current;
|
||||||
|
if (!dataReady || guard.checked) return;
|
||||||
|
|
||||||
|
/* honour skip flag (one-time after onboarding / sessionStorage) */
|
||||||
|
if (guard.skip) {
|
||||||
|
guard.skip = false; // consume it
|
||||||
|
guard.checked = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = (scenarioRow.college_enrollment_status || '').toLowerCase();
|
||||||
|
const requireCollege = ['currently_enrolled', 'prospective_student', 'deferred'].includes(status);
|
||||||
|
|
||||||
|
const missing = getMissingFields(
|
||||||
|
{ scenario: scenarioRow, financial: financialProfile, college: collegeProfile },
|
||||||
|
{ requireCollegeData: requireCollege }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missing.length) setShowEditModal(true);
|
||||||
|
guard.checked = true; // ensure we don’t rerun
|
||||||
|
}, [dataReady, scenarioRow, financialProfile, collegeProfile]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recommendations.length > 0) {
|
if (recommendations.length > 0) {
|
||||||
@ -612,31 +676,29 @@ useEffect(() => {
|
|||||||
}, [location.key, careerId]);
|
}, [location.key, careerId]);
|
||||||
|
|
||||||
|
|
||||||
// 4) scenarioRow + college
|
/* ------------------------------------------------------------------
|
||||||
useEffect(() => {
|
* 4) refresh scenario + college whenever the active profile-id changes
|
||||||
/** ---------------------------------------------------------------
|
* -----------------------------------------------------------------*/
|
||||||
* bail out IMMEDIATELY until we have a *real* id
|
useEffect(() => {
|
||||||
* (the rest of the body never even runs)
|
if (!careerProfileId) return; // nothing to fetch
|
||||||
* ------------------------------------------------------------- */
|
|
||||||
if (!careerProfileId) return; // ← nothing gets fetched
|
|
||||||
|
|
||||||
setScenarioRow(null); // clear stale data
|
// clear any stale UI traces while the new fetch runs
|
||||||
|
setScenarioRow(null);
|
||||||
setCollegeProfile(null);
|
setCollegeProfile(null);
|
||||||
setScenarioMilestones([]);
|
setScenarioMilestones([]);
|
||||||
|
|
||||||
localStorage.setItem('lastSelectedCareerProfileId', careerProfileId);
|
// remember for other tabs / future visits
|
||||||
|
localStorage.setItem('lastSelectedCareerProfileId', careerProfileId);
|
||||||
|
|
||||||
async function fetchScenario() {
|
// fetch both rows in parallel (defined via useCallback)
|
||||||
const s = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`);
|
reloadScenarioAndCollege();
|
||||||
if (s.ok) setScenarioRow(await s.json());
|
}, [careerProfileId, reloadScenarioAndCollege]);
|
||||||
}
|
|
||||||
async function fetchCollege() {
|
const refetchScenario = useCallback(async () => {
|
||||||
const c = await authFetch(`${apiURL}/premium/college-profile?careerProfileId=${careerProfileId}`);
|
if (!careerProfileId) return;
|
||||||
if (c.ok) setCollegeProfile(await c.json());
|
const r = await authFetch(`${apiURL}/premium/career-profile/${careerProfileId}`);
|
||||||
}
|
if (r.ok) setScenarioRow(await r.json());
|
||||||
fetchScenario();
|
}, [careerProfileId, apiURL]);
|
||||||
fetchCollege();
|
|
||||||
}, [careerProfileId]);
|
|
||||||
|
|
||||||
// 5) from scenarioRow => find the full SOC => strip
|
// 5) from scenarioRow => find the full SOC => strip
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -749,56 +811,59 @@ try {
|
|||||||
return aiRisk;
|
return aiRisk;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Salary
|
/* 6) Salary ------------------------------------------------------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!strippedSocCode) {
|
// show blank state instantly whenever the SOC or area changes
|
||||||
setSalaryData(null);
|
setSalaryData(null);
|
||||||
return;
|
if (!strippedSocCode) return;
|
||||||
}
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const qs = new URLSearchParams({ socCode: strippedSocCode, area: userArea }).toString();
|
|
||||||
const url = `${apiURL}/salary?${qs}`;
|
|
||||||
const r = await fetch(url);
|
|
||||||
if (!r.ok) {
|
|
||||||
console.error('[Salary fetch non-200 =>]', r.status);
|
|
||||||
setSalaryData(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dd = await r.json();
|
|
||||||
setSalaryData(dd);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Salary fetch error]', err);
|
|
||||||
setSalaryData(null);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [strippedSocCode, userArea]);
|
|
||||||
|
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams({ socCode: strippedSocCode, area: userArea });
|
||||||
|
const res = await fetch(`${apiURL}/salary?${qs}`, { signal: ctrl.signal });
|
||||||
|
|
||||||
// 7) Econ
|
if (res.ok) {
|
||||||
useEffect(() => {
|
setSalaryData(await res.json());
|
||||||
if (!strippedSocCode || !userState) {
|
} else {
|
||||||
setEconomicProjections(null);
|
console.error('[Salary fetch]', res.status);
|
||||||
return;
|
|
||||||
}
|
|
||||||
(async () => {
|
|
||||||
const qs = new URLSearchParams({ state: userState }).toString();
|
|
||||||
const econUrl = `${apiURL}/projections/${strippedSocCode}?${qs}`;
|
|
||||||
try {
|
|
||||||
const r = await authFetch(econUrl);
|
|
||||||
if (!r.ok) {
|
|
||||||
console.error('[Econ fetch non-200 =>]', r.status);
|
|
||||||
setEconomicProjections(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const econData = await r.json();
|
|
||||||
setEconomicProjections(econData);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Econ fetch error]', err);
|
|
||||||
setEconomicProjections(null);
|
|
||||||
}
|
}
|
||||||
})();
|
} catch (e) {
|
||||||
}, [strippedSocCode, userState]);
|
if (e.name !== 'AbortError') console.error('[Salary fetch error]', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// cancel if strippedSocCode / userArea changes before the fetch ends
|
||||||
|
return () => ctrl.abort();
|
||||||
|
}, [strippedSocCode, userArea, apiURL]);
|
||||||
|
|
||||||
|
/* 7) Economic Projections ---------------------------------------- */
|
||||||
|
useEffect(() => {
|
||||||
|
setEconomicProjections(null);
|
||||||
|
if (!strippedSocCode || !userState) return;
|
||||||
|
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams({ state: userState });
|
||||||
|
const res = await authFetch(
|
||||||
|
`${apiURL}/projections/${strippedSocCode}?${qs}`,
|
||||||
|
{ signal: ctrl.signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setEconomicProjections(await res.json());
|
||||||
|
} else {
|
||||||
|
console.error('[Econ fetch]', res.status);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name !== 'AbortError') console.error('[Econ fetch error]', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => ctrl.abort();
|
||||||
|
}, [strippedSocCode, userState, apiURL]);
|
||||||
|
|
||||||
|
|
||||||
// 8) Build financial projection
|
// 8) Build financial projection
|
||||||
async function buildProjection() {
|
async function buildProjection() {
|
||||||
@ -1331,20 +1396,23 @@ const fetchMilestones = useCallback(async () => {
|
|||||||
Edit Simulation Inputs
|
Edit Simulation Inputs
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ScenarioEditModal
|
|
||||||
show={showEditModal}
|
<ScenarioEditModal
|
||||||
onClose={() => {
|
key={careerProfileId}
|
||||||
setShowEditModal(false);
|
show={showEditModal}
|
||||||
window.location.reload();
|
onClose={(didSave) => {
|
||||||
}}
|
setShowEditModal(false);
|
||||||
scenario={scenarioRow}
|
if (didSave) reloadScenarioAndCollege(); // 👈 refresh after save
|
||||||
financialProfile={financialProfile}
|
}}
|
||||||
setFinancialProfile={setFinancialProfile}
|
scenario={scenarioRow}
|
||||||
collegeProfile={collegeProfile}
|
financialProfile={financialProfile}
|
||||||
setCollegeProfile={setCollegeProfile}
|
setFinancialProfile={setFinancialProfile}
|
||||||
apiURL={apiURL}
|
collegeProfile={collegeProfile}
|
||||||
authFetch={authFetch}
|
setCollegeProfile={setCollegeProfile}
|
||||||
/>
|
apiURL={apiURL}
|
||||||
|
authFetch={authFetch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* (E1) Interest Strategy */}
|
{/* (E1) Interest Strategy */}
|
||||||
<label className="ml-4 font-medium">Interest Rate:</label>
|
<label className="ml-4 font-medium">Interest Rate:</label>
|
||||||
|
@ -183,8 +183,18 @@ const OnboardingContainer = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate somewhere
|
const picked = { code: careerData.soc_code, title: careerData.career_name }
|
||||||
navigate('/career-roadmap');
|
|
||||||
|
// 🚀 right before you navigate away from the review page
|
||||||
|
sessionStorage.setItem('skipMissingModalFor', String(finalCareerProfileId));
|
||||||
|
localStorage.setItem('selectedCareer', JSON.stringify(picked));
|
||||||
|
localStorage.removeItem('lastSelectedCareerProfileId');
|
||||||
|
|
||||||
|
navigate(`/career-roadmap/${finalCareerProfileId}`, {
|
||||||
|
state: { fromOnboarding: true,
|
||||||
|
selectedCareer : picked
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in final submit =>', err);
|
console.error('Error in final submit =>', err);
|
||||||
|
@ -120,7 +120,8 @@ export default function ScenarioEditModal({
|
|||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
/*********************************************************
|
/*********************************************************
|
||||||
* 7) If scenario + collegeProfile => fill form
|
* 7) Whenever the **modal is shown** *or* **scenario.id changes**
|
||||||
|
+ * → hydrate the form + careerSearch box.
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!show || !scenario) return;
|
if (!show || !scenario) return;
|
||||||
@ -157,7 +158,7 @@ export default function ScenarioEditModal({
|
|||||||
|
|
||||||
is_in_state: !!c.is_in_state,
|
is_in_state: !!c.is_in_state,
|
||||||
is_in_district: !!c.is_in_district,
|
is_in_district: !!c.is_in_district,
|
||||||
is_online: !!c.is_in_online,
|
is_online: !!c.is_online,
|
||||||
college_enrollment_status_db: c.college_enrollment_status || 'not_enrolled',
|
college_enrollment_status_db: c.college_enrollment_status || 'not_enrolled',
|
||||||
|
|
||||||
annual_financial_aid: c.annual_financial_aid ?? '',
|
annual_financial_aid: c.annual_financial_aid ?? '',
|
||||||
@ -197,7 +198,7 @@ export default function ScenarioEditModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCareerSearchInput(s.career_name || '');
|
setCareerSearchInput(s.career_name || '');
|
||||||
}, [show, scenario, collegeProfile]);
|
}, [show, scenario?.id, collegeProfile]);
|
||||||
|
|
||||||
/*********************************************************
|
/*********************************************************
|
||||||
* 8) Auto-calc placeholders (stubbed out)
|
* 8) Auto-calc placeholders (stubbed out)
|
||||||
@ -230,18 +231,36 @@ export default function ScenarioEditModal({
|
|||||||
/*********************************************************
|
/*********************************************************
|
||||||
* 9) Career auto-suggest
|
* 9) Career auto-suggest
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
useEffect(() => {
|
/*********************************************************
|
||||||
if (!show) return;
|
* 9) Career auto-suggest
|
||||||
if (!careerSearchInput.trim()) {
|
*********************************************************/
|
||||||
setCareerMatches([]);
|
useEffect(() => {
|
||||||
return;
|
if (!show) return;
|
||||||
}
|
|
||||||
const lower = careerSearchInput.toLowerCase();
|
// 1️⃣ trim once, reuse everywhere
|
||||||
const partials = allCareers
|
const typed = careerSearchInput.trim();
|
||||||
.filter((title) => title.toLowerCase().includes(lower))
|
|
||||||
.slice(0, 15);
|
// Nothing typed → clear list
|
||||||
setCareerMatches(partials);
|
if (!typed) {
|
||||||
}, [show, careerSearchInput, allCareers]);
|
setCareerMatches([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2️⃣ Exact match (case-insensitive) → suppress dropdown */
|
||||||
|
if (allCareers.some(t => t.toLowerCase() === typed.toLowerCase())) {
|
||||||
|
setCareerMatches([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ Otherwise show up to 15 partial matches
|
||||||
|
const lower = typed.toLowerCase();
|
||||||
|
const partials = allCareers
|
||||||
|
.filter(title => title.toLowerCase().includes(lower))
|
||||||
|
.slice(0, 15);
|
||||||
|
|
||||||
|
setCareerMatches(partials);
|
||||||
|
}, [show, careerSearchInput, allCareers]);
|
||||||
|
|
||||||
|
|
||||||
/*********************************************************
|
/*********************************************************
|
||||||
* 9.5) Program Type from CIP
|
* 9.5) Program Type from CIP
|
||||||
@ -436,367 +455,75 @@ export default function ScenarioEditModal({
|
|||||||
/*********************************************************
|
/*********************************************************
|
||||||
* 12) handleSave => upsert scenario & college => re-fetch => simulate
|
* 12) handleSave => upsert scenario & college => re-fetch => simulate
|
||||||
*********************************************************/
|
*********************************************************/
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
try {
|
try {
|
||||||
function parseNumberIfGiven(val) {
|
/* ─── helpers ───────────────────────────────────────────── */
|
||||||
if (val == null) return undefined;
|
const n = v => (v === "" || v == null ? undefined : Number(v));
|
||||||
const valStr = String(val).trim();
|
const s = v => {
|
||||||
if (valStr === '') return undefined;
|
if (v == null) return undefined;
|
||||||
const num = Number(valStr);
|
const t = String(v).trim();
|
||||||
return isNaN(num) ? undefined : num;
|
return t === "" ? undefined : t;
|
||||||
}
|
};
|
||||||
|
|
||||||
function parseStringIfGiven(val) {
|
/* ─── 0) did the user change the title? ─────────────────── */
|
||||||
if (val == null) return undefined;
|
const originalName = scenario?.career_name?.trim() || "";
|
||||||
const trimmed = String(val).trim();
|
const editedName = (formData.career_name || "").trim();
|
||||||
return trimmed === '' ? undefined : trimmed;
|
const titleChanged = editedName && editedName !== originalName;
|
||||||
}
|
|
||||||
|
|
||||||
const chosenTuitionVal =
|
/* ─── 1) build scenario payload ─────────────────────────── */
|
||||||
manualTuition.trim() !== '' ? Number(manualTuition) : undefined;
|
const scenarioPayload = {
|
||||||
const chosenProgLengthVal =
|
scenario_title : s(formData.scenario_title),
|
||||||
manualProgLength.trim() !== '' ? Number(manualProgLength) : undefined;
|
career_name : editedName, // always include
|
||||||
|
college_enrollment_status : formData.college_enrollment_status,
|
||||||
|
currently_working : formData.currently_working || "no",
|
||||||
|
status : s(formData.status),
|
||||||
|
start_date : s(formData.start_date),
|
||||||
|
projected_end_date : s(formData.projected_end_date),
|
||||||
|
|
||||||
// Sync scenario's enrollment status with college row
|
planned_monthly_expenses : n(formData.planned_monthly_expenses),
|
||||||
let finalCollegeStatus = formData.college_enrollment_status_db;
|
planned_monthly_debt_payments : n(formData.planned_monthly_debt_payments),
|
||||||
if (
|
planned_monthly_retirement_contribution: n(formData.planned_monthly_retirement_contribution),
|
||||||
formData.college_enrollment_status === 'currently_enrolled' ||
|
planned_monthly_emergency_contribution : n(formData.planned_monthly_emergency_contribution),
|
||||||
formData.college_enrollment_status === 'prospective_student'
|
planned_surplus_emergency_pct : n(formData.planned_surplus_emergency_pct),
|
||||||
) {
|
planned_surplus_retirement_pct : n(formData.planned_surplus_retirement_pct),
|
||||||
finalCollegeStatus = formData.college_enrollment_status;
|
planned_additional_income : n(formData.planned_additional_income)
|
||||||
} else {
|
};
|
||||||
finalCollegeStatus = 'not_enrolled';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build scenario payload
|
/* If the title did NOT change, keep the id so the UPSERT
|
||||||
const scenarioPayload = {};
|
updates the existing row. Otherwise omit id → new row */
|
||||||
|
if (!titleChanged && scenario?.id) {
|
||||||
// If scenario already has an id, include it:
|
scenarioPayload.id = scenario.id;
|
||||||
if (scenario?.id) {
|
|
||||||
scenarioPayload.id = scenario.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
scenarioPayload.college_enrollment_status = finalCollegeStatus;
|
|
||||||
scenarioPayload.currently_working = formData.currently_working || 'no';
|
|
||||||
|
|
||||||
const scenarioTitle = parseStringIfGiven(formData.scenario_title);
|
|
||||||
if (scenarioTitle !== undefined) scenarioPayload.scenario_title = scenarioTitle;
|
|
||||||
|
|
||||||
const careerName = parseStringIfGiven(formData.career_name);
|
|
||||||
if (careerName !== undefined) scenarioPayload.career_name = careerName;
|
|
||||||
|
|
||||||
const scenarioStatus = parseStringIfGiven(formData.status);
|
|
||||||
if (scenarioStatus !== undefined) scenarioPayload.status = scenarioStatus;
|
|
||||||
|
|
||||||
if (formData.start_date && formData.start_date.trim() !== '') {
|
|
||||||
scenarioPayload.start_date = formData.start_date.trim();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
formData.projected_end_date &&
|
|
||||||
formData.projected_end_date.trim() !== ''
|
|
||||||
) {
|
|
||||||
scenarioPayload.projected_end_date = formData.projected_end_date.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const pme = parseNumberIfGiven(formData.planned_monthly_expenses);
|
|
||||||
if (pme !== undefined) scenarioPayload.planned_monthly_expenses = pme;
|
|
||||||
|
|
||||||
const pmdp = parseNumberIfGiven(formData.planned_monthly_debt_payments);
|
|
||||||
if (pmdp !== undefined)
|
|
||||||
scenarioPayload.planned_monthly_debt_payments = pmdp;
|
|
||||||
|
|
||||||
const pmrc = parseNumberIfGiven(
|
|
||||||
formData.planned_monthly_retirement_contribution
|
|
||||||
);
|
|
||||||
if (pmrc !== undefined) {
|
|
||||||
scenarioPayload.planned_monthly_retirement_contribution = pmrc;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pmec = parseNumberIfGiven(
|
|
||||||
formData.planned_monthly_emergency_contribution
|
|
||||||
);
|
|
||||||
if (pmec !== undefined) {
|
|
||||||
scenarioPayload.planned_monthly_emergency_contribution = pmec;
|
|
||||||
}
|
|
||||||
|
|
||||||
const psep = parseNumberIfGiven(formData.planned_surplus_emergency_pct);
|
|
||||||
if (psep !== undefined)
|
|
||||||
scenarioPayload.planned_surplus_emergency_pct = psep;
|
|
||||||
|
|
||||||
const psrp = parseNumberIfGiven(formData.planned_surplus_retirement_pct);
|
|
||||||
if (psrp !== undefined)
|
|
||||||
scenarioPayload.planned_surplus_retirement_pct = psrp;
|
|
||||||
|
|
||||||
const pai = parseNumberIfGiven(formData.planned_additional_income);
|
|
||||||
if (pai !== undefined) scenarioPayload.planned_additional_income = pai;
|
|
||||||
|
|
||||||
// 1) Upsert scenario
|
|
||||||
const scenRes = await authFetch('/api/premium/career-profile', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(scenarioPayload)
|
|
||||||
});
|
|
||||||
if (!scenRes.ok) {
|
|
||||||
const msg = await scenRes.text();
|
|
||||||
throw new Error(`Scenario upsert failed: ${msg}`);
|
|
||||||
}
|
|
||||||
const scenData = await scenRes.json();
|
|
||||||
const updatedScenarioId = scenData.career_profile_id;
|
|
||||||
|
|
||||||
// 2) Build college payload
|
|
||||||
const collegePayload = {
|
|
||||||
id: formData.college_profile_id || null,
|
|
||||||
career_profile_id: updatedScenarioId,
|
|
||||||
college_enrollment_status: finalCollegeStatus,
|
|
||||||
is_in_state: formData.is_in_state ? 1 : 0,
|
|
||||||
is_in_district: formData.is_in_district ? 1 : 0,
|
|
||||||
is_in_online: formData.is_online ? 1 : 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const selSchool = parseStringIfGiven(formData.selected_school);
|
|
||||||
if (selSchool !== undefined) collegePayload.selected_school = selSchool;
|
|
||||||
|
|
||||||
const selProg = parseStringIfGiven(formData.selected_program);
|
|
||||||
if (selProg !== undefined) collegePayload.selected_program = selProg;
|
|
||||||
|
|
||||||
const progType = parseStringIfGiven(formData.program_type);
|
|
||||||
if (progType !== undefined) collegePayload.program_type = progType;
|
|
||||||
|
|
||||||
const acCal = parseStringIfGiven(formData.academic_calendar);
|
|
||||||
if (acCal !== undefined) collegePayload.academic_calendar = acCal;
|
|
||||||
|
|
||||||
if (formData.expected_graduation && formData.expected_graduation.trim() !== '') {
|
|
||||||
collegePayload.expected_graduation = formData.expected_graduation
|
|
||||||
.trim()
|
|
||||||
.substring(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.enrollment_date && formData.enrollment_date.trim() !== '') {
|
|
||||||
collegePayload.enrollment_date = formData.enrollment_date
|
|
||||||
.trim()
|
|
||||||
.substring(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
const afa = parseNumberIfGiven(formData.annual_financial_aid);
|
|
||||||
if (afa !== undefined) collegePayload.annual_financial_aid = afa;
|
|
||||||
|
|
||||||
const ecd = parseNumberIfGiven(formData.existing_college_debt);
|
|
||||||
if (ecd !== undefined) collegePayload.existing_college_debt = ecd;
|
|
||||||
|
|
||||||
const tp = parseNumberIfGiven(formData.tuition_paid);
|
|
||||||
if (tp !== undefined) collegePayload.tuition_paid = tp;
|
|
||||||
|
|
||||||
if (chosenTuitionVal !== undefined && !isNaN(chosenTuitionVal)) {
|
|
||||||
collegePayload.tuition = chosenTuitionVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chosenProgLengthVal !== undefined && !isNaN(chosenProgLengthVal)) {
|
|
||||||
collegePayload.program_length = chosenProgLengthVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ltg = parseNumberIfGiven(formData.loan_term);
|
|
||||||
if (ltg !== undefined) collegePayload.loan_term = ltg;
|
|
||||||
|
|
||||||
const ir = parseNumberIfGiven(formData.interest_rate);
|
|
||||||
if (ir !== undefined) collegePayload.interest_rate = ir;
|
|
||||||
|
|
||||||
const ep = parseNumberIfGiven(formData.extra_payment);
|
|
||||||
if (ep !== undefined) collegePayload.extra_payment = ep;
|
|
||||||
|
|
||||||
const chpy = parseNumberIfGiven(formData.credit_hours_per_year);
|
|
||||||
if (chpy !== undefined) collegePayload.credit_hours_per_year = chpy;
|
|
||||||
|
|
||||||
const hc = parseNumberIfGiven(formData.hours_completed);
|
|
||||||
if (hc !== undefined) collegePayload.hours_completed = hc;
|
|
||||||
|
|
||||||
const chr = parseNumberIfGiven(formData.credit_hours_required);
|
|
||||||
if (chr !== undefined) collegePayload.credit_hours_required = chr;
|
|
||||||
|
|
||||||
const esal = parseNumberIfGiven(formData.expected_salary);
|
|
||||||
if (esal !== undefined) collegePayload.expected_salary = esal;
|
|
||||||
|
|
||||||
if (formData.loan_deferral_until_graduation) {
|
|
||||||
collegePayload.loan_deferral_until_graduation = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Upsert or skip
|
|
||||||
if (
|
|
||||||
finalCollegeStatus === 'currently_enrolled' ||
|
|
||||||
finalCollegeStatus === 'prospective_student'
|
|
||||||
) {
|
|
||||||
const colRes = await authFetch('/api/premium/college-profile', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(collegePayload)
|
|
||||||
});
|
|
||||||
if (!colRes.ok) {
|
|
||||||
const msg2 = await colRes.text();
|
|
||||||
throw new Error(`College upsert failed: ${msg2}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
'Skipping college-profile upsert in EditScenarioModal because user not enrolled'
|
|
||||||
);
|
|
||||||
// Optionally: if you want to delete an existing college profile:
|
|
||||||
// await authFetch(`/api/premium/college-profile/delete/${updatedScenarioId}`, { method: 'DELETE' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Re-fetch scenario, college, financial => aggregator => simulate
|
|
||||||
const [scenResp2, colResp2, finResp] = await Promise.all([
|
|
||||||
authFetch(`/api/premium/career-profile/${updatedScenarioId}`),
|
|
||||||
authFetch(`/api/premium/college-profile?careerProfileId=${updatedScenarioId}`),
|
|
||||||
authFetch(`/api/premium/financial-profile`)
|
|
||||||
]);
|
|
||||||
if (!scenResp2.ok || !colResp2.ok || !finResp.ok) {
|
|
||||||
console.error('One re-fetch failed after upsert.', {
|
|
||||||
scenarioStatus: scenResp2.status,
|
|
||||||
collegeStatus: colResp2.status,
|
|
||||||
financialStatus: finResp.status
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [finalScenarioRow, finalCollegeRaw, finalFinancial] = await Promise.all([
|
|
||||||
scenResp2.json(),
|
|
||||||
colResp2.json(),
|
|
||||||
finResp.json()
|
|
||||||
]);
|
|
||||||
|
|
||||||
let finalCollegeRow = Array.isArray(finalCollegeRaw)
|
|
||||||
? finalCollegeRaw[0] || {}
|
|
||||||
: finalCollegeRaw;
|
|
||||||
|
|
||||||
// -------------------------------------------
|
|
||||||
// 5) Before simulate: parse numeric fields
|
|
||||||
// to avoid .toFixed errors
|
|
||||||
// -------------------------------------------
|
|
||||||
// scenario planned_ fields
|
|
||||||
if (finalScenarioRow.planned_monthly_expenses != null) {
|
|
||||||
finalScenarioRow.planned_monthly_expenses = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_monthly_expenses,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (finalScenarioRow.planned_monthly_debt_payments != null) {
|
|
||||||
finalScenarioRow.planned_monthly_debt_payments = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_monthly_debt_payments,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (finalScenarioRow.planned_monthly_retirement_contribution != null) {
|
|
||||||
finalScenarioRow.planned_monthly_retirement_contribution = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_monthly_retirement_contribution,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (finalScenarioRow.planned_monthly_emergency_contribution != null) {
|
|
||||||
finalScenarioRow.planned_monthly_emergency_contribution = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_monthly_emergency_contribution,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (finalScenarioRow.planned_surplus_emergency_pct != null) {
|
|
||||||
finalScenarioRow.planned_surplus_emergency_pct = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_surplus_emergency_pct,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (finalScenarioRow.planned_surplus_retirement_pct != null) {
|
|
||||||
finalScenarioRow.planned_surplus_retirement_pct = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_surplus_retirement_pct,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (finalScenarioRow.planned_additional_income != null) {
|
|
||||||
finalScenarioRow.planned_additional_income = parseFloatOrZero(
|
|
||||||
finalScenarioRow.planned_additional_income,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// college numeric fields (force all to numbers or 0)
|
|
||||||
const numericFields = [
|
|
||||||
'existing_college_debt',
|
|
||||||
'extra_payment',
|
|
||||||
'tuition',
|
|
||||||
'tuition_paid',
|
|
||||||
'interest_rate',
|
|
||||||
'loan_term',
|
|
||||||
'credit_hours_per_year',
|
|
||||||
'hours_completed',
|
|
||||||
'program_length',
|
|
||||||
'expected_salary',
|
|
||||||
'annual_financial_aid',
|
|
||||||
'credit_hours_required'
|
|
||||||
];
|
|
||||||
for (const field of numericFields) {
|
|
||||||
if (finalCollegeRow[field] != null) {
|
|
||||||
finalCollegeRow[field] = parseFloatOrZero(finalCollegeRow[field], 0);
|
|
||||||
} else {
|
|
||||||
finalCollegeRow[field] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also ensure all scenario/financial fields used in buildMergedUserProfile are numbers
|
|
||||||
const scenarioNumericFields = [
|
|
||||||
'planned_monthly_expenses',
|
|
||||||
'planned_monthly_debt_payments',
|
|
||||||
'planned_monthly_retirement_contribution',
|
|
||||||
'planned_monthly_emergency_contribution',
|
|
||||||
'planned_surplus_emergency_pct',
|
|
||||||
'planned_surplus_retirement_pct',
|
|
||||||
'planned_additional_income'
|
|
||||||
];
|
|
||||||
for (const field of scenarioNumericFields) {
|
|
||||||
if (finalScenarioRow[field] != null) {
|
|
||||||
finalScenarioRow[field] = parseFloatOrZero(finalScenarioRow[field], 0);
|
|
||||||
} else {
|
|
||||||
finalScenarioRow[field] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (finalFinancial) {
|
|
||||||
const financialNumericFields = [
|
|
||||||
'current_salary',
|
|
||||||
'monthly_expenses',
|
|
||||||
'monthly_debt_payments',
|
|
||||||
'additional_income',
|
|
||||||
'emergency_fund',
|
|
||||||
'retirement_savings',
|
|
||||||
'retirement_contribution',
|
|
||||||
'emergency_contribution',
|
|
||||||
'extra_cash_emergency_pct',
|
|
||||||
'extra_cash_retirement_pct'
|
|
||||||
];
|
|
||||||
for (const field of financialNumericFields) {
|
|
||||||
if (finalFinancial[field] != null) {
|
|
||||||
finalFinancial[field] = parseFloatOrZero(finalFinancial[field], 0);
|
|
||||||
} else {
|
|
||||||
finalFinancial[field] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6) Now simulate
|
|
||||||
const userProfile = buildMergedUserProfile(
|
|
||||||
finalScenarioRow,
|
|
||||||
finalCollegeRow,
|
|
||||||
finalFinancial
|
|
||||||
);
|
|
||||||
console.log('UserProfile from Modal:', userProfile);
|
|
||||||
|
|
||||||
const results = simulateFinancialProjection(userProfile);
|
|
||||||
setProjectionData(results.projectionData);
|
|
||||||
setLoanPayoffMonth(results.loanPaidOffMonth);
|
|
||||||
|
|
||||||
// 7) Close or reload
|
|
||||||
onClose();
|
|
||||||
window.location.reload();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error saving scenario + college:', err);
|
|
||||||
alert(err.message || 'Failed to save scenario data.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── 2) POST (always) ─────────────────────────────────── */
|
||||||
|
const scenRes = await authFetch("/api/premium/career-profile", {
|
||||||
|
method : "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body : JSON.stringify(scenarioPayload)
|
||||||
|
});
|
||||||
|
if (!scenRes.ok) throw new Error(await scenRes.text());
|
||||||
|
const { career_profile_id } = await scenRes.json();
|
||||||
|
|
||||||
|
/* ─── 3) (optional) upsert college profile – keep yours… ─ */
|
||||||
|
|
||||||
|
/* ─── 4) update localStorage so CareerRoadmap re-hydrates ─ */
|
||||||
|
localStorage.setItem(
|
||||||
|
"selectedCareer",
|
||||||
|
JSON.stringify({ title: editedName })
|
||||||
|
);
|
||||||
|
localStorage.setItem(
|
||||||
|
"lastSelectedCareerProfileId",
|
||||||
|
String(career_profile_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ─── 5) close modal + tell parent to refetch ───────────── */
|
||||||
|
onClose(true); // CareerRoadmap’s onClose(true) triggers reload
|
||||||
|
window.location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("handleSave", err);
|
||||||
|
alert(err.message || "Failed to save scenario");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*********************************************************
|
/*********************************************************
|
||||||
* 13) Render
|
* 13) Render
|
||||||
@ -1076,7 +803,7 @@ export default function ScenarioEditModal({
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="is_online"
|
name="is_online"
|
||||||
checked={!!formData.is_in_online}
|
checked={!!formData.is_online}
|
||||||
onChange={handleFormChange}
|
onChange={handleFormChange}
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
/>
|
/>
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
// utils/getMissingFields.js
|
|
||||||
export default function getMissingFields({ scenario, financial, college }) {
|
|
||||||
const missing = [];
|
|
||||||
|
|
||||||
if (!scenario?.career_name) missing.push('Target career');
|
|
||||||
if (!scenario?.start_date) missing.push('Career start date');
|
|
||||||
|
|
||||||
if (!financial?.current_salary) missing.push('Current salary');
|
|
||||||
if (!financial?.monthly_expenses) missing.push('Monthly expenses');
|
|
||||||
|
|
||||||
if (college?.college_enrollment_status === 'currently_enrolled') {
|
|
||||||
if (!college.expected_graduation) missing.push('Expected graduation');
|
|
||||||
if (!college.existing_college_debt)
|
|
||||||
missing.push('Student-loan balance');
|
|
||||||
}
|
|
||||||
|
|
||||||
return missing;
|
|
||||||
}
|
|
74
src/utils/getMissingFields.js
Normal file
74
src/utils/getMissingFields.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Identify which *critical* fields are still empty so the UI can
|
||||||
|
* decide whether to pop the Scenario-Edit modal.
|
||||||
|
*
|
||||||
|
* – Scenario overrides (planned_… values) are **optional**
|
||||||
|
* – College data is only required when the user is actually
|
||||||
|
* enrolled / planning to enrol.
|
||||||
|
*/
|
||||||
|
export default function getMissingFields(
|
||||||
|
{ scenario = {}, financial = {}, college = {} },
|
||||||
|
{ requireCollegeData = true } = {}
|
||||||
|
) {
|
||||||
|
const missing = [];
|
||||||
|
|
||||||
|
/* ---------- 1 ▸ Scenario essentials ---------- */
|
||||||
|
const requiredScenario = [
|
||||||
|
'career_name',
|
||||||
|
'scenario_title',
|
||||||
|
'start_date',
|
||||||
|
'status',
|
||||||
|
'currently_working',
|
||||||
|
'college_enrollment_status'
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredScenario.forEach((f) => {
|
||||||
|
if (!hasValue(scenario[f])) missing.push(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- 2 ▸ Financial profile ---------- */
|
||||||
|
const requiredFin = [
|
||||||
|
'current_salary',
|
||||||
|
'monthly_expenses',
|
||||||
|
'monthly_debt_payments',
|
||||||
|
'emergency_fund',
|
||||||
|
'retirement_savings'
|
||||||
|
];
|
||||||
|
|
||||||
|
requiredFin.forEach((f) => {
|
||||||
|
if (!hasValue(financial[f])) missing.push(f);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- 3 ▸ College profile (conditional) ---------- */
|
||||||
|
if (requireCollegeData) {
|
||||||
|
const requiredCol = [
|
||||||
|
'selected_school',
|
||||||
|
'selected_program',
|
||||||
|
'program_type',
|
||||||
|
'academic_calendar',
|
||||||
|
'credit_hours_per_year',
|
||||||
|
'tuition', // can be auto-calc’d, but still required
|
||||||
|
'interest_rate',
|
||||||
|
'loan_term',
|
||||||
|
'existing_college_debt',
|
||||||
|
'expected_graduation'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!Object.keys(college).length) {
|
||||||
|
missing.push('collegeProfile');
|
||||||
|
} else {
|
||||||
|
requiredCol.forEach((f) => {
|
||||||
|
if (!hasValue(college[f])) missing.push(f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==== helpers ========================================================== */
|
||||||
|
function hasValue(v) {
|
||||||
|
if (v === null || v === undefined) return false;
|
||||||
|
if (typeof v === 'string') return v.trim() !== '';
|
||||||
|
return true; // numbers, booleans, etc.
|
||||||
|
}
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user