Fixed regional salary and state economic projections
This commit is contained in:
parent
41ee9877d2
commit
d42aaf1d7c
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import CareerSuggestions from './CareerSuggestions.js';
|
import CareerSuggestions from './CareerSuggestions.js';
|
||||||
@ -7,16 +7,84 @@ import CareerModal from './CareerModal.js';
|
|||||||
import CareerSearch from './CareerSearch.js';
|
import CareerSearch from './CareerSearch.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
|
const STATES = [
|
||||||
|
{ name: 'Alabama', code: 'AL' },
|
||||||
|
{ name: 'Alaska', code: 'AK' },
|
||||||
|
{ name: 'Arizona', code: 'AZ' },
|
||||||
|
{ name: 'Arkansas', code: 'AR' },
|
||||||
|
{ name: 'California', code: 'CA' },
|
||||||
|
{ name: 'Colorado', code: 'CO' },
|
||||||
|
{ name: 'Connecticut', code: 'CT' },
|
||||||
|
{ name: 'Delaware', code: 'DE' },
|
||||||
|
{ name: 'District of Columbia', code: 'DC' },
|
||||||
|
{ name: 'Florida', code: 'FL' },
|
||||||
|
{ name: 'Georgia', code: 'GA' },
|
||||||
|
{ name: 'Hawaii', code: 'HI' },
|
||||||
|
{ name: 'Idaho', code: 'ID' },
|
||||||
|
{ name: 'Illinois', code: 'IL' },
|
||||||
|
{ name: 'Indiana', code: 'IN' },
|
||||||
|
{ name: 'Iowa', code: 'IA' },
|
||||||
|
{ name: 'Kansas', code: 'KS' },
|
||||||
|
{ name: 'Kentucky', code: 'KY' },
|
||||||
|
{ name: 'Louisiana', code: 'LA' },
|
||||||
|
{ name: 'Maine', code: 'ME' },
|
||||||
|
{ name: 'Maryland', code: 'MD' },
|
||||||
|
{ name: 'Massachusetts', code: 'MA' },
|
||||||
|
{ name: 'Michigan', code: 'MI' },
|
||||||
|
{ name: 'Minnesota', code: 'MN' },
|
||||||
|
{ name: 'Mississippi', code: 'MS' },
|
||||||
|
{ name: 'Missouri', code: 'MO' },
|
||||||
|
{ name: 'Montana', code: 'MT' },
|
||||||
|
{ name: 'Nebraska', code: 'NE' },
|
||||||
|
{ name: 'Nevada', code: 'NV' },
|
||||||
|
{ name: 'New Hampshire', code: 'NH' },
|
||||||
|
{ name: 'New Jersey', code: 'NJ' },
|
||||||
|
{ name: 'New Mexico', code: 'NM' },
|
||||||
|
{ name: 'New York', code: 'NY' },
|
||||||
|
{ name: 'North Carolina', code: 'NC' },
|
||||||
|
{ name: 'North Dakota', code: 'ND' },
|
||||||
|
{ name: 'Ohio', code: 'OH' },
|
||||||
|
{ name: 'Oklahoma', code: 'OK' },
|
||||||
|
{ name: 'Oregon', code: 'OR' },
|
||||||
|
{ name: 'Pennsylvania', code: 'PA' },
|
||||||
|
{ name: 'Rhode Island', code: 'RI' },
|
||||||
|
{ name: 'South Carolina', code: 'SC' },
|
||||||
|
{ name: 'South Dakota', code: 'SD' },
|
||||||
|
{ name: 'Tennessee', code: 'TN' },
|
||||||
|
{ name: 'Texas', code: 'TX' },
|
||||||
|
{ name: 'Utah', code: 'UT' },
|
||||||
|
{ name: 'Vermont', code: 'VT' },
|
||||||
|
{ name: 'Virginia', code: 'VA' },
|
||||||
|
{ name: 'Washington', code: 'WA' },
|
||||||
|
{ name: 'West Virginia', code: 'WV' },
|
||||||
|
{ name: 'Wisconsin', code: 'WI' },
|
||||||
|
{ name: 'Wyoming', code: 'WY' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2) Helper to convert state code => full name
|
||||||
|
function getFullStateName(code) {
|
||||||
|
const found = STATES.find((s) => s.code === code?.toUpperCase());
|
||||||
|
return found ? found.name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function CareerExplorer({ }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
const [userProfile, setUserProfile] = useState(null);
|
const [userProfile, setUserProfile] = useState(null);
|
||||||
const [masterCareerRatings, setMasterCareerRatings] = useState([]);
|
const [masterCareerRatings, setMasterCareerRatings] = useState([]);
|
||||||
const [careerList, setCareerList] = useState([]);
|
const [careerList, setCareerList] = useState([]);
|
||||||
|
const [careerDetails, setCareerDetails] = useState(null);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [userState, setUserState] = useState(null);
|
||||||
|
const [areaTitle, setAreaTitle] = useState(null);
|
||||||
|
const [userZipcode, setUserZipcode] = useState(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||||
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
||||||
|
const [salaryData, setSalaryData] = useState([]);
|
||||||
|
const [economicProjections, setEconomicProjections] = useState(null);
|
||||||
const [selectedJobZone, setSelectedJobZone] = useState('');
|
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||||
const [selectedFit, setSelectedFit] = useState('');
|
const [selectedFit, setSelectedFit] = useState('');
|
||||||
const [selectedCareer, setSelectedCareer] = useState(null);
|
const [selectedCareer, setSelectedCareer] = useState(null);
|
||||||
@ -37,6 +105,235 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
|
|||||||
Good: 'Good - Less Strong Match',
|
Good: 'Good - Less Strong Match',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const res = await axios.get(`${apiUrl}/user-profile`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const profileData = res.data;
|
||||||
|
console.log('[fetchUserProfile] loaded profileData =>', profileData);
|
||||||
|
|
||||||
|
// 1) Set userProfile and all relevant states using `profileData`:
|
||||||
|
setUserProfile(profileData);
|
||||||
|
setUserState(profileData.state);
|
||||||
|
setAreaTitle(profileData.area);
|
||||||
|
setUserZipcode(profileData.zipcode);
|
||||||
|
|
||||||
|
// 2) Load saved career list if it exists
|
||||||
|
if (profileData.career_list) {
|
||||||
|
setCareerList(JSON.parse(profileData.career_list));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) If user has interest inventory answers, fetch suggestions
|
||||||
|
if (profileData.interest_inventory_answers) {
|
||||||
|
const answers = profileData.interest_inventory_answers;
|
||||||
|
const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, {
|
||||||
|
answers,
|
||||||
|
state: profileData.state,
|
||||||
|
area: profileData.area,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { careers = [] } = careerSuggestionsRes.data || {};
|
||||||
|
setCareerSuggestions(careers.flat());
|
||||||
|
} else {
|
||||||
|
// No inventory => no suggestions (or do something else here)
|
||||||
|
setCareerSuggestions([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Check if all priorities are answered
|
||||||
|
const priorities = profileData.career_priorities
|
||||||
|
? JSON.parse(profileData.career_priorities)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const allAnswered = ['interests','meaning','stability','growth','balance','recognition']
|
||||||
|
.every((key) => priorities[key]);
|
||||||
|
|
||||||
|
if (!allAnswered) {
|
||||||
|
// If user hasn't answered them all, show the priorities modal
|
||||||
|
setShowModal(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a 200 response => fallback
|
||||||
|
setShowModal(true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user profile:', err);
|
||||||
|
setShowModal(true); // fallback if error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
// Load suggestions from Interest Inventory if provided (optional)
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.state?.careerSuggestions) {
|
||||||
|
setCareerSuggestions(location.state.careerSuggestions);
|
||||||
|
}
|
||||||
|
}, [location.state]);
|
||||||
|
|
||||||
|
// Fetch Job Zones if suggestions are provided
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchJobZones = async () => {
|
||||||
|
if (!careerSuggestions.length) return;
|
||||||
|
|
||||||
|
const flatSuggestions = careerSuggestions.flat();
|
||||||
|
const socCodes = flatSuggestions.map((career) => career.code);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
||||||
|
const jobZoneData = response.data;
|
||||||
|
const updatedCareers = flatSuggestions.map((career) => ({
|
||||||
|
...career,
|
||||||
|
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// IMPORTANT: Ensure this actually sets a new array
|
||||||
|
setCareersWithJobZone([...updatedCareers]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching job zone information:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchJobZones();
|
||||||
|
}, [careerSuggestions, apiUrl]);
|
||||||
|
|
||||||
|
const handleCareerClick = useCallback(
|
||||||
|
async (career) => {
|
||||||
|
console.log('[handleCareerClick] career =>', career);
|
||||||
|
const socCode = career.code;
|
||||||
|
setSelectedCareer(career);
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setCareerDetails({});
|
||||||
|
setSalaryData([]);
|
||||||
|
setEconomicProjections({});
|
||||||
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// We can set selectedCareer immediately so that our Modal condition is met.
|
||||||
|
setSelectedCareer(career);
|
||||||
|
|
||||||
|
if (!socCode) {
|
||||||
|
console.error('SOC Code is missing');
|
||||||
|
setError('SOC Code is missing');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// CIP fetch
|
||||||
|
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||||
|
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
||||||
|
const { cipCode } = await cipResponse.json();
|
||||||
|
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||||
|
|
||||||
|
// Job details
|
||||||
|
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||||
|
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
||||||
|
const { description, tasks } = await jobDetailsResponse.json();
|
||||||
|
|
||||||
|
// Salary
|
||||||
|
let salaryResponse;
|
||||||
|
try {
|
||||||
|
salaryResponse = await axios.get(`${apiUrl}/salary`, {
|
||||||
|
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
salaryResponse = { data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build salary array
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Economic
|
||||||
|
const fullStateName = getFullStateName(userState);
|
||||||
|
let economicResponse = { data: {} };
|
||||||
|
try {
|
||||||
|
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
||||||
|
params: { state: fullStateName },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
economicResponse = { data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final details
|
||||||
|
const updatedCareerDetails = {
|
||||||
|
...career,
|
||||||
|
jobDescription: description,
|
||||||
|
tasks,
|
||||||
|
salaryData: salaryDataPoints,
|
||||||
|
economicProjections: economicResponse.data || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now set the fully fetched data
|
||||||
|
setCareerDetails(updatedCareerDetails);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing career click:', error.message);
|
||||||
|
setError('Failed to load data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[userState, apiUrl, areaTitle, userZipcode]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============= Let typed careers open PopoutPanel =============
|
||||||
|
const handleCareerFromSearch = useCallback(
|
||||||
|
(obj) => {
|
||||||
|
const adapted = {
|
||||||
|
code: obj.soc_code,
|
||||||
|
title: obj.title,
|
||||||
|
cipCode: obj.cip_code,
|
||||||
|
};
|
||||||
|
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
|
||||||
|
handleCareerClick(adapted);
|
||||||
|
},
|
||||||
|
[handleCareerClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingCareerForModal) {
|
||||||
|
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
|
||||||
|
handleCareerFromSearch(pendingCareerForModal);
|
||||||
|
setPendingCareerForModal(null);
|
||||||
|
}
|
||||||
|
}, [pendingCareerForModal, handleCareerFromSearch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/careers_with_ratings.json')
|
fetch('/careers_with_ratings.json')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -61,8 +358,7 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
|
|||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
await axios.post(`${apiUrl}/user-profile`, {
|
await axios.post(`${apiUrl}/user-profile`, {
|
||||||
// Provide all required fields from userProfile
|
|
||||||
// If your DB requires them, fill them in here:
|
|
||||||
firstName: userProfile?.firstname,
|
firstName: userProfile?.firstname,
|
||||||
lastName: userProfile?.lastname,
|
lastName: userProfile?.lastname,
|
||||||
email: userProfile?.email,
|
email: userProfile?.email,
|
||||||
@ -150,97 +446,7 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUserProfile = async () => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const res = await axios.get(`${apiUrl}/user-profile`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
const profileData = res.data;
|
|
||||||
setUserProfile(profileData);
|
|
||||||
|
|
||||||
// Explicitly set careerList from saved data
|
|
||||||
if (profileData.career_list) {
|
|
||||||
setCareerList(JSON.parse(profileData.career_list));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check explicitly for Interest Inventory answers
|
|
||||||
if (profileData.interest_inventory_answers) {
|
|
||||||
const answers = profileData.interest_inventory_answers;
|
|
||||||
const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, {
|
|
||||||
answers,
|
|
||||||
state: profileData.state,
|
|
||||||
area: profileData.area,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Destructure `careers` from the server response
|
|
||||||
const { careers = [] } = careerSuggestionsRes.data || {};
|
|
||||||
|
|
||||||
// Flatten in case it's a nested array (or just a no-op if already flat)
|
|
||||||
setCareerSuggestions(careers.flat());
|
|
||||||
} else {
|
|
||||||
// No interest inventory answers: fallback to an empty list
|
|
||||||
setCareerSuggestions([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const priorities = profileData.career_priorities
|
|
||||||
? JSON.parse(profileData.career_priorities)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const allAnswered = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'].every(
|
|
||||||
(key) => priorities[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!allAnswered) {
|
|
||||||
setShowModal(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setShowModal(true);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching user profile:', err);
|
|
||||||
setShowModal(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserProfile();
|
|
||||||
}, [apiUrl]);
|
|
||||||
|
|
||||||
// Load suggestions from Interest Inventory if provided (optional)
|
|
||||||
useEffect(() => {
|
|
||||||
if (location.state?.careerSuggestions) {
|
|
||||||
setCareerSuggestions(location.state.careerSuggestions);
|
|
||||||
}
|
|
||||||
}, [location.state]);
|
|
||||||
|
|
||||||
// Fetch Job Zones if suggestions are provided
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchJobZones = async () => {
|
|
||||||
if (!careerSuggestions.length) return;
|
|
||||||
|
|
||||||
const flatSuggestions = careerSuggestions.flat();
|
|
||||||
const socCodes = flatSuggestions.map((career) => career.code);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
|
|
||||||
const jobZoneData = response.data;
|
|
||||||
const updatedCareers = flatSuggestions.map((career) => ({
|
|
||||||
...career,
|
|
||||||
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// IMPORTANT: Ensure this actually sets a new array
|
|
||||||
setCareersWithJobZone([...updatedCareers]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching job zone information:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchJobZones();
|
|
||||||
}, [careerSuggestions, apiUrl]);
|
|
||||||
|
|
||||||
// Filtering logic (Job Zone and Fit)
|
// Filtering logic (Job Zone and Fit)
|
||||||
const filteredCareers = useMemo(() => {
|
const filteredCareers = useMemo(() => {
|
||||||
@ -432,16 +638,17 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
|
|||||||
areaTitle={areaTitle}
|
areaTitle={areaTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareer && (
|
||||||
<CareerModal
|
<CareerModal
|
||||||
career={selectedCareer}
|
career={selectedCareer}
|
||||||
closeModal={() => setSelectedCareer(null)}
|
careerDetails={careerDetails}
|
||||||
userState={userState}
|
closeModal={() => {
|
||||||
areaTitle={areaTitle}
|
setSelectedCareer(null);
|
||||||
userZipcode={userProfile?.zipcode}
|
setCareerDetails(null);
|
||||||
addCareerToList={addCareerToList} // <-- explicitly added here
|
}}
|
||||||
/>
|
addCareerToList={addCareerToList}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
||||||
Career results and details provided by
|
Career results and details provided by
|
||||||
|
@ -1,129 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { fetchSchools, clientGeocodeZip, haversineDistance} from '../utils/apiUtils.js';
|
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL;
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
const STATES = [
|
function CareerModal({ career, careerDetails, userState, areaTitle, userZipcode, closeModal, addCareerToList }) {
|
||||||
{ name: 'Alabama', code: 'AL' },
|
|
||||||
{ name: 'Alaska', code: 'AK' },
|
|
||||||
{ name: 'Arizona', code: 'AZ' },
|
|
||||||
{ name: 'Arkansas', code: 'AR' },
|
|
||||||
{ name: 'California', code: 'CA' },
|
|
||||||
{ name: 'Colorado', code: 'CO' },
|
|
||||||
{ name: 'Connecticut', code: 'CT' },
|
|
||||||
{ name: 'Delaware', code: 'DE' },
|
|
||||||
{ name: 'District of Columbia', code: 'DC' },
|
|
||||||
{ name: 'Florida', code: 'FL' },
|
|
||||||
{ name: 'Georgia', code: 'GA' },
|
|
||||||
{ name: 'Hawaii', code: 'HI' },
|
|
||||||
{ name: 'Idaho', code: 'ID' },
|
|
||||||
{ name: 'Illinois', code: 'IL' },
|
|
||||||
{ name: 'Indiana', code: 'IN' },
|
|
||||||
{ name: 'Iowa', code: 'IA' },
|
|
||||||
{ name: 'Kansas', code: 'KS' },
|
|
||||||
{ name: 'Kentucky', code: 'KY' },
|
|
||||||
{ name: 'Louisiana', code: 'LA' },
|
|
||||||
{ name: 'Maine', code: 'ME' },
|
|
||||||
{ name: 'Maryland', code: 'MD' },
|
|
||||||
{ name: 'Massachusetts', code: 'MA' },
|
|
||||||
{ name: 'Michigan', code: 'MI' },
|
|
||||||
{ name: 'Minnesota', code: 'MN' },
|
|
||||||
{ name: 'Mississippi', code: 'MS' },
|
|
||||||
{ name: 'Missouri', code: 'MO' },
|
|
||||||
{ name: 'Montana', code: 'MT' },
|
|
||||||
{ name: 'Nebraska', code: 'NE' },
|
|
||||||
{ name: 'Nevada', code: 'NV' },
|
|
||||||
{ name: 'New Hampshire', code: 'NH' },
|
|
||||||
{ name: 'New Jersey', code: 'NJ' },
|
|
||||||
{ name: 'New Mexico', code: 'NM' },
|
|
||||||
{ name: 'New York', code: 'NY' },
|
|
||||||
{ name: 'North Carolina', code: 'NC' },
|
|
||||||
{ name: 'North Dakota', code: 'ND' },
|
|
||||||
{ name: 'Ohio', code: 'OH' },
|
|
||||||
{ name: 'Oklahoma', code: 'OK' },
|
|
||||||
{ name: 'Oregon', code: 'OR' },
|
|
||||||
{ name: 'Pennsylvania', code: 'PA' },
|
|
||||||
{ name: 'Rhode Island', code: 'RI' },
|
|
||||||
{ name: 'South Carolina', code: 'SC' },
|
|
||||||
{ name: 'South Dakota', code: 'SD' },
|
|
||||||
{ name: 'Tennessee', code: 'TN' },
|
|
||||||
{ name: 'Texas', code: 'TX' },
|
|
||||||
{ name: 'Utah', code: 'UT' },
|
|
||||||
{ name: 'Vermont', code: 'VT' },
|
|
||||||
{ name: 'Virginia', code: 'VA' },
|
|
||||||
{ name: 'Washington', code: 'WA' },
|
|
||||||
{ name: 'West Virginia', code: 'WV' },
|
|
||||||
{ name: 'Wisconsin', code: 'WI' },
|
|
||||||
{ name: 'Wyoming', code: 'WY' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 2) Helper to convert state code => full name
|
|
||||||
function getFullStateName(code) {
|
|
||||||
const found = STATES.find((s) => s.code === code?.toUpperCase());
|
|
||||||
return found ? found.name : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function CareerModal({ career, userState, areaTitle, userZipcode, closeModal, addCareerToList }) {
|
|
||||||
const [careerDetails, setCareerDetails] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
console.log('CareerModal props:', { career, careerDetails, userState, areaTitle, userZipcode });
|
||||||
const handleCareerClick = async () => {
|
|
||||||
const socCode = career.code;
|
if (!careerDetails?.salaryData) {
|
||||||
setLoading(true);
|
return <div>Loading career details...</div>;
|
||||||
setError(null);
|
}
|
||||||
|
if (error) return <div>{error}</div>;
|
||||||
try {
|
|
||||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
|
||||||
if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code');
|
|
||||||
const { cipCode } = await cipResponse.json();
|
|
||||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
|
||||||
|
|
||||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
|
||||||
if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description');
|
|
||||||
const { description, tasks } = await jobDetailsResponse.json();
|
|
||||||
|
|
||||||
const salaryResponse = await axios.get(`${apiUrl}/salary`, {
|
|
||||||
params: { socCode: socCode.split('.')[0], area: areaTitle },
|
|
||||||
}).catch(() => ({ data: {} }));
|
|
||||||
|
|
||||||
const economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
|
||||||
params: { state: getFullStateName(userState) },
|
|
||||||
}).catch(() => ({ data: {} }));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const sData = salaryResponse.data || {};
|
|
||||||
const salaryDataPoints = sData && Object.keys(sData).length > 0 ? [
|
|
||||||
{ percentile: '10th', regionalSalary: sData.regional?.regional_PCT10 || 0, nationalSalary: sData.national?.national_PCT10 || 0 },
|
|
||||||
{ percentile: '25th', regionalSalary: sData.regional?.regional_PCT25 || 0, nationalSalary: sData.national?.national_PCT25 || 0 },
|
|
||||||
{ percentile: 'Median', regionalSalary: sData.regional?.regional_MEDIAN || 0, nationalSalary: sData.national?.national_MEDIAN || 0 },
|
|
||||||
{ percentile: '75th', regionalSalary: sData.regional?.regional_PCT75 || 0, nationalSalary: sData.national?.national_PCT75 || 0 },
|
|
||||||
{ percentile: '90th', regionalSalary: sData.regional?.regional_PCT90 || 0, nationalSalary: sData.national?.national_PCT90 || 0 },
|
|
||||||
] : [];
|
|
||||||
|
|
||||||
setCareerDetails({
|
|
||||||
...career,
|
|
||||||
jobDescription: description,
|
|
||||||
tasks,
|
|
||||||
economicProjections: economicResponse.data || {},
|
|
||||||
salaryData: salaryDataPoints,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
setError('Failed to load career details.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCareerClick();
|
|
||||||
}, [career, userState, areaTitle, userZipcode]);
|
|
||||||
|
|
||||||
if (loading) return <div>Loading...</div>;
|
|
||||||
if (error) return <div>{error}</div>;
|
|
||||||
|
|
||||||
const calculateStabilityRating = (salaryData) => {
|
const calculateStabilityRating = (salaryData) => {
|
||||||
const medianSalaryObj = salaryData.find(s => s.percentile === 'Median');
|
const medianSalaryObj = salaryData.find(s => s.percentile === 'Median');
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user