diff --git a/.env b/.env index c4057d2..76fb34f 100644 --- a/.env +++ b/.env @@ -2,4 +2,4 @@ CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http:// SERVER1_PORT=5000 SERVER2_PORT=5001 SERVER3_PORT=5002 -IMG_TAG=8449dc2-202508050250 \ No newline at end of file +IMG_TAG=6a57e00-202508051419 \ No newline at end of file diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index 23c1561..cadbdfa 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -9,6 +9,7 @@ import InterestMeaningModal from './InterestMeaningModal.js'; import CareerSearch from './CareerSearch.js'; import { Button } from './ui/button.js'; import axios from 'axios'; +import isAllOther from '../utils/isAllOther.js'; const STATES = [ { name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' }, @@ -338,181 +339,103 @@ function CareerExplorer() { } }, [location.state, userProfile, fetchSuggestions, navigate]); - // ------------------------------------------------------ - // handleCareerClick (detail fetch for CIP, Salary, etc.) - // ------------------------------------------------------ - const handleCareerClick = useCallback( + /* ------------------------------------------------------ + handleCareerClick – fetches all details for one career + ---------------------------------------------------- */ +const handleCareerClick = useCallback( async (career) => { - console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2)); - const socCode = career.code; + if (!socCode) return; + setSelectedCareer(career); - setError(null); - setCareerDetails(null); - setSalaryData([]); - setEconomicProjections({}); + setCareerDetails(null); // reset any previous modal setLoading(true); - if (!socCode) { - console.error('SOC Code is missing'); - setError('SOC Code is missing'); - setLoading(false); - return; - } - try { - // 1) CIP fetch - const cipResponse = await fetch(`/api/cip/${socCode}`); - if (!cipResponse.ok) { - setError( - `We're sorry, but specific details for "${career.title}" are not available at this time.` - ); + /* ---------- 1. CIP lookup ---------- */ + let cipCode = null; + const cipRes = await fetch(`/api/cip/${socCode}`); + if (cipRes.ok) { + cipCode = (await cipRes.json()).cipCode ?? null; + } + + /* ---------- 2. Job description & tasks ---------- */ + let description = ''; + let tasks = []; + const jobRes = await fetch(`/api/onet/career-description/${socCode}`); + if (jobRes.ok) { + const jd = await jobRes.json(); + description = jd.description ?? ''; + tasks = jd.tasks ?? []; + } + + /* ---------- 3. Salary data ---------- */ + const salaryRes = await axios.get('/api/salary', { + params: { socCode: socCode.split('.')[0], area: areaTitle }, + }).catch(() => ({ data: {} })); + + const s = salaryRes.data; + const salaryDataPoints = s && Object.keys(s).length > 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: '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: '90th Percentile', regionalSalary: +s.regional?.regional_PCT90 || 0, nationalSalary: +s.national?.national_PCT90 || 0 }, + ] : []; + + /* ---------- 4. Economic projections ---------- */ + const fullStateName = getFullStateName(userState); + const projRes = await axios.get( + `/api/projections/${socCode.split('.')[0]}`, + { params: { state: fullStateName } } + ).catch(() => ({ data: {} })); + + /* ---------- 5. Decide if we actually have data ---------- */ + const haveSalary = salaryDataPoints.length > 0; + const haveProj = !!(projRes.data.state || projRes.data.national); + const haveJobInfo = description || tasks.length > 0; + + if (!haveSalary && !haveProj && !haveJobInfo) { setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`, }); - setLoading(false); - return; - } - const { cipCode } = await cipResponse.json(); - const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); - - // 2) Job details (description + tasks) - const jobDetailsResponse = await fetch( - `/api/onet/career-description/${socCode}` - ); - if (!jobDetailsResponse.ok) { - setCareerDetails({ - error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`, - }); - setLoading(false); - return; - } - const { description, tasks } = await jobDetailsResponse.json(); - - // 3) Salary data - let salaryResponse; - try { - salaryResponse = await axios.get('/api/salary', { - params: { socCode: socCode.split('.')[0], area: areaTitle }, - }); - } catch (error) { - salaryResponse = { data: {} }; + return; // stops here – nothing useful to show } - const sData = salaryResponse.data || {}; - const salaryDataPoints = - sData && Object.keys(sData).length > 0 - ? [ - { - percentile: '10th Percentile', - regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0, - nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0, - }, - { - percentile: '25th Percentile', - regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0, - nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0, - }, - { - percentile: 'Median', - regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0, - nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0, - }, - { - percentile: '75th Percentile', - regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0, - nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0, - }, - { - percentile: '90th Percentile', - regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0, - nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0, - }, - ] - : []; - - // 4) Economic Projections - const fullStateName = getFullStateName(userState); // your helper - let economicResponse = { data: {} }; - try { - economicResponse = await axios.get( - `/api/projections/${socCode.split('.')[0]}`, - { - params: { state: fullStateName }, - } - ); - } catch (error) { - economicResponse = { data: {} }; - } - - // ---------------------------------------------------- - // 5) AI RISK ANALYSIS LOGIC - // Attempt to retrieve from server2 first; - // if not found => call server3 => store in server2. - // ---------------------------------------------------- + /* ---------- 6. AI‑risk (only if job details exist) ---------- */ let aiRisk = null; - const strippedSocCode = socCode.split('.')[0]; - - - try { - // Check local DB first (SQLite -> server2) - const localRiskRes = await axios.get(`/api/ai-risk/${socCode}`); - aiRisk = localRiskRes.data; - } catch (err) { - // If 404, we call server3's ChatGPT route at the SAME base url - if (err.response && err.response.status === 404) { - try { + if (haveJobInfo) { + try { + aiRisk = (await axios.get(`/api/ai-risk/${socCode}`)).data; + } catch (err) { + if (err.response?.status === 404) { const aiRes = await axios.post('/api/public/ai-risk-analysis', { socCode, - careerName: career.title, - jobDescription: description, + careerName : career.title, + jobDescription : description, tasks, }); - - const { riskLevel, reasoning } = aiRes.data; - - // store it back in server2 to avoid repeated GPT calls - await axios.post('/api/ai-risk', { - socCode, - careerName: aiRes.data.careerName, - jobDescription: aiRes.data.jobDescription, - tasks: aiRes.data.tasks, - riskLevel: aiRes.data.riskLevel, - reasoning: aiRes.data.reasoning, - }); - - // build final object - aiRisk = { - socCode, - careerName: career.title, - jobDescription: description, - tasks, - riskLevel, - reasoning, - }; - } catch (err2) { - console.error('Error calling server3 or storing AI risk:', err2); - // fallback + aiRisk = aiRes.data; + // store for next time (best‑effort) + axios.post('/api/ai-risk', aiRisk).catch(() => {}); } - } else { - console.error('Error fetching AI risk from server2:', err); } } - // 6) Build final details object + /* ---------- 7. Build the final object & show modal ---------- */ const updatedCareerDetails = { ...career, + cipCode, // may be null – EducationalPrograms handles it jobDescription: description, tasks, salaryData: salaryDataPoints, - economicProjections: economicResponse.data || {}, - aiRisk, // <--- Now we have it attached + economicProjections: projRes.data ?? {}, + aiRisk, }; setCareerDetails(updatedCareerDetails); - - } catch (error) { - console.error('Error processing career click:', error.message); + } catch (e) { + console.error('[handleCareerClick] fatal:', e); setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`, }); @@ -520,9 +443,10 @@ function CareerExplorer() { setLoading(false); } }, - [userState, areaTitle] + [areaTitle, userState] ); + // ------------------------------------------------------ // handleCareerFromSearch // ------------------------------------------------------ @@ -1071,7 +995,7 @@ const handleSelectForEducation = (career) => { {/* Legend container with less internal gap, plus a left margin */}
⚠️ - = Limited Data for this career path + = May have limited data for this career path
diff --git a/src/components/CareerModal.js b/src/components/CareerModal.js index 344249d..97f3acf 100644 --- a/src/components/CareerModal.js +++ b/src/components/CareerModal.js @@ -11,23 +11,36 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { const aiRisk = careerDetails?.aiRisk || null; - console.log('CareerModal props:', { career, careerDetails, aiRisk }); - - // Handle your normal careerDetails loading logic - if (careerDetails?.error) { + if (!careerDetails) { return (
-

{careerDetails.error}

- +

Loading career details…

); } - if (!careerDetails?.salaryData) { + // Handle your normal careerDetails loading logic + if (careerDetails?.error) { + return ( +
+
+

+ {careerDetails.error} +

+ +
+
+ ); +} + + if (!careerDetails?.salaryData === undefined) { return (
@@ -65,7 +78,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { You've selected an "umbrella" field that covers a wide range of careers—many people begin a career journey with a broad interest area and we don't want to discourage anyone from taking this approach. It's just difficult to display detailed career data - and day‑to‑day tasks for this “all‑other” occupatio.. Use it as a starting point, + and day‑to‑day tasks for this “all‑other” occupation. Use it as a starting point, keep exploring specializations, and we can show you richer insights as soon as you are able to narrow it down to a more specific role. If you know this is the field for you, go ahead to add it to your comparison list or move straight into Preparing & Upskilling for Your Career! @@ -80,20 +93,22 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { {careerDetails.title} - {/* AI RISK SECTION */} - {loadingRisk ? ( -

Loading AI risk...

- ) : aiRisk?.riskLevel && aiRisk?.reasoning ? ( -
- AI Risk Level: {aiRisk.riskLevel} -
- {aiRisk.reasoning} -
- ) : ( -

- No AI risk data available -

- )} + {/* AI RISK SECTION */} + {loadingRisk && ( +

Loading AI risk…

+ )} + + {!loadingRisk && aiRisk && aiRisk.riskLevel && aiRisk.reasoning && ( +
+ AI Risk Level: {aiRisk.riskLevel} +
+ {aiRisk.reasoning} +
+ )} + + {!loadingRisk && !aiRisk && ( +

No AI risk data available

+ )}
{/* Buttons */} @@ -126,136 +141,159 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
{/* Job Description */} -
-

Job Description:

-

{careerDetails.jobDescription}

-
+ {careerDetails.jobDescription && ( +
+

Job Description:

+

{careerDetails.jobDescription}

+
+ )} {/* Tasks */} -
-

Tasks:

- -
- - {/* Salary & Projections side-by-side */} -
- {/* Salary Data */} -
-

Salary Data:

- - - - - - - - - - {careerDetails.salaryData.map((s, i) => ( - - - - - - ))} - -
PercentileRegional SalaryNational Salary
{s.percentile} - ${s.regionalSalary.toLocaleString()} - - ${s.nationalSalary.toLocaleString()} -
+ {careerDetails.tasks?.length > 0 && ( +
+

Tasks:

+
    + {careerDetails.tasks.map((task, i) => ( +
  • {task}
  • + ))} +
+ )} - {/* Economic Projections */} -
-

Economic Projections

- - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - - - {careerDetails.economicProjections.state && ( - - )} - {careerDetails.economicProjections.national && ( - - )} - - -
- {careerDetails.economicProjections.state.area} - National
Current Jobs - {careerDetails.economicProjections.state.base.toLocaleString()} - - {careerDetails.economicProjections.national.base.toLocaleString()} -
Jobs in 10 yrs - {careerDetails.economicProjections.state.projection.toLocaleString()} - - {careerDetails.economicProjections.national.projection.toLocaleString()} -
Growth % - {careerDetails.economicProjections.state.percentChange}% - - {careerDetails.economicProjections.national.percentChange}% -
Annual Openings - {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} - - {careerDetails.economicProjections.national.annualOpenings.toLocaleString()} -
- {/* Conditional disclaimer when AI risk is Moderate or High */} - {(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && ( -

- Note: These 10-year projections may change if AI-driven - tools significantly affect {careerDetails.title} tasks. - With a {aiRisk.riskLevel.toLowerCase()} AI risk, - it’s possible that some tasks or responsibilities could be automated - over time. -

+{(careerDetails.salaryData?.length > 0 || + (careerDetails.economicProjections && + (careerDetails.economicProjections.state || + careerDetails.economicProjections.national))) && ( + +
+ + + + {/* ── Salary table ───────────────────────── */} + {careerDetails.salaryData?.length > 0 && ( +
+

Salary Data

+ + + + + + + + + + {careerDetails.salaryData.map((row, i) => ( + + + + + + ))} + +
PercentileRegional SalaryNational Salary
{row.percentile} + ${row.regionalSalary.toLocaleString()} + + ${row.nationalSalary.toLocaleString()} +
+
+ )} + + {/* ── Economic projections ───────────────── */} + {(careerDetails.economicProjections?.state || + careerDetails.economicProjections?.national) && ( +
+

Economic Projections

+ + + + + {careerDetails.economicProjections.state && ( + )} - + {careerDetails.economicProjections.national && ( + + )} + + + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + + + {careerDetails.economicProjections.state && ( + + )} + {careerDetails.economicProjections.national && ( + + )} + + +
+ {careerDetails.economicProjections.state.area} + National
+ Current Jobs + + {careerDetails.economicProjections.state.base.toLocaleString()} + + {careerDetails.economicProjections.national.base.toLocaleString()} +
+ Jobs in 10 yrs + + {careerDetails.economicProjections.state.projection.toLocaleString()} + + {careerDetails.economicProjections.national.projection.toLocaleString()} +
Growth % + {careerDetails.economicProjections.state.percentChange}% + + {careerDetails.economicProjections.national.percentChange}% +
+ Annual Openings + + {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} + + {careerDetails.economicProjections.national.annualOpenings.toLocaleString()} +
+ {/* Conditional disclaimer when AI risk is Moderate or High */} + {aiRisk?.riskLevel && + (aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && ( +

+ Note: These 10‑year projections may change if AI‑driven tools + significantly affect {careerDetails.title} tasks. With a  + {aiRisk.riskLevel.toLowerCase()} AI risk, it’s possible + some responsibilities could be automated over time. +

+ )} +
+ )}
+ )}
);