diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js index 5303a32..9977509 100644 --- a/src/components/CareerExplorer.js +++ b/src/components/CareerExplorer.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import CareerSuggestions from './CareerSuggestions.js'; @@ -7,16 +7,84 @@ import CareerModal from './CareerModal.js'; import CareerSearch from './CareerSearch.js'; import axios from 'axios'; -function CareerExplorer({ handleCareerClick, userState, areaTitle }) { +const STATES = [ + { name: 'Alabama', code: 'AL' }, + { name: 'Alaska', code: 'AK' }, + { name: 'Arizona', code: 'AZ' }, + { name: 'Arkansas', code: 'AR' }, + { name: 'California', code: 'CA' }, + { name: 'Colorado', code: 'CO' }, + { name: 'Connecticut', code: 'CT' }, + { name: 'Delaware', code: 'DE' }, + { name: 'District of Columbia', code: 'DC' }, + { name: 'Florida', code: 'FL' }, + { name: 'Georgia', code: 'GA' }, + { name: 'Hawaii', code: 'HI' }, + { name: 'Idaho', code: 'ID' }, + { name: 'Illinois', code: 'IL' }, + { name: 'Indiana', code: 'IN' }, + { name: 'Iowa', code: 'IA' }, + { name: 'Kansas', code: 'KS' }, + { name: 'Kentucky', code: 'KY' }, + { name: 'Louisiana', code: 'LA' }, + { name: 'Maine', code: 'ME' }, + { name: 'Maryland', code: 'MD' }, + { name: 'Massachusetts', code: 'MA' }, + { name: 'Michigan', code: 'MI' }, + { name: 'Minnesota', code: 'MN' }, + { name: 'Mississippi', code: 'MS' }, + { name: 'Missouri', code: 'MO' }, + { name: 'Montana', code: 'MT' }, + { name: 'Nebraska', code: 'NE' }, + { name: 'Nevada', code: 'NV' }, + { name: 'New Hampshire', code: 'NH' }, + { name: 'New Jersey', code: 'NJ' }, + { name: 'New Mexico', code: 'NM' }, + { name: 'New York', code: 'NY' }, + { name: 'North Carolina', code: 'NC' }, + { name: 'North Dakota', code: 'ND' }, + { name: 'Ohio', code: 'OH' }, + { name: 'Oklahoma', code: 'OK' }, + { name: 'Oregon', code: 'OR' }, + { name: 'Pennsylvania', code: 'PA' }, + { name: 'Rhode Island', code: 'RI' }, + { name: 'South Carolina', code: 'SC' }, + { name: 'South Dakota', code: 'SD' }, + { name: 'Tennessee', code: 'TN' }, + { name: 'Texas', code: 'TX' }, + { name: 'Utah', code: 'UT' }, + { name: 'Vermont', code: 'VT' }, + { name: 'Virginia', code: 'VA' }, + { name: 'Washington', code: 'WA' }, + { name: 'West Virginia', code: 'WV' }, + { name: 'Wisconsin', code: 'WI' }, + { name: 'Wyoming', code: 'WY' }, +]; + +// 2) Helper to convert state code => full name +function getFullStateName(code) { + const found = STATES.find((s) => s.code === code?.toUpperCase()); + return found ? found.name : ''; +} + +function CareerExplorer({ }) { const location = useLocation(); const apiUrl = process.env.REACT_APP_API_URL || ''; const [userProfile, setUserProfile] = useState(null); const [masterCareerRatings, setMasterCareerRatings] = useState([]); const [careerList, setCareerList] = useState([]); + const [careerDetails, setCareerDetails] = useState(null); const [showModal, setShowModal] = useState(false); + const [userState, setUserState] = useState(null); + const [areaTitle, setAreaTitle] = useState(null); + const [userZipcode, setUserZipcode] = useState(null); + const [error, setError] = useState(null); + const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [careerSuggestions, setCareerSuggestions] = useState([]); const [careersWithJobZone, setCareersWithJobZone] = useState([]); + const [salaryData, setSalaryData] = useState([]); + const [economicProjections, setEconomicProjections] = useState(null); const [selectedJobZone, setSelectedJobZone] = useState(''); const [selectedFit, setSelectedFit] = useState(''); const [selectedCareer, setSelectedCareer] = useState(null); @@ -37,6 +105,235 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) { Good: 'Good - Less Strong Match', }; + useEffect(() => { + const fetchUserProfile = async () => { + try { + const token = localStorage.getItem('token'); + const res = await axios.get(`${apiUrl}/user-profile`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (res.status === 200) { + const profileData = res.data; + console.log('[fetchUserProfile] loaded profileData =>', profileData); + + // 1) Set userProfile and all relevant states using `profileData`: + setUserProfile(profileData); + setUserState(profileData.state); + setAreaTitle(profileData.area); + setUserZipcode(profileData.zipcode); + + // 2) Load saved career list if it exists + if (profileData.career_list) { + setCareerList(JSON.parse(profileData.career_list)); + } + + // 3) If user has interest inventory answers, fetch suggestions + if (profileData.interest_inventory_answers) { + const answers = profileData.interest_inventory_answers; + const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, { + answers, + state: profileData.state, + area: profileData.area, + }); + + const { careers = [] } = careerSuggestionsRes.data || {}; + setCareerSuggestions(careers.flat()); + } else { + // No inventory => no suggestions (or do something else here) + setCareerSuggestions([]); + } + + // 4) Check if all priorities are answered + const priorities = profileData.career_priorities + ? JSON.parse(profileData.career_priorities) + : {}; + + const allAnswered = ['interests','meaning','stability','growth','balance','recognition'] + .every((key) => priorities[key]); + + if (!allAnswered) { + // If user hasn't answered them all, show the priorities modal + setShowModal(true); + } + } else { + // Not a 200 response => fallback + setShowModal(true); + } + } catch (err) { + console.error('Error fetching user profile:', err); + setShowModal(true); // fallback if error + } + }; + + fetchUserProfile(); +}, [apiUrl]); + + // Load suggestions from Interest Inventory if provided (optional) + useEffect(() => { + if (location.state?.careerSuggestions) { + setCareerSuggestions(location.state.careerSuggestions); + } + }, [location.state]); + + // Fetch Job Zones if suggestions are provided + useEffect(() => { + const fetchJobZones = async () => { + if (!careerSuggestions.length) return; + + const flatSuggestions = careerSuggestions.flat(); + const socCodes = flatSuggestions.map((career) => career.code); + + try { + const response = await axios.post(`${apiUrl}/job-zones`, { socCodes }); + const jobZoneData = response.data; + const updatedCareers = flatSuggestions.map((career) => ({ + ...career, + job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, + })); + + // IMPORTANT: Ensure this actually sets a new array + setCareersWithJobZone([...updatedCareers]); + } catch (error) { + console.error('Error fetching job zone information:', error); + } + }; + + fetchJobZones(); +}, [careerSuggestions, apiUrl]); + + const handleCareerClick = useCallback( + async (career) => { + console.log('[handleCareerClick] career =>', career); + const socCode = career.code; + setSelectedCareer(career); + setLoading(true); + setError(null); + setCareerDetails({}); + setSalaryData([]); + setEconomicProjections({}); + setError(null); + setLoading(true); + + // We can set selectedCareer immediately so that our Modal condition is met. + setSelectedCareer(career); + + if (!socCode) { + console.error('SOC Code is missing'); + setError('SOC Code is missing'); + setLoading(false); + return; + } + + try { + // CIP fetch + const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); + if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); + const { cipCode } = await cipResponse.json(); + const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); + + // Job details + const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`); + if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description'); + const { description, tasks } = await jobDetailsResponse.json(); + + // Salary + let salaryResponse; + try { + salaryResponse = await axios.get(`${apiUrl}/salary`, { + params: { socCode: socCode.split('.')[0], area: areaTitle }, + }); + } catch (error) { + salaryResponse = { data: {} }; + } + + // Build salary array + const sData = salaryResponse.data || {}; + const salaryDataPoints = sData && Object.keys(sData).length > 0 + ? [ + { + percentile: '10th Percentile', + regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0, + nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0, + }, + { + percentile: '25th Percentile', + regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0, + nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0, + }, + { + percentile: 'Median', + regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0, + nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0, + }, + { + percentile: '75th Percentile', + regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0, + nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0, + }, + { + percentile: '90th Percentile', + regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0, + nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0, + }, + ] + : []; + + // Economic + const fullStateName = getFullStateName(userState); + let economicResponse = { data: {} }; + try { + economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, { + params: { state: fullStateName }, + }); + } catch (error) { + economicResponse = { data: {} }; + } + + // Build final details + const updatedCareerDetails = { + ...career, + jobDescription: description, + tasks, + salaryData: salaryDataPoints, + economicProjections: economicResponse.data || {}, + }; + + // Now set the fully fetched data + setCareerDetails(updatedCareerDetails); + + } catch (error) { + console.error('Error processing career click:', error.message); + setError('Failed to load data'); + } finally { + setLoading(false); + } + }, + [userState, apiUrl, areaTitle, userZipcode] +); + + // ============= Let typed careers open PopoutPanel ============= + const handleCareerFromSearch = useCallback( + (obj) => { + const adapted = { + code: obj.soc_code, + title: obj.title, + cipCode: obj.cip_code, + }; + console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted); + handleCareerClick(adapted); + }, + [handleCareerClick] + ); + + useEffect(() => { + if (pendingCareerForModal) { + console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal); + handleCareerFromSearch(pendingCareerForModal); + setPendingCareerForModal(null); + } + }, [pendingCareerForModal, handleCareerFromSearch]); + useEffect(() => { fetch('/careers_with_ratings.json') .then((res) => { @@ -61,8 +358,7 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) { try { const token = localStorage.getItem('token'); await axios.post(`${apiUrl}/user-profile`, { - // Provide all required fields from userProfile - // If your DB requires them, fill them in here: + firstName: userProfile?.firstname, lastName: userProfile?.lastname, email: userProfile?.email, @@ -150,97 +446,7 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) { }); }; - useEffect(() => { - const fetchUserProfile = async () => { - try { - const token = localStorage.getItem('token'); - const res = await axios.get(`${apiUrl}/user-profile`, { - headers: { Authorization: `Bearer ${token}` }, - }); - - if (res.status === 200) { - const profileData = res.data; - setUserProfile(profileData); - - // Explicitly set careerList from saved data - if (profileData.career_list) { - setCareerList(JSON.parse(profileData.career_list)); - } - - // Check explicitly for Interest Inventory answers - if (profileData.interest_inventory_answers) { - const answers = profileData.interest_inventory_answers; - const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, { - answers, - state: profileData.state, - area: profileData.area, - }); - - // Destructure `careers` from the server response - const { careers = [] } = careerSuggestionsRes.data || {}; - - // Flatten in case it's a nested array (or just a no-op if already flat) - setCareerSuggestions(careers.flat()); - } else { - // No interest inventory answers: fallback to an empty list - setCareerSuggestions([]); - } - - const priorities = profileData.career_priorities - ? JSON.parse(profileData.career_priorities) - : {}; - - const allAnswered = ['interests', 'meaning', 'stability', 'growth', 'balance', 'recognition'].every( - (key) => priorities[key] - ); - - if (!allAnswered) { - setShowModal(true); - } - } else { - setShowModal(true); - } - } catch (err) { - console.error('Error fetching user profile:', err); - setShowModal(true); - } - }; - - fetchUserProfile(); -}, [apiUrl]); - // Load suggestions from Interest Inventory if provided (optional) - useEffect(() => { - if (location.state?.careerSuggestions) { - setCareerSuggestions(location.state.careerSuggestions); - } - }, [location.state]); - - // Fetch Job Zones if suggestions are provided - useEffect(() => { - const fetchJobZones = async () => { - if (!careerSuggestions.length) return; - - const flatSuggestions = careerSuggestions.flat(); - const socCodes = flatSuggestions.map((career) => career.code); - - try { - const response = await axios.post(`${apiUrl}/job-zones`, { socCodes }); - const jobZoneData = response.data; - const updatedCareers = flatSuggestions.map((career) => ({ - ...career, - job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, - })); - - // IMPORTANT: Ensure this actually sets a new array - setCareersWithJobZone([...updatedCareers]); - } catch (error) { - console.error('Error fetching job zone information:', error); - } - }; - - fetchJobZones(); -}, [careerSuggestions, apiUrl]); // Filtering logic (Job Zone and Fit) const filteredCareers = useMemo(() => { @@ -432,16 +638,17 @@ function CareerExplorer({ handleCareerClick, userState, areaTitle }) { areaTitle={areaTitle} /> - {selectedCareer && ( - setSelectedCareer(null)} - userState={userState} - areaTitle={areaTitle} - userZipcode={userProfile?.zipcode} - addCareerToList={addCareerToList} // <-- explicitly added here - /> - )} + {selectedCareer && ( + { + setSelectedCareer(null); + setCareerDetails(null); + }} + addCareerToList={addCareerToList} + /> + )}
Career results and details provided by diff --git a/src/components/CareerModal.js b/src/components/CareerModal.js index f052aba..cb08ec2 100644 --- a/src/components/CareerModal.js +++ b/src/components/CareerModal.js @@ -1,129 +1,18 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; -import { fetchSchools, clientGeocodeZip, haversineDistance} from '../utils/apiUtils.js'; const apiUrl = process.env.REACT_APP_API_URL; -const STATES = [ - { name: 'Alabama', code: 'AL' }, - { name: 'Alaska', code: 'AK' }, - { name: 'Arizona', code: 'AZ' }, - { name: 'Arkansas', code: 'AR' }, - { name: 'California', code: 'CA' }, - { name: 'Colorado', code: 'CO' }, - { name: 'Connecticut', code: 'CT' }, - { name: 'Delaware', code: 'DE' }, - { name: 'District of Columbia', code: 'DC' }, - { name: 'Florida', code: 'FL' }, - { name: 'Georgia', code: 'GA' }, - { name: 'Hawaii', code: 'HI' }, - { name: 'Idaho', code: 'ID' }, - { name: 'Illinois', code: 'IL' }, - { name: 'Indiana', code: 'IN' }, - { name: 'Iowa', code: 'IA' }, - { name: 'Kansas', code: 'KS' }, - { name: 'Kentucky', code: 'KY' }, - { name: 'Louisiana', code: 'LA' }, - { name: 'Maine', code: 'ME' }, - { name: 'Maryland', code: 'MD' }, - { name: 'Massachusetts', code: 'MA' }, - { name: 'Michigan', code: 'MI' }, - { name: 'Minnesota', code: 'MN' }, - { name: 'Mississippi', code: 'MS' }, - { name: 'Missouri', code: 'MO' }, - { name: 'Montana', code: 'MT' }, - { name: 'Nebraska', code: 'NE' }, - { name: 'Nevada', code: 'NV' }, - { name: 'New Hampshire', code: 'NH' }, - { name: 'New Jersey', code: 'NJ' }, - { name: 'New Mexico', code: 'NM' }, - { name: 'New York', code: 'NY' }, - { name: 'North Carolina', code: 'NC' }, - { name: 'North Dakota', code: 'ND' }, - { name: 'Ohio', code: 'OH' }, - { name: 'Oklahoma', code: 'OK' }, - { name: 'Oregon', code: 'OR' }, - { name: 'Pennsylvania', code: 'PA' }, - { name: 'Rhode Island', code: 'RI' }, - { name: 'South Carolina', code: 'SC' }, - { name: 'South Dakota', code: 'SD' }, - { name: 'Tennessee', code: 'TN' }, - { name: 'Texas', code: 'TX' }, - { name: 'Utah', code: 'UT' }, - { name: 'Vermont', code: 'VT' }, - { name: 'Virginia', code: 'VA' }, - { name: 'Washington', code: 'WA' }, - { name: 'West Virginia', code: 'WV' }, - { name: 'Wisconsin', code: 'WI' }, - { name: 'Wyoming', code: 'WY' }, -]; +function CareerModal({ career, careerDetails, userState, areaTitle, userZipcode, closeModal, addCareerToList }) { -// 2) Helper to convert state code => full name -function getFullStateName(code) { - const found = STATES.find((s) => s.code === code?.toUpperCase()); - return found ? found.name : ''; -} - -function CareerModal({ career, userState, areaTitle, userZipcode, closeModal, addCareerToList }) { - const [careerDetails, setCareerDetails] = useState(null); - const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - const handleCareerClick = async () => { - const socCode = career.code; - setLoading(true); - setError(null); - - try { - const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); - if (!cipResponse.ok) throw new Error('Failed to fetch CIP Code'); - const { cipCode } = await cipResponse.json(); - const cleanedCipCode = cipCode.replace('.', '').slice(0, 4); - - const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`); - if (!jobDetailsResponse.ok) throw new Error('Failed to fetch job description'); - const { description, tasks } = await jobDetailsResponse.json(); - - const salaryResponse = await axios.get(`${apiUrl}/salary`, { - params: { socCode: socCode.split('.')[0], area: areaTitle }, - }).catch(() => ({ data: {} })); - - const economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, { - params: { state: getFullStateName(userState) }, - }).catch(() => ({ data: {} })); - - - - const sData = salaryResponse.data || {}; - const salaryDataPoints = sData && Object.keys(sData).length > 0 ? [ - { percentile: '10th', regionalSalary: sData.regional?.regional_PCT10 || 0, nationalSalary: sData.national?.national_PCT10 || 0 }, - { percentile: '25th', regionalSalary: sData.regional?.regional_PCT25 || 0, nationalSalary: sData.national?.national_PCT25 || 0 }, - { percentile: 'Median', regionalSalary: sData.regional?.regional_MEDIAN || 0, nationalSalary: sData.national?.national_MEDIAN || 0 }, - { percentile: '75th', regionalSalary: sData.regional?.regional_PCT75 || 0, nationalSalary: sData.national?.national_PCT75 || 0 }, - { percentile: '90th', regionalSalary: sData.regional?.regional_PCT90 || 0, nationalSalary: sData.national?.national_PCT90 || 0 }, - ] : []; - - setCareerDetails({ - ...career, - jobDescription: description, - tasks, - economicProjections: economicResponse.data || {}, - salaryData: salaryDataPoints, - }); - } catch (error) { - console.error(error); - setError('Failed to load career details.'); - } finally { - setLoading(false); - } - }; - - handleCareerClick(); - }, [career, userState, areaTitle, userZipcode]); - - if (loading) return
Loading...
; - if (error) return
{error}
; + console.log('CareerModal props:', { career, careerDetails, userState, areaTitle, userZipcode }); + + if (!careerDetails?.salaryData) { + return
Loading career details...
; + } + if (error) return
{error}
; const calculateStabilityRating = (salaryData) => { const medianSalaryObj = salaryData.find(s => s.percentile === 'Median'); diff --git a/user_profile.db b/user_profile.db index 4125014..fbd2469 100644 Binary files a/user_profile.db and b/user_profile.db differ