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 */}
{careerDetails.error}
- +Loading career details…
+ {careerDetails.error} +
+ +Loading AI risk...
- ) : aiRisk?.riskLevel && aiRisk?.reasoning ? ( -- No AI risk data available -
- )} + {/* AI RISK SECTION */} + {loadingRisk && ( +Loading AI risk…
+ )} + + {!loadingRisk && aiRisk && aiRisk.riskLevel && aiRisk.reasoning && ( +No AI risk data available
+ )}{careerDetails.jobDescription}
-{careerDetails.jobDescription}
+Percentile | -Regional Salary | -National Salary | -
---|---|---|
{s.percentile} | -- ${s.regionalSalary.toLocaleString()} - | -- ${s.nationalSalary.toLocaleString()} - | -
- {careerDetails.economicProjections.state && ( - | - {careerDetails.economicProjections.state.area} - | - )} - {careerDetails.economicProjections.national && ( -National | - )} -
---|---|---|
Current Jobs | - {careerDetails.economicProjections.state && ( -- {careerDetails.economicProjections.state.base.toLocaleString()} - | - )} - {careerDetails.economicProjections.national && ( -- {careerDetails.economicProjections.national.base.toLocaleString()} - | - )} -
Jobs in 10 yrs | - {careerDetails.economicProjections.state && ( -- {careerDetails.economicProjections.state.projection.toLocaleString()} - | - )} - {careerDetails.economicProjections.national && ( -- {careerDetails.economicProjections.national.projection.toLocaleString()} - | - )} -
Growth % | - {careerDetails.economicProjections.state && ( -- {careerDetails.economicProjections.state.percentChange}% - | - )} - {careerDetails.economicProjections.national && ( -- {careerDetails.economicProjections.national.percentChange}% - | - )} -
Annual Openings | - {careerDetails.economicProjections.state && ( -- {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} - | - )} - {careerDetails.economicProjections.national && ( -- {careerDetails.economicProjections.national.annualOpenings.toLocaleString()} - | - )} -
- 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))) && ( + +Percentile | +Regional Salary | +National Salary | +
---|---|---|
{row.percentile} | ++ ${row.regionalSalary.toLocaleString()} + | ++ ${row.nationalSalary.toLocaleString()} + | +
+ {careerDetails.economicProjections.state && ( + | + {careerDetails.economicProjections.state.area} + | )} - + {careerDetails.economicProjections.national && ( +National | + )} +
---|---|---|
+ Current Jobs + | + {careerDetails.economicProjections.state && ( ++ {careerDetails.economicProjections.state.base.toLocaleString()} + | + )} + {careerDetails.economicProjections.national && ( ++ {careerDetails.economicProjections.national.base.toLocaleString()} + | + )} +
+ Jobs in 10 yrs + | + {careerDetails.economicProjections.state && ( ++ {careerDetails.economicProjections.state.projection.toLocaleString()} + | + )} + {careerDetails.economicProjections.national && ( ++ {careerDetails.economicProjections.national.projection.toLocaleString()} + | + )} +
Growth % | + {careerDetails.economicProjections.state && ( ++ {careerDetails.economicProjections.state.percentChange}% + | + )} + {careerDetails.economicProjections.national && ( ++ {careerDetails.economicProjections.national.percentChange}% + | + )} +
+ Annual Openings + | + {careerDetails.economicProjections.state && ( ++ {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} + | + )} + {careerDetails.economicProjections.national && ( ++ {careerDetails.economicProjections.national.annualOpenings.toLocaleString()} + | + )} +
+ 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. +
+ )} +