Fixed loading for CareerExplorer, and no reload on filter

This commit is contained in:
Josh 2025-05-21 18:56:32 +00:00
parent 3932194079
commit a053da72e5
4 changed files with 361 additions and 244 deletions

View File

@ -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');

View File

@ -8,27 +8,27 @@ 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' },
{ name: 'Florida', code: 'FL' }, { name: 'Georgia', code: 'GA' }, { name: 'Hawaii', code: 'HI' }, { 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: 'Idaho', code: 'ID' }, { name: 'Illinois', code: 'IL' }, { name: 'Indiana', code: 'IN' },
{ name: 'Iowa', code: 'IA' }, { name: 'Kansas', code: 'KS' }, { name: 'Kentucky', code: 'KY' }, { 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: 'Louisiana', code: 'LA' }, { name: 'Maine', code: 'ME' }, { name: 'Maryland', code: 'MD' },
{ name: 'Massachusetts', code: 'MA' }, { name: 'Michigan', code: 'MI' }, { name: 'Minnesota', code: 'MN' }, { 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: '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: '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: '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: '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: '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: '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: '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(
params: { state: fullStateName }, `${apiUrl}/projections/${socCode.split('.')[0]}`,
}); {
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,8 +592,10 @@ function CareerExplorer() {
}); });
}; };
// ===================== Let user pick a career from comparison => "Select for Education" ===================== // ------------------------------------------------------
const handleSelectForEducation = (career) => { // "Select for Education" => navigate with CIP codes
// ------------------------------------------------------
const handleSelectForEducation = (career) => {
// 1) Confirm // 1) Confirm
const confirmed = window.confirm( const confirmed = window.confirm(
`Are you sure you want to move on to Educational Programs for ${career.title}?` `Are you sure you want to move on to Educational Programs for ${career.title}?`
@ -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 || 'Im not sure yet'); const userInterestsWeight = priorityWeight(
const userMeaningWeight = priorityWeight('meaning', priorities.meaning); 'interests',
const userStabilityWeight = priorityWeight('stability', priorities.stability); priorities.interests || 'Im 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,19 +861,28 @@ 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 && (
<CareerModal <CareerModal
@ -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>

View File

@ -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' : ''}`}

View File

@ -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.');
} }