Fixed loading for CareerExplorer, and no reload on filter
This commit is contained in:
parent
3932194079
commit
a053da72e5
@ -89,6 +89,7 @@ function App() {
|
|||||||
// Logout
|
// Logout
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('careerSuggestionsCache');
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
navigate('/signin');
|
navigate('/signin');
|
||||||
|
@ -8,7 +8,7 @@ import CareerSearch from './CareerSearch.js';
|
|||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const STATES = [
|
const STATES = [
|
||||||
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
|
{ 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: '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: 'Connecticut', code: 'CT' }, { name: 'Delaware', code: 'DE' }, { name: 'District of Columbia', code: 'DC' },
|
||||||
@ -26,9 +26,9 @@ import axios from 'axios';
|
|||||||
{ name: 'Tennessee', code: 'TN' }, { name: 'Texas', code: 'TX' }, { name: 'Utah', code: 'UT' },
|
{ 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: 'Vermont', code: 'VT' }, { name: 'Virginia', code: 'VA' }, { name: 'Washington', code: 'WA' },
|
||||||
{ name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' },
|
{ name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// -------------- CIP HELPER FUNCTIONS --------------
|
// -------------- CIP HELPER FUNCTIONS --------------
|
||||||
|
|
||||||
// 1) Insert leading zero if there's only 1 digit before the decimal
|
// 1) Insert leading zero if there's only 1 digit before the decimal
|
||||||
function ensureTwoDigitsBeforeDecimal(cipStr) {
|
function ensureTwoDigitsBeforeDecimal(cipStr) {
|
||||||
@ -55,6 +55,7 @@ function CareerExplorer() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
|
||||||
|
// ---------- Component States ----------
|
||||||
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([]);
|
||||||
@ -65,8 +66,10 @@ function CareerExplorer() {
|
|||||||
const [userZipcode, setUserZipcode] = useState(null);
|
const [userZipcode, setUserZipcode] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||||
|
|
||||||
|
// This is where we'll hold ALL final suggestions (with job_zone merged)
|
||||||
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
const [careerSuggestions, setCareerSuggestions] = useState([]);
|
||||||
const [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
|
||||||
const [salaryData, setSalaryData] = useState([]);
|
const [salaryData, setSalaryData] = useState([]);
|
||||||
const [economicProjections, setEconomicProjections] = useState(null);
|
const [economicProjections, setEconomicProjections] = useState(null);
|
||||||
const [selectedJobZone, setSelectedJobZone] = useState('');
|
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||||
@ -89,7 +92,143 @@ function CareerExplorer() {
|
|||||||
Good: 'Good - Less Strong Match',
|
Good: 'Good - Less Strong Match',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===================== Load user profile =====================
|
// --------------------------------------------------
|
||||||
|
// fetchSuggestions - combined suggestions + job zone
|
||||||
|
// --------------------------------------------------
|
||||||
|
const fetchSuggestions = async (answers, profileData) => {
|
||||||
|
if (!answers) {
|
||||||
|
setCareerSuggestions([]);
|
||||||
|
localStorage.removeItem('careerSuggestionsCache');
|
||||||
|
|
||||||
|
// Reset loading & progress if userProfile has no answers
|
||||||
|
setLoading(true);
|
||||||
|
setProgress(0);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
// 1) O*NET answers -> initial career list
|
||||||
|
const submitRes = await axios.post(`${apiUrl}/onet/submit_answers`, {
|
||||||
|
answers,
|
||||||
|
state: profileData.state,
|
||||||
|
area: profileData.area,
|
||||||
|
});
|
||||||
|
const { careers = [] } = submitRes.data || {};
|
||||||
|
const flattened = careers.flat();
|
||||||
|
|
||||||
|
// We'll do an extra single call for job zones + 4 calls for each career:
|
||||||
|
// => total steps = 1 (jobZones) + (flattened.length * 4)
|
||||||
|
let totalSteps = 1 + (flattened.length * 4);
|
||||||
|
let completedSteps = 0;
|
||||||
|
|
||||||
|
// Increments the global progress bar
|
||||||
|
const increment = () => {
|
||||||
|
completedSteps++;
|
||||||
|
const pct = Math.round((completedSteps / totalSteps) * 100);
|
||||||
|
setProgress(pct);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A helper that does a GET request, increments progress on success/fail
|
||||||
|
const fetchWithProgress = async (url, params) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(url, { params });
|
||||||
|
increment();
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
increment();
|
||||||
|
return {}; // or null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2) job zones (one call for all SOC codes)
|
||||||
|
const socCodes = flattened.map((c) => c.code);
|
||||||
|
const zonesRes = await axios.post(`${apiUrl}/job-zones`, { socCodes }).catch(() => null);
|
||||||
|
// increment progress for this single request
|
||||||
|
increment();
|
||||||
|
|
||||||
|
const jobZoneData = zonesRes?.data || {};
|
||||||
|
|
||||||
|
// 3) For each career, also fetch CIP, job details, projections, salary
|
||||||
|
const enrichedPromiseArray = flattened.map(async (career) => {
|
||||||
|
const strippedSoc = career.code.split('.')[0];
|
||||||
|
|
||||||
|
// build URLs
|
||||||
|
const cipUrl = `${apiUrl}/cip/${career.code}`;
|
||||||
|
const jobDetailsUrl = `${apiUrl}/onet/career-description/${career.code}`;
|
||||||
|
const economicUrl = `${apiUrl}/projections/${strippedSoc}`;
|
||||||
|
const salaryParams = { socCode: strippedSoc, area: profileData.area };
|
||||||
|
|
||||||
|
// We'll fetch them in parallel with our custom fetchWithProgress:
|
||||||
|
const [cipRaw, jobRaw, ecoRaw, salRaw] = await Promise.all([
|
||||||
|
fetchWithProgress(cipUrl),
|
||||||
|
fetchWithProgress(jobDetailsUrl),
|
||||||
|
fetchWithProgress(economicUrl),
|
||||||
|
fetchWithProgress(`${apiUrl}/salary`, salaryParams),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// parse data
|
||||||
|
const cip = cipRaw || {};
|
||||||
|
const jobDetails = jobRaw || {};
|
||||||
|
const economic = ecoRaw || {};
|
||||||
|
const salary = salRaw || {};
|
||||||
|
|
||||||
|
// Check if data is missing
|
||||||
|
const isCipMissing = !cip || Object.keys(cip).length === 0;
|
||||||
|
const isJobDetailsMissing = !jobDetails || Object.keys(jobDetails).length === 0;
|
||||||
|
const isEconomicMissing =
|
||||||
|
!economic || Object.values(economic).every((val) => val === 'N/A' || val === '*');
|
||||||
|
const isSalaryMissing = !salary || Object.keys(salary).length === 0;
|
||||||
|
|
||||||
|
const isLimitedData =
|
||||||
|
isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...career,
|
||||||
|
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
|
||||||
|
limitedData: isLimitedData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for everything to finish
|
||||||
|
const finalEnrichedCareers = await Promise.all(enrichedPromiseArray);
|
||||||
|
|
||||||
|
// Store final suggestions in local storage
|
||||||
|
localStorage.setItem('careerSuggestionsCache', JSON.stringify(finalEnrichedCareers));
|
||||||
|
|
||||||
|
// Update React state
|
||||||
|
setCareerSuggestions(finalEnrichedCareers);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[fetchSuggestions] Error:', err);
|
||||||
|
setCareerSuggestions([]);
|
||||||
|
localStorage.removeItem('careerSuggestionsCache');
|
||||||
|
} finally {
|
||||||
|
// Hide spinner
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// On mount, load suggestions from cache
|
||||||
|
// --------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
const cached = localStorage.getItem('careerSuggestionsCache');
|
||||||
|
if (cached) {
|
||||||
|
const parsed = JSON.parse(cached);
|
||||||
|
if (parsed?.length) {
|
||||||
|
setCareerSuggestions(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// Load user profile
|
||||||
|
// --------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@ -102,49 +241,21 @@ function CareerExplorer() {
|
|||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const profileData = res.data;
|
const profileData = res.data;
|
||||||
console.log('[fetchUserProfile] loaded profileData =>', profileData);
|
|
||||||
|
|
||||||
setUserProfile(profileData);
|
setUserProfile(profileData);
|
||||||
setUserState(profileData.state);
|
setUserState(profileData.state);
|
||||||
setAreaTitle(profileData.area);
|
setAreaTitle(profileData.area);
|
||||||
setUserZipcode(profileData.zipcode);
|
setUserZipcode(profileData.zipcode);
|
||||||
|
|
||||||
// If they have a saved career list
|
|
||||||
if (profileData.career_list) {
|
if (profileData.career_list) {
|
||||||
setCareerList(JSON.parse(profileData.career_list));
|
setCareerList(JSON.parse(profileData.career_list));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If they have interest inventory, 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 {
|
|
||||||
setCareerSuggestions([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if priorities 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) {
|
|
||||||
setShowModal(true);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching user profile:', err);
|
console.error('Error fetching user profile:', err);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -152,39 +263,23 @@ function CareerExplorer() {
|
|||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
}, [apiUrl]);
|
}, [apiUrl]);
|
||||||
|
|
||||||
// ===================== If location.state has careerSuggestions =====================
|
// ------------------------------------------------------
|
||||||
|
// If user came from Interest Inventory => auto-fetch
|
||||||
|
// ------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state?.careerSuggestions) {
|
if (
|
||||||
setCareerSuggestions(location.state.careerSuggestions);
|
location.state?.fromInterestInventory &&
|
||||||
|
userProfile?.interest_inventory_answers
|
||||||
|
) {
|
||||||
|
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
|
||||||
|
// remove that state so refresh doesn't re-fetch
|
||||||
|
navigate('.', { replace: true });
|
||||||
}
|
}
|
||||||
}, [location.state]);
|
}, [location.state, userProfile, fetchSuggestions, navigate]);
|
||||||
|
|
||||||
// ===================== Fetch job zones for suggestions =====================
|
// ------------------------------------------------------
|
||||||
useEffect(() => {
|
// handleCareerClick (detail fetch for CIP, Salary, etc.)
|
||||||
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,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setCareersWithJobZone([...updatedCareers]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching job zone information:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchJobZones();
|
|
||||||
}, [careerSuggestions, apiUrl]);
|
|
||||||
|
|
||||||
// ===================== handleCareerClick (detail fetch) =====================
|
|
||||||
const handleCareerClick = useCallback(
|
const handleCareerClick = useCallback(
|
||||||
async (career) => {
|
async (career) => {
|
||||||
console.log('[handleCareerClick] career =>', career);
|
console.log('[handleCareerClick] career =>', career);
|
||||||
@ -195,8 +290,6 @@ function CareerExplorer() {
|
|||||||
setSalaryData([]);
|
setSalaryData([]);
|
||||||
setEconomicProjections({});
|
setEconomicProjections({});
|
||||||
|
|
||||||
setSelectedCareer(career);
|
|
||||||
|
|
||||||
if (!socCode) {
|
if (!socCode) {
|
||||||
console.error('SOC Code is missing');
|
console.error('SOC Code is missing');
|
||||||
setError('SOC Code is missing');
|
setError('SOC Code is missing');
|
||||||
@ -211,7 +304,9 @@ function CareerExplorer() {
|
|||||||
setError(
|
setError(
|
||||||
`We're sorry, but specific details for "${career.title}" are not available at this time.`
|
`We're sorry, but specific details for "${career.title}" are not available at this time.`
|
||||||
);
|
);
|
||||||
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
|
setCareerDetails({
|
||||||
|
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||||
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -219,9 +314,13 @@ function CareerExplorer() {
|
|||||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||||
|
|
||||||
// Job details
|
// Job details
|
||||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
const jobDetailsResponse = await fetch(
|
||||||
if (!jobDetailsResponse.ok){
|
`${apiUrl}/onet/career-description/${socCode}`
|
||||||
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
|
);
|
||||||
|
if (!jobDetailsResponse.ok) {
|
||||||
|
setCareerDetails({
|
||||||
|
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
|
||||||
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -237,35 +336,64 @@ function CareerExplorer() {
|
|||||||
salaryResponse = { data: {} };
|
salaryResponse = { data: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build salary array
|
|
||||||
const sData = salaryResponse.data || {};
|
const sData = salaryResponse.data || {};
|
||||||
const salaryDataPoints =
|
const salaryDataPoints =
|
||||||
sData && Object.keys(sData).length > 0
|
sData && Object.keys(sData).length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
percentile: '10th Percentile',
|
percentile: '10th Percentile',
|
||||||
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
|
regionalSalary: parseInt(
|
||||||
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
|
sData.regional?.regional_PCT10,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
|
nationalSalary: parseInt(
|
||||||
|
sData.national?.national_PCT10,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
percentile: '25th Percentile',
|
percentile: '25th Percentile',
|
||||||
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
|
regionalSalary: parseInt(
|
||||||
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
|
sData.regional?.regional_PCT25,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
|
nationalSalary: parseInt(
|
||||||
|
sData.national?.national_PCT25,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
percentile: 'Median',
|
percentile: 'Median',
|
||||||
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
|
regionalSalary: parseInt(
|
||||||
nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0,
|
sData.regional?.regional_MEDIAN,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
|
nationalSalary: parseInt(
|
||||||
|
sData.national?.national_MEDIAN,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
percentile: '75th Percentile',
|
percentile: '75th Percentile',
|
||||||
regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0,
|
regionalSalary: parseInt(
|
||||||
nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0,
|
sData.regional?.regional_PCT75,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
|
nationalSalary: parseInt(
|
||||||
|
sData.national?.national_PCT75,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
percentile: '90th Percentile',
|
percentile: '90th Percentile',
|
||||||
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
|
regionalSalary: parseInt(
|
||||||
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0,
|
sData.regional?.regional_PCT90,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
|
nationalSalary: parseInt(
|
||||||
|
sData.national?.national_PCT90,
|
||||||
|
10
|
||||||
|
) || 0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
@ -274,9 +402,12 @@ function CareerExplorer() {
|
|||||||
const fullStateName = getFullStateName(userState);
|
const fullStateName = getFullStateName(userState);
|
||||||
let economicResponse = { data: {} };
|
let economicResponse = { data: {} };
|
||||||
try {
|
try {
|
||||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
economicResponse = await axios.get(
|
||||||
|
`${apiUrl}/projections/${socCode.split('.')[0]}`,
|
||||||
|
{
|
||||||
params: { state: fullStateName },
|
params: { state: fullStateName },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
economicResponse = { data: {} };
|
economicResponse = { data: {} };
|
||||||
}
|
}
|
||||||
@ -300,10 +431,12 @@ function CareerExplorer() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[userState, apiUrl, areaTitle, userZipcode]
|
[userState, apiUrl, areaTitle]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===================== handleCareerFromSearch =====================
|
// ------------------------------------------------------
|
||||||
|
// handleCareerFromSearch
|
||||||
|
// ------------------------------------------------------
|
||||||
const handleCareerFromSearch = useCallback(
|
const handleCareerFromSearch = useCallback(
|
||||||
(obj) => {
|
(obj) => {
|
||||||
const adapted = {
|
const adapted = {
|
||||||
@ -317,6 +450,9 @@ function CareerExplorer() {
|
|||||||
[handleCareerClick]
|
[handleCareerClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// pendingCareerForModal effect
|
||||||
|
// ------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingCareerForModal) {
|
if (pendingCareerForModal) {
|
||||||
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
|
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
|
||||||
@ -325,7 +461,9 @@ function CareerExplorer() {
|
|||||||
}
|
}
|
||||||
}, [pendingCareerForModal, handleCareerFromSearch]);
|
}, [pendingCareerForModal, handleCareerFromSearch]);
|
||||||
|
|
||||||
// ===================== Load careers_with_ratings for CIP arrays =====================
|
// ------------------------------------------------------
|
||||||
|
// Load careers_with_ratings for CIP arrays
|
||||||
|
// ------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/careers_with_ratings.json')
|
fetch('/careers_with_ratings.json')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -336,17 +474,33 @@ function CareerExplorer() {
|
|||||||
.catch((err) => console.error('Error fetching career ratings:', err));
|
.catch((err) => console.error('Error fetching career ratings:', err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Derived data / Helpers
|
||||||
|
// ------------------------------------------------------
|
||||||
const priorities = useMemo(() => {
|
const priorities = useMemo(() => {
|
||||||
return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {};
|
return userProfile?.career_priorities
|
||||||
|
? JSON.parse(userProfile.career_priorities)
|
||||||
|
: {};
|
||||||
}, [userProfile]);
|
}, [userProfile]);
|
||||||
|
|
||||||
const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'];
|
const priorityKeys = [
|
||||||
|
'interests',
|
||||||
|
'meaning',
|
||||||
|
'stability',
|
||||||
|
'growth',
|
||||||
|
'balance',
|
||||||
|
'recognition',
|
||||||
|
];
|
||||||
|
|
||||||
const getCareerRatingsBySocCode = (socCode) => {
|
const getCareerRatingsBySocCode = (socCode) => {
|
||||||
return masterCareerRatings.find((c) => c.soc_code === socCode)?.ratings || {};
|
return (
|
||||||
|
masterCareerRatings.find((c) => c.soc_code === socCode)?.ratings || {}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===================== Save comparison list to backend =====================
|
// ------------------------------------------------------
|
||||||
|
// Save comparison list to backend
|
||||||
|
// ------------------------------------------------------
|
||||||
const saveCareerListToBackend = async (newCareerList) => {
|
const saveCareerListToBackend = async (newCareerList) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
@ -373,7 +527,9 @@ function CareerExplorer() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===================== Add/Remove from comparison =====================
|
// ------------------------------------------------------
|
||||||
|
// Add/Remove from comparison
|
||||||
|
// ------------------------------------------------------
|
||||||
const addCareerToList = (career) => {
|
const addCareerToList = (career) => {
|
||||||
const masterRatings = getCareerRatingsBySocCode(career.code);
|
const masterRatings = getCareerRatingsBySocCode(career.code);
|
||||||
|
|
||||||
@ -389,7 +545,10 @@ function CareerExplorer() {
|
|||||||
: fitRatingMap[career.fit] || masterRatings.interests || 3;
|
: fitRatingMap[career.fit] || masterRatings.interests || 3;
|
||||||
|
|
||||||
const meaningRating = parseInt(
|
const meaningRating = parseInt(
|
||||||
prompt("How important do you feel this job is to society or the world? (1-5):", "3"),
|
prompt(
|
||||||
|
"How important do you feel this job is to society or the world? (1-5):",
|
||||||
|
"3"
|
||||||
|
),
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -433,7 +592,9 @@ function CareerExplorer() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===================== Let user pick a career from comparison => "Select for Education" =====================
|
// ------------------------------------------------------
|
||||||
|
// "Select for Education" => navigate with CIP codes
|
||||||
|
// ------------------------------------------------------
|
||||||
const handleSelectForEducation = (career) => {
|
const handleSelectForEducation = (career) => {
|
||||||
// 1) Confirm
|
// 1) Confirm
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
@ -450,8 +611,7 @@ function CareerExplorer() {
|
|||||||
|
|
||||||
// 3) Clean CIP codes
|
// 3) Clean CIP codes
|
||||||
const rawCips = matching.cip_codes || [];
|
const rawCips = matching.cip_codes || [];
|
||||||
const cleanedCips = cleanCipCodes(rawCips); // from top-level function
|
const cleanedCips = cleanCipCodes(rawCips);
|
||||||
console.log('cleanedCips =>', cleanedCips);
|
|
||||||
|
|
||||||
// 4) Navigate
|
// 4) Navigate
|
||||||
navigate('/educational-programs', {
|
navigate('/educational-programs', {
|
||||||
@ -465,22 +625,26 @@ function CareerExplorer() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
// ===================== Filter logic for jobZone, Fit =====================
|
// Filter logic for jobZone, Fit
|
||||||
|
// ------------------------------------------------------
|
||||||
const filteredCareers = useMemo(() => {
|
const filteredCareers = useMemo(() => {
|
||||||
return careersWithJobZone.filter((career) => {
|
return careerSuggestions.filter((career) => {
|
||||||
|
// If user selected a jobZone, check if career.job_zone matches
|
||||||
const jobZoneMatches = selectedJobZone
|
const jobZoneMatches = selectedJobZone
|
||||||
? career.job_zone !== null &&
|
? career.job_zone !== null &&
|
||||||
career.job_zone !== undefined &&
|
career.job_zone !== undefined &&
|
||||||
Number(career.job_zone) === Number(selectedJobZone)
|
Number(career.job_zone) === Number(selectedJobZone)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
|
// If user selected a fit, check if career.fit matches
|
||||||
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
||||||
|
|
||||||
return jobZoneMatches && fitMatches;
|
return jobZoneMatches && fitMatches;
|
||||||
});
|
});
|
||||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
}, [careerSuggestions, selectedJobZone, selectedFit]);
|
||||||
|
|
||||||
// Weighted “match score” logic. (unchanged)
|
// Weighted "match score" logic. (unchanged)
|
||||||
const priorityWeight = (priority, response) => {
|
const priorityWeight = (priority, response) => {
|
||||||
const weightMap = {
|
const weightMap = {
|
||||||
interests: {
|
interests: {
|
||||||
@ -516,6 +680,9 @@ function CareerExplorer() {
|
|||||||
return weightMap[priority][response] || 1;
|
return weightMap[priority][response] || 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Loading Overlay
|
||||||
|
// ------------------------------------------------------
|
||||||
const renderLoadingOverlay = () => {
|
const renderLoadingOverlay = () => {
|
||||||
if (!loading) return null;
|
if (!loading) return null;
|
||||||
return (
|
return (
|
||||||
@ -535,15 +702,20 @@ function CareerExplorer() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Render
|
||||||
|
// ------------------------------------------------------
|
||||||
return (
|
return (
|
||||||
<div className="career-explorer-container bg-white p-6 rounded shadow">
|
<div className="career-explorer-container bg-white p-6 rounded shadow">
|
||||||
{renderLoadingOverlay()}
|
{renderLoadingOverlay()}
|
||||||
|
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<CareerPrioritiesModal
|
<CareerPrioritiesModal
|
||||||
userProfile={userProfile}
|
userProfile={userProfile}
|
||||||
onClose={() => setShowModal(false)}
|
onClose={() => setShowModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-semibold">
|
<h2 className="text-xl font-semibold">
|
||||||
Explore Careers - use the tools below to find your perfect career
|
Explore Careers - use the tools below to find your perfect career
|
||||||
@ -581,12 +753,30 @@ function CareerExplorer() {
|
|||||||
const balanceRating = ratings.balance || 3;
|
const balanceRating = ratings.balance || 3;
|
||||||
const recognitionRating = ratings.recognition || 3;
|
const recognitionRating = ratings.recognition || 3;
|
||||||
|
|
||||||
const userInterestsWeight = priorityWeight('interests', priorities.interests || 'I’m not sure yet');
|
const userInterestsWeight = priorityWeight(
|
||||||
const userMeaningWeight = priorityWeight('meaning', priorities.meaning);
|
'interests',
|
||||||
const userStabilityWeight = priorityWeight('stability', priorities.stability);
|
priorities.interests || 'I’m not sure yet'
|
||||||
const userGrowthWeight = priorityWeight('growth', priorities.growth);
|
);
|
||||||
const userBalanceWeight = priorityWeight('balance', priorities.balance);
|
const userMeaningWeight = priorityWeight(
|
||||||
const userRecognitionWeight = priorityWeight('recognition', priorities.recognition);
|
'meaning',
|
||||||
|
priorities.meaning
|
||||||
|
);
|
||||||
|
const userStabilityWeight = priorityWeight(
|
||||||
|
'stability',
|
||||||
|
priorities.stability
|
||||||
|
);
|
||||||
|
const userGrowthWeight = priorityWeight(
|
||||||
|
'growth',
|
||||||
|
priorities.growth
|
||||||
|
);
|
||||||
|
const userBalanceWeight = priorityWeight(
|
||||||
|
'balance',
|
||||||
|
priorities.balance
|
||||||
|
);
|
||||||
|
const userRecognitionWeight = priorityWeight(
|
||||||
|
'recognition',
|
||||||
|
priorities.recognition
|
||||||
|
);
|
||||||
|
|
||||||
const totalWeight =
|
const totalWeight =
|
||||||
userInterestsWeight +
|
userInterestsWeight +
|
||||||
@ -615,7 +805,9 @@ function CareerExplorer() {
|
|||||||
<td className="border p-2">{growthRating}</td>
|
<td className="border p-2">{growthRating}</td>
|
||||||
<td className="border p-2">{balanceRating}</td>
|
<td className="border p-2">{balanceRating}</td>
|
||||||
<td className="border p-2">{recognitionRating}</td>
|
<td className="border p-2">{recognitionRating}</td>
|
||||||
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
|
<td className="border p-2 font-bold">
|
||||||
|
{matchScore.toFixed(1)}%
|
||||||
|
</td>
|
||||||
<td className="border p-2 space-x-2">
|
<td className="border p-2 space-x-2">
|
||||||
<Button
|
<Button
|
||||||
className="bg-red-600 text-black-500"
|
className="bg-red-600 text-black-500"
|
||||||
@ -624,9 +816,11 @@ function CareerExplorer() {
|
|||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* New Button -> "Select for Education" */}
|
|
||||||
<Button
|
<Button
|
||||||
className="bg-green-600 text-white"
|
className="bg-green-600 text-white px-2 py-1 text-xs
|
||||||
|
sm:text-sm
|
||||||
|
whitespace-nowrap
|
||||||
|
"
|
||||||
onClick={() => handleSelectForEducation(career)}
|
onClick={() => handleSelectForEducation(career)}
|
||||||
>
|
>
|
||||||
Plan your Education/Skills
|
Plan your Education/Skills
|
||||||
@ -667,18 +861,27 @@ function CareerExplorer() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (!userProfile?.interest_inventory_answers) {
|
||||||
|
alert('No interest inventory answers. Complete the interest inventory first!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchSuggestions(userProfile.interest_inventory_answers, userProfile);
|
||||||
|
}}
|
||||||
|
className="bg-green-600 text-white px-3 py-1 text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
Reload Career Suggestions
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Now we pass the *filteredCareers* into the CareerSuggestions component */}
|
||||||
<CareerSuggestions
|
<CareerSuggestions
|
||||||
careerSuggestions={filteredCareers}
|
careerSuggestions={filteredCareers}
|
||||||
onCareerClick={(career) => {
|
onCareerClick={(career) => {
|
||||||
setSelectedCareer(career);
|
setSelectedCareer(career);
|
||||||
handleCareerClick(career);
|
handleCareerClick(career);
|
||||||
}}
|
}}
|
||||||
setLoading={setLoading}
|
|
||||||
setProgress={setProgress}
|
|
||||||
userState={userState}
|
|
||||||
areaTitle={areaTitle}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareer && (
|
||||||
@ -695,17 +898,29 @@ function CareerExplorer() {
|
|||||||
|
|
||||||
<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
|
||||||
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href="https://www.onetcenter.org"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{' '}
|
{' '}
|
||||||
O*Net
|
O*Net
|
||||||
</a>
|
</a>
|
||||||
, in partnership with
|
, in partnership with
|
||||||
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href="https://www.bls.gov"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{' '}
|
{' '}
|
||||||
Bureau of Labor Statistics
|
Bureau of Labor Statistics
|
||||||
</a>
|
</a>
|
||||||
and
|
and
|
||||||
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href="https://nces.ed.gov"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
{' '}
|
{' '}
|
||||||
NCES
|
NCES
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,112 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import axios from 'axios';
|
import './Dashboard.css'; // or Tailwind classes
|
||||||
import './Dashboard.css'; // or replace with Tailwind classes if desired
|
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
|
||||||
|
|
||||||
export function CareerSuggestions({
|
export function CareerSuggestions({
|
||||||
careerSuggestions = [],
|
careerSuggestions = [],
|
||||||
userState,
|
|
||||||
areaTitle,
|
|
||||||
setLoading,
|
|
||||||
setProgress,
|
|
||||||
onCareerClick,
|
onCareerClick,
|
||||||
}) {
|
}) {
|
||||||
const [updatedCareers, setUpdatedCareers] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If no careers provided, stop any loading state
|
|
||||||
if (!careerSuggestions || careerSuggestions.length === 0) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
|
|
||||||
const checkCareerDataAvailability = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setProgress(0);
|
|
||||||
|
|
||||||
// Each career has 4 external calls
|
|
||||||
const totalSteps = careerSuggestions.length * 4;
|
|
||||||
let completedSteps = 0;
|
|
||||||
|
|
||||||
// Helper function to increment the global progress
|
|
||||||
const updateProgress = () => {
|
|
||||||
completedSteps += 1;
|
|
||||||
const percent = Math.round((completedSteps / totalSteps) * 100);
|
|
||||||
setProgress(percent);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Universal fetch helper
|
|
||||||
const fetchJSON = async (url, params) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
params: params || {},
|
|
||||||
});
|
|
||||||
updateProgress(); // increment if success
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
updateProgress(); // increment even on failure
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map over careerSuggestions to fetch CIP, job details, economic, salary data in parallel
|
|
||||||
const careerPromises = careerSuggestions.map(async (career) => {
|
|
||||||
try {
|
|
||||||
// e.g. "15-1199.00" => "15-1199"
|
|
||||||
const strippedSoc = career.code.split('.')[0];
|
|
||||||
|
|
||||||
const [cipData, jobDetailsData, economicData, salaryData] = await Promise.all([
|
|
||||||
fetchJSON(`${apiUrl}/cip/${career.code}`),
|
|
||||||
fetchJSON(`${apiUrl}/onet/career-description/${career.code}`),
|
|
||||||
fetchJSON(`${apiUrl}/projections/${strippedSoc}`),
|
|
||||||
fetchJSON(`${apiUrl}/salary`, {
|
|
||||||
socCode: strippedSoc,
|
|
||||||
area: areaTitle,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Evaluate if any data is missing
|
|
||||||
const isCipMissing = !cipData || Object.keys(cipData).length === 0;
|
|
||||||
const isJobDetailsMissing = !jobDetailsData || Object.keys(jobDetailsData).length === 0;
|
|
||||||
const isEconomicMissing =
|
|
||||||
!economicData ||
|
|
||||||
Object.values(economicData).every((val) => val === 'N/A' || val === '*');
|
|
||||||
const isSalaryMissing = !salaryData;
|
|
||||||
|
|
||||||
const isLimitedData =
|
|
||||||
isCipMissing || isJobDetailsMissing || isEconomicMissing || isSalaryMissing;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...career,
|
|
||||||
limitedData: isLimitedData,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
// If any errors occur mid-logic, mark it limited
|
|
||||||
return { ...career, limitedData: true };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedCareerList = await Promise.all(careerPromises);
|
|
||||||
setUpdatedCareers(updatedCareerList);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkCareerDataAvailability();
|
|
||||||
}, [careerSuggestions, userState, areaTitle, setLoading, setProgress]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="career-suggestions-grid">
|
<div className="career-suggestions-grid">
|
||||||
{updatedCareers.map((career) => (
|
{careerSuggestions.map((career) => (
|
||||||
<button
|
<button
|
||||||
key={career.code}
|
key={career.code}
|
||||||
className={`career-button ${career.limitedData ? 'limited-data' : ''}`}
|
className={`career-button ${career.limitedData ? 'limited-data' : ''}`}
|
||||||
|
@ -156,7 +156,7 @@ const InterestInventory = () => {
|
|||||||
const { careers: careerSuggestions, riaSecScores } = data;
|
const { careers: careerSuggestions, riaSecScores } = data;
|
||||||
|
|
||||||
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
||||||
navigate('/career-explorer', { state: { careerSuggestions, riaSecScores } });
|
navigate('/career-explorer', { state: { careerSuggestions, riaSecScores, fromInterestInventory: true } });
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid data format from the server.');
|
throw new Error('Invalid data format from the server.');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user