Fixed navigation issue in Dashboard where it reverted to INterest Inventory

This commit is contained in:
Josh 2025-05-02 16:41:33 +00:00
parent 34fda5760d
commit 54d0fcb4e6
2 changed files with 234 additions and 148 deletions

View File

@ -3,13 +3,21 @@
import axios from 'axios'; import axios from 'axios';
import React, { useMemo, useState, useCallback, useEffect } from 'react'; import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { CareerSuggestions } from './CareerSuggestions.js'; import { CareerSuggestions } from './CareerSuggestions.js';
import PopoutPanel from './PopoutPanel.js'; import PopoutPanel from './PopoutPanel.js';
import MilestoneTracker from './MilestoneTracker.js'; import MilestoneTracker from './MilestoneTracker.js';
import CareerSearch from './CareerSearch.js'; // <--- Import your new search import CareerSearch from './CareerSearch.js'; // <--- Import your new search
import Chatbot from "./Chatbot.js"; import Chatbot from './Chatbot.js';
import './Dashboard.css'; import './Dashboard.css';
import { Bar } from 'react-chartjs-2'; import { Bar } from 'react-chartjs-2';
@ -51,12 +59,10 @@ function Dashboard() {
const [sessionHandled, setSessionHandled] = useState(false); const [sessionHandled, setSessionHandled] = useState(false);
// ============= NEW State ============= // ============= NEW State =============
// Holds the full career object { title, soc_code, cip_code } typed in CareerSearch
const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
// We'll treat "loading" as "loadingSuggestions" // We'll treat "loading" as "loadingSuggestions"
const loadingSuggestions = loading; const loadingSuggestions = loading;
// We'll consider the popout panel visible if there's a selectedCareer
const popoutVisible = !!selectedCareer; const popoutVisible = !!selectedCareer;
// ============= Auth & URL Setup ============= // ============= Auth & URL Setup =============
@ -64,9 +70,9 @@ function Dashboard() {
// AUTH fetch // AUTH fetch
const authFetch = async (url, options = {}, onUnauthorized) => { const authFetch = async (url, options = {}, onUnauthorized) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem('token');
if (!token) { if (!token) {
console.log("Token is missing, triggering session expired modal."); console.log('Token is missing, triggering session expired modal.');
if (typeof onUnauthorized === 'function') onUnauthorized(); if (typeof onUnauthorized === 'function') onUnauthorized();
return null; return null;
} }
@ -79,21 +85,21 @@ function Dashboard() {
}; };
try { try {
const res = await fetch(url, finalOptions); const res = await fetch(url, finalOptions);
console.log("Response Status:", res.status); console.log('Response Status:', res.status);
if (res.status === 401 || res.status === 403) { if (res.status === 401 || res.status === 403) {
console.log("Session expired, triggering session expired modal."); console.log('Session expired, triggering session expired modal.');
if (typeof onUnauthorized === 'function') onUnauthorized(); if (typeof onUnauthorized === 'function') onUnauthorized();
return null; return null;
} }
return res; return res;
} catch (err) { } catch (err) {
console.error("Fetch error:", err); console.error('Fetch error:', err);
if (typeof onUnauthorized === 'function') onUnauthorized(); if (typeof onUnauthorized === 'function') onUnauthorized();
return null; return null;
} }
}; };
// ============= User Profile Fetch ============= // ============= Fetch user profile =============
const fetchUserProfile = async () => { const fetchUserProfile = async () => {
const res = await authFetch(`${apiUrl}/user-profile`); const res = await authFetch(`${apiUrl}/user-profile`);
if (!res) return; if (!res) return;
@ -103,32 +109,93 @@ function Dashboard() {
setUserState(profileData.state); setUserState(profileData.state);
setAreaTitle(profileData.area.trim() || ''); setAreaTitle(profileData.area.trim() || '');
setUserZipcode(profileData.zipcode); setUserZipcode(profileData.zipcode);
// Store entire userProfile if needed
setUserProfile(profileData);
} else { } else {
console.error('Failed to fetch user profile'); console.error('Failed to fetch user profile');
} }
}; };
// ============= Lifecycle: Load Profile, Setup ============= // We'll store the userProfile for reference
const [userProfile, setUserProfile] = useState(null);
// ============= Lifecycle: Load Profile =============
useEffect(() => { useEffect(() => {
fetchUserProfile(); fetchUserProfile();
}, [apiUrl]); // load once }, [apiUrl]); // load once
// ============= jobZone & fit Setup ============= // ============= jobZone & Fit Setup =============
const jobZoneLabels = { const jobZoneLabels = {
'1': 'Little or No Preparation', '1': 'Little or No Preparation',
'2': 'Some Preparation Needed', '2': 'Some Preparation Needed',
'3': 'Medium Preparation Needed', '3': 'Medium Preparation Needed',
'4': 'Considerable Preparation Needed', '4': 'Considerable Preparation Needed',
'5': 'Extensive Preparation Needed' '5': 'Extensive Preparation Needed',
}; };
const fitLabels = { const fitLabels = {
'Best': 'Best - Very Strong Match', Best: 'Best - Very Strong Match',
'Great': 'Great - Strong Match', Great: 'Great - Strong Match',
'Good': 'Good - Less Strong Match' Good: 'Good - Less Strong Match',
}; };
// Fetch job zones for each career suggestion // ============= "Mimic" InterestInventory submission if user has 60 answers =============
const mimicInterestInventorySubmission = async (answers) => {
try {
setLoading(true);
setProgress(0);
const response = await authFetch(`${apiUrl}/onet/submit_answers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers }),
});
if (!response || !response.ok) {
throw new Error('Failed to submit stored answers');
}
const data = await response.json();
const { careers, riaSecScores } = data;
// This sets location.state, so the next effect sees it as if we came from InterestInventory
navigate('/dashboard', {
state: { careerSuggestions: careers, riaSecScores },
});
} catch (err) {
console.error('Error mimicking submission:', err);
alert('We could not load your saved answers. Please retake the Interest Inventory.');
navigate('/interest-inventory');
} finally {
setLoading(false);
}
};
// ============= On Page Load: get careerSuggestions from location.state, or mimic =============
useEffect(() => {
// If we have location.state from InterestInventory, proceed as normal
if (location.state) {
let descriptions = [];
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
descriptions = (scores || []).map((score) => score.description || 'No description available.');
setCareerSuggestions(suggestions || []);
setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions);
} else {
// We came here directly; wait for userProfile, then check answers
if (!userProfile) return; // wait until userProfile is loaded
const storedAnswers = userProfile.interest_inventory_answers;
if (storedAnswers && storedAnswers.length === 60) {
// Mimic the submission so we get suggestions
mimicInterestInventorySubmission(storedAnswers);
} else {
alert(
'We need your Interest Inventory answers to generate career suggestions. Redirecting...'
);
navigate('/interest-inventory');
}
}
}, [location.state, navigate, userProfile]);
// ============= jobZone fetch =============
useEffect(() => { useEffect(() => {
const fetchJobZones = async () => { const fetchJobZones = async () => {
if (careerSuggestions.length === 0) return; if (careerSuggestions.length === 0) return;
@ -148,7 +215,7 @@ function Dashboard() {
fetchJobZones(); fetchJobZones();
}, [careerSuggestions, apiUrl]); }, [careerSuggestions, apiUrl]);
// Filter careers by job zone, fit // ============= Filter by job zone, fit =============
const filteredCareers = useMemo(() => { const filteredCareers = useMemo(() => {
return careersWithJobZone.filter((career) => { return careersWithJobZone.filter((career) => {
const jobZoneMatches = selectedJobZone const jobZoneMatches = selectedJobZone
@ -163,7 +230,7 @@ function Dashboard() {
}); });
}, [careersWithJobZone, selectedJobZone, selectedFit]); }, [careersWithJobZone, selectedJobZone, selectedFit]);
// Merge updated data into chatbot context // ============= Merge data into chatbot context =============
const updateChatbotContext = (updatedData) => { const updateChatbotContext = (updatedData) => {
setChatbotContext((prevContext) => { setChatbotContext((prevContext) => {
const mergedContext = { const mergedContext = {
@ -179,56 +246,6 @@ function Dashboard() {
}); });
}; };
// Our final array for CareerSuggestions
const memoizedCareerSuggestions = useMemo(() => filteredCareers, [filteredCareers]);
// ============= Popout Panel Setup =============
const memoizedPopoutPanel = useMemo(() => {
return (
<PopoutPanel
isVisible={!!selectedCareer}
data={careerDetails}
schools={schools}
salaryData={salaryData}
economicProjections={economicProjections}
tuitionData={tuitionData}
closePanel={() => setSelectedCareer(null)}
loading={loading}
error={error}
userState={userState}
results={results}
updateChatbotContext={updateChatbotContext}
/>
);
}, [
selectedCareer,
careerDetails,
schools,
salaryData,
economicProjections,
tuitionData,
loading,
error,
userState,
results
]);
// ============= On Page Load: get careerSuggestions from location.state, etc. =============
useEffect(() => {
let descriptions = [];
if (location.state) {
const { careerSuggestions: suggestions, riaSecScores: scores } = location.state || {};
descriptions = (scores || []).map((score) => score.description || "No description available.");
setCareerSuggestions(suggestions || []);
setRiaSecScores(scores || []);
setRiaSecDescriptions(descriptions);
} else {
console.warn('No data found, redirecting to Interest Inventory');
navigate('/interest-inventory');
}
}, [location.state, navigate]);
// Once userState, areaTitle, userZipcode, etc. are set, update chatbot
useEffect(() => { useEffect(() => {
if ( if (
careerSuggestions.length > 0 && careerSuggestions.length > 0 &&
@ -240,15 +257,15 @@ function Dashboard() {
const newChatbotContext = { const newChatbotContext = {
careerSuggestions: [...careersWithJobZone], careerSuggestions: [...careersWithJobZone],
riaSecScores: [...riaSecScores], riaSecScores: [...riaSecScores],
userState: userState || "", userState: userState || '',
areaTitle: areaTitle || "", areaTitle: areaTitle || '',
userZipcode: userZipcode || "", userZipcode: userZipcode || '',
}; };
setChatbotContext(newChatbotContext); setChatbotContext(newChatbotContext);
} }
}, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]); }, [careerSuggestions, riaSecScores, userState, areaTitle, userZipcode, careersWithJobZone]);
// ============= handleCareerClick (for tile clicks) ============= // ============= handleCareerClick =============
const handleCareerClick = useCallback( const handleCareerClick = useCallback(
async (career) => { async (career) => {
console.log('[handleCareerClick] career =>', career); console.log('[handleCareerClick] career =>', career);
@ -284,7 +301,9 @@ function Dashboard() {
// Salary // Salary
let salaryResponse; let salaryResponse;
try { try {
salaryResponse = await axios.get(`${apiUrl}/salary`, { params: { socCode: socCode.split('.')[0], area: areaTitle } }); salaryResponse = await axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) { } catch (error) {
salaryResponse = { data: {} }; salaryResponse = { data: {} };
} }
@ -300,73 +319,77 @@ function Dashboard() {
// Tuition // Tuition
let tuitionResponse; let tuitionResponse;
try { try {
tuitionResponse = await axios.get(`${apiUrl}/tuition`, { params: { cipCode: cleanedCipCode, state: userState } }); tuitionResponse = await axios.get(`${apiUrl}/tuition`, {
params: { cipCode: cleanedCipCode, state: userState },
});
} catch (error) { } catch (error) {
tuitionResponse = { data: {} }; tuitionResponse = { data: {} };
} }
// Fetch schools // Fetch schools
const filteredSchools = await fetchSchools(cleanedCipCode, userState); const filteredSchools = await fetchSchools(cleanedCipCode, userState);
const schoolsWithDistance = await Promise.all(filteredSchools.map(async (school) => { const schoolsWithDistance = await Promise.all(
const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`; filteredSchools.map(async (school) => {
try { const schoolAddress = `${school.Address}, ${school.City}, ${school.State} ${school.ZIP}`;
const response = await axios.post(`${apiUrl}/maps/distance`, { try {
userZipcode, const response = await axios.post(`${apiUrl}/maps/distance`, {
destinations: schoolAddress, userZipcode,
}); destinations: schoolAddress,
const { distance, duration } = response.data; });
return { ...school, distance, duration }; const { distance, duration } = response.data;
} catch (error) { return { ...school, distance, duration };
return { ...school, distance: 'N/A', duration: 'N/A' }; } catch (error) {
} return { ...school, distance: 'N/A', duration: 'N/A' };
})); }
})
);
// Build salary array // Build salary array
const sData = salaryResponse.data || {}; const sData = salaryResponse.data || {};
const salaryDataPoints = sData && Object.keys(sData).length > 0 const salaryDataPoints =
? [ sData && Object.keys(sData).length > 0
{ ? [
percentile: "10th Percentile", {
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0, percentile: '10th Percentile',
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0 regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
}, nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
{ },
percentile: "25th Percentile", {
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0, percentile: '25th Percentile',
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0 regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
}, nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
{ },
percentile: "Median", {
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0, percentile: 'Median',
nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0 regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
}, nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0,
{ },
percentile: "75th Percentile", {
regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0, percentile: '75th Percentile',
nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0 regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0,
}, nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0,
{ },
percentile: "90th Percentile", {
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0, percentile: '90th Percentile',
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0 regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
}, nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0,
] },
: []; ]
: [];
// Build final details // Build final details
const updatedCareerDetails = { const updatedCareerDetails = {
...career, ...career,
jobDescription: description, jobDescription: description,
tasks: tasks, tasks: tasks,
economicProjections: (economicResponse.data || {}), economicProjections: economicResponse.data || {},
salaryData: salaryDataPoints, salaryData: salaryDataPoints,
schools: schoolsWithDistance, schools: schoolsWithDistance,
tuitionData: (tuitionResponse.data || []), tuitionData: tuitionResponse.data || [],
}; };
setCareerDetails(updatedCareerDetails); setCareerDetails(updatedCareerDetails);
updateChatbotContext({ careerDetails: updatedCareerDetails }); updateChatbotContext({ careerDetails: updatedCareerDetails });
} catch (error) { } catch (error) {
console.error('Error processing career click:', error.message); console.error('Error processing career click:', error.message);
setError('Failed to load data'); setError('Failed to load data');
@ -377,20 +400,20 @@ function Dashboard() {
[userState, apiUrl, areaTitle, userZipcode, updateChatbotContext] [userState, apiUrl, areaTitle, userZipcode, updateChatbotContext]
); );
// ============= Letting typed careers open PopoutPanel ============= // ============= Let typed careers open PopoutPanel =============
// Called if the user picks a career in "CareerSearch" => { title, soc_code, cip_code } const handleCareerFromSearch = useCallback(
const handleCareerFromSearch = useCallback((obj) => { (obj) => {
// Convert to shape used by handleCareerClick => { code, title, cipCode } const adapted = {
const adapted = { code: obj.soc_code,
code: obj.soc_code, title: obj.title,
title: obj.title, cipCode: obj.cip_code,
cipCode: obj.cip_code };
}; console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted); handleCareerClick(adapted);
handleCareerClick(adapted); },
}, [handleCareerClick]); [handleCareerClick]
);
// If the user typed a career and clicked confirm
useEffect(() => { useEffect(() => {
if (pendingCareerForModal) { if (pendingCareerForModal) {
console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal); console.log('[useEffect] pendingCareerForModal =>', pendingCareerForModal);
@ -433,6 +456,38 @@ function Dashboard() {
); );
}; };
// ============= Popout Panel Setup =============
const memoizedPopoutPanel = useMemo(() => {
return (
<PopoutPanel
isVisible={!!selectedCareer}
data={careerDetails}
schools={schools}
salaryData={salaryData}
economicProjections={economicProjections}
tuitionData={tuitionData}
closePanel={() => setSelectedCareer(null)}
loading={loading}
error={error}
userState={userState}
results={results}
updateChatbotContext={updateChatbotContext}
/>
);
}, [
selectedCareer,
careerDetails,
schools,
salaryData,
economicProjections,
tuitionData,
loading,
error,
userState,
results,
updateChatbotContext,
]);
return ( return (
<div className="dashboard"> <div className="dashboard">
{showSessionExpiredModal && ( {showSessionExpiredModal && (
@ -441,16 +496,19 @@ function Dashboard() {
<h3>Session Expired</h3> <h3>Session Expired</h3>
<p>Your session has expired or is invalid.</p> <p>Your session has expired or is invalid.</p>
<div className="modal-actions"> <div className="modal-actions">
<button className="confirm-btn" onClick={() => setShowSessionExpiredModal(false)}> <button
className="confirm-btn"
onClick={() => setShowSessionExpiredModal(false)}
>
Stay Signed In Stay Signed In
</button> </button>
<button <button
className="confirm-btn" className="confirm-btn"
onClick={() => { onClick={() => {
localStorage.removeItem("token"); localStorage.removeItem('token');
localStorage.removeItem("UserId"); localStorage.removeItem('UserId');
setShowSessionExpiredModal(false); setShowSessionExpiredModal(false);
navigate("/signin"); navigate('/signin');
}} }}
> >
Sign In Again Sign In Again
@ -464,7 +522,6 @@ function Dashboard() {
<div className="dashboard-content"> <div className="dashboard-content">
{/* ====== 1) The new CareerSearch bar ====== */} {/* ====== 1) The new CareerSearch bar ====== */}
{/* Existing filters + suggestions */} {/* Existing filters + suggestions */}
<div className="career-suggestions-container"> <div className="career-suggestions-container">
@ -475,7 +532,7 @@ function Dashboard() {
alignItems: 'center', alignItems: 'center',
marginBottom: '15px', marginBottom: '15px',
justifyContent: 'center', justifyContent: 'center',
gap: '15px' gap: '15px',
}} }}
> >
<label> <label>
@ -487,7 +544,9 @@ function Dashboard() {
> >
<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>
</label> </label>
@ -501,7 +560,9 @@ function Dashboard() {
> >
<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>
</label> </label>
@ -509,14 +570,15 @@ function Dashboard() {
<CareerSearch <CareerSearch
onCareerSelected={(careerObj) => { onCareerSelected={(careerObj) => {
console.log('[Dashboard] onCareerSelected =>', careerObj); console.log('[Dashboard] onCareerSelected =>', careerObj);
// Set the "pendingCareerForModal" so our useEffect fires below // Set the "pendingCareerForModal" so our useEffect fires
setPendingCareerForModal(careerObj); setPendingCareerForModal(careerObj);
}} }}
/> />
</div> </div>
</div> </div>
<CareerSuggestions <CareerSuggestions
careerSuggestions={memoizedCareerSuggestions} careerSuggestions={filteredCareers}
onCareerClick={handleCareerClick} onCareerClick={handleCareerClick}
setLoading={setLoading} setLoading={setLoading}
setProgress={setProgress} setProgress={setProgress}
@ -568,14 +630,38 @@ function Dashboard() {
borderTop: '1px solid #ccc', borderTop: '1px solid #ccc',
fontSize: '12px', fontSize: '12px',
color: '#666', color: '#666',
textAlign: 'center' textAlign: 'center',
}} }}
> >
<p> <p>
Career results and RIASEC scores are provided by Career results and RIASEC scores are provided by
<a href="https://www.onetcenter.org" target="_blank" rel="noopener noreferrer"> O*Net</a>, in conjunction with the <a
<a href="https://www.bls.gov" target="_blank" rel="noopener noreferrer"> Bureau of Labor Statistics</a>, and the href="https://www.onetcenter.org"
<a href="https://nces.ed.gov" target="_blank" rel="noopener noreferrer"> National Center for Education Statistics (NCES)</a>. target="_blank"
rel="noopener noreferrer"
>
{' '}
O*Net
</a>
, in conjunction with the
<a
href="https://www.bls.gov"
target="_blank"
rel="noopener noreferrer"
>
{' '}
Bureau of Labor Statistics
</a>
, and the
<a
href="https://nces.ed.gov"
target="_blank"
rel="noopener noreferrer"
>
{' '}
National Center for Education Statistics (NCES)
</a>
.
</p> </p>
</div> </div>
</div> </div>

Binary file not shown.