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
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('careerSuggestionsCache');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
navigate('/signin');
|
||||
|
@ -55,6 +55,7 @@ function CareerExplorer() {
|
||||
const location = useLocation();
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||
|
||||
// ---------- Component States ----------
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
const [masterCareerRatings, setMasterCareerRatings] = useState([]);
|
||||
const [careerList, setCareerList] = useState([]);
|
||||
@ -65,8 +66,10 @@ function CareerExplorer() {
|
||||
const [userZipcode, setUserZipcode] = useState(null);
|
||||
const [error, setError] = 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 [careersWithJobZone, setCareersWithJobZone] = useState([]);
|
||||
|
||||
const [salaryData, setSalaryData] = useState([]);
|
||||
const [economicProjections, setEconomicProjections] = useState(null);
|
||||
const [selectedJobZone, setSelectedJobZone] = useState('');
|
||||
@ -89,7 +92,143 @@ function CareerExplorer() {
|
||||
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(() => {
|
||||
setLoading(true);
|
||||
|
||||
@ -102,49 +241,21 @@ function CareerExplorer() {
|
||||
|
||||
if (res.status === 200) {
|
||||
const profileData = res.data;
|
||||
console.log('[fetchUserProfile] loaded profileData =>', profileData);
|
||||
|
||||
setUserProfile(profileData);
|
||||
setUserState(profileData.state);
|
||||
setAreaTitle(profileData.area);
|
||||
setUserZipcode(profileData.zipcode);
|
||||
|
||||
// If they have a saved career list
|
||||
if (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 {
|
||||
setShowModal(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching user profile:', err);
|
||||
setShowModal(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@ -152,39 +263,23 @@ function CareerExplorer() {
|
||||
fetchUserProfile();
|
||||
}, [apiUrl]);
|
||||
|
||||
// ===================== If location.state has careerSuggestions =====================
|
||||
// ------------------------------------------------------
|
||||
// If user came from Interest Inventory => auto-fetch
|
||||
// ------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (location.state?.careerSuggestions) {
|
||||
setCareerSuggestions(location.state.careerSuggestions);
|
||||
if (
|
||||
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(() => {
|
||||
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) =====================
|
||||
// ------------------------------------------------------
|
||||
// handleCareerClick (detail fetch for CIP, Salary, etc.)
|
||||
// ------------------------------------------------------
|
||||
const handleCareerClick = useCallback(
|
||||
async (career) => {
|
||||
console.log('[handleCareerClick] career =>', career);
|
||||
@ -195,8 +290,6 @@ function CareerExplorer() {
|
||||
setSalaryData([]);
|
||||
setEconomicProjections({});
|
||||
|
||||
setSelectedCareer(career);
|
||||
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
setError('SOC Code is missing');
|
||||
@ -211,7 +304,9 @@ function CareerExplorer() {
|
||||
setError(
|
||||
`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);
|
||||
return;
|
||||
}
|
||||
@ -219,9 +314,13 @@ function CareerExplorer() {
|
||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
// Job details
|
||||
const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
|
||||
const jobDetailsResponse = await fetch(
|
||||
`${apiUrl}/onet/career-description/${socCode}`
|
||||
);
|
||||
if (!jobDetailsResponse.ok) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@ -237,35 +336,64 @@ function CareerExplorer() {
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
regionalSalary: parseInt(
|
||||
sData.regional?.regional_PCT90,
|
||||
10
|
||||
) || 0,
|
||||
nationalSalary: parseInt(
|
||||
sData.national?.national_PCT90,
|
||||
10
|
||||
) || 0,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
@ -274,9 +402,12 @@ function CareerExplorer() {
|
||||
const fullStateName = getFullStateName(userState);
|
||||
let economicResponse = { data: {} };
|
||||
try {
|
||||
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, {
|
||||
economicResponse = await axios.get(
|
||||
`${apiUrl}/projections/${socCode.split('.')[0]}`,
|
||||
{
|
||||
params: { state: fullStateName },
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
economicResponse = { data: {} };
|
||||
}
|
||||
@ -300,10 +431,12 @@ function CareerExplorer() {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[userState, apiUrl, areaTitle, userZipcode]
|
||||
[userState, apiUrl, areaTitle]
|
||||
);
|
||||
|
||||
// ===================== handleCareerFromSearch =====================
|
||||
// ------------------------------------------------------
|
||||
// handleCareerFromSearch
|
||||
// ------------------------------------------------------
|
||||
const handleCareerFromSearch = useCallback(
|
||||
(obj) => {
|
||||
const adapted = {
|
||||
@ -317,6 +450,9 @@ function CareerExplorer() {
|
||||
[handleCareerClick]
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// pendingCareerForModal effect
|
||||
// ------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (pendingCareerForModal) {
|
||||
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
|
||||
@ -325,7 +461,9 @@ function CareerExplorer() {
|
||||
}
|
||||
}, [pendingCareerForModal, handleCareerFromSearch]);
|
||||
|
||||
// ===================== Load careers_with_ratings for CIP arrays =====================
|
||||
// ------------------------------------------------------
|
||||
// Load careers_with_ratings for CIP arrays
|
||||
// ------------------------------------------------------
|
||||
useEffect(() => {
|
||||
fetch('/careers_with_ratings.json')
|
||||
.then((res) => {
|
||||
@ -336,17 +474,33 @@ function CareerExplorer() {
|
||||
.catch((err) => console.error('Error fetching career ratings:', err));
|
||||
}, []);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Derived data / Helpers
|
||||
// ------------------------------------------------------
|
||||
const priorities = useMemo(() => {
|
||||
return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {};
|
||||
return userProfile?.career_priorities
|
||||
? JSON.parse(userProfile.career_priorities)
|
||||
: {};
|
||||
}, [userProfile]);
|
||||
|
||||
const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'];
|
||||
const priorityKeys = [
|
||||
'interests',
|
||||
'meaning',
|
||||
'stability',
|
||||
'growth',
|
||||
'balance',
|
||||
'recognition',
|
||||
];
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
@ -373,7 +527,9 @@ function CareerExplorer() {
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== Add/Remove from comparison =====================
|
||||
// ------------------------------------------------------
|
||||
// Add/Remove from comparison
|
||||
// ------------------------------------------------------
|
||||
const addCareerToList = (career) => {
|
||||
const masterRatings = getCareerRatingsBySocCode(career.code);
|
||||
|
||||
@ -389,7 +545,10 @@ function CareerExplorer() {
|
||||
: fitRatingMap[career.fit] || masterRatings.interests || 3;
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
@ -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) => {
|
||||
// 1) Confirm
|
||||
const confirmed = window.confirm(
|
||||
@ -450,8 +611,7 @@ function CareerExplorer() {
|
||||
|
||||
// 3) Clean CIP codes
|
||||
const rawCips = matching.cip_codes || [];
|
||||
const cleanedCips = cleanCipCodes(rawCips); // from top-level function
|
||||
console.log('cleanedCips =>', cleanedCips);
|
||||
const cleanedCips = cleanCipCodes(rawCips);
|
||||
|
||||
// 4) Navigate
|
||||
navigate('/educational-programs', {
|
||||
@ -465,22 +625,26 @@ function CareerExplorer() {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// ===================== Filter logic for jobZone, Fit =====================
|
||||
// ------------------------------------------------------
|
||||
// Filter logic for jobZone, Fit
|
||||
// ------------------------------------------------------
|
||||
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
|
||||
? career.job_zone !== null &&
|
||||
career.job_zone !== undefined &&
|
||||
Number(career.job_zone) === Number(selectedJobZone)
|
||||
: true;
|
||||
|
||||
// If user selected a fit, check if career.fit matches
|
||||
const fitMatches = selectedFit ? career.fit === selectedFit : true;
|
||||
|
||||
return jobZoneMatches && fitMatches;
|
||||
});
|
||||
}, [careersWithJobZone, selectedJobZone, selectedFit]);
|
||||
}, [careerSuggestions, selectedJobZone, selectedFit]);
|
||||
|
||||
// Weighted “match score” logic. (unchanged)
|
||||
// Weighted "match score" logic. (unchanged)
|
||||
const priorityWeight = (priority, response) => {
|
||||
const weightMap = {
|
||||
interests: {
|
||||
@ -516,6 +680,9 @@ function CareerExplorer() {
|
||||
return weightMap[priority][response] || 1;
|
||||
};
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Loading Overlay
|
||||
// ------------------------------------------------------
|
||||
const renderLoadingOverlay = () => {
|
||||
if (!loading) return null;
|
||||
return (
|
||||
@ -535,15 +702,20 @@ function CareerExplorer() {
|
||||
);
|
||||
};
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Render
|
||||
// ------------------------------------------------------
|
||||
return (
|
||||
<div className="career-explorer-container bg-white p-6 rounded shadow">
|
||||
{renderLoadingOverlay()}
|
||||
|
||||
{showModal && (
|
||||
<CareerPrioritiesModal
|
||||
userProfile={userProfile}
|
||||
onClose={() => setShowModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Explore Careers - use the tools below to find your perfect career
|
||||
@ -581,12 +753,30 @@ function CareerExplorer() {
|
||||
const balanceRating = ratings.balance || 3;
|
||||
const recognitionRating = ratings.recognition || 3;
|
||||
|
||||
const userInterestsWeight = priorityWeight('interests', priorities.interests || 'I’m not sure yet');
|
||||
const userMeaningWeight = priorityWeight('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 userInterestsWeight = priorityWeight(
|
||||
'interests',
|
||||
priorities.interests || 'I’m not sure yet'
|
||||
);
|
||||
const userMeaningWeight = priorityWeight(
|
||||
'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 =
|
||||
userInterestsWeight +
|
||||
@ -615,7 +805,9 @@ function CareerExplorer() {
|
||||
<td className="border p-2">{growthRating}</td>
|
||||
<td className="border p-2">{balanceRating}</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">
|
||||
<Button
|
||||
className="bg-red-600 text-black-500"
|
||||
@ -624,9 +816,11 @@ function CareerExplorer() {
|
||||
Remove
|
||||
</Button>
|
||||
|
||||
{/* New Button -> "Select for Education" */}
|
||||
<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)}
|
||||
>
|
||||
Plan your Education/Skills
|
||||
@ -667,18 +861,27 @@ function CareerExplorer() {
|
||||
</option>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Now we pass the *filteredCareers* into the CareerSuggestions component */}
|
||||
<CareerSuggestions
|
||||
careerSuggestions={filteredCareers}
|
||||
onCareerClick={(career) => {
|
||||
setSelectedCareer(career);
|
||||
handleCareerClick(career);
|
||||
}}
|
||||
setLoading={setLoading}
|
||||
setProgress={setProgress}
|
||||
userState={userState}
|
||||
areaTitle={areaTitle}
|
||||
/>
|
||||
|
||||
{selectedCareer && (
|
||||
@ -695,17 +898,29 @@ function CareerExplorer() {
|
||||
|
||||
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
||||
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
|
||||
</a>
|
||||
, 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
|
||||
</a>
|
||||
and
|
||||
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href="https://nces.ed.gov"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
NCES
|
||||
</a>
|
||||
|
@ -1,112 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import './Dashboard.css'; // or replace with Tailwind classes if desired
|
||||
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||
import React from 'react';
|
||||
import './Dashboard.css'; // or Tailwind classes
|
||||
|
||||
export function CareerSuggestions({
|
||||
careerSuggestions = [],
|
||||
userState,
|
||||
areaTitle,
|
||||
setLoading,
|
||||
setProgress,
|
||||
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 (
|
||||
<div className="career-suggestions-grid">
|
||||
{updatedCareers.map((career) => (
|
||||
{careerSuggestions.map((career) => (
|
||||
<button
|
||||
key={career.code}
|
||||
className={`career-button ${career.limitedData ? 'limited-data' : ''}`}
|
||||
|
@ -156,7 +156,7 @@ const InterestInventory = () => {
|
||||
const { careers: careerSuggestions, riaSecScores } = data;
|
||||
|
||||
if (Array.isArray(careerSuggestions) && Array.isArray(riaSecScores)) {
|
||||
navigate('/career-explorer', { state: { careerSuggestions, riaSecScores } });
|
||||
navigate('/career-explorer', { state: { careerSuggestions, riaSecScores, fromInterestInventory: true } });
|
||||
} else {
|
||||
throw new Error('Invalid data format from the server.');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user