Fixed regional salary and state economic projections

This commit is contained in:
Josh 2025-05-14 17:40:42 +00:00
parent 41ee9877d2
commit d42aaf1d7c
3 changed files with 318 additions and 222 deletions

View File

@ -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 CareerSuggestions from './CareerSuggestions.js';
@ -7,16 +7,84 @@ import CareerModal from './CareerModal.js';
import CareerSearch from './CareerSearch.js';
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 apiUrl = process.env.REACT_APP_API_URL || '';
const [userProfile, setUserProfile] = useState(null);
const [masterCareerRatings, setMasterCareerRatings] = useState([]);
const [careerList, setCareerList] = useState([]);
const [careerDetails, setCareerDetails] = useState(null);
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 [careersWithJobZone, setCareersWithJobZone] = useState([]);
const [salaryData, setSalaryData] = useState([]);
const [economicProjections, setEconomicProjections] = useState(null);
const [selectedJobZone, setSelectedJobZone] = useState('');
const [selectedFit, setSelectedFit] = useState('');
const [selectedCareer, setSelectedCareer] = useState(null);
@ -37,6 +105,235 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
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(() => {
fetch('/careers_with_ratings.json')
.then((res) => {
@ -61,8 +358,7 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
try {
const token = localStorage.getItem('token');
await axios.post(`${apiUrl}/user-profile`, {
// Provide all required fields from userProfile
// If your DB requires them, fill them in here:
firstName: userProfile?.firstname,
lastName: userProfile?.lastname,
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)
const filteredCareers = useMemo(() => {
@ -435,11 +641,12 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) {
{selectedCareer && (
<CareerModal
career={selectedCareer}
closeModal={() => setSelectedCareer(null)}
userState={userState}
areaTitle={areaTitle}
userZipcode={userProfile?.zipcode}
addCareerToList={addCareerToList} // <-- explicitly added here
careerDetails={careerDetails}
closeModal={() => {
setSelectedCareer(null);
setCareerDetails(null);
}}
addCareerToList={addCareerToList}
/>
)}

View File

@ -1,128 +1,17 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { fetchSchools, clientGeocodeZip, haversineDistance} from '../utils/apiUtils.js';
const apiUrl = process.env.REACT_APP_API_URL;
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' },
];
function CareerModal({ career, careerDetails, userState, areaTitle, userZipcode, closeModal, addCareerToList }) {
// 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);
useEffect(() => {
const handleCareerClick = async () => {
const socCode = career.code;
setLoading(true);
setError(null);
console.log('CareerModal props:', { career, careerDetails, userState, areaTitle, userZipcode });
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);
if (!careerDetails?.salaryData) {
return <div>Loading career details...</div>;
}
};
handleCareerClick();
}, [career, userState, areaTitle, userZipcode]);
if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;
const calculateStabilityRating = (salaryData) => {

Binary file not shown.