import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import CareerSuggestions from './CareerSuggestions.js'; import CareerPrioritiesModal from './CareerPrioritiesModal.js'; import CareerModal from './CareerModal.js'; import CareerSearch from './CareerSearch.js'; import { Button } from './ui/button.js'; import axios from 'axios'; const STATES = [ { name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' }, { name: 'Arkansas', code: 'AR' }, { name: 'California', code: 'CA' }, { name: 'Colorado', code: 'CO' }, { name: 'Connecticut', code: 'CT' }, { name: 'Delaware', code: 'DE' }, { name: 'District of Columbia', code: 'DC' }, { name: 'Florida', code: 'FL' }, { name: 'Georgia', code: 'GA' }, { name: 'Hawaii', code: 'HI' }, { name: 'Idaho', code: 'ID' }, { name: 'Illinois', code: 'IL' }, { name: 'Indiana', code: 'IN' }, { name: 'Iowa', code: 'IA' }, { name: 'Kansas', code: 'KS' }, { name: 'Kentucky', code: 'KY' }, { name: 'Louisiana', code: 'LA' }, { name: 'Maine', code: 'ME' }, { name: 'Maryland', code: 'MD' }, { name: 'Massachusetts', code: 'MA' }, { name: 'Michigan', code: 'MI' }, { name: 'Minnesota', code: 'MN' }, { name: 'Mississippi', code: 'MS' }, { name: 'Missouri', code: 'MO' }, { name: 'Montana', code: 'MT' }, { name: 'Nebraska', code: 'NE' }, { name: 'Nevada', code: 'NV' }, { name: 'New Hampshire', code: 'NH' }, { name: 'New Jersey', code: 'NJ' }, { name: 'New Mexico', code: 'NM' }, { name: 'New York', code: 'NY' }, { name: 'North Carolina', code: 'NC' }, { name: 'North Dakota', code: 'ND' }, { name: 'Ohio', code: 'OH' }, { name: 'Oklahoma', code: 'OK' }, { name: 'Oregon', code: 'OR' }, { name: 'Pennsylvania', code: 'PA' }, { name: 'Rhode Island', code: 'RI' }, { name: 'South Carolina', code: 'SC' }, { name: 'South Dakota', code: 'SD' }, { name: 'Tennessee', code: 'TN' }, { name: 'Texas', code: 'TX' }, { name: 'Utah', code: 'UT' }, { name: 'Vermont', code: 'VT' }, { name: 'Virginia', code: 'VA' }, { name: 'Washington', code: 'WA' }, { name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' }, ]; // -------------- CIP HELPER FUNCTIONS -------------- // 1) Insert leading zero if there's only 1 digit before the decimal function ensureTwoDigitsBeforeDecimal(cipStr) { // e.g. "4.0201" => "04.0201" return cipStr.replace(/^(\d)\./, '0$1.'); } // 2) Clean an array of CIP codes, e.g. ["4.0201", "14.0901"] => ["0402", "1409"] function cleanCipCodes(cipArray) { return cipArray.map((code) => { let codeStr = code.toString(); codeStr = ensureTwoDigitsBeforeDecimal(codeStr); // ensure "04.0201" return codeStr.replace('.', '').slice(0, 4); // => "040201" => "0402" }); } function getFullStateName(code) { const found = STATES.find((s) => s.code === code?.toUpperCase()); return found ? found.name : ''; } function CareerExplorer() { const navigate = useNavigate(); const location = useLocation(); const apiUrl = process.env.REACT_APP_API_URL || ''; const [userProfile, setUserProfile] = useState(null); const [masterCareerRatings, setMasterCareerRatings] = useState([]); const [careerList, setCareerList] = useState([]); const [careerDetails, setCareerDetails] = useState(null); const [showModal, setShowModal] = useState(false); const [userState, setUserState] = useState(null); const [areaTitle, setAreaTitle] = useState(null); const [userZipcode, setUserZipcode] = useState(null); const [error, setError] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [careerSuggestions, setCareerSuggestions] = useState([]); const [careersWithJobZone, setCareersWithJobZone] = useState([]); const [salaryData, setSalaryData] = useState([]); const [economicProjections, setEconomicProjections] = useState(null); const [selectedJobZone, setSelectedJobZone] = useState(''); const [selectedFit, setSelectedFit] = useState(''); const [selectedCareer, setSelectedCareer] = useState(null); const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); const jobZoneLabels = { '1': 'Little or No Preparation', '2': 'Some Preparation Needed', '3': 'Medium Preparation Needed', '4': 'Considerable Preparation Needed', '5': 'Extensive Preparation Needed', }; const fitLabels = { Best: 'Best - Very Strong Match', Great: 'Great - Strong Match', Good: 'Good - Less Strong Match', }; // ===================== Load user profile ===================== useEffect(() => { setLoading(true); const fetchUserProfile = async () => { try { const token = localStorage.getItem('token'); const res = await axios.get(`${apiUrl}/user-profile`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.status === 200) { const profileData = res.data; console.log('[fetchUserProfile] loaded profileData =>', profileData); 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); setLoading(false); } }; fetchUserProfile(); }, [apiUrl]); // ===================== If location.state has careerSuggestions ===================== useEffect(() => { if (location.state?.careerSuggestions) { setCareerSuggestions(location.state.careerSuggestions); } }, [location.state]); // ===================== 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) ===================== const handleCareerClick = useCallback( async (career) => { console.log('[handleCareerClick] career =>', career); const socCode = career.code; setSelectedCareer(career); setError(null); setCareerDetails(null); setSalaryData([]); setEconomicProjections({}); setSelectedCareer(career); if (!socCode) { console.error('SOC Code is missing'); setError('SOC Code is missing'); setLoading(false); return; } try { // CIP fetch const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); if (!cipResponse.ok) { 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.`}); setLoading(false); return; } const { cipCode } = await cipResponse.json(); const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); // Job details const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`); if (!jobDetailsResponse.ok){ setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`}); setLoading(false); return; } const { description, tasks } = await jobDetailsResponse.json(); // Salary let salaryResponse; try { salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle }, }); } catch (error) { salaryResponse = { data: {} }; } // Build salary array const sData = salaryResponse.data || {}; const salaryDataPoints = sData && Object.keys(sData).length > 0 ? [ { percentile: '10th Percentile', regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0, nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0, }, { percentile: '25th Percentile', regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0, nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0, }, { percentile: 'Median', regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0, nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0, }, { percentile: '75th Percentile', regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0, nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0, }, { percentile: '90th Percentile', regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0, nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0, }, ] : []; // Economic const fullStateName = getFullStateName(userState); let economicResponse = { data: {} }; try { economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, { params: { state: fullStateName }, }); } catch (error) { economicResponse = { data: {} }; } // Build final details const updatedCareerDetails = { ...career, jobDescription: description, tasks, salaryData: salaryDataPoints, economicProjections: economicResponse.data || {}, }; setCareerDetails(updatedCareerDetails); } catch (error) { console.error('Error processing career click:', error.message); setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`, }); } finally { setLoading(false); } }, [userState, apiUrl, areaTitle, userZipcode] ); // ===================== handleCareerFromSearch ===================== const handleCareerFromSearch = useCallback( (obj) => { const adapted = { code: obj.soc_code, title: obj.title, cipCode: obj.cip_code, }; console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted); handleCareerClick(adapted); }, [handleCareerClick] ); useEffect(() => { if (pendingCareerForModal) { console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal); handleCareerFromSearch(pendingCareerForModal); setPendingCareerForModal(null); } }, [pendingCareerForModal, handleCareerFromSearch]); // ===================== Load careers_with_ratings for CIP arrays ===================== useEffect(() => { fetch('/careers_with_ratings.json') .then((res) => { if (!res.ok) throw new Error('Failed to fetch ratings JSON'); return res.json(); }) .then((data) => setMasterCareerRatings(data)) .catch((err) => console.error('Error fetching career ratings:', err)); }, []); const priorities = useMemo(() => { return userProfile?.career_priorities ? JSON.parse(userProfile.career_priorities) : {}; }, [userProfile]); const priorityKeys = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition']; const getCareerRatingsBySocCode = (socCode) => { return masterCareerRatings.find((c) => c.soc_code === socCode)?.ratings || {}; }; // ===================== Save comparison list to backend ===================== const saveCareerListToBackend = async (newCareerList) => { try { const token = localStorage.getItem('token'); await axios.post( `${apiUrl}/user-profile`, { firstName: userProfile?.firstname, lastName: userProfile?.lastname, email: userProfile?.email, zipCode: userProfile?.zipcode, state: userProfile?.state, area: userProfile?.area, careerSituation: userProfile?.career_situation, interest_inventory_answers: userProfile?.interest_inventory_answers, career_priorities: userProfile?.career_priorities, career_list: JSON.stringify(newCareerList), }, { headers: { Authorization: `Bearer ${token}` }, } ); } catch (err) { console.error('Error saving career_list:', err); } }; // ===================== Add/Remove from comparison ===================== const addCareerToList = (career) => { const masterRatings = getCareerRatingsBySocCode(career.code); const fitRatingMap = { Best: 5, Great: 4, Good: 3, }; const interestsRating = priorities.interests === "I’m not sure yet" ? parseInt(prompt("Rate your interest in this career (1-5):", "3"), 10) : 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"), 10 ); const stabilityRating = career.ratings && career.ratings.stability !== undefined ? career.ratings.stability : masterRatings.stability || 3; const growthRating = masterRatings.growth || 3; const balanceRating = masterRatings.balance || 3; const recognitionRating = masterRatings.recognition || 3; const careerWithUserRatings = { ...career, ratings: { interests: interestsRating, meaning: meaningRating, stability: stabilityRating, growth: growthRating, balance: balanceRating, recognition: recognitionRating, }, }; setCareerList((prevList) => { if (prevList.some((c) => c.code === career.code)) { alert("Career already in comparison list."); return prevList; } const newList = [...prevList, careerWithUserRatings]; saveCareerListToBackend(newList); return newList; }); }; const removeCareerFromList = (careerCode) => { setCareerList((prevList) => { const newList = prevList.filter((c) => c.code !== careerCode); saveCareerListToBackend(newList); return newList; }); }; // ===================== Let user pick a career from comparison => "Select for Education" ===================== const handleSelectForEducation = (career) => { // 1) Confirm const confirmed = window.confirm( `Are you sure you want to move on to Educational Programs for ${career.title}?` ); if (!confirmed) return; // 2) Look up CIP codes from masterCareerRatings by SOC code const matching = masterCareerRatings.find((r) => r.soc_code === career.code); if (!matching) { alert(`No CIP codes found for ${career.title}.`); return; } // 3) Clean CIP codes const rawCips = matching.cip_codes || []; const cleanedCips = cleanCipCodes(rawCips); // from top-level function console.log('cleanedCips =>', cleanedCips); // 4) Navigate navigate('/educational-programs', { state: { socCode: career.code, cipCodes: cleanedCips, careerTitle: career.title, userZip: userZipcode, userState: userState, }, }); }; // ===================== Filter logic for jobZone, Fit ===================== const filteredCareers = useMemo(() => { return careersWithJobZone.filter((career) => { const jobZoneMatches = selectedJobZone ? career.job_zone !== null && career.job_zone !== undefined && Number(career.job_zone) === Number(selectedJobZone) : true; const fitMatches = selectedFit ? career.fit === selectedFit : true; return jobZoneMatches && fitMatches; }); }, [careersWithJobZone, selectedJobZone, selectedFit]); // Weighted “match score” logic. (unchanged) const priorityWeight = (priority, response) => { const weightMap = { interests: { 'I know my interests (completed inventory)': 5, 'I’m not sure yet': 1, }, meaning: { 'Yes, very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, stability: { 'Very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, growth: { 'Yes, very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, balance: { 'Yes, very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, recognition: { 'Very important': 5, 'Somewhat important': 3, 'Not as important': 1, }, }; return weightMap[priority][response] || 1; }; const renderLoadingOverlay = () => { if (!loading) return null; return (

