Finacial disclaimer and SOC fix between Explorer and Education pages
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Josh 2025-08-10 15:37:36 +00:00
parent b0cbb65cc2
commit bea86712cb
4 changed files with 80 additions and 22 deletions

2
.env
View File

@ -2,7 +2,7 @@ CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http://
SERVER1_PORT=5000 SERVER1_PORT=5000
SERVER2_PORT=5001 SERVER2_PORT=5001
SERVER3_PORT=5002 SERVER3_PORT=5002
IMG_TAG=4cfdf84-202508101351 IMG_TAG=b0cbb65-202508101532
ENV_NAME=dev ENV_NAME=dev
PROJECT=aptivaai-dev PROJECT=aptivaai-dev

View File

@ -384,11 +384,11 @@ const handleCareerClick = useCallback(
const s = salaryRes.data; const s = salaryRes.data;
const salaryData = s && Object.keys(s).length const salaryData = s && Object.keys(s).length
? [ ? [
{ percentile: '10th Percentile', regionalSalary: +s.regional?.regional_PCT10 || 0, nationalSalary: +s.national?.national_PCT10 || 0 }, { percentile: '10th Percentile', regionalSalary: +s.regional?.regional_PCT10 || 0, nationalSalary: +s.national?.national_PCT10 || 0 },
{ percentile: '25th Percentile', regionalSalary: +s.regional?.regional_PCT25 || 0, nationalSalary: +s.national?.national_PCT25 || 0 }, { percentile: '25th Percentile', regionalSalary: +s.regional?.regional_PCT25 || 0, nationalSalary: +s.national?.national_PCT25 || 0 },
{ percentile: 'Median', regionalSalary: +s.regional?.regional_MEDIAN || 0, nationalSalary: +s.national?.national_MEDIAN || 0 }, { percentile: 'Median', regionalSalary: +s.regional?.regional_MEDIAN || 0, nationalSalary: +s.national?.national_MEDIAN || 0 },
{ percentile: '75th Percentile', regionalSalary: +s.regional?.regional_PCT75 || 0, nationalSalary: +s.national?.national_PCT75 || 0 }, { percentile: '75th Percentile', regionalSalary: +s.regional?.regional_PCT75 || 0, nationalSalary: +s.national?.national_PCT75 || 0 },
{ percentile: '90th Percentile', regionalSalary: +s.regional?.regional_PCT90 || 0, nationalSalary: +s.national?.national_PCT90 || 0 }, { percentile: '90th Percentile', regionalSalary: +s.regional?.regional_PCT90 || 0, nationalSalary: +s.national?.national_PCT90 || 0 },
] ]
: []; : [];
@ -618,7 +618,6 @@ useEffect(() => {
: :
3; 3;
const defaultMeaningValue = 3; const defaultMeaningValue = 3;
// 4) open the InterestMeaningModal instead of using prompt() // 4) open the InterestMeaningModal instead of using prompt()
@ -684,44 +683,72 @@ useEffect(() => {
}); });
}; };
const stripSoc = (s = '') => s.split('.')[0];
// ------------------------------------------------------ // ------------------------------------------------------
// "Select for Education" => navigate with CIP codes // "Select for Education" => navigate with CIP codes
// ------------------------------------------------------ // ------------------------------------------------------
// CareerExplorer.js // CareerExplorer.js
const handleSelectForEducation = (career) => { // CareerExplorer.js
const handleSelectForEducation = async (career) => {
if (!career) return; if (!career) return;
// ─── 1. Ask first ─────────────────────────────────────────────
const ok = window.confirm( const ok = window.confirm(
`Are you sure you want to move on to Educational Programs for “${career.title}”?` `Are you sure you want to move on to Educational Programs for “${career.title}”?`
); );
if (!ok) return; if (!ok) return;
// ─── 2. Make sure we have a full SOC code ─────────────────────
const fullSoc = career.soc_code || career.code || ''; const fullSoc = career.soc_code || career.code || '';
if (!fullSoc) { const baseSoc = stripSoc(fullSoc);
if (!baseSoc) {
alert('Sorry this career is missing a valid SOC code.'); alert('Sorry this career is missing a valid SOC code.');
return; return;
} }
// ─── 3. Find & clean CIP codes (may be empty) ───────────────── // 1) try local JSON by base SOC (tolerates .00 vs none)
const match = masterCareerRatings.find(r => r.soc_code === fullSoc); const match = masterCareerRatings.find(r => stripSoc(r.soc_code) === baseSoc);
const rawCips = match?.cip_codes ?? []; // original array let rawCips = Array.isArray(match?.cip_codes) ? match.cip_codes : [];
const cleanedCips = cleanCipCodes(rawCips); // “0402”, “1409”, … let cleanedCips = cleanCipCodes(rawCips);
// ─── 4. Persist ONE tidy object for later pages ─────────────── // 2) fallback: ask server2 to map SOC→CIP if local didnt have any
if (!cleanedCips.length) {
try {
const candidates = [
fullSoc, // as-is
baseSoc, // stripped
fullSoc.includes('.') ? null : `${fullSoc}.00` // add .00 if missing
].filter(Boolean);
let fromApi = null;
for (const soc of candidates) {
const res = await fetch(`/api/cip/${soc}`);
if (res.ok) {
const { cipCode } = await res.json();
if (cipCode) { fromApi = cipCode; break; }
}
}
if (fromApi) {
rawCips = [fromApi];
cleanedCips = cleanCipCodes(rawCips);
}
} catch {
// best-effort fallback; continue
}
}
// Persist for the next page (keep raw list if we have it)
const careerForStorage = { const careerForStorage = {
...career, ...career,
soc_code : fullSoc, soc_code : fullSoc,
cip_code : rawCips // keep the raw list; page cleans them again if needed cip_code : rawCips
}; };
localStorage.setItem('selectedCareer', JSON.stringify(careerForStorage)); localStorage.setItem('selectedCareer', JSON.stringify(careerForStorage));
// ─── 5. Off we go ───────────────────────────────────────────── // Navigate with the robust base SOC + cleaned CIP prefixes
navigate('/educational-programs', { navigate('/educational-programs', {
state: { state: {
socCode : fullSoc, socCode : baseSoc,
cipCodes : cleanedCips, // can be [], page handles it cipCodes : cleanedCips, // can be [] if absolutely nothing found
careerTitle : career.title, careerTitle : career.title,
userZip : userZipcode, userZip : userZipcode,
userState : userState userState : userState
@ -729,7 +756,6 @@ const handleSelectForEducation = (career) => {
}); });
}; };
// ------------------------------------------------------ // ------------------------------------------------------
// Filter logic for jobZone, Fit // Filter logic for jobZone, Fit
// ------------------------------------------------------ // ------------------------------------------------------

View File

@ -29,6 +29,7 @@ import parseFloatOrZero from '../utils/ParseFloatorZero.js';
import { getFullStateName } from '../utils/stateUtils.js'; import { getFullStateName } from '../utils/stateUtils.js';
import CareerCoach from "./CareerCoach.js"; import CareerCoach from "./CareerCoach.js";
import ChatCtx from '../contexts/ChatCtx.js'; import ChatCtx from '../contexts/ChatCtx.js';
import FinancialDisclaimer from './FinancialDisclaimer.js';
import { Button } from './ui/button.js'; import { Button } from './ui/button.js';
import { Pencil } from 'lucide-react'; import { Pencil } from 'lucide-react';
@ -78,6 +79,7 @@ async function createCareerProfileFromSearch(selCareer) {
throw new Error('createCareerProfileFromSearch: selCareer.title is required'); throw new Error('createCareerProfileFromSearch: selCareer.title is required');
} }
/* ----------------------------------------------------------- /* -----------------------------------------------------------
* 1) Do we already have that title? * 1) Do we already have that title?
* --------------------------------------------------------- */ * --------------------------------------------------------- */
@ -1497,7 +1499,8 @@ const handleMilestonesCreated = useCallback(
<InfoTooltip message="This projection uses the salary, expense, loan and contribution inputs youve set below. Click “Edit Simulation Inputs” to adjust them, or edit your financial information in Profile -> Financial Profile." /> <InfoTooltip message="This projection uses the salary, expense, loan and contribution inputs youve set below. Click “Edit Simulation Inputs” to adjust them, or edit your financial information in Profile -> Financial Profile." />
</h3> </h3>
{/* Legal/assumptions disclaimer (static, matches simulator) */}
<FinancialDisclaimer />
{projectionData.length ? ( {projectionData.length ? (
<div> <div>
{/* Chart now full width */} {/* Chart now full width */}
@ -1526,7 +1529,7 @@ const handleMilestonesCreated = useCallback(
> >
Reset Zoom Reset Zoom
</Button> </Button>
{loanPayoffMonth && hasStudentLoan && ( {loanPayoffMonth && hasStudentLoan && (
<p className="font-semibold text-sm mt-2"> <p className="font-semibold text-sm mt-2">
Loan Paid Off:&nbsp; Loan Paid Off:&nbsp;

View File

@ -0,0 +1,29 @@
export default function FinancialDisclaimer() {
return (
<div className="mt-3 text-xs text-gray-600 border-t pt-3 leading-snug">
<p>
<strong>Important:</strong> These projections are educational estimates and <strong>not</strong> financial, tax,
legal, or investment advice. Federal tax is approximated using the <strong>singlefiler standard deduction
($13,850)</strong> and <strong>2023 federal brackets</strong>; state tax is a <strong>flat percentage by
state</strong> from an internal table. Estimates <strong>exclude</strong> Social Security/Medicare (FICA),
local taxes, itemized deductions, credits, AMT, capitalgains rules, and selfemployment taxes. Actual outcomes
vary.
</p>
<details className="mt-2">
<summary className="cursor-pointer select-none">Assumptions & limitations</summary>
<ul className="list-disc pl-5 mt-2 space-y-1">
<li><strong>Filing model:</strong> W2 wages, single filer, standard deduction only; no dependents, credits, or adjustments.</li>
<li><strong>Federal tax calc:</strong> Monthly estimate = (annualized income $13,850) through 2023 bands (10/12/22/24/32/35/37%), divided by 12.</li>
<li><strong>State tax:</strong> Single flat rate per state from a fixed lookup; local/city taxes not modeled.</li>
<li><strong>Payroll taxes:</strong> FICA is <strong>not</strong> included.</li>
<li><strong>Education & loans:</strong> Tuition follows the chosen academic calendar; if deferring, tuition is added to loan principal and interest accrues monthly at the specified APR. No IDR/PSLF/forbearance nuances.</li>
<li><strong>Income changes:</strong> Expected salary replaces current salary starting at/after the provided graduation date; other changes come from userdefined milestones.</li>
<li><strong>Contributions & withdrawals:</strong> Retirement/emergency contributions treated as posttax outflows. Retirement withdrawals treated as taxable ordinary income; no earlywithdrawal penalties/RMD rules modeled.</li>
<li><strong>Returns:</strong> Retirement balances use the selected strategy (none / flat annual rate / random monthly range). No general inflation is applied unless you model it via milestones.</li>
<li><strong>Data sources:</strong> Public datasets (e.g., BLS/NCES) may change without notice. COL adjustments not applied unless explicitly labeled.</li>
</ul>
</details>
</div>
);
}