Able to pass selected career from Explorer to EducationalProgramsPage.

This commit is contained in:
Josh 2025-05-16 17:00:15 +00:00
parent 77cd3b6845
commit 3b4c44c088
2 changed files with 437 additions and 392 deletions

View File

@ -17,7 +17,6 @@ import SignIn from './components/SignIn.js';
import SignUp from './components/SignUp.js'; import SignUp from './components/SignUp.js';
import PlanningLanding from './components/PlanningLanding.js'; import PlanningLanding from './components/PlanningLanding.js';
import CareerExplorer from './components/CareerExplorer.js'; import CareerExplorer from './components/CareerExplorer.js';
import EducationalPrograms from './components/EducationalPrograms.js';
import PreparingLanding from './components/PreparingLanding.js'; import PreparingLanding from './components/PreparingLanding.js';
import EducationalProgramsPage from './components/EducationalProgramsPage.js'; import EducationalProgramsPage from './components/EducationalProgramsPage.js';
import EnhancingLanding from './components/EnhancingLanding.js'; import EnhancingLanding from './components/EnhancingLanding.js';
@ -126,6 +125,11 @@ function App() {
Career Explorer Career Explorer
</Link> </Link>
</li> </li>
<li>
<Link className="text-blue-600 hover:text-blue-800" to="/educational-programs">
Skills/Educational Planner
</Link>
</li>
<li> <li>
<Link className="text-blue-600 hover:text-blue-800" to="/interest-inventory"> <Link className="text-blue-600 hover:text-blue-800" to="/interest-inventory">
Interest Inventory Interest Inventory

View File

@ -1,73 +1,57 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import CareerSuggestions from './CareerSuggestions.js'; import CareerSuggestions from './CareerSuggestions.js';
import CareerPrioritiesModal from './CareerPrioritiesModal.js'; import CareerPrioritiesModal from './CareerPrioritiesModal.js';
import CareerModal from './CareerModal.js'; import CareerModal from './CareerModal.js';
import CareerSearch from './CareerSearch.js'; 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: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
{ name: 'Alaska', code: 'AK' }, { name: 'Arkansas', code: 'AR' }, { name: 'California', code: 'CA' }, { name: 'Colorado', code: 'CO' },
{ name: 'Arizona', code: 'AZ' }, { name: 'Connecticut', code: 'CT' }, { name: 'Delaware', code: 'DE' }, { name: 'District of Columbia', code: 'DC' },
{ name: 'Arkansas', code: 'AR' }, { name: 'Florida', code: 'FL' }, { name: 'Georgia', code: 'GA' }, { name: 'Hawaii', code: 'HI' },
{ name: 'California', code: 'CA' }, { name: 'Idaho', code: 'ID' }, { name: 'Illinois', code: 'IL' }, { name: 'Indiana', code: 'IN' },
{ name: 'Colorado', code: 'CO' }, { name: 'Iowa', code: 'IA' }, { name: 'Kansas', code: 'KS' }, { name: 'Kentucky', code: 'KY' },
{ name: 'Connecticut', code: 'CT' }, { name: 'Louisiana', code: 'LA' }, { name: 'Maine', code: 'ME' }, { name: 'Maryland', code: 'MD' },
{ name: 'Delaware', code: 'DE' }, { name: 'Massachusetts', code: 'MA' }, { name: 'Michigan', code: 'MI' }, { name: 'Minnesota', code: 'MN' },
{ name: 'District of Columbia', code: 'DC' }, { name: 'Mississippi', code: 'MS' }, { name: 'Missouri', code: 'MO' }, { name: 'Montana', code: 'MT' },
{ name: 'Florida', code: 'FL' }, { name: 'Nebraska', code: 'NE' }, { name: 'Nevada', code: 'NV' }, { name: 'New Hampshire', code: 'NH' },
{ name: 'Georgia', code: 'GA' }, { name: 'New Jersey', code: 'NJ' }, { name: 'New Mexico', code: 'NM' }, { name: 'New York', code: 'NY' },
{ name: 'Hawaii', code: 'HI' }, { name: 'North Carolina', code: 'NC' }, { name: 'North Dakota', code: 'ND' }, { name: 'Ohio', code: 'OH' },
{ name: 'Idaho', code: 'ID' }, { name: 'Oklahoma', code: 'OK' }, { name: 'Oregon', code: 'OR' }, { name: 'Pennsylvania', code: 'PA' },
{ name: 'Illinois', code: 'IL' }, { name: 'Rhode Island', code: 'RI' }, { name: 'South Carolina', code: 'SC' }, { name: 'South Dakota', code: 'SD' },
{ name: 'Indiana', code: 'IN' }, { name: 'Tennessee', code: 'TN' }, { name: 'Texas', code: 'TX' }, { name: 'Utah', code: 'UT' },
{ name: 'Iowa', code: 'IA' }, { name: 'Vermont', code: 'VT' }, { name: 'Virginia', code: 'VA' }, { name: 'Washington', code: 'WA' },
{ name: 'Kansas', code: 'KS' }, { name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' },
{ name: 'Kentucky', code: 'KY' }, ];
{ name: 'Louisiana', code: 'LA' },
{ name: 'Maine', code: 'ME' }, // -------------- CIP HELPER FUNCTIONS --------------
{ name: 'Maryland', code: 'MD' },
{ name: 'Massachusetts', code: 'MA' }, // 1) Insert leading zero if there's only 1 digit before the decimal
{ name: 'Michigan', code: 'MI' }, function ensureTwoDigitsBeforeDecimal(cipStr) {
{ name: 'Minnesota', code: 'MN' }, // e.g. "4.0201" => "04.0201"
{ name: 'Mississippi', code: 'MS' }, return cipStr.replace(/^(\d)\./, '0$1.');
{ name: 'Missouri', code: 'MO' }, }
{ name: 'Montana', code: 'MT' },
{ name: 'Nebraska', code: 'NE' }, // 2) Clean an array of CIP codes, e.g. ["4.0201", "14.0901"] => ["0402", "1409"]
{ name: 'Nevada', code: 'NV' }, function cleanCipCodes(cipArray) {
{ name: 'New Hampshire', code: 'NH' }, return cipArray.map((code) => {
{ name: 'New Jersey', code: 'NJ' }, let codeStr = code.toString();
{ name: 'New Mexico', code: 'NM' }, codeStr = ensureTwoDigitsBeforeDecimal(codeStr); // ensure "04.0201"
{ name: 'New York', code: 'NY' }, return codeStr.replace('.', '').slice(0, 4); // => "040201" => "0402"
{ 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 getFullStateName(code) { function getFullStateName(code) {
const found = STATES.find((s) => s.code === code?.toUpperCase()); const found = STATES.find((s) => s.code === code?.toUpperCase());
return found ? found.name : ''; return found ? found.name : '';
} }
function CareerExplorer({ }) { function CareerExplorer() {
const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const apiUrl = process.env.REACT_APP_API_URL || ''; const apiUrl = process.env.REACT_APP_API_URL || '';
@ -105,242 +89,243 @@ function CareerExplorer({ }) {
Good: 'Good - Less Strong Match', Good: 'Good - Less Strong Match',
}; };
// ===================== Load user profile =====================
useEffect(() => { useEffect(() => {
setLoading(true); 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 fetchUserProfile = async () => {
const profileData = res.data; try {
console.log('[fetchUserProfile] loaded profileData =>', profileData); const token = localStorage.getItem('token');
const res = await axios.get(`${apiUrl}/user-profile`, {
headers: { Authorization: `Bearer ${token}` },
});
// 1) Set userProfile and all relevant states using `profileData`: if (res.status === 200) {
setUserProfile(profileData); const profileData = res.data;
setUserState(profileData.state); console.log('[fetchUserProfile] loaded profileData =>', profileData);
setAreaTitle(profileData.area);
setUserZipcode(profileData.zipcode);
// 2) Load saved career list if it exists setUserProfile(profileData);
if (profileData.career_list) { setUserState(profileData.state);
setCareerList(JSON.parse(profileData.career_list)); setAreaTitle(profileData.area);
} setUserZipcode(profileData.zipcode);
// 3) If user has interest inventory answers, fetch suggestions // If they have a saved career list
if (profileData.interest_inventory_answers) { if (profileData.career_list) {
const answers = profileData.interest_inventory_answers; setCareerList(JSON.parse(profileData.career_list));
const careerSuggestionsRes = await axios.post(`${apiUrl}/onet/submit_answers`, { }
answers,
state: profileData.state,
area: profileData.area,
});
const { careers = [] } = careerSuggestionsRes.data || {}; // If they have interest inventory, fetch suggestions
setCareerSuggestions(careers.flat()); 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 {
// 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); setShowModal(true);
} }
} else { } catch (err) {
// Not a 200 response => fallback console.error('Error fetching user profile:', err);
setShowModal(true); setShowModal(true);
setLoading(false);
} }
} catch (err) { };
console.error('Error fetching user profile:', err);
setShowModal(true); // fallback if error
setLoading(false);
}
};
fetchUserProfile(); fetchUserProfile();
}, [apiUrl]); }, [apiUrl]);
// ===================== If location.state has careerSuggestions =====================
useEffect(() => { useEffect(() => {
if (location.state?.careerSuggestions) { if (location.state?.careerSuggestions) {
setCareerSuggestions(location.state.careerSuggestions); setCareerSuggestions(location.state.careerSuggestions);
} }
}, [location.state]); }, [location.state]);
// Fetch Job Zones if suggestions are provided // ===================== Fetch job zones for suggestions =====================
useEffect(() => { useEffect(() => {
const fetchJobZones = async () => { const fetchJobZones = async () => {
if (!careerSuggestions.length) return; if (!careerSuggestions.length) return;
const flatSuggestions = careerSuggestions.flat(); const flatSuggestions = careerSuggestions.flat();
const socCodes = flatSuggestions.map((career) => career.code); const socCodes = flatSuggestions.map((career) => career.code);
try { try {
const response = await axios.post(`${apiUrl}/job-zones`, { socCodes }); const response = await axios.post(`${apiUrl}/job-zones`, { socCodes });
const jobZoneData = response.data; const jobZoneData = response.data;
const updatedCareers = flatSuggestions.map((career) => ({ const updatedCareers = flatSuggestions.map((career) => ({
...career, ...career,
job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null, job_zone: jobZoneData[career.code.slice(0, -3)]?.job_zone || null,
})); }));
// IMPORTANT: Ensure this actually sets a new array setCareersWithJobZone([...updatedCareers]);
setCareersWithJobZone([...updatedCareers]); } catch (error) {
} catch (error) { console.error('Error fetching job zone information:', error);
console.error('Error fetching job zone information:', error); }
} };
};
fetchJobZones(); fetchJobZones();
}, [careerSuggestions, apiUrl]); }, [careerSuggestions, apiUrl]);
const handleCareerClick = useCallback( // ===================== handleCareerClick (detail fetch) =====================
async (career) => { const handleCareerClick = useCallback(
console.log('[handleCareerClick] career =>', career); async (career) => {
const socCode = career.code; console.log('[handleCareerClick] career =>', career);
setSelectedCareer(career); const socCode = career.code;
setSelectedCareer(career);
setError(null); setError(null);
setCareerDetails(null); setCareerDetails(null);
setSalaryData([]); setSalaryData([]);
setEconomicProjections({}); setEconomicProjections({});
// We can set selectedCareer immediately so that our Modal condition is met. setSelectedCareer(career);
setSelectedCareer(career);
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
setLoading(false);
return;
}
try { if (!socCode) {
// CIP fetch console.error('SOC Code is missing');
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`); setError('SOC Code is missing');
if (!cipResponse.ok) {setError(`We're sorry, but specific details for "${career.title}" are not available at this time.`); setLoading(false);
setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`}); return;
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 { try {
economicResponse = await axios.get(`${apiUrl}/projections/${socCode.split('.')[0]}`, { // CIP fetch
params: { state: fullStateName }, const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
}); if (!cipResponse.ok) {
} catch (error) { setError(
economicResponse = { data: {} }; `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);
// Build final details // Job details
const updatedCareerDetails = { const jobDetailsResponse = await fetch(`${apiUrl}/onet/career-description/${socCode}`);
...career, if (!jobDetailsResponse.ok){
jobDescription: description, setCareerDetails({ error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`});
tasks, setLoading(false);
salaryData: salaryDataPoints, return;
economicProjections: economicResponse.data || {}, }
}; const { description, tasks } = await jobDetailsResponse.json();
// Now set the fully fetched data // Salary
setCareerDetails(updatedCareerDetails); let salaryResponse;
try {
salaryResponse = await axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) {
salaryResponse = { data: {} };
}
} catch (error) { // Build salary array
console.error('Error processing career click:', error.message); const sData = salaryResponse.data || {};
setCareerDetails({ const salaryDataPoints =
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.` sData && Object.keys(sData).length > 0
}); ? [
} finally { {
setLoading(false); percentile: '10th Percentile',
} regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
}, nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
[userState, apiUrl, areaTitle, userZipcode] },
); {
percentile: '25th Percentile',
// ============= Let typed careers open PopoutPanel ============= regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
const handleCareerFromSearch = useCallback( nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
(obj) => { },
const adapted = { {
code: obj.soc_code, percentile: 'Median',
title: obj.title, regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
cipCode: obj.cip_code, 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 || {},
}; };
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
handleCareerClick(adapted);
},
[handleCareerClick]
);
useEffect(() => {
if (pendingCareerForModal) {
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
handleCareerFromSearch(pendingCareerForModal);
setPendingCareerForModal(null);
}
}, [pendingCareerForModal, handleCareerFromSearch]);
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(() => { useEffect(() => {
fetch('/careers_with_ratings.json') fetch('/careers_with_ratings.json')
.then((res) => { .then((res) => {
@ -350,7 +335,7 @@ function CareerExplorer({ }) {
.then((data) => setMasterCareerRatings(data)) .then((data) => setMasterCareerRatings(data))
.catch((err) => console.error('Error fetching career ratings:', err)); .catch((err) => console.error('Error fetching career ratings:', err));
}, []); }, []);
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]);
@ -358,103 +343,129 @@ function CareerExplorer({ }) {
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 =====================
const saveCareerListToBackend = async (newCareerList) => { const saveCareerListToBackend = async (newCareerList) => {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
await axios.post(`${apiUrl}/user-profile`, { await axios.post(
`${apiUrl}/user-profile`,
firstName: userProfile?.firstname, {
lastName: userProfile?.lastname, firstName: userProfile?.firstname,
email: userProfile?.email, lastName: userProfile?.lastname,
zipCode: userProfile?.zipcode, email: userProfile?.email,
state: userProfile?.state, zipCode: userProfile?.zipcode,
area: userProfile?.area, state: userProfile?.state,
careerSituation: userProfile?.career_situation, area: userProfile?.area,
careerSituation: userProfile?.career_situation,
// For the rest, ensure we're not overwriting if not needed interest_inventory_answers: userProfile?.interest_inventory_answers,
interest_inventory_answers: userProfile?.interest_inventory_answers, career_priorities: userProfile?.career_priorities,
career_priorities: userProfile?.career_priorities, career_list: JSON.stringify(newCareerList),
},
// IMPORTANT: We convert the newCareerList to JSON if your DB expects text {
career_list: JSON.stringify(newCareerList), headers: { Authorization: `Bearer ${token}` },
}, { }
headers: { Authorization: `Bearer ${token}` }, );
}); } catch (err) {
} catch (err) { console.error('Error saving career_list:', err);
console.error('Error saving career_list:', err);
// optional: show a user-friendly error
}
};
const addCareerToList = (career) => {
const masterRatings = getCareerRatingsBySocCode(career.code);
const fitRatingMap = {
Best: 5,
Great: 4,
Good: 3,
};
const interestsRating =
priorities.interests === "Im 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]; // ===================== Add/Remove from comparison =====================
// Call the API to save const addCareerToList = (career) => {
saveCareerListToBackend(newList); const masterRatings = getCareerRatingsBySocCode(career.code);
return newList;
});
};
const fitRatingMap = {
Best: 5,
Great: 4,
Good: 3,
};
const interestsRating =
priorities.interests === "Im 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) => { const removeCareerFromList = (careerCode) => {
setCareerList((prevList) => { setCareerList((prevList) => {
const newList = prevList.filter((c) => c.code !== careerCode); const newList = prevList.filter((c) => c.code !== careerCode);
// Call the API to save saveCareerListToBackend(newList);
saveCareerListToBackend(newList); return 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;
// Filtering logic (Job Zone and Fit) // 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: {
cipCodes: cleanedCips,
careerTitle: career.title,
userZip: userZipcode,
userState: userState,
},
});
};
// ===================== Filter logic for jobZone, Fit =====================
const filteredCareers = useMemo(() => { const filteredCareers = useMemo(() => {
return careersWithJobZone.filter((career) => { return careersWithJobZone.filter((career) => {
const jobZoneMatches = selectedJobZone const jobZoneMatches = selectedJobZone
@ -468,6 +479,7 @@ function CareerExplorer({ }) {
}); });
}, [careersWithJobZone, selectedJobZone, selectedFit]); }, [careersWithJobZone, selectedJobZone, selectedFit]);
// Weighted “match score” logic. (unchanged)
const priorityWeight = (priority, response) => { const priorityWeight = (priority, response) => {
const weightMap = { const weightMap = {
interests: { interests: {
@ -502,28 +514,27 @@ function CareerExplorer({ }) {
}; };
return weightMap[priority][response] || 1; return weightMap[priority][response] || 1;
}; };
const renderLoadingOverlay = () => { const renderLoadingOverlay = () => {
if (!loading) return null; if (!loading) return null;
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
<div className="rounded bg-white p-6 shadow-lg"> <div className="rounded bg-white p-6 shadow-lg">
<div className="mb-2 w-full max-w-md rounded bg-gray-200"> <div className="mb-2 w-full max-w-md rounded bg-gray-200">
<div <div
className="h-2 rounded bg-blue-500 transition-all" className="h-2 rounded bg-blue-500 transition-all"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
/> />
</div> </div>
<p className="mt-1 text-center text-sm text-gray-600"> <p className="mt-1 text-center text-sm text-gray-600">
{progress}% Loading Career Suggestions... {progress}% Loading Career Suggestions...
</p> </p>
</div> </div>
</div> </div>
); );
}; };
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 && (
@ -533,14 +544,15 @@ function CareerExplorer({ }) {
/> />
)} )}
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Explore Careers - use the tools in this area to find your perfect career</h2> <h2 className="text-xl font-semibold">
Explore Careers - use the tools below to find your perfect career
</h2>
<CareerSearch <CareerSearch
onCareerSelected={(careerObj) => { onCareerSelected={(careerObj) => {
console.log('[Dashboard] onCareerSelected =>', careerObj); console.log('[Dashboard] onCareerSelected =>', careerObj);
// Set the "pendingCareerForModal" so our useEffect fires setPendingCareerForModal(careerObj);
setPendingCareerForModal(careerObj); }}
}} />
/>
</div> </div>
<h2 className="text-xl font-semibold mb-4">Career Comparison</h2> <h2 className="text-xl font-semibold mb-4">Career Comparison</h2>
@ -550,7 +562,9 @@ function CareerExplorer({ }) {
<tr> <tr>
<th className="border p-2">Career</th> <th className="border p-2">Career</th>
{priorityKeys.map((priority) => ( {priorityKeys.map((priority) => (
<th key={priority} className="border p-2 capitalize">{priority}</th> <th key={priority} className="border p-2 capitalize">
{priority}
</th>
))} ))}
<th className="border p-2">Match</th> <th className="border p-2">Match</th>
<th className="border p-2">Actions</th> <th className="border p-2">Actions</th>
@ -559,7 +573,7 @@ function CareerExplorer({ }) {
<tbody> <tbody>
{careerList.map((career) => { {careerList.map((career) => {
const ratings = career.ratings || {}; const ratings = career.ratings || {};
const interestsRating = ratings.interests || 3; // default to 3 if not rated via InterestInventory const interestsRating = ratings.interests || 3;
const meaningRating = ratings.meaning || 3; const meaningRating = ratings.meaning || 3;
const stabilityRating = ratings.stability || 3; const stabilityRating = ratings.stability || 3;
const growthRating = ratings.growth || 3; const growthRating = ratings.growth || 3;
@ -601,17 +615,30 @@ function CareerExplorer({ }) {
<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"> <td className="border p-2 space-x-2">
<Button className="bg-red-600 text-black-500" onClick={() => removeCareerFromList(career.code)}>Remove</Button> <Button
className="bg-red-600 text-black-500"
onClick={() => removeCareerFromList(career.code)}
>
Remove
</Button>
{/* New Button -> "Select for Education" */}
<Button
className="bg-green-600 text-white"
onClick={() => handleSelectForEducation(career)}
>
Search for Education
</Button>
</td> </td>
</tr> </tr>
); );
})} })}
</tbody> </tbody>
</table> </table>
) : <p>No careers added to comparison.</p>} ) : (
<p>No careers added to comparison.</p>
)}
<div className="flex gap-4 mb-4"> <div className="flex gap-4 mb-4">
<select <select
@ -621,7 +648,9 @@ function CareerExplorer({ }) {
> >
<option value="">All Preparation Levels</option> <option value="">All Preparation Levels</option>
{Object.entries(jobZoneLabels).map(([zone, label]) => ( {Object.entries(jobZoneLabels).map(([zone, label]) => (
<option key={zone} value={zone}>{label}</option> <option key={zone} value={zone}>
{label}
</option>
))} ))}
</select> </select>
@ -632,7 +661,9 @@ function CareerExplorer({ }) {
> >
<option value="">All Fit Levels</option> <option value="">All Fit Levels</option>
{Object.entries(fitLabels).map(([key, label]) => ( {Object.entries(fitLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option> <option key={key} value={key}>
{label}
</option>
))} ))}
</select> </select>
</div> </div>
@ -649,25 +680,35 @@ function CareerExplorer({ }) {
areaTitle={areaTitle} areaTitle={areaTitle}
/> />
{selectedCareer && ( {selectedCareer && (
<CareerModal <CareerModal
career={selectedCareer} career={selectedCareer}
careerDetails={careerDetails} careerDetails={careerDetails}
closeModal={() => { closeModal={() => {
setSelectedCareer(null); setSelectedCareer(null);
setCareerDetails(null); setCareerDetails(null);
}} }}
addCareerToList={addCareerToList} addCareerToList={addCareerToList}
/> />
)} )}
<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"> O*Net</a>, <a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer">
in partnership with {' '}
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a> O*Net
</a>
, in partnership with
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer">
{' '}
Bureau of Labor Statistics
</a>
and and
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer"> NCES</a>. <a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer">
{' '}
NCES
</a>
.
</div> </div>
</div> </div>
); );