{progress}% — Loading Career Suggestions...

); }; return (
{renderLoadingOverlay()} {showModal && ( setShowModal(false)} /> )}

Explore Careers - use the tools below to find your perfect career

{ console.log('[Dashboard] onCareerSelected =>', careerObj); setPendingCareerForModal(careerObj); }} />

Career Comparison

{careerList.length ? ( {priorityKeys.map((priority) => ( ))} {careerList.map((career) => { const ratings = career.ratings || {}; const interestsRating = ratings.interests || 3; const meaningRating = ratings.meaning || 3; const stabilityRating = ratings.stability || 3; const growthRating = ratings.growth || 3; 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 totalWeight = userInterestsWeight + userMeaningWeight + userStabilityWeight + userGrowthWeight + userBalanceWeight + userRecognitionWeight; const weightedScore = interestsRating * userInterestsWeight + meaningRating * userMeaningWeight + stabilityRating * userStabilityWeight + growthRating * userGrowthWeight + balanceRating * userBalanceWeight + recognitionRating * userRecognitionWeight; const matchScore = (weightedScore / (totalWeight * 5)) * 100; return ( ); })}
Career {priority} Match Actions
{career.title} {interestsRating} {meaningRating} {stabilityRating} {growthRating} {balanceRating} {recognitionRating} {matchScore.toFixed(1)}% {/* New Button -> "Select for Education" */}
) : (

No careers added to comparison.

)}
{ setSelectedCareer(career); handleCareerClick(career); }} setLoading={setLoading} setProgress={setProgress} userState={userState} areaTitle={areaTitle} /> {selectedCareer && ( { setSelectedCareer(null); setCareerDetails(null); }} addCareerToList={addCareerToList} /> )}
Career results and details provided by {' '} O*Net , in partnership with {' '} Bureau of Labor Statistics and {' '} NCES .
); } export default CareerExplorer;