Finacial disclaimer and SOC fix between Explorer and Education pages
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
b0cbb65cc2
commit
bea86712cb
2
.env
2
.env
@ -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
|
@ -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 didn’t 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
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
|
@ -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 you’ve 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 you’ve 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:
|
Loan Paid Off:
|
||||||
|
29
src/components/FinancialDisclaimer.js
Normal file
29
src/components/FinancialDisclaimer.js
Normal 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>single‑filer 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, capital‑gains rules, and self‑employment 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> W‑2 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 user‑defined milestones.</li>
|
||||||
|
<li><strong>Contributions & withdrawals:</strong> Retirement/emergency contributions treated as post‑tax outflows. Retirement withdrawals treated as taxable ordinary income; no early‑withdrawal 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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user